ฉันจะจัดการกับเหตุการณ์ปิดหน้าต่างใน Tkinter ได้อย่างไร


133

ฉันจะจัดการกับเหตุการณ์ปิดหน้าต่างได้อย่างไร (ผู้ใช้คลิกปุ่ม 'X') ในโปรแกรม Python Tkinter

คำตอบ:


178

Tkinter สนับสนุนกลไกที่เรียกว่าจัดการโปรโตคอล ในที่นี้คำว่าโปรโตคอลหมายถึงการโต้ตอบระหว่างแอปพลิเคชันและตัวจัดการหน้าต่าง โปรโตคอลที่ใช้บ่อยที่สุดเรียกว่าWM_DELETE_WINDOWและใช้เพื่อกำหนดสิ่งที่เกิดขึ้นเมื่อผู้ใช้ปิดหน้าต่างอย่างชัดเจนโดยใช้ตัวจัดการหน้าต่าง

คุณสามารถใช้protocolวิธีการติดตั้งตัวจัดการสำหรับโปรโตคอลนี้ (วิดเจ็ตต้องเป็นTkหรือToplevelวิดเจ็ต):

คุณมีตัวอย่างที่เป็นรูปธรรม:

import tkinter as tk
from tkinter import messagebox

root = tk.Tk()

def on_closing():
    if messagebox.askokcancel("Quit", "Do you want to quit?"):
        root.destroy()

root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()

2
หากคุณกำลังใช้บางอย่างเช่น Twisted ที่รักษาการวนซ้ำของเหตุการณ์อย่างอิสระหรือ Tkinter (เช่น: วัตถุเครื่องปฏิกรณ์ที่บิดเบี้ยว) ตรวจสอบให้แน่ใจว่าวงรอบหลักด้านนอกหยุดลงด้วยสิ่งที่มีค่าสมิเนติกส์ที่จัดเตรียมไว้สำหรับวัตถุประสงค์นั้น (เช่น reactor.stop () สำหรับการบิด)
Brian Jack

4
ใน Python 2.7 บน Windows Tkinterไม่มีกล่องข้อความโมดูลย่อย ฉันใช้import tkMessageBox as messagebox
IronManMark20

ฉันคิดว่าคุณควรแจ้งให้ทราบว่าคุณคัดลอกคำตอบและรหัสนี้มาจากใครบางคน / ที่อื่น
Christian Dean

1
ฉันไม่รู้ว่านั่นไม่ใช่รหัสที่ฉันโพสต์ไว้ตอนแรก
Matt Gregory

ไม่ได้ผลสำหรับฉัน มันไม่ได้เปลี่ยนปฏิกิริยาที่วุ่นวายของ Python แบบคลาสสิกต่อการหยุดชะงักของกราฟิกเมื่อมีการปิดหน้าต่างอย่างหนัก (เช่นด้วย Alt + F4)
Apostolos

29

Matt ได้แสดงการปรับเปลี่ยนปุ่มปิดแบบคลาสสิกอย่างหนึ่ง
อีกปุ่มหนึ่งคือการมีปุ่มปิดเพื่อย่อหน้าต่าง
คุณสามารถสร้างพฤติกรรมนี้ซ้ำได้โดยกำหนดให้เมธอดiconify
เป็นอาร์กิวเมนต์ที่สองของโปรโตคอลเมธอด

นี่คือตัวอย่างการทำงานที่ทดสอบบน Windows 7 และ 10:

# Python 3
import tkinter
import tkinter.scrolledtext as scrolledtext

root = tkinter.Tk()
# make the top right close button minimize (iconify) the main window
root.protocol("WM_DELETE_WINDOW", root.iconify)
# make Esc exit the program
root.bind('<Escape>', lambda e: root.destroy())

# create a menu bar with an Exit command
menubar = tkinter.Menu(root)
filemenu = tkinter.Menu(menubar, tearoff=0)
filemenu.add_command(label="Exit", command=root.destroy)
menubar.add_cascade(label="File", menu=filemenu)
root.config(menu=menubar)

# create a Text widget with a Scrollbar attached
txt = scrolledtext.ScrolledText(root, undo=True)
txt['font'] = ('consolas', '12')
txt.pack(expand=True, fill='both')

root.mainloop()

ในตัวอย่างนี้เราให้ตัวเลือกการออกใหม่แก่ผู้ใช้สองตัวเลือก:
ไฟล์คลาสสิก→ออกและEscปุ่ม


14

ขึ้นอยู่กับกิจกรรม Tkinter และโดยเฉพาะอย่างยิ่งเมื่อใช้ Tkinter หลังจากนั้นการหยุดกิจกรรมนี้ด้วยdestroy()- แม้จะใช้โปรโตคอล () ปุ่ม ฯลฯ - จะรบกวนกิจกรรมนี้ (ข้อผิดพลาด "ขณะดำเนินการ") แทนที่จะยุติเพียงอย่างเดียว . ทางออกที่ดีที่สุดในเกือบทุกกรณีคือการใช้ธง นี่คือตัวอย่างวิธีการใช้งานที่เรียบง่ายและไร้สาระ (แม้ว่าฉันมั่นใจว่าพวกคุณส่วนใหญ่ไม่ต้องการมัน! :)

from Tkinter import *

def close_window():
  global running
  running = False  # turn off while loop
  print( "Window closed")

root = Tk()
root.protocol("WM_DELETE_WINDOW", close_window)
cv = Canvas(root, width=200, height=200)
cv.pack()

running = True;
# This is an endless loop stopped only by setting 'running' to 'False'
while running: 
  for i in range(200): 
    if not running: 
        break
    cv.create_oval(i, i, i+1, i+1)
    root.update() 

สิ่งนี้จะยุติกิจกรรมกราฟิกได้อย่างดี คุณต้องตรวจสอบrunningในสถานที่ที่ถูกต้องเท่านั้น


4

ฉันขอขอบคุณคำตอบของ Apostolos ที่แจ้งให้ฉันทราบ นี่คือตัวอย่างรายละเอียดเพิ่มเติมสำหรับ Python 3 ในปี 2019 พร้อมคำอธิบายและโค้ดตัวอย่างที่ชัดเจนยิ่งขึ้น


ระวังความจริงที่ว่าdestroy()(หรือไม่มีตัวจัดการการปิดหน้าต่างแบบกำหนดเองเลย) จะทำลายหน้าต่างและการเรียกกลับที่ทำงานอยู่ทั้งหมดทันทีเมื่อผู้ใช้ปิดหน้าต่าง

สิ่งนี้อาจไม่ดีสำหรับคุณขึ้นอยู่กับกิจกรรม Tkinter ในปัจจุบันของคุณและโดยเฉพาะอย่างยิ่งเมื่อใช้tkinter.after(การโทรกลับเป็นระยะ) คุณอาจใช้การโทรกลับซึ่งประมวลผลข้อมูลบางส่วนและเขียนลงดิสก์ ... ในกรณีนี้คุณต้องการให้การเขียนข้อมูลเสร็จสิ้นโดยไม่ถูกฆ่าอย่างกะทันหัน

ทางออกที่ดีที่สุดคือการใช้ธง ดังนั้นเมื่อผู้ใช้ร้องขอการปิดหน้าต่างให้คุณทำเครื่องหมายว่าเป็นแฟล็กจากนั้นตอบสนอง

(หมายเหตุ: ปกติแล้วฉันจะออกแบบ GUI ให้เป็นคลาสที่ห่อหุ้มอย่างดีและเธรดของผู้ปฏิบัติงานแยกกันและฉันไม่ได้ใช้ "global" (ฉันใช้ตัวแปรอินสแตนซ์คลาสแทน) แต่นี่เป็นตัวอย่างที่เรียบง่ายและถูกถอดออกเพื่อแสดงให้เห็น Tk ฆ่าการโทรกลับเป็นระยะ ๆ ของคุณได้อย่างไรเมื่อผู้ใช้ปิดหน้าต่าง ... )

from tkinter import *
import time

# Try setting this to False and look at the printed numbers (1 to 10)
# during the work-loop, if you close the window while the periodic_call
# worker is busy working (printing). It will abruptly end the numbers,
# and kill the periodic callback! That's why you should design most
# applications with a safe closing callback as described in this demo.
safe_closing = True

# ---------

busy_processing = False
close_requested = False

def close_window():
    global close_requested
    close_requested = True
    print("User requested close at:", time.time(), "Was busy processing:", busy_processing)

root = Tk()
if safe_closing:
    root.protocol("WM_DELETE_WINDOW", close_window)
lbl = Label(root)
lbl.pack()

def periodic_call():
    global busy_processing

    if not close_requested:
        busy_processing = True
        for i in range(10):
            print((i+1), "of 10")
            time.sleep(0.2)
            lbl["text"] = str(time.time()) # Will error if force-closed.
            root.update() # Force redrawing since we change label multiple times in a row.
        busy_processing = False
        root.after(500, periodic_call)
    else:
        print("Destroying GUI at:", time.time())
        try: # "destroy()" can throw, so you should wrap it like this.
            root.destroy()
        except:
            # NOTE: In most code, you'll wanna force a close here via
            # "exit" if the window failed to destroy. Just ensure that
            # you have no code after your `mainloop()` call (at the
            # bottom of this file), since the exit call will cause the
            # process to terminate immediately without running any more
            # code. Of course, you should NEVER have code after your
            # `mainloop()` call in well-designed code anyway...
            # exit(0)
            pass

root.after_idle(periodic_call)
root.mainloop()

รหัสนี้จะแสดงให้คุณเห็นว่าWM_DELETE_WINDOWตัวจัดการทำงานแม้ในขณะที่ประเพณีของเราperiodic_call()กำลังยุ่งอยู่ระหว่างทำงาน / วนซ้ำ!

เราใช้.after()ค่าที่ค่อนข้างเกินจริง: 500 มิลลิวินาที นี่เป็นเพียงเพื่อให้คุณเห็นความแตกต่างระหว่างการปิดในขณะที่สายไม่ว่างหรือไม่ ... หากคุณปิดในขณะที่หมายเลขกำลังอัปเดตคุณจะเห็นว่าสิ่งที่WM_DELETE_WINDOWเกิดขึ้นในขณะที่คุณโทรเป็นระยะ "คือ ไม่ว่างประมวลผล: จริง " หากคุณปิดในขณะที่หมายเลขหยุดชั่วคราว (หมายความว่าการโทรกลับเป็นระยะไม่ประมวลผลในขณะนั้น) คุณจะเห็นว่าการปิดเกิดขึ้นในขณะที่ "ไม่ว่าง"

ในการใช้งานจริงคุณ.after()จะใช้บางอย่างเช่น 30-100 มิลลิวินาทีเพื่อให้มี GUI ที่ตอบสนอง นี่เป็นเพียงการสาธิตเพื่อช่วยให้คุณเข้าใจวิธีป้องกันตัวเองจากพฤติกรรม "ขัดจังหวะงานทั้งหมดทันทีเมื่อปิด" ของ Tk

โดยสรุป: กำหนดให้WM_DELETE_WINDOWตัวจัดการตั้งค่าสถานะจากนั้นตรวจสอบค่าสถานะนั้นเป็นระยะ ๆ และ.destroy()หน้าต่างด้วยตนเองเมื่อปลอดภัย (เมื่อแอปของคุณทำงานทั้งหมดเสร็จแล้ว)

PS: คุณสามารถใช้WM_DELETE_WINDOWเพื่อถามผู้ใช้ว่าพวกเขาต้องการปิดหน้าต่างจริงๆหรือไม่ และถ้าเขาตอบว่าไม่คุณก็ไม่ต้องตั้งค่าสถานะ มันง่ายมาก คุณเพียงแค่แสดงกล่องข้อความในของคุณWM_DELETE_WINDOWและตั้งค่าสถานะตามคำตอบของผู้ใช้


1

ลองรุ่นง่าย ๆ :

import tkinter

window = Tk()

closebutton = Button(window, text='X', command=window.destroy)
closebutton.pack()

window.mainloop()

หรือหากคุณต้องการเพิ่มคำสั่งเพิ่มเติม:

import tkinter

window = Tk()


def close():
    window.destroy()
    #More Functions


closebutton = Button(window, text='X', command=close)
closebutton.pack()

window.mainloop()

คำถามเกี่ยวกับปุ่ม X ของ OS สำหรับปิดหน้าต่างไม่ใช่ปุ่มควบคุมปกติ
user1318499

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.