问题背景
- 文本编辑器在操作系统中扮演着十分重要的角色,不论是配置系统文件还是编写程序代码都需要借助于文本编辑器来完成。不同的操作系统中存在不同的文本编辑器,例如,TextMate(Mac平台)、Notepad++(Windows平台)等。虽然这些文本编辑器的最基本需求都是用来编辑文件,但其内部设计的细节却不尽相同,因此也形成了各自独有的特色。
2 设计内容
- 该文本编辑器可以对文本实现新建,保持,打开,另存为,以及退出功能。在对文本进行编辑的时候可以实现撤销、恢复、复制、剪切、粘贴、查找、全选。为了配合使用者更好观察文本,还创建了添加行数,显示高亮行,以及主题切换功能。### 3 系统整体功能模块
4 代码模块
-
代码展示
# 导入类库 from tkinter import * from tkinter import filedialog, messagebox from tkinter.ttk import Scrollbar, Checkbutton, Label, Button import os import tkinter.font as tf class Notepad(Tk): #用一个字典来装主题颜色 theme_color = {"Default":"#000000.#FFFFFF", "Olive Green":"#D1E7E0.#5B8340", "Night Mode":"#FFFFFF.#000000" } #初始化操作 def __init__(self): super().__init__() self.file_name = None self.set_window() self.creat_menu_bar() self.create_body() self.creat_pop_menu() # 设置窗口界面、 def set_window(self): self.title("XXX专用文本编辑器") align_center = "900x700" self.geometry(align_center) # 设置窗口大小 # 创建菜单项目 def creat_menu_bar(self): menu_bar = Menu(self) # 添加菜单项目 #[文件菜单] file_menu = Menu(menu_bar, tearoff=False)# 这个menu是在menu_bar上创建的 # False是指无法将此下拉菜单从文本编辑器的窗口中分离,不然会有分割线,每次在新的菜单界面的时候开头要加 menu_bar.add_cascade(label="文件", menu=file_menu) # 将上面定义的file菜单命名为`文件`,放在菜单栏中,就是装入那个容器中 # 当一个菜单会有子菜单时候使用cascade,添加一个父菜单 # 通过command为菜单绑定方法 file_menu.add_command(label="新建", accelerator="Ctrl + N", command=self.new_file) file_menu.add_command(label="打开", accelerator="Ctrl + o", command=self.open_file) file_menu.add_command(label="保存", accelerator="Ctrl + s", command=self.save_file) file_menu.add_command(label="另存为", accelerator="Shift + Ctrl + N", command=self.save_as) file_menu.add_separator() # 分隔线 file_menu.add_command(label="退出", accelerator="Alt + F4", command=self.exit_notepad) #[编辑菜单] editor_menu = Menu(menu_bar, tearoff=False) editor_menu.add_command(label="撤销", accelerator="Ctrl + Z", command=lambda:self.right_handle_menu_action("撤销")) editor_menu.add_command(label="恢复", accelerator="Ctrl + Y", command=lambda:self.right_handle_menu_action("恢复")) editor_menu.add_separator() editor_menu.add_command(label="剪切", accelerator="Ctrl + X", command=lambda:self.right_handle_menu_action("剪切")) editor_menu.add_command(label="复制", accelerator="Ctrl + C", command=lambda:self.right_handle_menu_action("复制")) editor_menu.add_command(label="粘贴", accelerator="Ctrl + V", command=lambda:self.right_handle_menu_action("粘贴")) editor_menu.add_separator() editor_menu.add_command(label="查找", accelerator="Ctrl + F", command=self.find_text_dialog) editor_menu.add_separator() editor_menu.add_command(label="全选", accelerator="Ctrl + A", command=lambda:self.select_all) menu_bar.add_cascade(label="编辑", menu=editor_menu) # [视图菜单] view_menu = Menu(menu_bar, tearoff=False) menu_bar.add_cascade(label="视图", menu=view_menu) # 显示行号 self.show_line_num = IntVar() # intvar():属于Tkinter下的对象。用于处理整型 self.show_line_num.set(1) #onvalue = 1 1 表示默认选状态,0 表示不选 #offvalue = 0 未选择的返回值 view_menu.add_checkbutton(label="屏蔽行号", onvalue= 0,offvalue=1, variable=self.show_line_num, command=self.update_line_num) # 高亮当前行 self.is_heighlight_line = IntVar() view_menu.add_checkbutton(label="高亮当前行",variable=self.is_heighlight_line,command=self.toggle_highlight) view_menu.add_separator() # 主题 themes_menu = Menu(menu_bar,tearoff=False) #子菜单,都是在menu_bar上面 self.theme_choice = StringVar() self.theme_choice.set("Default")#默认主题 for K in self.theme_color: themes_menu.add_radiobutton(label=K,variable=self.theme_choice,command=self.change_theme) view_menu.add_cascade(label="主题",menu=themes_menu) #字体大小 view_menu.add_separator() # 分隔线 view_menu.add_command(label="调整字体大小为10", command=self.change_add_word10) view_menu.add_command(label="调整字体大小为20", command=self.change_add_word20) view_menu.add_command(label="调整字体大小为30", command=self.change_add_word30) view_menu.add_command(label="调整字体大小为40", command=self.change_add_word40) # [关于菜单] about_menu = Menu(menu_bar, tearoff=False) about_menu.add_command(label="关于",command=lambda :self.show_messagebox("关于")) about_menu.add_command(label="帮助", command=lambda :self.show_messagebox("帮助")) menu_bar.add_cascade(label="关于", menu=about_menu) self["menu"] = menu_bar #编辑界面操作的主体 def create_body(self): # 左边是行号,中间是文本编辑区,右边是滚条 #行号区域 font1 = tf.Font(family='微软雅黑', size=10) self.line_number_bar = Text(self,width = 4,padx=3,takefocus=0,border=0, background="#FFFFFF",state="disable",font=font1 ) #padx是靠边距,takefocus = 0不让其获取到焦点,border=0没有边框,state="disable"不能编辑 self.line_number_bar.pack(side = 'left',fill= 'y') #fill= 'y'填充整个y轴 #文本编辑区 self.context_text = Text(self,wrap="word",undo = True,font=font1) #wrap是如何换行,word是按单词自动换行,undo是否具撤销功能 #Text控件通常用作编辑框,可以接收文字的输入 self.context_text.pack(fill='both',expand = 'yes') #fill='both'两边全部填空,expand = 'yes'具备延展功能 #设置文本输入区 self.context_text.tag_config("active_line", background="#EEEEE0") # 热键的绑定 self.context_text.bind("<Control-o>",self.open_file) self.context_text.bind("<Control-O>", self.open_file) self.context_text.bind("<Control-s>", self.save_file) self.context_text.bind("<Control-S>", self.save_file) self.context_text.bind("<Control-n>", self.new_file) self.context_text.bind("<Control-N>", self.new_file) self.context_text.bind("<Any-KeyPress>",lambda e:self.update_line_num()) #每次换行行号都会随之增加 self.context_text.bind() #滚动条 scoll_bar = Scrollbar(self.context_text) #滚条是在文本输入框里面的 scoll_bar['command'] = self.context_text.yview self.context_text["yscrollcommand"] = scoll_bar.set #context_text的垂直y轴的滚动交给scoll_bar.set来处理 scoll_bar.pack(side="right",fill="y") # 打开文件 def open_file(self,event=None): #打开文件并进行设置 input_file = filedialog.askopenfilename(filetypes=[("所有文件","*.*"),("文本文档","*.txt")]) #askopenfilename的参数,返回值为选择文件的绝对路径 if input_file: self.title("打开文件为:{}".format(os.path.basename(input_file))) #找到文件名字 #os.path.basename 用来返回变量路径的最后文件名 self.file_name = input_file self.context_text.delete(1.0,END) #把原来context_text的值都清空 with open(input_file,'r') as file: self.context_text.insert(1.0, file.read()) # 文件的保存 def write_to_file(self,file_name): try: content = self.context_text.get(1.0,END) #得到context_text的所有内容 print(file_name) with open(file_name,"w") as file: file.write(content) self.title("保存文件的名字:{}".format(os.path.basename(file_name))) except IOError: messagebox.showerror("文件保存失败!") def save_file(self,event=None): if not self.file_name: self.save_as() else: self.write_to_file(self.file_name) #新建 def new_file(self,event=None): self.title("新建-----NotePad") self.context_text.delete(1.0,END) self.file_name = None #另存为 def save_as(self): input_file = filedialog.askopenfilename(filetypes=[("所有文件", "*.*"), ("文本文档", "*.txt")]) if input_file: self.file_name = input_file self.write_to_file(self.file_name) #退出 def exit_notepad(self): if messagebox.askyesno('确认操作', '确认执行退出操作吗?') == True : self.destroy() else: pass #右键菜单 def creat_pop_menu(self): pop_menu = Menu(self.context_text,tearoff=0) for item1,item2 in zip(["剪切","复制","粘贴","撤销","恢复"],["cut","copy","paste","undo","redo"]): pop_menu.add_command(label=item1,compound="left",command=self.tool_bar_action(item2)) pop_menu.add_separator() pop_menu.add_command(label="全选",command=self.select_all) # 绑定,不然菜单不会出来 self.context_text.bind("<Button-3>",lambda event:pop_menu.tk_popup(event.x_root,event.y_root)) #右键菜单的处理 def right_handle_menu_action(self,action_type): if action_type == "撤销": self.context_text.event_generate("<<Undo>>") elif action_type == "恢复": self.context_text.event_generate("<<Rndo>>") elif action_type == "剪切": self.context_text.event_generate("<<Cut>>") elif action_type == "复制": self.context_text.event_generate("<<Copy>>") elif action_type == "粘贴": self.context_text.event_generate("<<Paste>>") #防止事件传递 return "break" #工具栏命令处理 def tool_bar_action(self,action_type): def handle(): if action_type == "open_file": self.open_file() elif action_type == "save": self.save_file() elif action_type == "new_file": self.new_file() elif action_type == "cut": self.right_handle_menu_action("剪切") elif action_type == "copy": self.right_handle_menu_action("复制") elif action_type == "paste": self.right_handle_menu_action("粘贴") elif action_type == "undo": self.right_handle_menu_action("撤销") elif action_type == "redo": self.right_handle_menu_action("恢复") #handle 的返回处理 return handle #全选 def select_all(self): self.context_text.tag_add("sel",1.0,END) return "break" #行号处理 def update_line_num(self): if self.show_line_num.get(): #获取所有行 row, col = self.context_text.index(END).split(".") #列举每行的行号 line_num_content = "\n".join([str(i) for i in range(1,int(row))]) self.line_number_bar.config(state="normal")#把之前的disable变成可用 self.line_number_bar.delete(1.0,END) #每次使用都要把之前的删掉 self.line_number_bar.insert(1.0,line_num_content) self.line_number_bar.config(state="disable") else: self.line_number_bar.config(state="normal") self.line_number_bar.delete(1.0,END) self.line_number_bar.config(state="disable") # 高亮当前行 def toggle_highlight(self): if self.is_heighlight_line.get(): self.context_text.tag_remove("active_line",1.0,END) #把之前写的都清除掉 # 设置高亮 self.context_text.tag_add("active_line","insert linestart","insert lineend+1c") # 通过递归的方式进行处理 self.context_text.after(200,self.toggle_highlight) #规定递归只能递归200行,浪费时间 else: self.context_text.tag_remove("active_line",1.0,END) def change_add_word10(self): #修改文本输入框的同时,也修改行号框,不然字体对不上 font1 = tf.Font(family='微软雅黑', size=10) self.context_text.config(background="#ffffff",font=font1) self.line_number_bar.config(background="#ffffff",font=font1) def change_add_word20(self): font1 = tf.Font(family='微软雅黑', size=20) self.context_text.config(background="#ffffff",font=font1) self.line_number_bar.config(background="#ffffff", font=font1) def change_add_word30(self): font1 = tf.Font(family='微软雅黑', size=30) self.context_text.config(background="#ffffff",font=font1) self.line_number_bar.config(background="#ffffff", font=font1) def change_add_word40(self): font1 = tf.Font(family='微软雅黑', size=40) self.context_text.config(background="#ffffff",font=font1) self.line_number_bar.config(background="#ffffff", font=font1) #设置查找对话框 def find_text_dialog(self): search_dialog = Toplevel(self) search_dialog.title("查找文本") align_center = "400x100" search_dialog.geometry(align_center) # 设置窗口大小 search_dialog.resizable(False,False) #不让查找对话框大小可变 Label(search_dialog,text="查找全部").grid(row=0,column=0,sticky="e")#sticky="e"指在最左边 search_text = Entry(search_dialog,width = 25) search_text.grid(row=0,column=1,padx=2,pady=2,sticky="we") search_text.focus_set() #让它获取焦点、就是开始输入地方就选中了 # 忽略大小写 ignore_case_value = IntVar() Checkbutton(search_dialog,text="忽略大小写",variable=ignore_case_value).grid( row=1,column=1,sticky='e',padx=2,pady=2 ) Button(search_dialog,text="查找",command=lambda:self.search_result(search_text.get(), ignore_case_value,search_dialog,search_text )).grid(row = 0,column=2,sticky="w"+"e",pady=2,padx=2) def close_search_dialog(): self.context_text.tag_remove("match",1.0,END) search_dialog.destroy() search_dialog.protocol("WM_DELETE_WINDOW",close_search_dialog)#把WM_DELETE_WINDOW(关闭从窗口)这件事交给close_search_dialog来做 return "break" #防止事件的传递 #查找结果的方法 def search_result(self,key,ignore_case,search_dialog,search_box): self.context_text.tag_remove("match",1.0,END)#把之前匹配的东西进行删除 mathches_found = 0 #初始化相同匹配结果个数为0 if key: start_pos = 1.0 while True: start_pos = self.context_text.search(key,start_pos,nocase=ignore_case,stopindex=END)#stopindex=END查到最后的取值 if not start_pos: #没有匹配的 break end_pos = "{}+{}c".format((start_pos),len(key)) self.context_text.tag_add("match",start_pos,end_pos) mathches_found += 1 start_pos = end_pos #把查到的赋给下一次要查的开始 self.context_text.tag_config("match",foreground="red",background="yellow") #把匹配结果标记出来 search_box.focus_set() search_dialog.title("发现了%d个匹配项"% mathches_found) #切换主题 def change_theme(self): selected_theme = self.theme_choice.get() fg_bg = self.theme_color.get(selected_theme) fg_color,bg_color = fg_bg.split(".") self.context_text.config(bg=bg_color,fg=fg_color) # 关于菜单 def show_messagebox(self,tpye): if tpye == "帮助": messagebox.showinfo("帮助","如果有疑问请联系QQ:2996927011") else: messagebox.showinfo("关于","这是毕仁和编写的一个简单记事本") if __name__ == "__main__": app = Notepad() app.mainloop()
5 界面效果
我希望这篇文章能够为你提供一些帮助,帮助你在成为一名顶尖程序员的道路上迈出一小步,如果您觉得这个文章对您有帮助或者让您感到开心,请不要吝啬您的赞哟!