python3tkinter实践历程(四)——基于socket通信与tkinter的TCP串口客户端(仿CRT)文章目录系列文章目录分享背景制作背景最终功能工具截图展示代码详解系列
python3+tkinter实践历程(四)——基于socket通信与tkinter的TCP串口客户端(仿CRT)
文章目录 系列文章目录 分享背景 制作背景 最终功能 工具截图展示 代码详解
系列文章目录 python3+tkinter实践历程(一)——基于requests与tkinter的API工具 python3+tkinter实践历程(二)——基于tkinter的日志检索工具 python3+tkinter实践历程(三)——基于requests与tkinter的模拟web登录工具 python3+tkinter实践历程(四)——模仿CRT完成基于socket通信与tkinter的TCP串口客户端
分享背景 ①分享意图在于帮助新入门的朋友,提供思路,里面详细的注释多多少少能解决一些问题。欢迎大佬指点跟交流。 ②2021年8月,开始陆续有需求制作一些工具,因为python语言跟tkinter工具相对简单,所以就基于这些做了好几个不同用处的工具。 ③分享从完成的第一个工具开始分享,分享到最新完成的工具,对于tkinter的理解也从一开始的摸索入门,到后来逐渐熟练,完成速度也越来越快,所用到的tk的功能点也越来越多。 ④这是发布前做的最后一个工具,也是最难、最复杂、要求最多、耗时最长的一个。
制作背景 ① 研发部自己做一个串口服务器,硬件部解决板子问题,开发将软件写入板子。最终服务器可以用特制的线连通24个设备的串口,开启了TCP服务端口,服务器驱动将24个设备的日志实时的通过socket套接字将发送给已连接的TCP客户端。 ② 本人所做的就是这个接收、处理串口服务器传过来的日志的TCP客户端。 ③ 功能需求如最终功能所示,完全实现了所有预期需求。
最终功能 ① 提供界面输入-TCP服务器的IP、端口 ② 提供界面选项-保存日志的目录,成功连接TCP服务器后,自动将所有接收到的日志保存,按日期划分文件夹,按串口划分log文件。 ③ 提供界面打印界面,自主选择串口打印,且与保存日志功能完全分离,互不干扰。 ④ 打印日志的界面支持增、删、重命名。 ⑤ 工具支持读取界面配置、记录历史配置 ⑥ 支持给不同串口发送命令,发送命令框支持回车发送+按钮发送 ⑦ 快速发送按钮支持读取配置 ⑧ 提供选项-滚动条是否自动跟随新打印的日志,支持实时改变
工具截图展示
代码详解 # -*- coding&#61;utf-8 -*- import tkinter as tk from tkinter import ttk import tkinter.font as tf import threading import os import datetime import socket import re import time import json from tkinter.filedialog import askdirectory# 用于连接TCP服务器、接收、发送数据的类 class SocketServer(object):def __init__(self, ip, port):# 初始化时自动连接TCP服务器self.sock &#61; socket.socket(socket.AF_INET, socket.SOCK_STREAM)print(&#39;开始连接串口服务器%s:%d&#39; % (ip, port))self.sock.connect((ip, port))print(&#39;连接完成&#39;)def get_serial_data(self):# 接收从服务器发送过来的日志&#xff0c;返回给上层recv_info &#61; self.sock.recv(7168).decode("utf-8", "replace") # 服务器传过来的是bytes类型,收到数据后编码变成str# print(recv_info)return recv_infodef send_data(self, data):# 将已经处理好的数据直接发送给服务器self.sock.send(data)class Application(tk.Frame):def __init__(self, master&#61;None):# 进行初始化&#xff0c;打开工具时完成的动作tk.Frame.__init__(self, master)self.grid()self.save_log_dir &#61; &#39;&#39; # 记录保存日志的路径self.is_running &#61; &#39;&#39; # 记录是否TCP交互正在进行self.LOG_LINE_NUM &#61; 0 # 记录日志框中打印的日志行数self.see_log &#61; True # 是否滚动条跟随最新打印的日志self.button_column &#61; 0 # 用于记录多个tab页所用过的column属性的最大值&#xff0c;以便于后新增的tab页可以出现在最后面。否则在多次删减过后&#xff0c;出现新增tab页生成位置异常的bugself.top &#61; &#39;&#39; # 重命名窗口&#xff0c;后续变成重命名窗口界面的对象self.tab &#61; {} # TAB集合&#xff0c;所有的TAB按钮与日志打印框的对象和显示的text文本都记录在这里&#xff0c;多tab页日志打印框的核心字典self.active &#61; &#39;&#39; # 当前置顶&#xff0c;记录当前切换的TAB&#xff0c;查看、发送、删除、重命名四个的操作都基于这个调用self.create_widgets() # 执行下面函数&#xff0c;创建图形化界面def create_widgets(self):"""创建图形化界面"""# 每一行用一个Frame包裹&#xff0c;里面的控件基于Frame来创建# 第一行row1 &#61; tk.Frame(self)row1.grid(row&#61;0, column&#61;0, sticky&#61;&#39;w&#39;, padx&#61;(15, 0), pady&#61;10)# IP端口输入框server_url &#61; tk.Label(row1, text&#61;"serial server IP and port(xx:xx)")server_url.grid(row&#61;0, column&#61;1)server_url &#61; tk.StringVar()server_url.set("10.100.10.180:9036") # set的作用的预置的文字(在标签内可变的文本)self.server_url &#61; tk.Entry(row1, textvariable&#61;server_url, width&#61;20)self.server_url.grid(row&#61;0, column&#61;2, padx&#61;2, pady&#61;2)# 日志存放目录tk.Label(row1, text&#61;"", width&#61;1).grid(row&#61;0, column&#61;3)tk.Label(row1, text&#61;"log dir").grid(row&#61;0, column&#61;4)self.dir &#61; tk.StringVar()self.dir.set("")self.log_dir &#61; tk.Entry(row1, textvariable&#61;self.dir, width&#61;35)self.log_dir.grid(row&#61;0, column&#61;5)tk.Button(row1, text&#61;"select directory", command&#61;self.selectPath).grid(row&#61;0, column&#61;6)# 开始按钮--连接服务器并接收保存日志tk.Label(row1, text&#61;"", width&#61;2).grid(row&#61;0, column&#61;7)start_btn &#61; tk.Button(row1, text&#61;"start connect and save log", command&#61;self.start_socket_conn).grid(row&#61;0, column&#61;8)# 停止按钮stop_btn &#61; tk.Button(row1, text&#61;"stop conn", command&#61;self.start_stop_task).grid(row&#61;0, column&#61;9, padx&#61;(10, 0))# 滚动条跟随日志复选框self.see_log_value &#61; tk.IntVar()self.see_log_value.set(1)tk.Checkbutton(row1, text&#61;"Follow log", variable&#61;self.see_log_value, command&#61;self.change_see_log).grid(row&#61;0, column&#61;14, padx&#61;20)# 第二、第三行都先创建一个Frame用来固定宽度# 第二行 tab页 非固定行self.row2 &#61; tk.Frame(self)self.row2.grid(row&#61;1, column&#61;0, sticky&#61;&#39;w&#39;, padx&#61;(15, 0))# 第二行的三个按钮: “&#43;”、“-”、“重命名”# 对应增加一个TAB页、删除当前置顶的TAB页、重命名当前TAB页的功能# “&#43;”绑定self.add_log_text方法来实现&#xff0c;“-”绑定self.del_log_text方法来实现&#xff0c;“重命名”绑定self.start_tab_rename方法来实现&#xff0c;三个函数均没有放进线程执行# 三个按钮的column属性分别为1000、1001、1002&#xff0c;目的是实现无论新增多少个TAB&#xff0c;这三个按钮都按顺序排在其他TAB按钮之后&#xff0c;具体展示可看图tk.Button(self.row2, text&#61;&#39;&#43;&#39;, relief&#61;&#39;solid&#39;, bd&#61;1, command&#61;self.add_log_text, width&#61;2).grid(row&#61;0, column&#61;1000, padx&#61;2)tk.Button(self.row2, text&#61;&#39;-&#39;, relief&#61;&#39;solid&#39;, bd&#61;1, command&#61;self.del_log_text, width&#61;2).grid(row&#61;0, column&#61;1001, padx&#61;2)tk.Button(self.row2, text&#61;&#39;重命名&#39;, relief&#61;&#39;solid&#39;, bd&#61;1, command&#61;self.start_tab_rename, width&#61;5).grid(row&#61;0, column&#61;1002, padx&#61;2)# 第三行 非固定行# 先创建一个Frame固定第三行&#xff0c;然后有多少个TAB就在这个Frame里的同一位置创建多少个Frame# 这样需要隐藏、显示、销毁这个界面的时候&#xff0c;只需控制里面的Frame完成grid(显示)、grid_forget(隐藏)、destroy(销毁)即可self.row3 &#61; tk.Frame(self, height&#61;650)self.row3.grid(row&#61;2, column&#61;0, sticky&#61;&#39;w&#39;, padx&#61;(15, 0))# 以下从外部文件interface_data.txt读取第二第三行(TAB按钮&#43;日志打印框)的内容# 调用work_for_data方法&#xff0c;返回字典interface_data &#61; self.work_for_data(&#39;tab&#39;, &#39;read&#39;)# 如果存在数据&#xff0c;则使用存在的数据来创建界面&#xff0c;如果不存在数据&#xff0c;则使用初始界面if not interface_data:# 初始界面为&#xff1a;{&#39;log1&#39;: {&#39;name_str&#39;: &#39;log1&#39;, &#39;serial_value&#39;: &#39;&#39;}}&#xff0c;即一个TAB&#xff0c;标识符为log1&#xff0c;输出在界面的名称也为log1&#xff0c;选择的串口号为空&#xff0c;即不选择interface_data &#61; {&#39;log1&#39;: {&#39;name_str&#39;: &#39;log1&#39;, &#39;serial_value&#39;: &#39;&#39;}}for name in interface_data:column &#61; self.button_column &#43; 1 # 每一次column &#43; 1&#xff0c;即在下一个位置创建控件# tab字典以这个遍历到的name作为key来创建新字典&#xff0c;来记录这个name名下所有的数据# 解释tab字典内的每一个key的含义&#xff1a;# tab[name][&#39;name&#39;] &#xff1a;字符串容器&#xff0c;用来修改显示在界面上的tab名称&#xff0c;如图的10.108&#xff0c;用set()设置&#xff0c;get()获取# tab[name][&#39;name_str&#39;] &#xff1a;字符串&#xff0c;记录显示在界面上的tab名称&#xff0c;如图的10.108# tab[name][&#39;c_button&#39;]&#xff1a;作为第二行的TAB的对象&#xff0c;绑定change_interface方法&#xff0c;来更改置顶的TAB# tab[name][&#39;Frame&#39;]&#xff1a;作为第三行的Frame里面的Frame&#xff0c;控制name名下的第三行全部控件&#xff0c;只需对该对象使用grid(显示)、grid_forget(隐藏)、destroy(销毁)方法即可# tab[name][&#39;text&#39;]: Text框(日志打印框)的对象&#xff0c;该TAB日志框的增删都通过该对象操作# tab[name][&#39;serial&#39;]&#xff1a;串口选择下拉框的对象# tab[name][&#39;serial&#39;][&#39;value&#39;]&#xff1a;串口下拉框的所有选项self.tab[name] &#61; dict()self.tab[name][&#39;name&#39;] &#61; tk.StringVar()self.tab[name][&#39;name&#39;].set(interface_data[name][&#39;name_str&#39;])self.tab[name][&#39;name_str&#39;] &#61; interface_data[name][&#39;name_str&#39;]self.tab[name][&#39;c_button&#39;] &#61; tk.Button(self.row2, textvariable&#61;self.tab[name][&#39;name&#39;], relief&#61;&#39;raised&#39;,bd&#61;1, command&#61;lambda _name&#61;name: self.change_interface(_name))self.tab[name][&#39;c_button&#39;].grid(row&#61;0, column&#61;column)self.button_column &#61; column # 刷新self.button_column的值&#xff0c;等于当前columnself.tab[name][&#39;Frame&#39;] &#61; tk.Frame(self.row3)self.tab[name][&#39;Frame&#39;].grid(row&#61;0, column&#61;0)ft &#61; tf.Font(size&#61;10)self.tab[name][&#39;text&#39;] &#61; tk.Text(self.tab[name][&#39;Frame&#39;], width&#61;180, height&#61;50, font&#61;ft, padx&#61;5, pady&#61;5,relief&#61;&#39;sunken&#39;)log_slide_bar &#61; tk.Scrollbar(self.tab[name][&#39;Frame&#39;], command&#61;self.tab[name][&#39;text&#39;].yview,orient&#61;"vertical") # 日志框的滚动条log_slide_bar.grid(row&#61;0, column&#61;1, sticky&#61;&#39;ns&#39;)self.tab[name][&#39;text&#39;].grid(row&#61;0, column&#61;0)self.tab[name][&#39;text&#39;].config(font&#61;ft, yscrollcommand&#61;log_slide_bar.set)tk.Label(self.tab[name][&#39;Frame&#39;], text&#61;"select serial").grid(row&#61;0, column&#61;2, sticky&#61;&#39;n&#39;, padx&#61;(0, 5))self.tab[name][&#39;serial&#39;] &#61; ttk.Combobox(self.tab[name][&#39;Frame&#39;], state&#61;&#39;readonly&#39;, width&#61;10)self.tab[name][&#39;serial&#39;].grid(row&#61;0, column&#61;3, sticky&#61;&#39;n&#39;)self.tab[name][&#39;serial_value&#39;] &#61; interface_data[name][&#39;serial_value&#39;]self.tab[name][&#39;serial&#39;][&#39;value&#39;] &#61; (&#39;serial01&#39;, &#39;serial02&#39;, &#39;serial03&#39;, &#39;serial04&#39;, &#39;serial05&#39;, &#39;serial06&#39;,&#39;serial07&#39;, &#39;serial08&#39;, &#39;serial09&#39;, &#39;serial10&#39;, &#39;serial11&#39;, &#39;serial12&#39;,&#39;serial13&#39;, &#39;serial14&#39;, &#39;serial15&#39;, &#39;serial16&#39;, &#39;serial17&#39;, &#39;serial18&#39;,&#39;serial19&#39;, &#39;serial20&#39;, &#39;serial21&#39;, &#39;serial22&#39;, &#39;serial23&#39;, &#39;serial24&#39;)self.tab[name][&#39;serial&#39;].bind(&#39;<>&#39;, self.start_change_serial)if interface_data[name][&#39;serial_value&#39;]:self.tab[name][&#39;serial&#39;].set(interface_data[name][&#39;serial_value&#39;])# TAB全部生成完成&#xff0c;选择一个TAB作为置顶&#xff0c;显示该TAB的第三行&#xff0c;同时按钮设置成不可操作。其他TAB的第三行隐藏&#xff0c;按钮设置成可操作for i in self.tab:if not self.active:self.active &#61; iself.tab[i][&#39;Frame&#39;].grid()self.tab[i][&#39;c_button&#39;].config(state&#61;tk.DISABLED)print(&#39;置顶%s界面&#39; % i)else:self.tab[i][&#39;Frame&#39;].grid_forget()self.tab[i][&#39;c_button&#39;].config(state&#61;tk.ACTIVE)# 第四行&#xff08;命令输入框&#43;发送按钮&#xff09;row4 &#61; tk.Frame(self)row4.grid(row&#61;3, column&#61;0, sticky&#61;&#39;w&#39;, padx&#61;(15, 0), pady&#61;(15, 0))# 命令输入框ft &#61; tf.Font(size&#61;10)self.log_input &#61; tk.Text(row4, width&#61;100, height&#61;4, font&#61;ft)self.log_input.grid(row&#61;0, column&#61;1)self.log_input.bind(&#39;&#39;, self.start_send_data) # text框绑定回车键&#xff0c;当收到回车键就触发start_send_data方法# 注意&#xff1a;self.start_send_data以return &#39;break&#39;结尾&#xff0c;则回车的换行效果不会在文本框触发&#xff0c;如果没有&#xff0c;则会触发换行tk.Button(row4, text&#61;"send info", command&#61;self.start_send_data).grid(row&#61;0, column&#61;2, padx&#61;10)# 第五行 (快速发送命令按钮)row5 &#61; tk.Frame(self)row5.grid(row&#61;4, column&#61;0, sticky&#61;&#39;w&#39;, padx&#61;(15, 0), pady&#61;(15, 0))# 快速发送命令按钮# 只能从外部的quick_send_data.txt文件获取&#xff0c;不能在界面上添加quick_send_data &#61; self.work_for_data(&#39;quick_send&#39;) 调用work_for_data方法获取文本中的信息if quick_send_data:print(quick_send_data)column &#61; 0for data in quick_send_data:# 解析出文本的信息&#xff0c;遍历内容构建按钮&#xff0c;按钮绑定self.start_quick_print方法# data是一个字典&#xff0c;包含了显示在界面的按钮名称和执行的命令。# 将data2&#61;data&#xff0c;然后把data2传入self.start_quick_print方法tk.Button(row5, text&#61;data[&#39;name&#39;], command&#61;lambda data2&#61;data: self.start_quick_print(data2)).grid(row&#61;0, column&#61;column, padx&#61;(0, 10))column &#61; column &#43; 1def add_log_text(self):"""增加多个tab页"""# 创建界面完成后&#xff0c;点击“&#43;”按钮触发此函数&#xff0c;新增TAB按钮与第三行的日志框&#xff0c;方法与create_widgets中第二第三行的逻辑大同小异now_log_text_num &#61; 0for ii in range(1, 1000):if &#39;log&#39; &#43; str(ii) not in self.tab:now_log_text_num &#61; iibreakelse:now_log_text_num &#61; 1000column &#61; self.button_column &#43; 1name &#61; &#39;log&#39; &#43; str(now_log_text_num)self.tab[name] &#61; dict()self.tab[name][&#39;name&#39;] &#61; tk.StringVar()self.tab[name][&#39;name&#39;].set(name)self.tab[name][&#39;name_str&#39;] &#61; nameself.tab[name][&#39;c_button&#39;] &#61; tk.Button(self.row2, textvariable&#61;self.tab[name][&#39;name&#39;], relief&#61;&#39;raised&#39;, bd&#61;1, command&#61;lambda _name&#61;name: self.change_interface(_name))self.tab[name][&#39;c_button&#39;].grid(row&#61;0, column&#61;column)self.button_column &#61; columnself.tab[name][&#39;Frame&#39;] &#61; tk.Frame(self.row3)self.tab[name][&#39;Frame&#39;].grid(row&#61;0, column&#61;0)ft &#61; tf.Font(size&#61;10)self.tab[name][&#39;text&#39;] &#61; tk.Text(self.tab[name][&#39;Frame&#39;], width&#61;180, height&#61;50, font&#61;ft, padx&#61;5, pady&#61;5, relief&#61;&#39;sunken&#39;)log_slide_bar &#61; tk.Scrollbar(self.tab[name][&#39;Frame&#39;], command&#61;self.tab[name][&#39;text&#39;].yview,orient&#61;"vertical")log_slide_bar.grid(row&#61;0, column&#61;1, sticky&#61;&#39;ns&#39;)self.tab[name][&#39;text&#39;].grid(row&#61;0, column&#61;0)self.tab[name][&#39;text&#39;].config(font&#61;ft, yscrollcommand&#61;log_slide_bar.set)tk.Label(self.tab[name][&#39;Frame&#39;], text&#61;"select serial").grid(row&#61;0, column&#61;2, sticky&#61;&#39;n&#39;, padx&#61;(0, 5))self.tab[name][&#39;serial&#39;] &#61; ttk.Combobox(self.tab[name][&#39;Frame&#39;], state&#61;&#39;readonly&#39;, width&#61;10)self.tab[name][&#39;serial&#39;].grid(row&#61;0, column&#61;3, sticky&#61;&#39;n&#39;)self.tab[name][&#39;serial_value&#39;] &#61; &#39;&#39;self.tab[name][&#39;serial&#39;][&#39;value&#39;] &#61; (&#39;serial01&#39;, &#39;serial02&#39;, &#39;serial03&#39;, &#39;serial04&#39;, &#39;serial05&#39;, &#39;serial06&#39;,&#39;serial07&#39;, &#39;serial08&#39;, &#39;serial09&#39;, &#39;serial10&#39;, &#39;serial11&#39;, &#39;serial12&#39;,&#39;serial13&#39;, &#39;serial14&#39;, &#39;serial15&#39;, &#39;serial16&#39;, &#39;serial17&#39;, &#39;serial18&#39;,&#39;serial19&#39;, &#39;serial20&#39;, &#39;serial21&#39;, &#39;serial22&#39;, &#39;serial23&#39;, &#39;serial24&#39;)self.tab[name][&#39;serial&#39;].bind(&#39;<>&#39;, self.start_change_serial)self.active &#61; namefor i in self.tab:if i &#61;&#61; self.active:self.tab[i][&#39;Frame&#39;].grid()self.tab[i][&#39;c_button&#39;].config(state&#61;tk.DISABLED)print(&#39;置顶%s界面&#39; % i)else:self.tab[i][&#39;Frame&#39;].grid_forget()self.tab[i][&#39;c_button&#39;].config(state&#61;tk.ACTIVE)# 存放tab信息result &#61; self.work_for_data(&#39;tab&#39;, &#39;write&#39;)if result:print(&#39;存放tab信息与interface_data.txt完成&#39;)def del_log_text(self):"""删除当前置顶的tab页"""# 先换一个置顶的tab页面if len(self.tab) &#61;&#61; 1:print(&#39;只有一个tab页&#xff0c;不可销毁&#39;)returntarget &#61; self.active# 换一个置顶的TAB&#xff0c;该TAB显示&#xff0c;按钮不可用&#xff0c;其他TAB隐藏&#xff0c;按钮可用for i in self.tab:if i !&#61; self.active:self.active &#61; iself.tab[i][&#39;Frame&#39;].grid()self.tab[i][&#39;c_button&#39;].config(state&#61;tk.DISABLED)print(&#39;置顶%s界面&#39; % i)break# 销毁目标的第三行、与TAB按钮self.tab[target][&#39;Frame&#39;].destroy()self.tab[target][&#39;c_button&#39;].destroy()del self.tab[target]# 存放tab信息result &#61; self.work_for_data(&#39;tab&#39;, &#39;write&#39;)if result:print(&#39;存放tab信息与interface_data.txt完成&#39;)def tab_rename(self):"""给tab页面重命名"""try:if self.top: # 如果重命名窗口已经存在&#xff0c;就销毁这个窗口self.top.destroy()self.top &#61; &#39;&#39;self.top &#61; tk.Toplevel() # 创建最上层的窗口self.top.title(&#39;重命名&#39;) # 给该窗口命令self.top.transient(self) # Toplevel注册成master的临时窗口self.top.resizable(0, 0) # 不可改变大小# 居中width &#61; self.winfo_screenwidth()height &#61; self.winfo_screenheight()ww &#61; 150wh &#61; 75x &#61; (width-ww)/2y &#61; (height-wh)/2self.top.geometry("%dx%d&#43;%d&#43;%d" % (ww, wh, x, y)) # 自适应居中self.top.grid()# 重命名输入框name &#61; tk.StringVar()name.set(self.tab[self.active][&#39;name_str&#39;]) # 把该TAB按钮原来的名字显示出来self.rename_entry &#61; tk.Entry(self.top, textvariable&#61;name)self.rename_entry.grid(row&#61;0, column&#61;0, columnspan&#61;2, ipady&#61;5)# 确定按钮 # 绑定self.control_rename_btn方法self.rename_sure_btn &#61; tk.Button(self.top, text&#61;&#39;确定&#39;, command&#61;lambda : self.control_rename_btn(&#39;确定&#39;))self.rename_sure_btn.grid(row&#61;1, column&#61;0, padx&#61;10, pady&#61;5)# 取消按钮self.rename_cancel_btn &#61; tk.Button(self.top, text&#61;&#39;取消&#39;, command&#61;lambda : self.control_rename_btn(&#39;取消&#39;))self.rename_cancel_btn.grid(row&#61;1, column&#61;1, padx&#61;10, pady&#61;5)except Exception as e:print(&#39;初始化重命名界面异常:%s&#39; % e)def control_rename_btn(self, operate):"""控制重命名的确定取消按钮"""# self.tab[self.active][&#39;name&#39;]为该TAB按钮绑定的字符串容器&#xff0c;set()方法直接改变显示的名称&#xff0c;最后销毁重命名窗口if operate &#61;&#61; &#39;确定&#39;:if self.rename_entry.get():self.tab[self.active][&#39;name_str&#39;] &#61; self.rename_entry.get()self.tab[self.active][&#39;name&#39;].set(self.rename_entry.get())self.top.destroy()if operate &#61;&#61; &#39;取消&#39;:self.top.destroy()def socket_conn(self):"""与串口服务器建立TCP连接,无限循环获取数据&#xff0c;放进线程中执行"""ip_port &#61; self.server_url.get().split(&#39;:&#39;) # 获取TCP服务器IP和端口ip &#61; ip_port[0]port &#61; int(ip_port[1])global sock_conn # 创建全局变量&#xff0c;为SocketServer类的对象sock_conn &#61; SocketServer(ip, port)self.is_running &#61; True # 将 self.is_running设置为Truewhile True:if self.is_running: # 如果self.is_running被设置为False时&#xff0c;退出循环try:recv_info &#61; sock_conn.get_serial_data() # 获取数据except Exception as e:print(&#39;获取数据异常:%s&#39; % e)continuetry:# 以下为解析数据的方法&#xff0c;根据TCP服务器已经设定好的规则处理接收的数据&#xff0c;分解出日志本身与对应的串口parse_rule &#61; re.compile(r&#39;\*<\[(\d{2})]>\*&#39;) parse_result &#61; parse_rule.search(recv_info)if parse_result:log_info &#61; recv_info.replace(parse_result.group(0), &#39;&#39;)serial &#61; &#39;serial&#39; &#43; parse_result.group(1)else:print(&#39;串口标识识别失败:%s&#39; % recv_info)continueexcept Exception as e:print(&#39;解析数据异常:%s,异常数据:%s&#39; % (e, recv_info))continuetry:self.save_log(log_info, serial) # 保存该行日志except Exception as e:print(&#39;保存日志异常:%s&#39; % e)continuetry:self.output_log(log_info, serial) # 打印该行日志except Exception as e:print(&#39;输出日志异常:%s&#39; % e)continueelse:print(&#39;stopped&#39;)breakdef selectPath(self):"""把获取到的串口目录传入Entry"""dir_ &#61; askdirectory()self.dir.set(str(dir_))self.save_log_dir &#61; self.log_dir.get()def save_log(self, log_info, serial_port):"""把收到的日志分别传入不同的log文本中"""if not self.save_log_dir:# print(&#39;无传入日志目录&#39;)return# print(&#39;获取到的日志目录为:%s&#39; % self.save_log_dir)now_day &#61; datetime.datetime.now().strftime(&#39;%Y-%m-%d&#39;)# print(&#39;当前日期为:%s&#39; % now_day)log_folder &#61; os.path.join(self.save_log_dir, now_day)# print(&#39;存放今天日志目录为:%s&#39; % log_folder)if not os.path.exists(log_folder):# print(&#39;不存在%s文件夹,进行新建&#39; % log_folder)os.mkdir(log_folder)log_file &#61; os.path.join(log_folder, &#39;%s_%s.log&#39; % (serial_port, now_day))log_info &#61; log_info.rstrip(&#39;\n&#39;)with open(log_file, &#39;a&#43;&#39;, errors&#61;&#39;ignore&#39;, newline&#61;&#39;&#39;) as f:f.write(log_info)def change_serial(self):"""更改输出日志的串口"""# 当串口下拉框被操作时&#xff0c;会将当前置顶的TAB字典中的serial_value的值变成选择的串口&#xff0c;并且清空该TAB的日志框if self.tab[self.active][&#39;serial_value&#39;] !&#61; self.tab[self.active][&#39;serial&#39;].get():self.tab[self.active][&#39;serial_value&#39;] &#61; self.tab[self.active][&#39;serial&#39;].get()self.tab[self.active][&#39;text&#39;].delete(0.0, &#39;end&#39;)print(&#39;更改%s的输出串口为:%s&#39; % (self.active, self.tab[self.active][&#39;serial_value&#39;]))# 存放tab信息&#xff0c;因为有TAB的串口发生变成&#xff0c;所以把当前tab字典更新在interface_data.txt文件result &#61; self.work_for_data(&#39;tab&#39;, &#39;write&#39;)if result:print(&#39;存放tab信息与interface_data.txt完成&#39;)def change_interface(self, interface):"""显示界面"""for i in self.tab:if i &#61;&#61; interface:self.tab[interface][&#39;Frame&#39;].grid()self.tab[interface][&#39;c_button&#39;].config(state&#61;tk.DISABLED)else:self.tab[i][&#39;Frame&#39;].grid_forget()self.tab[i][&#39;c_button&#39;].config(state&#61;tk.ACTIVE)self.active &#61; interfaceprint(&#39;置顶%s:%s页面&#39; % (interface, self.tab[interface][&#39;name&#39;].get()))def work_for_data(self, key, work&#61;&#39;&#39;):"""存/取tab字典&#xff0c;取快捷输入数据"""try:if key &#61;&#61; &#39;tab&#39;:if work &#61;&#61; &#39;write&#39;:self.save_tab &#61; {}for i in self.tab:self.save_tab[i] &#61; {}self.save_tab[i][&#39;name_str&#39;] &#61; self.tab[i][&#39;name_str&#39;]self.save_tab[i][&#39;serial_value&#39;] &#61; self.tab[i][&#39;serial_value&#39;]with open(&#39;interface_data.txt&#39;, &#39;w&#39;) as f:data &#61; json.dumps(self.save_tab)f.write(data)return Trueif work &#61;&#61; &#39;read&#39;:with open(&#39;interface_data.txt&#39;, &#39;r&#39;) as f:data &#61; f.read()data &#61; json.loads(data)print(&#39;读取到的tab:%s&#39; % data)return dataif key &#61;&#61; &#39;quick_send&#39;:data_list &#61; []with open(&#39;quick_send_data.txt&#39;, &#39;r&#39;) as f:for i in f:if &#39;*start-&#39; in i:info &#61; {}l &#61; i.replace(&#39;*start-&#39;, &#39;&#39;)name &#61; l[0:l.rfind(&#39;&#61;&#39;)]l &#61; l[l.rfind(&#39;&#61;&#39;) &#43; 1:]if &#39;\enter&#39; in l:enter &#61; Truel &#61; l.replace(&#39;\enter&#39;, &#39;&#39;)else:enter &#61; Falseif &#39;\n&#39; in l:l &#61; l.replace(&#39;\n&#39;, &#39;&#39;)l &#61; l.split(&#39;;&#39;)info[&#39;data&#39;] &#61; linfo[&#39;name&#39;] &#61; nameinfo[&#39;enter&#39;] &#61; enterdata_list.append(info)return data_listexcept Exception as e:print(&#39;对%s进行%s操作异常:%s&#39; % (key, work, e))def output_log(self, log_info, serial_port):"""实时输出日志"""for i in self.tab:if self.tab[i][&#39;serial_value&#39;] &#61;&#61; serial_port:self.write_log_to_Text(i, log_info)breakdef stop_recv_data(self):self.is_running &#61; Falseprint(&#39;stopping&#39;)def send_data_to_server(self, data&#61;&#39;&#39;):"""发送数据给服务器"""try:if not self.tab[self.active][&#39;serial_value&#39;]:print(&#39;无指定串口&#39;)returnif data:print(&#39;有指定数据:%s&#39; % data)else:data &#61; self.log_input.get(0.0, &#39;end&#39;)self.log_input.delete(0.0, &#39;end&#39;)if not data:print(&#39;无数据&#39;)returnif data[-1] not in [&#39;\n&#39;, &#39;\r&#39;]:print(&#39;给数据加回车&#39;)data &#61; data &#43; &#39;\n&#39;print(&#39;给%s发送%s指令&#39; % (self.tab[self.active][&#39;serial_value&#39;], data))data &#61; &#39;*<[%s]>*&#39; % self.tab[self.active][&#39;serial_value&#39;][-2:] &#43; datasock_conn.send_data(bytes(data, encoding&#61;&#39;utf8&#39;))except Exception as e:print(&#39;发送数据异常:%s&#39; % e)def quick_print(self, data):if data:if len(data[&#39;data&#39;]) &#61;&#61; 1: # 只有一条数据if data[&#39;enter&#39;]: # 自动发送self.start_send_data(data&#61;data[&#39;data&#39;][0])else: # 打印出来&#xff0c;不自动发送self.log_input.insert(&#39;end&#39;, data[&#39;data&#39;][0])else: # 大于1条数据,需要发送多次for i in data[&#39;data&#39;]:if i &#61;&#61; &#39;p&#39;:print(&#39;sleep(1)&#39;)time.sleep(1)else:if data[&#39;enter&#39;]:self.start_send_data(data&#61;i)else:self.log_input.insert(&#39;end&#39;, i)def change_see_log(self):if self.see_log_value.get() in [&#39;1&#39;, 1]:self.see_log &#61; Trueprint(&#39;切换成跟随日志&#39;)else:self.see_log &#61; Falseprint(&#39;切换成不跟随日志&#39;)def write_log_to_Text(self, interface, log_msg):"""日志动态打印"""self.LOG_LINE_NUM &#61; int(self.tab[interface][&#39;text&#39;].index(&#39;end&#39;).split(&#39;.&#39;)[0]) - 1if self.LOG_LINE_NUM > 3001:print(&#39;log超过3000行&#xff0c;%s&#39; % self.LOG_LINE_NUM)del_target &#61; float(self.LOG_LINE_NUM - 3000 - 1)self.tab[interface][&#39;text&#39;].delete(1.0, del_target)logmsg_in &#61; "%s\n" % log_msgself.tab[interface][&#39;text&#39;].insert(&#39;end&#39;, logmsg_in)if self.active &#61;&#61; interface and self.see_log:self.tab[interface][&#39;text&#39;].see(&#39;end&#39;)def thread_it(self, func, *args):"""将函数打包进线程"""# 创建t &#61; threading.Thread(target&#61;func, args&#61;args)# 守护 !!!t.setDaemon(True)# 启动t.start()# 将以下的操作都放进线程中执行&#xff0c;以放工具主界面卡死def start_socket_conn(self, *args):self.thread_it(self.socket_conn)def start_save_log(self, *args):self.thread_it(self.save_log)def start_change_serial(self, *args):self.thread_it(self.change_serial)def start_output_log(self, *args):self.thread_it(self.output_log)def start_send_data(self, ev&#61;None, data&#61;&#39;&#39;):self.thread_it(self.send_data_to_server, data)return &#39;break&#39;def start_stop_task(self, *args):self.thread_it(self.stop_recv_data)def start_quick_print(self, data&#61;&#39;&#39;):self.thread_it(self.quick_print, data)def start_write_log(self, log_msg):self.thread_it(self.write_log_to_Text, log_msg)def start_tab_rename(self, *args):self.thread_it(self.tab_rename)root &#61; tk.Tk() # 创建Tk对象 app &#61; Application(master&#61;root) # 创建Application类的对象app&#xff0c;主界面为root root.title("串口收发客户端") # 设置app标题 root.resizable(False, False) # 禁止调整窗口大小sw &#61; root.winfo_screenwidth() # 得到屏幕宽度 sh &#61; root.winfo_screenheight() # 得到屏幕高度 # 窗口宽高为1500x900 ww &#61; 1500 wh &#61; 900 x &#61; (sw-ww) / 2 y &#61; (sh-wh) / 2 root.geometry("%dx%d&#43;%d&#43;%d" % (ww, wh, x, y)) # 自适应居中root.deiconify() # 显示窗口 app.mainloop() # 进入消息循环