使用 Ctrl-C 退出 tkinter 应用程序并捕获 SIGINT

5

Ctrl-C/SIGTERM/SIGINT似乎被tkinter忽略了。通常可以通过回调重新捕获。但是这似乎没有起作用,所以我想在另一个线程中运行tkinter,因为它的mainloop()是一个无限循环并且会阻塞。我实际上还想在单独的线程中从stdin读取。即使在此之后,直到关闭窗口,Ctrl-C仍然无法处理。这是我的MWE:

#! /usr/bin/env python
import Tkinter as tk
import threading
import signal
import sys

class MyTkApp(threading.Thread):
    def run(self):
        self.root = tk.Tk()
        self.root.mainloop()

app = MyTkApp()
app.start()

def signal_handler(signal, frame):
    sys.stderr.write("Exiting...\n")

    # think only one of these is needed, not sure
    app.root.destroy()
    app.root.quit()

signal.signal(signal.SIGINT, signal_handler)

结果:

  • 运行应用程序
  • 在终端中按Ctrl-C(没有任何反应)
  • 关闭窗口
  • 打印“Exiting...”并出现关于循环已经退出的错误。

这里发生了什么,我该如何使终端中的Ctrl-C关闭应用程序?


更新:添加投票如建议所述,在主线程中运行正常,但在另一个线程中启动无效...

class MyTkApp(threading.Thread):
    def poll(self):
        sys.stderr.write("poll\n")
        self.root.after(50, self.poll)

    def run(self):
        self.root = tk.Tk()
        self.root.after(50, self.poll)
        self.root.mainloop()

请你明确一下:你想在终端中执行Control-C,而不是从图形用户界面中执行吗? - Bryan Oakley
@BryanOakley 是的,在开发过程中经常测试会非常方便。 - jozxyqk
以下链接是否回答了您的问题?https://dev59.com/_WvXa4cB1Zd3GeqPNdpO#13784297 - Bryan Oakley
@BryanOakley 我的应用程序中已经有了这个轮询,并且它不会影响行为。信号回调只有在窗口关闭之前才会运行。 - jozxyqk
你确定轮询正在运行吗?在使用轮询时,您是否也在使用多线程?另外,您在哪个平台上遇到了这个问题?如果您从https://dev59.com/_WvXa4cB1Zd3GeqPNdpO#13784297获取确切的代码并运行它,它是否适用于您? - Bryan Oakley
是的,我有一个类似于stderror的写法,正在运行轮询。但是当我在主线程中运行轮询时,它确实可以工作。Ubuntu 14.04.5,Python 2.7.6。 - jozxyqk
4个回答

6

以下是一个可在Windows或命令行中捕获Control+C的工作示例。此示例已在3.7.2上进行了测试。这似乎比其他解决方案更简单,我几乎感觉自己错过了什么。

import tkinter as TK

import signal

def hello():
    print("Hello")

root = TK.Tk()

TK.Button(root, text="Hi", command=(hello)).pack(  )

def handler(event):
    root.destroy()
    print('caught ^C')

def check():
    root.after(500, check)  #  time in ms.

# the or is a hack just because I've shoving all this in a lambda. setup before calling main loop
signal.signal(signal.SIGINT, lambda x,y : print('terminal ^C') or handler(None))

# this let's the terminal ^C get sampled every so often
root.after(500, check)  #  time in ms.

root.bind_all('<Control-c>', handler)
 
root.mainloop()

这是唯一对我有效的方法。谢谢!你知道为什么需要check方法吗?它如何允许对^C进行采样? - Nathan
1
不用担心,我知道这会导致上下文切换,从而允许处理器捕捉到^C。 - kdubs

4

Python中正确使用CTRL-C和SIGINT

问题在于您正在退出主线程,因此信号处理程序基本上是无用的。您需要保持它运行,在while循环中或者我个人偏好的Events来自threading模块。您也可以捕获由CTRL-C事件生成的KeyboardInterrupt异常,而不是处理信号处理程序。

Tkinter中的SIGINT

使用tkinter,您必须将tkinter应用程序运行在单独的线程中,以便它不会干扰信号处理程序或KeyboardInterrupt异常。在处理程序中,要退出,您需要销毁并更新tkinter根。更新允许tkinter更新以关闭,而不必等待mainloop。否则,用户必须单击活动窗口以激活mainloop。

# Python 3
from tkinter import *
from threading import Thread
import signal

class MyTkApp(Thread):
    def run(self):
        self.root = Tk()
        self.root.mainloop()

def sigint_handler(sig, frame):
    app.root.quit()
    app.root.update()

app = MyTkApp()

# Set signal before starting
signal.signal(signal.SIGINT, sigint_handler)

app.start()

注意:如果您在与 tkinter 主循环相同的线程中设置处理程序,则也可以捕获 SIGINTs,但是您需要在信号后使 tkinter 窗口处于活动状态,以便其主循环可以运行。除非您在新线程中运行,否则没有其他方法来解决这个问题。
有关 Tkinter 和命令行通信的更多信息,请参见 不使用主循环使用 Tkinter。基本上,您可以在循环中使用 update 方法,然后与其他线程和进程通信等。我个人不建议这样做,因为您实际上正在执行 python 线程控制系统的工作,这可能与您想要做的相反。(python 有一个进程在一个外部线程中运行所有内部线程,因此除非使用 multiprocessing 模块,否则您无法利用多线程)
# Python 2
from Tkinter import *

ROOT = Tk()
LABEL = Label(ROOT, text="Hello, world!")
LABEL.pack()
LOOP_ACTIVE = True
while LOOP_ACTIVE:
    ROOT.update()
    USER_INPUT = raw_input("Give me your command! Just type \"exit\" to close: ")
    if USER_INPUT == "exit":
        ROOT.quit()
        LOOP_ACTIVE = False
    else:
        LABEL = Label(ROOT, text=USER_INPUT)
        LABEL.pack()

谢谢@gagarwal。为了避免“仅链接答案”,您能否复制/粘贴来自链接的关键片段或其他内容,以防页面崩溃或移动。 - jozxyqk
我根据自己的经验进行了更新。抱歉,由于我在手机上,现在无法发布代码。如果您有任何问题,请联系我。 - gagarwa

4

由于您的tkinter应用程序正在另一个线程中运行,因此您不需要在主线程中设置信号处理程序,只需在app.start()语句之后使用以下代码块即可:

import time

while app.is_alive():
    try:
        time.sleep(0.5)
    except KeyboardInterrupt:
        app.root.destroy()
        break

您可以使用Ctrl-C来触发KeyboardInterrupt异常,关闭tkinter应用程序并打破while循环。如果关闭tkinter应用程序,则while循环也将终止。

请注意,上述代码仅适用于Python 2(因为您在代码中使用了Tkinter)。


1
谢谢!这确实关闭了窗口,但仍有一些程序在运行,进程拒绝关闭。您能否解释一下为什么信号处理程序不起作用?我想了解为什么它如此困难。 - jozxyqk
1
信号处理程序不起作用,因为它只在主线程中工作。但是,在您的代码中,主线程在signal.signal(...)语句之后就已经结束了。您可以尝试在signal.signal(...)语句之后添加while True: pass,使您的主线程保持活动状态,然后信号处理程序将会起作用。 - acw1668

0

简洁版:

import tkinter as tk
import signal

tk_root = tk.Tk()

signal.signal(signal.SIGINT, lambda x, y: tk_root.destroy())
tk_check = lambda: tk_root.after(500, tk_check)
tk_root.after(500, tk_check)
tk_root.bind_all("<Control-c>", lambda e: tk_root.destroy())

tk_root.mainloop()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接