Python的tkinter如何把日志弄进文本框(Text)

当我们用python的Tkinter包给程序设计界面时,在有些时候,我们是希望程序的日志显示在界面上的,因为用户也需要知道程序目前运行到哪一步了,以及程序当前的运行状态是否良好。python的通过print函数打印出来的日志通常显示在后台,但用户一般不希望查看后台,甚至希望后台被隐藏,所以希望直接在界面上看见日志。

例如,在SourceForge上下载的EasyModbusTCP Server Simulator,是一款用于模拟Modbus服务器通讯的PLC的程序。该程序的运行界面如下(如需了解Modbus是什么,请阅读ModbusTCP协议 - ioufev - 博客园):

在图中可看出,左侧Protocol Information下方的文本框显示的是该程序收到的Modbus客户端发给它的通讯请求。这个文本框里的内容是不断更新的,因为不断地会有新的请求发给它。这个日志的显示,有助于用户确认该Modbus服务器模拟器是否以及收到了请求,从而有助于Modbus相关的程序调试。

在通过python的tkinter设计程序时,可以将python程序中产生的日志显示在文本框(Text)中,实现一样的效果。

一、程序基本思路

通常情况下,在python中,print函数就是把一些日志打印在控制台中。但是,通过一些代码,可以要求程序的print函数打印的日志不打印在控制台中,而是打印在其它的数据流中。

可以阅读该网站:Two Ways to Capture Print。该网站介绍了两种办法,我们这里主要关注第二种方法,即把print()函数的输出导向另一个变量。

output_buffer = io.StringIO()
sys.stdout = output_buffer

在python中,运行这两句语句后,此时再运行print()函数,控制台上就不会再出现任何内容。这并不是说print()函数失效了,而是说它的输出流已不再是系统默认的sys.__stdout__了(之后会再提到并详细说明),而是改为通过io.StringIO()创建的output_buffer输出数据流。print()函数输出的内容之后都进入output_buffer了。

output = output_buffer.getvalue()

通过这句代码,把output_buffer输出数据流的内容取出,赋值到变量output中。

下面,把print()函数的输出数据流还原为控制台

sys.stdout = sys.__stdout__

注意:python中有一个叫sys的包,里面的__stdout__是python自带的控制台输出流。默认情况下,输出流都是python自带的控制台输出流。当然可以用上述代码临时更改。

现在,就可以通过print()函数在控制台上看见output的值,即当时从控制流output_buffer接收到的值。

二、Tkinter实现

所以,现在用tkinter做一个程序界面,实现将日志显示在文本框(Text)的功能。关于文本框如何使用,见Tkinter Text。

(一)例子说明

本文的例子,是用tkinter做一个AGV调度系统的客户端。文章中不涉及调度系统的具体交互方式,只展示界面本身的代码。在该界面中,植入车辆和创建任务的过程都是异步执行的线程,使用方式介绍见How to Use Thread in Tkinter Applications。

代码:

from tkinter import *
from tkinter import ttk
from threading import Thread
from os.path import dirname, join
from tkinter.scrolledtext import ScrolledText
import sys
sys.path.append(dirname(__file__))
sys.path.append(join(dirname(__file__), '..'))
#print(sys.path)
#from ANTServerRESTClient import ANTServerRestClient, MissionType, RetCode, FlexibleRouting, MissionPriority
#from ToolsAPI import MissionMaker, ServerManager, VehicleManager
import datetime
from time import sleep
from tkinter import messagebox
from InsertVehicles import insertProgram
from SimulateInstallation import simulation
import io
from icon import img
import base64
import os
#from os import execl


class insertThread(Thread):
    def __init__(self):
        super().__init__()
    def run(self):
        insertProgram()
        
class simulationThread(Thread): 
    def __init__(self, num):
        super().__init__()
        self.num = num
    def run(self):
        simulation(self.num)



class App(Tk):
    def __init__(self):
        super().__init__()
        self.title("Demo of End of Chain Logistics")
        self.resizable(False, False)
        tmp = open("tmp.ico", "wb+")
        tmp.write(base64.b64decode(img))
        tmp.close()
        self.iconbitmap('tmp.ico')
        os.remove("tmp.ico")
        #self.iconbitmap(join(dirname(__file__), 'favicon.ico'))
        style = ttk.Style()
        style.configure('TButton', font=('Helvetica', 14))
        #style.configure('TSpinbox', font=('Helvetica', 14))
        #self.geometry('500x500')
        self.labelAll = ttk.Label(self, text="Control panel of Logistics Demo", font=('Helvetica', 24))
        self.labelAll.grid(row=0, columnspan=2)
        self.insertButton = ttk.Button(self, text="Insert vehicles", command=self.insertVehicles)
        self.insertButton.grid(row=1, column=0)
        self.missionButton = ttk.Button(self, text="Create missions", command=self.createMissions)
        self.missionButton.grid(row=2, column=0)
        self.missNum = IntVar(value=50)
        self.missionNum = ttk.Spinbox(self, from_=1, to=100, textvariable=self.missNum, wrap=False, state='readonly', font=('Helvetica', 14))
        self.missionNum.grid(row=2, column=1)
        self.logLabel = ttk.Label(self, text="Logs:", font=('Helvetica', 14))
        self.logLabel.grid(row=3, column=0)
        self.logClearButton = ttk.Button(self, text="Clear all logs", command=self.clearLogs)
        self.logClearButton.grid(row=3, column=1)
        self.logs = StringVar()
        self.logText = ScrolledText(self, width=60, height=10, state='disabled')
        self.logText.grid(row=4, columnspan=2)
        self.logs.trace_add('write', self.updatelog)
        self.out_buffer = io.StringIO()
        sys.stdout.flush()
        sys.stdout = self.out_buffer
        self.protocol("WM_DELETE_WINDOW", self.on_closing)
        self.logPos = 0

        
    def insertVehicles(self):
        insertThd = insertThread()
        insertThd.start()
        self.monitorThread(insertThd, self.insertButton)
        
    def clearLogs(self):
        self.logText.config(state=NORMAL)
        self.logText.delete(0.0, END)
        self.logText.config(state=DISABLED)
        
    def monitorThread(self, thd:Thread, btn:ttk.Button):
        if thd.is_alive():
            btn.state(['disabled'])
            self.after(100, lambda: self.monitorThread(thd, btn))
        else:
            btn.state(['!disabled'])
        self.logs.set(self.out_buffer.getvalue())
            
    def createMissions(self):
        missionThd = simulationThread(self.missNum.get())
        missionThd.start()
        self.monitorThread(missionThd, self.missionButton)
        
    def updatelog(self, a, b, c):
        self.logText.config(state=NORMAL)
        self.logText.delete(0.0, END)
        self.logText.insert(0.0, self.logs.get())
        self.logText.config(state=DISABLED)
    
    def on_closing(self):
    # 处理关闭窗口事件的代码
        sys.stdout=sys.__stdout__
        sys.stdout.flush()
        self.destroy()
        #execl(sys.executable, sys.executable, *sys.argv)
        
    
            
if __name__=="__main__":
    app = App()
    app.mainloop()

首先,阅读App()里的__init__()函数,里面把sys.stdout改为了self.out_buffer,所以print的打印内容都会进入self.out_buffer。另外,monitorThread函数里的最后一句。也就是说,每次按下按钮后,监控线程的过程中,都会把self.logs变量更新为self.out_buffer里的内容。最后,在updatelog()中,文本框self.logText里的内容会被清空,并赋值为self.logs变量。这样,所有的日志,在线程运行时,都会不断被用于更新文本框。

运行结果:

(二)问题及解决方式

从动画中可知,每次运行Create missions时,日志会更新,但每次都会让滚动条滑到顶部。这不科学也不美观。原因主要在于,更新文本框的方法,是把日志整个(无论是已有的日志还是新产生的日志)都赋值给文本框,而不是只把新产生的日志附在文本框的最后。因此,为了让实现的方式更合理,需要做一些修改。修改的结果应该是:每次点击按钮,运行程序后,文本框不要删除任何文字,只是把新的日志附在最后。因此,新的日志,要和原来已有的日志分开。

例如:第一次按键后,产生的日志是:

1234
5678

第二次按键后,新产生的日志是

9101112
13141516

因此,在整个通过io.StringIO()创建的输出数据流output_buffer中,所有的日志都被保留。如果此时读取output_buffer.getValue(),结果是:

1234
5678
9101112
13141516

但我们要把前两行和后两行分开,要能做到在前两行已经在文本框里,再次读取该输出流时,只取出后两行,然后将它附在(即从末尾插入)文本框中。

关于如何使用输出流output_buffer,即这个io.StringIO类型的输出流,请阅读Python StringIO 模块完整指南及示例。在本文中,需要使用三个函数:

1. StringIO.seek():这个用于设置输出流目前的光标位置

2. StringIO.tell():这个用于得到输出流目前的光标位置

3. StringIO.read():这个用于从光标位置开始读取输出流的内容。和getValue()不同之处在于,getValue()是读取整个输出流的内容。

所以,读取输出流,从哪里开始读,是可以控制的。只要光标设置妥当,可以实现只读取最后即最新的几行的功能。

基本思路:每次读取后,记录输出流目前光标位置(即最后一个位置),用tell()函数。假设此时为时间点A,那么之后输出流有了更新(新日志)再读取时,首先把光标移到时间点A记录的光标位置(用seek()函数),然后再用read()函数读取,此时读取的内容只是新的几行日志,不包含之前已经读过的日志。这样,新日志只需附在文本框最后即可。

改进后,代码如下:

from tkinter import *
from tkinter import ttk
from threading import Thread
from os.path import dirname, join
from tkinter.scrolledtext import ScrolledText
import sys
sys.path.append(dirname(__file__))
sys.path.append(join(dirname(__file__), '..'))
#print(sys.path)
#from ANTServerRESTClient import ANTServerRestClient, MissionType, RetCode, FlexibleRouting, MissionPriority
#from ToolsAPI import MissionMaker, ServerManager, VehicleManager
import datetime
from time import sleep
from tkinter import messagebox
from InsertVehicles import insertProgram
from SimulateInstallation import simulation
import io
from icon import img
import base64
import os
#from os import execl


class insertThread(Thread):
    def __init__(self):
        super().__init__()
    def run(self):
        insertProgram()
        
class simulationThread(Thread): 
    def __init__(self, num):
        super().__init__()
        self.num = num
    def run(self):
        simulation(self.num)



class App(Tk):
    def __init__(self):
        super().__init__()
        self.title("Demo of End of Chain Logistics")
        self.resizable(False, False)
        tmp = open("tmp.ico", "wb+")
        tmp.write(base64.b64decode(img))
        tmp.close()
        self.iconbitmap('tmp.ico')
        os.remove("tmp.ico")
        #self.iconbitmap(join(dirname(__file__), 'favicon.ico'))
        style = ttk.Style()
        style.configure('TButton', font=('Helvetica', 14))
        #style.configure('TSpinbox', font=('Helvetica', 14))
        #self.geometry('500x500')
        self.labelAll = ttk.Label(self, text="Control panel of Logistics Demo", font=('Helvetica', 24))
        self.labelAll.grid(row=0, columnspan=2)
        self.insertButton = ttk.Button(self, text="Insert vehicles", command=self.insertVehicles)
        self.insertButton.grid(row=1, column=0)
        self.missionButton = ttk.Button(self, text="Create missions", command=self.createMissions)
        self.missionButton.grid(row=2, column=0)
        self.missNum = IntVar(value=50)
        self.missionNum = ttk.Spinbox(self, from_=1, to=100, textvariable=self.missNum, wrap=False, state='readonly', font=('Helvetica', 14))
        self.missionNum.grid(row=2, column=1)
        self.logLabel = ttk.Label(self, text="Logs:", font=('Helvetica', 14))
        self.logLabel.grid(row=3, column=0)
        self.logClearButton = ttk.Button(self, text="Clear all logs", command=self.clearLogs)
        self.logClearButton.grid(row=3, column=1)
        self.logs = StringVar()
        self.logText = ScrolledText(self, width=60, height=10, state='disabled')
        self.logText.grid(row=4, columnspan=2)
        self.logs.trace_add('write', self.updatelog)
        self.out_buffer = io.StringIO()
        sys.stdout.flush()
        sys.stdout = self.out_buffer
        self.protocol("WM_DELETE_WINDOW", self.on_closing)
        self.logPos = 0

        
    def insertVehicles(self):
        insertThd = insertThread()
        insertThd.start()
        self.monitorThread(insertThd, self.insertButton)
        
    def clearLogs(self):
        self.logText.config(state=NORMAL)
        self.logText.delete(0.0, END)
        self.logText.config(state=DISABLED)
        
    def monitorThread(self, thd:Thread, btn:ttk.Button):
        if thd.is_alive():
            btn.state(['disabled'])
            self.after(100, lambda: self.monitorThread(thd, btn))
        else:
            btn.state(['!disabled'])
        self.out_buffer.seek(self.logPos)
        outValue = self.out_buffer.read()
        #self.out_buffer.truncate(0)
        self.logPos = self.out_buffer.tell()
        if outValue != '':
            self.logs.set(outValue)
            
    def createMissions(self):
        missionThd = simulationThread(self.missNum.get())
        missionThd.start()
        self.monitorThread(missionThd, self.missionButton)
        
    def updatelog(self, a, b, c):
        self.logText.config(state=NORMAL)
        self.logText.insert(END, self.logs.get())
        self.logText.update()
        self.logText.config(state=DISABLED)
    
    def on_closing(self):
    # 处理关闭窗口事件的代码
        sys.stdout=sys.__stdout__
        sys.stdout.flush()
        self.destroy()
        #execl(sys.executable, sys.executable, *sys.argv)
        
           
if __name__=="__main__":
    app = App()
    app.mainloop()

注意阅读monitorThread()updatelog()的代码。这里,self.logs存的只是新的日志。运行效果如下:

从动画中看,每次产生新日志时,滚动条不动,新日志只是附在最后。这样的效果是合理的,也是有助于调试的。

三、总结

总之,用tkinter设计程序界面时,若要让日志显示在文本框中,首先要通过几句代码临时修改系统输出的数据流。然后,要将数据流的内容写进文本框。文本框光标可用于只提取最新的几行日志,这样更新文本框时,只需把新内容附在最后,无需全部删除重新赋值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/921777.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

flux的版本

1.flux1-dev.safetensors https://huggingface.co/black-forest-labs/FLUX.1-devhttps://huggingface.co/black-forest-labs/FLUX.1-dev原生的23.8G的模型。原生12B的模型,float16的。需要配合ae.safetensors,flux1-dev.safetensors以及clip-l和T5的权重使用,注意ae.sft和f…

阿里云私服地址

1.解压apache-maven-3.6.1-bin 2.配置本地仓库&#xff1a;修改conf/dettings.xml中的<localReoisitory>为一个指定目录。56行 <localRepository>D:\apache-maven-3.6.1-bin\apache-maven-3.6.1\mvn_repo</localRepository> 3.配置阿里云私服&#xff1a;…

【大数据学习 | Spark-Core】yarn-client与yarn-cluster的区别

1. yarn的提交命令 # yarn的提交命令参数 --master yarn #执行集群 --deploy-mode # 部署模式 --class #指定运行的类 --executor-memory #指定executor的内存 --executor-cores # 指定核数 --num-executors # 直接指定executor的数量 --queue # 指定队列 2. yarn-client模式…

【汽车制动】汽车制动相关控制系统

目录 1.ABS (Anti-lock Brake System&#xff0c;防抱死制动系统) 2.EBD&#xff08;Electronic Brake-force Distribution&#xff0c;电子制动力分配系统&#xff09; 3.TCS&#xff08;Traction Control System&#xff0c;牵引力控制系统&#xff09; 4.VDC&#xff08…

《TCP/IP网络编程》学习笔记 | Chapter 15:套接字与标准 I/O

《TCP/IP网络编程》学习笔记 | Chapter 15&#xff1a;套接字与标准 I/O 《TCP/IP网络编程》学习笔记 | Chapter 15&#xff1a;套接字与标准 I/O标准 I/O 函数标准 I/O 函数的两个优点标准 I/O 函数和系统函数之间的性能对比标准 I/O 函数的几个缺点 使用标准 I/O 函数利用 fd…

<OS 有关> ubuntu 24 不同版本介绍 安装 Vmware tools

原因 想用 apt-get download 存到本地 / NAS上&#xff0c;减少网络流浪。 看到 VMware 上的确实有 ubuntu&#xff0c;只是版本是16。 ubuntu 版本比较&#xff1a;LTS vs RR LTS: Long-Term Support 长周期支持&#xff0c; 一般每 2 年更新&#xff0c;会更可靠与更稳定…

支持多种快充协议和支持多种功能的诱骗取电协议芯片

汇铭达XSP15是一款应用于手持电动工具、智能家居、显示器、音箱等充电方案的大功率快充协议芯片&#xff0c;支持最大功率100W给设备快速充电&#xff0c;大大缩短了充电时间。芯片支持通过UART串口发送电压/电流消息供其它芯片读取。支持自动识别连接的是电脑或是充电器。支持…

【一篇搞定配置】网络分析工具WireShark的安装与入门使用

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;各种软件安装与配置_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1.…

JavaWeb之综合案例

前言 这一节讲一个案例 1. 环境搭建 然后就是把这些数据全部用到sql语句中执行 2.查询所有-后台&前台 我们先写后台代码 2.1 后台 2.2 Dao BrandMapper&#xff1a; 注意因为数据库里面的名称是下划线分割的&#xff0c;我们类里面是驼峰的&#xff0c;所以要映射 …

【STM32】MPU6050初始化常用寄存器说明及示例代码

一、MPU6050常用配置寄存器 1、电源管理寄存器1&#xff08; PWR_MGMT_1 &#xff09; 此寄存器允许用户配置电源模式和时钟源。 DEVICE_RESET &#xff1a;用于控制复位的比特位。设置为1时复位 MPU6050&#xff0c;内部寄存器恢复为默认值&#xff0c;复位结束…

隐私友好型分析平台Plausible Analytics

什么是 Plausible Analytics &#xff1f; Plausible Analytics 是一个简单、轻量级&#xff08;小于1KB&#xff09;、开源且隐私友好的网站分析工具&#xff0c;旨在作为 Google Analytics 的替代品。它不使用 cookies 并且完全符合 GDPR、CCPA 和 PECR 法规&#xff0c;因此…

Flutter:RotationTransition旋转动画

配置vsync&#xff0c;需要实现一下with SingleTickerProviderStateMixinclass _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin{// 定义 AnimationController late AnimationController _controller;overridevoid initState() {super…

【大数据学习 | Spark-Core】Spark提交及运行流程

spark的集群运行结构 我们要选择第一种使用方式 命令组成结构 spark-submit [选项] jar包 参数 standalone集群能够使用的选项。 --master MASTER_URL #集群地址 --class class_name #jar包中的类 --executor-memory MEM #executor的内存 --executor-cores NUM # executor的…

青训营刷题笔记16

问题描述 小R从班级中抽取了一些同学&#xff0c;每位同学都会给出一个数字。已知在这些数字中&#xff0c;某个数字的出现次数超过了数字总数的一半。现在需要你帮助小R找到这个数字。 测试样例 样例1&#xff1a; 输入&#xff1a;array [1, 3, 8, 2, 3, 1, 3, 3, 3] 输出…

C4D技巧总结

鼠标右键单击这两个小箭头可以把参数恢复到默认值&#xff01; 对象坐标 全局坐标 按住Alt键&#xff0c;点击挤压&#xff08;或者其他绿色的图标&#xff09;&#xff0c;可以快速形成父子级效果&#xff01;

(动画)Qt控件 QLCDNumer

文章目录 LCD Number1. 介绍2. 核心属性3 . 代码实现:倒计时1. 在界⾯上创建⼀个 QLCDNumber , 初始值设为 10.2. 修改 widget.h 代码, 创建⼀个 QTimer 成员, 和⼀个 updateTime 函数3. 修改 widget.cpp, 在构造函数中初始化 QTimer4. 修改 widget.cpp, 实现 updateTime 4. 动…

draggable的el-dialog实现对话框标题可以选择

请看图 这个对话框使用了el-dialog并且draggable属性设置成了true&#xff0c;所以标题栏这里就可以拖动&#xff0c;现在用户想选中标题栏的文本进而复制。我看到这个需求头都大了。 我能想到的方案有三个&#xff1a;1. 取消draggable为true 2. 标题文案后面加一个复制按钮 …

DeepSpeed-chat RLHF实战

轩辕-6bRLHF落地实战 模型介绍&#xff1a;轩辕-6B 模型库 (modelscope.cn) 1.1偏好数据集构建 ​ 1.1.1Prompt构建 1.1.2 Response生成 保证RM训练数据和测试数据分布一致 使用模型来生成response&#xff0c;为了评价response的质量&#xff0c;可以提高采样参数中的…

Java-05 深入浅出 MyBatis - 配置深入 动态 SQL 参数、循环、片段

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 大数据篇正在更新&#xff01;https://blog.csdn.net/w776341482/category_12713819.html 目前已经更新到了&#xff1a; MyBatis&#xff…

Prompting LLMs to Solve Complex Tasks: A Review

文章目录 题目简介任务分解未来方向结论 题目 促使 LLM 解决复杂任务&#xff1a; 综述 论文地址&#xff1a;https://www.intjit.org/cms/journal/volume/29/1/291_3.pdf 简介 大型语言模型 (LLM) 的最新趋势显而易见&#xff0c;这体现在大型科技公司的投资以及媒体和在线社…