使用tkinter制作tkinterUI编辑器
前言
这篇文章记录一下滚动canvas控件的制作,在制作编辑器的过程中我有很多地方需要实现滚动页面的操作,网上大部分都是教怎么滚动列表的,但是我需要滚动的不是列表,我需要实现滚动任何控件的滑动条,在网上找了很久,发现canvas可以实现我想要的功能。
一、制作ScrollCanvas之前
- 在componentProperty.py中添加一个获取控件默认属性的函数,如下:
def get_default_component_info(component_type, prop=None): """ 根据控件类型获取控件默认属性 :param component_type: 控件类型 :param prop: 需要更新的属性 :return: dict """ property_dict = {} for prop_name, value in PROP_CONFIGURE.items(): if component_type not in value: continue property_dict[prop_name] = value[component_type][0] for prop_name, value in PROP_PLACE_CONFIGURE.items(): if component_type not in value: continue property_dict[prop_name] = value[component_type][0] for prop_name, value in PROP_EXT.items(): if component_type not in value: continue property_dict[prop_name] = value[component_type][0] for prop_name, value in PROP_LIKE_TOP_LEVEL.items(): if component_type not in value: continue property_dict[prop_name] = value[component_type][0] if prop is not None: property_dict.update(prop) return property_dict
- 在components.py中添加一个创建默认控件的函数,如下:
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
- 创建默认控件的函数会在后面的文章里使用,这里会把这个函数复制到ScrollCanvas.py中,因为不能循环引用,我只能先拷贝一下,components.py中还需要导入我新加的控件,from ScrollCanvas import ScrollCanvas
二、ScrollCanvas实现
先上代码,ScrollCanvas.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
from tkinter import Canvas, Frame, Scrollbar
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 ScrollCanvas(Canvas):
def __init__(self, master=None, cnf={}, **kw):
Canvas.__init__(self, master, cnf, **kw)
self.is_show_scroll_x = 1 # 是否显示水平滚动条
self.is_show_scroll_y = 1 # 是否显示垂直滚动条
self.is_always_show_scroll = 1 # 是否总是显示滚动条
self.scroll_x_height = 17 # 水平滑动条默认高度
self.scroll_x_width = 200 # 水平滑动条默认宽度
self.scroll_y_height = 200 # 垂直滑动条默认高度
self.scroll_y_width = 17 # 垂直滑动条默认宽度
def set_is_show_scroll_x(self, is_show_scroll_x):
"""
设置是否显示水平滑动条
:param is_show_scroll_x:是否显示
:return:None
"""
if self.is_show_scroll_x == is_show_scroll_x:
return
self.is_show_scroll_x = is_show_scroll_x
self.do_layout_need_control()
def get_is_show_scroll_x(self):
"""
获取是否显示水平滑动条
:return:bool
"""
return self.is_show_scroll_x
def set_is_show_scroll_y(self, is_show_scroll_y):
"""
设置是否显示垂直滑动条
:param is_show_scroll_y:是否显示
:return:None
"""
if self.is_show_scroll_y == is_show_scroll_y:
return
self.is_show_scroll_y = is_show_scroll_y
self.do_layout_need_control()
def get_is_show_scroll_y(self):
"""
获取是否显示垂直滑动条
:return:bool
"""
return self.is_show_scroll_y
def set_is_always_show_scroll(self, is_always_show_scroll):
"""
设置是否一直显示滑动条
:param is_always_show_scroll:是否一直显示
:return:None
"""
if self.is_always_show_scroll == is_always_show_scroll:
return
self.is_always_show_scroll = is_always_show_scroll
self.do_layout_need_control()
def get_is_always_show_scroll(self):
"""
获取是否一直显示滑动条
:return:bool
"""
return self.is_always_show_scroll
@property
def scroll_bar_x(self):
return self.children.get("scroll_bar_x", None)
@property
def scroll_bar_y(self):
return self.children.get("scroll_bar_y", None)
@property
def slide_window(self):
return self.children.get("slide_window", None)
def on_update(self):
"""
初始化后会被调用,在这里创建滚动条和滑动窗口
:return: None
"""
self.create_need_control()
self.update_scroll()
def create_need_control(self):
"""
创建所需控件
:return:None
"""
self.create_slide_window()
self.create_scroll_bar()
self.do_layout_need_control()
def create_slide_window(self):
"""
创建滑动窗口
:return:None
"""
prop = {
"background": self["background"],
}
create_default_component(self, "Frame", "slide_window", prop)
self.create_window((1, 1), window=self.slide_window, anchor="nw")
self.slide_window.bind("<MouseWheel>", self.scroll_slide_window_y)
self.slide_window.bind("<Control-MouseWheel>", self.scroll_slide_window_x)
def create_scroll_bar(self):
"""
创建滑动条
:return:None
"""
prop_scroll_y = {
"command": self.yview,
"width": self.scroll_y_width, "height": self.scroll_y_height
}
create_default_component(self, "Scrollbar", "scroll_bar_y", prop_scroll_y)
prop_scroll_x = {
"orient": "horizontal", "command": self.xview,
"width":self.scroll_x_width, "height":self.scroll_x_height
}
create_default_component(self, "Scrollbar", "scroll_bar_x", prop_scroll_x)
# 绑定滑动条事件
self.configure(xscrollcommand=self.scroll_bar_x.set)
self.configure(yscrollcommand=self.scroll_bar_y.set)
def do_layout_need_control(self):
"""
重新布局界面
:return:None
"""
self.do_layout_scroll_bar_x()
self.do_layout_slide_window()
self.do_layout_scroll_bar_y()
def do_layout_scroll_bar_x(self):
"""
重新布局水平滑动条
:return: None
"""
if self.scroll_bar_x is None:
return
self.scroll_bar_x.place_configure(x=1, y=int(self["height"]) - self.scroll_x_height)
self.scroll_bar_x.place_configure(width=int(self["width"]) - int(self.scroll_bar_y.place_info().get("width", 0)) - 1)
self.scroll_bar_x.place_configure(height=self.scroll_x_height)
# 隐藏水平滑动条
if not self.get_is_show_scroll_x():
self.scroll_bar_x.place_forget()
def do_layout_scroll_bar_y(self):
"""
重新布局垂直滑动条
:return: None
"""
if self.scroll_bar_y is None:
return
self.scroll_bar_y.place_configure(x=int(self["width"]) - int(self.scroll_y_width), y=2)
self.scroll_bar_y.place_configure(width=self.scroll_y_width)
self.scroll_bar_y.place_configure(height=int(self["height"]) - 2)
# 隐藏垂直滑动条
if not self.get_is_show_scroll_y():
self.scroll_bar_y.place_forget()
def do_layout_slide_window(self):
"""
重新布局slide window
:return: None
"""
if self.slide_window is None:
return
self.slide_window["width"] = int(self["width"])
self.slide_window["height"] = int(self["height"])
def update_scroll(self):
"""
更新滑动条
:return:None
"""
self.update_scroll_vertical()
self.update_scroll_horizontal()
self.configure(scrollregion=self.bbox("all"))
def update_scroll_vertical(self):
"""
更新垂直滑动条
:return:None
"""
pos_y = self.calc_slide_window_height()
is_always_show = self.get_is_always_show_scroll()
visible = False
if pos_y > int(self["height"]):
self.slide_window["height"] = pos_y + 20
visible = True
else:
if int(self.slide_window["height"]) > int(self["height"]):
self.slide_window["height"] = int(self["height"]) - self.scroll_x_height
# 一直显示垂直滑动条
if is_always_show:
visible = True
if not self.get_is_show_scroll_y():
visible = False
if visible:
self.do_layout_scroll_bar_y()
else:
self.scroll_bar_y.place_forget()
def calc_slide_window_height(self):
"""
计算滑动窗口的高度
:return: int
"""
pos_y = 0
for (childName, child) in self.slide_window.children.items():
if int(child.place_info()["y"]) + child.winfo_reqheight() > pos_y:
pos_y = int(child.place_info()["y"]) + child.winfo_reqheight()
return pos_y
def update_scroll_horizontal(self):
"""
更新水平滑动条
:return:None
"""
pos_x = self.calc_slide_window_width()
is_always_show = self.get_is_always_show_scroll()
visible = False
if pos_x > int(self["width"]):
self.slide_window["width"] = pos_x + 20
visible = True
else:
if int(self.slide_window["width"]) > int(self["width"]):
self.slide_window["width"] = int(self["width"]) - self.scroll_y_width
# 一直显示垂直滑动条
if is_always_show:
visible = True
if not self.get_is_show_scroll_x():
visible = False
if visible:
self.do_layout_scroll_bar_x()
else:
self.scroll_bar_x.place_forget()
def calc_slide_window_width(self):
"""
计算滑动窗口的宽度
:return: int
"""
pos_x = 0
for (childName, child) in self.slide_window.children.items():
if int(child.place_info()["x"]) + child.winfo_reqwidth() > pos_x:
pos_x = int(child.place_info()["x"]) + child.winfo_reqwidth()
return pos_x
def scroll_slide_window_y(self, event):
"""
垂直滚动页面
:param event:
:return:None
"""
if int(self.slide_window["height"]) <= int(self["height"]):
return
units = -5 if event.delta > 0 else 5
self.yview_scroll(units, "units")
def scroll_slide_window_x(self, event):
"""
水平滚动页面
:param event:
:return:None
"""
if int(self.slide_window["width"]) <= int(self["width"]):
return
units = -5 if event.delta > 0 else 5
self.xview_scroll(units, "units")
def get_child_master(self):
return self.slide_window
def on_end_drag_master(self):
self.update_scroll()
def on_size_change(self):
"""
窗口尺寸变化时的处理
:return: None
"""
self.do_layout_need_control()
def refresh_slide_window_bg(self):
"""
刷新slide_window背景
:return: None
"""
prop = {
"background": self["background"],
}
self.slide_window.configure(prop)
@staticmethod
def create_default(master, prop=None):
return create_default_component(master, "ScrollCanvas", None, prop, False)
说明:
- 我制作这个的思路就是当有任意子控件超过父控件的高度或者宽度时就可以进行滚动
- 计算高度和宽度时用到了winfo_reqwidth和winfo_reqheight函数,这两个函数是计算真实像素的,属性里面的width和height在文本类的控件里取的都是字符的高度和宽度,所以不能用
- 滚动canvas的话需要在canvas里创建一个window,然后把子控件都创建到这个window里进行滚动,使用self.create_window((1, 1), window=self.slide_window, anchor=“nw”)创建window,使用self.configure(scrollregion=self.bbox(“all”))设置滚动区域
- get_child_master这个函数就是上一章在创建子控件的时候调用的,ScrollCanvas的子控件需要创建到slide_window中
- scroll_slide_window_x和scroll_slide_window_y函数是鼠标滑轮滚动时调用的
- on_update函数是上一章更新属性最后调用的,就是为了在更新完属性之后创建滑动条与滑动窗口
- on_end_drag_master,on_size_change,refresh_slide_window_bg这三个函数是后面的文章里使用的,这里不用管
- is_show_scroll_x,is_show_scroll_y,is_always_show_scroll这3个属性是我加的自定义属性,之后在编辑器里可以修改
- is_show_scroll_x设置成0的话就始终不会显示水平滑动条,is_show_scroll_y设置成0的话就始终不会显示垂直滑动条,is_always_show_scroll设置成0的话子控件没有超过父控件的高度或宽度的话滑动条是不会显示出来的
三、ScrollCanvas测试
- 修改tkinterEditor.xml中的主界面类型,<gui_type>ScrollCanvas</gui_type>
- 给tkinterEditor.xml中的主界面添加3个属性
<is_show_scroll_x>1</is_show_scroll_x>
<is_show_scroll_y>1</is_show_scroll_y>
<is_always_show_scroll>1</is_always_show_scroll> - 修改tkinterEditor.xml中button的位置,让button的位置超过主界面的宽高即可
<x>531</x>
<y>583</y> - 修改tkinterEditor.py文件的初始化函数,调用一下ScrollCanvas的update_scroll函数,调用这个函数会重新计算是否可以滚动,上面忘记说明了,代码如下:
class tkinterEditor(componentMgr): def __init__(self, master, gui_path): componentMgr.__init__(self, master) self.load_from_xml(master, gui_path, True) self.master.children["frame_6615dd7e1e7911eb8d886045cb848950"].update_scroll()
- 更新componentProperty.py这个文件,这个文件我加了很多其他控件的属性,属性太多了,下一章进行记录
- 运行tkinterEditor.py,如下:
- 操作滑动条后,如下: