python tkinter.Text 高级用法 -- 设计功能齐全的文本编辑器

2023-10-26

众所周知, tkinterText文本框功能强大, Python自带的IDLE编辑器也是用tkinter编写的。这里作者也用tkinterText文本框等控件, 设计功能较齐全的文本编辑器程序。

目标功能:

效果图

编辑文本文件
编辑二进制文件
源代码见: pynotepad.py · GitCode 。代码较多, 在下文中将会逐渐讲解。

1.创建tkinter界面、菜单

在tkinter中, 应用大多使用类实现, 因此设计的Editor类是内置Tk类的继承。
以下是涉及到的tkinter基础知识:

一、Tk()对象

  • pack()方法
    参数side:指定将控件停靠在哪一条边上,LEFT、RIGHT、TOP或BOTTOM。
    参数expand, fill:让控件随父控件(这里是窗口)一起增大,可将expand设为True, fill设为BOTH。
  • protocol()方法
    Tk对象.protocol("协议名称",函数名)
    该方法用于设置窗口管理程序应用程序之间的协议, 最常用的是WM_DELETE_WINDOW协议, 用于窗口关闭按钮点击时执行什么函数。
    需要注意的是: WM_DELETE_WINDOW中函数调用完后, Tk窗口不会自动关闭, 需要在函数中手动调用Tk对象.destroy()关闭。

二、ScrolledText控件
ScrolledText控件继承了普通Text控件的所有功能, 而且增加了滚动条。要编辑ScrolledText控件的内容,可使用insert, delete方法。

  • ScrolledText控件的字体:
    字体属性默认为"<字体名> <大小> <样式>"的格式。需要注意的是: tkinter会将带空格的字体名称用{}括起来。
    要获取所有可用字体名称, 可用tkinter.font.families()函数。
    要设置或获取字体, 可使用text["font"]键。

三、ComboBox (组合框)控件

  • value属性
    该属性获取或设置组合框列表中所有的项, 可以是列表或元组。
  • textvariable属性
    该属性为一个StringVar()对象, 获取或设置组合框中的文字。
  • <<ComboboxSelected>>事件
    该事件用于bind()方法, 当组合框列表中的某一项被选中时触发。

四、Menu控件
方法add_command: 增加一个菜单项。
方法add_checkbutton: 增加一个单选的菜单项。
方法entryconfig: 配置菜单的某一项, 可用该方法设置菜单是否有效。

五、tkinter.messagebox
该模块中有askyesno等方法, 注意调用时, 需指定parent为一个Tk窗口实例。
(其它的Frame等控件, 就不一一介绍了。)

注意事项:
在tkinter中, 一切对象(Tk()除外)都有自己的masterparent, 表示对象属于哪个父对象(父窗口), 如: 代码btn=Button(root)中, root的作用就是master
由于程序支持多窗口, 在创建IntVar, StringVar, 和显示消息框, 调用(选择文件)对话框的时候, 一定要设置masterparent参数或属性, 否则会引起混乱。

import sys,os,pickle
from tkinter import *
from tkinter.scrolledtext import ScrolledText
import tkinter.ttk as ttk
import tkinter.messagebox as msgbox
import tkinter.filedialog as filediag
import tkinter.simpledialog as simpledialog
from tkinter.colorchooser import askcolor
from tkinter import font

# 以下为可选(非必需)的模块
import webbrowser
try:import windnd
except ImportError:windnd=None
try:import chardet
except ImportError:chardet=None

def handle(err,parent=None):
    # 用于处理错误
    # showinfo()中,parent参数指定消息框的父窗口
    msgbox.showinfo("错误",type(err).__name__+': '+str(err),parent=parent)
class Editor(Tk): # 继承Tk类
    TITLE="PyNotepad"
    encodings="ansi","utf-8","utf-16","utf-32","gbk","big5"
    # 判断是否有chardet库, 有就启用"自动"功能
    if chardet is not None:encodings=("自动",)+encodings
    ICON="notepad.ico" # 以下为常量, 用大写字母表示
    NORMAL_CODING="自动" if chardet is not None else "utf-8"
    FONTSIZES=8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 36, 48
    NORMAL_FONT='宋体'
    NORMAL_FONTSIZE=11
    TEXT_BG="SystemWindow";TEXT_FG="SystemWindowText" # 系统默认颜色
    FILETYPES=[("所有文件","*.*")]
    CONFIGFILE=os.getenv("userprofile")+"\\.pynotepad.pkl"
    AUTOWRAP=CHAR
    SHOW_STATUS=True

    instances=[]
    def __init__(self,filename=""):
        super().__init__()
        self.withdraw() # 暂时隐藏窗口,避免调用create_widgets()时窗口闪烁
        self.title(self.TITLE) # 初始化时预先显示标题
        self.bind("<Key>",self.window_onkey)
        self.protocol("WM_DELETE_WINDOW",self.ask_for_save) # 窗口关闭按钮点击时, 自动调用ask_for_save()方法

        self.isbinary=self.file_modified=False
        self.colorobj=self._codefilter=None
        self._dialogs={}
        Editor.instances.append(self)

        self.load_icon()
        self.loadconfig() # 加载配置, 需要"附: 配置 & 文件拖放功能"中的代码
        self.create_widgets()
        self.wm_deiconify();self.update() # wm_deiconfy恢复被隐藏的窗口
        if windnd:windnd.hook_dropfiles(self,func=self.onfiledrag);self.drag_files=[]
        self.filename=''
        if filename:
            self.load(filename)
        else:self.change_title() # 更改标题
    def load_icon(self): # 自动寻找图标
        for path in sys.path + [os.path.split(sys.executable)[0]]:# 后半部分用于Py2exe
            try:
                self.iconbitmap("{}\{}".format(path,self.ICON))
            except TclError:pass
            else:break
    def create_widgets(self):
        # 创建控件
        self.statusbar=Frame(self)
        if self.SHOW_STATUS:
            self.statusbar.pack(side=BOTTOM,fill=X)
        self.status=Label(self.statusbar,justify=RIGHT)
        self.status.pack(side=RIGHT)
        self.txt_decoded=ScrolledText(self.statusbar,
                        bg=self.TEXT_BG,fg=self.TEXT_FG,width=6,height=6)
        self.txt_decoded.insert('1.0',"在这里查看和编辑解码的数据")
        self.hexdata=ScrolledText(self.statusbar,
                        bg=self.TEXT_BG,fg=self.TEXT_FG,width=14,height=5)
        self.hexdata.insert('1.0',"在这里查看hex十六进制值")

        frame=Frame(self)
        frame.pack(side=TOP,fill=X)

        ttk.Button(frame,text='新建', command=self.new,width=7).pack(side=LEFT)
        ttk.Button(frame,text='打开', command=self.open,width=7).pack(side=LEFT)
        ttk.Button(frame,text='打开二进制文件',
                   command=self.open_as_binary,width=13).pack(side=LEFT)
        ttk.Button(frame,text='保存', command=self.save,width=7).pack(side=LEFT)

        Label(frame,text="编码:").pack(side=LEFT)
        self.coding=StringVar(self)
        self.coding.set(self.NORMAL_CODING)
        coding=ttk.Combobox(frame,textvariable=self.coding)
        def tip(event):
            self.msg['text']='重新打开或保存即可生效'
            self.msg.after(2500,clear)
        def clear():self.msg['text']=''
        coding.bind('<<ComboboxSelected>>',tip)
        coding["value"]=self.encodings
        coding.pack(side=LEFT)
        self.msg=Label(frame)
        self.msg.pack(side=LEFT)

        self.contents=ScrolledText(self,undo=True, width=75, height=24,
                        font = (self.NORMAL_FONT,self.NORMAL_FONTSIZE,"normal"),
                        wrap=self.AUTOWRAP, bg=self.TEXT_BG,fg=self.TEXT_FG)
        self.contents.pack(expand=True,fill=BOTH)
        self.contents.bind("<Key>",self.text_change)
        self.contents.bind("<B1-ButtonRelease>",self.update_status)
        order = self.contents.bindtags() # 修复无法获取选定的文本的bug
        self.contents.bindtags((order[1], order[0])+order[2:])
        self.update_offset()

        self.create_menu()
    def create_menu(self):
        menu=Menu(self)
        filemenu=Menu(self,tearoff=False)
        filemenu.add_command(label="新建",
                             command=self.new,accelerator="Ctrl+N")
        filemenu.add_command(label="新建二进制文件",command=self.new_binary)
        filemenu.add_command(label="打开",
                             command=self.open,accelerator="Ctrl+O")
        filemenu.add_command(label="打开二进制文件",command=self.open_as_binary)
        filemenu.add_command(label="保存",
                             command=self.save,accelerator="Ctrl+S")
        filemenu.add_command(label="另存为",command=self.save_as)
        filemenu.add_separator()
        filemenu.add_command(label="退出",command=self.ask_for_save)

        self.editmenu=Menu(self.contents,tearoff=False)
        master = self.contents
        self.editmenu.add_command(label="剪切  ",
                         command=lambda:self.text_change()\
                                ==master.event_generate("<<Cut>>"))
        self.editmenu.add_command(label="复制  ",
                         command=lambda:master.event_generate("<<Copy>>"))
        self.editmenu.add_command(label="粘贴  ",
                         command=lambda:self.text_change()\
                                ==master.event_generate("<<Paste>>"))
        self.editmenu.add_separator()
        self.editmenu.add_command(label="查找",accelerator="Ctrl+F",
                                  command=lambda:self.show_dialog(SearchDialog))
        self.editmenu.add_command(label="查找下一个",accelerator="F3",
                                  command=self.findnext)
        self.editmenu.add_command(label="替换",accelerator="Ctrl+H",
                                  command=lambda:self.show_dialog(ReplaceDialog))
        self.editmenu.add_separator()
        self.editmenu.add_command(label="插入十六进制数据",state=DISABLED,
                                  command=self.insert_hex)

        view=Menu(self.contents,tearoff=False)
        self.is_autowrap=IntVar(self.contents) # 是否自动换行
        self.is_autowrap.set(1 if self.AUTOWRAP!=NONE else 0)
        view.add_checkbutton(label="自动换行", command=self.set_wrap,
                             variable=self.is_autowrap)
        fontsize=Menu(self.contents,tearoff=False)
        fontsize.add_command(label="选择字体",
                             command=self.choose_font)
        fontsize.add_separator()
        fontsize.add_command(label="增大字体   ",accelerator='Ctrl+ "+"',
                             command=self.increase_font)
        fontsize.add_command(label="减小字体   ",accelerator='Ctrl+ "-"',
                             command=self.decrease_font)
        fontsize.add_separator()

        for i in range(len(self.FONTSIZES)):
            def resize(index=i):
                self.set_fontsize(index)
            fontsize.add_command(label=self.FONTSIZES[i],command=resize)

        self.contents.bind("<Button-3>",
                    lambda event:self.editmenu.post(event.x_root,event.y_root))
        view.add_cascade(label="字体",menu=fontsize)
        theme_menu=Menu(self,tearoff=False)
        theme_menu.add_command(label="选择前景色",command=self.select_fg)
        theme_menu.add_command(label="选择背景色",command=self.select_bg)
        theme_menu.add_command(label="重置",command=self.reset_theme)
        view.add_cascade(label="主题",menu=theme_menu)
        self._show_status=IntVar(self)
        self._show_status.set(1 if self.SHOW_STATUS else 0)
        view.add_checkbutton(label="显示状态栏",command=self.show_statusbar,
                         variable=self._show_status)

        helpmenu=Menu(self,tearoff=False)
        helpmenu.add_command(label="关于",command=self.about)
        helpmenu.add_command(label="反馈",command=self.feedback)

        menu.add_cascade(label="文件",menu=filemenu)
        menu.add_cascade(label="编辑",menu=self.editmenu)
        menu.add_cascade(label="查看",menu=view)
        menu.add_cascade(label="帮助",menu=helpmenu)

        # 创建弹出在self.txt_decoded和self.hexdata的菜单
        popup1=Menu(self.txt_decoded,tearoff=False)
        def _cut():
            self.txt_decoded.event_generate("<<Cut>>")
            self._edit_decoded_event()
        def _paste():
            self.txt_decoded.event_generate("<<Paste>>")
            self._edit_decoded_event()
        popup1.add_command(label="剪切",command=_cut)
        popup1.add_command(
            label="复制",command=lambda:self.txt_decoded.event_generate("<<Copy>>"))
        popup1.add_command(label="粘贴",command=_paste)

        popup2=Menu(self.hexdata,tearoff=False)
        popup2.add_command(
            label="复制",command=lambda:self.hexdata.event_generate("<<Copy>>"))

        self.txt_decoded.bind("<Button-3>",
                    lambda event:popup1.post(event.x_root,event.y_root))
        self.txt_decoded.bind("<Key>",self._edit_decoded_event)
        self.hexdata.bind("<Button-3>",
                    lambda event:popup2.post(event.x_root,event.y_root))

        # 显示菜单
        self.config(menu=menu)
    def create_binarytools(self): # 用于二进制文件
        if self.isbinary: # self.txt_decoded 用于显示解码的转义字符
            self.txt_decoded.pack(side=LEFT,expand=True,fill=BOTH)
            self.hexdata.pack(fill=Y) # hexdata 用于显示转义字符的十六进制
            self.status.pack_forget()
            self.status.pack(fill=X)
            self.editmenu.entryconfig(8,state=NORMAL)
        else: # 隐藏工具
            if self.txt_decoded:
                self.txt_decoded.pack_forget()
            if self.hexdata:
                self.hexdata.pack_forget()
            self.status.pack(side=RIGHT)
            self.editmenu.entryconfig(8,state=DISABLED) # 禁止插入

以下是部分控件事件的处理, 包含了实现快捷键、改变字体和显示/隐藏状态栏功能:

    def _get_fontname(self):
        font=' '.join(self.contents["font"].split(' ')[:-2])
        # tkinter会将带空格的字体名称用{}括起来
        if '{' in font:
            font = font[1:-1]
        return font
    def set_fontsize(self,index):
        newsize=self.FONTSIZES[index]
        fontname = self._get_fontname()
        self.contents["font"]=(fontname,newsize,"normal")
    def choose_font(self):
        def ok():
            self.contents["font"]=[opt.get()] + \
                       self.contents["font"].split(' ')[-2:] # 保留原先大小、样式
            dialog.destroy()
        dialog = Toplevel(self)
        dialog.title('选择字体')
        dialog.resizable(False,False)
        dialog.attributes('-toolwindow',True)
        opt = ttk.Combobox(dialog)
        # tkinter.font.families() 获取所有字体名称, 注意root参数
        opt['values']=sorted(font.families(root=self))
        opt.grid(row=0,column=0,columnspan=2,padx=15,pady=20)
        ttk.Button(dialog,text='确定',command=ok).grid(row=1,column=0)
        ttk.Button(dialog,text='取消',command=dialog.destroy).grid(row=1,column=1)
        oldfont = self._get_fontname()
        opt.set(oldfont)
        dialog.grab_set() # 对话框打开时, 不允许用户操作主窗口
        dialog.focus_force()
    def increase_font(self):
        # 增大字体
        fontsize=int(self.contents["font"].split(' ')[1])
        index=self.FONTSIZES.index(fontsize)+1
        if 0<=index<len(self.FONTSIZES): self.set_fontsize(index)
    def decrease_font(self):
        # 减小字体
        fontsize=int(self.contents["font"].split(' ')[1])
        index=self.FONTSIZES.index(fontsize)-1
        if 0<=index<len(self.FONTSIZES): self.set_fontsize(index)
    def set_wrap(self):
        if self.is_autowrap.get():
            self.contents['wrap'] = CHAR
        else:
            self.contents['wrap'] = NONE
        # 注意:由于tkinter会自动设置菜单复选框的变量, 所以不需要此行代码
##        self.is_autowrap.set(int(not self.is_autowrap.get()))
    def show_statusbar(self):
        if self._show_status.get():
            if self.isbinary:
                self.statusbar.pack(side=BOTTOM,fill=X)
            else:
                self.statusbar.pack(side=BOTTOM,fill=X)
        else:
            self.statusbar.pack_forget()
    def window_onkey(self,event):
        # 实现快捷键的部分
        # 如果按下Ctrl键
        if event.state in (4,6,12,14,36,38,44,46): # 适应多种按键情况(Num,Caps,Scroll)
            key=event.keysym.lower()
            if key=='o':#按下Ctrl+O键
                self.open()
            elif key=='s':#Ctrl+S键
                self.save()
            elif key=='n':
                self.new()
            elif key=='f':
                self.show_dialog(SearchDialog)
            elif key=='h':
                self.show_dialog(ReplaceDialog)
            elif key=='equal':#Ctrl+ "+" 增大字体
                self.increase_font()
            elif key=='minus':#Ctrl+ "-" 减小字体
                self.decrease_font()
        elif event.keysym.lower()=='f3':
            self.findnext()
        elif event.keycode == 93: # 按下了菜单键
            self.editmenu.post(self.winfo_x()+self.winfo_width(),
                               self.winfo_y()+self.winfo_height())
    def about(self):
        msgbox.showinfo("关于","版本: %s\n作者: %s"%(__version__, __author__), parent=self)

2.文本打开, 保存

打开文件有两种方法, 一种是在当前窗口中打开, 第二种是新建一个Editor实例, 在新窗口中打开。这里在新窗口中打开文件。

ask_for_save()的部分有一些复杂, 需要多次判断, 如判断用户是否取消操作、询问打开文件前是否需要保存等。
如: 用户在"是否保存"中选择了"是", 但在输入文件名的对话框中点击了取消, 应该如何处理?具体可看注释。

关于使用chardet库自动检测编码:
chardet.detect()函数返回一个字典, 包含检测结果。其中encoding键即为检测到的编码。

在之前的Editor类中加入以下代码:

    def new(self): # 新建一个Editor实例
        try:self.saveconfig() # 保存配置,使新的窗口加载修改后的配置
        except OSError:pass # 忽略写入文件可能产生的异常
        window=Editor()
        window.focus_force()
        return window
    def new_binary(self): # 创建新二进制文件
        try:self.saveconfig()
        except OSError:pass
        window=Editor()
        window.isbinary=True
        window.create_binarytools()
        window.change_title()
        window.change_mode()
        window.contents.edit_reset()
        window.focus_force()
        return window
    def open(self):
        #加载一个文件
        filename=filediag.askopenfilename(master=self,title='打开',
                            initialdir=os.path.split(self.filename)[0],
                            filetypes=self.FILETYPES)
        if not filename:return
        if not self.filename and not self.file_modified: # 如果是刚新建的, 在当前窗口中打开
            self.load(filename)
        else:self.new().load(filename)
    def open_as_binary(self):
        filename=filediag.askopenfilename(master=self,title='打开二进制文件',
                                initialdir=os.path.split(self.filename)[0],
                                filetypes=self.FILETYPES)
        if not filename:return
        if not self.filename and not self.file_modified: # 如果是刚新建的
            self.load(filename,binary=True)
        else:self.new().load(filename,binary=True)
    def load(self,filename,binary=False):
        # 加载文件
        self.isbinary=binary
        try:
            data=self._load_data(filename)
            if data==0:return
            self.filename=filename
            self.contents.delete('1.0', END)
            if self.isbinary:
                self.contents.insert(INSERT,data)
            else:
                for char in data:
                    try:
                        self.contents.insert(INSERT,char)
                    except TclError:self.contents.insert(INSERT,' ')
            self.contents.mark_set(INSERT,"1.0")
            self.create_binarytools()
            self.file_modified=False
            self.change_title()
            self.change_mode()
            self.contents.edit_reset() # 重置文本框的撤销功能
            self.contents.focus_force()
        except Exception as err:handle(err,parent=self)
    def _load_data(self,filename):
        # 从文件加载数据
        f=open(filename,"rb")
        if self.isbinary:
            data=to_escape_str(f.read())
            return data
        else:
            try:
                #读取文件,并对文件内容进行编码
                raw=f.read()
                if self.coding.get()=="自动":
                    # 调用chardet库
                    encoding=chardet.detect(raw[:100000])['encoding']
                    if encoding is None:
                        encoding='utf-8'
                    self.coding.set(encoding)
                data=str(raw,encoding=self.coding.get())
            except UnicodeDecodeError:
                f.seek(0)
                result=msgbox.askyesnocancel("PyNotepad","""%s编码无法解码此文件,
是否使用二进制模式打开?"""%self.coding.get(),parent=self)
                if result:
                    self.isbinary=True
                    data=to_escape_str(f.read())
                elif result is not None:
                    self.isbinary=False
                    data=str(f.read(),encoding=self.coding.get(),errors="replace")
                else:
                    return 0 # 表示取消
            return data
    def ask_for_save(self,quit=True):
        my_ret=None
        if self.file_modified:
            retval=msgbox.askyesnocancel("文件尚未保存",
                              "是否保存{}的更改?".format(
                                  os.path.split(self.filename)[1] or "当前文件")
                              ,parent=self)
            # retval 为 None表示取消, False为否, True为是
            if not retval is None: 
                if retval==True:
                    # 是
                    ret=self.save()
                    # 在保存对话框中取消
                    if ret==0:
                        my_ret=0;quit=False
                # 否
            else:
                # 取消
                my_ret=0;quit=False  # 0表示cancel
        if quit:
            Editor.windows.remove(self)
            try:self.saveconfig() # 保存配置, 见附
            except OSError:pass
            self.destroy() # tkinter不会自动关闭窗口, 需调用函数手动关闭
        return my_ret
    def save(self):
        #保存文件
        if not self.filename:
            self.filename=filediag.asksaveasfilename(master=self,
                    initialdir=os.path.split(self.filename)[0],
                    filetypes=self.FILETYPES)
        filename=self.filename
        if filename.strip():
            text=self.contents.get('1.0', END)[:-1] # [:-1]: 去除末尾换行符
            if self.isbinary:
                data=to_bytes(text)
            else:
                data=bytes(text,encoding=self.coding.get(),errors='replace')
                # Text文本框的bug:避免多余的\r换行符
                # 如:输入文字foobar, data中变成\rfoobar
                # -感谢文章末尾用户评论的反馈-
                data=data.replace(b'\r',b'')
            with open(filename, 'wb') as f:
                f.write(data)
            self.filename=filename
            self.file_modified=False
            self.change_title()
            self.change_mode()
        else:
            return 0 # 0表示cancel
    def save_as(self):
        filename=filediag.asksaveasfilename(master=self,
                    initialdir=os.path.split(self.filename)[0],
                    filetypes=self.FILETYPES)
        if filename: # 如果未选择取消
            self.filename=filename
            self.save()
    def change_title(self):
        file = os.path.split(self.filename)[1] or "未命名"
        newtitle="PyNotepad - "+ file +\
                  (" (二进制模式)" if self.isbinary else '')
        if self.file_modified:
            newtitle="*%s*"%newtitle
        self.title(newtitle)

3.文本编辑

其中, text_change()在文本被修改时调用, update_status()update_offset()用于更新状态栏中的数据。
在二进制模式中, update_status获取用户选择的文本, 更新解码的数据和十六进制值。update_offset更新偏移量。在文本模式中, update_offset更新当前的行数和列数。
在ScrolledText控件中,
获取选择的文本: text.get(SEL_FIRST,SEL_LAST)
获取当前光标位置: text.index(INSERT)
在之前的Editor类中加入以下代码:

    def text_change(self,event=None):
        self.file_modified=True
        self.update_status();self.change_title()
    def update_status(self,event=None):
        if not self._show_status.get():return
        if self.isbinary:
            # 用于二进制文件
            try:
                selected=self.contents.get(SEL_FIRST,SEL_LAST) # 获取从开头到光标处的文本
                raw=to_bytes(selected)
                coding=self.coding.get()
                # 调用chardet库
                if coding=="自动":
                    coding=chardet.detect(raw[:100000])['encoding']
                    if coding is None:coding='utf-8'
                try:text=str(raw,encoding=coding,
                             errors="backslashreplace")
                except TypeError:
                    # 修复Python 3.4中的bug: don't know how to handle
                    # UnicodeDecodeError in error callback
                    text=str(raw,encoding=coding,
                             errors="replace")
                except LookupError as err: # 未知编码
                    handle(err,parent=self);return
                self.txt_decoded.delete("1.0",END)
                self.txt_decoded.insert(INSERT,text)
                self.hexdata.delete("1.0",END)
                self.hexdata.insert(INSERT,view_hex(raw))
                self.status["text"]="选区长度: %d (Bytes)"%len(raw)
            except (TclError,SyntaxError): #忽略未选取内容, 或格式不正确
                self.txt_decoded.delete("1.0",END)
                self.hexdata.delete("1.0",END)
                self.update_offset()
        else:self.update_offset()
    def update_offset(self,event=None):
        if self.isbinary:
            prev=self.contents.get("1.0",INSERT) # 获取从开头到光标处的文本
            try:
                data=to_bytes(prev)
            except SyntaxError:
                sep='\\'
                prev=sep.join(prev.split(sep)[0:-1])
                try:data=to_bytes(prev)
                except SyntaxError:data=None
            if data is not None:
                self.status["text"]="偏移量: {} ({})"\
                                     .format(len(data),hex(len(data)))
        else:
            offset=self.contents.index(INSERT).split('.') # 不能用CURRENT
            self.status["text"]="Ln: {}  Col: {}".format(*offset)

4.编辑二进制文件

在Python中, bytes数据可用转义序列形式表示, 如\x00\x01\x02\n属于转义序列,
可通过repr(bytes)[2:-1],eval('b"""'+str+'"""') 实现转义序列与bytes类型的转换。
这次, 在Editor类外部加入以下代码:

def to_escape_str(byte):
    # 将字节(bytes)转换为转义字符串
    str='';length=1024
    for i in range(0,len(byte),length):
        str+=repr( byte[i: i+length] ) [2:-1]
        str+='\n'
    return str

def to_bytes(escape_str):
    # 将转义字符串转换为字节
    # -*****- 1.2.5版更新: 忽略二进制模式中文字的换行符
    escape_str=escape_str.replace('\n','')
    escape_str=escape_str.replace('"""','\\"\\"\\"') # 避免引号导致的SyntaxError
    escape_str=escape_str.replace("'''","\\'\\'\\'")
    try:
        return eval('b"""'+escape_str+'"""')
    except SyntaxError:
        return eval("b'''"+escape_str+"'''")

以下代码用于兼容WinHex等软件的十六进制数据, 使用bytes对象的fromhexhex方法。
知识点: bytes对象有fromhex()hex()方法, 可实现十六进制数据和bytes对象之间的相互转换fromhex()方法会忽略参数中的空格, 换行符等字符。
在Editor类外部加入以下代码:

def view_hex(byte):
    result=''
    for i in range(0,len(byte)):
        result+= byte[i:i+1].hex().zfill(2) + ' '
        if (i+1) % 4 == 0:result+='\n'
    return result

Editor类中加入以下代码:

    def insert_hex(self):
        hex = simpledialog.askstring('',
                    "输入WinHex十六进制数据(如:00 1a 3d ff) :",parent=self)
        if hex is None:return
        try:
            data=bytes.fromhex(hex)
            self.contents.insert('insert',to_escape_str(data))
        except Exception as err:
            handle(err,parent=self)
    # 以下代码用于直接在self.txt_decoded中编辑数据
    def _edit_decoded_event(self,event=None):
        self.after(20,self.edit_decoded) # 如果不使用after(),self.txt_decoded.get不会返回最新的值
    def edit_decoded(self):
        range_=self.contents.tag_ranges(SEL) # 获取选区
        if range_:
            start,end=range_[0].string,range_[1].string # 转换为字符串
        else:start=self.contents.index(INSERT);end=None
        try:
            coding=self.coding.get()
            if coding=="自动":
                msgbox.showinfo('','不支持自动编码, 请选择或输入其他编码',parent=self)
                return
            byte = self.txt_decoded.get('1.0',END)[:-1].encode(coding)
            esc_char = to_escape_str(byte,linesep=False)
            self.file_modified=True;self.change_title()
            if range_:
                self.contents.delete(start,end)
            self.contents.insert(start,esc_char)
            end = '%s+%dc'%(start, len(esc_char))
            self.contents.tag_add(SEL,start,end)
        except Exception as err:handle(err,parent=self)

5.查找、替换对话框

这里主要用到文本框Text的search方法,
该函数接收2个必选参数, 分别是pattern和index, index为起始索引, search 方法返回起始索引处或之后的第一个匹配项的索引。
search方法还有一些可选参数:
regexp: 是否使用正则表达式查找 (比自己编写代码查找要快)。
nocase: 是否不区分大小写。

显示对话框时, 还需调用Tk,Toplevel的focus方法, 用于使对象获得焦点。
注意: 创建IntVar(), StringVar()时, 需指定参数master, 避免创建的变量无法使用。
* 使对话框跟随父窗口最小化、恢复显示的方法: 调用transient()方法。

Editor类外部加入以下代码:

class SearchDialog(Toplevel):
    #查找对话框
    def __init__(self,master):
        self.master=master
        self.coding=self.master.coding.get()
    def init_window(self,title="查找"):
        Toplevel.__init__(self,self.master)
        self.title(title)
        self.attributes("-toolwindow",True)
        self.attributes("-topmost",True)
        # 当父窗口隐藏后,窗口也跟随父窗口隐藏
        self.transient(self.master)
        self.wm_protocol("WM_DELETE_WINDOW",self.onquit)
    def show(self):
        self.init_window()
        frame=Frame(self)
        ttk.Button(frame,text="查找下一个",command=self.search).pack()
        ttk.Button(frame,text="退出",command=self.onquit).pack()
        frame.pack(side=RIGHT,fill=Y)
        inputbox=Frame(self)
        Label(inputbox,text="查找内容:").pack(side=LEFT)
        self.keyword=StringVar(self.master)
        keyword=ttk.Entry(inputbox,textvariable=self.keyword)
        keyword.pack(side=LEFT,expand=True,fill=X)
        keyword.bind("<Key-Return>",self.search)
        keyword.focus_force()
        inputbox.pack(fill=X)
        options=Frame(self)
        self.create_options(options)
        options.pack(fill=X)
    def create_options(self,master):
        Label(master,text="选项: ").pack(side=LEFT)
        self.use_regexpr=IntVar(self.master)
        ttk.Checkbutton(master,text="使用正则表达式",variable=self.use_regexpr)\
        .pack(side=LEFT)
        self.match_case=IntVar(self.master)
        ttk.Checkbutton(master,text="区分大小写",variable=self.match_case)\
        .pack(side=LEFT)
        self.use_escape_char=IntVar(self.master)
        self.use_escape_char.set(self.master.isbinary)
        ttk.Checkbutton(master,text="使用转义字符",variable=self.use_escape_char)\
        .pack(side=LEFT)

    def search(self,event=None,mark=True,bell=True):
        text=self.master.contents
        key=self.keyword.get()
        if not key:return
        # 验证用户输入是否正常
        if self.use_escape_char.get():
            try:key=str(to_bytes(key),encoding=self.coding)
            except Exception as err:
                handle(err,parent=self);return
        if self.use_regexpr.get():
            try:re.compile(key)
            except re.error as err:
                handle(err,parent=self);return
        # 默认从当前光标位置开始查找
        pos=text.search(key,INSERT,'end-1c',# end-1c:忽略末尾换行符
                        regexp=self.use_regexpr.get(),
                        nocase=not self.match_case.get())
        if not pos:
            # 尝试从开头循环查找
            pos=text.search(key,'1.0','end-1c',
                        regexp=self.use_regexpr.get(),
                        nocase=not self.match_case.get())
        if pos:
            if self.use_regexpr.get(): # 获取正则表达式匹配的字符串长度
                text_after = text.get(pos,END)
                flag = re.IGNORECASE if not self.match_case.get() else 0
                length = re.match(key,text_after,flag).span()[1]
            else:
                length = len(key)
            newpos="%s+%dc"%(pos,length)
            text.mark_set(INSERT,newpos)
            if mark:self.mark_text(pos,newpos)
            return pos,newpos
        elif bell: # 未找到,返回None
            bell_(widget=self)
    def findnext(self,cursor_pos='end',mark=True,bell=True):
        # cursor_pos:标记文本后将光标放在找到文本开头还是末尾
        # 因为search()默认从当前光标位置开始查找
        # end 用于查找下一个操作, start 用于替换操作
        result=self.search(mark=mark,bell=bell)
        if not result:return
        if cursor_pos=='end':
            self.master.contents.mark_set('insert',result[1])
        elif cursor_pos=='start':
            self.master.contents.mark_set('insert',result[0])
        return result
    def mark_text(self,start_pos,end_pos):
        text=self.master.contents
        text.tag_remove("sel","1.0",END) # 移除旧的tag
        # 已知问题: 代码高亮显示时, 无法突出显示找到的文字
        text.tag_add("sel", start_pos,end_pos) # 添加新的tag 
        lines=text.get('1.0',END)[:-1].count(os.linesep) + 1
        lineno=int(start_pos.split('.')[0])
         # 滚动文本框, 使被找到的内容显示 ( 由于只判断行数, 已知有bug); 另外, text['height']不会随文本框缩放而变化
        text.yview('moveto', str((lineno-text['height'])/lines))
        text.focus_force()
        self.master.update_status()
    def onquit(self):
        self.withdraw()

class ReplaceDialog(SearchDialog):
    #替换对话框
    def show(self):
        self.init_window(title="替换")
        frame=Frame(self)
        ttk.Button(frame,text="查找下一个", command=self._findnext).pack()
        ttk.Button(frame,text="替换", command=self.replace).pack()
        ttk.Button(frame,text="全部替换", command=self.replace_all).pack()
        ttk.Button(frame,text="退出", command=self.onquit).pack()
        frame.pack(side=RIGHT,fill=Y)

        inputbox=Frame(self)
        Label(inputbox,text="查找内容:").pack(side=LEFT)
        self.keyword=StringVar(self.master)
        keyword=ttk.Entry(inputbox,textvariable=self.keyword)
        keyword.pack(side=LEFT,expand=True,fill=X)
        keyword.focus_force()
        inputbox.pack(fill=X)

        replace=Frame(self)
        Label(replace,text="替换为:  ").pack(side=LEFT)
        self.text_to_replace=StringVar(self.master)
        replace_text=ttk.Entry(replace,textvariable=self.text_to_replace)
        replace_text.pack(side=LEFT,expand=True,fill=X)
        replace_text.bind("<Key-Return>",self.replace)
        replace.pack(fill=X)

        options=Frame(self)
        self.create_options(options)
        options.pack(fill=X)

    def _findnext(self):# 仅用于"查找下一个"按钮功能
        text=self.master.contents
        sel_range=text.tag_ranges('sel') # 获得选区的起点和终点
        if sel_range:
            selectarea = sel_range[0].string, sel_range[1].string
            result = self.findnext('start')
            if result is None:return
            if result[0] == selectarea[0]: # 若仍停留在原位置
                text.mark_set('insert',result[1])# 从选区终点继续查找
                self.findnext('start')
        else:
            self.findnext('start')
    def replace(self,bell=True,mark=True):
        text=self.master.contents
        result=self.search(mark=False,bell=bell)
        if not result:return # 标志已无文本可替换
        self.master.text_change()
        pos,newpos=result
        newtext=self.text_to_replace.get()
        try:
            if self.use_escape_char.get():
                newtext=to_bytes(newtext).decode(self.master.coding.get())
            if self.use_regexpr.get():
                old=text.get(pos,newpos)
                newtext=re.sub(self.keyword.get(),newtext,old)
        except Exception as err:
            handle(err,parent=self);return
        text.delete(pos,newpos)
        text.insert(pos,newtext)
        end_pos="%s+%dc"%(pos,len(newtext))
        if mark:self.mark_text(pos,end_pos)
        return pos,end_pos
    def replace_all(self):
        self.master.contents.mark_set("insert","1.0") # 将光标移到开头
        flag=False # 标志是否已有文字被替换

        # 以下代码会导致无限替换, 使程序卡死, 新的代码修复了该bug
        #while self.replace(bell=False)!=-1:
        #    flag=True
        last = (0,0)
        while True:
            result=self.replace(bell=False,mark=False)
            if result is None:break
            flag = True
            result = self.findnext('start',bell=False,mark=False)
            if result is None:return
            ln,col = result[0].split('.')
            ln = int(ln);col = int(col)
            # 判断新的偏移量是增加还是减小
            if ln < last[0] or (ln==last[0] and col<last[1]):
                self.mark_text(*result) # 已完成一轮替换
                break
            last=ln,col
        if not flag:bell_()

Editor类内部加入以下代码:

    def findnext(self):
        fd = self._dialogs.get(SearchDialog,None)
        if fd:
            if fd.findnext():return
        rd = self._dialogs.get(ReplaceDialog,None)
        if rd:
            rd.findnext()
    def show_dialog(self,dialog_type):
        # dialog_type是对话框的类型
        if dialog_type in self._dialogs:
            # 不再显示新的对话框
            d=self._dialogs[dialog_type]
            d.state('normal') # 恢复隐藏的窗口
            d.focus_force()
        else:
            d = dialog_type(self);d.show()
            self._dialogs[dialog_type] = d

6.代码高亮显示

有点复杂, 该部分参考了turtledemo模块的源码。程序调用了IDLE内置的代码高亮显示组件。
在文件开头加入:

try:
    from idlelib.colorizer import ColorDelegator
    from idlelib.percolator import Percolator
except ImportError: # 可能未安装IDLE
    ColorDelegator=Percolator=None

Editor类中加入以下代码:

    def change_mode(self):
        if ColorDelegator:
            if self.filename.lower().endswith((".py",".pyw"))\
                   and (not self.isbinary):
                   # 设置代码高亮显示
                self._codefilter=ColorDelegator()
                if not self.colorobj:
                    self.colorobj=Percolator(self.contents)
                self.colorobj.insertfilter(self._codefilter)
                self.set_tag_bg()
            elif self.colorobj and self._codefilter.delegate:
                # 取消代码高亮显示
                self.colorobj.removefilter(self._codefilter)

7.选择文本框主题颜色

设置文本框颜色, 主要用到文本框的"bg"(背景色),"fg"(前景色)属性键。
选择颜色, 使用tkinter.colorchooser中的askcolor函数, 其中color参数指定默认的颜色。
Editor类中加入如下代码:

    def select_fg(self):
        self.contents["fg"]=self.txt_decoded["fg"]\
                    =self.hexdata["fg"] = askcolor(parent=self,
                                                   color=self.contents["fg"])[1]
    def select_bg(self):
        self.contents["bg"]=self.txt_decoded["bg"]\
                    =self.hexdata["bg"] = askcolor(parent=self,
                                                   color=self.contents["bg"])[1]
        self.set_tag_bg()
    def reset_theme(self):
        self.contents["bg"]=self.txt_decoded["bg"]\
                    =self.hexdata["bg"] = "SystemWindow"
        self.contents["fg"]=self.txt_decoded["fg"]\
                    =self.hexdata["fg"] = "SystemWindowText"
        self.set_tag_bg()

我们知道, 在代码高亮显示中, ColorDelegator会创建tag, 且tag默认是白色的。因此需要设置tag的背景色, 使其与文本框背景色匹配。

    def set_tag_bg(self):
        for tag in self.contents.tag_names(): # tag_names()获取所有的tag名称
            if tag.lower() != "sel":
                self.contents.tag_config(tag, background=self.contents["bg"])

8.完成

在文件末尾加入:

def main(): 
    # 初始化Editor实例
    if len(sys.argv)>1:# 检测程序启动参数 sys.argv
        for arg in sys.argv[1:]:
            try:
                Editor(arg)
            except OSError:pass
    else: Editor()
    mainloop()

__author__="qfcy"
__version__="1.3.4"
if __name__=="__main__":main()

到这里, 你已经基本完成了文本编辑器的制作。
如果你不想复制代码,请看这里: ​pynotepad.py · qfcy_ / Python · GitCode

9.附: 配置 & 文件拖放功能

配置功能使用pickle模块, 保存和读取数据。

class Editor(Tk):
   # --snip-- (略)
   CONFIGFILE=os.getenv("userprofile")+"\.pynotepad.pkl"
   # --snip--
    def loadconfig(self):
        try:
            with open(self.CONFIGFILE,'rb') as f:
                cfg=pickle.load(f)
                for key in cfg:
                    setattr(Editor,key,cfg[key]) # 设置Editor类的各个属性
        except OSError:
            pass
    # bug修复:未安装chardet时编码设为"自动"的情况
        if Editor.NORMAL_CODING=="自动" and not chardet:
            Editor.NORMAL_CODING="utf-8"
    def saveconfig(self):
        font=self.contents['font'].split(' ')
        cfg={'NORMAL_CODING':self.coding.get(),
             'NORMAL_FONT': self._get_fontname(),
             'NORMAL_FONTSIZE': int(font[-2]),
             'AUTOWRAP': self.contents['wrap'],
             'TEXT_BG':self.contents["bg"],
             'TEXT_FG':self.contents["fg"],
             "SHOW_STATUS":bool(self._show_status.get())}
        with open(self.CONFIGFILE,'wb') as f:
            pickle.dump(cfg,f)

文件拖放功能使用了windnd模块, 具体可参考: tk windnd 实现文件拖放到窗口
Editor类中加入:

    def __init__(self, filename=""):
        # --snip--
        if windnd:windnd.hook_dropfiles(self,func=self.onfiledrag)
        self.drag_files=[]
    def onfiledrag(self,files):
        self.drag_files=files
        self.after(50,self.onfiledrag2)
    def onfiledrag2(self):
        self.saveconfig()
        if not self.filename and not self.file_modified: # 如果刚新建窗口
            # 注意windnd的文件名为二进制, 需要被解码
            self.load(self.drag_files[0].decode('ansi'))
            del self.drag_files[0]
        for item in self.drag_files:
            Editor(item.decode('ansi'))
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

python tkinter.Text 高级用法 -- 设计功能齐全的文本编辑器 的相关文章

  • 与区域指示符字符类匹配的 python 正则表达式

    我在 Mac 上使用 python 2 7 10 表情符号中的标志由一对表示区域指示符号 https en wikipedia org wiki Regional Indicator Symbol 我想编写一个 python 正则表达式来在
  • 安装了 32 位的 Python,显示为 64 位

    我需要运行 32 位版本的 Python 我认为这就是我在我的机器上运行的 因为这是我下载的安装程序 当我重新运行安装程序时 它会将当前安装的 Python 版本称为 Python 3 5 32 位 然而当我跑步时platform arch
  • Python 中的舍入浮点问题

    我遇到了 np round np around 的问题 它没有正确舍入 我无法包含代码 因为当我手动设置值 而不是使用我的数据 时 返回有效 但这是输出 In 177 a Out 177 0 0099999998 In 178 np rou
  • 使用 Python 从文本中删除非英语单词

    我正在 python 上进行数据清理练习 我正在清理的文本包含我想删除的意大利语单词 我一直在网上搜索是否可以使用像 nltk 这样的工具包在 Python 上执行此操作 例如给出一些文本 Io andiamo to the beach w
  • 使用Python请求登录Google帐户

    在多个登录页面上 需要谷歌登录才能继续 我想用requestspython 中的库以便让我自己登录 通常这很容易使用requests库 但是我无法让它工作 我不确定这是否是由于 Google 做出的一些限制 也许我需要使用他们的 API 或
  • datetime.datetime.now() 返回旧值

    我正在通过匹配日期查找 python 中的数据存储条目 我想要的是每天选择 今天 的条目 但由于某种原因 当我将代码上传到 gae 服务器时 它只能工作一天 第二天它仍然返回相同的值 例如当我上传代码并在 07 01 2014 执行它时 它
  • 为什么 PyYAML 花费这么多时间来解析 YAML 文件?

    我正在解析一个大约 6500 行的 YAML 文件 格式如下 foo1 bar1 blah name john age 123 metadata whatever1 whatever whatever2 whatever stuff thi
  • 在 Sphinx 文档中*仅*显示文档字符串?

    Sphinx有一个功能叫做automethod从方法的文档字符串中提取文档并将其嵌入到文档中 但它不仅嵌入了文档字符串 还嵌入了方法签名 名称 参数 我如何嵌入only文档字符串 不包括方法签名 ref http www sphinx do
  • Numpy - 根据表示一维的坐标向量的条件替换数组中的值

    我有一个data多维数组 最后一个是距离 另一方面 我有距离向量r 例如 Data np ones 20 30 100 r np linspace 10 50 100 最后 我还有一个临界距离值列表 称为r0 使得 r0 shape Dat
  • Cython 和类的构造函数

    我对 Cython 使用默认构造函数有疑问 我的 C 类 Node 如下 Node h class Node public Node std cerr lt lt calling no arg constructor lt lt std e
  • pip 列出活动 virtualenv 中的全局包

    将 pip 从 1 4 x 升级到 1 5 后pip freeze输出我的全局安装 系统 软件包的列表 而不是我的 virtualenv 中安装的软件包的列表 我尝试再次降级到 1 4 但这并不能解决我的问题 这有点类似于这个问题 http
  • 如何使用原始 SQL 查询实现搜索功能

    我正在创建一个由 CS50 的网络系列指导的应用程序 这要求我仅使用原始 SQL 查询而不是 ORM 我正在尝试创建一个搜索功能 用户可以在其中查找存储在数据库中的书籍列表 我希望他们能够查询 书籍 表中的 ISBN 标题 作者列 目前 它
  • 如何断言 Unittest 上的可迭代对象不为空?

    向服务提交查询后 我会收到一本字典或一个列表 我想确保它不为空 我使用Python 2 7 我很惊讶没有任何assertEmpty方法为unittest TestCase类实例 现有的替代方案看起来并不正确 self assertTrue
  • python import inside函数隐藏现有变量

    我在我正在处理的多子模块项目中遇到了一个奇怪的 UnboundLocalError 分配之前引用的局部变量 问题 并将其精简为这个片段 使用标准库中的日志记录模块 import logging def foo logging info fo
  • 将 Python 中的日期与日期时间进行比较

    所以我有一个日期列表 datetime date 2013 7 9 datetime date 2013 7 12 datetime date 2013 7 15 datetime date 2013 7 18 datetime date
  • Scipy Sparse:SciPy/NumPy 更新后出现奇异矩阵警告

    我的问题是由大型电阻器系统的节点分析产生的 我基本上是在设置一个大的稀疏矩阵A 我的解向量b 我正在尝试求解线性方程A x b 为了做到这一点 我正在使用scipy sparse linalg spsolve method 直到最近 一切都
  • 如何应用一个函数 n 次? [关闭]

    Closed 这个问题需要细节或清晰度 help closed questions 目前不接受答案 假设我有一个函数 它接受一个参数并返回相同类型的结果 def increment x return x 1 如何制作高阶函数repeat可以
  • 如何计算Python中字典中最常见的前10个值

    我对 python 和一般编程都很陌生 所以请友善 我正在尝试分析包含音乐信息的 csv 文件并返回最常听的前 n 个乐队 从下面的代码中 每听一首歌曲都是一个列表中的字典条目 格式如下 album Exile on Main Street
  • cv2.VideoWriter:请求一个元组作为 Size 参数,然后拒绝它

    我正在使用 OpenCV 4 0 和 Python 3 7 创建延时视频 构造 VideoWriter 对象时 文档表示 Size 参数应该是一个元组 当我给它一个元组时 它拒绝它 当我尝试用其他东西替换它时 它不会接受它 因为它说参数不是
  • 使用 z = f(x, y) 形式的 B 样条方法来拟合 z = f(x)

    作为一个潜在的解决方案这个问题 https stackoverflow com questions 76476327 how to avoid creating many binary switching variables in gekk

随机推荐

  • 一键端2

    新仙剑ol 链接 http pan baidu com s 1i3xZHLb 密码 hyly 链接 http pan baidu com s 1c0HgGKw 密码 druj 本帖隐藏的内容 龙之谷133服务端 链接 http pan ba
  • 索琦c语言程序设计第二版第七章,C语言程序设计自学考试大纲(2000年7月版).doc...

    天津市高等教育自学考试课程考试大纲 课程名称 应用程序基础及设计 课程代码 1115 4874 第一部分 课程性质与设置目的 一 课程性质与特点 应用程序基础及设计 课程是高等教育自学考试电子信息工程专业的必修课 是该专业一门重要的基础课
  • Qt实现TCP客户端和服务器通讯程序

    复习的心态过一遍之前基础的一些东西 Qt封装了QTcpServer和QTcpSocket两个类 其中QTcpServer继承自QObject 通过listen 函数监听传入的客户端连接 当Client连接上时 QTcpServer会发出ne
  • 使 QComboBox 下拉一个带复选框的树形列表

    背景 在项目开发过程中需要使 QComboBox 下拉一个树形列表 直接通过 setModel 和 setView 设置 combox 控件可以实现 但是在单击节点箭头按钮时也会隐藏下拉框的显示 因此需要重新实现 QComboBox 的方法
  • 解决vue 路由传参 页面刷新或者后退参数丢失的问题

    vue路由传递参数如果用params传递参数 那么页面刷新就会丢失数据 可以改用query来传递参数 这样刷新就不会丢失 this router push path business bizInspectionTaskSub query f
  • Db2 v11.1 Upgrade to V11.5 HADR 模式

    https www ibm com docs en db2 11 5 topic methods universal versus product specific fix packs Universal versus product sp
  • RocketMQ重置消费位点源码分析

    这里是weihubeats 觉得文章不错可以关注公众号小奏技术 文章首发 拒绝营销号 拒绝标题党 背景 最近在使用RocketMQ的重置消费位点的时候经常出现报错 所以就打算研究下RocketMQ是如何重置消费者的消费位点的 RocketM
  • 图像形态学处理(膨胀腐蚀开闭运算)——数字图像处理学习八(C++版)

    一 基本概念 1 形态学的基本思想是利用一种特殊的结构元来测量或提取输入图像中相应的形状或特征 以便进一步进行图像分析和目标识别 2 图像的形态学处理是对二值图像进行处理 所以在形态学处理前需要将图像二值化 3 结构元可以是任意形状 结构元
  • 可信执行环境(TEE)技术介绍(Trusted Execution Environment)

    本文对当前流行的移动终端TEE技术做简要概述 并对一些细节展开讨论 1 当前移动安全背景 当前移动终端面临这严重的安全威胁 威胁点如下图所示 因此移动厂商 用户 服务提供商等各方都对移动安全提出了强烈的需求 2 REE介绍 Rich Exe
  • 线程安全

    线程安全 多线程的执行顺序不可重现 但是必须要求执行结果必须可以重现 线程的共享数据操作不完整性就一定会出现数据被破坏 而导致结果无法预知的问题 线程的安全问题 同步处理的引入 在java语言中存在两种内建的synchronized语法 s
  • ng-model

    ng model指令用来将input select text area或自定义表单控件同包含它们的作用域中的属性进行绑定 它可以提供并处理表单验证功能 在元素上设置相关的CSS类 ng valid ng invalid等 并负责在父表单中注
  • 【解决】2021-07-30Caused by: com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException:

    Caused by org springframework beans factory BeanCreationException Error creating bean with name sqlTemplate defined in U
  • python实现向图像随机添加高斯白噪声,并修改尺寸

    基于python向图像随机添加高斯噪声 并修改尺寸 噪声分布设置为 均值为0 方差分布在0 50之间 coding utf 8 Created on Nov Mon 29 14 09 45 2021 author 瀛台夜雪 import o
  • 【opencv】基于opencv实现人脸识别,从环境搭建到代码实现(超详细教程)

    前言 目标 让计算机通过训练做到认识我或者检测出视频中的人是谁 本文是一个学习笔记 记录一下自己的实现过程 在实现过程中遇到的问题以及个人对知识的理解 一 环境配置 1 软件安装 首先先安装一下必须的软件 python pycharm op
  • 通俗易懂的java设计模式(6)-代理模式

    1 什么是代理模式 为某个对象提供一个代理对象 通过这个代理对象 可以控制对原对象的访问 通俗的解释 我们电脑桌面上的一个个快接方式 当我们点击这个快捷方式的时候 我们就间接地访问到了这个程序 2 静态代理 何为静态 即在程序运行之前 代理
  • java后端解决重复提交问题

    一 为什么会出现重复提交 主要是由于网络的延迟问题以及页面刷新的操作 二 表单的重复提交会导致的问题 主要能够造成很多脏数据 三 解决的办法 3 1 前端解决办法 通过前端的方法将提交按钮变灰 对于前端的办法这里就不做演示了 因为前端的控制
  • C++11标准中按值传递类对象参数的使用时机

    严正声明 本文系作者davidhopper原创 未经许可 不得转载 作为一名资深C 程序员 在C 98 03标准时代 一直将 不得按值传递类对象参数 的规定奉为圭臬 例如 void SetParam const std string nam
  • 自定义实现Java ListNode链表

    写在前面 今天写代码的时候 发现我居然被Java的ListNode的输入卡了半天 所以就打算写一篇博客来整理一下ListNode 链表的定义 首先ListNode就是链表 它的基本结构如下 及一个包含数据 和指针的结构 我们将一个节点的指针
  • Unity3D游戏编程——离散仿真作业

    Unity3D游戏编程 离散仿真作业 1 简答题asd 1 1解释游戏对象 GameObjects 和资源 Assets 的区别与联系 答 游戏对象 GameObjects 是一系列资源的组成序列 资源 Assets 是一系列可以被Unit
  • python tkinter.Text 高级用法 -- 设计功能齐全的文本编辑器

    众所周知 tkinter的Text文本框功能强大 Python自带的IDLE编辑器也是用tkinter编写的 这里作者也用tkinter的Text文本框等控件 设计功能较齐全的文本编辑器程序 目标功能 编辑文本文件 编辑二进制文件 字符会以