闲着无聊自己写了个简单的代码编辑器, 支持语法高亮, 符号补全, 查找, 打印等功能。有部分功能还不完善。
有点长。可以到Github下载XIAOQINGeq/PyScripter: A very simple python code editor made with python tkinter (github.com)
顺便点颗星, 谢谢。
打印和加载配置功能使用了pywin32和commentjson库, 需要另外安装。
请确保你的电脑上安装了IDLE
## main.py
import tkinter as tk
from idlelib.colorizer import color_config, ColorDelegator
from idlelib.percolator import Percolator
from tkinter import *
from tkinter import messagebox, filedialog
from tkinter.scrolledtext import ScrolledText
import commentjson as json
import win32print
import win32ui
from dialog import FindDialog, ReplaceDialog
class Editor:
def __init__(self):
self.root = tk.Tk()
self.root.title('PyScripter - Untitled')
self.root.geometry('900x600')
self.root.protocol('WM_DELETE_WINDOW', self.exit_app)
# 储存数据
self.data = {
'file_path': 'untitled'
}
# 加载配置文件
self.config = self.load_config()
# 文本编辑区
self.text_area = ScrolledText(self.root, font=('Consolas', 12), undo=True, autoseparators=True, maxundo=-1)
self.text_area.pack(fill=BOTH, expand=True)
# Tab键前进4格
self.text_area.bind("<Tab>", self.insert_tab_spaces)
# 添加键盘输入事件处理函数
self.text_area.bind('<Key>', self.on_key_press)
# 应用语法高亮
if self.config['python_syntax_highlight']:
color_config(self.text_area)
p = Percolator(self.text_area)
d = ColorDelegator()
p.insertfilter(d)
# 状态栏
self.status_bar = Label(self.root, text='Ln: 1 | Col: 0', bd=1, relief=SUNKEN, anchor=W)
self.status_bar.pack(side=BOTTOM, fill=X)
# 行数列数显示
self.text_area.bind('<KeyRelease>', self.update_line_col)
self.text_area.bind('<ButtonRelease-1>', self.update_line_col)
# 菜单栏
self.menubar = Menu(self.root)
self.root.config(menu=self.menubar)
# 文件菜单
file_menu = Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label='File', menu=file_menu)
file_menu.add_command(label='New File', accelerator='Ctrl+N', command=self.new_file)
file_menu.add_command(label='Open File', accelerator='Ctrl+O', command=self.open_file)
file_menu.add_command(label='Save', accelerator='Ctrl+S', command=self.save_file)
file_menu.add_command(label='Save As...', accelerator='Ctrl+Shift+S', command=self.save_as_file)
file_menu.add_separator()
file_menu.add_command(label='Print', accelerator='Ctrl+P', command=self.print_text)
file_menu.add_command(label='File Statistics', command=self.file_statistics)
file_menu.add_separator()
file_menu.add_command(label='Exit', command=self.exit_app)
# 编辑菜单
edit_menu = Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label='Edit', menu=edit_menu)
edit_menu.add_command(label='Undo', accelerator='Ctrl+Z', command=self.undo)
edit_menu.add_command(label='Redo', accelerator='Ctrl+Y', command=self.redo)
edit_menu.add_separator()
edit_menu.add_command(label='Cut', accelerator='Ctrl+X', command=self.cut)
edit_menu.add_command(label='Copy', accelerator='Ctrl+C', command=self.copy)
edit_menu.add_command(label='Paste', accelerator='Ctrl+V', command=self.paste)
edit_menu.add_separator()
edit_menu.add_command(label='Find All', accelerator='Ctrl+F', command=self.find_all)
edit_menu.add_command(label='Cancel Find', accelerator='Esc', command=self.cancel_find)
edit_menu.add_command(label='Replace...', accelerator='Ctrl+H', command=self.replace)
# 选择菜单
select_menu = Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label='Selection', menu=select_menu)
select_menu.add_command(label='Select All', accelerator='Ctrl+A', command=self.select_all)
select_menu.add_command(label='Select Line', accelerator='Ctrl+L', command=self.select_line)
select_menu.add_command(label='Select Paragraph', accelerator='Ctrl+Shift+P', command=self.select_paragraph)
select_menu.add_separator()
select_menu.add_command(label='Expand Selection', accelerator='Ctrl+W', command=self.expand_selection)
select_menu.add_command(label='Shrink Selection', accelerator='Ctrl+E', command=self.shrink_selection)
# 首选项菜单
preference_menu = Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label='Preference', menu=preference_menu)
# 语法高亮选项
preference_menu.add_command(label='Enable/Disable Syntax Highlight', command=self.update_syntax_highlight)
# 帮助菜单
help_menu = Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label='Help', menu=help_menu)
help_menu.add_command(label='About', command=self.about)
self.root.mainloop()
def load_config(self):
"""加载配置文件"""
with open('./app/config/config.json', 'r', encoding='utf-8') as f:
config = json.load(f)
return config
def update_line_col(self, event=None):
"""更新光标所在的行列数"""
line, col = self.text_area.index('insert').split('.')
self.status_bar.config(text=f'Ln: {line} | Col: {col}')
def insert_tab_spaces(self, event):
"""Tab键前进4格"""
self.text_area.insert(INSERT, ' ' * self.config['number_of_tab_indents'])
return 'break'
def on_key_press(self, event):
"""键盘输入事件处理函数"""
if event.char == '"':
self.text_area.insert(INSERT, '"') # 在光标位置插入"
self.text_area.mark_set("insert", "insert-1c") # 光标位置不变
elif event.char == "'":
self.text_area.insert(INSERT, "'") # 在光标位置插入'
self.text_area.mark_set("insert", "insert-1c") # 光标位置不变
elif event.char in ['(', '[', '{', '<']:
end_char = {
'(': ')',
'[': ']',
'{': '}',
'<': '>'
}
self.text_area.insert(INSERT, end_char[event.char]) # 在光标位置插入对应的括号
self.text_area.mark_set("insert", "insert-1c") # 光标位置不变
def check_file_saved(self, file_path):
"""检查文件是否已保存"""
if file_path == 'untitled':
if self.text_area.get('1.0', END).strip() == '':
return True
else:
return False
else:
with open(file_path, 'r', encoding='utf-8') as f:
if f.read() == self.text_area.get('1.0', END).strip():
return True
else:
return False
def exit_app(self):
"""退出程序"""
if self.check_file_saved(self.data['file_path']):
self.root.destroy()
else:
response = messagebox.askyesnocancel('Exit', 'Do you want to save changes?')
if response is True:
self.save_file()
self.root.destroy()
elif response is False:
self.root.destroy()
else:
pass
def new_file(self):
"""新建文件"""
if self.check_file_saved(self.data['file_path']):
self.text_area.delete('1.0', END)
self.data['file_path'] = 'untitled'
self.root.title('PyScripter - Untitled')
else:
response = messagebox.askyesnocancel('New File', 'Do you want to save changes?')
if response is True:
self.save_file()
elif response is False:
self.text_area.delete('1.0', END)
self.data['file_path'] = 'untitled'
self.root.title('PyScripter - Untitled')
else:
pass
def open_file(self):
"""打开文件"""
if self.check_file_saved(self.data['file_path']):
file_path = filedialog.askopenfilename(title='Open File', filetypes=[('Python Files', '*.py')])
if file_path:
with open(file_path, 'r', encoding='utf-8') as f:
self.text_area.delete('1.0', END)
self.text_area.insert('1.0', f.read())
self.data['file_path'] = file_path
self.root.title(f'PyScripter - {file_path}')
else:
response = messagebox.askyesnocancel('Open File', 'Do you want to save changes?')
if response is True:
self.save_file()
elif response is False:
file_path = filedialog.askopenfilename(title='Open File', filetypes=[('Python Files', '*.py')])
if file_path:
with open(file_path, 'r', encoding='utf-8') as f:
self.text_area.delete('1.0', END)
self.text_area.insert('1.0', f.read())
self.data['file_path'] = file_path
self.root.title(f'PyScripter - {file_path}')
else:
pass
def save_file(self):
"""保存文件"""
if self.data['file_path'] == 'untitled':
file_path = filedialog.asksaveasfilename(title='Save File', filetypes=[('Python Files', '*.py')])
if file_path:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(self.text_area.get('1.0', END))
self.data['file_path'] = file_path
self.root.title(f'PyScripter - {file_path}')
else:
with open(self.data['file_path'], 'w', encoding='utf-8') as f:
f.write(self.text_area.get('1.0', END))
def save_as_file(self):
"""另存为文件"""
file_path = filedialog.asksaveasfilename(title='Save As', filetypes=[('Python Files', '*.py')])
if file_path:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(self.text_area.get('1.0', END))
self.data['file_path'] = file_path
self.root.title(f'PyScripter - {file_path}')
def print_text(self):
"""打印文本"""
text_to_print = self.text_area.get("1.0", "end-1c")
if not text_to_print.strip():
messagebox.showerror('Print Error', 'There is no text to print.')
# 打印文本
printer_name = win32print.GetDefaultPrinter()
hprinter = win32print.OpenPrinter(printer_name)
try:
printer_info = win32print.GetPrinter(hprinter, 2)
hdc = win32ui.CreateDC()
hdc.CreatePrinterDC(printer_name)
hdc.StartDoc(f"PyScripter - {self.data['file_path']}")
hdc.StartPage()
hdc.TextOut(100, 100, text_to_print)
hdc.EndPage()
hdc.EndDoc()
hdc.DeleteDC()
finally:
win32print.ClosePrinter(hprinter)
def file_statistics(self):
"""文件统计"""
file_path = self.data['file_path']
text = self.text_area.get('1.0', END)
lines = text.count('\n')
words = len(text.split())
characters = len(text)
messagebox.showinfo('File Statistics',
f'File: {file_path}\n\nLines: {lines}\nWords: {words}\nCharacters: {characters}')
def undo(self):
"""撤销"""
self.text_area.event_generate("<<Undo>>")
def redo(self):
"""重做"""
self.text_area.event_generate("<<Redo>>")
def cut(self):
"""剪切"""
self.text_area.event_generate("<<Cut>>")
def copy(self):
"""复制"""
self.text_area.event_generate("<<Copy>>")
def paste(self):
"""粘贴"""
self.text_area.event_generate("<<Paste>>")
def find_all(self):
"""查找全部匹配项"""
self.find_dialog = FindDialog(self.root, self.text_area)
self.find_dialog.show()
def replace(self):
"""替换功能"""
self.replace_dialog = ReplaceDialog(self.root, self.text_area)
self.replace_dialog.show()
def cancel_find(self):
"""取消查找"""
self.text_area.tag_remove('found', '1.0', tk.END)
def select_all(self):
"""全选"""
self.text_area.event_generate('<<SelectAll>>')
def select_line(self):
"""选中当前行"""
line, col = map(int, self.text_area.index('insert').split('.'))
self.text_area.tag_add('sel', f'{line}.0', f'{line}.end')
def select_paragraph(self):
"""选中当前段落"""
line, col = map(int, self.text_area.index('insert').split('.'))
paragraph_start = self.text_area.search(r'\n\s*\n', f'{line}.0', backwards=True, regexp=True)
paragraph_end = self.text_area.search(r'\n\s*\n', f'{line}.0', forwards=True, regexp=True)
self.text_area.tag_add('sel', paragraph_start, paragraph_end)
def expand_selection(self):
"""扩大选择"""
self.text_area.tag_add('sel', 'insert linestart', 'insert lineend+1c')
def shrink_selection(self):
"""缩小选择"""
self.text_area.tag_remove('sel', 'insert linestart', 'insert lineend+1c')
def update_syntax_highlight(self):
"""更新是否启用语法高亮"""
if self.config['python_syntax_highlight']:
self.config['python_syntax_highlight'] = False
with open('./app/config/config.json', 'w', encoding='utf-8') as f:
json.dump(self.config, f, indent=4)
messagebox.showinfo('Syntax Highlight', 'Syntax Highlight Disabled, Restart PyScripter to take effect.')
else:
self.config['python_syntax_highlight'] = True
with open('./app/config/config.json', 'w', encoding='utf-8') as f:
json.dump(self.config, f, indent=4)
messagebox.showinfo('Syntax Highlight', 'Syntax Highlight Enabled, Restart PyScripter to take effect.')
def about(self):
"""关于"""
with open('./app/description.txt', 'r', encoding='utf-8') as f:
description = f.read()
messagebox.showinfo('About', 'About PyScripter\n\n'
'Version: 1.2.0\n'
'Author: Xiaoqing\n'
'License: MIT\n'
'Built Date: 2024-05-26\n\n'
f'Description:{description}\n')
if __name__ == '__main__':
Editor()
## dialog.py
import tkinter as tk
from tkinter import ttk
class FindDialog:
def __init__(self, parent, text_widget):
self.parent = parent
self.text_widget = text_widget
self.dialog = tk.Toplevel(parent)
self.dialog.title('Find All')
self.search_label = ttk.Label(self.dialog, text='Find:')
self.search_label.pack(side=tk.LEFT)
self.search_entry = ttk.Entry(self.dialog, width=30)
self.search_entry.pack(side=tk.LEFT)
self.search_entry.bind('<Return>', lambda event: self.find())
self.find_button = ttk.Button(self.dialog, text='Find All', command=self.find)
self.find_button.pack(side=tk.LEFT)
self.cancel_button = ttk.Button(self.dialog, text='Cancel', command=self.cancel_find)
self.cancel_button.pack(side=tk.LEFT)
self.close_button = ttk.Button(self.dialog, text='Close', command=self.close)
self.close_button.pack(side=tk.LEFT)
def find(self):
self.text_widget.tag_remove('found', '1.0', tk.END)
s = self.search_entry.get()
if s:
idx = '1.0'
while True:
idx = self.text_widget.search(s, idx, nocase=1, stopindex=tk.END)
if not idx:
break
lastidx = f'{idx}+{len(s)}c'
self.text_widget.tag_add('found', idx, lastidx)
idx = lastidx
self.text_widget.tag_config('found', background='lightblue')
def cancel_find(self):
self.text_widget.tag_remove('found', '1.0', tk.END)
def close(self):
self.dialog.destroy()
def show(self):
self.dialog.deiconify()
class ReplaceDialog:
def __init__(self, parent, text_widget):
self.parent = parent
self.text_widget = text_widget
self.dialog = tk.Toplevel(parent)
self.dialog.title('Replace')
self.search_label = ttk.Label(self.dialog, text='Find:')
self.search_label.pack(side=tk.LEFT)
self.search_entry = ttk.Entry(self.dialog, width=30)
self.search_entry.pack(side=tk.LEFT)
self.search_entry.bind('<Return>', lambda event: self.replace())
self.replace_label = ttk.Label(self.dialog, text='Replace with:')
self.replace_label.pack(side=tk.LEFT)
self.replace_entry = ttk.Entry(self.dialog, width=30)
self.replace_entry.pack(side=tk.LEFT)
self.replace_button = ttk.Button(self.dialog, text='Replace', command=self.replace)
self.replace_button.pack(side=tk.LEFT)
self.cancel_button = ttk.Button(self.dialog, text='Undo', command=self.undo)
self.cancel_button.pack(side=tk.LEFT)
self.close_button = ttk.Button(self.dialog, text='Close', command=self.close)
self.close_button.pack(side=tk.LEFT)
self.history = []
def replace(self):
s = self.search_entry.get()
r = self.replace_entry.get()
if s and r:
text = self.text_widget.get('1.0', tk.END)
new_text = text.replace(s, r)
self.history.append(text)
self.text_widget.delete('1.0', tk.END)
self.text_widget.insert('1.0', new_text)
def close(self):
self.dialog.destroy()
def show(self):
self.dialog.deiconify()
def undo(self):
if self.history:
text = self.history.pop()
self.text_widget.delete('1.0', tk.END)
self.text_widget.insert('1.0', text)
## ./app/description.txt
PyScripter is a simple python code editor that allows you to write Python code.
It has python syntax highlighting, find, replace, and other features.
It can also be used as a regular text editor.
## ./app/config/config.json
{
"umber_of_tab_indents": 4,
"python_syntax_highlight": true
}