跳转至

§2.1 画布容器控件

字数 1898 个   代码 67 行   阅读时间 7 分钟   访问量

一、以画布为底

1.1 为什么以画布为底

tkttkinter 程序不同,tkinter 程序是直接以窗口为底板,在上面放置控件的,但由于 tkt 的控件都是虚拟的,需要以画布为底板,在上面呈现出来。所以,在显示任何 tkt 控件之前,都需要一个画布来作为它们的容器。

1.2 将画布当作页面

我们可以把画布当作网页的一个个页面,每个画布都独立运作,这样层次就比较清晰,多个场景的切换也比较方便。

二、两种画布容器

2.1 Canvas

查阅文档可知,tkt.Canvas 继承自 tkinter.Canvas,它是在 tkinter.Canvas 的基础之上“魔改”而来的,增加了很多功能。

2.1.1 初始化参数的说明

直接查看文档可能还是不容易理解 tkt.Canvas 的初始化参数,这里做一个详细解释。

  • master: 父控件;
  • expand: 这个可以取得值有 4 个,但实际含义就是画布本身的缩放的规则,"x" 表示横向要缩放,"y" 表示纵向要缩放,两个都有就表示横纵都缩放,空字符串则表示不缩放;
  • zoom_item: 布尔值,标志是否缩放画布里面的东西;
  • keep_ratio: 可取值有 "min""max"None 标志缩放保持比例的规则,"min" 表示跟随最小的缩放比,"max" 表示跟随最大的缩放比,None 表示缩放不保持比例;
  • free_anchor: 布尔值,标志画布的锚点是否为动态的;
  • name: 样式处理的标识;

这里着重讲一下后面三个参数。

保持画布横纵比例意味着画布里面的东西永远不会变形,这对于某些场景极为重要,比如播放视频(后续会引入),或者小游戏的画面。有两种模式,"min" 像播放视频那样,保证画面始终在父控件之中,而不会溢出画面,而 "max" 则是保证画面一定被铺满,就算某些部分会被父控件遮挡。

free_anchor 则涉及到画布缩放,且只对 Place 布局有效。它会使锚点位置的是动态缩放的,而不是一直位于某个地方。比如锚点位于画布左上角,初始位置在 (100, 100),假设缩放倍率为 2,那么当 free_anchor 的值为 True 时,锚点的位置会变成 (200, 200),反之则没有任何变化。

参数 name 在后面我们还会看到,基本上每个控件都有这个参数,它是样式名称的标识,处理样式的时候会根据这个参数的值为其绑定对应的样式,若找不到则不做处理。因此当我们想临时自定义一个控件的颜色时,就需要把这个参数改成其它的,为避免和已有的样式名称冲突,一般将其改为空字符串。

2.1.2 隐藏 / 显示来切换页面

页面切换的方式有很多,你可以通过 tkinter 提供的方式,也可以通过 tkt 提供的方式。

当你使用 Place 布局时,在 tkinter 里面可以通过 place_forget 方法来将画布暂时隐藏掉。对于 Pack 和 Grid 布局也是类似的,可以使用 pack_forgetgrid_forget 隐藏画布。但是当你想重新显示画布的时候,你必须再次使用对应的布局方法。而这个时候,你需要对当前窗口的缩放做出一定的处理。

如果窗口这个时候已经放大了,但你还是使用之前的方式去显示画布,就会导致画布看起来变小了。

下面是一个不正确的示例:

import tkintertools as tkt

root = tkt.Tk()
root.center()

cv = tkt.Canvas(root, bg="orange", name="")
cv.place(width=1280, height=720)
cv.place_forget()

root.after(3000, lambda: cv.place(width=1280, height=720))
root.mainloop()

在上述程序开始运行后,用户可以快速缩放窗口,然后等待 3 秒,看看画布是否处于正确的大小。

很显然,上面的示例就会导致画布没有同步缩放。应该修改为如下代码:

import tkintertools as tkt

root = tkt.Tk()
root.center()

cv = tkt.Canvas(root, bg="orange", name="")
cv.place(width=1280, height=720)
cv.place_forget()


def func() -> None:
    cv.place(width=1280, height=720)
    cv.update_idletasks()#(1)!
    cv._re_place()#(2)!


root.after(3000, func)
root.mainloop()
  1. 更新画布,防止执行下一行的操作时画布的数据还没及时更新。
  2. 根据父控件和画布本身的数据重新调整画布的缩放。

此方法虽然可以,不过需要特别提醒的是,此方法仅对 Place 布局有效。

危险警告:尽量不要调用保护方法

示例的代码虽然目前可以用,但在第 14 行调用了画布的 _re_place 保护方法(方法名带下划线前缀),此方法目前并非公开的,后续其名称或 API 可能会发生改变!

2.1.3 动画移动来切换页面

当然除了上面说的这种方式之外,还有一种比较取巧的办法。那就是在程序开始的时候就把所有的画布一次性创建好,然后通过 Place 布局的方式将它们放置在看不到的位置,需要的时候就把他们移动到可见范围内,反之就移开。正好,tkt 提供了比较好的动画机制,可以试着用这种方式解决这个问题:

import tkintertools as tkt
import tkintertools.animation as animation

root = tkt.Tk()
cv1 = tkt.Canvas(root, name="", bg="orange")
cv1.place(width=1280, height=720, x=0)
cv2 = tkt.Canvas(root, name="", bg="lightgreen")
cv2.place(width=1280, height=720, x=-1280)

animation.MoveTkWidget(cv1, 1000, (1280, 0), controller=animation.smooth, fps=60).start(delay=1000)
animation.MoveTkWidget(cv2, 1000, (1280, 0), controller=animation.smooth, fps=60).start(delay=1000)

root.mainloop()

上述代码在运行 1 秒后,两个画布会有移动的动画。

特别注意:可能造成性能问题

对于上述第二种方式,在画布的数量较多时缩放窗口可能会产生性能问题。因为缩放机制会对当前所有“可见”的控件进行缩放(未第一次放置或者调用了类似 *_forget 方法的画布不可见),而这里的可见是指在程序层面可见的,故这种方式可能导致缩放时产生明显卡顿。

2.2 Frame

目前 tkt.Frame 的存在完全就是为了方便布局,它完全继承自 tkt.Canvas,唯一的区别是默认的颜色不同。亮色主题下会比 tkt.Canvas 更亮,暗色主题下会比 tkt.Canvas 更暗。

2.3 混合 tkinter 控件

画布可以承载 tkt 的虚拟控件,但它本身也是 tkinter 的控件,因此也可以包含 tkinter 的控件,这是完全可以的。

不过为了兼容性考虑,推荐在 tkt.Canvas 里的 tkinter 控件使用 Place 布局方式,这样在缩放时,其缩放模式会和 tkt 的虚拟控件保持一致。使用其它的布局方式也是可以,但无法保证缩放时能得到正确的结果。

import tkinter.ttk as ttk

import tkintertools as tkt
import tkintertools.core.constants as constants
import tkintertools.style as style

style.set_color_mode("light")

root = tkt.Tk(title="登录")
root.center()

cv = tkt.Canvas(root, zoom_item=True)
cv.place(width=1280, height=720)

ttk.Label(cv, text="账 号 登 录", font=(constants.FONT, -48), anchor="center").place(width=400, height=100, x=440, y=150)

ttk.Label(cv, text="账号").place(x=450, y=300)
ttk.Entry(cv, font=(constants.FONT, -20)).place(width=380, height=50, x=450, y=340)
ttk.Label(cv, text="密码").place(x=450, y=400)
ttk.Entry(cv, font=(constants.FONT, -20), show="●").place(width=380, height=50, x=450, y=440)

ttk.Button(cv, text="注 册").place(width=180, height=50, x=450, y=540)
ttk.Button(cv, text="登 录").place(width=180, height=50, x=650, y=540)

root.mainloop()

上面代码就是在 tkt.Canvas 里混合了 tkinter 控件的示例。

运行这段代码,你可能会对这个效果很熟悉。其实在前面的章节 §1.2 实现一个简单的界面 的示例代码中我们已经用 tkt 实现了类似的效果,这里只是用 tkinter.ttk 再次实现了它,但不同的是,这里是 tkt 混合 tkinter.ttk 做的,其不仅具有 tkinter.ttk 的样式,还拥有了 tkt 的功能。(1)

  1. 💡你可以试着缩放窗口,看看与原生的 tkinter.ttk 程序相比,有什么不同之处

2.4 嵌套画布

既然画布本身是 tkinter 控件,那自然画布可以嵌套在画布之中,当然,tkt.Canvastkt.Frame 之间也是可以任意嵌套的。

由于 tkt.Canvas 使用 PackGrid 布局时对虚拟控件的缩放不是非常友好,所以不妨尝试以 tkt.Frame 为底,用 tkinter 的布局方式得到合适的结果后,再往其中添入 tkt.Canvas。这或许对解决问题有一定的帮助。