Expose Tcl's external event loop API in tkinter
Chưa có ai nhận issue này.
- Ngôn ngữ chính
- Python
- Star
- 77.2k
- Fork
- 35.9k
- Chỉ số merge pull request
- Chỉ số pull request đang chờ
Mô tả
Feature or enhancement
Proposal:
Tkinter runs Tcl's event loop by calling Tcl_DoOneEvent repeatedly, which is the usual way of running the event loop in Tcl/Tk. This puts Tcl/Tk in charge of the event loop.
Tcl also supports running external event loops via the function Tcl_SetNotifier and associated functions (Tcl_QueueEvent and Tcl_ServiceAll in particular). This part of Tcl's API is currently not exposed in tkinter.
Exposing Tcl_SetNotifier and associated functions would allow Tcl to use the event loop of a different GUI toolkit (e.g. Glib via pygobject) as an external event loop. Alternatively, a single event loop managed by Python can be used as an external event loop both by tkinter and other GUI toolkits at the same time.
This has two advantages:
- It allows the use of multiple GUI toolkits at the same time, without the need for threads or polling (i.e., no busy-sleep loop, which is wasteful, slower, and fragile).
- It makes threading easier, as it avoids the need for
ENTER_TCL/LEAVE_TCLandENTER_PYTHON/LEAVE_PYTHON(in the case of tkinter).
Below is a minimal script demonstrating how two GUI toolkits can run at the same time using a single event loop managed by Python (using select from the Python standard library, just to keep it simple).
import os
import select
import sys
import time
import tkinter
from tkinter import _tkinter
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib
# --- Create an event source for tkinter
class TkinterEventHandler:
def __init__(self, callback):
self.callback = callback
self.ready_events = 0
def process_event(self, flags):
if not (flags & _tkinter.FILE_EVENTS):
return False
events = self.ready_events
self.ready_events = 0
if events != 0:
self.callback(_tkinter.READABLE)
return True
def __call__(self, events):
if self.ready_events == 0:
_tkinter.queue_event(self.process_event)
self.ready_events = events
class TkinterEventSource:
def __init__(self):
self.handlers = {}
self.timer_abstime = 0
def set_timer(self, interval_usec=None):
mode = _tkinter.set_service_mode(_tkinter.SERVICE_ALL)
if interval_usec is None:
self.timer_abstime = 0
else:
now = time.perf_counter_ns() // 1000
self.timer_abstime = now + interval_usec
def wait_for_event(self, timeout):
return 0
def init_notifier(self):
return
def create_file_handler(self, fd, mask, callback):
event_handler = TkinterEventHandler(callback)
if mask & _tkinter.READABLE:
self.handlers[fd] = event_handler
def delete_file_handler(self, fd):
try:
del self.handlers[fd]
except KeyError:
pass
def finalize_notifier(self, clientData):
return
def alert_notifier(self, clientData):
return
def service_mode_hook(self, mode):
return
def setup(self):
timer_abstime = self.timer_abstime
if timer_abstime > 0:
now = time.perf_counter_ns() // 1000
timeout_usec = timer_abstime - now
if timeout_usec < 0:
timeout = 0
else:
timeout = timeout_usec / 1000000
else:
timeout = None
fds = list(self.handlers.keys())
return timeout, fds
def check(self, ready_events):
timer_abstime = self.timer_abstime
result = 0;
now = time.perf_counter_ns() // 1000
if timer_abstime < now:
self.timer_abstime = 0
ready_rlist, ready_wlist, ready_xlist = ready_events
for fd in ready_rlist:
handler = self.handlers.get(fd)
if handler is None:
continue
handler(_tkinter.READABLE)
_tkinter.service_all()
# --- Tell Tcl to use TkinterEventSource as its notifier
tkinter_event_source = TkinterEventSource()
_tkinter.set_notifier(tkinter_event_source.set_timer,
tkinter_event_source.wait_for_event,
tkinter_event_source.create_file_handler,
tkinter_event_source.delete_file_handler,
tkinter_event_source.init_notifier,
tkinter_event_source.finalize_notifier,
tkinter_event_source.alert_notifier,
tkinter_event_source.service_mode_hook
)
# --- Create an event source for GTK
class GTKEventSource:
def setup(self):
context = GLib.MainContext.default()
if not context.acquire():
raise RuntimeError("Failed to acquire the context")
ready, priority = context.prepare()
timeout_msec, self.fds = context.query(priority)
fds = [fd.fd for fd in self.fds]
if timeout_msec == -1:
timeout = None
else:
timeout = timeout_msec / 1000
context.release()
return timeout, fds
def check(self, ready_events):
context = GLib.MainContext.default()
if not context.acquire():
raise RuntimeError("Failed to acquire the context")
ready_rlist, ready_wlist, ready_xlist = ready_events
ready_rlist = set(ready_rlist)
for fd in self.fds:
if fd.fd in ready_rlist:
fd.revents |= GLib.IO_IN
some_ready = context.check(GLib.MAXINT, self.fds)
context.dispatch()
context.release()
gtk_event_source = GTKEventSource()
# --- Make one example window for tkinter
class TkinterWindow:
def __init__(self):
self.window = tkinter.Tk()
self.window.title("Tkinter")
self.button1 = tkinter.Button(self.window,
text="Click me",
command=self.clicked,
font='Helvetica 20',
width=20,
background='pale green')
self.button1.pack()
pipe_read_fd, self.pipe_write_fd = os.pipe()
self.window.tk.createfilehandler(pipe_read_fd,
tkinter.READABLE,
self.read_from_pipe)
self.button2 = tkinter.Button(self.window,
text="Trigger the file descriptor",
command=self.write_to_pipe,
font='Helvetica 20',
width=20,
background='lightblue')
self.button2.pack()
self.button3 = tkinter.Button(self.window,
text="Start the timer (1 sec)",
command=self.update_timer,
font='Helvetica 20',
width=20,
background='steelblue')
self.button3.pack()
def clicked(self):
print("Tkinter button clicked")
def write_to_pipe(self):
message = "Tkinter file descriptor triggered"
n = os.write(self.pipe_write_fd, message.encode())
def read_from_pipe(self, fd, mask):
message = os.read(fd, 1024)
print(message.decode())
def update_timer(self, counter=0):
print("Tkinter timer update %d" % counter)
self.window.after(1000, self.update_timer, counter+1)
tkinter_window = TkinterWindow()
# --- Make one example window for GTK
settings = Gtk.Settings.get_default()
settings.props.gtk_theme_name = "Adwaita"
class GTKWindow(Gtk.ApplicationWindow):
def __init__(self, **kargs):
super().__init__(**kargs, title='GTK')
self.grid = Gtk.Grid()
self.add(self.grid)
button1 = self.create_button(1, text="Click me", color="red")
button1.connect("clicked", self.clicked)
self.grid.attach(button1, 0, 0, 1, 1)
pipe_read_fd, self.pipe_write_fd = os.pipe()
GLib.io_add_watch(pipe_read_fd, GLib.IOCondition.IN, self.read_from_pipe)
button2 = self.create_button(2, text="Trigger the file descriptor", color='orchid')
button2.connect("clicked", self.write_to_pipe)
self.grid.attach(button2, 0, 1, 1, 1)
button3 = self.create_button(3, text="Start the timer (2 sec)", color='salmon')
button3.connect("clicked", self.start_timer)
self.grid.attach(button3, 0, 2, 1, 1)
self.show_all()
def create_button(self, number, text, color):
button = Gtk.Button.new_with_label(text)
key = "custom-button%d" % number
button.get_style_context().add_class(key)
css_provider = Gtk.CssProvider()
css_provider.load_from_data(b"""
.%s {
font-family: Helvetica;
font-size: 20pt;
background: %s;
color: black;
}
""" % (key.encode(), color.encode()))
# Apply the CSS to the display
style_context = button.get_style_context()
style_context.add_provider(css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
style_context.add_class(key) # Add the CSS class to the button
return button
def clicked(self, button):
print("GTK button clicked")
def write_to_pipe(self, button):
message = "GTK file descriptor triggered"
n = os.write(self.pipe_write_fd, message.encode())
def read_from_pipe(self, fd, condition):
if condition & GLib.IOCondition.IN:
message = os.read(fd, 1024)
print(message.decode())
return True
def update_timer(self):
print("GTK timer update %d" % self.counter)
self.counter += 1
return True
def start_timer(self, button):
self.counter = 0
GLib.timeout_add(2000, self.update_timer)
glib_window = GTKWindow()
# --- run the event loop with the two GUI frameworks as event sources
sources = [tkinter_event_source, gtk_event_source ]
while True:
rlist = []
wlist = []
xlist = []
shortest_timeout = None
for source in sources:
timeout, fds = source.setup()
if timeout is not None:
if shortest_timeout is None:
shortest_timeout = timeout
else:
if timeout < shortest_timeout:
shortest_timeout = timeout
rlist.extend(fds)
ready_events = select.select(rlist, wlist, xlist, shortest_timeout)
for source in sources:
source.check(ready_events)
Has this already been discussed elsewhere?
I have already discussed this feature proposal on Discourse
Links to previous discussion of this feature:
https://discuss.python.org/t/connecting-asyncio-and-tkinter-event-loops/14722/40
Linked PRs
- gh-155805
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Hướng nghiên cứu
Bắt đầu bằng việc xem xét tích hợp vòng lặp sự kiện hiện có của tkinter và _tkinter, sau đó so sánh với Tcl_SetNotifier cùng các API liên quan Tcl_QueueEvent và Tcl_ServiceAll. Trước tiên, hãy đọc cuộc thảo luận được liên kết và PR gh-155805; công việc được hoàn tất khi API notifier bên ngoài được cung cấp và hành vi của nó trong vòng lặp sự kiện được xác thực.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- python
- Lĩnh vực
- api, desktop
- Loại issue
- Tính năng
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức độ hoạt động
- Đình trệ
- Độ rõ ràng
- Khá rõ ràng
- Mức phù hợp với người mới
- 25/100