使用tkinter制作tkinterUI编辑器
前言
最近在思考多重选择怎么做时又研究了一下vs里的c#编辑器,突然想实现一下它的选中效果,如下:
感觉不太好实现,我希望能在编辑器中调试这个东西,不用每次改个属性就重启编辑器,所以决定将python解释器嵌入到编辑器里,直接在编辑器里调试好我想要的属性然后再修改编辑器的代码,这样能省不少时间。
解释器控件效果图如下,在下面输入python语句,上面输出结果:
一、使用方法
- 在debug菜单点击open_debug_window或者alt+p打开解释器
- 解释器开着的时候使用esc关闭解释器
- 输入python语句,按alt+回车或者alt+s发送命令
- alt+上或者alt+下可以在当前行插入历史输入信息
- 可以在输入框调用编辑器(tkinterEditor类)的函数或者修改属性,例如:在输入框输入Editor.change_theme()然后发送就可以直接修改主题
二、实现自己的解释器类与解释器控件
先上代码,debugInterpreter.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
from tkinter import INSERT, END, Toplevel, Text
from code import InteractiveInterpreter
from WidgetRedirector import WidgetRedirector
from componentProperty import update_all_property, get_default_component_info
def create_default_component(master, component_type, component_name, prop=None, use_name=True):
"""
创建默认控件
:param master: 父控件
:param component_type: 控件类型
:param component_name: 控件名字
:param prop: 需要更新的属性
:param use_name: 是否使用控件名字
:return: 控件
"""
class_name = getattr(sys.modules[__name__], component_type)
if use_name:
component = class_name(master, name=component_name)
else:
component = class_name(master)
component_info = get_default_component_info(component_type, prop)
update_all_property(component, component_info, component_type)
return component, component_info
class DebugInterpreter(InteractiveInterpreter):
def __init__(self, master, locals=sys.modules["__main__"].__dict__):
InteractiveInterpreter.__init__(self, locals=locals)
self.master = master
sys.stdout.write = master.debug_write
sys.stderr.write = master.debug_error_write
def send_msg(self, msg):
"""
发送消息
:param msg: 消息
:return: None
"""
file_name = "DebugInterpreter"
lines = msg.split("\n")
symbol = "single"
if len(lines) > 1:
symbol = "exec"
self.runsource(msg, file_name, symbol)
class DebugInterpreterFrame(Toplevel):
def __init__(self, master=None, cnf={}, **kw):
Toplevel.__init__(self, master, cnf, **kw)
self.input_width = 10
self.input_height = 1
self.input_pos_y = 1
self.output_width = 10
self.output_height = 1
self.debug_history = [] # 存储调试历史信息
self.cur_debug_history_index = 0 # 当前调试历史信息索引
self.max_debug_history = 50 # 最多存储调试历史条数
self.debug_interpreter = DebugInterpreter(self)
def set_input_width(self, input_width):
if self.input_width == input_width:
return
self.input_width = input_width
self.do_layout()
def get_input_width(self):
return self.input_width
def set_input_height(self, input_height):
if self.input_height == input_height:
return
self.input_height = input_height
self.do_layout()
def get_input_height(self):
return self.input_height
def set_output_width(self, output_width):
if self.output_width == output_width:
return
self.output_width = output_width
self.do_layout()
def get_output_width(self):
return self.output_width
def set_output_height(self, output_height):
if self.output_height == output_height:
return
self.output_height = output_height
self.do_layout()
def get_output_height(self):
return self.output_height
def set_input_pos_y(self, input_pos_y):
if self.input_pos_y == input_pos_y:
return
self.input_pos_y = input_pos_y
self.do_layout()
def get_input_pos_y(self):
return self.input_pos_y
@property
def debug_input(self):
return self.children.get("debug_input", None)
@property
def debug_output(self):
return self.children.get("debug_output", None)
def on_update(self):
"""
初始化后会被调用,在这里创建输入框和输出框
:return: None
"""
input_prop = {
"background": "white", "foreground": "black",
"width": self.get_input_width(), "height": self.get_input_height(),
}
create_default_component(self, "Text", "debug_input", input_prop)
output_prop = {
"background": "black", "foreground": "white",
"width": self.get_output_width(), "height": self.get_output_height()
}
create_default_component(self, "Text", "debug_output", output_prop)
self.do_layout()
copy_right = 'Type "help", "copyright", "credits" or "license" for more information.'
self.debug_interpreter.write("Python %s on %s\n%s\n(%s)\n" % (sys.version, sys.platform, copy_right,self.__class__.__name__))
redir = WidgetRedirector(self.debug_input)
def input_insert(*args):
if args[1] == "\n":
self.save_debug_history()
original_insert(*args)
def input_delete(*args):
original_delete(*args)
original_insert = redir.register("insert", input_insert)
original_delete = redir.register("delete", input_delete)
def send_msg():
msg = self.debug_input.get(0.0, "end")
msg = msg.strip("\n")
if not msg:
return
self.save_debug_history()
self.debug_interpreter.send_msg(msg)
self.debug_input.delete(0.0, "end")
return "break"
def press_alt_up(event):
self.insert_debug_history(-1)
return 'break'
def press_alt_down(event):
self.insert_debug_history(1)
return 'break'
self.debug_input.bind("<Alt-Key-Return>", lambda event: send_msg())
self.debug_input.bind("<Alt-Key-s>", lambda event: send_msg())
self.debug_input.bind("<Alt-Up>", lambda event: press_alt_up(event))
self.debug_input.bind("<Alt-Down>", lambda event: press_alt_down(event))
def save_debug_history(self):
"""
保存调试历史信息
:return: None
"""
line_row_no = self.debug_input.index("insert").split('.')[0]
msg = self.debug_input.get('{}.0'.format(line_row_no), 'insert')
if not msg:
return
self.debug_history.append(msg)
if len(self.debug_history) >= self.max_debug_history:
self.debug_history.pop(0)
self.cur_debug_history_index = len(self.debug_history)
def insert_debug_history(self, step):
"""
插入历史信息
:param step: 步长
:return: None
"""
if len(self.debug_history) == 0:
return
new_index = self.cur_debug_history_index + step
line_row_no = self.debug_input.index("insert").split('.')[0]
self.debug_input.delete('%s.0' % line_row_no, 'insert')
if step > 0 and new_index >= len(self.debug_history):
return
if step < 0 and new_index < 0:
new_index = len(self.debug_history) - 1
self.cur_debug_history_index = new_index
history = self.debug_history[self.cur_debug_history_index]
self.debug_input.insert('%s.0' % line_row_no, history)
def do_layout(self):
"""
布局
:return: None
"""
# todo:之后输入与输出框改成动态计算的,目前先不处理
self.debug_output.configure(width=self.get_output_width(), height=self.get_output_height())
self.debug_input.configure(width=self.get_input_width(), height=self.get_input_height())
self.debug_output.place(x=0, y=0, anchor='nw')
self.debug_input.place(x=0, y=self.get_input_pos_y(), anchor='nw')
def debug_write(self, msg):
"""
调试窗口写函数回调
:param msg: 写的内容
:return: None
"""
if msg == "\n":
return
str_output = ">>> "
self.debug_output.insert("end", str_output + msg + "\n")
self.debug_output.see("end")
def debug_error_write(self, msg):
"""
调试窗口error函数回调
:param msg: error的内容
:return: None
"""
str_output = ">>> "
self.debug_output.insert("end", str_output + msg + "\n")
self.debug_output.see("end")
- 解释器类继承InteractiveInterpreter,初始化时将系统输出与错误的write函数赋值成解释器控件类里相对应的函数,这样就能监听到系统的输出与错误然后显示在想显示的地方,我这里是将信息输出到一个Text控件里
- 解释器实现一个发送消息的函数,调用runsource函数处理,symbol是“single”的话只能处理一条语句,例如print(”hello workd“),symbol是“exec”的话可以处理多条语句,例如for i in range(10): print(i)
- 解释器控件中的输入框与输出框的尺寸以及输入框的y坐标目前是写到配置文件中的,以后考虑改成按照父控件进行计算的
- 解释器控件中的debug_history是用来存储历史输入信息的,输入框每输入一次回车就把这一行记录进去,之后通过上下键进行快速输入
- 解释器控件输入框又使用了WidgetRedirector监听insert与delete事件,之前属性列表控件那里也用了,目前这里只处理了保存历史记录,之后我还想加一个简单的代码补全功能
- self.debug_input.index(“insert”).split(’.’)[0]这句可以获取输入框当前行的行号
三、将解释器控件加入编辑器
-
先将解释器控件加到componentProperty.py与components.py中,代码这里就不贴了
-
在配置文件default.ini的menu中添加一个菜单用来打开解释器debug = open_debug_window#alt+p
-
将解释器ui加到tkinterEditor.xml中,
<debug_window> <anchor>nw</anchor> <alpha>0.8</alpha> <background>black</background> <component_name>debug_window</component_name> <height>990</height> <input_height>5</input_height> <input_width>274</input_width> <input_pos_y>977</input_pos_y> <output_height>72</output_height> <output_width>274</output_width> <x>735</x> <y>450</y> <positionfrom>program</positionfrom> <title>debug</title> <topmost>1</topmost> <toolwindow>1</toolwindow> <takefocus>0</takefocus> <width>1920</width> <gui_type>DebugInterpreterFrame</gui_type> </debug_window>
-
修改tkinterEditor.py
Editor = None class tkinterEditor(componentMgr): def __init__(self, master, gui_path): componentMgr.__init__(self, master) self.config_parser = ToolConfigParser() self.config_parser.read("default.ini", encoding="utf-8-sig") self.load_from_xml(master, gui_path, True) self.theme = EDITOR_THEME_DEFAULT # 主题 self.right_edit_menu = None # 鼠标右键edit菜单 self.edit_components = {} # 存储可编辑的控件 self.selected_component = None # 当前被选中的控件 self.created_time = 0 # 创建控件时的时间 self.created_pos_x = 0 # 创建控件时的坐标x self.created_pos_y = 0 # 创建控件时的坐标y self.is_new_project_show = True # 创建新project界面是否显示 self.is_debug_window_show = True # 调试窗口界面是否显示 self.copied_component = None # 复制的控件信息 self.init_frame() @property def debug_window(self): return self.editor_window.children.get("debug_window", None) def init_frame(self): """ 初始化ui :return: None """ self.init_menu() self.init_theme() self.init_file_tab_window() self.init_property_list() self.init_treeview() self.init_quick_btn() self.init_top_level() self.init_hot_key() self.init_debug_window() ############################################## debug menu ################################################# def open_debug_window(self): """ 打开调试窗口 :return: None """ self.change_debug_window_ui() ############################################### hot key ################################################### def init_hot_key(self): """ 初始化快捷键 :return: None """ self.master.bind("<Control-s>", lambda event: self.save_gui()) self.master.bind("<Control-o>", lambda event: self.open_gui()) self.master.bind("<Control-n>", lambda event: self.new_gui()) self.master.bind("<Control-p>", lambda event: self.new_project()) self.master.bind("<Control-Delete>", lambda event: self.delete_control()) self.master.bind("<Control-comma>", lambda event: self.copy()) self.master.bind("<Control-period>", lambda event: self.paste()) for k in ("Up", "Down", "Left", "Right"): self.master.bind("<Control-{0}>".format(k), partial(self.move_control, k)) self.master.bind("<Alt-p>", lambda event: self.change_debug_window_ui()) ########################################### debug window ################################################## def change_debug_window_ui(self): """ 打开关闭new_project界面 :return: None """ if self.is_debug_window_show: self.is_debug_window_show = False self.debug_window.withdraw() self.debug_window.master.master.wm_attributes('-disabled', False) self.debug_window.overrideredirect(False) return self.is_debug_window_show = True self.debug_window.deiconify() self.debug_window.master.master.wm_attributes('-disabled', True) self.debug_window.overrideredirect(True) self.debug_window.geometry("{}x{}+{}+{}".format(self.master.winfo_width(), self.master.winfo_height() + 52, self.master.winfo_x() + 7, self.master.winfo_y())) self.debug_window.do_layout() def init_debug_window(self): """ 初始化调试窗口 :return: None """ self.change_debug_window_ui() self.debug_window.bind("<KeyRelease-Escape>", lambda event: self.change_debug_window_ui()) def main(): root = Tk() #root.resizable(0, 0) path = os.path.join(os.getcwd(), "tkinterEditor.xml") global Editor Editor = tkinterEditor(root, path) root.mainloop()
-
添加了一个全局变量Editor,在解释器中就可以使用这个变量调用tkinterEditor类里面的函数
-
change_debug_window_ui函数处理了解释器控件的打开与关闭,目前我还没有找到怎么判断toplevel是否显示,只能自己先加一个变量控制