跳转至

§2.1 画布容器控件

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

一、以画布为底

1.1 为什么以画布为底

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

1.2 将画布当作页面

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

二、画布

2.1 Canvas

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

2.1.1 初始化参数的说明

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

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

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

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

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

auto_updateTrue 时,maliang 的样式管理机制会自动根据窗口的主题修改画布的主题,也就是说,当它的值为 True 的时候,你无法修改画布的主题,就算通过某种手段修改了,当窗口主题发生变化时,画布的主题还是会自动更新。相对应地,如果你想自定义画布的样式,如背景颜色等,你可能需要将这个参数设为 False,此时画布的一切样式都会回归 tkinter 的默认值。当然,此时你可以自定义画布的主题:

1
2
3
4
5
6
7
import maliang

root = maliang.Tk()
cv = maliang.Canvas(auto_update=False, bg="lightyellow")
cv.pack()

root.mainloop()

上述代码可以让你的画布不受窗口主题变化的影响,但同样的,画布的外观细节都需要你自己设置。

2.1.2 隐藏/显示来切换页面

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

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

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

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

import maliang

root = maliang.Tk()
root.center()

cv = maliang.Canvas(bg="orange", auto_update=False)
cv.place(width=1280, height=720)
cv.place_forget()

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

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

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

import maliang

root = maliang.Tk()
root.center()

cv = maliang.Canvas(bg="orange", auto_update=False)
cv.place(width=1280, height=720)
cv.place_forget()


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


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

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

2.1.3 动画移动来切换页面

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

import maliang
import maliang.animation as animation

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

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

root.mainloop()

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

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

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

2.2 混合 tkinter 控件

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

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

import tkinter.ttk as ttk

import maliang
import maliang.theme as theme

theme.set_color_mode("light")

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

cv = maliang.Canvas(auto_zoom=True)
cv.place(width=1280, height=720)

ttk.Label(cv, text="账 号 登 录", font=(maliang.Font.family, -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=(maliang.Font.family, -20)).place(width=380, height=50, x=450, y=340)
ttk.Label(cv, text="密码").place(x=450, y=400)
ttk.Entry(cv, font=(maliang.Font.family, -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()

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

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

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

2.4 嵌套画布

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