上一篇:Python 使用tkinter復刻Windows記事本UI和菜單功能(二)-CSDN博客
下一篇:敬請耐心等待,如發現BUG以及建議,請在評論區發表,謝謝!
本文章完成了記事本的新建、保存、另存、打開文件、狀態欄顯隱等部分基本功能,還設計還原了頁面設置UI。很抱歉現階段我沒有精力再去完善和優化這個項目了,不止是能力受限,還耽擱太多時間了。
運行結果
代碼實例
string = \
"""復刻Windows記事本BUG:1、快捷鍵:Ctrl+O 打開文件實現時發現光標處會插入換行'\\n'(現在我仍未知是否是為解釋器BUG)未實現:1、文件的新窗口無法實現(未使用線程)2、無法實現單擊菜單欄顯示菜單項后與鍵盤交互(非快捷鍵),因為Menu無法與bind捆綁事件及交互3、文件的頁面設置的具體功能交互還沒完成,只完成UI和交互框架4、文件的打印還沒實現(我不知道怎么連接外設)4、除了文件以外的菜單還沒實現
"""# 通配符
__all__ = ['main']import tkinter as tk
from tkinter import ttk
from tkinter import font
import tkinter.messagebox as tkmb
import tkinter.filedialog as tkfd# 全局變量
# 初始化
FONT_SIZE = 12 # 默認字體大小
# 永久保存變量
PAPER_VAR = 'A4'
PAPER_ORIENT = 1
LEFT_VAR = 20
RIGHT_VAR = 20
TOP_VAR = 25
BOTTOM_VAR = 25
HEADER_VAR = None
FOOTER_VAR = None# (打印)頁面設置UI
class PageSetup:orientVar = NoneleftVar = NonerightVar = NonetopVar = NonebottomVar = NoneheaderVar = NonefooterVar = None# (打印)頁面設置@classmethoddef pageSetup(cls):set = tk.Toplevel() # 頁面設置頂級窗口set.title('頁面設置') # 窗口標題set.geometry(f'622x418+{set.winfo_screenwidth()//4+60}+{set.winfo_screenheight()//8+52}')set.focus_set() # 設置窗口焦點set.resizable(0, 0) # 禁止窗口的放大set.grab_set() # 鎖定父窗口# 窗口布局# (打印)紙張選擇paperFrame = ttk.LabelFrame(set, text='紙張', padding=(191, 38))paperFrame.place(x=14, y=16)tk.Label(paperFrame).pack()# 大小size = tk.Label(set, text='大小(Z):')size.place(x=24, y=48)# 來源source = tk.Label(set, text='來源(S):', state='disable')source.place(x=24, y=92)# 紙張大小下拉菜單# 修改 OptionMenu 的樣式style = ttk.Style()style.configure("my.TMenubutton", background='#DCDCDC', width=35)# 紙張大小下拉菜單paperVar = tk.StringVar(value=PAPER_VAR)paperOption = [paperVar.get(), f'A3{" "*55}', 'A4', 'A5', 'B4 (JIS)', 'B5 (JIS)', 'Executive', 'Statement', 'Tabloid', '法律專用紙', '信紙']paperMenu = ttk.OptionMenu(set, paperVar, *paperOption, style="my.TMenubutton", command=cls.paperOption)paperMenu.place(x=110, y=46)# 默認選擇(打印紙張)cls.paperOption(paperVar.get())# 紙張來源下拉菜單# 修改 OptionMenu 的樣式style.configure("my2.TMenubutton", background='#C0C0C0', width=35)# 紙張大小下拉菜單cls.sourceVar = tk.StringVar()sourceOption = [None, f'選項1{" " * 55}', '選項2', '選項3']sourceMenu = ttk.OptionMenu(set, cls.sourceVar, *sourceOption, style="my2.TMenubutton")sourceMenu.config(state="disabled")sourceMenu.place(x=110, y=90)# (打印紙張)方向選擇orientFrame = ttk.LabelFrame(set, text='方向', padding=(50, 38))orientFrame.place(x=14, y=147)tk.Label(orientFrame).pack()cls.orientVar = tk.IntVar(value=PAPER_ORIENT)# (打印紙張)縱向lengthways = ttk.Radiobutton(set, text='縱向(O)', variable=cls.orientVar, value=1, command=cls.orientOption)lengthways.place(x=26, y=180)# (打印紙張)橫向crosswise = ttk.Radiobutton(set, text='橫向(A)', variable=cls.orientVar, value=2, command=cls.orientOption)crosswise.place(x=26, y=220)# 默認(打印紙張)縱向cls.orientOption()# (打印紙張)頁邊距(毫米)marginFrame = ttk.LabelFrame(set, text='頁邊距(毫米)', padding=(130, 38))marginFrame.place(x=136, y=147)tk.Label(marginFrame).pack()# 文字標簽tk.Label(set, text='左(L):').place(x=148, y=180)tk.Label(set, text='右(R):').place(x=274, y=180)tk.Label(set, text='上(T):').place(x=148, y=220)tk.Label(set, text='下(B):').place(x=274, y=220)# 輸入框cls.leftVar = tk.StringVar(value=LEFT_VAR)cls.rightVar = tk.StringVar(value=RIGHT_VAR)cls.topVar = tk.StringVar(value=TOP_VAR)cls.bottomVar = tk.StringVar(value=BOTTOM_VAR)leftEntry = ttk.Entry(set, width=6, textvariable=cls.leftVar)leftEntry.place(x=200, y=180)rightEntry = ttk.Entry(set, width=6, textvariable=cls.rightVar)rightEntry.place(x=326, y=180)topEntry = ttk.Entry(set, width=6, textvariable=cls.topVar)topEntry.place(x=200, y=220)bottomEntry = ttk.Entry(set, width=6, textvariable=cls.bottomVar)bottomEntry.place(x=326, y=220)# (打印紙張)預覽previewFrame = ttk.LabelFrame(set, text='預覽', padding=(88, 147))previewFrame.place(x=420, y=16)tk.Label(previewFrame).pack()image = tk.PhotoImage(file='.\\..\\photo\\微信余額.png')tk.Label(set, image=image).place(x=421, y=37)cls.headerVar = tk.StringVar(value=HEADER_VAR)cls.footerVar = tk.StringVar(value=FOOTER_VAR)# 頁眉tk.Label(set, text='頁眉(H):').place(x=14, y=288)headerEntry = ttk.Entry(set, width=42, textvariable=cls.headerVar)headerEntry.place(x=106, y=288)# 頁腳tk.Label(set, text='頁腳(F):').place(x=14, y=330)footerEntry = ttk.Entry(set, width=42, textvariable=cls.footerVar)footerEntry.place(x=106, y=330)# 頁眉頁腳輸入值網頁詳情介紹# 修改 Button 的樣式# style.configure("my.TButton", width=6, font=("Arial", 10, 'underline'), foreground="blue")# ttk.Button(set, text='輸入值', style='my.TButton').place(x=106, y=360)headerFooterWeb = tk.Label(set, text='輸入值', relief='flat', foreground="blue", font=("Arial", 10, 'underline'))headerFooterWeb.place(x=106, y=360)# 捆綁跳轉網頁事件import webbrowserheaderFooterWeb.bind('<Button-1>', lambda event: webbrowser.open('https://support.microsoft.com/zh-cn/windows/更改記事本中的頁眉和頁腳命令-c1b0e27b-497d-c478-c4c1-0da491cac148'))# 確定# 修改 Button 的樣式style.map("my.TButton", background=[('!active', '!disabled', '#00BFFF')])confirm = ttk.Button(set, text='確定', width=13, style='my.TButton', command=lambda: cls.confirmCancel('確定', set))confirm.place(x=394, y=372)# 取消cancel = ttk.Button(set, text='取消', width=13, command=lambda: cls.confirmCancel('取消', set))cancel.place(x=506, y=372)# 捆綁獲取輸入框的數據事件set.bind('<KeyRelease>', cls.getEntry)set.mainloop() # 窗口循環# (打印紙張)頁面設置確定與取消@classmethoddef confirmCancel(cls, option, win=None):print(option)if option == '確定':# 修改的數值保存到文件# 發出警告聲音win.bell()pass# 關閉當前窗口win.destroy()# 獲取輸入框的數據@classmethoddef getEntry(cls, event=None):# (打印紙張)設置頁邊距(毫米)print('頁邊距:',cls.leftVar.get(),cls.rightVar.get(),cls.topVar.get(),cls.bottomVar.get())global LEFT_VAR, RIGHT_VAR, TOP_VAR, BOTTOM_VARLEFT_VAR = cls.leftVar.get()RIGHT_VAR = cls.rightVar.get()TOP_VAR = cls.topVar.get()BOTTOM_VAR = cls.bottomVar.get()# (打印紙張)設置頁眉頁腳print('頁眉/頁腳:',cls.headerVar.get(),cls.footerVar.get())global HEADER_VAR, FOOTER_VARHEADER_VAR = cls.headerVar.get()FOOTER_VAR = cls.footerVar.get()# (打印紙張)方向選擇@classmethoddef orientOption(cls):global PAPER_ORIENTPAPER_ORIENT = cls.orientVar.get()# (打印紙張)方向選擇if PAPER_ORIENT == 1:print('方向:縱向')elif PAPER_ORIENT == 2:print('方向:橫向')# 紙張選擇@classmethoddef paperOption(cls, option):global PAPER_VARPAPER_VAR = option# 紙張設置if option == 'A3':print('大小:A3')elif option == 'A4':print('大小:A4')elif option == 'A5':print('大小:A5')elif option == 'B4 (JIS)':print('大小:B4 (JIS)')elif option == 'B5 (JIS)':print('大小:B5 (JIS)')elif option == 'Executive':print('大小:Executive')elif option == 'Statement':print('大小:Statement')elif option == 'Tabloid':print('大小:Tabloid')elif option == '法律專用紙':print('大小:法律專用紙')elif option == '信紙':print('大小:信紙')# 文本編輯器窗口UI
class WindowsUI(PageSetup):readText = '' # 讀取文本數據@classmethoddef __init__(cls, base):cls.base = base# cls.base = tk.Tk() # 新建一個窗口cls.base.title('無標題 - 記事本') # 窗口標題cls.base.geometry(f'750x550+{cls.base.winfo_screenwidth()//4}+{cls.base.winfo_screenheight()//8}')# 創建一級菜單欄(此時為空)cls.menubar = tk.Menu(cls.base)cls.base.config(menu=cls.menubar)# 文件菜單# 創建二級菜單欄(此時為空)cls.fileMenu = tk.Menu(cls.menubar, tearoff=0)# 向一級菜單欄添加 文件 項,并與二級菜單(fileMenu)建立級聯關系(從屬/上下級)cls.menubar.add_cascade(label='文件(F)', menu=cls.fileMenu)# 文件的二級菜單欄添加 ... 項cls.fileMenu.add_command(label=f'新建(N){" "*28}Ctrl+N', command=cls.newText)cls.fileMenu.add_command(label=f'新窗口(W){" "*16}Ctrl+Shift+N', command=newWindow)cls.fileMenu.add_command(label=f'打開(O)...{" "*26}Ctrl+O', command=cls.openFile)cls.fileMenu.add_command(label=f'保存(S){" "*29}Ctrl+S', command=cls.saveFile)cls.fileMenu.add_command(label=f'另存為(A)...{" "*15}Ctrl+Shift+S', command=cls.saveAsFile)cls.fileMenu.add_command(label=f'頁面設置(U)...', command=cls.pageSetup)cls.fileMenu.add_command(label=f'打印(P)...{" "*27}Ctrl+P')cls.fileMenu.add_command(label=f'退出(X)', command=cls.base.destroy)# 菜單之間插入分隔線cls.fileMenu.insert_separator(5)cls.fileMenu.insert_separator(8)# 編輯菜單# 創建二級菜單欄(此時為空)cls.editMenu = tk.Menu(cls.menubar, tearoff=0)# 向一級菜單欄添加 編輯 項,并與二級菜單(editMenu)建立級聯關系(從屬/上下級)cls.menubar.add_cascade(label='編輯(E)', menu=cls.editMenu)# 編輯的二級菜單欄添加 ... 項cls.editMenu.add_command(label=f'撤銷(U){" "*26}Ctrl+Z', command=cls.repealEdit)cls.editMenu.add_command(label=f'剪切(T){" "*26}Ctrl+X')cls.editMenu.add_command(label=f'復制(C){" "*26}Ctrl+C')cls.editMenu.add_command(label=f'粘貼(V){" "*26}Ctrl+V')cls.editMenu.add_command(label=f'刪除(L){" "*27}Delete')cls.editMenu.add_command(label=f'使用 Bing 搜索...{" "*14}Ctrl+E')cls.editMenu.add_command(label=f'查找(F)...{" "*25}Ctrl+F')cls.editMenu.add_command(label=f'查找上一個(N){" "*23}F3')cls.editMenu.add_command(label=f'查找下一個(V){" "*15}Shift+F3')cls.editMenu.add_command(label=f'替換(R)...{" "*23}Ctrl+H')cls.editMenu.add_command(label=f'轉到(G)...{" "*23}Ctrl+G')cls.editMenu.add_command(label=f'全選(A){" "*26}Ctrl+A')cls.editMenu.add_command(label=f'時間/日期(D){" "*25}F5')# 菜單之間插入分隔線cls.editMenu.insert_separator(1)cls.editMenu.insert_separator(6)cls.editMenu.insert_separator(13)# 格式菜單# 全局變量cls.wrap = tk.BooleanVar(value=True)# 創建二級菜單欄(此時為空)cls.formatMenu = tk.Menu(cls.menubar, tearoff=0)# 向一級菜單欄添加 格式 項,并與二級菜單(formatMenu)建立級聯關系(從屬/上下級)cls.menubar.add_cascade(label='格式(O)', menu=cls.formatMenu)# 格式的二級菜單欄添加 ... 項cls.formatMenu.add_checkbutton(label='自動換行(W)', variable=cls.wrap, onvalue=1, offvalue=0, command=cls.setWrap)cls.formatMenu.add_command(label='字體(F)...')# 查看菜單# 全局變量cls.state = tk.BooleanVar(value=True)# 創建二級菜單欄(此時為空)cls.viewMenu = tk.Menu(cls.menubar, tearoff=0)# 向一級菜單欄添加 查看 項,并與二級菜單(checkMenu)建立級聯關系(從屬/上下級)cls.menubar.add_cascade(label='查看(V)', menu=cls.viewMenu)# 創建三級菜單欄(此時為空)cls.threeViewMenu = tk.Menu(cls.viewMenu, tearoff=0)# 查看的二級菜單欄添加 ... 項cls.viewMenu.add_cascade(label='縮放(Z)', menu=cls.threeViewMenu)cls.viewMenu.add_checkbutton(label='狀態欄(S)', variable=cls.state, onvalue=1, offvalue=0, command=cls.setState)# 縮放的三級菜單欄添加 ... 項cls.threeViewMenu.add_command(label=f'放大(I){" " * 14}Ctrl + 加號', command=lambda: cls.FontSizeEvent('放大'))cls.threeViewMenu.add_command(label=f'縮小(O){" " * 13}Ctrl + 減號', command=lambda: cls.FontSizeEvent('縮小'))cls.threeViewMenu.add_command(label=f'恢復默認縮放{" " * 11}Ctrl+0', command=lambda: cls.FontSizeEvent('默認縮放'))# 幫助菜單# 創建二級菜單欄(此時為空)cls.helpMenu = tk.Menu(cls.menubar, tearoff=0)# 向一級菜單欄添加 幫助 項,并與二級菜單(helpMenu)建立級聯關系(從屬/上下級)cls.menubar.add_cascade(label='幫助(H)', menu=cls.helpMenu)# 幫助的二級菜單欄添加 ... 項cls.helpMenu.add_command(label='查看幫助(H)')cls.helpMenu.add_command(label='發送反饋(F)')cls.helpMenu.add_command(label='關于文本編輯器(A)')# 菜單之間插入分隔線cls.helpMenu.insert_separator(2)# 右鍵菜單# 創建二級菜單欄(此時為空)cls.rightKeyMenu = tk.Menu(cls.base, tearoff=0)# 創建三級菜單欄(此時為空)cls.threeRightMenu = tk.Menu(cls.rightKeyMenu, tearoff=0)# 右鍵菜單的二級菜單欄添加 ... 項cls.rightKeyMenu.add_command(label='撤銷(U)')cls.rightKeyMenu.add_command(label='剪切(T)')cls.rightKeyMenu.add_command(label='復制(C)')cls.rightKeyMenu.add_command(label='粘貼(P)')cls.rightKeyMenu.add_command(label='刪除(D)')cls.rightKeyMenu.add_command(label='全選(A)')cls.rightKeyMenu.add_checkbutton(label='從右到左的閱讀順序(R)')cls.rightKeyMenu.add_checkbutton(label='顯示 Unicode 控制字符(S)')cls.rightKeyMenu.add_cascade(label='插入 Unicode 控制字符(I)', menu=cls.threeRightMenu)cls.rightKeyMenu.add_command(label='關閉輸入法(L)')cls.rightKeyMenu.add_command(label='漢字重選(R)')cls.rightKeyMenu.add_command(label='使用 Bing 搜索(B)...')# 插入 Unicode 控制字符(I)的三級菜單欄添加 ... 項cls.threeRightMenu.add_command(label='特殊字符1')cls.threeRightMenu.add('command', label='特殊字符2')cls.threeRightMenu.insert(3, 'command', label='特殊字符3')# ...# 菜單之間插入分隔線cls.rightKeyMenu.insert_separator(1)cls.rightKeyMenu.insert_separator(6)cls.rightKeyMenu.insert_separator(8)cls.rightKeyMenu.insert_separator(12)cls.rightKeyMenu.insert_separator(15)# 捆綁鼠標右鍵事件cls.base.bind('<Button-3>', lambda event: cls.rightKeyEvent(event, cls.rightKeyMenu))# 底行內容顯示# 底部內容框架cls.bottomFrame = tk.Frame(cls.base)cls.bottomFrame.pack(side=tk.BOTTOM, fill='both')# 狀態欄框架cls.stateFrame = tk.Frame(cls.bottomFrame, borderwidth=2, relief=tk.GROOVE)cls.stateFrame.pack(side=tk.BOTTOM, fill='both')# 字符編碼cls.charCodeLabel = tk.Label(cls.stateFrame, text=' UTF-8', width=16, anchor='w', borderwidth=2, relief=tk.GROOVE)cls.charCodeLabel.pack(side=tk.RIGHT)# 換行方式(回車換行)cls.CRLFlabel = tk.Label(cls.stateFrame, text=' Windows (CRLF)', width=17, anchor='w', borderwidth=2, relief=tk.GROOVE)cls.CRLFlabel.pack(side=tk.RIGHT)# 字體大小cls.fontSizeLabel = tk.Label(cls.stateFrame, text='100%', width=6, borderwidth=2, relief=tk.GROOVE)cls.fontSizeLabel.pack(side=tk.RIGHT)# 光標位置cls.locationLabel = tk.Label(cls.stateFrame, text=' 第 1 行,第 1 列', width=19, anchor='w', borderwidth=2, relief=tk.GROOVE)cls.locationLabel.pack(side=tk.RIGHT)# 空白填充(也可以按需顯示內容)cls.blankLabel = tk.Label(cls.stateFrame, text='歡迎使用記事本', borderwidth=2, relief=tk.GROOVE)cls.blankLabel.pack(fill=tk.BOTH)# 右側滾動條cls.rightScrollbar = tk.Scrollbar(cls.base, orient='vertical')cls.rightScrollbar.pack(side=tk.RIGHT, fill='both')# 底側滾動條cls.bottomScrollbar = tk.Scrollbar(cls.bottomFrame, orient="horizontal")# 文本編輯區域cls.fontSize = tk.IntVar()cls.fontSize.set(FONT_SIZE)cls.setFont = font.Font(family='Tahoma', size=cls.fontSize.get())cls.text = tk.Text(cls.base, wrap="word", xscrollcommand=cls.bottomScrollbar.set, yscrollcommand=cls.rightScrollbar.set, font=cls.setFont)cls.text.pack(expand=True, fill='both')# 將焦點設置到Text控件上cls.text.focus_set()# 底側滾動條與文本域關聯cls.bottomScrollbar.config(command=cls.text.xview)# 右側滾動條與文本域關聯cls.rightScrollbar.config(command=cls.text.yview)# 修改窗口標題的圖片cls.icon = tk.PhotoImage(file='.\\..\\photo\\記事本.png')cls.base.iconphoto(True, cls.icon)# cls.base.mainloop() # 窗口主循環# 類里的變量base = Nonetext = NonefontSize = NonesetFont = NonefontSizeLabel = NonelocationLabel = Nonewrap = NonebottomScrollbar = NonerightScrollbar = Nonestate = NonebottomFrame = NonestateFrame = NoneeditMenu = NonetextGet = None@classmethoddef mainLoop(cls):cls.base.mainloop() # 窗口主循環# 項目運行函數@classmethoddef workFunc(cls):# 捆綁事件,獲取Text文本的光標位置cls.text.bind('<KeyPress>', cls.cursorPosition) # 鍵盤按下觸發cls.text.bind('<KeyRelease>', cls.cursorPosition) # 鍵盤釋放觸發cls.text.bind('<ButtonPress>', cls.cursorPosition) # 鼠標按下觸發cls.text.bind('<ButtonRelease>', cls.cursorPosition) # 鼠標釋放觸發# 自定義注冊事件# cls.text.event_add('<<CursorEvent>>', *('<KeyPress>', '<KeyRelease>', '<ButtonPress>', '<ButtonRelease>'))# cls.text.bind('<<CursorEvent>>', cls.cursorPosition)# 自定義注冊縮放事件('<<ZoomEvent>>')cls.base.event_add('<<ZoomEvent>>', *('<Control-MouseWheel>', '<Control-Key-=>', '<Control-Key-+>', '<Control-minus>', '<Control-Key-0>'))# 捆綁自定義注冊縮放事件改變字體大小cls.base.bind('<<ZoomEvent>>', cls.FontSizeEvent) # 鼠標上滾縮小,下滾放大# 捆綁按鍵按下編輯文本事件cls.base.bind('<KeyPress>', cls.editText)# 新建文本cls.base.bind('<Control-Key-n>', cls.newText)# 創建新窗口cls.base.bind('<Control-Shift-Key-N>', newWindow)# 打開文件(BUG)cls.base.bind('<Control-Key-o>', cls.openFile)# 保存文件cls.base.bind('<Control-Key-s>', cls.saveFile)# 文件另存為cls.base.bind('<Control-Shift-Key-S>', cls.saveAsFile)# 文件打印(未完成)# 撤銷編輯cls.base.bind('<Control-Key-z>', cls.repealEdit)# 撤銷返編輯cls.base.bind('<Control-Shift-Key-Z>', cls.repealEdit)# 編輯撤銷菜單狀態cls.base.bind('<Motion>', cls.repealState)# 編輯剪切# 編輯復制# 編輯粘貼# 編輯刪除# 編輯撤銷菜單狀態@classmethoddef repealState(cls, event=None):if len(cls.editData) > 1 and cls.editIndex != -len(cls.editData):cls.editMenu.entryconfig(0, state='active', activebackground='#4169E1')elif cls.editIndex == -len(cls.editData):cls.editMenu.entryconfig(0, state='disable', activebackground='#DCDCDC')# 撤銷編輯editData = [('\n', FONT_SIZE)] # 編輯數據editIndex = -1@classmethoddef repealEdit(cls, event=None):# 菜單欄觸發if not event:cls.base.event_generate('<Control-Key-z>')return# 快捷鍵觸發elif event.keysym == 'z':# 限制條件if len(cls.editData) == 1 or -len(cls.editData) == cls.editIndex:returncls.editIndex -= 1# 快捷鍵觸發elif event.keysym == 'Z':# 限制條件if cls.editIndex >= -1:returncls.editIndex += 1# 初始化文本cls.text.delete('1.0', 'end')# 插入上次編輯的文本數據cls.text.insert('1.0', cls.editData[cls.editIndex][0][:-1:])# 修改字體大小cls.fontSize.set(cls.editData[cls.editIndex][1])# 改變字體大小cls.setFont.config(size=cls.fontSize.get())# 改變底部顯示字體大小百分比cls.fontSizeLabel.config(text='{:.0%}'.format(cls.fontSize.get() / FONT_SIZE))# 文件另存為@classmethoddef saveAsFile(cls, event=None):global openPath# 文件保存類型filetypes = [("文本文檔", ".txt"), ("所有文件", ".*")]# 保存文件對話框savePath = tkfd.asksaveasfile(defaultextension=".txt", initialfile='*.txt', filetypes=filetypes)# 確定保存if savePath:# 把文本編輯的數據寫入文件with open(savePath.name, 'w', encoding=savePath.encoding) as file:file.write(cls.text.get('1.0', 'end')[:-1:])# 窗口標題前去掉'*'cls.base.title(cls.base.title()[1::])# 修改標題cls.base.title(f'{savePath.name.split("/")[-1]} - 記事本')cls.readText = cls.text.get('1.0', 'end')[:-1:]openPath = savePath# 保存文件@classmethoddef saveFile(cls, event=None):global openPath# 判斷文本是否編輯過if cls.base.title()[0] == '*':# 從程序打開進入的編輯if not cls.readText:# 消息對話框選擇是否保存ifYes = tkmb.askyesnocancel('記事本', f'你想將更改保存到 無標題 嗎?')# 如果選擇是if ifYes:# 文件另存為cls.saveAsFile()# 打開文件后保存else:# 保存文本數據,寫入文件with open(openPath.name, 'w', encoding=openPath.encoding) as file:file.write(cls.text.get('1.0', 'end')[:-1:])# 窗口標題前去掉'*'cls.base.title(cls.base.title()[1::])cls.readText = cls.text.get('1.0', 'end')[:-1:]# 打開文件@classmethoddef openFile(cls, keyTrigger=None):global openPathifYes = TruesavePath = True# 如果是鍵盤觸發(必須)if keyTrigger:# Ctrl+O 觸發,光標處會加入換行(不知道是否為BUG,求指教)cls.text.delete('1.0', 'end')cls.text.insert('1.0', cls.textGet[:-1:])# 判斷文本是否編輯過if cls.base.title()[0] == '*':# 從程序打開進入的編輯if not cls.readText:fileName = f'你想將更改保存到 {cls.base.title().split(" ")[0][1::]} 嗎?'# 從文件打開進入的編輯else:fileName = f'你想將更改保存到\n{openPath.name}\n嗎?'# 在消息對話框選擇是否保存當前編輯的文本ifYes = tkmb.askyesnocancel('記事本', fileName)# 確定if ifYes:# 判斷保存的文件是否存在if not cls.readText:# 文件另存為cls.saveAsFile()else:# 保存文本數據,寫入文件with open(openPath.name, 'w', encoding=openPath.encoding) as file:file.write(cls.text.get('1.0', 'end')[:-1:])# 窗口標題前去掉'*'cls.base.title(cls.base.title()[1::])cls.readText = cls.text.get('1.0', 'end')[:-1:]# 打開文件if ifYes != None and savePath:# 消息對話框打開文件# must be -defaultextension, -filetypes, -initialdir, -initialfile, -multiple, -parent, -title, or -typevariablefiletypes = [('文本文檔', '.txt'), ('所有文件', '.*')]# 打開文件對話框cls.openPath = tkfd.askopenfile(filetypes=filetypes)# 確定打開文件if cls.openPath:openPath = cls.openPath# 窗口初始化cls.text.delete('1.0', 'end')cls.readText = ''# 打開文件讀取數據插入到文本域with open(cls.openPath.name, 'r', encoding=cls.openPath.encoding) as file:for i in file:cls.text.insert('end', i)# 更新文本讀取數據cls.readText += i# 修改窗口標題cls.base.title(f'{cls.openPath.name.split("/")[-1]} - 記事本')cls.base.event_generate('<Key>')# 數據更新cls.editData.clear()cls.editData.append((cls.textGet, cls.fontSize.get()))cls.editIndex = -1# 新建文本@classmethoddef newText(cls, event=None):ifYes = TruesavePath = True# 判斷文本是否編輯過if cls.base.title()[0] == '*':# 從程序打開進入的編輯if not cls.readText:fileName = f'你想將更改保存到 {cls.base.title().split(" ")[0][1::]} 嗎?'# 從文件打開進入的編輯else:fileName = f'你想將更改保存到\n{openPath.name}\n嗎?'# 在消息對話框選擇是否保存當前編輯的文本ifYes = tkmb.askyesnocancel('記事本', fileName)# 確定if ifYes:# 判斷保存的文件是否存在if not cls.readText:# 文件保存類型filetypes = [("文本文檔", ".txt"), ("所有文件", ".*")]# 保存文件對話框savePath = tkfd.asksaveasfile(defaultextension=".txt", initialfile='*.txt', filetypes=filetypes)# 確定保存if savePath:# 把文本編輯的數據寫入文件with open(savePath.name, 'w', encoding=savePath.encoding) as file:file.write(cls.text.get('1.0', 'end')[:-1:])else:# 保存文本數據,寫入文件with open(openPath.name, 'w', encoding=openPath.encoding) as file:file.write(cls.text.get('1.0', 'end')[:-1:])# 初始化窗口if ifYes != None and savePath:cls.base.title('無標題 - 記事本')cls.readText = ''cls.text.delete('1.0', 'end')cls.base.event_generate('<Key>')# 編輯Text文本@classmethoddef editText(cls, event=None):print('1editText:', [cls.text.get('1.0', 'end')])data = cls.text.get('1.0', 'end')# 文本編輯時窗口標題前加入'*'if data[:-1:] != cls.readText and data != '\n' and cls.base.title()[0] != '*':cls.base.title('*' + cls.base.title())# 編輯過文本未保存或與原文本相同時窗口標題前去掉'*'elif data[:-1:] == cls.readText or data == '\n':if cls.base.title()[0] == '*':cls.base.title(cls.base.title()[1::])# 如果是鍵盤Ctrl+O觸發打開文件(必須)cls.textGet = cls.text.get('1.0', 'end')# 編輯撤銷數據存儲if cls.editIndex == -1:cls.editData.append((cls.textGet, cls.fontSize.get()))if len(cls.editData) != 1 and cls.textGet == cls.editData[cls.editIndex-1][0]:cls.editData.pop(-2)else:if cls.textGet != cls.editData[cls.editIndex][0]:cls.editData.insert(len(cls.editData) + cls.editIndex + 1, (cls.textGet, cls.fontSize.get()))buf = cls.editData[:len(cls.editData) + cls.editIndex + 1:]cls.editData.clear()cls.editData.extend(buf)cls.editIndex = -1else:cls.editData.pop(len(cls.editData) + cls.editIndex)cls.editData.insert(len(cls.editData) + cls.editIndex+1, (cls.textGet, cls.fontSize.get()))print('2editText:', [cls.text.get('1.0', 'end')])print('2readText:', [cls.readText])print('2editData:', cls.editData)# 狀態欄:更新字體大小百分比@classmethoddef FontSizeEvent(cls, event):# 菜單調整字體大小if event == '放大':cls.base.event_generate('<Control-Key-+>') # 引起鍵盤觸發事件returnelif event == '縮小':cls.base.event_generate('<Control-minus>') # 引起鍵盤觸發事件returnelif event == '默認縮放':cls.base.event_generate('<Control-Key-0>') # 引起鍵盤觸發事件return# 快捷鍵調整字體大小# 向下滾動if event.delta < 0 or event.keysym == 'minus':# 字體大小范圍if cls.fontSize.get() <= 1:return# 縮小字體cls.fontSize.set(cls.fontSize.get() - 1)print('向上滾動,字體大小:', cls.fontSize.get())# 向上滾動else:# 字體大小范圍if cls.fontSize.get() >= FONT_SIZE * 5:return# 放大字體cls.fontSize.set(cls.fontSize.get() + 1)# 恢復默認縮放if event.keysym == '0':cls.fontSize.set(FONT_SIZE)print('向下滾動,字體大小:', cls.fontSize.get())# 改變字體大小cls.setFont.config(size=cls.fontSize.get())# 改變底部顯示字體大小百分比cls.fontSizeLabel.config(text='{:.0%}'.format(cls.fontSize.get() / FONT_SIZE))# 狀態欄:獲取Text光標位置@classmethoddef cursorPosition(cls, event):row, column = event.widget.index("insert").split(".")print("光標位置:行", row, "列", int(column) + 1)cls.locationLabel.config(text=f' 第 {row} 行,第 {int(column) + 1} 列')# 勾選自動換行顯示與否@classmethoddef setWrap(cls):# 設置自動換行if cls.wrap.get():# 自動換行設置cls.text.config(wrap='word')# 移除底部水平滑動條cls.bottomScrollbar.pack_forget()# 底部框架沒有組件顯示時移除if not cls.state.get():cls.bottomFrame.pack_forget()# 設置取消自動換行else:# 先移除右側滾動條,再顯示cls.rightScrollbar.pack_forget()# 先移除中間文本域,再顯示cls.text.pack_forget()# 顯示底部框架cls.bottomFrame.pack(side=tk.BOTTOM, fill='both')# 取消自動換行設置cls.text.config(wrap='none')# 顯示底部水平滑動條cls.bottomScrollbar.pack(fill='both')# 再顯示右側滾動條cls.rightScrollbar.pack(side=tk.RIGHT, fill='both')# 再中間文本域cls.text.pack(expand=True, fill='both')# 勾選底部狀態欄顯示與否@classmethoddef setState(cls):# 底部顯示狀態欄if cls.state.get():# 先移除右側滾動條,再顯示cls.rightScrollbar.pack_forget()# 先移除中間文本域,再顯示cls.text.pack_forget()# 顯示底部框架cls.bottomFrame.pack(side=tk.BOTTOM, fill='both')# 顯示狀態欄cls.stateFrame.pack(side=tk.BOTTOM, fill='both')# 底部移除狀態欄else:# 移除狀態欄cls.stateFrame.pack_forget()# 底部框架沒有組件顯示時移除if cls.wrap.get():cls.bottomFrame.pack_forget()# 再顯示右側滾動條cls.rightScrollbar.pack(side=tk.RIGHT, fill='both')# 再中間文本域cls.text.pack(expand=True, fill='both')# Text文本鼠標右鍵菜單事件@classmethoddef rightKeyEvent(cls, event, object):object.post(event.x_root, event.y_root)# 創建頂級窗口后,根窗口會對其產生影響(比如:打開的對話框是對根窗口打開的,操作的卻是頂級窗口)
# 創建新窗口(需要用到線程,否則前面創建的窗口不能運行)
def newWindow(event=None):newBase = tk.Toplevel()UI2 = WindowsUI(newBase)UI2.workFunc() # 項目運行函數# 主窗口
def mainWindow():base = tk.Tk() # 新建主窗口UI = WindowsUI(base)UI.workFunc() # 項目運行函數WindowsUI.mainLoop() # 窗口主循環# 全局變量
openPath = ''# 主函數
def main():mainWindow() # 主窗口# 代碼測試
if __name__ == '__main__':main()
else:print(f'導入{__name__}模塊')
作者:周華
創作日期:2023/11/23