Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decoupled winforms event loop from AppContext. #2112

Merged
merged 13 commits into from
Sep 15, 2023
Merged
1 change: 1 addition & 0 deletions changes/750.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The WinForms event loop was decoupled from the main form, allowing background tasks to run without a main window being present.
12 changes: 12 additions & 0 deletions winforms/src/toga_winforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

import toga

# Add a reference to the Winforms assembly
clr.AddReference("System.Windows.Forms")

# Add a reference to the WindowsBase assembly. This is needed to access
# System.Windows.Threading.Dispatcher.
#
# This assembly isn't exposed as a simple dot-path name; we have to extract it from the
# Global Assembly Cache (GAC). The version number and public key doesn't appear to
# change with Windows version or the underlying .NET, and has been available since
# Windows 7.
clr.AddReference(
"WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
)

__version__ = toga._package_version(__file__, __name__)
9 changes: 4 additions & 5 deletions winforms/src/toga_winforms/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@
from ctypes import windll

import System.Windows.Forms as WinForms
from System import (
Environment,
Threading,
)
from System import Environment, Threading
from System.Media import SystemSounds
from System.Net import SecurityProtocolType, ServicePointManager
from System.Windows.Threading import Dispatcher

import toga
from toga import Key
Expand Down Expand Up @@ -60,6 +58,7 @@ def __init__(self, interface):
def create(self):
self.native = WinForms.Application
self.app_context = WinForms.ApplicationContext()
self.app_dispatcher = Dispatcher.CurrentDispatcher

# Check the version of windows and make sure we are setting the DPI mode
# with the most up to date API
Expand Down Expand Up @@ -250,7 +249,7 @@ def run_app(self):
# in a usable form.
self.native.ThreadException += self.winforms_thread_exception

self.loop.run_forever(self.app_context)
self.loop.run_forever(self)
except Exception as e:
# In case of an unhandled error at the level of the app,
# preserve the Python stacktrace
Expand Down
22 changes: 9 additions & 13 deletions winforms/src/toga_winforms/libs/proactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


class WinformsProactorEventLoop(asyncio.ProactorEventLoop):
def run_forever(self, app_context):
def run_forever(self, app):
"""Set up the asyncio event loop, integrate it with the Winforms event loop, and
start the application.

Expand All @@ -27,8 +27,8 @@ def run_forever(self, app_context):
# select call to process.
self.call_soon(self._loop_self_reading)

# Remember the application context.
self.app_context = app_context
# Remember the application.
self.app = app

# Set up the Proactor.
# The code between the following markers should be exactly the same as
Expand Down Expand Up @@ -65,23 +65,19 @@ def run_forever(self, app_context):
self.enqueue_tick()

# Start the Winforms event loop.
WinForms.Application.Run(self.app_context)
WinForms.Application.Run(self.app.app_context)

def enqueue_tick(self):
# Queue a call to tick in 5ms.
self.task = Action[Task](self.tick)
Task.Delay(5).ContinueWith(self.task)
if not self.app._is_exiting:
self.task = Action[Task](self.tick)
Task.Delay(5).ContinueWith(self.task)

def tick(self, *args, **kwargs):
"""Cause a single iteration of the event loop to run on the main GUI thread."""
# FIXME: this only works if there is a "main window" registered with the
# app (#750).
#
# If the app context has a main form, invoke run_once_recurring()
# on the thread associated with that form.
if self.app_context.MainForm:
if not self.app._is_exiting:
action = Action(self.run_once_recurring)
self.app_context.MainForm.Invoke(action)
self.app.app_dispatcher.Invoke(action)

def run_once_recurring(self):
"""Run one iteration of the event loop, and enqueue the next iteration (if we're
Expand Down
15 changes: 7 additions & 8 deletions winforms/src/toga_winforms/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,18 +131,17 @@ def get_visible(self):
return self.native.Visible

def winforms_FormClosing(self, sender, event):
# If the app is exiting, or a manual close has been requested,
# don't get confirmation; just close.
# If the app is exiting, or a manual close has been requested, don't get
# confirmation; just close.
if not self.interface.app._impl._is_exiting and not self._is_closing:
if not self.interface.closeable:
# Closeability is implemented by shortcutting the close handler.
# Window isn't closable, so any request to close should be cancelled.
event.Cancel = True
elif self.interface.on_close._raw:
# If there is an on_close event handler, process it;
# but then cancel the close event. If the result of
# on_close handling indicates the window should close,
# then it will be manually triggered as part of that
# result handling.
# If there is an on_close event handler, process it; but then cancel
# the close event. If the result of on_close handling indicates the
# window should close, then it will be manually triggered as part of
# that result handling.
self.interface.on_close(self)
event.Cancel = True

Expand Down