关于tkinter基础组件的介绍和使用,网络上很多,但是对于可以像可视化编辑器编辑图片尺寸、移动图片位置的那种组件,网上很难找到什么资料。
这一类的组件用途广泛,像 Visual Basic 一样的可视化UI、图片编辑、页面排版等都可以应用到。
很多人认为tkinter这一类UI库不会有这样的控件,但大错特错。
就现成的来说,tix中就有这样的控件,而且网上有使用范例。但是tix中的这个控件反应慢且不及时,同时还会在窗口上显示一个框来呈现改变的程度。实在是太落伍了。
因此,自己动手丰衣足食(当然,是站在巨人的肩膀上),我们现在开始来制作一个名为SelectedCanvas的组件,达成目标。
团结才能胜利,这篇文章的技术来自两个部分。
主要部分是在CSDN中的archmage199制作的tkinterUI编辑器,该项目基于tkinter本身,本篇文章将使用其中的“Canvas选中项”。文章地址
另一个部分是在PYPI中qfcy提供的“tk-dragtool”。
本篇文章没有直接使用两种技术,而是基于这两种技术,重写了一个SelectedCanvas类,实现可以拖动、可以改变大小的高级组件。
因为 archmage199 的SelectedCanvas还存在初始化不及时、使用没有实现完全的统一化、不可拖动、不可以根据焦点判断是否显示调整框等缺点,这里,我给出重写目标:
现在,开始重写SelectedCanvas。
根据 archmage199 的文章,我们可以得到SelectedCanvas的原型:
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, "" , partial(self.on_mouse_enter, name))
self.tag_bind(name, "" , partial(self.on_mouse_leave, name))
self.tag_bind(name, "" , partial(self.on_mouse_click, name))
self.tag_bind(name, "" , partial(self.on_mouse_move, name))
self.tag_bind(name, "" , 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"
基于tk-dragtool,在SelectedCanvas类中添加如下两个函数:
def _mousedown(self,event):#通过拖动画布移动
self.startx=event.x
self.starty=event.y
def _drag(self,event):
try:
self.place(x=self.winfo_x()+(event.x-self.startx),y=self.winfo_y()+(event.y-self.starty))
except AttributeError:
raise ValueError("The widget %s is not draggable"%widget)
在 on_update() 函数中添加:
self.tag_bind('side',"" ,self._mousedown,add='+')
self.tag_bind('side',"" ,self._drag,add='+')
self.tag_bind('side','' ,lambda event:self.config(cursor='fleur'))
self.tag_bind('side','' ,lambda event:self.config(cursor='arrow'))
这样就可在拖动调节框的边框是拖动画布了。
在原来的SelectedCanvas中,并没有一个能够加入子组件的方法。如果需要自己添加,则组件必须使用pack方法,还要设置padx、pady等参数,未免有些麻烦。
这里,我加入了一个创建子组件的方法,而且能够避免子组件重复加入。
首先,在初始化中加入:
def __init__(self, master=None, cnf={}, **kw):
#...
self.have_child=False
然后创建一个 create_widget 函数:
def create_widget(self,widget_class,cnf={},**kw):
"""
创建组件
:widget_class: 组件类
:kw: 组件的参数,不需要指定父组件
"""
if self.have_child==True:#如果已经创建,则忽略
return
self.have_child=True
self.widget=widget_class(self,cnf,**kw)
self.widget.pack(fill='both',expand=True,pady=9,padx=9)
这样,如果要加入一个Label组件,只需要这样写:
#...
selectedcanvas.create_widget(Label,text='text',bg='black',fg='white')
在使用中,我们不一定非要拖动调节框,我们要是能够直接拖动组件,让整体都能够移动就好了。
现在大改tk-dragtool,在SelectedCanvas添加如下两个函数:
def mousedown(self,event):
self.__startx=event.x
self.__starty=event.y
def drag(self,event):
self.place(x=self.winfo_x()+(event.x-self.__startx),
y=self.winfo_y()+(event.y-self.__starty))
在 create_widget 中添加:
self.widget.bind("" ,self.mousedown,add='+')
self.widget.bind("" ,self.drag,add='+')
调节框虽然能够起到很大作用,但是,一直显示这个调节框自然不爽。
当组件获得焦点时,重绘调节框。当失去焦点时,清除调节框。
我首先在 create_widget 中添加了如下事件绑定:
self.widget.bind('' ,lambda event:self.delete('all'))
self.widget.bind('' ,lambda event:(self.on_update(),self.show()))
但我发现并不是每一个子组件都能按照我预期地去工作。原来,并不是每一个控件都能够得到使用者的焦点。只有交互类控件,如:按钮、文本框等,而像Label这样的静态组件,则不会获取焦点。
不过,当我们想要拖动控件,也就是点击它时,它就应当获得焦点。因此,在 mousedown 中添加一行代码,使组件获得焦点:
self.widget.focus_set()
至此,我们完成了强大的SelectedCanvas。来看一下完整代码:
class SelectedCanvas(Canvas):
'''可调整组件大小、可移动的画布'''
def __init__(self, master=None, cnf={}, **kw):
Canvas.__init__(self, master, cnf, **kw)
self.config(bd=0,highlightthickness = 0)
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
self.have_child=False#用以辨别是否有组件创建
def _mousedown(self,event):
self.startx=event.x
self.starty=event.y
def _drag(self,event):
try:
self.place(x=self.winfo_x()+(event.x-self.startx),y=self.winfo_y()+(event.y-self.starty))
except AttributeError:
raise ValueError("The widget %s is not draggable"%widget)
def set_on_resize_complete(self, on_resize_complete):
self.on_resize_complete = on_resize_complete
def on_update(self):
self.create_rectangle(-1, -1, -2, -2, tag='side', dash=3, outline='grey')
self.tag_bind('side',"" ,self._mousedown,add='+')
self.tag_bind('side',"" ,self._drag,add='+')
self.tag_bind('side','' ,lambda event:self.config(cursor='fleur'))
self.tag_bind('side','' ,lambda event:self.config(cursor='arrow'))
for name in ('nw', 'w', 'sw', 'n', 's', 'ne', 'e', 'se'):
self.create_rectangle(-1, -1, -2, -2, tag=name, outline='blue')
self.tag_bind(name, "" , partial(self.on_mouse_enter, name))
self.tag_bind(name, "" , partial(self.on_mouse_leave, name))
self.tag_bind(name, "" , partial(self.on_mouse_click, name))
self.tag_bind(name, "" , partial(self.on_mouse_move, name))
self.tag_bind(name, "" , partial(self.on_mouse_release, name))
def show(self, is_fill=False):
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='blue')
def hide(self):
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):
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):
if self.is_sizing:
return
self["cursor"] = "arrow"
def on_mouse_click(self, tag_name, event):
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):
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):
self.is_sizing = False
if self.on_resize_complete is not None:
self.on_resize_complete()
self["cursor"] = "arrow"
def create_widget(self,widget_class,cnf={},**kw):
if self.have_child==True:#如果已经创建,则忽略
return
self.have_child=True
self.widget=widget_class(self,cnf,**kw)
self.widget.pack(fill='both',expand=True,pady=9,padx=9)
#即使拖动组件,也可以移动
self.widget.bind("" ,self.mousedown,add='+')
self.widget.bind("" ,self.drag,add='+')
self.widget.bind('' ,lambda event:self.delete('all'))
self.widget.bind('' ,lambda event:(self.on_update(),self.show()))
def mousedown(self,event):
self.widget.focus_set()
self.__startx=event.x
self.__starty=event.y
def drag(self,event):
self.place(x=self.winfo_x()+(event.x-self.__startx),y=self.winfo_y()+(event.y-self.__starty))
a=Tk()
a.geometry('500x500+750+20')
b=SelectedCanvas(a)
#添加组件请使用可移动改变大小画布中的 create_widget() 方法
b.create_widget(Label,text='可调节Label',font=('微软雅黑',12),
fg='white',bg='black')
b.place(x=30,y=70,width=150,height=50)
b.update()
c=Text(a,height=5)
c.pack(side='top')
c.insert(1.0,'''这是一个可以改变大小、改变位置的组件。
该组件由可移动画布为框架,使用 create_widget() 创建了一个Label控件。
当该控件失去焦点时,调节器会隐藏。
当该控件获得焦点时,调节器会显示。
拖动边框或组件均可以移动整体。''')
d=SelectedCanvas(a)
d.create_widget(Button,text='~~~button☀tkinter创新☀~~~',font=('微软雅黑',11))
d.place(x=20,y=130,width=200,height=60)
d.update()
a.mainloop()
我们基于团结而一起开发出 SelectedCanvas 组件,可以运用到很多场景,再一次证明了tkinter确实有极大的拓展能力。
☀tkinter创新☀