Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3c691b5
Add .NET Core 10 loader.
freakboy3742 Apr 16, 2026
6545233
Add ARM64 CI jobs on Windows.
freakboy3742 Apr 16, 2026
7f89203
Document .NET requirements for the Winforms backend.
freakboy3742 Apr 16, 2026
0876d60
Add changenote.
freakboy3742 Apr 16, 2026
05d5cb9
Adapt status icons to use .NET Core 10 API
freakboy3742 Apr 20, 2026
c158d3c
Provide an opt-out path for .NET Core 10 usage.
freakboy3742 Apr 20, 2026
349b641
Adapt Winforms testbed for .NET Core 10 events.
freakboy3742 Apr 20, 2026
c13e086
Do a separate .NET Core CI run.
freakboy3742 Apr 20, 2026
0e58364
Correct .NET core behavior on folder select tests.
freakboy3742 Apr 21, 2026
a8c8d59
Correct testbed CI targets.
freakboy3742 Apr 21, 2026
c4bd86d
Use ARM-compatible briefcase branch, and add diagnostics for testbed.
freakboy3742 Apr 21, 2026
cebcd6c
Cleanup .NET Core docstrings.
freakboy3742 Apr 21, 2026
0742f8d
Correct winforms netfx targeting.
freakboy3742 Apr 21, 2026
e4d8e54
Correct some .NET Framework 4.x compatibility issues.
freakboy3742 Apr 21, 2026
820fe70
Add tmate for ARM64 diagnostics.
freakboy3742 Apr 21, 2026
2acaae4
Add no-cover handling for netfx.
freakboy3742 Apr 21, 2026
060ec77
Use an explicit tmate version.
freakboy3742 Apr 21, 2026
dcaa81e
Move coverage config to testbed.
freakboy3742 Apr 21, 2026
75a5db6
Ensure that window size is restored correctly after fullscreen/presen…
freakboy3742 Apr 21, 2026
013beba
Update WebView2 DLLs, including ARM64 versions.
freakboy3742 Apr 21, 2026
436cab4
Generate arm64 windows wheel.
freakboy3742 Apr 21, 2026
2311c24
Tweak package naming.
freakboy3742 Apr 21, 2026
bc3702a
Turn on slow testing for Win ARM64
freakboy3742 Apr 21, 2026
04ae259
Get a narrow set of netcore tests.
freakboy3742 Apr 21, 2026
7fd6b05
Correct test targeting.
freakboy3742 Apr 21, 2026
18699bc
Only perform a basic app start tests for Winforms on ARM.
freakboy3742 Apr 22, 2026
14f5735
Apply suggestions from code review
freakboy3742 Apr 22, 2026
3df895f
Simplify environment variable usage.
freakboy3742 Apr 22, 2026
4558839
Miscellanous cleanups and clarifications.
freakboy3742 Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 77 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ jobs:

package:
name: Package Toga
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
attestations: write
strategy:
matrix:
subdir:
wheel:
- "android"
- "cocoa"
- "core"
Expand All @@ -63,11 +64,60 @@ jobs:
- "travertino"
- "textual"
- "web"
- "winforms"
uses: beeware/.github/.github/workflows/python-package-create.yml@main
with:
build-subdirectory: ${{ matrix.subdir }}
attest: ${{ inputs.attest-package }}
- "winforms-x64"
- "winforms-arm64"
include:
- subdir: ""

- wheel: "winforms-x64"
subdir: "winforms"
plat-name: "win_amd64"
exclude-runtime: "win-arm64"
- wheel: "winforms-arm64"
subdir: "winforms"
arch: "arm64"
Comment thread
mhsmith marked this conversation as resolved.
Outdated
plat-name: "win_arm64"
exclude-runtime: "win-x64"

# On Windows, we need to produce py3-none-win_arm64 and py3-none-win_amd64 wheels
# because the wheel includes a platform-specific binary to support WebView2. The
# existence of a `subdir` value is used as an indicator. However, this means we
# can't use the common BeeWare package building workflow. Reproduce the core parts here,
# with the extra pieces to support windows wheels.
#
# uses: beeware/.github/.github/workflows/python-package-create.yml@main
# with:
# build-subdirectory: ${{ matrix.subdir || matrix.wheel }}
# attest: ${{ inputs.attest-package }}

steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
repository: ${{ github.repository }}
fetch-depth: 0 # Fetch all refs so setuptools_scm can generate the correct version number

# A tagged wheel is generated by writing a `bdist_wheel` section to pyproject.toml
- name: Configure platform tag
if: ${{ matrix.subdir }}
run: |
printf '[tool.distutils.bdist_wheel]\nplat-name = "${{ matrix.plat-name }}"\n' >> winforms/pyproject.toml

# To ensure the binary wheel only contains the binaries for that platform,
# delete content that is for the other architecture.
- name: Remove other-arch native DLL
if: ${{ matrix.subdir }}
run: |
rm -rf winforms/src/toga_winforms/libs/WebView2/runtimes/${{ matrix.exclude-runtime }}

- name: Build Package & Upload Artifact
id: package
uses: hynek/build-and-inspect-python-package@v2.15.0
with:
path: ${{ matrix.subdir || matrix.wheel }}
upload-name-suffix: ${{ format('-{0}', matrix.wheel) }}
attest-build-provenance-github: ${{ inputs.attest-package }}


docs-lint:
name: Documentation linting
Expand Down Expand Up @@ -143,7 +193,7 @@ jobs:
- name: Get Packages
uses: actions/download-artifact@v8.0.1
with:
pattern: ${{ format('{0}-*', needs.package.outputs.artifact-basename) }}
pattern: "Packages-*"
merge-multiple: true
path: dist

Expand Down Expand Up @@ -232,7 +282,7 @@ jobs:
- name: Get Package
uses: actions/download-artifact@v8.0.1
with:
pattern: ${{ format('{0}-*', needs.package.outputs.artifact-basename) }}
pattern: "Packages-travertino"
merge-multiple: true
path: dist

Expand All @@ -251,7 +301,9 @@ jobs:
backend:
- "macOS-x86_64"
- "macOS-arm64"
- "windows"
- "windows-netfx-x86_64"
- "windows-netcore-x86_64"
- "windows-netcore-arm64"
- "linux-x11-gtk3"
- "linux-wayland-gtk3"
- "linux-wayland-gtk4"
Expand All @@ -267,6 +319,7 @@ jobs:
- pre-command: ""
briefcase-run-prefix: ""
briefcase-run-args: ""
briefcase-test-args: ""
setup-python: true
testbed-app: "testbed"
copy-command: "cp -r"
Expand Down Expand Up @@ -489,10 +542,21 @@ jobs:
briefcase-run-args: --config 'requires=["toga-core", "toga-textual"]' --config 'console_app=true'
app-user-data-path: '$HOME\AppData\Local\Tiberius Yak\Toga Testbed\Data'

- backend: "windows"
- backend: "windows-netfx-x86_64"
platform: "windows"
runs-on: "windows-latest"
app-user-data-path: '$HOME\AppData\Local\Tiberius Yak\Toga Testbed\Data'
briefcase-run-prefix: TOGA_WINFORMS_USE_NETFX=1

- backend: "windows-netcore-x86_64"
platform: "windows"
runs-on: "windows-latest"
app-user-data-path: '$HOME\AppData\Local\Tiberius Yak\Toga Testbed\Data'

- backend: "windows-netcore-arm64"
platform: "windows"
runs-on: "windows-11-arm"
app-user-data-path: '$HOME\AppData\Local\Tiberius Yak\Toga Testbed\Data'

- backend: "iOS"
platform: "iOS"
Expand Down Expand Up @@ -555,7 +619,7 @@ jobs:
- name: Get Packages
uses: actions/download-artifact@v8.0.1
with:
pattern: ${{ format('{0}-*', needs.package.outputs.artifact-basename) }}
pattern: "Packages-*"
merge-multiple: true
path: dist

Expand All @@ -565,7 +629,7 @@ jobs:
run: |
${{ matrix.briefcase-run-prefix }} \
briefcase run ${{ matrix.platform }} --log --test \
${{ matrix.briefcase-run-args }} --app ${{ matrix.testbed-app }} -- --ci
${{ matrix.briefcase-run-args }} --app ${{ matrix.testbed-app }} -- --ci ${{ matrix.briefcase-test-args }}

- name: Upload Logs
uses: actions/upload-artifact@v7.0.1
Expand Down Expand Up @@ -634,7 +698,7 @@ jobs:
- name: Get Packages
uses: actions/download-artifact@v8.0.1
with:
pattern: ${{ format('{0}-*', needs.package.outputs.artifact-basename) }}
pattern: "Packages-positron"
merge-multiple: true
path: dist

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
- name: Get Packages
uses: actions/download-artifact@v8.0.1
with:
pattern: ${{ format('{0}-*', needs.ci.outputs.artifact-basename) }}
pattern: "Packages-*"
merge-multiple: true
path: dist

Expand Down Expand Up @@ -79,7 +79,7 @@ jobs:
- name: Get Packages
uses: actions/download-artifact@v8.0.1
with:
pattern: ${{ format('{0}-*', needs.ci.outputs.artifact-basename) }}
pattern: "Packages-*"
merge-multiple: true
path: staging_dist

Expand Down
1 change: 1 addition & 0 deletions changes/2782.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Toga's WinForms backend can now be used on ARM64 machines with a native ARM64 Python interpreter.
8 changes: 5 additions & 3 deletions docs/en/reference/platforms/windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ The Toga backend for Windows is [`toga-winforms`](https://github.com/beeware/tog

`toga-winforms` requires Python 3.10+, and Windows 10 or newer.

/// note | Note
Toga requires the use of either .NET Framework 4.x, or .NET Core 10.

Toga uses the [Python.NET](https://pythonnet.github.io) library to access the underlying Winforms GUI toolkit on Windows. Unfortunately, Python.NET doesn't always keep up with the release schedule of Python itself, and as a result, may not be compatible with recently-released versions of Python (i.e., a Python release with a version number of 3.X.0 or 3.X.1). If you experience problems installing Toga, and you're using a recently-released version of Python, try downgrading to the previous minor release (e.g. 3.13.9 instead of 3.14.0).
If you're on an x86-64 machine, .NET Framework 4.x is installed by default on Windows 10 and 11. Toga will use .NET Core 10 if it is installed. If you explicitly *want* to use .NET Framework 4.x, set the `TOGA_WINFORMS_USE_NETFX` environment variable.
Comment thread
freakboy3742 marked this conversation as resolved.
Outdated

If you're using an ARM64 machine, and you're using a native ARM64 Python interpreter, you *must* use .NET Core 10. The [.NET Desktop Runtime can be downloaded from the .NET website](https://dotnet.microsoft.com/en-us/download/dotnet/10.0). If you're using an x86-64 interpreter on an ARM64 machine, Toga can use the .NET Framework install that is provided by default.

///
Toga uses the [Python.NET](https://pythonnet.github.io) library to access the underlying Winforms GUI toolkit on Windows. Unfortunately, Python.NET doesn't always keep up with the release schedule of Python itself, and as a result, may not be compatible with recently-released versions of Python (i.e., a Python release with a version number of 3.X.0 or 3.X.1). If you experience problems installing Toga, and you're using a recently-released version of Python, try downgrading to the previous minor release (e.g. 3.13.9 instead of 3.14.0).
Comment thread
freakboy3742 marked this conversation as resolved.
Outdated

If you are using Windows 10 and want to use a WebView to display web content, you will also need to install the [Edge WebView2 Evergreen Runtime.](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download) Windows 11 has this runtime installed by default.

Expand Down
8 changes: 8 additions & 0 deletions testbed/src/testbed/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,14 @@ async def on_running(self):


def main(appname):
if toga.backend == "toga_winforms":
import toga_winforms

if toga_winforms._use_dotnet_core:
print("Running testbed using .NET Core")
else:
print("Running testbed using .NET Framework 4.x")

return Testbed(
app_name=appname,
document_types=[ExampleDoc, ReadonlyDoc],
Expand Down
6 changes: 5 additions & 1 deletion testbed/tests/app/test_desktop.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,8 @@ def find_event(event_path, main_window_probe):
from System import Array, Object
from System.Reflection import BindingFlags

from toga_winforms import _use_dotnet_core

event_class, event_name = event_path.split(".")
if event_class == "Form":
return getattr(main_window_probe.native, f"On{event_name}")
Expand All @@ -801,7 +803,9 @@ def find_event(event_path, main_window_probe):
][0]

event_key = SystemEvents_type.GetField(
f"On{event_name}Event", binding_flags
# .NET Core 10 uses a different naming convention for system events.
(f"s_on{event_name}Event" if _use_dotnet_core else f"On{event_name}Event"),
Comment thread
freakboy3742 marked this conversation as resolved.
Outdated
binding_flags,
).GetValue(None)

return lambda event_args: RaiseEvent.Invoke(
Expand Down
30 changes: 25 additions & 5 deletions testbed/tests/testbed.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import platform
import sys
import tempfile
import time
Expand Down Expand Up @@ -34,13 +35,25 @@ def run_tests(app, cov, args, report_coverage, run_slow, running_in_ci):

print("ready.")

# Textual backend does not yet support testing.
# However, this will verify a Textual app can at least start.
# Some backends and platforms do not support interactive GUI testing.
# On those platforms, perform a basic app start test.
import toga

if toga.backend == "toga_textual":
time.sleep(1) # wait for the Textual app to start
app.returncode = 0 if app._impl.native.is_running else 1
if (
# Textual doesn't have a test probe
toga.backend == "toga_textual"
# On GitHub Actions, Windows/ARM64 runners don't have an interactive
# logon session, so you can't run most of the GUI tests.
Comment thread
mhsmith marked this conversation as resolved.
Outdated
or (
toga.backend == "toga_winforms"
and platform.machine() == "ARM64"
and running_in_ci
)
):
time.sleep(1) # wait for the app to start
print("Performing a basic app startup test...", end="")
app.returncode = 0 if app._impl.loop.is_running() else 1
print("done.")
return

# Control the run speed of the test app.
Expand Down Expand Up @@ -144,10 +157,12 @@ def main(main_package_name, backend_override=None):
cov.set_option(
"coverage_conditional_plugin:rules",
{
# Linux X vs Wayland
"no-cover-if-linux-wayland": "os_environ.get('WAYLAND_DISPLAY', '') != ''",
"no-cover-if-linux-x": (
"os_environ.get('WAYLAND_DISPLAY', 'not-set') == 'not-set'"
),
# Linux GTK3/4 + Adwaita versions
"no-cover-if-gtk4": "os_environ.get('TOGA_GTK', '') == '4'",
"no-cover-if-gtk3": "os_environ.get('TOGA_GTK', '3') == '3'",
"no-cover-unless-plain-gtk4": (
Expand All @@ -159,6 +174,11 @@ def main(main_package_name, backend_override=None):
"os_environ.get('TOGA_GTK', '') != '4' "
"or os_environ.get('TOGA_GTKLIB', '') != 'Adw'"
),
# Windows .NET usage
"no-cover-if-netfx": "os_environ.get('TOGA_WINFORMS_USE_NETFX', '') == '1'",
"no-cover-if-netcore": (
"os_environ.get('TOGA_WINFORMS_USE_NETFX', '') != '1'"
),
},
)
cov.start()
Expand Down
5 changes: 0 additions & 5 deletions winforms/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,6 @@ WebView = "toga_winforms.widgets.webview:WebView"
MainWindow = "toga_winforms.window:MainWindow"
Window = "toga_winforms.window:Window"

[tool.distutils.bdist_wheel]
# This backend needs to be tagged `py3-none-win_arm64`. All the code in this backend is
# pure Python, *but* it contains pre-compiled binary libraries to support WebView2.
plat-name = "win_amd64"

[tool.setuptools_scm]
root = ".."

Expand Down
70 changes: 70 additions & 0 deletions winforms/src/toga_winforms/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,66 @@
import os
import platform
from pathlib import Path

import clr_loader
from pythonnet import set_runtime

try:
####################################################################################
# Toga Winforms requires the use of .NET; either .NET Framework 4.x, or .NET Core.
#
# .NET Framework 4.x is available by default on Windows 10 and 11. However, on
# Windows on ARM64, it is an x86-64 binary, so it can't be used by a native ARM64
# Python interpreter.
#
# However, it *can* be used on ARM64 if you have an x86-64 Python interpreter -
# which is what you get if you run `py install -3.13` or `py install -3.14`. This
# will apparently change in Python 3.15.
#
# Using .NET Core requires a separate install - but it will be present on a lot of
# systems.
#
# So - try to load .NET Core; if it succeeds, use it. If the load fails, fall back
# to .NET Framework. If we're on ARM64, check to see if the interpreter is running
# in emulation mode. If it is, we're OK; if we're not, stop the interpreter; the
# .NET gives instructions on how to install .NET.
#
# But: If TOGA_WINFORMS_USE_NETFX is defined in the environment, ignore .NET Core
# and prefer .NET Framework 4.x
####################################################################################
if os.environ.get("TOGA_WINFORMS_USE_NETFX", False): # pragma: no-cover-if-netcore
Comment thread
freakboy3742 marked this conversation as resolved.
Outdated
raise RuntimeError("Explicitly requesting .NET Framework 4.x")
else: # pragma: no-cover-if-netfx
# runtime.json defines the .NET version. .NET 10 is the current LTS release.
set_runtime(
clr_loader.get_coreclr(
runtime_config=Path(__file__).parent / "resources/runtime.json"
)
)

# .NET Core load succeeded
_use_dotnet_core = True
except (clr_loader.util.clr_error.ClrError, RuntimeError): # pragma: no cover
# .NET Core load failed. This whole branch is no-cover because we can't
# easily describe no-cover conditions for the failure modes.
if platform.machine() == "ARM64" and "ARM64" in platform.python_compiler():
# If you're on a native ARM64 machine running an ARM64 Python, .NET Framework
# 4.x isn't an option. On Python 3.10 and 3.11, an x86-64 Python running on
# ARM64 will return `platform.machine() == "AMD64"`, so it fails the first
# part of the test.
raise RuntimeError("""

On Windows, Toga requires .NET Core 10. Please visit:

https://dotnet.microsoft.com/en-us/download/dotnet/10.0

and install the .NET Desktop Runtime.""") from None
else:
# Either a native x86_64 machine, or an ARM64 machine with and x86_64 Python
# interpreter in emulation mode. We can use .NET Framework 4.x
_use_dotnet_core = False


import clr
import travertino

Expand All @@ -9,6 +72,13 @@
# Add a reference to the Winforms assembly
clr.AddReference("System.Windows.Forms")

# .NET Core requires some other explicit assemblies
if _use_dotnet_core: # pragma: no-cover-if-netfx
clr.AddReference("Microsoft.Win32.SystemEvents")
clr.AddReference("System.Windows.Extensions")
else: # pragma: no-cover-if-netcore
pass
Comment thread
mhsmith marked this conversation as resolved.

# Add a reference to the WindowsBase assembly. This is needed to access
# System.Windows.Threading.Dispatcher.
#
Expand Down
Loading