如何在tkinter中将一个脚本的可滚动框架导入另一脚本的框架中
实现方案
核心问题是原workingcatch.py中的ShowProcessOutputDemo继承自tk.Tk,属于独立顶层窗口,无法直接嵌入其他窗口的框架中,我们只需要将其改为继承tk.Frame,就可以作为普通组件挂载到wrapper.py的outputframe下。
1. 修改workingcatch.py代码
完整修改后的代码如下:
from subprocess import Popen, PIPE, STDOUT from threading import Thread import tkinter as tk import logging import time import sys info = logging.getLogger(__name__).info logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") platform = windows = 'mswin' # 定义生成输出的示例子进程 cmd = [sys.executable or "python", "-u", "-c", """ import itertools, time for i in itertools.count(): print(i) time.sleep(0.5) exit() """, "exit"] class OStream(Thread): enable_print = True def handel_line(self, line): if platform == windows: # Windows系统换行符为"\r\n"而非"\n" line = line.replace(b"\r\n", b"\n") if self.enable_print: info("got: %r", line) if self.stream_print is not None: self.stream_print(line) def stop(self): self.alive = False def run(self): while self.alive: for s in self.ostrams: line = s.stdout.readline() if line: self.handel_line(line) time.sleep(0.2) info("OStream Exit") def pipe_proc(self, stream): self.ostrams.append(stream) def stream_callback(self, func): self.stream_print = func def __init__(self): self.ostrams = [] self.alive = True self.stream_print = None Thread.__init__(self) class Scrolable_Frame(tk.Frame): def get(self): return self.interior def __init__(self, master): tk.Frame.__init__(self, master) self.scroll = tk.Scrollbar(self) self.scroll.pack(side=tk.RIGHT, fill=tk.Y) self.canvas = tk.Canvas( self, bd=0, highlightthickness=0, yscrollcommand=self.scroll.set) self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.TRUE) self.scroll.config(command=self.canvas.yview) self.canvas.xview_moveto(0) self.canvas.yview_moveto(0) self.interior = tk.Frame(self.canvas) interior_id = self.canvas.create_window( 0, 0, window=self.interior, anchor=tk.NW ) def _configure_interior(_): size = (self.interior.winfo_reqwidth(), self.interior.winfo_reqheight()) self.canvas.config(scrollregion="0 0 %s %s" % size) if self.interior.winfo_reqwidth() != self.canvas.winfo_width(): self.canvas.config(width=self.interior.winfo_reqwidth()) self.interior.bind('<Configure>', _configure_interior) def _configure_canvas(_): if self.interior.winfo_reqwidth() != self.canvas.winfo_width(): self.canvas.itemconfigure(interior_id, width=self.canvas.winfo_width()) self.canvas.bind('<Configure>', _configure_canvas) def _on_mousewheel(event): self.canvas.yview_scroll(int(-1 * (event.delta / 120)), 'units') self.canvas.bind_all("<MouseWheel>", _on_mousewheel) # 改为继承tk.Frame而非tk.Tk class ShowProcessOutputDemo(tk.Frame): def __init__(self, master=None, **kwargs): """启动子进程,创建GUI组件""" super().__init__(master, **kwargs) self.proc = Popen(cmd, stdout=PIPE, stderr=STDOUT) self.ostream = OStream() self.ostream.pipe_proc(self.proc) self.ostream.stream_callback(self.add_label) self.ostream.start() self.exit_button = tk.Button( self, text="Stop subprocess", command=self.stop ) self.exit_button.pack(pady=20) self.scolable_frame = Scrolable_Frame(self) self.scolable_frame.pack( expand=True, fill=tk.BOTH, pady=20, padx=20 ) def add_label(self, line): line_text = 'OStream line content: {0}'.format(line[:-1].decode()) # 子线程操作UI改为调度到主线程执行,避免Tkinter线程安全问题 def _add(): tk.Label( self.scolable_frame.get(), text=line_text ).pack(anchor=tk.CENTER, expand=True, fill=tk.X) self.after(0, _add) def stop(self): """停止子进程并释放资源""" self.ostream.stop() self.proc.kill() self.proc.stdout.close() self.proc.wait(timeout=2) info("Process resource released") if __name__ == '__main__': # 单独运行时兼容原有逻辑 root = tk.Tk() root.geometry('300x200+500+300') app = ShowProcessOutputDemo(root) app.pack(fill=tk.BOTH, expand=True) def on_close(): app.stop() root.destroy() root.protocol("WM_DELETE_WINDOW", on_close) root.mainloop()
改动点说明:
- 将
ShowProcessOutputDemo的父类从tk.Tk改为tk.Frame - 移除了原来顶层窗口专属的
geometry、protocol配置 add_label方法增加主线程调度逻辑,避免线程安全问题- 调整
stop方法,移除了退出整个程序的self.quit()调用 - 调整单独运行的入口逻辑,兼容原有使用方式
2. 修改wrapper.py代码
完整修改后的代码如下:
import tkinter as tk import workingcatch from tkinter import * root = tk.Tk() outputframe = LabelFrame(master=root, width=800, height=700) outputframe.pack(side=LEFT, padx=10,pady=10, fill=BOTH, expand=True) # 实例化进程输出组件,挂载到outputframe下 process_output = workingcatch.ShowProcessOutputDemo(master=outputframe) process_output.pack(fill=BOTH, expand=True, padx=10, pady=10) # 主窗口关闭时先释放子进程和线程资源 def on_root_close(): process_output.stop() root.destroy() root.protocol("WM_DELETE_WINDOW", on_root_close) root.geometry('1280x720') root.mainloop()
改动点说明:
- 增加
ShowProcessOutputDemo实例化逻辑,指定父容器为outputframe - 增加主窗口关闭回调,确保进程、线程资源正常释放,避免后台残留进程
修改完成后直接运行wrapper.py即可看到子进程的输出内容正常展示在outputframe中。
内容的提问来源于stack exchange,提问作者Connor Gold
相关产品推荐
相关产品推荐

