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

目标功能:

  • 编辑文本文件
  • 编辑二进制文件 (字符会以转义序列形式显示, 如’abc\xff’)
  • 支持ansi、gbk、utf-8等编码, 支持自动检测文件编码
  • 支持查找、替换
  • 支持撤销、重做
  • 支持自由选择主题、改变字体
  • 编辑python代码文件时, 支持代码高亮显示, 类似IDLE。

    目录

    • 效果图
    • 1.创建tkinter界面、菜单
    • 2.文本打开, 保存
    • 3.文本编辑
    • 4.编辑二进制文件
    • 5.查找、替换对话框
    • 6.代码高亮显示
    • 7.选择文本框主题颜色
    • 8.完成
    • 9.附: 配置 & 文件拖放功能

效果图



源代码见: 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=Nonedef 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=("自动",)+encodingsICON="notepad.ico" # 以下为常量, 用大写字母表示NORMAL_CODING="自动" if chardet is not None else "utf-8"FONTSIZES=8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 36, 48NORMAL_FONT='宋体'NORMAL_FONTSIZE=11TEXT_BG="SystemWindow";TEXT_FG="SystemWindowText" # 系统默认颜色FILETYPES=[("所有文件","*.*")]CONFIGFILE=os.getenv("userprofile")+"\\.pynotepad.pkl"AUTOWRAP=CHARSHOW_STATUS=Trueinstances=[]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=Falseself.colorobj=self._codefilter=Noneself._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]]:# 后半部分用于Py2exetry:self.iconbitmap("{}\{}".format(path,self.ICON))except TclError:passelse:breakdef 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.encodingscoding.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() # 修复无法获取选定的文本的bugself.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.contentsself.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 fontdef 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)+1if 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)-1if 0<=index<len(self.FONTSIZES): self.set_fontsize(index)def set_wrap(self):if self.is_autowrap.get():self.contents['wrap'] = CHARelse: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 windowdef new_binary(self): # 创建新二进制文件try:self.saveconfig()except OSError:passwindow=Editor()window.isbinary=Truewindow.create_binarytools()window.change_title()window.change_mode()window.contents.edit_reset()window.focus_force()return windowdef open(self):#加载一个文件filename=filediag.askopenfilename(master=self,title='打开',initialdir=os.path.split(self.filename)[0],filetypes=self.FILETYPES)if not filename:returnif 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:returnif 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=binarytry:data=self._load_data(filename)if data==0:returnself.filename=filenameself.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=Falseself.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 dataelse: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=Truedata=to_escape_str(f.read())elif result is not None:self.isbinary=Falsedata=str(f.read(),encoding=self.coding.get(),errors="replace")else:return 0 # 表示取消return datadef ask_for_save(self,quit=True):my_ret=Noneif 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表示cancelif quit:Editor.windows.remove(self)try:self.saveconfig() # 保存配置, 见附except OSError:passself.destroy() # tkinter不会自动关闭窗口, 需调用函数手动关闭return my_retdef save(self):#保存文件if not self.filename:self.filename=filediag.asksaveasfilename(master=self,initialdir=os.path.split(self.filename)[0],filetypes=self.FILETYPES)filename=self.filenameif 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=filenameself.file_modified=Falseself.change_title()self.change_mode()else:return 0 # 0表示canceldef save_as(self):filename=filediag.asksaveasfilename(master=self,initialdir=os.path.split(self.filename)[0],filetypes=self.FILETYPES)if filename: # 如果未选择取消self.filename=filenameself.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*"%newtitleself.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=Trueself.update_status();self.change_title()def update_status(self,event=None):if not self._show_status.get():returnif 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 callbacktext=str(raw,encoding=coding,errors="replace")except LookupError as err: # 未知编码handle(err,parent=self);returnself.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=Noneif data is not None:self.status["text"]="偏移量: {} ({})"\.format(len(data),hex(len(data)))else:offset=self.contents.index(INSERT).split('.') # 不能用CURRENTself.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=1024for i in range(0,len(byte),length):str+=repr( byte[i: i+length] ) [2:-1]str+='\n'return strdef to_bytes(escape_str):# 将转义字符串转换为字节# -*****- 1.2.5版更新: 忽略二进制模式中文字的换行符escape_str=escape_str.replace('\n','')escape_str=escape_str.replace('"""','\\"\\"\\"') # 避免引号导致的SyntaxErrorescape_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:returntry: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=Nonetry:coding=self.coding.get()if coding=="自动":msgbox.showinfo('','不支持自动编码, 请选择或输入其他编码',parent=self)returnbyte = 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=masterself.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.contentskey=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);returnif 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 0length = 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,newposelif bell: # 未找到,返回Nonebell_(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:returnif cursor_pos=='end':self.master.contents.mark_set('insert',result[1])elif cursor_pos=='start':self.master.contents.mark_set('insert',result[0])return resultdef mark_text(self,start_pos,end_pos):text=self.master.contentstext.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) + 1lineno=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.contentssel_range=text.tag_ranges('sel') # 获得选区的起点和终点if sel_range:selectarea = sel_range[0].string, sel_range[1].stringresult = self.findnext('start')if result is None:returnif 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.contentsresult=self.search(mark=False,bell=bell)if not result:return # 标志已无文本可替换self.master.text_change()pos,newpos=resultnewtext=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);returntext.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_posdef replace_all(self):self.master.contents.mark_set("insert","1.0") # 将光标移到开头flag=False # 标志是否已有文字被替换# 以下代码会导致无限替换, 使程序卡死, 新的代码修复了该bug#while self.replace(bell=False)!=-1:#    flag=Truelast = (0,0)while True:result=self.replace(bell=False,mark=False)if result is None:breakflag = Trueresult = self.findnext('start',bell=False,mark=False)if result is None:returnln,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) # 已完成一轮替换breaklast=ln,colif not flag:bell_()

Editor类内部加入以下代码:

    def findnext(self):fd = self._dialogs.get(SearchDialog,None)if fd:if fd.findnext():returnrd = 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 ColorDelegatorfrom idlelib.percolator import Percolator
except ImportError: # 可能未安装IDLEColorDelegator=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.argvfor arg in sys.argv[1:]:try:Editor(arg)except OSError:passelse: 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=filesself.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'))

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

  1. python Tkinter Text的简单用法

    1.设置python Tkinter Text控件文本的方法 text.insert(index,string)  index = x.y的形式,x表示行,y表示列 向第一行插入数据,text.ins ...

  2. python点名代码_基于python tkinter的点名小程序功能的实例代码

    基于python tkinter的点名小程序功能的实例代码,花名册,次数,窗口,未找到,初始化 基于python tkinter的点名小程序功能的实例代码 易采站长站,站长之家为您整理了基于pytho ...

  3. Kate  一款功能丰富的文本编辑器(可能是目前最好的开源跨平台轻量编辑器 之一)

    参考:kate 跨平台KDE文本编辑器使用方法 汇集 https://blog.csdn.net/ken2232/article/details/130167973 有网络消息说:这款编辑器停止了更新 ...

  4. Python requests模块高级用法

    2019独角兽企业重金招聘Python工程师标准>>> 快速入门的话可以参考这篇文章,但是进阶的话还是建议参考官方的文档,毕竟官方的文档更新比较及时,也有些高级用法,猛戳这里! 会话 ...

  5. Python + Tkinter 图形化界面设计1 —— 第一个图形化界面

    图形化界面设计的基本理解 Python自带了tkinter 模块,实质上是一种流行的面向对象的GUI工具包 TK 的Python编程接口,提供了快速便利地创建GUI应用程序的方法.其图像化编程的基本步 ...

  6. python中text格式_python读取各种格式的文本

    1. 读取word文本 Python可以利用python-docx模块处理word文档,处理方式是面向对象的,python-docx模块会把word文档中的段落.文本.字体等都看做对象,对对象进行处理 ...

  7. 代理商丨UltraEdit是一套功能强大的文本编辑器

    UltraEdit是世界上公认的标准文本编辑器. 程序员.专业开发人员.研究人员.博客.Web开发人员,IT专业人员以及介于两者之间的所有人都可以使用它作为首选编辑器! 无论工作需要什么 - 从基本编 ...

  8. Powerful***功能强大的文本编辑器***PilotEdit Lite

    PilotEdit Lite是一款功能强大的文件编辑器,PilotEdit将帮助您搜索和替换多行文字,PilotEdit编辑FTP文件甚至下载和上传FTP文件和目录.PilotEdit支持文本编辑,比 ...

  9. KindEditor富文本编辑器【图片、视频等功能的富文本编辑器】

    官方下载地址:http://kindeditor.net/demo.php 下载插件: 简单模式.默认模式富文本编辑器: 默认模式html页面: <!DOCTYPE html> <h ...

最新文章

  1. C++中定义类的对象:用new和不用new的区别
  2. bss,data,text,rodata,堆,栈,常量段
  3. 软件工程--需求分析
  4. Py之wxPython:wxPython的简介、安装、使用方法之详细攻略
  5. 水经注叠加cad_如何下载等高线并在CAD中与卫星影像叠加
  6. LINUX系统用户操作命令
  7. type=file的未选择任何文件修改_Excel基础—文件菜单之创建保存
  8. 记某单机游戏的一次内购破解
  9. JSON_UNQUOTE 和JSON_EXTRACT 的简单认识
  10. 用matlab软件心得体会,MATLAB软件实训报告 - 图文
  11. DEVC报错[Error] expected initializer before '.'
  12. 【CSDN竞赛第四期】编程赛后总结与分享
  13. 利用三角形三条边求三角形面积
  14. Python财务分析
  15. ts文件编译与运行,vscode自动编译
  16. 「原创」大数据岗位总结和相关书籍推荐
  17. KP1210代替品 喆光WISELITE MPC-851 晶体管输出350V 光电晶体管
  18. 数学建模专栏 | 第十二篇:MATLAB CUMCM真题求解实例三:机理建模型
  19. python爬虫(三)——多线程+正则匹配下载图片(wallheaven图片网站)
  20. 计算机器人夹具的方向,一种机器人夹具抓取算法的制作方法

热门文章

  1. iCloud数据存储
  2. 【OpenCV学习笔记】之图像金字塔(Image Pyramid)
  3. ~1前端开发是干什么的?
  4. 微信开放平台开发(3) 移动应用微信登录
  5. 备战面试日记(2.4) - (JVM.GC算法)
  6. Error: L6218E: Undefined symbol f_mkfs (referred from main.o)
  7. 麓言科技怎么成为广告设计师
  8. python学习三-基础语法
  9. 单片机计时器100000秒以内
  10. python中字符串如何新增元素_python向字符串中添加元素的实例方法