Python 使用tkinter复刻Windows记事本UI和菜单功能(三)

上一篇:Python 使用tkinter复刻Windows记事本UI和菜单功能(二)-CSDN博客

下一篇:敬请耐心等待,如发现BUG以及建议,请在评论区发表,谢谢!

本文章完成了记事本的新建、保存、另存、打开文件、状态栏显隐等部分基本功能,还设计还原了页面设置UI。很抱歉现阶段我没有精力再去完善和优化这个项目了,不止是能力受限,还耽搁太多时间了。

运行结果

Python 使用tkinter复刻Windows记事本UI和菜单功能(三)_第1张图片

Python 使用tkinter复刻Windows记事本UI和菜单功能(三)_第2张图片

代码实例

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 = None
    leftVar = None
    rightVar = None
    topVar = None
    bottomVar = None
    headerVar = None
    footerVar = None


    # (打印)页面设置
    @classmethod
    def 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 webbrowser
        headerFooterWeb.bind('', 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('', cls.getEntry)

        set.mainloop()      # 窗口循环


    # (打印纸张)页面设置确定与取消
    @classmethod
    def confirmCancel(cls, option, win=None):
        print(option)
        if option == '确定':
            # 修改的数值保存到文件
            # 发出警告声音
            win.bell()
            pass

        # 关闭当前窗口
        win.destroy()


    # 获取输入框的数据
    @classmethod
    def 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_VAR
        LEFT_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_VAR
        HEADER_VAR = cls.headerVar.get()
        FOOTER_VAR = cls.footerVar.get()


    # (打印纸张)方向选择
    @classmethod
    def orientOption(cls):
        global PAPER_ORIENT
        PAPER_ORIENT = cls.orientVar.get()
        # (打印纸张)方向选择
        if PAPER_ORIENT == 1:
            print('方向:纵向')
        elif PAPER_ORIENT == 2:
            print('方向:横向')


    # 纸张选择
    @classmethod
    def paperOption(cls, option):
        global PAPER_VAR
        PAPER_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 = ''  # 读取文本数据
    @classmethod
    def __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('', 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 = None
    text = None
    fontSize = None
    setFont = None
    fontSizeLabel = None
    locationLabel = None
    wrap = None
    bottomScrollbar = None
    rightScrollbar = None
    state = None
    bottomFrame = None
    stateFrame = None
    editMenu = None
    textGet = None

    @classmethod
    def mainLoop(cls):
        cls.base.mainloop()  # 窗口主循环


    # 项目运行函数
    @classmethod
    def workFunc(cls):
        # 捆绑事件,获取Text文本的光标位置
        cls.text.bind('', cls.cursorPosition)  # 键盘按下触发
        cls.text.bind('', cls.cursorPosition)  # 键盘释放触发
        cls.text.bind('', cls.cursorPosition)  # 鼠标按下触发
        cls.text.bind('', cls.cursorPosition)  # 鼠标释放触发
        # 自定义注册事件
        # cls.text.event_add('<>', *('', '', '', ''))
        # cls.text.bind('<>', cls.cursorPosition)

        # 自定义注册缩放事件('<>')
        cls.base.event_add('<>', *('', '', '', '', ''))
        # 捆绑自定义注册缩放事件改变字体大小
        cls.base.bind('<>', cls.FontSizeEvent)  # 鼠标上滚缩小,下滚放大

        # 捆绑按键按下编辑文本事件
        cls.base.bind('', cls.editText)

        # 新建文本
        cls.base.bind('', cls.newText)

        # 创建新窗口
        cls.base.bind('', newWindow)

        # 打开文件(BUG)
        cls.base.bind('', cls.openFile)

        # 保存文件
        cls.base.bind('', cls.saveFile)

        # 文件另存为
        cls.base.bind('', cls.saveAsFile)

        # 文件打印(未完成)

        # 撤销编辑
        cls.base.bind('', cls.repealEdit)
        # 撤销返编辑
        cls.base.bind('', cls.repealEdit)

        # 编辑撤销菜单状态
        cls.base.bind('', cls.repealState)

        # 编辑剪切
        # 编辑复制
        # 编辑粘贴
        # 编辑删除

    # 编辑撤销菜单状态
    @classmethod
    def 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
    @classmethod
    def repealEdit(cls, event=None):
        # 菜单栏触发
        if not event:
            cls.base.event_generate('')
            return
        # 快捷键触发
        elif event.keysym == 'z':
            # 限制条件
            if len(cls.editData) == 1 or -len(cls.editData) == cls.editIndex:
                return
            cls.editIndex -= 1
        # 快捷键触发
        elif event.keysym == 'Z':
            # 限制条件
            if cls.editIndex >= -1:
                return
            cls.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))


    # 文件另存为
    @classmethod
    def 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


    # 保存文件
    @classmethod
    def 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:]


    # 打开文件
    @classmethod
    def openFile(cls, keyTrigger=None):
        global openPath
        ifYes = True
        savePath = 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 -typevariable
            filetypes = [('文本文档', '.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('')
                # 数据更新
                cls.editData.clear()
                cls.editData.append((cls.textGet, cls.fontSize.get()))
                cls.editIndex = -1


    # 新建文本
    @classmethod
    def newText(cls, event=None):
        ifYes = True
        savePath = 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('')


    # 编辑Text文本
    @classmethod
    def 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 = -1
            else:
                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)


    # 状态栏:更新字体大小百分比
    @classmethod
    def FontSizeEvent(cls, event):
        # 菜单调整字体大小
        if event == '放大':
            cls.base.event_generate('')  # 引起键盘触发事件
            return
        elif event == '缩小':
            cls.base.event_generate('')  # 引起键盘触发事件
            return
        elif event == '默认缩放':
            cls.base.event_generate('')  # 引起键盘触发事件
            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光标位置
    @classmethod
    def cursorPosition(cls, event):
        row, column = event.widget.index("insert").split(".")
        print("光标位置:行", row, "列", int(column) + 1)
        cls.locationLabel.config(text=f'  第 {row} 行,第 {int(column) + 1} 列')


    # 勾选自动换行显示与否
    @classmethod
    def 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')


    # 勾选底部状态栏显示与否
    @classmethod
    def 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文本鼠标右键菜单事件
    @classmethod
    def 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

你可能感兴趣的:(Python,入门案例,python,windows,ui,开发语言,记事本)