python tkinter Text文本框显示行数且同步滚动

抽空做的一个显示行数的文本框,结合上一次的带关闭按钮的Notebook,做个简单的框架,菜单功能未添加,有需要的拿去使用。
使用示例
目前就文本框的里面相对完善可以自动同步行数,包括:选中拖动、上下左右按钮、修改内容、光标在范围外按钮。
不多说什么自己试下。

import tkinter as tk, os
from tkinter import ttk

class CustomNotebook(ttk.Notebook): # 带关闭按钮的Notebook
    """定义一个名为CustomNotebook的类,继承自ttk.Notebook"""

    __initialized = False  # 初始化一个私有变量,用于标记是否已经初始化

    def __init__(s, *args, **kwargs):
        # 如果尚未初始化,则调用自定义初始化方法,并设置已初始化标志
        if not s.__initialized:
            s.__initialize_custom_style()
            CustomNotebook.__initialized = True
            # s.__inititialized = True # 这样会导致第二次调用出错,请用上面的方法标记

        # 设置notebook的样式为"CustomNotebook"
        kwargs["style"] = "CustomNotebook"
        # 调用父类的初始化方法
        ttk.Notebook.__init__(s, *args, **kwargs)

        s._active = None # 初始化一个私有变量,用于存储当前活动的tab

        # 绑定鼠标左键按下事件到on_close_press方法
        s.bind("<ButtonPress-1>", s.on_close_press, True)
        # 绑定鼠标左键释放事件到on_close_release方法
        s.bind("<ButtonRelease-1>", s.on_close_release)

    def __initialize_custom_style(s):
        # 创建一个ttk样式对象
        style = ttk.Style()
        # 定义四个图片对象,分别表示关闭按钮的不同状态
        s.images = (
            # 元素普通状态时图标
            tk.PhotoImage("img_closenormal", data='''
                R0lGODdhCwALAIMAAJKSkpeXl5ubm5+fn6CgoKampqqqqq2trbGxsba2tr29vcHBwdnZ2QAAAAAAAAAA
                ACwAAAAACwALAAAIXQAXJEBwwEDBAgUGHEigYOCBhwYMEBCAIAEDBgYQXhwg4ACCiwwKgOQIEeRGjgUK
                GgBJYABKgyYZuBRAQORFmwwEBBhgE6FNAQAEDCBAtCVHoAcC6AzANAAAAAYCAgA7
                '''),
            # 选中的选项卡的关闭图标
            tk.PhotoImage("img_closeselected", data='''
                R0lGODdhCwALAIVUAJ9dcptfe6NfcpthfZ5hfJ9ifp5nfaNjda9lc6Rmea1nfqRpe6Fqfq9ofK1ofqxr
                frFndbJqeLJufZ9rgKpngaBqgKtpg6tphK5uhKxvia5xhq91iq1xjK1zj692jLFyg7N1hrJ3i7B2jbF4
                ja51ka94kq97l698mq9+m7B8mbB/nLCDn6aEoayMprCForGMq7KNq7KPrrOXt7WfwP///wAAAAAAAAAA
                AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAACwALAAAIcgBnwFiBwoQJEh0yUGARQ8YLFwRPHLwQ
                wEUMGjQOcsB4YYAKFxhpbMRooYCJFSlCYlTgoAAJFSlMhHTgQEGBDilSqoyggACGEiFFhGxwAINQGiNC
                gMAIQcADDR48bAjxQUIEBABaGJgwoQKDBQkOCGAREAA7
                '''),
            # 鼠标经过时选项卡的关闭图标
            tk.PhotoImage("img_closeactive", data='''
                R0lGODdhCwALAIVSAN5IN+lRNeBQP+5bPvtSP+FSQuJXR+VbRu5dQetfRuVbSf9dS/xeTO5iR+hhTOxl
                TP1wTP1iUP1kUv9lVPxnVv9rVP1pWP9rWv9vXv5/WP54Xf9yYv11YP90ZP99YP94af97bP99bv6BW/6C
                Yv6FYP+BZf6JZv+Eb/6Jav+Oa+eJe/+Bcf+Edf+Hef+KfP+Rd/+WfP+bev+Pgu+Rhv+RhP+ViP+Zjf+h
                lv+lmv+1rP+/twAAAAAAAAAAAAAAACwAAAAACwALAAAIcwB13KDhgkWIEB8wRJiBIwcOGzRktFjx
                wUKBGjh27GABYoNGCQZc2NC4owNJkCsIktRYoYKCDywmkizhQYODDSFWrNyRYsSDCSY1wiBJosGCCxpP
                vIihMcMAAgwoXOBQAoUJERACzAAAoICAAwkaIAigIiAAOw==
                '''),
            # 按下关闭按钮时选项卡的关闭图标
            tk.PhotoImage("img_closepressed", data='''
                R0lGODdhCwALAIUAAGsiD20jEHIkEHYlEXsnEX4oEogrE4orFIwsFJEtFJUvFZoxFpwwFp4yF6EzF6Yz
                F642GbU3GbY4Grk5Grk6G7s7G7w6G7w8G8A+HcJAHcVDH8ZEH8dEIMhGIMtJIsxKIs1KIpdxZ8eAbtqI
                cuKdiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
                AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAACwALAAAIcABDABBA4AACBQscNBARIAGDBxAkVKhw
                oeIABiRIRJhQIeMFDAUeZCTRMWOGCwYgRBhpMgMGBBEqWBipYcOGDAoolBz5wcOGBRdGchjps0FQEhk6
                eMgIgoODihgycOgAAoQHDiIsYMBwoeYGDhtGBAQAOw==
                '''),
            )

        # 使用element_create方法创建一个名为"close"的元素,类型为"image",图像文件名为"img_closenormal"
        style.element_create("close", "image", "img_closenormal",
                            # 当元素处于激活、按下、未禁用状态时,显示"img_closepressed"图片
                            ("active", "pressed", "!disabled", "img_closepressed"),
                            # 当元素处于激活且未禁用状态时,显示"img_closeactive"图片
                            ("active", "!disabled", "img_closeactive"), 
                            # 当选项卡处于选中状态时,显示"img_closeselected"图片
                            ("selected", "img_closeselected"), 
                            # 设置元素的边框宽度为8像素,无边框;设置元素的粘性属性为空字符串,表示不粘附在其他元素上。
                            border=9, sticky='')
        '''
        notebook有如下状态
            disabled    禁用状态,该状态下的控件无法接收用户输入。
            normal      正常状态,该状态下的控件可以接收用户输入。
            active      激活状态(鼠标经过),该状态下的控件可以接收用户输入,并且会显示特殊效果(如闪烁)。
            selected    选中状态,该状态下的控件会显示特殊效果(如高亮)。
            insensitive 不敏感状态,该状态下的控件不会响应用户的键盘操作。
            focus       聚焦状态,该状态下的控件会显示特殊效果(如边框)。
        '''
        # 设置Notebook的样式为"CustomNotebook",并为"CustomNotebook.client"添加一个样式选项,设置其"sticky"属性为"nswe"
        style.layout("CustomNotebook", [("CustomNotebook.client", {"sticky": "nswe"})])
        # 设置CustomNotebook.Tab的布局样式
        style.layout("CustomNotebook.Tab", [
            ("CustomNotebook.tab", { # 设置 CustomNotebook.tab 的样式
                "sticky": "nswe", # 设置 tab 的粘性属性为 NSWE,表示在水平方向上可拉伸,垂直方向上可滚动
                "children": [ # 设置 tab 的子元素
                    ("CustomNotebook.padding", { # 设置 CustomNotebook.padding 的样式
                        "side": "top", # 设置 padding 的侧边距在顶部
                        "sticky": "nswe", # 设置 padding 的粘性属性为 NSWE
                        "children": [ # 设置 padding 的子元素
                            ("CustomNotebook.focus", { # 设置 CustomNotebook.focus 的样式
                                "side": "top", # 设置 focus 的侧边距在顶部
                                "sticky": "nswe", # 设置 focus 的粘性属性为 NSWE
                                "children": [ # 设置 focus 的子元素
                                    # 设置 CustomNotebook.label 的样式
                                    ("CustomNotebook.label", {"side": "left", "sticky": ''}), # 设置 label 的侧边距在左侧,无粘性
                                    # 设置 CustomNotebook.close 的样式
                                    ("CustomNotebook.close", {"side": "left", "sticky": ''}), # 设置 close 的侧边距在左侧,无粘性
                                ]
                        })
                    ]
                })
            ]
        })
    ])

    def on_close_press(s, event):
        """当按钮被按下时触发,位于关闭按钮上方"""

        # 获取鼠标点击位置的元素
        element = s.identify(event.x, event.y)

        # 如果元素包含"close",则执行以下操作
        if "close" in element:
            # 获取鼠标点击位置的索引值
            index = s.index("@%d,%d" % (event.x, event.y))
            # 将按钮状态设置为按下
            s.state(['pressed'])
            # 将_active属性设置为点击的索引值
            s._active = index

    def on_close_release(s, event):
        """ 
        当鼠标在关闭按钮上释放时调用此方法。
        event:  包含鼠标事件信息的对象。
        """
        if not s.instate(['pressed']): # 如果按钮没有按下状态,直接返回
            return
        try:
            element =  s.identify(event.x, event.y) # 获取鼠标释放位置的元素
            index = s.index("@%d,%d" % (event.x, event.y)) # 获取元素在列表中的索引
            if "close" in element and s._active == index: # 元素是关闭按钮,且当前激活的标签页与释放位置的标签页相同
                s.event_generate("<<NotebookTabClosed>>") # 生成一个表示标签页关闭的事件
                #s.forget(index)  # 删除该标签页,但并未销毁,要销毁需要用destroy(),也可以将删除写入NotebookTabClosed事件里面
        except: pass
        s.state(["!pressed"]) # 将按钮状态设置为非按下状态
        s._active = None # 将当前激活的标签页设置为None

class ScrolledText(tk.Text): # 带右侧滚动条的文本框
    def __init__(s, master=None, **kw):
        s.frame = tk.Frame(master)
        s.vbar = tk.Scrollbar(s.frame)
        s.vbar.pack(side='right', fill='y')

        kw.update({'yscrollcommand': s.vbar.set})
        tk.Text.__init__(s, s.frame, **kw)
        s.pack(side='left', fill='both', expand=True)
        s.vbar['command'] = s.yview

        # Copy geometry methods of s.frame without overriding Text
        # methods -- hack!
        text_meths = vars(tk.Text).keys()
        methods = vars(tk.Pack).keys() | vars(tk.Grid).keys() | vars(tk.Place).keys()
        methods = methods.difference(text_meths)
        for m in methods:
            if m[0] != '_' and m != 'config' and m != 'configure':
                setattr(s, m, getattr(s.frame, m))

        # 为底层控件创建一个代理
        # 为变量s添加一个属性_orig,其值为s的_w属性值加上"_orig"字符串
        s._orig = s._w + "_orig" 
        # 使用tkinter库的call方法,将s的_w属性对应的窗口重命名为_orig
        s.tk.call("rename", s._w, s._orig)
        # 使用tkinter库的createcommand方法, 为s的_w属性对应的窗口创建一个命令, 该命令执行s的_proxy属性对应的函数
        s.tk.createcommand(s._w, s._proxy)

    def __str__(s):
        return str(s.frame)

    def _proxy(s, command, *args):
        # 避免复制时出错
        if command == 'get' and (args[0] == 'sel.first' and args[1] == 'sel.last') and not s.tag_ranges('sel'): return
        # 避免删除时出错
        if command == 'delete' and (args[0] == 'sel.first' and args[1] == 'sel.last') and not s.tag_ranges('sel'): return
        #if command not in ['index','yview','count']:
            #print (command, args)

        cmd = (s._orig, command) + args # 将原始对象、命令和参数组合成一个新的命令
        result = s.tk.call(cmd) # 调用新命令并获取结果
        if command in ("insert", "delete", "replace"): # 如果命令是插入、删除或替换操作
            s.event_generate("<<TextModified>>") # 生成一个<<TextModified>>事件,表示文本已被修改
        return result

class RowScrolledText: # 左侧带行数显示的文本框
    def __init__(s, frame, master, spacing=5, font=("等线等线 (Light)", 14)):
        s.root  = master
        s.frame = frame
        # 创建一个文本框,用于输入和显示文本
        s.line_text = tk.Text(s.frame, width=5, height=600, spacing3=spacing, bg="#DCDCDC", bd=0, 
                                       font=font, takefocus=0, state="disabled", cursor="arrow")
        s.line_text.pack(side="left", expand="no")
        frame.update() # 更新画布和文本框的显示

        # 创建一个带滚动条的文本框,用于显示大文本,设置边框样式为"ridge"、"solid"、"double"、"groove"、"ridgeless"或"none"
        s.ScrolledText = ScrolledText(s.frame, height=1, wrap="none", spacing3=spacing, bg="white", bd=0,
                                               font=font, undo=True, insertwidth=1, relief="solid")
        s.ScrolledText.vbar.configure(command=s.scroll)
        s.ScrolledText.pack(side="right", fill="both", expand=True)
        #events = s.ScrolledText.event_info()
        #print(events)
        
        # 每行插入数字,用来对比行数显示的效果
        for i in range(60): s.ScrolledText.insert('end', str(i+1)+"\n")

        s.line_text.bind   ("<MouseWheel>", s.wheel) # line_text鼠标滚轮事件
        s.ScrolledText.bind("<MouseWheel>", s.wheel) # ScrolledText鼠标滚轮事件
        s.ScrolledText.bind("<KeyPress-Up>"   , s.KeyPress_scroll)
        s.ScrolledText.bind("<KeyPress-Down>" , s.KeyPress_scroll)
        s.ScrolledText.bind("<KeyPress-Left>" , s.KeyPress_scroll)
        s.ScrolledText.bind("<KeyPress-Right>", s.KeyPress_scroll)
        s.ScrolledText.bind("<<Selection>>"   , s.on_selection) # 文本选中事件
        s.ScrolledText.bind("<<TextModified>>", s.get_txt)      # 绑定文本修改事件
        s.show_line() # 显示行数
        
    def on_selection(s,event): # 处理选中文本事件
        # text = event.widget.get("sel.first", "sel.last") # 获取选中文本的内容
        s.line_text.yview('moveto', s.ScrolledText.vbar.get()[0]) # 确保选中拖动导致滚动条滚动时行数显示能同步

    def wheel(s, event): # 处理鼠标滚轮事件
        # 根据鼠标滚轮滚动的距离,更新line_text和ScrolledText的垂直滚动位置
        s.line_text.yview_scroll(int(-1 * (event.delta / 120)), "units")
        s.ScrolledText.yview_scroll(int(-1 * (event.delta / 120)), "units")
        return "break"

    def see_line(s, line):
        s.ScrolledText.see(f"{line}.0")
        s.line_text.see(f"{line}.0")

    def KeyPress_scroll(s, event=None, moving = 0, row = 0):
        # 光标所在行的行数和位置
        line, column = map(int, s.ScrolledText.index("insert").split('.'))
        # 屏幕显示范围最上面的行
        first_line = int(s.ScrolledText.index("@0,0").split('.')[0])
        # 屏幕显示范围最下面的行
        last_line = int(s.ScrolledText.index("@0," + str(s.ScrolledText.winfo_height())).split('.')[0])

        # 光标超显示范围事件,先滚动屏幕到光标能显示区域
        if line <= first_line+row or line >= last_line-row:
            s.see_line(line)

        if row: return # show_line 转过来的到这里结束

        if event.keysym == 'Up': # 按上键,在光标小于顶部能显示的下一行时激活滚动
            if line <= first_line+1: moving = -1 # 这里用first_line+1,是为了防止最上面一行只露出一点的情况,下面同理
        elif  event.keysym == 'Down': # 按下键,在光标大于底部能显示的上一行时激活滚动
            if line >= last_line-1: moving = 1
        elif  event.keysym == 'Left': # 按左键,在光标小于顶部能显示的下一行且光标在开头时激活滚动
            if line <= first_line+1 and not column: moving = -1
        elif  event.keysym == 'Right': # 按右键,在光标大于底部能显示的上一行且光标在结尾时激活滚动
            text = s.ScrolledText.get("1.0", "end") # 获取文本内容
            cursor_line = text.split("\n")[line-1]  # 获取光标所在行内容
            line_length = len(cursor_line) # 光标在当前行的位置
            if line >= last_line-1 and column == line_length: moving = 1 

        s.line_text.yview_scroll(moving, "units")
        s.ScrolledText.yview_scroll(moving, "units")
    
    def scroll(s, *xy): # 处理滚动条滚动事件
        # 根据滚动条,更新line_text和ScrolledText的垂直滚动位置
        s.line_text.yview(*xy)
        s.ScrolledText.yview(*xy)

    def get_txt(s, event=None): # 用于获取文本内容并显示
        '修改内容后需要的操作都可以写在这里'
        # txt = s.ScrolledText.get("1.0", "end")[:-1] # 文本框内容
        s.show_line()

    def show_line(s):
        # 获取文本行数
        text_lines = int(s.ScrolledText.index('end-1c').split('.')[0])
        # 计算行数最多右几位数,调整
        len_lines  = len(str(text_lines))
        s.line_text['width'] = len_lines + 2

        # 将显示行数文本的状态设置为正常
        s.line_text.configure(state="normal")
        # 删除行文本中的所有内容
        s.line_text.delete("1.0", "end")

        # 遍历文本数组,逐行插入到行文本中
        for i in range(1, text_lines + 1):
            if i == 1: s.line_text.insert("end", " "*(len_lines-len(str(i))+1) + str(i))
            else: s.line_text.insert("end", "\n"+" "*(len_lines-len(str(i))+1) + str(i))

        s.scroll('moveto', s.ScrolledText.vbar.get()[0]) # 模拟滚动条滚动事件

        s.line_text.configure(state="disabled") # 将行文本的状态设置为禁用

        s.KeyPress_scroll(row = 1) # 处理光标超过显示范围事件,否则行数会不同步

class Editor: # 主窗口
    def __init__(s):
        super().__init__()
        s.scrolledtext_list = {} # 存储scrolledtext窗口
        s.root = tk.Tk()
        s.save_images = (
            # 原色
            tk.PhotoImage('save_original', data = '''
                R0lGODdhDgAOAIU9ADFjpTFjrTFjtTFrtTljrTlrrcDAwEJzvUpzvUp7vVJ7rVJ7vVqEvWOEvWOMzmuU
                zmuU1nOUxnOc1nOc3nuUxnucxnul53ut54Slzoylxoyl1oyt1ozGY5StxpS13py13qWtxqW9563G57XO
                773O78bW78bW987e9zlrvQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
                AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAADgAOAAAIjAAzUFhwIIXBgykYgDAQ4cQJFBAjQiTh
                oEOCExdQOJA4kcSBAycsaOSIwkKJASlCkoQ4IYSAFCYsOHDwoOZMBxI+vIxpoWfPCRKC6kxRwqdPoEE9
                BEhBwqiFoFA3LG26EgUEDUtHiOTAtSuHBxiybvXK1UEFAAhEiKhagUIBCg0ODBBAN4DdAAAIKAgIADs=
                '''),
            # 黑白
            tk.PhotoImage('save_gray', data = '''
                R0lGODdhDgAOAIU9AFxcXF1dXV9fX2JiYmRkZGVlZW1tbW9vb3R0dHZ2dn5+foGBgYeHh46Ojo+Pj5CQ
                kJKSkpaWlpeXl6CgoKGhoaOjo6WlpaioqKmpqa2trbCwsLKysru7u8DAwMLCwsrKys3NzdTU1NXV1dzc
                3P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
                AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAADgAOAAAIiwApQEhgoIDBgwUUZOjwYMQIEhAjQgTB
                4AKCERZIMJA4EYQBAyMmaORIYkKIAQVCkoQogUOAAiImMGDQoOZMBhE2vIw5oWdPCRGC6iwQwqdPoEE1
                ACgAwuiEoFAvLG26koSDCks/iMTAtSuGBhOybvXKlYEEAAc8eKgqAQIBCAsMDAhAF4BduwIQBAQAOw==
                '''),
            # 红色
            tk.PhotoImage('save_red', data = '''
                R0lGODdhDgAOAIU9AP/h4f++vv+3t/+2tv+vr/+srP+kpP+iov+dnf+UlP+Skv+Pj/+Li/+Kiv+Hh/+F
                hf+Dg/+Cgv95ef94eP90dP9ycv9xcf9wcP9paf9jY/9gYP9YWP9WVv9RUf9PT/tHR/pGRvhERPVBQfM/
                P/I+PgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
                AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAADgAOAAAIiwAhUNjg4YPBgx80LDhQIUAAABAjQiSA
                oQGHAA4AYJA4kYAHDwEiaOQIIMKAEB9CkoQoAcGIDwIiYMBwoeZMDBMSvIwZoWdPCROC6vwwwKdPoEEV
                kPhAwGiEoFAbLG26EoCFB0sLiGTAtSuDCxGybvXKFYMEEh0MGKgqgQIIChk8hBhBl4RduyI4BAQAOw==
                '''),
            # 淡红色
            tk.PhotoImage('save_lowred', data = '''
                R0lGODdhDgAOAIU9AP/w8P/p6f/o6P/h4f/e3v/W1v/U1P/Pz//Gxv/ExP/Bwf+9vf+8vP+5uf+3
                t/+1tf+0tP+rq/+qqv+mpv+kpP+jo/+iov+bm/+Vlf+Skv+Kiv+IiP+Dg/+Bgft5efp4ePh2dvVzc/Nx
                cfJwcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
                AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAADgAOAAAIiwAhUNjg4YPBgx80LDhQIUAAABAjQiSA
                oQGHAA4AYJA4kYAHDwEiaOQIIMKAEB9CkoQoAcGIDwIiYMBwoeZMDBMSvIwZoWdPCROC6vwwwKdPoEEV
                kPhAwGiEoFAbLG26EoCFB0sLiGTAtSuDCxGybvXKFYMEEh0MGKgqgQIIChk8hBhBl4RduyI4BAQAOw==
                '''),
            )

    def close(s, event=None):
        "关闭窗口。"
        s.root.destroy()
        os._exit(0)

    def win_menu(s):
        def child_command(parent, item):
            for m in item.keys():
                if type(item[m]) == dict:
                    menu = tk.Menu(parent, tearoff=0)
                    parent.add_cascade(label = m, menu = menu)
                    child_command(menu, item[m])
                elif "separator" in m:
                    parent.add_separator()
                elif m == "历史记录":
                    for L in item[m]:
                        parent.add_command(label=L, command=None)
                else:
                    parent.add_command(label=m, command=item[m])

        menu_dict = {
            "文件(F)":{
                "新建":None,
                "打开":None,
                "打开所在文件夹":None,
                "使用默认查看器打开":None,
                "打开文件夹作为工作区":None,
                "重新读取文件":None,
                "保存":None,
                "另存为":None,
                "全部保存":None,
                "重命名":None,
                "关闭":None,
                "全部关闭":None,
                "更多关闭方式":{
                    "关闭当前以外所有文件":None,
                    "关闭左侧所有文件"  :None,
                    "关闭右侧所有文件"  :None,
                    "关闭所有未修改文件":None},
                "从磁盘删除":None,
                "separator1" :None,
                "读取会话"  :None,
                "保存会话"  :None,
                "separator2" :None,
                "历史记录":['1.aaaa.py','2.bbbb.py','3.cccc.py','4.dddd.py','5.eeee.py'],
                "separator3" :None,
                "恢复最近关闭文件":None,
                "打开文件列表":None,
                "清除文件列表":None,
                "separator":None,
                "退出":None,},
            "编辑(E)":{
                "撤销":None,
                "恢复":None,
                "separator1":None,
                "剪切":None,
                "复制":None,
                "粘贴":None,
                "删除":None,
                "全选":None,
                "开始/结束 选择":None,
                "separator2":None,
                "复制到剪切板":{
                    "复制当前文件路径":None,
                    "复制当前文件名"  :None,
                    "复制当前目录路径":None,},
                "缩进":{
                    "插入制表符(缩进)":None,
                    "删除制表符(退格)":None},
                "转换大小写":{
                    "转成大写":None,
                    "转成小写":None,
                    "每词转成仅首字母大写":None,
                    "每词的首字母转成大写":None,
                    "每句转成仅首字母大写":None,
                    "每句的首字母转成大写":None,
                    "大小写互换":None,
                    "随机大小写":None,},
                "行操作":{
                    "复制当前行":None,
                    "删除连续重复行":None,
                    "分割行":None,
                    "合并行":None,
                    "上移当前行":None,
                    "下移当前行":None,
                    "移除空行":None,
                    "移除空行(包括空白字符)":None,
                    "在当前行上方插入空行":None,
                    "在当前行下方插入空行":None,
                    "separator1":None,
                    "升序排列文本行":None,
                    "升序排列整数":None,
                    "升序排列小数(逗号作为小数点)":None,
                    "升序排列小数(句号作为小数点)":None,
                    "separator2":None,
                    "降序排列文本行":None,
                    "降序排列整数":None,
                    "降序排列小数(逗号作为小数点)":None,
                    "降序排列小数(句号作为小数点)":None,},
                "注释/取消注释":{
                    "添加/删除单行注释":None,
                    "设置行注释":None,
                    "取消行注释":None,
                    "区块注释":None,
                    "取消区块注释":None,},
                "空白字符操作":{
                    "移除行尾空格":None,
                    "移除行首空格":None,
                    "移除行首和行尾空格":None,
                    "EOL转空格":None,
                    "移除非必须的空白和EOL":None,
                    "separator1":None,
                    "TAB转空格":None,
                    "空格转TAB(全部)":None,
                    "空格转TAB(行首)":None,},
                "separator3":None,
                "历史剪切板":None,
                "设为只读":None,
                "清除只读标记":None,},
            "搜索(S)":{
                "查找":None,
                "在文件中查找":None,
                "查找下一个":None,
                "查找上一个":None,
                "选定并查找下一个":None,
                "选定并查找上一个":None,
                "快速查找下一个":None,
                "快速查找上一个":None,
                "替换":None,
                "增量查找":None,
                "寻找结果":None,
                "下一个寻找结果":None,
                "上一个寻找结果":None,
                "行定位":None,
                "转到匹配的括号":None,
                "选中所有匹配括号间字符":None,
                "标记":None,
                "separator1":None,
                "标记所有":{
                    "使用格式1":None,
                    "使用格式2":None,
                    "使用格式3":None,
                    "使用格式4":None,
                    "使用格式5":None,},
                "清除颜色标记":{
                    "清除格式1":None,
                    "清除格式2":None,
                    "清除格式3":None,
                    "清除格式4":None,
                    "清除格式5":None,
                    "清除所有格式":None,},
                "到上一个颜色标记":{
                    "格式1":None,
                    "格式2":None,
                    "格式3":None,
                    "格式4":None,
                    "格式5":None,
                    "寻找格式":None,},
                "到下一个颜色标记":{
                    "格式1":None,
                    "格式2":None,
                    "格式3":None,
                    "格式4":None,
                    "格式5":None,
                    "寻找格式":None,},
                "separator2":None,
                "书签":{
                    "设置/取消书签":None,
                    "上一书签":None,
                    "下一书签":None,
                    "清除所有书签":None,
                    "剪切书签行":None,
                    "复制书签行":None,
                    "粘贴(替换)书签行":None,
                    "删除书签行":None,
                    "删除未标记行":None,
                    "反向标记书签":None,},
                "separator3":None,
                "查找范围内字符":None,
                },
            "视图(V)":{
                "总在最前":None,
                "切换全屏模式":None,
                "便签模式":None,
                "separator1":None,
                "将当前文件显示到":None,
                "separator2":None,
                "显示符号":None,
                "缩放":None,
                "移动/复制当前文档":None,
                "标签页(Tab)":None,
                "自动换行":None,
                "激活从视图":None,
                "隐藏行":None,
                "separator3":None,
                "折叠所有层次":None,
                "展开所有层次":None,
                "折叠当前层次":None,
                "展开当前层次":None,
                "折叠层次":None,
                "展开层次":None,
                "separator4":None,
                "摘要...":None,
                "separator5":None,
                "工程":None,
                "文件夹工作区":None,
                "文档结构图":None,
                "函数列表":None,
                "separator6":None,
                "垂直同步滚动":None,
                "水平同步滚动":None,
                "separator7":None,
                "文字方向从右到左":None,
                "文字方向从左到右":None,
                "separator8":None,
                "监视日志 (tail -f)":None,},
            "编码(N)":{
                "使用 ANSI 编码":None,
                "使用 UTF-8 编码":None,
                "使用 UTF-8-BOM 编码":None,
                "使用 UCS-2 Big Endian 编码":None,
                "使用 UCS-2 Little Endian 编码":None,
                "编码字符集":None,
                "separator1":None,
                "转为 ANSI 编码":None,
                "转为 UTF-8 编码":None,
                "转为 UTF-8-BOM 编码":None,
                "转为 UCS-2 Big Endian 编码":None,
                "转为 UCS-2 Little Endian 编码":None,},
            "设置(T)":{
                "首选项...":None,
                "语言格式设置...":None,
                "管理快捷键...":None,
                "separator1":None,
                "导入":{
                    "导入插件":None,
                    "导入主题":None,},
                "separator2":None,
                "编辑弹出菜单":None,},
            "工具(O)":{
                "MD5":{
                    "生成...":None,
                    "从文件生成...":None,
                    "从选区生成并复制到剪切板":None,},
                "SHA-256":{
                    "生成...":None,
                    "从文件生成...":None,
                    "从选区生成并复制到剪切板":None,},},
            "运行(R)":{
                "运行":None,
                "管理快捷键":None,},
            "插件(P)":{
                "PyNpp":None,
                "MIME":None,
                "separator1":None,
                "插件管理":None,
                "separator2":None,
                "打开插件文件夹":None,},
            }

        menubar = tk.Menu(s.root)
        s.root.config(menu = menubar)
        child_command(menubar,menu_dict)

    def geticonimage(s, name):
        "根据名称获取图标文件,生成tkImage对象返回"
        try: return s.iconimages[name] # 如果存在同名图标,返回已经生成的tkImage对象
        except KeyError: pass
        file, ext = os.path.splitext(name) # 获取文件名和后缀
        ext = ext or ".gif" # 没有后缀的以".gif"为后缀
        fullname = os.path.join(icon_path, file + ext) # 连接文件路径
        image = tk.PhotoImage(master=s.canvas, file=fullname) # 生成tkImage对象
        s.iconimages[name] = image # 将tkImage对象缓存在s.iconimages
        return image

    def button_menu(s, button_frame):
        canvas = tk.Canvas(button_frame, height=24, highlightthickness=0)
        canvas.pack(anchor='w', fill='x')

        #绘制图片,贴图(这里的贴图必须是 全局 或者和 mainloop在同一个函数下,否则会被清除导致不显示)
        for i in range(16):
            canvas.create_image(10 + i*25, 12, anchor='w', image = ['save_original','save_gray','save_red','save_lowred'][i%4], tags = "button")

    def main(s):
        # 设置窗口大小和可调整性
        s.root.resizable(True, True)
        s.root.geometry("900x600")
        s.root.title("文本编辑器")
        s.root.protocol("WM_DELETE_WINDOW", s.close)
        s.root.focus_set()
        s.win_menu() # 顶部菜单

        button_frame = tk.Frame(s.root)
        button_frame.pack(anchor='w', side='top', fill='x')
        s.button_menu(button_frame)

        editor_frame = tk.Frame(s.root)
        editor_frame.pack(anchor='w', side='bottom', fill='x')

        s.editor_tab = CustomNotebook(editor_frame)
        #s.editor_tab = ttk.Notebook(editor_frame)   # 创建Notebook选项卡控件
        s.editor_tab.pack(expand = 1, fill = "both") # 让Notebook控件显示出来
        s.editor_tab.enable_traversal()              # 为s.editor_tab启用键盘快捷方式,Control-Tab向后切换,Shift-Control-Tab向前切换
        s.editor_tab.bind("<<NotebookTabClosed>>", s.EditorTabClosed)
        s.new_scrolledtext(title = 'new 1.py')
        s.new_scrolledtext(title = 'new 2.py', image = 'save_gray')
        s.new_scrolledtext(title = 'new 3.py', image = 'save_red')
        s.new_scrolledtext(title = 'new 4.py', image = 'save_lowred')

        s.root.mainloop()

    def EditorTabClosed(s, event=None):
        children = s.editor_tab.children # 所有标签信息,只要没有destroy,从创建开始的都可以查询到
        childstr = s.editor_tab.tabs()   # 所有的显示标签名称
        select = s.editor_tab.select()   # 选中要关闭的标签名称
        index  = s.editor_tab.index('current') # 选中要关闭的标签序号
        tab_dict = s.editor_tab.tab(index)     # 选中要关闭的标签信息
        select_frame = s.editor_tab.children[ select.split('.')[-1] ] # 选中标签页的frame对象
        data = {'所有标签信息': children,
                '所有标签名称': childstr,
                '选中标签信息': tab_dict,
                '选中标签名称': select,
                '选中标签序号': index ,
                '选中标签对象': select_frame,
                }
        #print (data)

        s.editor_tab.forget(index) # 删除选中标签页,但并未销毁,再一次add输入选项卡名称可以继续调用,要销毁需要用destroy(),
        # select_frame.destroy() # 销毁选中标签页的frame对象
        if len(childstr) == 1: s.new_scrolledtext(title = 'new 1.py') # 删除的是最后一个标签,则创建一个

    def new_scrolledtext(s, path = None, title = '', image = 'save_original'):
        new_tab = tk.Frame(s.editor_tab) # 添加tab选项卡
        s.editor_tab.add(new_tab, text = title, image = image, compound='left')

        new_Frame = tk.Frame(new_tab)
        new_Frame.pack(fill="both", expand=True)# 这里需要 fill="both", expand=True 否则窗口可能无法填充满

        rowscrolledtext = RowScrolledText(new_Frame, s.root)
        rowscrolledtext.see_line(0)

        ScrolledText = rowscrolledtext.ScrolledText
        text = ScrolledText.get(0.0, 'end')
        
if __name__ == "__main__":
    run = Editor()
    run.main()
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是一个使用 Python Tkinter 模块编写的多线程程序,每隔1秒在图形界面的文本框中刷新显示当前的系统时间。 ```python import threading import time import tkinter as tk class ClockThread(threading.Thread): def __init__(self, text_widget): super().__init__() self._stop_event = threading.Event() self.text_widget = text_widget def run(self): while not self._stop_event.is_set(): current_time = time.strftime("%H:%M:%S") self.text_widget.config(state="normal") self.text_widget.delete(1.0, tk.END) self.text_widget.insert(tk.END, current_time) self.text_widget.config(state="disabled") time.sleep(1) def stop(self): self._stop_event.set() class GUI(tk.Tk): def __init__(self): super().__init__() self.title("Clock") self.geometry("200x100") self.text_widget = tk.Text(self, state="disabled", height=1, font=("Arial", 24)) self.text_widget.pack(fill="both", expand=True) self.clock_thread = ClockThread(self.text_widget) self.clock_thread.start() def on_close(self): self.clock_thread.stop() self.destroy() if __name__ == "__main__": gui = GUI() gui.protocol("WM_DELETE_WINDOW", gui.on_close) gui.mainloop() ``` 在主程序中,创建一个名为 `ClockThread` 的自定义线程类,该类继承自 `threading.Thread` 类。在 `ClockThread` 类的构造函数中,设置一个 `Event` 对象 `_stop_event`,用于控制线程的启动和停止。还需要传入一个文本框控件 `text_widget`,用于在图形界面中显示时间。 在 `run` 方法中,使用 `time.strftime()` 函数获取当前时间,并将其格式化为字符串。然后,将文本框控件的状态设置为可写入,清空其中的文本内容,插入当前时间字符串,最后再将其状态设置为不可写入。在每次更新完文本框后,线程会休眠1秒钟。 在 `stop` 方法中,将 `_stop_event` 对象设置为已触发,以停止线程的运行。 在主程序中,创建一个名为 `GUI` 的自定义类,该类继承自 `tkinter.Tk` 类。在 `__init__` 方法中,创建一个文本框控件 `text_widget`,并设置其状态为不可写入。然后,创建一个 `ClockThread` 对象 `clock_thread`,并启动线程。最后,通过 `protocol` 方法为图形界面设置关闭事件处理函数。 在 `on_close` 方法中,调用 `stop` 方法停止线程,并销毁图形界面。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值