你觉得这篇文章怎么样? 帮助我们为您提供更好的内容。
Thank you! Your feedback has been received.
There was a problem submitting your feedback, please try again later.
你觉得这篇文章怎么样?
作者 |
张嘉钧 |
难度 |
普通 |
材料表 |
Windows 或Linux 环境计算机 |
目录
Tkinter 简介
因为近年来Python的使用率大幅提升,根据TIOBE的数据显示2020年全球的程序语言使用率Python稳居第三,而PyPl的套件安装Python的数量则是第一名。为什么提到这件事呢? Tkinter是TK GUI整合到Python中的GUI开发工具包,更白话一点就是Python内建的GUI设计套件,当你使用Python开发项目的时候可以考虑使用Tkinter来制作操作接口。在Python开发GUI的榜上前两位是Tkinter与PyQt,两者的差别:pyqt整合度高、有图形化接口可以使用;Tkinter因为是Python自带的GUI套件,功能简单但效能可能更好。如果要追求高颜质的接口,我比较推荐pyqt,但如果是只简单的接口设计,追求效率就选择Tkinter吧!
1、第一个 Tkinter程序 : Hello World
我们来建构一个简单的窗口并且显示Hello World吧!
步骤一、导入 Tkinter 函式库 ( Python 3 ),如果使用Python 2 则T要大写
import tkinter as tk
步骤二、定义一个窗口 名叫 window
window = tk.Tk()
步骤三、设定标题
window.title('window')
步骤四、设定像素大小
window.geometry('600x800')
步骤五、宣告一个标签
lbl_1 = tk.Label(window, text='Hello World', bg='yellow', fg='#263238', font=('Arial', 12))
步骤六、设定放置的位置 ( 使用 grid 布局 )
lbl_1.grid(column=0, row=0)
步骤七、主窗口循环显示
window.mainloop()
完整程序代码如下:
import tkinter as tk
window = tk.Tk()
window.title('window')
window.geometry('500x100')
lbl_1 = tk.Label(window, text='Hello World', bg='yellow', fg='#263238', font=('Arial', 12))
lbl_1.grid(column=0, row=0)
window.mainloop()
这样就完成一个简单的GUI接口啰!结果如下:
二、窗口组件总揽
这边顺便提供一个很实用的教学文件:https://tkdocs.com/tutorial/index.html。
类别 | 介绍 |
Frame | 窗口。 |
Label | 文字卷标。 |
Button | 按钮。 |
Canvas | 可以用来绘图、文字等都可以,像我就会来拿放图片。 |
Checkbutton | 核取按钮。 |
Entry | 文字输入栏。 |
Listbox | 列表选单。 |
Menu | 选单列的下拉式选单。 |
LabelFrame | 文字卷标窗口。 |
MenuButton | 选单的选项。 |
Message | 类似 Label ,可多行。 |
OptionMenu | 下拉式的选项选单。 |
PaneWindow | 类似 Frame ,可包含其他窗口组件。 |
Radiobutton | 单选按钮。 |
Scale | 拉杆。 |
Scrollbar | 滚动条。 |
Spinbox | 微调器 |
Text | 文本框。 |
Toplevel | 新增窗口。 |
三、简单的组件实作
由于组件的参数或详细用法网络上已经有很多介绍了,所以我这边快速带过。
3-1. 标签 ( Label )
我们在宣告标签的时候,需要先宣告一个卷标,再给予位置,如果没有给予位置信息的话将不会被放在窗口上面,这边我们使用grid来告诉窗口卷标要放在该容器中的 (0, 0) 这个位置,此外没有给定窗口宽、高大小的话,窗口会依照Widget大小而自动去调整:
import tkinter as tk
window = tk.Tk()
window.title('window')
def create_label(txt):
lbl_1 = tk.Label(window, text=txt, bg='yellow', fg='#263238', font=('Arial', 12), width=100, height=2)
lbl_1.grid(column=0, row=0)
create_label('Hello World !!!')
window.mainloop()
稍微介绍一下组件的参数,大部分的组件在宣告的时候,可以引入的参数都雷同,第一个是你要放在哪个容器当中,我们给予window,代表这个标签会放在window当中,而text代表文字,bg是background背景,fg是foreground前景,可以当作是文字的颜色,font是文字的格式width、height是宽与高,这边要注意的是他们的单位是字符宽度、字符高度,而不是像素值,此外还有很多属性可以用像是边框、影像、对齐样式等等。
3-2. 按钮 ( Button )
上方宣告 label的时候是将 width 跟 height 带入到宣告的时候,也有另外一个做法,就是使用字典的方式宣告属性,bt_1 的 ‘width’ 属性要定义成如何,这边可以注意到 width 是定义字符的宽、height是定义字符的高,我个人觉得tkinter的组件大小很难控制,与分辨率大小没有一定的关系,待会在教怎么样可以平均大小跟自动缩放;此外这边还有提供 activebackground跟activeforeground为按下按钮的背景、前景颜色变化。
import tkinter as tk
window = tk.Tk()
window.title('window')
def create_button(txt):
bt_1 = tk.Button(window, text=txt, bg='red', fg='white', font=('Arial', 12))
bt_1['width'] = 50
bt_1['height'] = 4
bt_1['activebackground'] = 'red' # 按钮被按下的背景颜色
bt_1['activeforeground'] = 'yellow' # 按钮被按下的文字颜色 ( 前景 )
bt_1.grid(column=0, row=0)
create_button('Button')
window.mainloop()
3-3. 使用Label显示图片
这边要注意的是需要将图片转成Tkinter 可以读的格式:
import tkinter as tk
from PIL import Image, ImageTk
window = tk.Tk()
window.title('window')
# 放照片在UI上
def create_label_image():
img = Image.open('./images/cat_1.jpg') # 读取图片
img = img.resize( (img.width // 10, img.height // 10) ) # 缩小图片
imgTk = ImageTk.PhotoImage(img) # 转换成Tkinter可以用的图片
lbl_2 = tk.Label(window, image=imgTk) # 宣告卷标并且设定图片
lbl_2.image = imgTk
lbl_2.grid(column=0, row=0) # 排版位置
create_label_image()
window.mainloop()
四、我的设计方法与Grid 布局
在开始实作复杂的GUI前,我先介绍一下我设计GUI时的做法以及使用Grid编排的方式。首先,在设计一个GUI之前我会先进行空间的区隔。
我会透过frame按照上方的图绘分成四个颜色的区块,灰色是主要窗口,蓝色为显示图片的区块,橘色为显示文字的区块,绿色为按钮的区块。
实作程序代码如下:
import tkinter as tk
from PIL import Image, ImageTk
window = tk.Tk()
window.title('Window')
div_size = 200
img_size = div_size * 2
div1 = tk.Frame(window, width=img_size , height=img_size , bg='blue')
div2 = tk.Frame(window, width=div_size , height=div_size , bg='orange')
div3 = tk.Frame(window, width=div_size , height=div_size , bg='green')
div1.grid(column=0, row=0, rowspan=2)
div2.grid(column=1, row=0)
div3.grid(column=1, row=1)
显示结果如下:
但目前会有缩放的问题,当你放大之后他的大小仍然会保持原样,不会跟着缩放:
倘若要能够伸缩需要使用到 columnconfigure、rowconfigure,其中可以用的参数:
minsize |
最小的窗口大小( pixel ) |
pad |
上下左右各添加多少 ( pixel ) |
weight |
如果weight=0的话就不会进行缩放的动作,可以想象是权重值 ( 1 除以 weight ),参考下列范例当有两个组件需要定义网格的时候:
这时候Tkinter会将六分之一的空间分配给第0栏的组件,其余六分之五给第二栏的组件。 如果大小都是填1则是各一半
|
接下来针对先前写好的frame添加权重,这边我先写了一个副函式 ( define_layout ) 用来定义grid,引入obj为UI widget、cols该widget中有几栏、row该widget中有几列:
def define_layout(obj, cols=1, rows=1):
def method(trg, col, row):
for c in range(cols):
trg.columnconfigure(c, weight=1)
for r in range(rows):
trg.rowconfigure(r, weight=1)
if type(obj)==list:
[ method(trg, cols, rows) for trg in obj ]
else:
trg = obj
method(trg, cols, rows)
接着套用到刚刚的框架,稍微修改了一些地方,主要在grid的部分加上了 pad以及 sticky,pad是向外拓展 ( pixel );sticky 是对齐方式,这边用 align_mode 来统一所有的对其方式,而给予字符串 ‘nswe’ 是置中的意思 ( n 上 s 下 w左 e 右):
window = tk.Tk()
window.title('Window')
align_mode = 'nswe'
pad = 5
div_size = 200
img_size = div_size * 2
div1 = tk.Frame(window, width=img_size , height=img_size , bg='blue')
div2 = tk.Frame(window, width=div_size , height=div_size , bg='orange')
div3 = tk.Frame(window, width=div_size , height=div_size , bg='green')
div1.grid(column=0, row=0, padx=pad, pady=pad, rowspan=2, sticky=align_mode)
div2.grid(column=1, row=0, padx=pad, pady=pad, sticky=align_mode)
div3.grid(column=1, row=1, padx=pad, pady=pad, sticky=align_mode)
定义好UI之后在来处理布局问题,先来看第一行要注意的地方是如果下层UI要套用权重上层的也一定要套用,所以如果三个frame要套用的话,最主要的窗口window也需要使用weight分配:
define_layout(window, cols=2, rows=2)
define_layout([div1, div2, div3])
执行下去可以看到进行缩放的时候,我们的三个frame也会跟着拉伸:
接着再把需要的UI项目放进去,完整程序代码如下:
window = tk.Tk()
window.title('Window')
align_mode = 'nswe'
pad = 5
div_size = 200
img_size = div_size * 2
div1 = tk.Frame(window, width=img_size , height=img_size , bg='blue')
div2 = tk.Frame(window, width=div_size , height=div_size , bg='orange')
div3 = tk.Frame(window, width=div_size , height=div_size , bg='green')
window.update()
win_size = min( window.winfo_width(), window.winfo_height())
print(win_size)
div1.grid(column=0, row=0, padx=pad, pady=pad, rowspan=2, sticky=align_mode)
div2.grid(column=1, row=0, padx=pad, pady=pad, sticky=align_mode)
div3.grid(column=1, row=1, padx=pad, pady=pad, sticky=align_mode)
define_layout(window, cols=2, rows=2)
define_layout([div1, div2, div3])
im = Image.open('./images/cat_1.jpg')
imTK = ImageTk.PhotoImage( im.resize( (img_size, img_size) ) )
image_main = tk.Label(div1, image=imTK)
image_main['height'] = img_size
image_main['width'] = img_size
image_main.grid(column=0, row=0, sticky=align_mode)
lbl_title1 = tk.Label(div2, text='Hello', bg='orange', fg='white')
lbl_title2 = tk.Label(div2, text="World", bg='orange', fg='white')
lbl_title1.grid(column=0, row=0, sticky=align_mode)
lbl_title2.grid(column=0, row=1, sticky=align_mode)
bt1 = tk.Button(div3, text='Button 1', bg='green', fg='white')
bt2 = tk.Button(div3, text='Button 2', bg='green', fg='white')
bt3 = tk.Button(div3, text='Button 3', bg='green', fg='white')
bt4 = tk.Button(div3, text='Button 4', bg='green', fg='white')
bt1.grid(column=0, row=0, sticky=align_mode)
bt2.grid(column=0, row=1, sticky=align_mode)
bt3.grid(column=0, row=2, sticky=align_mode)
bt4.grid(column=0, row=3, sticky=align_mode)
bt1['command'] = lambda : get_size(window, image_main, im)
define_layout(window, cols=2, rows=2)
define_layout(div1)
define_layout(div2, rows=2)
define_layout(div3, rows=4)
window.mainloop()
.
最终结果:
那布局的使用方法,大家在这里应该也练习得差不多了,接下来会提供几个常用的功能,像是全屏幕、按按钮互动、实时影响读取等等。
6、进阶GUI实作
在这里为了参数调用的方便,我会使用Class来完成接下来的实作。
6-1. 全屏幕窗口 ( bind )
我们默认按下F12的时候切换成全屏幕窗口,在按下一次则返回,这边会使用到 bind 的函式,这个是可以将函式绑定到动作上面,像是按下左键、放开左键、按下F12等。首先我们要先能够将窗口全屏幕,在Windows环境下我们将去调整他的attributes成为 ‘-fullscreen,而Linux环境下则调整成 ‘-zoomed’,范例程序如下:
window.attributes('-fullscreen', True) # For Windows
window.attributes('-zoomed', True) # For Linux
接着我们要想办法辨识系统环境,在Python中自带一个函式库叫 Platform,可以使用Platform.system()得知现在的环境,在我的程序当中如果系统是Windows的话就回传1不是的话就回传 0 ,进而再给予特定的全屏幕变量。
def toggle_fullScreen(self, event):
is_windows = lambda : 1 if platform.system() == 'Windows' else 0
self.isFullScreen = not self.isFullScreen
self.window.attributes("-fullscreen" if is_windows() else "-zoomed", self.isFullScreen)
接着使用bind将动作连结到toggle_fullScreen函式:
# 切换全屏幕
self.isFullScreen = False
self.window.bind('<F12>', self.toggle_fullScreen)
接着就可以执行看看了,在给完整程序代码之前可以先看一下结果:
由于全屏幕的关系工具栏也会被取消掉,不过我常做的GUI都会带一个关闭程序的按钮,或者我们也可以透过 bind 将 ESC 按键绑定关闭窗口:
def del_window(self, event):
self.window.destroy()
self.window.bind('<Escape>', self.del_window)
完整程序代码如下:
self.window = tk.Tk()
self.window.title('Window')
im = Image.open('./images/cat.jpg').resize( (300, 300) )
imTK = ImageTk.PhotoImage( im )
self.lbl_img = tk.Label(self.window, image=imTK)
self.lbl_img.image = imTK
self.lbl_img.grid(column=0, row=0, sticky='nwes')
# 切换全屏幕
self.isFullScreen = False
self.window.bind('<F12>', self.toggle_fullScreen)
self.window.mainloop()
6-2. 按按钮开启图像 ( command )
整体设计概念很简单,在一开始的时候由frame将按钮及图片显示区块隔开,宣告个别组件。
def define_layout(self, obj, cols=1, rows=1):
def method(trg, col, row):
[ trg.columnconfigure(c, weight=1) for c in range(cols) ]
[ trg.rowconfigure(r, weight=1) for r in range(rows) ]
if type(obj)==list:
[ method(trg, cols, rows) for trg in obj ]
else:
method(obj, cols, rows)
self.window = tk.Tk()
self.window.title('Window')
self.align_mode = 'nsew'
self.pad = 10
self.div_size, self.img_size = 200, 400
self.div1 = tk.Frame(self.window, width=self.div_size , height=self.div_size)
self.div2 = tk.Frame(self.window, width=self.img_size , height=self.img_size)
self.div1.grid(column=0, row=0, padx=self.pad, pady=self.pad, sticky=self.align_mode)
self.div2.grid(column=0, row=1, padx=self.pad, pady=self.pad, sticky=self.align_mode)
self.bt1 = tk.Button(self.div1, text='Cat')
self.bt1.grid(column=0, row=0, sticky=self.align_mode)
self.bt2 = tk.Button(self.div1, text='Dog')
self.bt2.grid(column=1, row=0, sticky=self.align_mode)
self.bt3 = tk.Button(self.div1, text='Clear')
self.bt3.grid(column=2, row=0, sticky=self.align_mode)
self.bt4 = tk.Button(self.div1, text='Quit')
self.bt4.grid(column=3, row=0, sticky=self.align_mode)
self.define_layout(self.window, cols=1, rows=2)
接着先来显示图片,由于一开始开启程序希望是没有图片的所以给予空白值,但因为是空白值宽高也是0,图片区域的frame会因此变得很小,所以我将该frame的参数grid_propagate() 设为False,这个动作目的是要不要让该frame被子组件的图片大小给影响,也就是维持一开始预设的大小:
self.imTK = '' # 预设给空白
self.lbl_img = tk.Label(self.div2, image=self.imTK)
self.lbl_img.image = self.imTK
self.lbl_img.grid(column=0, row=0, sticky=self.align_mode)
self.div2.grid_propagate(0) # 不会被子组件改变大小
接着就是按钮事件的宣告,按下按钮的时候希望执行什么动作:
def bt1_event(self):
im = Image.open('./images/cat_1.jpg')
self.imTK = ImageTk.PhotoImage( im.resize( (self.img_size, self.img_size) ) )
self.lbl_img.configure(image=self.imTK) # image有时会被清除
self.lbl_img.image = self.imTK # 防止图片被垃圾清扫给除掉
def bt2_event(self):
im = Image.open('./images/dog_1.jpg')
self.imTK = ImageTk.PhotoImage( im.resize( (self.img_size, self.img_size) ) )
self.lbl_img.configure(image=self.imTK) # image有时会被清除
self.lbl_img.image = self.imTK # 防止图片被垃圾清扫给除掉
def bt3_event(self):
self.lbl_img.configure(image='')
# 绑定按钮事件
self.bt1['command'] = self.bt1_event
self.bt2['command'] = self.bt2_event
self.bt3['command'] = self.bt3_event
# self.bt4['command'] = lambda : self.window.destroy()
self.bt4.bind('<Button-1>', self.del_window)
可以注意到绑定方法与刚刚全屏幕窗口的做法不太相同,可以在宣告按钮的时候直接用command绑定事件,也可以透过我这种方式额外宣告,在bt4_event可以看到用bind的方法也是可以的,只是要去找一下对应的动作参数是什么,像在这里如果bt4被左键点击定义为 <’Button-1’>,除此之外我也提供了lambda的写法,可以再参考看看。
6-3. 随窗口大小改变图片大小(静态) – bind 延伸
从上一个案例开始应用到bind之后,我们可以在窗口大小改变的时候 ( Configure ) 或许该窗口的大小或某一个组件的大小,实验程序如下,大家可以玩玩看,注意这个是静态的,只有在点击按钮生成图片的时候才会符合缩放后的大小:
def bt1_event(self):
im = Image.open('./images/cat_1.jpg')
self.imTK = ImageTk.PhotoImage( im.resize( (self.w, self.h) ) )
self.lbl_img.configure(image=self.imTK)
self.lbl_img.image = self.imTK
def get_size(self, event, obj=''):
trg_obj = self.window if obj == '' else obj
self.w, self.h = trg_obj.winfo_width(), trg_obj.winfo_height()
print(f'\r{(self.w, self.h)}', end='')
# 每次改变状态,都会获取 某组件 大小
self.window.bind('<Configure>', lambda event, obj=self.div2 :self.get_size(event, obj))
# 按钮的部分
self.bt1['command'] = self.bt1_event
6-4. 开启摄影机进行实时影像撷取 (after)
实时影像的部分会带到一个新的使用方法以及一个新的UI组件,第一个组件,由于是实时影像,如果有写过OpenCV实时影像撷取的人应该知道,需要写一个While循环不断撷取帧 ( Frame ),并且透过不断显示获取到的帧来构成一个实时影像画面,而在Tkinter中也是要如此,但这边我们会使用 「after」来模拟While循环;另个要介绍的Widget是对话框,我个人平常不会写但是有时候蛮好玩的,所以也记录下来使用方法。
先来完成简单版本的实时影像撷取,先宣告一个架构出来:
window = tk.Tk()
window.title('Video Stream')
main = tk.Frame(window, bg="white")
main.grid()
video = tk.Label(main)
video.grid()
window.bind('<Escape>', lambda event: window.destroy())
接下来透OpenCV取得摄影机以及进行实时影像撷取,使用after来模拟While不停执行的状况,这边10是指10「毫秒」:
# 宣告摄影机
status, frame = 0, []
cap = cv2.VideoCapture(0)
def stream():
# 读取当前的影像
global status, frame
status, frame = cap.read()
# 如果有影像的话
if status:
# 将 OpenCV 色改格式 ( BGR ) 转换成 RGB
im_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
# 将 OpenCV 图档转换成 PIL
im_pil = Image.fromarray(im_rgb)
# 转换成 ImageTK
imgTK = ImageTk.PhotoImage(image=im_pil)
# 放入图片
video.configure(image=imgTK)
# 防止图片丢失,做二次确认
video.image = imgTK
# 10 豪秒 后执行 stream 函式,这里是模拟 While 循环的部分
window.after(10, stream)
# 先执行一次
stream()
window.mainloop()
# 释出摄影机内存、关闭所有窗口
cap.release()
cv2.destroyAllWindows()
对话框的部分,加在主程序里就可以了:
def quit(self, event):
quit_check = tk.messagebox.askokcancel('提示', '是否要退出?')
if quit_check:
print('离开程序')
cv2.destroyAllWindows()
self.cap.release()
self.window.destroy()
self.window.bind('<Escape>', self.quit)
6-5. 图片随着窗口大小而改变 (动态)
有了仿真循环的功能,我们可以来玩玩看图片的动态缩放了!由于如果一直实时改变会相当的消耗资源,所以我这边有设定参数resize_rate,可以决定几秒更新一次,使用的架构是之前写的范例,先来看看结果吧!
首先是每次窗口更动的时候撷取大小:
def get_size(self, event):
self.w = self.div2.winfo_width()
self.h = self.div2.winfo_height()
self.window.bind('<ButtonRelease>', lambda event: self.get_size)
接着是Update的部分,这里的逻辑是「当我没有缩放窗口的时候不断获取当前时间,一旦更动窗口大小N秒后进行缩放,进行缩放的时候会抓取最后的窗口大小」
def update(self):
if self.w == self.div2.winfo_width() and self.h ==self.div2.winfo_height():
self.reszie_time = time.time()
if self.w != self.div2.winfo_width() or self.h !=self.div2.winfo_height():
if time.time()-self.reszie_time>= self.resize_rate and self.im is not '':
self.w, self.h = self.div2.winfo_width(), self.div2.winfo_height()
self.imTK = ImageTk.PhotoImage( self.im.resize( (self.w, self.h) ) )
self.lbl_img.configure(image=self.imTK)
self.lbl_img.image = self.imTK
self.window.after(10, self.update)
self.reszie_time, self.resize_rate = 0, 0.5
self.w, self.h = self.div2.winfo_width(), self.div2.winfo_height()
self.update()
self.window.mainloop()
结语
经过一连串小范例的实作,是否了解Tkinter的用法了?当然如果要熟悉的话还是多找几个小专题是做看看,一定会越来越厉害的。一些边缘装置专题分享,或多或少都会结合小的屏幕去做触碰操作或显示,这时候制作GUI就很重要了!接下来的文章会模拟工厂产线问题进行小专题制作,届时也会制作一个小界面供用户使用,更多小技巧会收录哦!
相关文章
Python is TIOBE's Programming Language of 2020!
评论