檔案建立日期: 2020/02/28
最後修訂日期: None
相關軟體資訊:
說明: 本文請隨意引用或更改, 只須標示出處及作者, 作者不保證內容絶對正確無誤, 如造成任何後果, 請自行負責.
標題: 標籤化檔案管理系統
一直以來, 在計算機中的檔案大量的增加, 每當想找到某一個最近很少用到的檔案, 總很難想到放在哪一個目錄下, 也沒找到合適自己使用的軟體, 所以自己寫了一個, 重點是給自己用.
設計要求
- 標籤的管理
- 新增標籤 (同名的標籤在不同分類下視為不同)
- 刪除標籤 (已有檔案歸類在該標籤下, 不得刪除)
- 標籤改名
- 標籤排序 (同層級之間的排序)
- 標籤搜尋
- 為方便處理, 以字典轉字串儲存與讀取
- 檔案管理
- 新增選擇條件標籤, 最多五筆
- 刪除選擇條件標籤 (先刪除最後加入的標籤)
- 檔案刪除, 改名, 複製, 移動等 (目前不提供)
- 雙擊檔案開啟
- 顯示檔案分類標籤
- 檔案全部放在
D:\FILE
下, 按附屬檔名分放在附屬檔名子目錄下 - 多選檔案加入系統後, 再移動檔案
- 資料庫管理
- 兩個表
tag_table
及file_table
tag_table
只有一個column:key TEXT
, 字串化的標籤樹字典file_table
有六個columns:filename
key1
,key2
,key3
,key4
,key5
- key1 ~ key5 代表分類標籤程式碼
- 兩個表
- 更新要求
- 標籤更名, 必須更新選擇條件標籤, 以及檔案分類標籤
- 選擇條件標籤更動, 必須更新合乎條件的檔案列表
輸出畫面
程式碼及說明
- 使用的庫
import os
import shutil
from pathlib import Path
import PySimpleGUI as sg
import sqlite3
import ctypes
- 檔案處理類
- 建立主目錄
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
- 提供所有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
- 資料庫處理
- 連線資料庫 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)
- 標籤樹 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]]
- 檔案樹 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]]
- 類的例項化
F = FILE()
S = SQL()
T0 = Tree0('TREE0')
T1 = Tree1('TREE1')
G = GUI()
- 事件處理
# 定義每個事件的處理方法
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 協議》,轉載必須註明作者和本文連結