5

Ctrl-C/SIGTERM/SIGINT seem to be ignored by tkinter. Normally it can be captured again with a callback. This doesn't seem to be working, so I thought I'd run tkinter in another thread since its mainloop() is an infinite loop and blocks. I actually also want to do this to read from stdin in a separate thread. Even after this, Ctrl-C is still not processed until I close the window. Here's my 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)

Results:

  • Run the app
  • Ctrl-C in the terminal (nothing happens)
  • Close the window
  • "Exiting..." is printed and I get an error about the loop already having exited.

What's going on here and how can I make Ctrl-C from the terminal close the app?


Update: Adding a poll, as suggested, works in the main thread but does not help when started in another thread...

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()
Community
  • 1
  • 1
jozxyqk
  • 16,424
  • 12
  • 91
  • 180
  • Just to be clear: you want to do control-c from the terminal and not from the GUI itself, correct? – Bryan Oakley Oct 03 '16 at 22:04
  • @BryanOakley yes, it would be very convenient for frequently testing during development. – jozxyqk Oct 03 '16 at 22:07
  • Does the following link answer your question? http://stackoverflow.com/a/13784297/7432 – Bryan Oakley Oct 03 '16 at 22:10
  • @BryanOakley I have this poll in my application already and it does not affect the behavior. The signal callback simply isn't run until window closes. – jozxyqk Oct 03 '16 at 22:12
  • Are you certain the poll is running? When using the poll, are you also using multithreading? Also, what platform are you experiencing this on? If you take the exact code from http://stackoverflow.com/a/13784297/7432 and run it, does it work for you? – Bryan Oakley Oct 03 '16 at 22:13
  • Yes, I have a similar write to stderror, the poll is running. When I run the poll in the main thread it does work though. Ubuntu 14.04.5, Python 2.7.6. – jozxyqk Oct 03 '16 at 22:23

4 Answers4

6

here's a working example that catches control c in the windows or from the command line. this was tested with 3.7.2 this seems simpler than the other solutions. I almost feel like I'm missing something.

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()
kdubs
  • 1,596
  • 1
  • 21
  • 36
  • This was the only one that worked for me. Thank you! Do you know exactly why the `check` method is needed? How does that allow ^C to get sampled? – Nathan Jul 27 '22 at 18:27
  • 1
    exactly no. I know that it cause a "context switch" that allows the ^C to be caught by the handler. – kdubs Jul 27 '22 at 20:05
4

Since your tkinter app is running in another thread, you do not need to set up the signal handler in the main thread and just use the following code block after the app.start() statement:

import time

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

You can then use Ctrl-C to raise the KeyboardInterrupt exception to close the tkinter app and break the while loop. The while loop will also be terminated if you close your tkinter app.

Note that the above code is working only in Python 2 (as you use Tkinter in your code).

Keshan Nageswaran
  • 8,060
  • 3
  • 28
  • 45
acw1668
  • 40,144
  • 5
  • 22
  • 34
  • 1
    Thanks! This does indeed close the window, but something is still running and the process refuses to close. Would you also be able to say why the signal handler doesn't work? I'd like to understand why it's being so difficult. – jozxyqk Oct 04 '16 at 19:04
  • 1
    The signal handler does not work because it only works in main thread. But the main thread in your code is finished after the `signal.signal(...)` statement. You can try adding `while True: pass` after the `signal.signal(...)` statement which keep your main thread alive, and then the signal handler will be working. – acw1668 Oct 05 '16 at 00:33
4

Proper CTRL-C & SIGINT Usage in Python

The problem is that you are exiting the main thread, so the signal handler is basically useless. You need to keep it running, in a while loop, or my personal preference, Events from threading module. You can also just catch the KeyboardInterrupt exception generated by the CTRL-C event, rather than dealing with signal handlers.

SIGINT in Tkinter

Using tkinter, you must have the tkinter app run in a separate thread, so that it doesn't interfere with the signal handler or KeyboardInterrupt exception. In the handler, to exit, you need to destroy then update tkinter root. Update allows the tkinter to update so that it closes, without waiting for mainloop. Otherwise, user has to click on the active window to activate 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()

Note: SIGINTs can also be caught if you set handler in same thread as tkinter mainloop, but you need to make tkinter window active after the signal so that it's mainloop can run. There is no way around this unless you run in new thread.

More Information on Tkinter & Command Line Communication

For more on communicating between tkinter and the command line, see Using Tkinter Without Mainloop. Basically, you can use update method in your loop, and then communicate with other threads and processes, etc. I would personally NOT recommend this, as you are essentially doing the job of the python thread control system, which is probably opposite of what you want to do. (python has a process that runs all internal threads in one external thread, so you are not taking advantage of multitheading, unless using multiprocessing module)

# 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()
adambro
  • 310
  • 4
  • 16
gagarwa
  • 1,426
  • 1
  • 15
  • 28
  • Thanks @gagarwal. To avoid "link-only-answers" would you be able to copy/paste a key snippet or something from the link just in case the page goes down or moves. – jozxyqk Jul 25 '18 at 22:58
  • I updated based on my experience with this. Sorry can't post code now, as on mobile. If you have any more questions, please reach out. – gagarwa Jul 27 '18 at 05:22
0

Compact version:

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()
Combinacijus
  • 405
  • 2
  • 8