【Python】Tkinter打造摄像头与蓝牙综合监视器:零基础教程
摘要
本例主要基于Tkinter创建两类窗口,在input.py中,先从输入窗口输入摄像头IP(Esp32摄像头传递视频流MJPEG格式)、蓝牙端口,传递给monitor.py用OpenCV和Serial打开,再在监视窗口中实现2×2网格,分窗口展示摄像头图像、版本信息、两个传感器数据及曲线(本来是三个1,但一个坏了qwq)
声明
作者纯纯萌新,现为大一升大二的非计算机系学生,为比赛所迫,人生中第一次正式写Python代码、也是第一次写GUI,其中很多原理和逻辑也是现学的,如有错误或者更好的修正,欢迎大佬在评论区指正!
初始库
Tkinter,PIL,cv2,os,time,serial,threading,queue,matplotlib
内容
大概逻辑
如前所述,通过建立两个简单的窗口,实现基本的GUI功能,相比下图,具体代码中涉及到更多异常处理和用户体验优化,不过基本逻辑下图已经表述得相当完备了。
以下为代码,代码较长,建议复制到自己的Pycharm或者VSCode里查看比较方便,也有详细的注释)
input窗口
解读
关于函数功能的逐条解读(缩进表示函数的附属调用关系):
【__init__】 创建输入窗口
【input_window_init】 创建主窗口root,为配件提供“位置”(master),同时给窗口关闭键关联上on_closing函数
【on_closing】 关闭窗口
【input_label_init】 创建输入标签、提示符、复选框(☑决定是否保存数据,和是否打开摄像头/蓝牙,会传递给monitor决定打开窗口数量),同时给确认键关联上input_ok函数
【input_ok】 检查输入,判断是否进入monitor窗口还是重试,同时调用remember_info或clear_info函数,决定是否保存/清空信息
【remember_info】 在桌面创建文件保存输入信息
【clear_info】 清空桌面该文件的信息
【check_info】 打开文件自动读入上一次输入保存的信息,若不存在则跳过
代码
# input.py
# 创建输入窗口,并获取视频流IP和蓝牙串口名
# 从而传递给monitor.py
import tkinter as tk
from tkinter import ttk, messagebox
import os
# 输入窗口大小
input_root_size = "400x200+200+200"
# 后续要传递给monitor的值
url = ""
port = ""
signal = False
video_enable = True
serial_enable = True
class input:
# 构造函数,实现窗口的初始化
# 此时不能输入数据,要在mainloop中实现
# check_info函数可读取并自动输入上次保存的数据
def __init__(self):
self.url = None
self.port = None
self.input_window_init()
self.input_label_init()
self.check_info()
# 创建总窗口
def input_window_init(self):
self.root = tk.Tk()
self.root.title("连接窗口")
self.root.geometry(input_root_size)
self.root.resizable(False,False)
# 将右上角"x"与on_closing函数关联,后续用来实现"是否关闭"弹窗
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
# 创建提示栏和提示栏
def input_label_init(self):
self.label_url = tk.Label(self.root, text="输入摄像头ID")
self.label_url.pack()
self.entry_url = tk.Entry(self.root)
self.entry_url.pack()
self.video_enable = tk.BooleanVar()
self.video_enable.set(True)
self.video_enable_checkbutton = tk.Checkbutton(self.root, text="打开摄像头", variable=self.video_enable)
self.video_enable_checkbutton.pack()
self.label_port = tk.Label(self.root, text="输入蓝牙端口")
self.label_port.pack()
self.entry_port = tk.Entry(self.root)
self.entry_port.pack()
self.serial_enable = tk.BooleanVar()
self.serial_enable.set(True)
self.serial_enable_checkbutton = tk.Checkbutton(self.root, text="打开蓝牙", variable=self.serial_enable)
self.serial_enable_checkbutton.pack()
# 一个"记住我的输入"复选框,避免重复输入
# remember真值用于决定是否将输入信息保存至info.txt桌面文件
self.remember = tk.BooleanVar()
self.remember.set(True)
self.remember_checkbutton = tk.Checkbutton(self.root, text="记住我的输入", variable=self.remember)
self.remember_checkbutton.pack()
# "确认"按钮,关联input_ok函数
self.ok_button = ttk.Button(self.root, text="确认", command=self.input_ok)
self.ok_button.pack()
# 确认键关联的函数,用于启动其他的处理函数
def input_ok(self):
# url,port传值
# 目的:input窗口关闭后不知道input对象是否会析构
# 用类外全局变量可保存数据
global url, port, signal, video_enable, serial_enable
self.url = self.entry_url.get()
self.port = self.entry_port.get()
if (self.url or not self.video_enable.get()) and (self.port or not self.serial_enable.get()):
# print(1)
url = self.url
port = self.port
video_enable = self.video_enable.get()
serial_enable = self.serial_enable.get()
self.root.destroy()
# signal传给monitor.py使后续monitor窗口能创建
signal = True
# 处理前面"数据是否保存"的选择
if self.remember.get():
self.remember_info()
else:
self.clear_info()
# 只输入一个空就匆忙提交的错误处理
else:
# print(2)
messagebox.showerror("错误","输入有误,请重新输入!")
# 关闭键关联的函数,弹出"是否关闭"弹窗,防止误触
def on_closing(self):
global signal
if messagebox.askyesno("关闭窗口", "是否关闭?"):
self.root.destroy()
# 不会再打开monitor窗口
signal = False
# 自动读取与输入函数
# 在用户输入前,读取并自动输入上一次输入信息
# 信息存在桌面上的info.txt里(可自己改路径)
# 如果上一次选了不保存,则会把文件清空,可以实现关闭自动输入
def check_info(self):
filename = "info.txt"
desktop_path = os.path.join(os.path.expanduser("~"), "Desktop", filename)
# 如果文件存在,则如下处理
# 文件不存在,对应的是第一次运行程序的情况,不存在上一次输入,直接跳过
if os.path.exists(desktop_path):
with open(desktop_path, 'r') as file:
# 读第一行,判断是否为空,若空则跳过
line1 = file.readline().strip()
if not line1:
pass
# 将一二行都分别读取,输入
# 注意strip()去除字符串末尾的"\n"" "等字符
else:
self.entry_url.insert(0,line1)
line2 = file.readline().strip()
self.entry_port.insert(0,line2)
# 数据保存函数,input_ok函数的一个处理项
# 正确的调用时机已经在input_ok函数里构建好
# 只用将两行信息输入到info.txt(没有会自动创建,'w'用于覆写)
def remember_info(self):
filename = "info.txt"
desktop_path = os.path.join(os.path.expanduser("~"), "Desktop", filename)
with open(desktop_path, 'w') as file:
file.write(self.url + '\n' + self.port)
# 数据清理函数,input_ok函数的一个处理项
# 正确的调用时机也已经在input_ok函数里构建好
# 用'w'的覆写功能,打开后自动清空
def clear_info(self):
filename = "info.txt"
desktop_path = os.path.join(os.path.expanduser("~"), "Desktop", filename)
with open(desktop_path, 'w'):
pass
效果
monitor窗口
解读
函数逐条解读(同上)(注意此段代码较长):
【__init__】 窗口、视频、蓝牙所有东西初始化,负责信息update的线程的建立
【window_init】 初始化总窗口root
【video_init】 尝试用cv2连接摄像头,并提供异常处理
【serial_init】 尝试用serial连接蓝牙,同样提供异常处理
【button_init】 提供保存图片(蓝牙数据自动保存)、中断线程、视频窗口三大功能的按键
【button_save_image】 保存最近单帧图片
【only_video】 单独开摄像头窗口,不能监视蓝牙数据,但延时更低
【setup_ui】 绑定窗口关闭(同上input),创建数据更新的队列和update线程
【all_update】 update线程,执行摄像头和蓝牙数据更新
【video_update】 将读到的单帧frame放入video_queue,等待主线程处理
【serial_update】 将读到的蓝牙数据预处理后放入serial_queue
【check_all_queue】 主线程中无限循环,直到结束数据更新,内含所有从队列中获取信息并更新到GUI界面的函数
【check_video_queue】 将队列里frame类型的单帧图片转换为image,在(0,0~1)窗口展示并更新
【check_serial_queue_pre】 蓝牙连接的Arduino板上烧写代码,已使蓝牙按“#xxx/xxx*”格式传数据,在serial_queue里获取的是初步处理后的单个data,将xxx/xxx分解,传给后续处理
【auto_save_data】 自动保存蓝牙数据
【check_serial_queue2/3】 (1因为传感器坏了一个而舍弃)保存二十个数据,并在(1,0)和(1,2)分别按列展示后10个数据
【draw_serial_figure2/3】 根据保存的20组数据绘图,展示在(1,1)和(1,3)
【on_closing】 结束数据更新,释放资源,关闭窗口
代码
# monitor.py
# 创建并实时更新监视窗口
# 内含连接失败的错误处理
import tkinter as tk
from tkinter import ttk, messagebox
import cv2
import serial
from PIL import Image, ImageTk
import os
import threading
import queue
import time
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
# 监视总窗口大小
monitor_root_size = "910x760+50+50"
# 视频缩放尺寸
frame_size = (450, 350)
# 摄像头帧率,鉴于python的处理速度和本程序的逻辑,建议不大于7
FPS = 7
# 数据绘图大小,像素*100=英寸
figsize = (3.5, 3.5)
# 要连接的蓝牙波特率
baud_rate = 9600
# 蓝牙传输字符串的分隔符
symbol = ['#', '/', '*']
# 数据列表标签
data_label = ['', '水温', 'TDS']
# 数据图像标签
figure_legend = ['', '°C', 'ppm']
class monitor:
# 整个监视窗口的初始化
def __init__(self, url, port, video_enable, serial_enable):
self.url = url
self.port = port
self.video_enable = video_enable
self.serial_enable = serial_enable
# 创建停止事件,后续判定中用到
self.stop_event = threading.Event()
self.retry_event = False
self.only_video_choice = False
self.window_init()
# 视频的video_init初始化
# 若连接失败,选择重试使retry_event真,则后续全跳过
# 在main.py中的循环重回input窗口
self.video_init()
if self.retry_event:
pass
else:
# 蓝牙传输数据的serial_init初始化
# 重试功能同上
self.serial_init()
if self.retry_event:
pass
else:
self.button_init()
# 最重要也最抽象的,线程创建
self.setup_ui()
# 大窗口的初始化
# 此时不显示视频,蓝牙数据,按钮
def window_init(self):
self.root = tk.Tk()
self.root.title("监视窗口")
self.root.geometry(monitor_root_size)
self.root.resizable(False,False)
# 创建工具栏,给后面创建的按钮
self.toolbar_frame = tk.Frame(self.root, bd=2, relief=tk.RAISED, bg='green')
self.toolbar_frame.pack(side=tk.TOP, pady=1, fill=tk.X)
self.toolbar = ttk.Frame(self.toolbar_frame)
self.toolbar.pack(side=tk.TOP, fill=tk.X)
# 前几行忘记给root建立frame了,补一个后面用grid方便
self.image_frame = tk.Frame(self.root)
self.image_frame.pack(fill=tk.BOTH)
# 摄像头连接与错误处理
def video_init(self):
# 在image_frame中建立视频展示的小窗口
self.video_label = tk.Label(self.image_frame)
self.video_label.grid(row=0, column=0, columnspan=2, sticky="nsew", padx=1, pady=1)
if self.video_enable:
# 用OpenCV尝试打开视频
self.cap = cv2.VideoCapture(self.url)
# video_flag传给后面video_update函数,决定是否更新视频流
self.video_flag = True
# 提供了摄像头未连接的错误处理
# 由于尝试连接的时间较长,所以python终端报Connection failed之后一会儿才会弹出下面错误窗口
if not self.cap.isOpened():
if messagebox.askyesno("错误","无法连接摄像头,是否重试?"):
self.stop_event.set()
# 重试,retry_event置True,传给main.py循环
self.retry_event = True
self.cap.release()
self.root.destroy()
else:
# 不想重试,直接不开摄像头
self.video_label.config(text="摄像头未连接" , font=('Helvetica', 12), bg='white')
self.video_flag = False
else:
self.video_label.config(text="摄像头未连接" , font=('Helvetica', 12), bg='white')
self.video_flag = False
# 蓝牙连接与错误处理
def serial_init(self):
# 创建数组,日后三个传感器要分别保存十位数据
# self.serial_data1 = ['0'] * 10
self.serial_data2 = [0] * 20
self.serial_data3 = [0] * 20
# 创建蓝牙展示窗口
# 因为本项目第一个传感器-浊度传感器不工作,所以直接注释掉第一条,以下类似处理
# self.serial_data_label1 = tk.Label(self.image_frame, text="等待蓝牙数据...", font=('Helvetica', 12), bg='white')
# self.serial_data_label1.grid(row=0, column=3, sticky="nsew", padx=1, pady=1)
# 取而代之的是一个版本信息展示框
self.serial_data_label0 = tk.Label(self.image_frame, font=('Helvetica', 12), bg='white',
text=f"综合监视器ver2.2\nFPS={FPS}\n波特率={baud_rate}\n测量数据:"
+data_label[1]+','+data_label[2])
self.serial_data_label0.grid(row=0, column=2, columnspan=2, sticky='nsew', padx=1, pady=1)
self.serial_data_label2 = tk.Label(self.image_frame, text="等待蓝牙数据...", font=('Helvetica', 12), bg='white')
self.serial_data_label2.grid(row=1, column=0, sticky="nsew", padx=1, pady=1)
self.serial_data_label3 = tk.Label(self.image_frame, text="等待蓝牙数据...", font=('Helvetica', 12), bg='white')
self.serial_data_label3.grid(row=1, column=2, sticky="nsew", padx=1, pady=1)
# self.serial_data_label.pack(side=tk.LEFT, fill=tk.Y, padx=1, pady=1)
if self.serial_enable:
# 尝试连接蓝牙
try:
self.serial = serial.Serial(self.port, baud_rate, timeout=1)
# serial_flag传给后面serial_update函数,决定是否更新蓝牙
self.serial_flag = True
# 无法连接的错误处理,逻辑同video_init函数
except serial.SerialException:
if messagebox.askyesno("错误","无法连接蓝牙,是否重试?"):
self.stop_event.set()
self.retry_event = True
if self.video_flag:
self.cap.release()
self.root.destroy()
else:
# self.serial_data_label1.config(text="蓝牙未连接", font=('Helvetica', 12), bg='white')
self.serial_data_label2.config(text="蓝牙未连接", font=('Helvetica', 12), bg='white')
self.serial_data_label3.config(text="蓝牙未连接", font=('Helvetica', 12), bg='white')
self.serial_flag = False
else:
# self.serial_data_label1.config(text="蓝牙未连接", font=('Helvetica', 12), bg='white')
self.serial_data_label2.config(text="蓝牙未连接", font=('Helvetica', 12), bg='white')
self.serial_data_label3.config(text="蓝牙未连接", font=('Helvetica', 12), bg='white')
self.serial_flag = False
# 按钮创建
def button_init(self):
# 当视频流打开的时候才显示按钮
if self.video_flag:
video_button1 = ttk.Button(self.toolbar, text="保存图片", command=self.button_save_image)
video_button1.pack(side=tk.LEFT, padx=(25, 0))
video_button2 = ttk.Button(self.toolbar, text="中断线程", command=self.stop_event.set)
video_button2.pack(side=tk.LEFT, padx=(25, 0))
video_button3 = ttk.Button(self.toolbar, text="视频窗口", command=self.only_video)
video_button3.pack(side=tk.LEFT, padx=(25, 0))
# 由于视频逐帧转为图片太慢,开个窗口低延时监控
def only_video(self):
while True:
ret, frame = self.cap.read()
if ret:
cv2.imshow("video-only window, stops with 'q' & exits with 'x'",frame)
self.only_video_choice = True
if cv2.waitKey(1) == ord('q'):
self.only_video_choice = False
break
# GUI线程创建
def setup_ui(self):
# "x"绑定on_closing函数,提供关闭窗口
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
# 创建两个队列,用于从两个update函数传值给两个check函数
# 为什么不在update函数直接执行更新:GUI界面的更新不在主线程中执行就会出bug
self.video_queue = queue.Queue()
self.serial_queue = queue.Queue()
# 创建两个线程,分别绑定两个update函数
# 但由于不是主线程,所以只能实现实时获取数据
# 在GUI界面更新得用两个check函数
self.thread = threading.Thread(target=self.all_update)
self.thread.start()
# 执行获取数据的线程后进行GUI界面的更新:主线程
self.root.after(10, self.check_all_queue)
# GUI界面更新的总函数
# 原理都是从queue中获取信息再处理
def check_all_queue(self):
if not self.stop_event.is_set():
if not self.only_video_choice:
self.check_video_queue()
self.check_serial_queue_pre()
# self.check_serial_queue1()
self.check_serial_queue2()
self.check_serial_queue3()
# 循环,直到窗口关闭
self.root.after(int(1000/FPS), self.check_all_queue)
# 视频展示框更新
def check_video_queue(self):
# 从queue中读到video_update函数传递的frame变量(由cap.read获得)
# 由于想展示到大窗口的小展示框里,所以必须转成image
# 此逻辑影响了视频更新效率,如果不在意分窗口的话可以直接cv2.imshow
if not self.video_queue.empty():
self.frame = self.video_queue.get()
self.frame = cv2.resize(self.frame, frame_size)
self.frame = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB)
image = Image.fromarray(self.frame)
# 注意这里一定要用类的成员(或者可能有别的手段)保存image!
# 否则python会将image当垃圾回收,视频无法显示
self.photo = ImageTk.PhotoImage(image=image)
self.video_label.config(image=self.photo)
# 先将queue里的数据分解为三个,传给三个check函数
def check_serial_queue_pre(self):
if not self.serial_queue.empty():
self.data = str(self.serial_queue.get())
newdata = self.data.split(symbol[0])[-1]
newdata = newdata.split(symbol[2])[0]
# 自动保存数据到桌面文件的函数
self.auto_save_data(newdata)
# print(self.data)
# self.data1 = self.data.split(symbol,1)[0]
self.data2 = int(newdata.split(symbol[1],1)[0])/100
self.data3 = int(newdata.split(symbol[1],1)[1])/100
# print(self.data1,self.data2,self.data3)
else:
# self.data1 = 0
self.data2 = 0
self.data3 = 0
# 蓝牙展示框更新1:浊度传感器(本项目中用了三个传感器)-已取消
# def check_serial_queue1(self):
# if self.data1:
# self.serial_data1.append(self.data1)
# # 维持10个数据
# if len(self.serial_data1) > 10:
# self.serial_data1.pop(0)
# # 一口气展示十条数据
# # print(self.serial_data1)
# display_text = "\n".join(self.serial_data1)
# self.serial_data_label1.config(text=data_label[0]+"\n"+display_text)
# self.draw_serial_figure1()
def check_serial_queue2(self):
if self.data2:
self.serial_data2.append(self.data2)
if len(self.serial_data2) > 20:
self.serial_data2.pop(0)
self.serial_data_label2.config(text=data_label[1]+"\n"+str(self.serial_data2[10])+"\n"+str(self.serial_data2[11])
+"\n"+str(self.serial_data2[12])+"\n"+str(self.serial_data2[13])
+"\n"+str(self.serial_data2[14])+"\n"+str(self.serial_data2[15])
+"\n"+str(self.serial_data2[16])+"\n"+str(self.serial_data2[17])
+"\n"+str(self.serial_data2[18])+"\n"+str(self.serial_data2[19]))
self.draw_serial_figure2()
def check_serial_queue3(self):
# 因为本人第三个传感器用的是TDS,在空气中读数为0,所以只能用self.data2而非self.data3判定信息有无
# 但self.data2和self.data3应该是一起传的,在将data put到queue里的函数和将data分解为data2,data3的函数中应该能检测异常
if self.data2:
self.serial_data3.append(self.data3)
if len(self.serial_data3) > 20:
self.serial_data3.pop(0)
self.serial_data_label3.config(text=data_label[2]+"\n"+str(self.serial_data3[10])+"\n"+str(self.serial_data3[11])
+"\n"+str(self.serial_data3[12])+"\n"+str(self.serial_data3[13])
+"\n"+str(self.serial_data3[14])+"\n"+str(self.serial_data3[15])
+"\n"+str(self.serial_data3[16])+"\n"+str(self.serial_data3[17])
+"\n"+str(self.serial_data3[18])+"\n"+str(self.serial_data3[19]))
self.draw_serial_figure3()
# 蓝牙数据绘图
# def draw_serial_figure1(self):
# serial_figure = Figure(figsize=figsize, dpi=100)
# serial_subplot = serial_figure.add_subplot()
# x = [1,2,3,4,5,6,7,8,9,10]
# serial_subplot.plot(x, self.serial_data1, label=figure_legend[0])
# serial_subplot.legend()
# canvas = FigureCanvasTkAgg(serial_figure, master=self.image_frame)
# canvas.get_tk_widget().grid(row=0, column=3)
# canvas.draw()
def draw_serial_figure2(self):
serial_figure = Figure(figsize=figsize, dpi=100)
serial_subplot = serial_figure.add_subplot()
x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
serial_subplot.plot(x, self.serial_data2, label=figure_legend[1])
serial_subplot.legend()
canvas = FigureCanvasTkAgg(serial_figure, master=self.image_frame)
canvas.get_tk_widget().grid(row=1, column=1)
canvas.draw()
def draw_serial_figure3(self):
serial_figure = Figure(figsize=figsize, dpi=100)
serial_subplot = serial_figure.add_subplot()
x = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
serial_subplot.plot(x, self.serial_data3 ,label=figure_legend[2])
serial_subplot.legend()
canvas = FigureCanvasTkAgg(serial_figure, master=self.image_frame)
canvas.get_tk_widget().grid(row=1, column=3)
canvas.draw()
# 使用同一线程更新两类数据
def all_update(self):
while not self.stop_event.is_set():
self.video_update()
self.serial_update()
# 一定要停一段时间,不然queue会被frame塞满,视频压根打不开!
time.sleep(1/FPS)
# 视频流实时获取
# 将前面cv2.VideoCapture获取的cap进行read,获取单帧的frame
def video_update(self):
# 如果前面连接失败后选择不重连,则取消更新
if self.video_flag and not self.only_video_choice:
# if self.stop_event.is_set():
# break
ret, frame = self.cap.read()
if not ret:
self.video_label.config(text="摄像头断开连接")
self.video_flag = False
# break
self.video_queue.put(frame)
# frame = cv2.resize(frame, global_data.frame_size)
# frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# image = Image.fromarray(frame)
# self.photo = ImageTk.PhotoImage(image=image)
# self.video_label.config(image=self.photo)
# 蓝牙数据实时获取
def serial_update(self):
if self.serial_flag:
# flag作用与上面video相同
# if self.stop_event.is_set():
# break
# 数据解码,默认utf-8格式
if self.serial.in_waiting > 0:
data = self.serial.read_all().decode('utf-8').rstrip()
if data != '' and data.startswith(symbol[0]) and data.endswith(symbol[2]):
self.serial_queue.put(data)
time.sleep(1/FPS)
# global count
# if count %2 == 1:
# self.serial_queue.put("#114/514*")
# else:
# self.serial_queue.put("#1919/810*")
# count += 1
# print(count)
# a = "1/1/4"
# self.serial_queue.put(a)
# self.serial_data.append(data)
# if len(self.serial_data) > 10:
# self.serial_data.pop(0)
# self.auto_save_data(data)
# display_text = "\n".join(self.serial_data)
# self.serial_label.config(text=display_text)
# 保存图片的按钮关联的:手动保存图片函数
def button_save_image(self):
if self.frame is not None:
#将图片保存至桌面
base_filename = "image.png"
desktop_path = os.path.join(os.path.expanduser("~"), "Desktop", base_filename)
counter = 1
#若图片已存在,则命名后加(1)(2),etc.,类似电脑自身的方式
while os.path.exists(desktop_path):
new_filename = f"{os.path.splitext(base_filename)[0]}({counter}){os.path.splitext(base_filename)[1]}"
desktop_path = os.path.join(os.path.expanduser("~"), "Desktop", new_filename)
counter += 1
frame = cv2.cvtColor(self.frame, cv2.COLOR_RGB2BGR)
cv2.imwrite(desktop_path, frame)
messagebox.showinfo("保存成功", f"图片已保存到 {desktop_path}")
# 蓝牙展示框更新函数中的:自动保存蓝牙数据函数
# 将queue中读取的data传给函数
def auto_save_data(self, data):
filename = "bluetooth_data.txt"
desktop_path = os.path.join(os.path.expanduser("~"), "Desktop", filename)
# 狠狠写入文件
if data:
with open(desktop_path, 'a') as file:
file.write(data +'\n')
# 关闭窗口
# 提供是否关闭的选择
def on_closing(self):
if messagebox.askyesno("关闭窗口", "是否关闭?"):
self.stop_event.set()
if self.video_enable:
self.cap.release()
self.root.destroy()
效果
已连接
(用的是上个版本,没有单独视频窗口的按键,plot曲线y轴数据也没有从str转为float导致数值大小错位,但大晚上不好打扰舍友睡觉,算了大伙意会一下qwq)
未连接
main主函数
解读
这里还有上面monitor.__init__里面的奇怪逻辑,就是对付各种“重试”“关闭”的异常处理;
以下主要是实现“retry”的循环功能,其他相当于input和monitor的拼接。
具体实现效果已在前两部分展示。
代码
# main.py
# 主函数
# 一个简单的单片机项目附带的监视器GUI软件
# 其中esp32(或其他摄像头)通过WiFi与PC相连传输视频流
# 传感器的数据通过arduino(或其他单片机)连接的蓝牙传给PC
# 并展示数据与自动绘制的图像
# 在同一窗口分四个窗格实时更新并可保存
import input
import monitor
# 创建窗口,获得视频流URL格式(或者OpenCV可处理的格式都行)和蓝牙串口名
input1 = input.input()
input1.root.mainloop()
# input窗口正常运行,用户没有选择中途关闭
# input.py中将signal置True,相当于后续操作的使能信号
if input.signal:
# 实现最主要的功能,展示视频和蓝牙数据
monitor1 = monitor.monitor(input.url, input.port, input.video_enable, input.serial_enable)
monitor1.root.mainloop()
# 特殊处理中涉及重试,跳回输入窗口,用retry_event创建循环
while monitor1.retry_event:
input1 = input.input()
input1.root.mainloop()
if input.signal:
monitor1 = monitor.monitor(input.url, input.port, input.video_enable, input.serial_enable)
monitor1.root.mainloop()
tips
如果想要将程序打包成可执行文件,实现真正可以小小炫耀一下的GUI软件功能,操作流程如下:
确认已经下载了Pyinstaller,如果没有则可以:
pip install Pyinstaller
接着有两种选择:
简简单单生成一个默认图标的可执行文件:
Pyinstaller -F -w main.py
或者如果有想要带有的特殊图标,则可以将图标的ico格式文件拖入mian.py所在文件夹中(假设命名为aaa.ico,且推荐这么干),然后:
Pyinstaller -F -w -i aaa.ico main.py
就能实现想要的图标力(喜)压力马斯内(图穷匕见)
示例
(声明:以下图标版权属于米哈游,作者不过是崩铁魔怔人,如有侵权请联系修改)
文件夹内:
生成的exe文件:
作者:可莉宝贝