最近需要用python做一个GUI,作为一个还没学过python的小白,在经历了两三周断断续续的学习和请教学长之后,总算是做出了一些成果,特于此记录一下学习过程。(第一次写博客,排版混乱还请见谅)
一、了解Python3基础语法
因为做GUI不需要太复杂的语法或者功能,我简单的浏览了一个很不错的比较适合小白的Python3教程:
廖雪峰的官方网站: 点击打开链接
我觉得做GUI需要的基础语法,里面前七章和第十二章的内容就大概可以。
紧接着,我选择了很好用的Python IDE,Pycharm作为编译器。
二、库的选择和学习
Python令我很惊喜的就是库太强大了,几乎无所不包,大大降低了上手难度。就像当初玩单片机遇到Arduino一样。
Python做GUI的库有好几个,我选择了上手比较容易的tkinter,它创建GUI的流程大体上就是,首先创建顶层窗口对象,然后创建其他组件放进该对象中,最后进入主事件循环:循环刷新窗口。所以在布局的过程中,布局到哪里,对应的功能就相应的跟在后面。
由于实时显示采集数据和串口助手功能很相似,所以我决定先设计一个最普通的串口助手,为后面GUI的框架再用以改进。
我最开始想的大概布局就是这样:
接下来最重要的就是学会使用tkinter的各个组件了,csdn上有很多教程,我从里面学到了很多,这边我姑且以自己的理解总结一下我用到的一些组件和编程的思路,其中组件选项参数的图片均转载自博客:点击打开链接。
1.创建顶层窗口对象,我就叫它父容器
GUI = tk.Tk() # 创建父容器GUI
GUI.title("Serial Tool") # 父容器标题
GUI.geometry("460x380") # 设置父容器窗口初始大小,如果没有这个设置,窗口会随着组件大小的变化而变化
此时的效果如下:
2.接下来是每个子容器及其组件的创建
首先是调试信息窗口,我设想的是一个带有滚动条的窗口,可以显示当前的操作状态和一些操作信息,当窗口显示满的时候可以自动往下滚动显示。
在这里,我用到的组件主要有LabelFrame,ScrolledText,grid,place,button,Entry
LabelFrame组件会自动绘制一个边框将子组件包围起来,并在它们上方显示一个文本标题,组件选项如下:
ScrolledText组件是创建一个带有滚动条的文本窗口,全部参数选项有哪些我也没搞清楚,因为它是Text的一个子组件,所以Text部分选项参数它也可以调用,其中Text的全部选项我就不贴了,只选取一个贴一下:
我在这里使用的是选项是wrap=tk.WORD,这个值表示在行的末尾如果有一个单词跨行,会将该单词放到下一行显示,比如输入hello,he在第一行的行尾,llo在第二行的行首, 这时如果wrap=tk.WORD,则表示会将 hello 这个单词挪到下一行行首显示
grid,pack,place的用法和区别我参考了一篇博客,写的特别好:点击打开链接
button组件用于实现各种各样的按钮,选项参数如下:
Entry组件通常用于获取用户输入的文本,选项参数如下:
以上很多选项默认即可,首先我的调试信息窗口写法如下:
Information = tk.LabelFrame(GUI, text="操作信息", padx=10, pady=10) # 创建子容器,水平,垂直方向上的边距均为10
Information.place(x=20, y=20)
Information_Window = scrolledtext.ScrolledText(Information, width=20, height=5, padx=10, pady=10,wrap=tk.WORD)
Information_Window.grid()
此时的效果如下:
接下来如法炮制,创建数据接收子容器:
Receive = tk.LabelFrame(GUI, text="接收区", padx=10, pady=10 ) # 水平,垂直方向上的边距均为 10
Receive.place(x=240, y=150)
Receive_Window = scrolledtext.ScrolledText(Receive, width=20, height=12, padx=10, pady=10, wrap=tk.WORD)
Receive_Window.grid()
此时效果如下:
发送子容器需要文本窗口,和发送按钮,按下发送按钮时将文本窗口的内容发送至设备,写法如下:
Send = tk.LabelFrame(GUI, text="发送指令", padx=10, pady=5)
Send.place(x=240, y=20)
DataSend = tk.StringVar() # 定义DataSend为保存文本框内容的字符串
EntrySend = tk.StringVar()
Send_Window = ttk.Entry(Send, textvariable=EntrySend, width=23)
Send_Window.grid()
def WriteData(): # 按钮按下时触发的动作函数
global DataSend
DataSend = EntrySend.get() # 读取当前文本框的内容保存到字符串变量DataSend
Information_Window.insert("end", '发送指令为:' + str(DataSend) + '\n') # 在操作信息窗口显示发送的指令并换行,end为在窗口末尾处显示
Information_Window.see("end") # 此处为显示操作信息窗口进度条末尾内容,以上两行可实现窗口内容满时,进度条自动下滚并在最下方显示新的内容
SerialPort.write(bytes(DataSend, encoding='utf8')) # 串口发送文本框内容
tk.Button(Send, text="发送", command=WriteData).grid(pady=5, sticky=tk.E)
此时效果如下:
接下来开始创建选项子容器,作为一个串口助手,选项里肯定得有端口号,波特率的下拉菜单栏,在这里使用的主要组件有Label,Combobox
Label组件用于显示文本和图像,组件选项如下:
Combobox用来创建下拉菜单栏,我用到的组件选项如下:
values 设定下拉菜单可选内容
state 设定状态。readonly时为只可选择,不可更改内容
current 设定初始选择内容,参数为可选列表的0-index
于是我选项子容器的写法如下:
option = tk.LabelFrame( GUI, text = "选项", padx = 10, pady = 10 )
option.place(x = 20, y = 150, width = 203)
# ************创建下拉列表**************
ttk.Label( option, text = "串口号:" ).grid( column = 0, row = 0 ) # 添加串口号标签
ttk.Label( option, text = "波特率:" ).grid( column = 0, row = 1 ) # 添加波特率标签
Port = tk.StringVar() # 端口号字符串
Port_list = ttk.Combobox( option, width = 12, textvariable = Port, state = 'readonly' )
ListPorts = list(serial.tool.list_ports.comports) # 扫描当前可用串口保存到表ListPorts
Port_list['values'] = [i[0] for i in ListPorts] # 下拉列表的值为ListPorts的所有值
Port_list.current(0) # 初始显示表中第一个值
Port_list.grid(column=1, row=0) # 设置其在界面中出现的位置 column代表列 row 代表行
BaudRate = tk.StringVar() # 波特率字符串
BaudRate_list = ttk.Combobox( option, width = 12, textvariable = BaudRate, state = 'readonly' )
BaudRate_list['values'] = (1200, 2400, 4800, 9600, 14400, 19200, 38400, 43000, 57600, 76800, 115200)
BaudRate_list.current(3) # 初始显示9600
BaudRate_list.grid(column=1, row=1)
此时效果如下:
最后是创建开/停按钮的子容器了,停止按钮则是直接关闭串口即可实现停止采集,而开始采集按钮按下后需要接收数据并把数据显示到接收区,接收数据的核心是要添加一个线程跟主线程并行,这样才能保证一直接收数据且不和主事件循环冲突。这一块我的写法如下:
switch = tk.LabelFrame( GUI, text = "", padx = 10, pady = 10 )
switch.place(x = 20, y = 315, width = 203)
def ReceiveData():
while SerialPort.isOpen():
Receive_Window.insert("end", str(SerialPort.readline()) + '\n')
Receive_Window.see("end")
def Close_Serial():
SerialPort.close()
def Open_Serial():
if not SerialPort.isOpen():
SerialPort.port = Port_list.get() # 读取端口下拉菜单栏的选择,将其设为串口的端口
SerialPort.baudrate = BaudRate_list.get() # 读取波特率下拉菜单栏的选择,将其设为串口的波特率
SerialPort.timeout = 0.1
SerialPort.open() # 打开串口
if SerialPort.isOpen():
t = threading.Thread(target=ReceiveData) # 新建线程用来不断接收数据并显示
t.setDaemon(True) # 守护线程
t.start() # 开始线程
else:
SerialPort.close()
tk.Button( switch, text = "开始采集", command = Open_Serial ).pack( side = "left", padx = 13 )
tk.Button( switch, text = "停止采集", command = Close_Serial ).pack( side = "right", padx = 13 )
此时效果如下:
至此,最基础的串口助手设计完成了,可以实现最基础的串口助手功能,也为我后边GUI的设计搭建了基础框架,最后运行效果如下图所示:
最后附上我的最终全部代码:
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import threading
import tkinter as tk
import serial.tools.list_ports
from tkinter import ttk
from tkinter import scrolledtext
SerialPort = serial.Serial()
GUI = tk.Tk() # 父容器
GUI.title("Serial Tool") # 父容器标题
GUI.geometry("440x320") # 父容器大小
Information = tk.LabelFrame(GUI, text="操作信息", padx=10, pady=10) # 水平,垂直方向上的边距均为10
Information.place(x=20, y=20)
Information_Window = scrolledtext.ScrolledText(Information, width=20, height=5, padx=10, pady = 10,wrap = tk.WORD)
Information_Window.grid()
Send = tk.LabelFrame(GUI, text="发送指令", padx=10, pady=5) # 水平,垂直方向上的边距均为 10
Send.place(x=240, y=20)
DataSend = tk.StringVar() # 定义DataSend为保存文本框内容的字符串
EntrySend = tk.StringVar()
Send_Window = ttk.Entry(Send, textvariable=EntrySend, width=23)
Send_Window.grid()
def WriteData():
global DataSend
DataSend = EntrySend.get()
Information_Window.insert("end", '发送指令为:' + str(DataSend) + '\n')
Information_Window.see("end")
SerialPort.write(bytes(DataSend, encoding='utf8'))
tk.Button(Send, text="发送", command=WriteData).grid(pady=5, sticky=tk.E)
Receive = tk.LabelFrame( GUI, text = "接收区", padx = 10, pady = 10 ) # 水平,垂直方向上的边距均为 10
Receive.place(x = 240, y = 124)
Receive_Window = scrolledtext.ScrolledText(Receive, width = 18, height = 9, padx = 8, pady = 10,wrap = tk.WORD)
Receive_Window.grid()
option = tk.LabelFrame( GUI, text = "选项", padx = 10, pady = 10 ) # 水平,垂直方向上的边距均为10
option.place(x = 20, y = 150, width = 203) # 定位坐标
# ************创建下拉列表**************
ttk.Label( option, text = "串口号:" ).grid( column = 0, row = 0 ) # 添加串口号标签
ttk.Label( option, text = "波特率:" ).grid( column = 0, row = 1 ) # 添加波特率标签
Port = tk.StringVar() # 端口号字符串
Port_list = ttk.Combobox( option, width = 12, textvariable = Port, state = 'readonly' )
ListPorts = list(serial.tools.list_ports.comports())
Port_list['values'] = [i[0] for i in ListPorts]
Port_list.current(0)
Port_list.grid(column=1, row=0) # 设置其在界面中出现的位置 column代表列 row 代表行
BaudRate = tk.StringVar() # 波特率字符串
BaudRate_list = ttk.Combobox( option, width = 12, textvariable = BaudRate, state = 'readonly' )
BaudRate_list['values'] = (1200, 2400, 4800, 9600, 14400, 19200, 38400, 43000, 57600, 76800, 115200)
BaudRate_list.current(3)
BaudRate_list.grid(column=1, row=1) # 设置其在界面中出现的位置 column代表列 row 代表行
switch = tk.LabelFrame( GUI, text = "", padx = 10, pady = 10 ) # 水平,垂直方向上的边距均为 10
switch.place(x = 20, y = 250, width = 203) # 定位坐标
def ReceiveData():
while SerialPort.isOpen():
Receive_Window.insert("end", str(SerialPort.readline()) + '\n')
Receive_Window.see("end")
def Close_Serial():
SerialPort.close()
def Open_Serial():
if not SerialPort.isOpen():
SerialPort.port = Port_list.get()
SerialPort.baudrate = BaudRate_list.get()
SerialPort.timeout = 0.1
SerialPort.open()
if SerialPort.isOpen():
t = threading.Thread(target=ReceiveData)
t.setDaemon(True)
t.start()
else:
SerialPort.close()
tk.Button( switch, text = "开始采集", command = Open_Serial ).pack( side = "left", padx = 13 )
tk.Button( switch, text = "停止采集", command = Close_Serial ).pack( side = "right", padx = 13 )
GUI.mainloop()