標籤化檔案管理系統

Jason990420發表於2020-02-28

檔案建立日期: 2020/02/28

最後修訂日期: None

相關軟體資訊:

說明: 本文請隨意引用或更改, 只須標示出處及作者, 作者不保證內容絶對正確無誤, 如造成任何後果, 請自行負責.

標題: 標籤化檔案管理系統

一直以來, 在計算機中的檔案大量的增加, 每當想找到某一個最近很少用到的檔案, 總很難想到放在哪一個目錄下, 也沒找到合適自己使用的軟體, 所以自己寫了一個, 重點是給自己用.

設計要求

  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

相關文章