【记录】python3 使用tkinter制作tkinterUI编辑器 《四》

使用tkinter制作tkinterUI编辑器



前言

这篇文章记录一下滚动canvas控件的制作,在制作编辑器的过程中我有很多地方需要实现滚动页面的操作,网上大部分都是教怎么滚动列表的,但是我需要滚动的不是列表,我需要实现滚动任何控件的滑动条,在网上找了很久,发现canvas可以实现我想要的功能。

完整代码已上传到github,可从第一篇记录下载


一、制作ScrollCanvas之前

  1. 在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
    
  2. 在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
    
  3. 创建默认控件的函数会在后面的文章里使用,这里会把这个函数复制到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)

说明:

  1. 我制作这个的思路就是当有任意子控件超过父控件的高度或者宽度时就可以进行滚动
  2. 计算高度和宽度时用到了winfo_reqwidth和winfo_reqheight函数,这两个函数是计算真实像素的,属性里面的width和height在文本类的控件里取的都是字符的高度和宽度,所以不能用
  3. 滚动canvas的话需要在canvas里创建一个window,然后把子控件都创建到这个window里进行滚动,使用self.create_window((1, 1), window=self.slide_window, anchor=“nw”)创建window,使用self.configure(scrollregion=self.bbox(“all”))设置滚动区域
  4. get_child_master这个函数就是上一章在创建子控件的时候调用的,ScrollCanvas的子控件需要创建到slide_window中
  5. scroll_slide_window_x和scroll_slide_window_y函数是鼠标滑轮滚动时调用的
  6. on_update函数是上一章更新属性最后调用的,就是为了在更新完属性之后创建滑动条与滑动窗口
  7. on_end_drag_master,on_size_change,refresh_slide_window_bg这三个函数是后面的文章里使用的,这里不用管
  8. is_show_scroll_x,is_show_scroll_y,is_always_show_scroll这3个属性是我加的自定义属性,之后在编辑器里可以修改
  9. is_show_scroll_x设置成0的话就始终不会显示水平滑动条,is_show_scroll_y设置成0的话就始终不会显示垂直滑动条,is_always_show_scroll设置成0的话子控件没有超过父控件的高度或宽度的话滑动条是不会显示出来的

三、ScrollCanvas测试

  1. 修改tkinterEditor.xml中的主界面类型,<gui_type>ScrollCanvas</gui_type>
  2. 给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>
  3. 修改tkinterEditor.xml中button的位置,让button的位置超过主界面的宽高即可
    <x>531</x>
    <y>583</y>
  4. 修改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()
    
  5. 更新componentProperty.py这个文件,这个文件我加了很多其他控件的属性,属性太多了,下一章进行记录
  6. 运行tkinterEditor.py,如下:
    在这里插入图片描述
  7. 操作滑动条后,如下:
    在这里插入图片描述

上一篇记录 下一篇记录

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
这是一个VB6的IDE插件(Addin),使用VB6的IDE直接设计Python的界面。 Python和VB都是能让人快乐的编程语言,我使用Python之后,很多自己使用的工具都使用Python开发或改写了,因为最终实现的Python代码实在太短了(相比VB),有时候Python一行代码就可以实现VB一个函数的功能。 Python就是这种让人越用越开心的语言。 不过说实在,使用Python开发GUI界面还是麻烦了一些了,自的标准库Tkinter使用起来非常简单,不过对于习惯了VB拖放控件完成界面设计的偶来说,还是不够人性化。TK也有一个工具叫GUI Builder,不过它使用Layout布局,不够直观,而且用起来很不爽。。 至于PyQt/wxPython等GUI库,尽管有可视化设计工具,但总感觉做一般的轻量级应用是杀鸡用牛刀, 而且不够环保,不够低碳,要一个很大的库,需要目标机器上夜同样安装了PyQt/wxPython,做不了绿色软件。 所以最终的结果是我更喜欢Tkinter,用起来很简单,绿色环保,真正的跨平台,一个py文件到处运行(担心泄密就编译成pyc)。 很多人都认为TK的界面不够美观,不过我经过多次实验后发现导入Python的标准TTK主题库,界面非常Native,不输PyQt/wxPython。 此Addin默认启用TTK支持,也可选择关闭。 总而言之,轻量级GUI,TK+TTK足够。 使用此Addin,你可以不用写一句代码就可以生成一个完整可运行的Python的GUI界面,支持2.X和3.X。 安装方法:将压缩包解压到你希望的目录,然后执行Setup.exe完成注册插件过程,打开VB6就可以用了。 在VB窗体上设计完成界面后(你可以大胆的设置各控件的属性,Addin尽量将其翻译为tkinter的控件属性),点工具栏上的VisualTkinter(图标为一片羽毛),再点'生成代码'按钮,即可生成可运行的Python代码,可以拷贝至剪贴板或保存至文件。 一般情况下你可以不用再改变tkinter的控件属性,但是如果你熟悉tkinter,需要更多的控制,可以一一核对各属性,并且修改,再生成代码。 当然除了用来设计界面外,此ADDIN内置的各控件属性列表可以做为编程参考,比较完整,除了极少数我认为大多数人都不用的属性外,属性定义基本上是我从官方的tkinter文档直接翻译的。 如果还没有VB6,网上找一个VB6精简版即可,不到20M,小巧玲珑。 代码已经在Github上托管,更新的版本可以在这上面找到,需求也可以在上面提: https://github.com/cdhigh/Visual-Tkinter-for-Python
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

archmage199

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值