tkinter可拖动可改变大小的组件

引言

关于tkinter基础组件的介绍和使用,网络上很多,但是对于可以像可视化编辑器编辑图片尺寸、移动图片位置的那种组件,网上很难找到什么资料。

这一类的组件用途广泛,像 Visual Basic 一样的可视化UI、图片编辑、页面排版等都可以应用到。

很多人认为tkinter这一类UI库不会有这样的控件,但大错特错

就现成的来说,tix中就有这样的控件,而且网上有使用范例。但是tix中的这个控件反应慢且不及时,同时还会在窗口上显示一个框来呈现改变的程度。实在是太落伍了。

因此,自己动手丰衣足食(当然,是站在巨人的肩膀上),我们现在开始来制作一个名为SelectedCanvas的组件,达成目标。

声明

团结才能胜利,这篇文章的技术来自两个部分。

主要部分是在CSDN中的archmage199制作的tkinterUI编辑器,该项目基于tkinter本身,本篇文章将使用其中的“Canvas选中项”。文章地址

另一个部分是在PYPI中qfcy提供的“tk-dragtool”。

本篇文章没有直接使用两种技术,而是基于这两种技术,重写了一个SelectedCanvas类,实现可以拖动、可以改变大小的高级组件。

目标

因为 archmage199 的SelectedCanvas还存在初始化不及时、使用没有实现完全的统一化、不可拖动、不可以根据焦点判断是否显示调整框等缺点,这里,我给出重写目标:

  1. 可以改变大小,可以被拖动
  2. 及时拖动画布内的被调节控件,也可以拖动整体
  3. 初始化及时
  4. 可以根据焦点判断是否显示调整框

现在,开始重写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, "<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"

调节框可拖动

基于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',"<Button-1>",self._mousedown,add='+')
        self.tag_bind('side',"<B1-Motion>",self._drag,add='+')
        self.tag_bind('side','<Enter>',lambda event:self.config(cursor='fleur'))
        self.tag_bind('side','<Leave>',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("<Button-1>",self.mousedown,add='+')
        self.widget.bind("<B1-Motion>",self.drag,add='+')

根据焦点判断是否显示调节框

调节框虽然能够起到很大作用,但是,一直显示这个调节框自然不爽。

当组件获得焦点时,重绘调节框。当失去焦点时,清除调节框。

我首先在 create_widget 中添加了如下事件绑定:

        self.widget.bind('<FocusOut>',lambda event:self.delete('all'))
        self.widget.bind('<FocusIn>',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',"<Button-1>",self._mousedown,add='+')
        self.tag_bind('side',"<B1-Motion>",self._drag,add='+')
        self.tag_bind('side','<Enter>',lambda event:self.config(cursor='fleur'))
        self.tag_bind('side','<Leave>',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, "<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):
        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("<Button-1>",self.mousedown,add='+')
        self.widget.bind("<B1-Motion>",self.drag,add='+')
        self.widget.bind('<FocusOut>',lambda event:self.delete('all'))
        self.widget.bind('<FocusIn>',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创新☀

  • 11
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值