标签化档案管理系统

文件建立日期: 2020/02/28

最后修订日期: None

相关软件信息:

Win 10 Python 3.8.2 PySimpleGUI 4.16.0 sqlite3 2.6.0

说明: 本文请随意引用或更改, 只须标示出处及作者, 作者不保证内容絶对正确无误, 如造成任何后果, 请自行负责.

标题: 标签化档案管理系统

一直以来, 在计算机中的档案大量的增加, 每当想找到某一个最近很少用到的档案, 总很难想到放在哪一个目录下, 也没找到合适自己使用的软件, 所以自己写了一个, 重点是给自己用.

设计要求

  1. 标签的管理
    • 新增标签 (同名的标签在不同分类下视为不同)
    • 删除标签 (已有档案归类在该标签下, 不得删除)
    • 标签改名
    • 标签排序 (同层级之间的排序)
    • 标签搜索
    • 为方便处理, 以字典转字符串储存与读取
  2. 档案管理
    • 新增选择条件标签, 最多五笔
    • 删除选择条件标签 (先删除最后加入的标签)
    • 档案删除, 改名, 复制, 移动等 (目前不提供)
    • 双击档案开启
    • 显示档案分类标签
    • 档案全部放在D:\FILE下, 按附属檔名分放在附属檔名子目录下
    • 多选档案加入系统后, 再移动档案
  3. 数据库管理
    • 两个表 tag_tablefile_table
    • tag_table只有一个column: key TEXT, 字符串化的标签树字典
    • file_table有六个columns:
      • filename
      • key1, key2, key3, key4, key5
      • key1 ~ key5 代表分类标签代码
  4. 更新要求
    • 标签更名, 必须更新选择条件标签, 以及档案分类标签
    • 选择条件标签更动, 必须更新合乎条件的档案列表

输出画面

标签化档案管理系统

代码及说明

  1. 使用的库
import os
import shutil
from pathlib import Path
import PySimpleGUI as sg
import sqlite3
import ctypes
  1. 档案处理类
    • 建立主目录D:\FILE, 以及各个子目录D:\FILE\附属檔名 (没有则为No_Extension)
    • 档案移动
class FILE():
    # 目录建立及档案移动
    def __init__(self):
        self.file_directory = Path('D:/FILE')
        self.no_extension = 'No_Extension'
        if not self.file_directory.is_dir():
            self.file_directory.mkdir()
    # 按附属文件名建立子目录
    def Create_Directory_By_File_Extension(self, path_object):
        suffix = path_object.suffix
        relative_path = self.no_extension if suffix=='' else suffix[1:].upper()
        directory = self.file_directory.joinpath(relative_path)
        if not directory.is_dir():
            directory.mkdir()
        return directory
    # 文件名转换成絶对路径
    def Get_Path(self, text):
        path = Path(text)
        suffix = path.suffix
        sub_dir = self.no_extension if suffix == '' else suffix[1:].upper()
        result = self.file_directory.joinpath(sub_dir, path.name)
        return result
    # 档案移动
    def Move_File(self, file_object_from, file_object_to):
        if not file_object_from.is_file() or file_object_to.is_file():
            return False
        shutil.move(file_object_from, file_object_to)
        return True
  1. 提供所有GUI接口的处理
    • 窗口的生成
    • 控件的动作及更新
    • 事件的处理
class GUI():

    def __init__(self):
        self.title       = '档案管理系统'
        self.font        = ('微软雅黑', 12)
        self.button_size = ( 8, 1)
        self.select      = ['', '', '', '', '']
        self.index       = 0
        # 按键的键值及显示的文字
        button00         = [['NEW TAG',  '新建标签'],
                            ['ROOT TAG', '选主标签'],
                            ['DELETE',   '删除标签']]
        button01         = [['RENAME',   '标签改名'],
                            ['SEARCH',   '搜索标签'],
                            ['NEXT ONE', '找下一个']]
        button02         = [['SORTING' , '标签排序']]
        button10         = [['INSERT',   '档案移入'],
                            ['REMOVE',   '标签移除']]
        # 选择标签及档案分类标签
        self.text0       = ['S1', 'S2', 'S3', 'S4', 'S5']
        self.text1       = ['T1', 'T2', 'T3', 'T4', 'T5']
        # 标签树及档案树的事件绑定
        self.binds       = [['TREE0', '<Button-1>',        '_CLICK'],
                            ['TREE0', '<Double-Button-1>', '_DOUBLE'],
                            ['TREE1', '<Button-1>',        '_CLICK'],
                            ['TREE1', '<Double-Button-1>', '_DOUBLE']]
        # 左右两个框及其内容
        layout0          = self.Frame([self.Buttons(button00),
                           self.Buttons(button01), self.Buttons(button02),
                           [T0]])
        layout1          = self.Frame([self.Buttons(button10),
                           self.Texts(self.text0, p=(7, 7)), [T1],
                           self.Texts(self.text1, p=(7, 7))])
        ctypes.windll.user32.SetProcessDPIAware()
        # 窗口生成
        self.window      = sg.Window(self.title, layout=[[layout0, layout1]],
                                margins=(2, 2), finalize=True)
        # 隐藏树的标题栏
        self.Hide_Header()
        # 加载数据库已建立的标签, 并更新标签树
        T0.Load_Tree(S.Load_Tag_Table())
        # 事件绑定
        self.Binds()
        # INSERT按钮停用
        self.Disable_Insert()

    def _clear_frame1(self):
        # 清除选项, 并更新档案树
        self.index = 0
        self.select = ['', '', '', '', '']
        self._frame1_update()

    def _frame1_update(self):
        # 更新框架1中的标签选项, 档案分类标签, 以及档案树, 按钮'INSERT'
        self._text0_update()
        self._tree1_update()
        self._text1_update()
        self.Disable_Insert()

    def _is_select_not_ok(self):
        # 标签树中的树根不可加入选择标签中, 重复也不接受
        return True if T0.Where() in ['']+self.select else False

    def _text0_append(self):
        # 新增标签到五个选择标签中
        if self.index == 5:
            self.select =  self.select[1:5]+['']
        self.index = self.index + 1 if self.index < 5 else 5
        self.select[self.index-1]=T0.Where()
        return

    def _text0_clear(self):
        # 清除选择标签
        self.index = 0
        self.select = ['', '', '', '', '']

    def _text0_update(self):
        # 更新选择标签的显示内容
        for i in range(5):
            self.window[self.text0[i]].Update(
                value='' if self.select[i]=='' else
                    T0.treedata.tree_dict[self.select[i]].text)

    def _text1_update(self):
        # 更新档案分类标签的显示内容
        value = T1.treedata.tree_dict[T1.Where()].values
        if value == []: value = ['', '', '', '', '']
        for i in range(5):
            text = '' if value[i]=='' else T0.treedata.tree_dict[value[i]].text
            self.window[self.text1[i]].Update(value=text)

    def _tree1_update(self):
        # 按选择标签条件, 更新档案树的显示内容
        T1.Tree_Update(S.Load_File_Table())

    def Binds(self):
        # 绑定事件
        for bind in self.binds: self.window[bind[0]].bind(bind[1], bind[2])

    def Disable_Insert(self):
        # 停用或启用INSERT按钮
        disabled = True if self.index == 0 else False
        self.window['INSERT'].Update(disabled=disabled)

    def Buttons(self, buttons):
    # 多按钮生成, INSERT按钮为多档案选取
        result = []
        for i, button in enumerate(buttons):
            pad = (0, (2, 0)) if i == 0 else ((2, 0), (2, 0))
            if button[0] == 'INSERT':
                result.append(sg.FilesBrowse(button[1], target=button[0],
                size=self.button_size, key=button[0], font=self.font, pad=pad,
                file_types=(('All files', '*.*'),), enable_events=True))
            else:
                result.append(sg.Button(button[1], auto_size_button=False,
                font=self.font, enable_events=True, size=self.button_size,
                key=button[0], pad=pad))
        return result

    def Frame(self, layout):
        # 框架生成
        return sg.Frame('', layout=layout, pad=(0, 0))

    def Hide_Header(self):
        # 隐藏标签树及档案树的标题栏
        T0.Widget.configure(show='tree')
        T1.Widget.configure(show='tree')

    def Insert(self):
        # 选择标签的加入, 并更新档案树
        if self._is_select_not_ok(): return
        self._text0_append()
        self._frame1_update()

    def Pop(self, text):
        # 通知文字弹框
        sg.PopupOK(text, font=self.font, no_titlebar=True)

    def Popup(self, text):
        # 输入文字弹框
        text = sg.popup_get_text(message=text, font=self.font, size=(40,1),
            default_text='', no_titlebar=True, keep_on_top=True)
        return None if text == None or text.strip()=='' else text.strip()

    def Remove(self):
        # 移除最后一项的选择标签
        if self.index == 0:
            return
        self.select[self.index-1] = ''
        self.index -= 1
        self._frame1_update()
        self.Disable_Insert()

    def Texts(self, texts, p, size=(20, 1)):
        # 文字框生成
        result = []
        for i, text in enumerate(texts):
            pad = (0, p) if i == 0 else ((9, 0), p)
            result.append(sg.Text('', size=size, font=self.font,
            justification='center', auto_size_text=False, key=text,
            text_color='white', background_color='green', pad=pad))
        return result
  1. 数据库处理
    • 连接数据库 conn = sqlite3.connect(数据库文件名), c = conn.cursor()
    • 建立表格 CREATE TABLE IF NOT EXISTS 表格名 (字段1 格式, 字段2 格式, …, 字段N 格式)
    • 插入数据 INSERT INTO表格名 (字段1, 字段2, …, 字段N) VALUES (值1, 值2, .., 值N)
    • 删除数据 DELETE FROM 表格名 WHERE 条件
    • 查询数据 SELECT * FROM 表格名
    • 查询结果 c.fetchone(); c.fetchall()
    • 条件查询 SELECT 字段 FROM 表格名 WHERE 条件
    • 命令送出及更新
      • c.execute(命令, 相关参数)
      • conn.commit()
class SQL():

    def __init__(self):
        # 建立与数据库的连接, 并设置表格及其字段
        self.database   = 'D:/FILE/file.db'
        self.file_table = 'file_table'
        self.file_cols  = ('filename', 'key1', 'key2', 'key3', 'key4', 'key5')
        self.tag_table  = 'tag_table'
        self.tag_cols   = ('key',)
        self.conn       = sqlite3.connect(self.database)
        self.c          = self.conn.cursor()
        self.Create_File_Table()
        self.Create_Tag_Table()

    def Commit(self, command):
        # 发出命令并更新
        self.c.execute(command)
        self.conn.commit()

    def Create_File_Table(self):
        # 建立档案表格
        command = ('CREATE TABLE IF NOT EXISTS file_table '
                   '(filename TEXT, key1 TEXT, key2 TEXT, key3 TEXT, '
                   'key4 TEXT, key5 TEXT)')
        self.Commit(command)

    def Create_Tag_Table(self):
        # 建立标签表格
        command = 'CREATE TABLE IF NOT EXISTS tag_table (key TEXT)'
        self.Commit(command)

    def Insert_File_Table(self, values):
        # 新增一笔档案记录
        command = ('INSERT INTO file_table ' +
                   '(filename, key1, key2, key3, key4, key5) VALUES ' +
                   repr(tuple(values)))
        self.Commit(command)

    def Load_File_Table(self):
        # 搜索符合选择标签的档案记录, 多条件交互查询
        if G.index == 0: return []
        t1 = "'), ('".join(G.select[:G.index])
        command = """
            with list(col) as (VALUES ('""" +t1+"""')), cte as ( select rowid,
            ',' || key1 || ',' || key2 || ',' || key3 || ',' || key4 || ',' ||
            key5 || ',' col from file_table ) select * from file_table where
            rowid in ( select c.rowid from cte c inner join list l on c.col
            like '%,' || l.col || ',%' group by c.rowid having count(*) =
            (select count(*) from list) )"""
        self.Commit(command)
        return self.c.fetchall()

    def Load_Tag_Table(self):
        # 读取标签表格, 始终只有一笔, 包含所有的标签, 文字字典
        command = ' '.join(('SELECT * from', self.tag_table))
        self.Commit(command)
        data = self.c.fetchone()
        return None if data == None else eval(data[0])

    def Records_In_File_Table(self, key):
        # 检查标签是否有档案使用该标签, 传回有使用该标签的档案记录
        command = ("SELECT * FROM file_table WHERE '" + key +
                   "' IN (key1, key2, key3, key4, key5)")
        self.Commit(command)
        return self.c.fetchall()

    def Save_To_Tag_Table(self, dictionary):
        # 删除原记录, 再加入新的标签记录
        self.Commit(' '.join(('DELETE FROM', self.tag_table, 'WHERE ROWID=1')))
        command = ' '.join(('INSERT INTO', self.tag_table, '(',
            self.tag_cols[0], ')', 'VALUES', '(', repr(str(dictionary)), ')'))
        self.Commit(command)
  1. 标签树 TREE0
    • 新增, 删除, 搜索, 排序, 更新
    • 树的数据结构
    • treedata.tree_dict {'key':Node}
      • Node.parent 父Node的key
      • Node.children 子Node的列表
      • Node.text Node的文字
      • Node.values Node的columns值
      • Node.icon Node的图示
class Tree0(sg.Tree):

    def __init__(self, key):
        # 生成标签数据结构treedata及标签树tree
        self.font = ('微软雅黑', 12)
        self.key = key
        self.search = []
        self.index = 0
        self.text = None
        self.treedata = sg.TreeData()
        super().__init__(data=self.treedata, col0_width=26, font=self.font,
            num_rows=20, row_height=30, background_color='white',
            show_expanded=False, justification='left', key=self.key,
            visible_column_map=[False,], auto_size_columns=False,
            headings=['Nothing',], enable_events=True,
            select_mode=sg.TABLE_SELECT_MODE_BROWSE, pad=(0, 0))

    def _delete_tag(self, key):
        # 删除标签以及以下所有的子标签
        # 从父标签中的子标签列表移除, 删除标签数据结构中该标签, 删除该标签对象
        # 再删除其所有的子标签
        node = self.treedata.tree_dict[key]
        self.treedata.tree_dict[node.parent].children.remove(node)
        node_list = [node]
        while node_list != []:
            temp = []
            for item in node_list:
                temp += item.children
                del self.treedata.tree_dict[item.key]
                del item
            node_list = temp

    def _is_root(self):
        # 选择的是不是标签的树根
        return True if self.Where()=='' else False

    def _records(self, key):
        # 检查有多少档案用到该标签
        tag_list = [key] + self.All_Tags(key)
        records = 0
        for tag in tag_list:
            records += len(S.Records_In_File_Table(tag))
        return records

    def All_Tags(self, parent='', new=True):
        # 取得该标签所有的子标签
        if new: self.search = []
        children = self.treedata.tree_dict[parent].children
        for child in children:
            self.search.append(child.key)
            self.All_Tags(parent=child.key, new=False)
        return self.search

    def Delete(self):
        # 删除标签, 如果有档案使用该标签或其子标签, 则不准删除
        key = self.Where()
        if self._is_root(): return
        if self._records(key)!= 0:
            G.Pop('有档案带有该标签或子标签, 不能删除, 请先修改档案标签 !')
            return
        previous_key = self.Previous_Key(key)
        self._delete_tag(key)
        self.Tree_Update()
        S.Save_To_Tag_Table(self.Tree_To_Dictionary())
        self.Expand(previous_key)
        self.Select(previous_key)
        G._clear_frame1()

    def Expand(self, key):
        # 展开标签及其子标签, 使他们能被看到, 而不是收折起来, 看不到.
        children = self.treedata.tree_dict[key].children
        for child in children:
            self.Select(child.key)

    def _get_key(self):
        # 取得一个唯一的键值
        i = 1
        while True:
            if str(i) in self.treedata.tree_dict:
                i += 1
            else:
                return str(i)

    def Insert(self):
        # 新增标签, 为方便加入, 选择其父标签, 而不选择该新标签
        text = G.Popup('新增标签, 同标签也会被视为不同')
        if text == None: return
        parent = self.Where()
        key = self._get_key()
        self.treedata.insert(parent, key, text, [])
        self.Tree_Update()
        self.Select(key)
        self.Select(parent)
        S.Save_To_Tag_Table(self.Tree_To_Dictionary())

    def Key_To_ID(self, key):
        # 转换PySimpleGUI的Key为Tkinter的iid
        return [k for k in self.IdToKey if (self.IdToKey[k] == key)][0]

    def Load_Tree(self, dictionary):
        # 由来自数据库标签表的字典, 建立标签数据结构, 供标签树更新及显示
        if dictionary == None:
            return
        children = dictionary[''][1]
        while children != []:
            temp = []
            for child in children:
                node = dictionary[child]
                self.treedata.insert(node[0], child, node[2], node[3])
                temp += node[1]
            children = temp
        self.Tree_Update()
        for child in self.treedata.tree_dict[''].children:
            self.Expand(child.key)
        self.Select('')

    def Previous_Key(self, key):
        # 取得该标签的显示位置的上一个标签
        self.All_Tags('')
        index = self.search.index(key)
        result = '' if index==0 else self.search[index-1]
        return result

    def Rename(self):
        # 更改标签的文字内容
        key = self.Where()
        if key == '': return
        text = G.Popup('新标签文字, 同标签也会被视为不同')
        if text == None: return
        self.treedata.tree_dict[key].text = text
        self.Update(key=key, text=text)
        S.Save_To_Tag_Table(self.Tree_To_Dictionary())

    def _search_text(self, text=None, next=False):
        # 搜索标签数据结构中, 带有该字符串部份的标签
        # self.search中保留全部的标签列表, self.index指到下一个未搜索的标签
        # 供找下一个标签使用, 如果是新的搜索, 会指到从头开始.
        if len(self.treedata.tree_dict) < 2: return
        if not next:
            self.All_Tags()
            self.text = text
            self.index = 0
        else:
            if self.text == None:
                return
            text = self.text
        length = len(self.search)
        for i in range(self.index, length):
            key = self.search[i]
            if text.upper() in self.treedata.tree_dict[key].text.upper():
                self.Select(key)
                self.index = i + 1 if i + 1 < length else 0
                return
        G.Pop('找不到相关的标签')
        self.index = 0

    def Search(self):
        # 从头开始搜索标签数据结构中的文字
        text = G.Popup('搜索标签文字')
        if text != None: self._search_text(text)

    def Search_Next(self):
        # 从上一次搜索位置后继搜索
        self._search_text(next=True)

    def Select(self, key=''):
        # 移到某个标签
        iid = self.Key_To_ID(key)
        self.Widget.see(iid)
        self.Widget.selection_set(iid)

    def Sorting(self):
        # 将每个标签的子标签按其文字大小排序
        for key, node in self.treedata.tree_dict.items():
            children = node.children
            node.children = sorted(children, key=lambda x: x.text)
        self.Tree_Update()
        S.Save_To_Tag_Table(self.Tree_To_Dictionary())

    def Tree_To_Dictionary(self):
        # 将标签数据结构转换成需要的字典, 供存数据库使用
        dictionary = {}
        for key, node in self.treedata.tree_dict.items():
            children = [n.key for n in node.children]
            dictionary[key]=[node.parent, children, node.text, node.values]
        return dictionary

    def Tree_Update(self):
        # 更新标签树的标签数据结构
        self.Update(values=self.treedata)

    def Where(self):
        # 查询标签树的选择位置
        item = self.Widget.selection()
        return '' if len(item) == 0 else self.IdToKey[item[0]]
  1. 档案树 TREE1
    内容类似标签树, 只是方法不太一样.
class Tree1(sg.Tree):

    def __init__(self, key):
        # 档案树的生成
        self.font = ('微软雅黑', 12)
        self.key = key
        self.treedata = sg.TreeData()
        super().__init__(data=self.treedata, col0_width=112, font=self.font,
            num_rows=20, row_height=30, background_color='white',
            show_expanded=False, justification='left', key=self.key,
            visible_column_map=[False,], auto_size_columns=False,
            headings=['Nothing',], enable_events=True,
            select_mode=sg.TABLE_SELECT_MODE_BROWSE, pad=(0, 0))

    def _get_key(self):
    # 生成独一无二的键值
        i = 1
        while True:
            if str(i) in self.treedata.tree_dict:
                i += 1
            else:
                return str(i)

    def Execute(self):
        # 双击执行开启该档案
        filename = F.Get_Path(self.treedata.tree_dict[self.Where()].text)
        if Path(filename).is_file:
            os.startfile(filename)
        else:
            G.Pop('File not exist !!')

    def Insert(self):
        # 插入多选档案, 有建立子目录, 移动档案, 
        # 并更新数据库, 更新档案数据结构, 以及档案树显示
        if G.index == 0: return
        paths = values['INSERT'].split(';')
        for file in paths:
            path = Path(file)
            directory= F.Create_Directory_By_File_Extension(path)
            target = directory.joinpath(path.name)
            if F.Move_File(path, target):
                S.Insert_File_Table([target.name]+G.select)
                key = self._get_key()
                self.treedata.insert('', key, target.name, G.select)
        G._frame1_update()

    def Key_To_ID(self, key):
        # 转换PySimpleGUI的KEY为Tkinter的iid
        return [k for k in self.IdToKey if (self.IdToKey[k] == key)][0]

    def Select(self, key=''):
        # 选择档案树的某一行
        iid = self.Key_To_ID(key)
        self.Widget.see(iid)
        self.Widget.selection_set(iid)

    def Tree_Update(self, files):
        # 删除档案树, 更新档案树, 再更新档案树的显示
        self.treedata = sg.TreeData()
        for file in files:
            key = self._get_key()
            self.treedata.insert('', key, file[0], file[1:])
        self.Update(values=self.treedata)
        self.Select('')

    def Where(self):
        # 查询目前档案树选择所在
        item = self.Widget.selection()
        return '' if len(item) == 0 else self.IdToKey[item[0]]
  1. 类的实例化
F  = FILE()
S  = SQL()
T0 = Tree0('TREE0')
T1 = Tree1('TREE1')
G  = GUI()
  1. 事件处理
# 定义每个事件的处理方法
function = {'NEW TAG':T0.Insert,       'ROOT TAG':T0.Select, 'DELETE':T0.Delete,
            'RENAME' :T0.Rename,       'SEARCH'  :T0.Search, 'INSERT':T1.Insert,
            'REMOVE' :G.Remove,        'NEXT ONE':T0.Search_Next,
            'TREE0_DOUBLE':G.Insert,   'TREE1_CLICK':G._text1_update,
            'TREE1_DOUBLE':T1.Execute, 'SORTING':T0.Sorting}

while True:
    # 读取事件
    event, values = G.window.read()
    # 窗口闗闭
    if event == None:
        break
    # 各个按钮事件
    elif event in function:
        function[event]()

# 程序结束, 闗闭数据库连接, 闗闭窗口, 删除相闗的主变量
S.conn.close()
G.window.close()
del F, S, T0, T1, G
本作品采用《CC 协议》,转载必须注明作者和本文链接
Jason Yang
讨论数量: 1

我想问一下,pysimplegui对于其它的gui库有什么优势?

9个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!