Làm cách nào để bạn chạy mã của riêng mình cùng với vòng lặp sự kiện của Tkinter?


119

Em trai tôi mới bắt đầu học lập trình, và đối với dự án Hội chợ Khoa học của mình, anh ấy đang thực hiện mô phỏng một đàn chim trên bầu trời. Anh ấy đã viết hầu hết các mã của mình và nó hoạt động rất tốt, nhưng những con chim cần phải di chuyển mỗi giây phút .

Tuy nhiên, Tkinter kéo dài thời gian cho vòng lặp sự kiện của chính nó và vì vậy mã của anh ấy sẽ không chạy. Thực hiện root.mainloop()chạy, chạy và tiếp tục chạy, và điều duy nhất nó chạy là các trình xử lý sự kiện.

Có cách nào để mã của anh ấy chạy cùng với mainloop (không có đa luồng, nó khó hiểu và điều này nên được giữ đơn giản), và nếu có, đó là gì?

Ngay bây giờ, anh ta nghĩ ra một bản hack xấu xí, buộc move()chức năng của anh ta thành <b1-motion>, chỉ cần anh ta giữ nút và lắc chuột, nó sẽ hoạt động. Nhưng phải có một cách tốt hơn.

Câu trả lời:


141

Sử dụng afterphương thức trên Tkđối tượng:

from tkinter import *

root = Tk()

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

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

Đây là khai báo và tài liệu cho afterphương thức:

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
nếu bạn chỉ định thời gian chờ là 0, tác vụ sẽ tự quay lại vòng lặp sự kiện ngay sau khi kết thúc. điều này sẽ không chặn các sự kiện khác, trong khi vẫn chạy mã của bạn thường xuyên nhất có thể.
Nathan

Sau khi kéo tóc ra hàng giờ đồng hồ để cố gắng làm cho opencv và tkinter hoạt động bình thường và đóng sạch khi nút [X] được nhấp vào, điều này cùng với win32gui.FindWindow (Không có, 'tiêu đề cửa sổ') đã thực hiện một mẹo nhỏ! Tôi đúng là một
kẻ ngu ngốc

Đây không phải là lựa chọn tốt nhất; mặc dù nó hoạt động trong trường hợp này, nó không tốt cho hầu hết các tập lệnh (nó chỉ chạy 2 giây một lần) và đặt thời gian chờ là 0, theo đề xuất được đăng bởi @Nathan vì nó chỉ chạy khi tkinter không bận (có thể gây ra sự cố trong một số chương trình phức tạp). Tốt nhất hãy gắn bó với threadingmô-đun.
Anonymous

59

Các giải pháp đăng bởi Bjorn kết quả trong một "RuntimeError: Gọi Tcl từ chung cư khác nhau" tin nhắn trên máy tính của tôi (RedHat Enterprise 5, python 2.6.1). Bjorn có thể không nhận được thông báo này, vì, theo một nơi tôi đã kiểm tra , việc xử lý sai luồng với Tkinter là không thể đoán trước và phụ thuộc vào nền tảng.

Vấn đề dường như được app.start()tính là một tham chiếu đến Tk, vì ứng dụng chứa các phần tử Tk. Tôi đã sửa lỗi này bằng cách thay thế app.start()bằng một self.start()bên trong __init__. Tôi cũng đã thực hiện nó để tất cả các tham chiếu Tk đều nằm bên trong hàm gọimainloop() hoặc bên trong các hàm được gọi bởi hàm gọi mainloop()(điều này rõ ràng là rất quan trọng để tránh lỗi "căn hộ khác nhau").

Cuối cùng, tôi đã thêm một trình xử lý giao thức với một cuộc gọi lại, vì nếu không có điều này, chương trình sẽ thoát với lỗi khi người dùng đóng cửa sổ Tk.

Mã sửa đổi như sau:

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

Bạn sẽ chuyển các đối số cho runphương thức như thế nào? Tôi dường như không thể tìm ra cách để ...
TheDoctor

5
thường bạn sẽ vượt qua đối số __init__(..), lưu trữ chúng trong selfvà sử dụng chúng trongrun(..)
Andre Holzner

1
Thư mục gốc hoàn toàn không hiển thị, đưa ra cảnh báo: `` CẢNH BÁO: Các vùng kéo của NSWindow chỉ nên bị vô hiệu trên Chủ đề chính! Điều này sẽ tạo ra một ngoại lệ trong tương lai '
Bob Bobster

1
Nhận xét này xứng đáng được công nhận nhiều hơn. Kinh ngạc.
Daniel Reyhanian

Đây là một tiết kiệm cuộc sống. Mã bên ngoài GUI phải được kiểm tra xem luồng tkinter có còn tồn tại hay không nếu bạn không thể thoát khỏi tập lệnh python sau khi thoát khỏi gui. Một cái gì đó giống nhưwhile app.is_alive(): etc
m3nda

21

Khi viết vòng lặp của riêng bạn, như trong mô phỏng (tôi giả sử), bạn cần gọi updatehàm thực hiện chức năng mainloop: cập nhật cửa sổ với các thay đổi của bạn, nhưng bạn thực hiện nó trong vòng lặp của mình.

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

while 1:
   task()  

10
Bạn phải rất cẩn thận với kiểu lập trình này. Nếu bất kỳ sự kiện nào taskđược gọi, bạn sẽ kết thúc với các vòng lặp sự kiện lồng nhau và điều đó thật tệ. Trừ khi bạn hoàn toàn hiểu cách hoạt động của các vòng lặp sự kiện, bạn nên tránh gọi updatebằng mọi giá.
Bryan Oakley

Tôi đã sử dụng kỹ thuật này một lần - hoạt động tốt nhưng tùy thuộc vào cách bạn thực hiện, bạn có thể có một số điều đáng kinh ngạc trong giao diện người dùng.
jldupont

@Bryan Oakley Sau đó, cập nhật có phải là một vòng lặp không? Và làm thế nào đó sẽ là vấn đề?
Green05

6

Một tùy chọn khác là để tkinter thực thi trên một luồng riêng biệt. Một cách để làm điều đó là như sau:

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

Tuy nhiên, hãy cẩn thận, lập trình đa luồng rất khó và bạn rất dễ tự bắn vào chân mình. Ví dụ, bạn phải cẩn thận khi thay đổi các biến thành viên của lớp mẫu ở trên để không làm gián đoạn vòng lặp sự kiện của Tkinter.


3
Không chắc điều này có thể hoạt động. Chỉ cần thử một cái gì đó tương tự và tôi nhận được "RuntimeError: main thread is not in main loop".
jldupont

5
jldupont: Tôi nhận được "RuntimeError: Đang gọi Tcl từ các căn hộ khác nhau" (có thể cùng một lỗi trong một phiên bản khác). Cách khắc phục là khởi chạy Tk trong run (), không phải trong __init __ (). Điều này có nghĩa rằng bạn đang initialising Tk trong các chủ đề tương tự như bạn gọi mainloop () trong.
mgiuca

2

Đây là phiên bản làm việc đầu tiên của thiết bị đọc GPS và trình chiếu dữ liệu. tkinter là một thứ rất mỏng manh với quá ít thông báo lỗi. Nó không đưa những thứ lên và không cho biết lý do tại sao nhiều thời gian. Rất khó đến từ một nhà phát triển biểu mẫu WYSIWYG giỏi. Dù sao, điều này chạy một quy trình nhỏ 10 lần một giây và trình bày thông tin trên một biểu mẫu. Phải mất một thời gian để làm cho nó xảy ra. Khi tôi thử giá trị bộ đếm thời gian là 0, biểu mẫu không bao giờ xuất hiện. Đầu tôi bây giờ rất đau! 10 lần trở lên mỗi giây là đủ tốt cho tôi. Tôi hy vọng nó sẽ giúp người khác. 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()
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.