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