คุณรันโค้ดของคุณเองควบคู่ไปกับเหตุการณ์วนซ้ำของ Tkinter ได้อย่างไร?


119

น้องชายของฉันเพิ่งเริ่มเขียนโปรแกรมและสำหรับโครงการ Science Fair ของเขาเขากำลังจำลองฝูงนกบนท้องฟ้า เขาอากาศที่สุดของรหัสของเขาเขียนและการทำงานเป็นอย่างดี แต่นกจำเป็นต้องย้ายทุกช่วงเวลา

อย่างไรก็ตาม Tkinter ใช้เวลาในการวนรอบเหตุการณ์ของตัวเองดังนั้นรหัสของเขาจะไม่ทำงาน การทำเช่นroot.mainloop()วิ่งวิ่งและช่วยให้ทำงานและสิ่งเดียวที่มันจะทำงานเป็นตัวจัดการเหตุการณ์

มีวิธีที่จะให้โค้ดของเขาทำงานควบคู่ไปกับ mainloop หรือไม่ (โดยไม่ต้องใช้มัลติเธรดทำให้สับสนและควรทำให้ง่าย) และถ้าเป็นเช่นนั้นมันคืออะไร?

ตอนนี้เขามาพร้อมกับแฮ็คที่น่าเกลียดโดยผูกmove()ฟังก์ชั่นของเขาไว้<b1-motion>ดังนั้นตราบใดที่เขากดปุ่มค้างไว้และกระดิกเมาส์มันก็ใช้งานได้ แต่มันต้องมีวิธีที่ดีกว่านี้

คำตอบ:


141

ใช้afterวิธีการกับTkวัตถุ:

from tkinter import *

root = Tk()

def task():
    print("hello")
    root.after(2000, task)  # reschedule event in 2 seconds

root.after(2000, task)
root.mainloop()

นี่คือคำประกาศและเอกสารสำหรับafterวิธีการ:

def after(self, ms, func=None, *args):
    """Call function once after given time.

    MS specifies the time in milliseconds. FUNC gives the
    function which shall be called. Additional parameters
    are given as parameters to the function call.  Return
    identifier to cancel scheduling with after_cancel."""

30
หากคุณระบุการหมดเวลาเป็น 0 งานจะทำให้ตัวเองกลับมาอยู่ในลูปเหตุการณ์ทันทีหลังจากเสร็จสิ้น สิ่งนี้จะไม่ปิดกั้นเหตุการณ์อื่น ๆ ในขณะที่ยังคงเรียกใช้โค้ดของคุณให้บ่อยที่สุด
นาธาน

หลังจากดึงผมออกมาหลายชั่วโมงโดยพยายามดึง opencv และ tkinter ให้ทำงานร่วมกันอย่างถูกต้องและปิดสนิทเมื่อคลิกปุ่ม [X] สิ่งนี้พร้อมกับ win32gui.FindWindow (ไม่มี 'ชื่อหน้าต่าง') ทำเคล็ดลับ! ฉันช่างเป็น noob ;-)
JxAxMxIxN

นี่ไม่ใช่ตัวเลือกที่ดีที่สุด แม้ว่าจะใช้งานได้ในกรณีนี้ แต่ก็ไม่ดีสำหรับสคริปต์ส่วนใหญ่ (ทำงานทุกๆ 2 วินาทีเท่านั้น) และตั้งค่าการหมดเวลาเป็น 0 ตามคำแนะนำที่โพสต์โดย @Nathan เนื่องจากจะทำงานเฉพาะเมื่อ tkinter ไม่ว่าง (ซึ่งสามารถทำได้ ทำให้เกิดปัญหาในโปรแกรมที่ซับซ้อนบางโปรแกรม) ติดthreadingโมดูลได้ดีที่สุด
ไม่ระบุชื่อ

59

ทางออกที่โพสต์โดย Bjornผลใน "Runtimeerror: โทร Tcl จากพาร์ทเมนต์ที่แตกต่างกัน" ข้อความในคอมพิวเตอร์ของฉัน (RedHat องค์กร 5 หลาม 2.6.1) Bjorn อาจไม่ได้รับข้อความนี้เนื่องจากตามที่ฉันตรวจสอบที่เดียวการจัดการเธรดที่ไม่ถูกต้องกับ Tkinter นั้นไม่สามารถคาดเดาได้และขึ้นอยู่กับแพลตฟอร์ม

ปัญหาดูเหมือนจะapp.start()นับเป็นการอ้างอิงถึง Tk เนื่องจากแอปมีองค์ประกอบ Tk ฉันคงนี้โดยการแทนที่app.start()ด้วยภายในself.start() __init__ฉันยังสร้างมันขึ้นมาเพื่อให้การอ้างอิง Tk ทั้งหมดอยู่ในฟังก์ชันที่เรียกใช้mainloop()หรืออยู่ในฟังก์ชันที่เรียกโดยฟังก์ชันที่เรียกใช้mainloop()(เห็นได้ชัดว่ามีความสำคัญอย่างยิ่งในการหลีกเลี่ยงข้อผิดพลาด "อพาร์ทเมนต์ที่แตกต่างกัน")

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

รหัสที่แก้ไขมีดังนี้:

# Run tkinter code in another thread

import tkinter as tk
import threading

class App(threading.Thread):

    def __init__(self):
        threading.Thread.__init__(self)
        self.start()

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

    def run(self):
        self.root = tk.Tk()
        self.root.protocol("WM_DELETE_WINDOW", self.callback)

        label = tk.Label(self.root, text="Hello World")
        label.pack()

        self.root.mainloop()


app = App()
print('Now we can continue running code while mainloop runs!')

for i in range(100000):
    print(i)

คุณจะส่งผ่านข้อโต้แย้งไปยังrunเมธอดอย่างไร? ฉันคิดไม่ออกว่าจะทำอย่างไร ...
TheDoctor

5
โดยทั่วไปคุณจะส่งข้อโต้แย้งไป__init__(..)เก็บไว้selfและใช้ในrun(..)
Andre Holzner

1
รูทไม่ปรากฏขึ้นเลยโดยให้คำเตือน: คำเตือน: พื้นที่การลาก NSWindow ควรทำให้ไม่ถูกต้องในเธรดหลักเท่านั้น! สิ่งนี้จะทำให้เกิดข้อยกเว้นในอนาคต `
Bob Bobster

1
ความคิดเห็นนี้สมควรได้รับการยอมรับมากขึ้น น่าอัศจรรย์
Daniel Reyhanian

นี่คือตัวช่วยชีวิต โค้ดนอก GUI ควรตรวจสอบว่าเธรด tkinter ยังมีชีวิตอยู่หากคุณไม่สามารถออกจากสคริปต์ python ได้เมื่อออกจาก gui สิ่งที่ชอบwhile app.is_alive(): etc
m3nda

21

เมื่อเขียนลูปของคุณเองเช่นเดียวกับในการจำลอง (ฉันถือว่า) คุณต้องเรียกใช้updateฟังก์ชันที่ทำสิ่งที่mainloopทำ: อัปเดตหน้าต่างด้วยการเปลี่ยนแปลงของคุณ แต่คุณทำในวงของคุณ

def task():
   # do something
   root.update()

while 1:
   task()  

10
คุณจะต้องมีมากระมัดระวังกับชนิดของการเขียนโปรแกรมนี้ หากมีtaskการเรียกเหตุการณ์ใด ๆคุณจะจบลงด้วยการวนซ้ำเหตุการณ์ที่ซ้อนกันซึ่งไม่ดี เว้นแต่คุณจะเข้าใจอย่างถ่องแท้ว่าเหตุการณ์ลูปทำงานอย่างไรคุณควรหลีกเลี่ยงการโทรupdateโดยเสียค่าใช้จ่าย
Bryan Oakley

ฉันใช้เทคนิคนี้ครั้งเดียว - ใช้งานได้ดี แต่ขึ้นอยู่กับว่าคุณทำอย่างไรคุณอาจมีบางอย่างที่ส่ายใน UI
jldupont

@ ไบรอัน Oakley อัพเดตลูปแล้วหรือยัง? และจะเป็นปัญหาได้อย่างไร?
Green05

6

อีกทางเลือกหนึ่งคือให้ tkinter ดำเนินการกับเธรดแยกต่างหาก วิธีหนึ่งในการทำเช่นนี้:

import Tkinter
import threading

class MyTkApp(threading.Thread):
    def __init__(self):
        self.root=Tkinter.Tk()
        self.s = Tkinter.StringVar()
        self.s.set('Foo')
        l = Tkinter.Label(self.root,textvariable=self.s)
        l.pack()
        threading.Thread.__init__(self)

    def run(self):
        self.root.mainloop()


app = MyTkApp()
app.start()

# Now the app should be running and the value shown on the label
# can be changed by changing the member variable s.
# Like this:
# app.s.set('Bar')

โปรดระวังว่าการเขียนโปรแกรมแบบมัลติเธรดนั้นยากและง่ายมากที่จะถ่ายภาพตัวเองด้วยเท้า ตัวอย่างเช่นคุณต้องระมัดระวังเมื่อคุณเปลี่ยนตัวแปรสมาชิกของคลาสตัวอย่างด้านบนเพื่อที่คุณจะได้ไม่ขัดจังหวะด้วยเหตุการณ์วนซ้ำของ Tkinter


3
ไม่แน่ใจว่าสามารถใช้งานได้ เพิ่งลองสิ่งที่คล้ายกันและได้รับ "RuntimeError: main thread is not in main loop"
jldupont

5
jldupont: ฉันได้รับ "RuntimeError: กำลังเรียก Tcl จากพาร์ทเมนต์อื่น" (อาจเป็นข้อผิดพลาดเดียวกันในเวอร์ชันอื่น) การแก้ไขคือการเริ่มต้น Tk ในการรัน () ไม่ใช่ใน __init __ () ซึ่งหมายความว่าคุณกำลังเริ่มต้น Tk ในเธรดเดียวกับที่คุณเรียก mainloop () ใน
mgiuca

2

นี่เป็นเวอร์ชันที่ใช้งานได้รุ่นแรกของสิ่งที่จะเป็นตัวอ่าน GPS และผู้นำเสนอข้อมูล tkinter เป็นสิ่งที่เปราะบางมากโดยมีข้อความแสดงข้อผิดพลาดน้อยเกินไป มันไม่ใส่ของและไม่บอกว่าทำไมถึงใช้เวลามาก ยากมากที่มาจากผู้พัฒนาแบบฟอร์ม WYSIWYG ที่ดี อย่างไรก็ตามสิ่งนี้จะทำงานเป็นประจำเล็ก ๆ 10 ครั้งต่อวินาทีและนำเสนอข้อมูลในแบบฟอร์ม ใช้เวลาสักครู่เพื่อให้มันเกิดขึ้น เมื่อฉันลองใช้ค่าตัวจับเวลาเป็น 0 แบบฟอร์มก็ไม่เกิดขึ้น ตอนนี้หัวฉันเจ็บ! 10 ครั้งขึ้นไปต่อวินาทีก็ดีพอสำหรับฉัน ฉันหวังว่ามันจะช่วยคนอื่น ไมค์พรุ่งนี้

import tkinter as tk
import time

def GetDateTime():
  # Get current date and time in ISO8601
  # https://en.wikipedia.org/wiki/ISO_8601 
  # https://xkcd.com/1179/
  return (time.strftime("%Y%m%d", time.gmtime()),
          time.strftime("%H%M%S", time.gmtime()),
          time.strftime("%Y%m%d", time.localtime()),
          time.strftime("%H%M%S", time.localtime()))

class Application(tk.Frame):

  def __init__(self, master):

    fontsize = 12
    textwidth = 9

    tk.Frame.__init__(self, master)
    self.pack()

    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
             text='Local Time').grid(row=0, column=0)
    self.LocalDate = tk.StringVar()
    self.LocalDate.set('waiting...')
    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
             textvariable=self.LocalDate).grid(row=0, column=1)

    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
             text='Local Date').grid(row=1, column=0)
    self.LocalTime = tk.StringVar()
    self.LocalTime.set('waiting...')
    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
             textvariable=self.LocalTime).grid(row=1, column=1)

    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
             text='GMT Time').grid(row=2, column=0)
    self.nowGdate = tk.StringVar()
    self.nowGdate.set('waiting...')
    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
             textvariable=self.nowGdate).grid(row=2, column=1)

    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
             text='GMT Date').grid(row=3, column=0)
    self.nowGtime = tk.StringVar()
    self.nowGtime.set('waiting...')
    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
             textvariable=self.nowGtime).grid(row=3, column=1)

    tk.Button(self, text='Exit', width = 10, bg = '#FF8080', command=root.destroy).grid(row=4, columnspan=2)

    self.gettime()
  pass

  def gettime(self):
    gdt, gtm, ldt, ltm = GetDateTime()
    gdt = gdt[0:4] + '/' + gdt[4:6] + '/' + gdt[6:8]
    gtm = gtm[0:2] + ':' + gtm[2:4] + ':' + gtm[4:6] + ' Z'  
    ldt = ldt[0:4] + '/' + ldt[4:6] + '/' + ldt[6:8]
    ltm = ltm[0:2] + ':' + ltm[2:4] + ':' + ltm[4:6]  
    self.nowGtime.set(gdt)
    self.nowGdate.set(gtm)
    self.LocalTime.set(ldt)
    self.LocalDate.set(ltm)

    self.after(100, self.gettime)
   #print (ltm)  # Prove it is running this and the external code, too.
  pass

root = tk.Tk()
root.wm_title('Temp Converter')
app = Application(master=root)

w = 200 # width for the Tk root
h = 125 # height for the Tk root

# get display screen width and height
ws = root.winfo_screenwidth()  # width of the screen
hs = root.winfo_screenheight() # height of the screen

# calculate x and y coordinates for positioning the Tk root window

#centered
#x = (ws/2) - (w/2)
#y = (hs/2) - (h/2)

#right bottom corner (misfires in Win10 putting it too low. OK in Ubuntu)
x = ws - w
y = hs - h - 35  # -35 fixes it, more or less, for Win10

#set the dimensions of the screen and where it is placed
root.geometry('%dx%d+%d+%d' % (w, h, x, y))

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