【问题标题】:Disable Exit (or [ X ]) in tkinter Window在 tkinter 窗口中禁用退出(或 [X])
【发布时间】:2018-01-10 01:13:57
【问题描述】:

我发布这个是因为我自己一直在努力寻找这个问题的明确答案。 . .

在尝试为我的程序创建进度条时,我发现使用 tkinter 很难做到。要在不遇到可怕的“主循环”的情况下完成创建进度条,I opted to make a class out of the progress bar using threads。通过大量的尝试一个错误,我发现由于使用了多线程(tkinter喜欢在主线程中),可以定制的东西并不多。以下是我尝试过的两种选择,其次是最适合我需要的第三种:

选项 1:使用回调函数

给定以下代码:

import tkinter as tk
import tkinter.ttk as ttk
import threading


class ProgressbarApp(threading.Thread):

    def __init__(self, max_value: int):
        self.max_value = max_value

        self.root = None
        self.pb = None

        threading.Thread.__init__(self)
        self.lock = threading.Lock()    # (1)
        self.lock.acquire()             # (2)
        self.start()

        # (1) Makes sure progressbar is fully loaded before executing anything
        with self.lock:
            return

    def close(self):
        self.root.quit()

    def run(self):

        self.root = tk.Tk()
        self.root.protocol("WM_DELETE_WINDOW", self.__callback)

        self.pb = ttk.Progressbar(self.root, orient='horizontal', length=400, mode='determinate')
        self.pb['value'] = 0
        self.pb['maximum'] = self.max_value
        self.pb.pack()

        self.lock.release()             # (2) Will release lock when finished
        self.root.mainloop()

    def update(self, value: int):
        self.pb['value'] = value

    @staticmethod
    def __callback():
        return

if __name__ == '__main__':
    interval = 100000
    my_pb = ProgressbarApp(interval)

    for i in range(interval):
        my_pb.update(i)

    my_pb.close()

    # Other stuff goes on . . .

在哪里

self.root.protocol("WM_DELETE_WINDOW", self.__callback)

防止窗口被关闭。但是,如果要按住 Exit 或 [ X ] 按钮,进度条将冻结,直到用户释放按钮。 (__callback 函数不断被调用,阻止其他任务完成)。

选项 2:使用 root.overriderdirect(True)

给定以下代码:

import tkinter as tk
import tkinter.ttk as ttk
import threading


class ProgressbarApp(threading.Thread):

    def __init__(self, max_value: int):
        self.max_value = max_value

        self.root = None
        self.pb = None

        threading.Thread.__init__(self)
        self.lock = threading.Lock()    # (1)
        self.lock.acquire()             # (2)
        self.start()

        # (1) Makes sure progressbar is fully loaded before executing anything
        with self.lock:
            return

    def close(self):
        self.root.quit()

    def run(self):

        self.root = tk.Tk()
        self.root.overrideredirect(True)

        self.pb = ttk.Progressbar(self.root, orient='horizontal', length=400, mode='determinate')
        self.pb['value'] = 0
        self.pb['maximum'] = self.max_value
        self.pb.pack()

        self.lock.release()             # (2) Will release lock when finished
        self.root.mainloop()

    def update(self, value: int):
        self.pb['value'] = value

if __name__ == '__main__':
    interval = 100000
    my_pb = ProgressbarApp(interval)

    for i in range(interval):
        my_pb.update(i)

    my_pb.close()

    # Other stuff goes on . . .

在哪里

self.root.overrideredirect(True)

清除所有 tkinter 窗口选项。但是,进度条不仅位于奇怪的位置,而且还遮挡了用户窗口。进度条应该是用户友好的。

选项 3:使用 root.attributes('-disabled', True)

给定以下代码:

import tkinter as tk
import tkinter.ttk as ttk
import threading


class ProgressbarApp(threading.Thread):

    def __init__(self, max_value: int):
        self.max_value = max_value

        self.root = None
        self.pb = None

        threading.Thread.__init__(self)
        self.lock = threading.Lock()    # (1)
        self.lock.acquire()             # (2)
        self.start()

        # (1) Makes sure progressbar is fully loaded before executing anything
        with self.lock:
            return

    def close(self):
        self.root.quit()

    def run(self):

        self.root = tk.Tk()
        self.root.attributes('-disabled', True)

        self.pb = ttk.Progressbar(self.root, orient='horizontal', length=400, mode='determinate')
        self.pb['value'] = 0
        self.pb['maximum'] = self.max_value
        self.pb.pack()

        self.lock.release()             # (2) Will release lock when finished
        self.root.mainloop()

    def update(self, value: int):
        self.pb['value'] = value

if __name__ == '__main__':
    interval = 100000
    my_pb = ProgressbarApp(interval)

    for i in range(interval):
        my_pb.update(i)

    my_pb.close()

    # Other stuff goes on . . .

在哪里

self.root.attributes('-disabled', True)

阻止任何用户与窗口的交互。这最适合我对该程序的需求,因为它可以防止窗口关闭并且仍然具有漂亮的外观。 (我唯一的小问题是用户不能再最小化进度条或移动它)。

如果有任何更好的解决方案,我很乐意看到它们。希望这对某人有所帮助。

【问题讨论】:

标签: python python-3.x tkinter progress-bar exit


【解决方案1】:

您可以创建一个只使用pass 什么都不做的函数。

请看下面:

import tkinter as tk


root=tk.Tk()

def close_program():
    root.destroy()

def disable_event():
    pass

btn = tk.Button(root, text = "Click me to close", command = close_program)
btn.pack()

root.protocol("WM_DELETE_WINDOW", disable_event)

root.mainloop()

您还可以将工具栏连同root.overrideredirect(True) 一起删除,这将阻止用户使用任何工具栏。离开root.protocol("WM_DELETE_WINDOW", disable_event) 也会阻止使用ALT + F4

import tkinter as tk


root=tk.Tk()
root.geometry("400x400")
root.overrideredirect(True)

def close_program():
    root.destroy()

def disable_event():
    pass

btn = tk.Button(root, text = "Click me to close", command = close_program)
btn.pack()

root.protocol("WM_DELETE_WINDOW", disable_event)

root.mainloop()

【讨论】:

  • 我不喜欢使用 root.overrideredirect(True) 因为执行时窗口卡在屏幕的左上角。但是,我会尝试在 root.protocol("WM_DELETE_WINDOW", disable_event) 中“通过”。我之前使用的是 return。
  • @JoshuaVanDeren:您可以使用overrideredirect(True),然后您可以创建自己的自定义工具栏。它有点工作,但非常可定制。
  • 编辑:使用 pass 仍然不能解决问题。当用户按住 [ X ] @Sierra Mountain Tech 时,进度条仍处于冻结状态,您确定在进度条处于线程中时可以吗?我已经多次遇到需要在主线程中出现的问题。
  • @JoshuaVanDeren:如果你能创建一个Minimal, Complete, and Verifiable example,然后我可以进行测试,我可以提出一个可行的解决方案。
  • 当然可以。如果我有时间,我会制作一个这样的例子。
【解决方案2】:

在 Windows 上实现此目的的另一种方法:

#!python3

import tkinter as tk
from tkinter import ttk
import threading, time

import tkinter as tk
from ctypes import windll, wintypes

GWL_STYLE = -16
WS_CHILD = 0x40000000
WS_SYSMENU = 0x00080000

SWP_FRAMECHANGED = 0x0020
SWP_NOACTIVATE = 0x0010
SWP_NOMOVE = 0x0002
SWP_NOSIZE = 0x0001

# write short names for functions and specify argument and return types
GetWindowLong = windll.user32.GetWindowLongW
GetWindowLong.restype = wintypes.ULONG
GetWindowLong.argtpes = (wintypes.HWND, wintypes.INT)

SetWindowLong = windll.user32.SetWindowLongW
SetWindowLong.restype = wintypes.ULONG
SetWindowLong.argtpes = (wintypes.HWND, wintypes.INT, wintypes.ULONG)

SetWindowPos = windll.user32.SetWindowPos

class App(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.pb = ttk.Progressbar(self, orient="horizontal", length=400, mode="determinate", maximum=100)
        self.pb.pack()
        tk.Button(self, text="Remove buttons", command=self.remove_buttons).pack()
        tk.Button(self, text="Add buttons", command=self.add_buttons).pack()


    def start(self):
        self.t = threading.Thread(target=self.loop)
        self.t.start()

    def loop(self):
        while True:
            for num in range(0, 100):
                self.pb['value']=num
                time.sleep(0.1)

    def _get_hwnd(self):
        w_id = self.winfo_id() # gets handle
        style = GetWindowLong(w_id, GWL_STYLE) # get existing style
        newstyle = style & ~WS_CHILD # remove child style
        res = SetWindowLong(w_id, GWL_STYLE, newstyle) # set new style
        res = SetWindowPos(w_id, 0, 0,0,0,0, SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE)
        hwnd = int(self.wm_frame(), 16) # find handle of parent
        res = SetWindowLong(w_id, GWL_STYLE, style) # set back to old style
        res = SetWindowPos(w_id, 0, 0,0,0,0, SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE)
        return hwnd # return parents handle

    def remove_buttons(self):
        hwnd = self._get_hwnd()
        style = GetWindowLong(hwnd, GWL_STYLE) # get existing style
        style = style & ~WS_SYSMENU
        res = SetWindowLong(hwnd, GWL_STYLE, style)
        res = SetWindowPos(hwnd, 0, 0,0,0,0, SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE)

    def add_buttons(self):
        hwnd = self._get_hwnd()
        style = GetWindowLong(hwnd, GWL_STYLE) # get existing style
        style = style | WS_SYSMENU
        res = SetWindowLong(hwnd, GWL_STYLE, style)
        res = SetWindowPos(hwnd, 0, 0,0,0,0, SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE)

if __name__ == "__main__":
    app = App()
    app.start()
    app.mainloop()

【讨论】:

  • 看起来很复杂,但似乎可行!
  • 它看起来很复杂,因为 tkinter 不能很好地使用 windows api 来获取父窗口的句柄,解决这个问题的方法是在 _get_hwnd 函数中,以及添加或删除按钮函数按原样获取窗口样式,然后添加删除标题栏中所有按钮所需的样式,因为它修改了现有样式,这应该适用于大多数窗口样式
  • @JamesKent 你为什么不直接使用hwnd = GetParent(super().winfo_id())?看起来它获得了窗口句柄。取自here
猜你喜欢
  • 1970-01-01
  • 2018-07-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多