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

ops/2024/11/24 5:48:44/

当我们用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()函数的输出导向另一个变量。

python">output_buffer = io.StringIO()
sys.stdout = output_buffer

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

python">output = output_buffer.getvalue()

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

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

python">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。

代码:

python">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 execlclass insertThread(Thread):def __init__(self):super().__init__()def run(self):insertProgram()class simulationThread(Thread): def __init__(self, num):super().__init__()self.num = numdef 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_bufferself.protocol("WM_DELETE_WINDOW", self.on_closing)self.logPos = 0def 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()函数读取,此时读取的内容只是新的几行日志,不包含之前已经读过的日志。这样,新日志只需附在文本框最后即可。

改进后,代码如下:

python">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 execlclass insertThread(Thread):def __init__(self):super().__init__()def run(self):insertProgram()class simulationThread(Thread): def __init__(self, num):super().__init__()self.num = numdef 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_bufferself.protocol("WM_DELETE_WINDOW", self.on_closing)self.logPos = 0def 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设计程序界面时,若要让日志显示在文本框中,首先要通过几句代码临时修改系统输出的数据流。然后,要将数据流的内容写进文本框。文本框光标可用于只提取最新的几行日志,这样更新文本框时,只需把新内容附在最后,无需全部删除重新赋值。


http://www.ppmy.cn/ops/136244.html

相关文章

SpringMVC应用专栏介绍

专栏导读 在当今快速发展的互联网时代,SpringMVC作为Java开发中的核心框架之一,已经成为构建企业级Web应用的首选技术。本“SpringMVC应用”专栏旨在为读者提供一个全面深入的学习平台,帮助读者掌握SpringMVC的精髓,提升Web开发能…

MySQL子查询介绍和where后的标量子查询

子查询介绍 出现在其他语句中的select语句,被包裹的select语句就是子查询或内查询 包裹子查询的外部的查询语句:称主查询语句 select last_name from employees where department_id in( select department_id from departments where location_id170…

MS16-075(烂土豆)

烂土豆提取 所谓的烂土豆提权就是俗称的MS16-075,其是一个本地提权,是针对本地用户的,不能用于域用户。可以将Windows工作站上的特权从最低级别提升到“ NT AUTHORITY \ SYSTEM” – Windows计算机上可用的最高特权级别 复现 上线webshell靶机为windows server 2012 r2 使…

基于网页的大语言模型聊天机器人

代码功能 用户交互界面: 包括聊天历史显示区域和输入框,用户可以输入消息并发送。 消息发送和显示: 用户输入消息后点击“Send”按钮或按下回车键即可发送。 消息发送后显示在聊天记录中,并通过异步请求与后端 AI 模型通信&am…

Thymeleaf模板引擎生成的html字符串转换成pdf

依赖引入implementation("org.springframework.boot:spring-boot-starter-thymeleaf")implementation("org.xhtmlrenderer:flying-saucer-pdf")将ITemplateEngine注入到spring管理的类中, Context context new Context(); context.setVariable…

蓝桥杯第22场小白入门赛2~5题

这场比赛开打第二题就理解错意思了,还以为只能用3个消除和5个消除其中一种呢,结果就是死活a不过去,第三题根本读不懂题意,这蓝桥杯的题面我只能说出的是一言难尽啊。。第四题写出来一点但是后来知道是错了,不会正解&am…

快速图像识别:落叶植物叶片分类

1.背景意义 研究背景与意义 随着全球生态环境的变化,植物的多样性及其在生态系统中的重要性日益受到关注。植物叶片的分类不仅是植物学研究的基础,也是生态监测、农业管理和生物多样性保护的重要环节。传统的植物分类方法依赖于人工观察和专家知识&…

.net 8使用hangfire实现库存同步任务

C# 使用HangFire 第一章:.net Framework 4.6 WebAPI 使用Hangfire 第二章:net 8使用hangfire实现库存同步任务 文章目录 C# 使用HangFire前言项目源码一、项目架构二、项目服务介绍HangFire服务结构解析HangfireCollectionExtensions 类ModelHangfireSettingsHttpAuthInfoUs…