使用tkinter制作tkinterUI编辑器

目录

  • 使用tkinter制作tkinterUI编辑器
  • 前言
  • 一、实现新的选中控件
  • 二、选中控件的使用

前言

这篇记录记录一下选中框的重构,内容改动较大,可能会出现一些问题,所以目前只在1.0分支中提交。
完整代码已上传到github,可从第一篇记录下载

提示:
1.由于代码改动较大,之前创建的工程都不好使了,需要重新创建。
2.为了让所有控件都能拖动,增加了两个属性,pixel_width,pixel_height,修改这两个属性就能改变控件的尺寸,其实就是调用了place_configure(width=xx, height=xx)实现的,之后在编辑器就不能编辑width和height属性了。
3.在代码里直接修改width和height属性可能会没有效果,需要使用place_configure(width=xx, height=xx)进行修改。
4.我在使用滑动条的时候发现canvas在调用create_window((0,0), window=xxx)后就不能再用place_configure修改xxx的place属性了,需要特殊处理。
5.有时候还是会想要按照字符数修改Button,Entry等控件的大小,这些地方也需要特殊处理,之后有时间加个函数统一进行处理。
6.新的拖拽逻辑需要根据控件的anchor属性做不同的处理,目前我还来不及处理,只能处理anchor是’nw‘的情况,所以anchor属性目前也不能在编辑器编辑了,以后有时间再处理。

新的选中框效果如下,可以从8个方向拖拽修改控件的大小:

一、实现新的选中控件

先上代码,selectedCanvas.py

#!/usr/bin/python
# -*- coding: utf-8 -*-
from tkinter import Canvas
from functools import partial
class SelectedCanvas(Canvas):
def __init__(self, master=None, cnf={ }, **kw):
Canvas.__init__(self, master, cnf, **kw)
self.is_sizing = False
self.old_width = 0
self.old_height = 0
self.old_pos_x = 0
self.old_pos_y = 0
self.start_x = 0
self.start_y = 0
self.start_root_x = 0
self.start_root_y = 0
self.on_resize_complete = None
def set_on_resize_complete(self, on_resize_complete):
self.on_resize_complete = on_resize_complete
def on_update(self):
""" 初始化后会被调用,在这里绘制矩形 :return: None """
self.create_rectangle(-1, -1, -2, -2, tag='side', dash=3, outline='red')
for name in ('nw', 'w', 'sw', 'n', 's', 'ne', 'e', 'se'):
self.create_rectangle(-1, -1, -2, -2, tag=name, outline='red')
self.tag_bind(name, "<Enter>", partial(self.on_mouse_enter, name))
self.tag_bind(name, "<Leave>", partial(self.on_mouse_leave, name))
self.tag_bind(name, "<Button-1>", partial(self.on_mouse_click, name))
self.tag_bind(name, "<B1-Motion>", partial(self.on_mouse_move, name))
self.tag_bind(name, "<ButtonRelease-1>", partial(self.on_mouse_release, name))
def show(self, is_fill=False):
""" 显示 :param is_fill: 是否填充 :return: None """
width = self.winfo_width()
height = self.winfo_height()
self.coords('side', 6, 6, width - 6, height - 6)
self.coords('nw', 0, 0, 7, 7)
self.coords('sw', 0, height - 8, 7, height - 1)
self.coords('w', 0, (height - 7) / 2, 7, (height - 7) / 2 + 7)
self.coords('n', (width - 7) / 2, 0, (width - 7) / 2 + 7, 7)
self.coords('s', (width - 7) / 2, height - 8, (width - 7) / 2 + 7, height - 1)
self.coords('ne', width - 8, 0, width - 1, 7)
self.coords('se', width - 8, height - 8, width - 1, height - 1)
self.coords('e', width - 8, (height - 7) / 2, width - 1, (height - 7) / 2 + 7)
if is_fill:
for name in ('nw', 'w', 'sw', 'n', 's', 'ne', 'e', 'se'):
self.itemconfig(name, fill='red')
def hide(self):
""" 隐藏 :return: None """
self.coords('side', -1, -1, -2, -2,)
for name in ('nw', 'w', 'sw', 'n', 's', 'ne', 'e', 'se'):
self.coords(name, -1, -1, -2, -2)
def on_mouse_enter(self, tag_name, event):
""" 鼠标进入事件 :param tag_name: 标签名字 :param event: event :return: None """
if tag_name in ("nw", "sw", "ne", "se"):
self["cursor"] = "sizing"
elif tag_name in ("w", "e"):
self["cursor"] = "sb_h_double_arrow"
else:
self["cursor"] = "sb_v_double_arrow"
def on_mouse_leave(self, tag_name, event):
""" 鼠标离开事件 :param tag_name: 标签名字 :param event: event :return: None """
if self.is_sizing:
return
self["cursor"] = "arrow"
def on_mouse_click(self, tag_name, event):
""" 鼠标点击事件 :param tag_name: 标签名字 :param event: event :return: None """
self.is_sizing = True
self.start_x = event.x
self.start_y = event.y
self.start_root_x = event.x_root
self.start_root_y = event.y_root
self.old_width = self.winfo_width()
self.old_height = self.winfo_height()
self.old_pos_x = int(self.place_info()['x'])
self.old_pos_y = int(self.place_info()['y'])
def on_mouse_move(self, tag_name, event):
""" 鼠标移动事件 :param tag_name: 标签名字 :param event: event :return: None """
if not self.is_sizing:
return
if 'e' in tag_name:
width = max(0, self.old_width + (event.x - self.start_x))
self.place_configure(width=width)
if 'w' in tag_name:
width = max(0, self.old_width + (self.start_root_x - event.x_root))
to_x = event.x - self.start_x + int(self.place_info()['x'])
self.place_configure(width=width, x=to_x)
if 's' in tag_name:
height = max(0, self.old_height + (event.y - self.start_y))
self.place_configure(height=height)
if 'n' in tag_name:
height = max(0, self.old_height + (self.start_root_y - event.y_root))
to_y = event.y - self.start_y + int(self.place_info()['y'])
self.place_configure(height=height, y=to_y)
self.after_idle(self.show)
def on_mouse_release(self, tag_name, event):
""" 鼠标松开事件 :param tag_name: 标签名字 :param event: event :return: None """
self.is_sizing = False
if self.on_resize_complete is not None:
self.on_resize_complete()
self["cursor"] = "arrow"

说明:

  1. 控件继承Canvas,主要利用了canvas的画矩形功能。
  2. 初始化后绘制9个矩形,1个显示边框,8个用来调整大小,然后给调整大小的矩形绑定事件。
  3. show函数将9个矩形显示出来,使用coords函数重新修改矩形的位置,is_fill参数会填充8个小矩形的颜色,之后做多重选择的时候使用。
  4. hide函数将矩形移动到看不见的位置。
  5. on_mouse_move函数用来修改自己的尺寸,目前只支持anchor=’nw’的情况,移动后需要使用after_idle函数延迟调用show函数,因为修改尺寸后控件还没有更新,调用winfo_width,winfo_height获取不到真正的尺寸。

二、选中控件的使用

  1. 修改tkinterEditor.py,创建控件时先创建一个SelectedCanvas,然后将控件创建到SelectedCanvas中,控件创建后新增了绑定控件尺寸变化以及SelectedCanvas尺寸变化的函数

        def create_component_by_info(self, master, component_info, is_init_main, on_create_success=None):
    """ 暂时重写componentMgr的创建控件函数,主要是为了选中控件时的效果,以后可能会去掉 :param master: 父控件 :param component_info: 控件信息 :param is_init_main: 是否是初始化主界面时调用的 :param on_create_success: 创建成功回调 :return: 控件 """
    # 初始化调用的话直接调父类的就可以
    if is_init_main:
    return componentMgr.create_component_by_info(self, master, component_info, is_init_main, on_create_success)
    gui_type = component_info.get("gui_type", "None")
    if gui_type == "None":
    return None
    # toplevel直接调父类的
    if gui_type == "Toplevel":
    return componentMgr.create_component_by_info(self, master, component_info, is_init_main, on_create_success)
    # 创建一个frame套在真正的控件外面
    frame_prop = { 
    "background": master["background"],
    }
    frame, info = create_default_component(master, "SelectedCanvas", "None", frame_prop, False)
    # 创建控件
    component = create_component_from_dict(frame, component_info)
    frame.place_configure(x=int(component_info["x"]), y=int(component_info["y"]), anchor=component_info["anchor"])
    component.place_configure(x=0, y=0, anchor="nw")
    if on_create_success:
    on_create_success(component, component_info, master)
    # 创建children
    for child in component_info.get("children", ()):
    if component == None:
    print("create_component error component=" + child["component_name"])
    continue
    master2 = component.get_child_master()
    self.create_component_by_info(master2, child, is_init_main, on_create_success)
    return component
    def on_component_create(self, tab_num, is_quick_create, component, component_info, master):
    """ 控件创建成功回调 :param tab_num: 标签编号 :param is_quick_create: 是否是从功能快捷键创建的 :param component: 控件 :param component_info: 控件信息 :param master: 父控件 :return: None """
    data = self.file_tab_window.get_data(tab_num)
    master_edit_component = self.find_edit_component_by_component(master, data["path"])
    if master_edit_component is None:
    print("create component error", component_info)
    return
    if not hasattr(component, "get_child_master"):
    component.get_child_master = partial(self.default_get_child_master, component)
    edit_component = editComponent(self, component, component_info, master, master_edit_component)
    master_edit_component.add_child(edit_component, is_quick_create)
    child_master = component.get_child_master()
    child_master.bind("<Button-1>", partial(self.on_edit_component_selected, edit_component, False))
    child_master.bind("<Button-3>", partial(self.on_edit_component_button_3_click, edit_component))
    child_master.bind("<B1-Motion>", partial(self.on_edit_component_motion, edit_component))
    child_master.bind("<ButtonRelease-1>", partial(self.on_edit_component_btn_release, edit_component))
    child_master.bind("<Configure>", partial(self.on_edit_component_configure, edit_component))
    component.master.set_on_resize_complete(partial(self.on_edit_component_master_resize_complete, edit_component))
    def on_edit_component_configure(self, edit_component, event):
    """ 当控件尺寸变化时 :param edit_component: 编辑控件 :param event: event :return: None """
    edit_component.on_edit_component_configure(edit_component is self.selected_component)
    def on_edit_component_master_resize_complete(self, edit_component):
    """ 编辑控件master尺寸变化后调用 :param edit_component: 编辑控件 :return: None """
    edit_component.on_edit_component_master_resize_complete()
    def create_control(self, quick_name, gui_name, property_dict=None):
    """ 创建控件 :param quick_name: 快捷按钮名字 :param gui_name: 控件名字 :return: None """
    if self.get_selected_component() is None:
    return
    child_master = self.get_selected_component().component.get_child_master()
    if time.time() - self.created_time > 2:
    self.created_pos_x = 0
    self.created_pos_y = 0
    control_name = self.create_random_name(gui_name)
    prop = { 
    "background": "white", "x": self.created_pos_x, "y": self.created_pos_y,
    "component_name": control_name, "gui_type": gui_name,
    }
    if child_master["background"]== "white":
    prop["background"] = "SystemScrollbar"
    # 创建一个frame套在真正的控件外面
    frame_prop = { 
    "background": child_master["background"],
    }
    frame, info = create_default_component(child_master, "SelectedCanvas", "None", frame_prop, False)
    # 创建控件
    if property_dict is not None:
    property_dict["component_name"] = control_name
    property_dict["is_main"] = 0
    property_dict["x"] = prop["x"]
    property_dict["y"] = prop["y"]
    component = create_component_from_dict(frame, property_dict)
    else:
    component, property_dict = create_default_component(frame, gui_name, control_name, prop)
    frame.place_configure(x=int(property_dict["x"]), y=int(property_dict["y"]), anchor=property_dict["anchor"])
    component.place_configure(x=0, y=0, anchor="nw")
    self.on_component_create(self.file_tab_window.get_cur_tab(), True, component, property_dict, child_master)
    self.created_time = time.time()
    self.created_pos_x += 10
    self.created_pos_y += 10
    
  2. 修改componentEdited.py,删除初始化的can_not_sizing,选中控件时显示选中框,并且降低选中框的层级,这样选中框就不会覆盖在其他控件上,取消选中时隐藏选中框,编辑控件尺寸变化的时候修改选中框的尺寸,选中框尺寸变化的时候修改编辑控件的尺寸。

    class editComponent(ComponentDragAble):
    def __init__(self, editor, component, component_info, component_master, parent):
    ComponentDragAble.__init__(self)
    self.editor = editor
    self.component = component
    self.component_info = component_info
    self.component_master = component_master
    self.parent = parent
    self.children = []
    self.can_not_move = ("Toplevel",)
    def on_edit_component_select(self, is_tree_select):
    """ 被选中时 :return: None """
    self.editor.property_list.add_show_rows(self)
    if not is_tree_select:
    self.editor.treeview.tree.selection_set(self.name)
    self.component.master.place(
    x=int(self.component_info["x"]) - 8, y=int(self.component_info["y"]) - 8,
    width=int(self.component_info["pixel_width"]) + 16, height=int(self.component_info["pixel_height"]) + 16
    )
    self.component.place(x=8, y=8)
    self.component.master.tk.call('lower', self.component.master._w)
    self.component.master.show()
    def on_edit_component_cancel_select(self):
    """ 取消选中时 :return: None """
    if not self.component or not self.component.master:
    return
    self.component.master.place(
    x=self.component_info["x"], y=self.component_info["y"],
    width=self.component_info["pixel_width"], height=self.component_info["pixel_height"]
    )
    self.component.place(x=0, y=0)
    self.component.master.hide()
    def on_edit_component_configure(self, is_selected):
    """ 当编辑控件尺寸变化时 :return: None """
    width = self.component.winfo_width()
    height = self.component.winfo_height()
    if is_selected:
    width += 16
    height += 16
    update_single_prop(self.component.master, "pixel_width", width, "SelectedCanvas")
    update_single_prop(self.component.master, "pixel_height", height, "SelectedCanvas")
    if is_selected:
    self.component.master.after_idle(self.component.master.show)
    def on_edit_component_master_resize_complete(self):
    """ 编辑控件master尺寸变化后调用 :return: None """
    width = self.component.master.winfo_width() - 16
    height = self.component.master.winfo_height() - 16
    self.update_property({ "pixel_width": width, "pixel_height": height})
    def update_property(self, prop_dict, not_update=None):
    """ 更新属性 :param prop_dict: key:属性名,value:属性值 :param not_update: 不更新的 :return: None """
    try:
    # 更新属性信息
    self.component_info.update(prop_dict)
    # 更新属性列表
    if not_update != "prop_list":
    self.editor.property_list.update_property(self.component_info, prop_dict.keys())
    # 更新控件属性
    if not_update != "component":
    for prop_name, prop_value in prop_dict.items():
    # 更改坐标的话修改控件的parent
    if prop_name in ("x", "y"):
    update_single_prop(self.component.master, prop_name, int(prop_value) - 8, "SelectedCanvas")
    continue
    update_single_prop(self.component, prop_name, prop_value, self.gui_type)
    # 更改图片的话更新一下尺寸
    if prop_name == "image" and prop_value not in ("", "None"):
    self.component.after_idle(self.update_property,
    { 
    "pixel_width": self.component.winfo_reqwidth(),
    "pixel_height": self.component.winfo_reqheight(),
    }, "component"
    )
    # 更改背景的话更新一下child的背景
    if prop_name == "background":
    for child in self.children:
    update_single_prop(child.component.master, "background", prop_value, "SelectedCanvas")
    pass
    # 更新树
    if "component_name" in prop_dict:
    self.editor.refresh_tree()
    self.editor.treeview.tree.selection_set(prop_dict["component_name"])
    except Exception as e:
    print(e)
    
  3. 删除componentDragAble中改变尺寸的逻辑。

  4. 其他一些修改就不贴了。

上一篇记录

本文地址:https://blog.csdn.net/m0_51849494/article/details/110880443