You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.15 01:45:07