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

Add a WinUI3 backend #2574

Open
jhi2 opened this issue May 16, 2024 · 10 comments
Open

Add a WinUI3 backend #2574

jhi2 opened this issue May 16, 2024 · 10 comments
Labels
enhancement New features, or improvements to existing features. windows The issue relates to Microsoft Windows support.

Comments

@jhi2
Copy link

jhi2 commented May 16, 2024

(Ticket content has been edited significantly from the original to provide better guidance on the feature request)

What is the problem or limitation you are having?

Toga's look and feel on Windows currently uses Winforms, which doesn't reflect the current look and feel of Windows apps.

Describe the solution you'd like

We should add a WinUI3 backend.

Describe alternatives you've considered

Continue to live with Winforms.

Additional context

The prerequisite for this backend is the development of Python bindings for WinUI3.

@jhi2 jhi2 added the enhancement New features, or improvements to existing features. label May 16, 2024
@freakboy3742 freakboy3742 changed the title Does not look native on Windows 11 Add a WinUI3 backend May 18, 2024
@freakboy3742
Copy link
Member

Thanks for the suggestion - we'd love to add a WinUI3 backend; the problem is that there are no Python bindings for WinUI3. Once those bindings exist and are stable, a WinUI3 backend for Toga can be developed.

@freakboy3742 freakboy3742 added the windows The issue relates to Microsoft Windows support. label May 18, 2024
@jhi2
Copy link
Author

jhi2 commented Jun 2, 2024

Maybe I could help make one

@JPHutchins
Copy link

JPHutchins commented Jul 8, 2024

Hi @jhi2, I am a C programmer and would be happy to help. In this case, I think that the ctypes + DLL approach taken by win32more makes sense, at least to get an MVP. Generally, I am having a hard time understanding that repo, it is very complex. It sorta looks like a code generation tool PLUS some generated code, which makes usage confusing. Plus, organizing all these namespaces by way of directories is perhaps nice for repo organization but it may be not so great for Python import initialization. I have concerns about the "DLL function decorator". Perhaps win32more can be used to generate only the bindings that a Toga backend would need?

Happy to help with testing or other tasks.

TBH, anything other than a Microsoft supported solution will be a bit of a hack. MS probably doesn't have the money or talent for this 🙄.

@JPHutchins
Copy link

JPHutchins commented Jul 8, 2024

To see how simple the ctypes APIs can be, try this example:

import ctypes as c

user32 = c.WinDLL("user32.dll")

user32.MessageBoxW(0, "Hello World", "Hello", 1)

Where the documentation is from here: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxw

The tedious bit is having nice wrappers with typing.

I am looking into the winrt package collection: https://pypi.org/search/?q=winrt&page=1

So far I have not been able to reproduce a simpler version of what win32more is doing. I am able to import an XAML Application class but cannot create the application.

@JPHutchins
Copy link

The win32more interface to Application seems different than WinRT: https://github.com/ynkdir/py-win32more/blob/87f331b014fd1cda3ede67c4cf10715acafe575a/win32more/Microsoft/UI/Xaml/__init__.py#L67-L130

Compare to the interface generated by WinRT:

@typing.final
class Application_Static(type):
    @typing.overload
    def load_component(cls, component: typing.Optional[winrt.system.Object], resource_locator: typing.Optional[windows_foundation.Uri], /) -> None: ...
    @typing.overload
    def load_component(cls, component: typing.Optional[winrt.system.Object], resource_locator: typing.Optional[windows_foundation.Uri], component_resource_location: microsoft_ui_xaml_controls_primitives.ComponentResourceLocation, /) -> None: ...
    def start(cls, callback: typing.Optional[ApplicationInitializationCallback], /) -> None: ...
    @_property
    def current(cls) -> typing.Optional[Application]: ...

@typing.final
class Application(winrt.system.Object, metaclass=Application_Static):
    @staticmethod
    def _from(obj: winrt.system.Object, /) -> Application: ...
    def __new__(cls: typing.Type[Application]) -> Application: ...
    def exit(self) -> None: ...
    def add_unhandled_exception(self, handler: typing.Optional[UnhandledExceptionEventHandler], /) -> windows_foundation.EventRegistrationToken: ...
    def remove_unhandled_exception(self, token: windows_foundation.EventRegistrationToken, /) -> None: ...
    def add_resource_manager_requested(self, handler: windows_foundation.TypedEventHandler[winrt.system.Object, ResourceManagerRequestedEventArgs], /) -> windows_foundation.EventRegistrationToken: ...
    def remove_resource_manager_requested(self, token: windows_foundation.EventRegistrationToken, /) -> None: ...
    @_property
    def resources(self) -> typing.Optional[ResourceDictionary]: ...
    @resources.setter
    def resources(self, value: typing.Optional[ResourceDictionary]) -> None: ...
    @_property
    def requested_theme(self) -> ApplicationTheme: ...
    @requested_theme.setter
    def requested_theme(self, value: ApplicationTheme) -> None: ...
    @_property
    def high_contrast_adjustment(self) -> ApplicationHighContrastAdjustment: ...
    @high_contrast_adjustment.setter
    def high_contrast_adjustment(self, value: ApplicationHighContrastAdjustment) -> None: ...
    @_property
    def focus_visual_kind(self) -> FocusVisualKind: ...
    @focus_visual_kind.setter
    def focus_visual_kind(self, value: FocusVisualKind) -> None: ...
    @_property
    def debug_settings(self) -> typing.Optional[DebugSettings]: ...
    @_property
    def dispatcher_shutdown_mode(self) -> DispatcherShutdownMode: ...
    @dispatcher_shutdown_mode.setter
    def dispatcher_shutdown_mode(self, value: DispatcherShutdownMode) -> None: ...

Here's the C# doc: https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.application?view=windows-app-sdk-1.5

@freakboy3742
Copy link
Member

If you're calling APIs in user32, you're not calling WinUI3 API; you're calling the 1995 era win32 API. It's also available using the win32api package, which is mostly a set of convenience wrappers around the win32 API.

It may be possible to use ctypes to call WinUi3 libraries, but the API calls and libraries will be different.

@JPHutchins
Copy link

It may be possible to use ctypes to call WinUi3 libraries, but the API calls and libraries will be different.

Yes! The intention is to show how easy it is to make calls to any DLL from python.

Presently I'm trying to track down how win32more is hooking into the new APIs. This decorator is on Application::Start:

https://github.com/ynkdir/py-win32more/blob/87f331b014fd1cda3ede67c4cf10715acafe575a/win32more/_winrt.py#L710-L724

It's getting the "Start" function from this:

https://github.com/ynkdir/py-win32more/blob/87f331b014fd1cda3ede67c4cf10715acafe575a/win32more/_winrt.py#L786-L793

Seems like the magic is in the RoGetActivationFactory, which I've not heard of before: https://learn.microsoft.com/en-us/windows/win32/api/roapi/nf-roapi-rogetactivationfactory

@JPHutchins
Copy link

xaml = ctypes.WinDLL("windows.ui.xaml.dll")

Is actually loading something... not sure what - can't find any function pointers.

@freakboy3742
Copy link
Member

If you're digging into this, I'd pay particular attention to this comment on the WinUI3 ticket I've referenced earlier in this discussion. @zooba is a Microsoft employee and member of the CPython core team; his offer around co-developing bindings should not be ignored or taken lightly.

@zooba
Copy link

zooba commented Jul 8, 2024

Yeah, this is a really deep hole to dive into :) I was lucky to spend my intern project at MSFT (in 2011) working on this stuff while the APIs were being developed, and I don't think any of the low-level intro material we had ever made it public.

Basically, all the WinRT APIs are approx. COM, which means you don't load them as normal C APIs. The RoGetActivationFactory1 returns an interface that has function pointers to functions that will create instances of particular objects. Those then have interfaces to access and use those objects (as well as reference counting). It's a very powerful model, particularly for forwards and backwards binary compatibility - something of a holy grail in OS design - but it takes a bit to get it all into your head.

WinUI3 is based around these same APIs, but are registered somewhat differently. I haven't dug too far into all the details, but it's more convenient (and more wasteful of memory and disk space) than the original Windows 8 UWP APIs.

But having now implemented 4 different approaches of accessing it from Python, the one I think is best is to just generate C++ code to do all the GUI side and have it call back into Python. I've implemented something along these lines here2. It was pretty easy to translate some existing C# samples into Python, but I haven't really tried it at scale (though it already scales so much better than previous efforts that I'm sure it'd be fine). There might be something of value in there for you.

Footnotes

  1. IIRC, "RO" stood for Runtime Object. The equivalent COM APIs start with "CO".

  2. Based on my pymsbuild build backend, since building is an unavoidable part of code generation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New features, or improvements to existing features. windows The issue relates to Microsoft Windows support.
Projects
None yet
Development

No branches or pull requests

4 participants