Bagaimana Anda menjalankan kode Anda sendiri bersama event loop Tkinter?

119

Adik laki-laki saya baru saja masuk ke pemrograman, dan untuk proyek Science Fair-nya, dia melakukan simulasi sekawanan burung di langit. Dia mendapatkan sebagian besar kodenya tertulis, dan itu bekerja dengan baik, tetapi burung perlu bergerak setiap saat .

Tkinter, bagaimanapun, memonopoli waktu untuk event loopnya sendiri, sehingga kodenya tidak akan berjalan. Melakukan root.mainloop()lari, lari, dan terus berlari, dan satu-satunya yang dijalankannya adalah pengendali kejadian.

Apakah ada cara agar kodenya berjalan bersama mainloop (tanpa multithreading, itu membingungkan dan ini harus dibuat sederhana), dan jika demikian, apa itu?

Saat ini, dia datang dengan peretasan jelek, mengikat move()fungsinya ke <b1-motion>, sehingga selama dia menahan tombol dan menggoyangkan mouse, itu berhasil. Tapi pasti ada cara yang lebih baik.

Allan S
sumber

Jawaban:

141

Gunakan aftermetode pada Tkobjek:

from tkinter import *

root = Tk()

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

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

Berikut deklarasi dan dokumentasi untuk aftermetode tersebut:

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."""
Dave Ray
sumber
30
jika Anda menetapkan waktu tunggu menjadi 0, tugas akan menempatkan dirinya kembali di loop acara segera setelah selesai. ini tidak akan memblokir acara lain, sambil tetap menjalankan kode Anda sesering mungkin.
Nathan
Setelah menjambak rambut saya selama berjam-jam mencoba untuk mendapatkan opencv dan tkinter untuk bekerja sama dengan benar dan menutup dengan rapi ketika tombol [X] diklik, ini bersama dengan win32gui. FindWindow (None, 'window title') berhasil! Saya seperti noob ;-)
JxAxMxIxN
Ini bukan pilihan terbaik; meskipun berfungsi dalam kasus ini, ini tidak baik untuk sebagian besar skrip (hanya berjalan setiap 2 detik), dan menyetel batas waktu menjadi 0, sesuai saran yang diposting oleh @Nathan karena hanya berjalan ketika tkinter tidak sibuk (yang dapat menyebabkan masalah di beberapa program yang kompleks). Lebih baik tetap menggunakan threadingmodul.
Anonim
59

The solusi diposting oleh Bjorn hasil dalam "RuntimeError: Memanggil Tcl dari apartemen yang berbeda" pesan di komputer saya (RedHat Enterprise 5, python 2.6.1). Bjorn mungkin tidak mendapatkan pesan ini, karena menurut satu tempat yang saya periksa , kesalahan penanganan threading dengan Tkinter tidak dapat diprediksi dan bergantung pada platform.

Masalahnya tampaknya yang app.start()dihitung sebagai referensi ke Tk, karena aplikasi berisi elemen Tk. Saya memperbaikinya dengan mengganti app.start()dengan bagian self.start()dalam __init__. Saya juga membuatnya sehingga semua referensi Tk baik di dalam fungsi yang memanggilmainloop() atau di dalam fungsi yang dipanggil oleh fungsi yang memanggil mainloop()(ini tampaknya penting untuk menghindari kesalahan "apartemen berbeda").

Akhirnya, saya menambahkan penangan protokol dengan callback, karena tanpa ini program keluar dengan kesalahan ketika jendela Tk ditutup oleh pengguna.

Kode yang direvisi adalah sebagai berikut:

# 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)
Kevin
sumber
Bagaimana Anda menyampaikan argumen ke runmetode tersebut? Sepertinya saya tidak tahu bagaimana ...
TheDoctor
5
biasanya Anda akan menyampaikan argumen ke __init__(..), menyimpannya selfdan menggunakannya dirun(..)
Andre Holzner
1
Akar tidak muncul sama sekali, memberikan peringatan: PERINGATAN: Wilayah seret NSWindow seharusnya hanya dibatalkan di Thread Utama! Ini akan memunculkan pengecualian di masa mendatang `
Bob Bobster
1
Komentar ini membutuhkan lebih banyak pengakuan. Luar biasa.
Daniel Reyhanian
Ini adalah penyelamat hidup. Kode di luar GUI harus memeriksa utas tkinter untuk hidup jika Anda tidak ingin dapat keluar dari skrip python setelah keluar dari gui. Sesuatu sepertiwhile app.is_alive(): etc
m3nda
21

Saat menulis loop Anda sendiri, seperti dalam simulasi (saya asumsikan), Anda perlu memanggil updatefungsi yang melakukan apa yang mainloopdilakukannya: memperbarui jendela dengan perubahan Anda, tetapi Anda melakukannya di loop Anda.

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

while 1:
   task()  
jma
sumber
10
Anda harus sangat berhati-hati dengan jenis pemrograman ini. Jika ada peristiwa yang menyebabkan taskdipanggil, Anda akan berakhir dengan loop acara bersarang, dan itu buruk. Kecuali Anda benar-benar memahami cara kerja loop acara, Anda harus menghindari panggilan updatedengan cara apa pun.
Bryan Oakley
Saya menggunakan teknik ini sekali - berfungsi dengan baik tetapi tergantung pada bagaimana Anda melakukannya, Anda mungkin akan terkejut di UI.
jldupont
@ Bryan Oakley Apakah memperbarui loop? Dan bagaimana hal itu bisa menjadi masalah?
Green05
6

Pilihan lainnya adalah membiarkan tkinter mengeksekusi pada utas terpisah. Salah satu caranya adalah seperti ini:

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')

Berhati-hatilah, pemrograman multithread itu sulit dan sangat mudah untuk menembak diri Anda sendiri. Misalnya Anda harus berhati-hati ketika Anda mengubah variabel anggota dari kelas contoh di atas sehingga Anda tidak mengganggu event loop Tkinter.


sumber
3
Tidak yakin ini bisa berhasil. Baru saja mencoba sesuatu yang serupa dan saya mendapatkan "RuntimeError: utas utama tidak di loop utama".
jldupont
5
jldupont: Saya mendapat "RuntimeError: Memanggil Tcl dari apartemen yang berbeda" (mungkin kesalahan yang sama di versi yang berbeda). Perbaikannya adalah menginisialisasi Tk di run (), bukan di __init __ (). Ini berarti Anda menjalankan Tk di utas yang sama seperti yang Anda panggil mainloop () masuk.
mgiuca
2

Ini adalah versi kerja pertama dari apa yang akan menjadi pembaca GPS dan penyaji data. tkinter adalah hal yang sangat rapuh dengan terlalu sedikit pesan kesalahan. Itu tidak meletakkan barang-barang dan tidak memberi tahu mengapa banyak waktu. Sangat sulit datang dari pengembang formulir WYSIWYG yang baik. Bagaimanapun, ini menjalankan rutinitas kecil 10 kali per detik dan menyajikan informasi pada formulir. Butuh beberapa saat untuk mewujudkannya. Ketika saya mencoba nilai pengatur waktu 0, bentuknya tidak pernah muncul. Kepalaku sekarang sakit! 10 kali atau lebih per detik sudah cukup baik untuk saya. Saya harap ini membantu orang lain. Mike Morrow

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()
Micheal Morrow
sumber