python线程和tkinter.tix.NoteBook标签在第一个打开的标签之外不可访问

0 投票
1 回答
1667 浏览
提问于 2025-04-16 21:10

在下面这个例子中,我无法在默认打开的日志标签之外写入其他标签:

#!/usr/bin/python
import os, sys
import threading
import time
from itertools import count

import tkinter
from tkinter import *
from tkinter import tix
import tkinter.tix
from tkinter.constants import *
import traceback, tkinter.messagebox
from tkinter import ttk

TCL_DONT_WAIT           = 1<<1
TCL_WINDOW_EVENTS       = 1<<2
TCL_FILE_EVENTS         = 1<<3
TCL_TIMER_EVENTS        = 1<<4
TCL_IDLE_EVENTS         = 1<<5
TCL_ALL_EVENTS          = 0

class GUI(threading.Thread):

    def __init__(self):
        """ thread init
        defines some vars and starts stuff when the class is called (gui=GUI())
        """
        self.root=tkinter.tix.Tk()
        z = self.root.winfo_toplevel()
        z.wm_title('minimal example')
        if z.winfo_screenwidth() <= 800:
            z.geometry('790x590+10+10')
        else:
            z.geometry('890x640+10+10')
        frame1 = self.MkMainNotebook()
        frame2 = self.MkMainStatus()
        frame1.pack(side=TOP, expand=1, fill=BOTH, padx=4, pady=4)
        frame2.pack(side=BOTTOM, fill=X)
        z.wm_protocol("WM_DELETE_WINDOW", lambda self=self: self.stop())
        threading.Thread.__init__(self)

    def run(self):
        """ thread start
        kick starts the main loop when the thread start()
        """
        self.root.mainloop()

    def stop(self):
        """ escape plan
        Exits gui thread
        """
        self._stop()
        raise SystemExit

    def MkMainStatus(self):
        """ status bar
        """
        top = self.root
        w = tkinter.tix.Frame(top, relief=tkinter.tix.RAISED, bd=1)
        self.exitbutton = tkinter.Button(w, text='Exit GUI Thread', width=20, command=lambda self=self: self.stop())
        self.exitbutton.grid(row=0, column=0, sticky=E, padx=3, pady=3)
        return w

    def MkMainNotebook(self):
        """ the tabs frame
        defines the tabs
        """
        top = self.root
        w = tkinter.tix.NoteBook(top, ipadx=5, ipady=5, options="""
        tagPadX 6
        tagPadY 4
        borderWidth 2
        """)
        top['bg'] = w['bg']

        w.add('log', label='Log', underline=0,
              createcmd=lambda w=w, name='log': self.MkLog(w, name))
        w.add('pro', label='Progress', underline=0,
              createcmd=lambda w=w, name='pro': self.MkProgress(w, name))
        w.add('set', label='Settings', underline=0,
              createcmd=lambda w=w, name='set': self.MkSettings(w, name))
        return w

    def MkSettings(self, nb, name):
        """ TODO: settings tab
        """
        w = nb.page(name)
        options="label.width %d label.anchor %s entry.width %d" % (10, tkinter.tix.E, 13)
        settings_scr_win = tix.ScrolledWindow(w, width=400, height=400)
        settings_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)        
        self.SettingsFrame = settings_scr_win.window

    def MkProgress(self, nb, name):
        """ TODO: progress tab
        """
        w = nb.page(name)
        options = "label.padX 4"
        progress_scr_win = tix.ScrolledWindow(w, width=400, height=400)
        progress_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)        
        self.ProgressFrame = progress_scr_win.window

    def MkLog(self, nb, name):
        """ log
        """
        w = nb.page(name)
        options = "label.padX 4"
        log_scr_win = tix.ScrolledWindow(w, width=400, height=400)
        log_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)
        self.LogFrame = log_scr_win.window

def main(argv):
    """ main function
    Keyword arguments:
    args[0] -- 
    args[1] -- 
    Returns: 
    None
    """

    #GUI
    gui=GUI()
    gui.start()
    time.sleep(1) # give it a sec to draw the gui... 

    tix.Label(gui.LogFrame, text=("log")).pack()
    tix.Label(gui.SettingsFrame, text=("settings")).pack()
    tix.Label(gui.ProgressFrame, text=("progress")).pack()

    return None    

if __name__ == "__main__":
    sys.exit(main(sys.argv))

控制台的错误信息:

Traceback (most recent call last):
  File "C:\minimal example.py", line 132, in <module>
    sys.exit(main(sys.argv))
  File "C:\minimal example.py", line 126, in main
    tix.Label(gui.SettingsFrame, text=("settings")).pack()
AttributeError: 'GUI' object has no attribute 'SettingsFrame'

每当我尝试在 gui.SettingsFrame 或 gui.ProgressFrame 中创建小部件时,就会出现这个问题。 如果我在 main() 中把 time.sleep(1) 的时间增加,然后在 tix.Label 部分开始之前点击标签,代码就能正常工作,因为这时标签命令已经被调用了。

那么,我该如何提前声明 gui.SettingsFrame 和 gui.ProgressFrame 呢?我能在到达 main() 中的 tix.Label 之前在代码中切换标签吗?

注意:我需要它是线程安全的,因为这是一个更大的程序的一部分,这个程序做了十几件不同的事情,并且是多线程的,还有几个进程。

谢谢

编辑 1: 我可以给这个类添加方法,而不是直接引用这些框架:

    def print_log(self, text):
        """ log printer
        """
        tix.Label(self.LogFrame, text=(text)).pack()

    def print_progress(self, text):
        """ log printer
        """
        tix.Label(self.ProgressFrame, text=(text)).pack()

    def print_settings(self, text):
        """ log printer
        """
        tix.Label(self.SettingsFrame, text=(text)).pack()

然后在 main 中调用它们:

    #tix.Label(gui.LogFrame, text=("log")).pack()
    gui.print_log("log")
    #tix.Label(gui.SettingsFrame, text=("settings")).pack()
    gui.print_settings("settings")
    #tix.Label(gui.ProgressFrame, text=("progress")).pack()
    gui.print_progress("progress")    

但结果还是一样:

Traceback (most recent call last):
  File "C:\minimal example.py", line 150, in <module>
    sys.exit(main(sys.argv))
  File "C:\minimal example.py", line 143, in main
    gui.print_settings("settings")
  File "C:\minimal example.py", line 124, in print_settings
    tix.Label(self.SettingsFrame, text=(text)).pack()
AttributeError: 'GUI' object has no attribute 'SettingsFrame'

编辑 2:Bryan Oakley 提供了一个非常简单快捷的解决方案:

#!/usr/bin/python
import os, sys
import threading
import time
from itertools import count

import tkinter
from tkinter import *
from tkinter import tix
import tkinter.tix
from tkinter.constants import *
import traceback, tkinter.messagebox
from tkinter import ttk

TCL_DONT_WAIT           = 1<<1
TCL_WINDOW_EVENTS       = 1<<2
TCL_FILE_EVENTS         = 1<<3
TCL_TIMER_EVENTS        = 1<<4
TCL_IDLE_EVENTS         = 1<<5
TCL_ALL_EVENTS          = 0

class GUI(threading.Thread):

    def __init__(self):
        """ thread init
        defines some vars and starts stuff when the class is called (gui=GUI())
        """
        self.root=tkinter.tix.Tk()
        z = self.root.winfo_toplevel()
        z.wm_title('minimal example')
        if z.winfo_screenwidth() <= 800:
            z.geometry('790x590+10+10')
        else:
            z.geometry('890x640+10+10')
        frame1 = self.MkMainNotebook()
        frame2 = self.MkMainStatus()
        frame1.pack(side=TOP, expand=1, fill=BOTH, padx=4, pady=4)
        frame2.pack(side=BOTTOM, fill=X)
        z.wm_protocol("WM_DELETE_WINDOW", lambda self=self: self.stop())
        threading.Thread.__init__(self)

    def run(self):
        """ thread start
        kick starts the main loop when the thread start()
        """
        self.root.mainloop()

    def stop(self):
        """ escape plan
        Exits gui thread
        """
        self._stop()
        raise SystemExit

    def MkMainStatus(self):
        """ status bar
        """
        top = self.root
        w = tkinter.tix.Frame(top, relief=tkinter.tix.RAISED, bd=1)
        self.exitbutton = tkinter.Button(w, text='Exit GUI Thread', width=20, command=lambda self=self: self.stop())
        self.exitbutton.grid(row=0, column=0, sticky=E, padx=3, pady=3)
        return w

    def MkMainNotebook(self):
        """ the tabs frame
        defines the tabs
        """
        top = self.root
        w = tkinter.tix.NoteBook(top, ipadx=5, ipady=5, options="""
        tagPadX 6
        tagPadY 4
        borderWidth 2
        """)
        top['bg'] = w['bg']

        w.add('log', label='Log', underline=0)
              #createcmd=lambda w=w, name='log': self.MkLog(w, name))
        w.add('pro', label='Progress', underline=0)
              #createcmd=lambda w=w, name='pro': self.MkProgress(w, name))
        w.add('set', label='Settings', underline=0)
              #createcmd=lambda w=w, name='set': self.MkSettings(w, name))

        #log_it = w.subwidget('log')
        #pro_it = w.subwidget('pro')
        #set_it = w.subwidget('set')

        self.MkLog(w, 'log')
        self.MkProgress(w, 'pro')
        self.MkSettings(w, 'set')

        return w

    def MkSettings(self, nb, name):
        """ TODO: settings tab
        """
        w = nb.page(name)
        options="label.width %d label.anchor %s entry.width %d" % (10, tkinter.tix.E, 13)
        settings_scr_win = tix.ScrolledWindow(w, width=400, height=400)
        settings_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)        
        self.SettingsFrame = settings_scr_win.window

    def MkProgress(self, nb, name):
        """ TODO: progress tab
        """
        w = nb.page(name)
        options = "label.padX 4"
        progress_scr_win = tix.ScrolledWindow(w, width=400, height=400)
        progress_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)        
        self.ProgressFrame = progress_scr_win.window

    def MkLog(self, nb, name):
        """ log
        """
        w = nb.page(name)
        options = "label.padX 4"
        log_scr_win = tix.ScrolledWindow(w, width=400, height=400)
        log_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)
        self.LogFrame = log_scr_win.window

    def print_log(self, text):
        """ log printer
        """
        tix.Label(self.LogFrame, text=(text)).pack()

    def print_progress(self, text):
        """ log printer
        """
        tix.Label(self.ProgressFrame, text=(text)).pack()

    def print_settings(self, text):
        """ log printer
        """
        tix.Label(self.SettingsFrame, text=(text)).pack()

def main(argv):
    """ main function
    Keyword arguments:
    args[0] -- 
    args[1] -- 
    Returns: 
    None
    """

    #GUI
    gui=GUI()
    gui.start()
    time.sleep(1) # give it a sec to draw the gui... 

    tix.Label(gui.LogFrame, text=("log")).pack()
    #gui.print_log("log")
    tix.Label(gui.SettingsFrame, text=("settings")).pack()
    #gui.print_settings("settings")
    tix.Label(gui.ProgressFrame, text=("progress")).pack()
    #gui.print_progress("progress")    

    return None    

if __name__ == "__main__":
    sys.exit(main(sys.argv))

谢谢!

编辑 3:以下是一个线程安全的实现。我还添加了一个额外的计时器:

#!/usr/bin/python
import os, sys
import threading
import queue
from queue import Empty
import time
from itertools import count

import tkinter
from tkinter import *
from tkinter import tix
import tkinter.tix
from tkinter.constants import *
import traceback, tkinter.messagebox
from tkinter import ttk

TCL_DONT_WAIT           = 1<<1
TCL_WINDOW_EVENTS       = 1<<2
TCL_FILE_EVENTS         = 1<<3
TCL_TIMER_EVENTS        = 1<<4
TCL_IDLE_EVENTS         = 1<<5
TCL_ALL_EVENTS          = 0

class GUI(threading.Thread):

    def __init__(self):
        """ thread init
        defines some vars and starts stuff when the class is called (gui=GUI())
        """
        self.root=tkinter.tix.Tk()
        z = self.root.winfo_toplevel()
        z.wm_title('minimal example')
        if z.winfo_screenwidth() <= 800:
            z.geometry('790x590+10+10')
        else:
            z.geometry('890x640+10+10')
        frame1 = self.MkMainNotebook()
        frame2 = self.MkMainStatus()
        frame1.pack(side=TOP, expand=1, fill=BOTH, padx=4, pady=4)
        frame2.pack(side=BOTTOM, fill=X)
        z.wm_protocol("WM_DELETE_WINDOW", lambda self=self: self.stop())

        threading.Thread.__init__(self)

    def run(self):
        """ thread start
        kick starts the main loop when the thread start()
        """
        self.root.mainloop()

    def stop(self):
        """ escape plan
        Exits gui thread
        """
        self._stop()
        raise SystemExit

    def MkMainStatus(self):
        """ status bar
        """
        top = self.root
        w = tkinter.tix.Frame(top, relief=tkinter.tix.RAISED, bd=1)

        self.status = tkinter.tix.Label(w, anchor=E, bd=1)
        self.exitbutton = tkinter.Button(w, text='Exit GUI Thread', width=20, command=lambda self=self: self.stop())

        self.print_queue=queue.Queue()
        self.print_label()

        self.status.grid(row=0, column=0, sticky=W, padx=3, pady=3)
        self.exitbutton.grid(row=0, column=1, sticky=E, padx=3, pady=3)
        return w

    def print_label(self):
        """ listner
        listner
        """
        rate=0.5 # seconds to re-read queue; 0.5=half a second, 1=a full second
        counter = count(0, rate)
        def update_func():
            secs= str(counter.__next__())
            try:
                self.status.config(text=str("%s(secs): Processing queue..." % (secs.split('.'))[0]), fg=str("red"))
                a = tix.Label(self.LogFrame, text=(self.print_queue.get(False)))
            except Empty:
                self.status.config(text=str("%s(secs): Waiting for queue..." % (secs.split('.'))[0]), fg=str("black"))
                self.status.after(int(rate*1000), update_func)
            else:
                a.pack()
                a.after(int(rate*1000), update_func)
        update_func()

    def MkMainNotebook(self):
        """ the tabs frame
        defines the tabs
        """
        top = self.root
        w = tkinter.tix.NoteBook(top, ipadx=5, ipady=5, options="""
        tagPadX 6
        tagPadY 4
        borderWidth 2
        """)
        top['bg'] = w['bg']

        w.add('log', label='Log', underline=0)
        self.MkLog(w, 'log')
        w.add('pro', label='Progress', underline=0)
        self.MkProgress(w, 'pro')
        w.add('set', label='Settings', underline=0)
        self.MkSettings(w, 'set')
        return w

    def MkSettings(self, nb, name):
        """ TODO: settings tab
        """
        w = nb.page(name)
        options="label.width %d label.anchor %s entry.width %d" % (10, tkinter.tix.E, 13)
        settings_scr_win = tix.ScrolledWindow(w, width=400, height=400)
        settings_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)        
        self.SettingsFrame = settings_scr_win.window

    def MkProgress(self, nb, name):
        """ TODO: progress tab
        """
        w = nb.page(name)
        options = "label.padX 4"
        progress_scr_win = tix.ScrolledWindow(w, width=400, height=400)
        progress_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)        
        self.ProgressFrame = progress_scr_win.window

    def MkLog(self, nb, name):
        """ log
        """
        w = nb.page(name)
        options = "label.padX 4"
        log_scr_win = tix.ScrolledWindow(w, width=400, height=400)
        log_scr_win.pack(side=tkinter.tix.TOP, padx=2, pady=2, fill='both', expand=1)
        self.LogFrame = log_scr_win.window

def main(argv):
    """ main function
    Keyword arguments:
    args[0] -- 
    args[1] -- 
    Returns: 
    None
    """

    #GUI
    gui=GUI()
    gui.start()

    gui.print_queue.put("log")

    time.sleep(10) # timed release test
    gui.print_queue.put("timed release test")

    return None    

if __name__ == "__main__":
    sys.exit(main(sys.argv))

这可能是将线程安全的 tkinter 界面实现到控制台应用程序中最简单的方法。gui.print_queue.put() 替代了 print(),而 gui 更新的速率可以通过调整 print_label(self) 中的 rate 变量来修改。

希望你喜欢!

1 个回答

0

首先,Tkinter 这个库是不支持多线程的。也就是说,你不能从多个线程同时访问 Tkinter 的界面组件。如果这样做,结果是不可预测的(不过通常可以预测到会导致程序崩溃)。

至于另一个问题——如何提前声明 gui.SettingsFrame——我不太确定怎么回答而不说一些显而易见的事情。要“声明”它,你必须先创建它,而要创建它,你需要调用一个创建它的方法。而且你必须在使用 gui.SettingsFrame 作为其他方法的参数之前完成这一步。

你为什么要在创建框架的方法之外创建一个标签呢?我觉得解决这个问题的方法是把“设置”标签的创建从 Main 移到 MkSettings(同样的道理也适用于“进度”和“日志”标签)。

撰写回答