文章目录
python上位机开发经验总结01python变量与文件的处理全局变量与局部变量文件间的变量处理threading模块使用经验管理线程定义线程tkinter使用经验tkinter显示视频Frame的使用以及各种摆放方式PanedWindow配合LabelFrame下拉框与输入框下拉框输入框tkinter.after()实现定时器动态的显示:StringVar()带滑条的Text好看的字体设置tkinter修改关闭窗口操作其它零零碎碎的经验OpenCV实现点击'X'关闭窗口打包文件pyinstaller错误:RecursionError: maximum recursion depth exceededpython上位机开发经验总结01
魔方机器人比赛中使用python写了上位机,总结一些经验待以后翻看。主要分4个方面,python变量与文件的处理,threading多线程模块使用经验,tkinter使用经验,其它零零碎碎的经验
python变量与文件的处理
主要是单文件内变量的处理经验与多文件内变量的处理经验。
全局变量与局部变量
python中的局部变量产生在函数内,在函数返回之后被释放,可以(但不建议)与全局变量同名。全局变量只能在模块(文件)级别使用,不能跨模块使用。
python中的全局变量在使用时有两个注意
全局变量需要在模块级别定义在函数中修改全局变量时需要先用修饰词global
声明
当函数中没有对变量作global
修饰时,有两种情况
函数中只对变量作读取操作,则可以直接读取在模块级别定义的同名变量函数中对变量作了写(修改)操作,则认为此变量为局部变量,即使变量与模块级别定义的变量同名,也会新建局部变量而不会去修改同名变量
如以下的例子
flag = 1def false_reset_flag():flag = 0def right_reset_flag():global flagflag = 0false_reset_flag()print(flag)right_reset_flag()print(flag)
以上程序的执行结果为
10
并且,把变量传入函数之后进行修改也是不可行的,python会将传入的变量复制作为局部参数。但是如果在程序中需要很多flag(标志变量),那么逐一定义并修改全局变量是非常不优雅的,我们可以采用传入实例,调用实例中的函数修改自己的变量的方法。终究,python还是面向对象的语言。如以下例子
flag = 1class FlagClass(object):def __init__(self):self.flag = 1def reset_flag(self):self.flag = 0def false_reset(x):x = 0def reset(x):x.flag = 0false_reset(flag)print(flag)flag_class = FlagClass()reset(flag_class)print(flag_class.flag)
以上程序的执行结果为
10
文件间的变量处理
上面其实已经提到了,python最优雅的编程方式是面向对象编程,模块(文件)间的处理也是一样。最好把每个模块中的东西全部封装成类,实现模块之间的解耦。但是有的时候(尤其是完成一个特定的算法和工作流程)面向过程的编程更为方便,这时候我们就需要去处理模块之间的很多flag(标志变量)了。
首先我们要明确一个核心思想:模块之间是不能相互写(修改)变量的。如果从其它模块import
了某个变量,实际上是把这个变量复制过来,作为一个本模块内的“局部变量”。所以我们模块间修改变量的策略也是和封装成类差不多:把一个模块看成一个庞大的实体,在模块内定义修改自己变量的函数,在其它模块中调用这个函数。如以下例子
# module1.pyflag = 1def reset_flag():global flagflag = 0# 下面是需要调用的函数,里面会修改flag的值来标志有没有完成......
# module2.pyfrom module1 import flag, reset_flagprint('flag')if flag:print('动作完成,重置标志')reset_flag()from module1 import flagprint('flag')
运行module2得到的结果如下
1动作完成,重置标志0
threading模块使用经验
threading是python提供的在底层的_threading基础上封装好的使用方便的多线程模块,但是这个模块并不太完善。
之所以说它使用方便,是因为它可以方便地创建线程,且线程中可以直接修改全局变量(像上面说的一样用global
声明即可);说它不方便是因为它没有办法很好地管理线程(我的意思是很简单方便,有能力自己设计机制管理的大佬请忽略),如果你的线程中运行的是一个死循环,那么一旦线程开始,就处于失控状态,再也无法结束。
分享我本次使用threading模块的2个经验。
管理线程
下面是我在本次工程中管理线程的经验,虽然并不是一个非常优雅的方法,但是还算好用。核心思想是定义一个线程的flag,定义成全局变量还是定义在类里可以根据实际情况决定。把死循环改为一个带判断的while循环,那么结束线程时只需修改flag的值即可。如以下例子
class COM(object):def __init__(self, port, baud):self.port = portself.baud = int(baud) = serial.Serial()self.recvBuffer = Queue(maxsize=10)self.__recv_running = True # 标志接收线程是否运行self.recvThread = threading.Thread()def open(self):try: = serial.Serial(self.port, self.baud)except Exception:print('Open COM failed: {}'.format(self.port))def close(self):if is not None and .isOpen():.close()self.stop_receive()def isOpen(self):return .isOpen()def activate_receive(self):def receive_loop():while self.__recv_running:temp = .read()if temp in serial_dict:self.recvBuffer.put(serial_dict[temp])time.sleep(0.02)self.recvThread = threading.Thread(target=receive_loop)self.recvThread.start()def stop_receive(self):self.__recv_running = False
以上是我在本次工程中定义的串口类,其中接收串口数据就是用了单独的一个线程。在类中我定义了__recv_running
变量来标志接收线程是否运行,在线程的target
函数中每次循环都取判断__recv_running
的值,需要结束线程时只需调用stop_receive
修改__recv_running
的值,那么接收线程执行完当前循环就会自动结束。
当然你应该发现了,修改标志变量值之后需要等待线程中本次循环结束,线程才能结束,无法强行杀死线程。一方面,这样结束线程可能效率不高;另一方面,这样结束线程对程序的冲击最小。
定义线程
在定义线程时,最好把线程的函数定义在启动的函数内,这样程序的可读性有很大的提高。这一点在上面的代码中就可以看出,这里我把它截取出来
def activate_receive(self):def receive_loop():while self.__recv_running:temp = .read()if temp in serial_dict:self.recvBuffer.put(serial_dict[temp])time.sleep(0.02)self.recvThread = threading.Thread(target=receive_loop)self.recvThread.start()
tkinter使用经验
这次的工程中我用tkinter绘制了一个界面,先上图
如果你还没有学习tkinter,并且时间充裕的话,我建议你使用pyQt。tkinter虽然上手非常快(从学开始到编写完这个界面我花了2天),但是它所有的排版都需要你手动敲代码,相比于pyQt的画图而言,就显得太不人性化了。
本次工程中,关于tkinter的资料搜索量是最大的,总结了以下几点经验。
在这次的工程中我参考了以下网页:Python 实现串口调试助手
tkinter显示视频
这个问题面向百度和CSDN搜索了很久,没有一个非常好的解答。最后在python摄像头视频显示到TK窗口改良版找到的代码基础上改良了一下,很好地实现了视频显示。因为找到的代码是需要付积分下载的,原本我并不想放出源码,但是抱着python全部开源的精神,在此我放出源码并配上讲解,希望各位帅哥美女留下一个赞再走。
import cv2 as cvfrom PIL import Image, ImageTkimport threadingfrom queue import Queue# 放置画布canvases = []for i in range(4):canvases.append(tk.Canvas(FRAME_cameras, bg='#ffffff', height=240, width=320))x = [0, 0, 1, 1]y = [0, 1, 0, 1]for i in range(4):canvases[i].grid(row=x[i], column=y[i], sticky='nsew')# 下面的代码解决视频流的显示和关闭show_cam_flag = Truedef stop_show_cam_thread():"""shut down the show_cameras thread:return: None"""global show_cam_flagshow_cam_flag = False# show camera flowsdef show_cameras():def cc():imgQueue = Queue(maxsize=10) # 使用队列存储读取的图像,解决视频闪烁的问题global show_cam_flagwhile show_cam_flag:for i in range(4):ret, frame = cameras[i].read()cv_image = cv.cvtColor(frame, cv.COLOR_BGR2RGB)image = Image.fromarray(cv_image)image_file = ImageTk.PhotoImage(image)imgQueue.put(image_file)canvases[i].create_image(0, 0, anchor='nw', image=image_file, tags='c1')if imgQueue.full():imgQueue.get() # 队满则出队一个,防止内存占用太大程序崩溃time.sleep(0.04)del imgQueueshow_cam_flag = True # 关闭视频流后重新置一,避免无法再次打开t = threading.Thread(target=cc)t.start()
核心的显示思想是使用Canvas来显示图片(因为用Label不断刷新图片,内存会过大,程序会崩溃),用单独的一个线程不断地读取摄像头并刷新Canvas显示的图片。因为OpenCV读取的图像格式没有办法直接显示,所以用其它的库处理了一下。
需要注意的是,如果对显示的图片不加以保存的话,每次刷新图片时之前的图片就会被释放,导致显示的视频闪烁。最早找到的代码把图片保存在了字典中(我不太懂为什么要这样操作,可能是试出来的吧),这样做显然是很不优雅的,而且不定期进行删除操作,一定会导致内存过大程序崩溃(和Label就没有区别了)。所以我定义了一个队列imgQueue
来存储图片,每个循环都会检查队列并处理溢出的图片,最终在关闭视频流时删除这个队列。这应该是非常优雅的方法了,欢迎各位交流更好的方法。
Frame的使用以及各种摆放方式
这一次我主要使用了tkinter的三种排版方式:tkinter.pack(), tkinter.grid(), tkinter.place()。tkinter的排版还是比较麻烦的,全部使用place肯定非常不优雅。想要整齐的排版,就需要多建立一些Frame,然后3个函数配合使用。这里贴上我的设计
具体怎么写就不赘述了,提几点小建议
在新建完Frame之后直接放置,这样排版和实现功能的代码可以分开在开始绘制的时候再新建Frame,否则写多了容易找不到另外,注意实例的命名格式,个人使用的是全拼大写开头,小驼峰表征位置和功能,例如FRAME_left
,ENTRY_release
,TEXT_status
PanedWindow配合LabelFrame
个人认为这些划线框让界面看起来更有条理,它们的实现方式其实也非常简单,如标题一样用PanedWindow配合LabelFrame即可。可参考如下代码
PAN_com = tk.PanedWindow(FRAME_r_up, orient=tk.VERTICAL, height=180)PAN_init = tk.PanedWindow(FRAME_r_up, orient=tk.VERTICAL, height=180)FRAME_com = tk.LabelFrame(PAN_com, text="串口设置")FRAME_init = tk.LabelFrame(PAN_init, text="转换初始化")PAN_com.add(FRAME_com)PAN_com.pack(side=tk.LEFT)PAN_init.add(FRAME_init)PAN_init.pack(side=tk.RIGHT)
然后把要加的东西加到LabelFrame里就行了,即上面的FRAME_com
和FRAME_init
里。
下拉框与输入框
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vQuEECl8-1601652622895)(./pictures/PanedWindow.png)]
还是这张图,里面的下拉框和输入框是不是也很简介美观,下面是实现它们的方法。
下拉框
bobox()个人认为是不大好用的,我使用了tkinter.ttk中的Combobox(),可以这样实现
from tkinter import ttkport_list = list(ports())serial_com_list = []# varPort = tk.StringVar()COMBO_com_com = bobox(FRAME_com, width=8, height=2, justify=tk.CENTER)for port in port_list:tmp_port = list(port)serial_com_list.append(tmp_port[0])COMBO_com_com.grid(row=0, column=1)COMBO_com_com['values'] = serial_com_listdef refresh_com():global port_list, serial_com_listport_list = list(ports())serial_com_list = []for port in port_list:tmp_port = list(port)serial_com_list.append(tmp_port[0])COMBO_com_com['values'] = serial_com_listFRAME_com.after(500, refresh_com)refresh_com()
这一段代码实现了COM:后面的下拉窗口,可以动态显示可用的串口,每500ms刷洗一次。后面还会讲使用tkinter.StringVar()实现动态显示的方法。
输入框
输入框使用tkinter.Entry即可,读取时使用tkinter.Entry.get()
,设定默认值可以使用tkinter.Entry.insert()
,例如
ENTRY_init_release = tk.Entry(FRAME_init, show=None, width=4)ENTRY_init_release.insert(0, 25)ENTRY_init_release.grid(row=0, column=1)
tkinter.after()实现定时器
tkinter.after()函数实际上是等待一段时间后执行一个函数,但是可以通过函数本身的嵌套去实现一个定时器的功能,在stringVar()中一起介绍
动态的显示:StringVar()
有时候你会想在界面上显示一个实时变动的东西,这时候你就需要一个变量去放置你要显示的东西。我使用了一个很好用的类tkinter.StringVar。下面给出使用tkinter.after和tkinter.StringVar实现计时器的方法
global time_countrun_time = -1def start_time_count():"""开始计时界面的定时刷新,此定时器可用time_count来停止:return:"""global time_countglobal run_timerun_time += 1var_time.set('%.2f' % (run_time / 100))time_count = FRAME_time.after(10, start_time_count)PAN_time = tk.PanedWindow(FRAME_r_down, orient=tk.VERTICAL, height=100, width=364)FRAME_time = tk.LabelFrame(PAN_time, text="运行计时")PAN_time.add(FRAME_time)PAN_time.pack()var_time = tk.StringVar()LABEL_time = tk.Label(FRAME_time, textvariable=var_time, font=('Arial', 40), fg='red')LABEL_time.pack()var_time.set('0.00')run_time = -1
可以通过函数start_time_count()
来启动计时器,使用FRAME_time.after_cancel(time_count)
来结束。
带滑条的Text
如果你觉得创建Text再创建滑条太麻烦,那么可以像我一样使用tkinter.scrolledtext中的同名类来创建带滑条的Text窗口。可参考如下代码
import tkinter as tkfrom tkinter import scrolledtext# FRAME_statusFRAME_status = tk.Frame(FRAME_r_down)FRAME_status.pack()TEXT_status = scrolledtext.ScrolledText(FRAME_status, width=49, height=8, font=('Consolas', 10))TEXT_status.pack()TEXT_status.insert(tk.END, 'Welcome to use Beihang Robotics CubicRobot\n')TEXT_status.see(tk.END)
其中TEXT_status.see(tk.END)
可以使Text窗聚焦在底部
好看的字体设置
tkinter几乎所有地方都可以设置字体,例如以下:
LABEL_time = tk.Label(FRAME_time, textvariable=var_time, font=('Arial', 40), fg='red')BUTTON_start = tk.Button(FRAME_main, text='开始', font=('黑体', 16), width=10, height=2, command=main_start)TEXT_status = scrolledtext.ScrolledText(FRAME_status, width=49, height=8, font=('Consolas', 10))
在此推荐一种好看的英文字体,适合用在提示窗信息中:Consolas
tkinter修改关闭窗口操作
有时候为了保证程序的正常退出(如需要手动关闭线程),需要修改点击窗口右上方’X’号时触发的函数。tkinter中点击’X’产生的事件为'WM_DELETE_WINDOW'
,想要修改可以参考如下方法
# 重写关闭窗口函数,先关闭线程再关闭窗口def terminate():# 关闭视频流线程stop_show_cam_thread()# 若串口打开,关闭串口try:com.close()except NameError:pass# 等待线程关闭再关闭窗口,实测有效root.after(1000, root.destroy)root.protocol('WM_DELETE_WINDOW', terminate)
其它零零碎碎的经验
OpenCV实现点击’X’关闭窗口
一般来说OpenCV都是通过按键盘来关闭(waitKey),但是这样的方法放在一个成熟的GUI中就有失优雅,于是我在Stack Overflow上找到了解决方法,可以看以下例子,也可以参见网址OpenCV Python: How to detect if a window is closed?
total_frames = 50cv2.cv.NamedWindow("Dragonfly Simulation")cv2.cv.StartWindowThread()for i in range(total_frames):# do stuffimg_name = # somethingimg = cv2.cv.LoadImage(img_name)cv2.cv.ShowImage("Dragonfly Simulation", img)cv2.cv.WaitKey(2)cv2.cv.DestroyWindow("Dragonfly Simulation")cv2.cv.WaitKey(1)# rest of code
以上是原网址的代码示例
while get_points_cnt < 9:cv.imshow(get_points_window, image)cv.waitKey(50)if cv.getWindowProperty(get_points_window, 0) < 0:TEXT_status.insert(END, 'Window shut down before finishing setting sampling points\n')TEXT_status.see(END)return
以上是我本次使用的代码,简单地说就是每次刷新图像的循环都对cv2.getWindowProperty(window_name, 0)
进行判断,如果窗口已经关闭,这个函数的返回值为-1,如果窗口已经关闭,则打断循环即可
打包文件
写完的代码可以用pyinstaller打包成exe文件,可以让你的程序在没有python环境的电脑上运行。这里只记录一个常用的打包方式
pyinstaller -F -i logo.ico -w main.py -p sub1.py -p sub2.py
-F是指打包成单独的exe文件,而不是很多dll依赖文件的文件夹。
-i指exe文件的图标
-w指生成窗口程序
-p后是子模块
pyinstaller错误:RecursionError: maximum recursion depth exceeded
打包程序是遇到了这个问题,意思是递归深度超过限制。在Stack Overflow上找到了解决方法:pyinstaller creating EXE RuntimeError: maximum recursion depth exceeded while calling a Python object
大概意思就是,使用的某一个包在疯狂递归,导致超过了python的栈深度限制。解决方法是首次运行pyinstaller命令,提示出错后,生成了一个module_name.spec
文件,记事本打开这个文件,加上下面两行即可
import syssys.setrecursionlimit(5000)
这里的5000指迭代深度,如果不够可以设置得更大