pywinui 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pywinui/__init__.py
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyWinUI — a small, Pythonic wrapper over PyWinRT's ``winui3`` (Windows App SDK)
|
|
3
|
+
bindings.
|
|
4
|
+
|
|
5
|
+
Goal: declare native WinUI 3 UIs the way you'd write Flet/Flutter-style Python,
|
|
6
|
+
while all the verbose WinRT glue stays in ONE place (the `_Native` layer below).
|
|
7
|
+
|
|
8
|
+
import pywinui as ui
|
|
9
|
+
|
|
10
|
+
class Demo(ui.App):
|
|
11
|
+
def build(self):
|
|
12
|
+
count = ui.TextBlock("0", font_size=32)
|
|
13
|
+
|
|
14
|
+
def bump(sender, args):
|
|
15
|
+
count.text = str(int(count.text) + 1)
|
|
16
|
+
|
|
17
|
+
return ui.Window(
|
|
18
|
+
title="PyWinUI Demo",
|
|
19
|
+
content=ui.StackPanel(
|
|
20
|
+
spacing=12, padding=24,
|
|
21
|
+
children=[
|
|
22
|
+
ui.TextBlock("Counter", font_size=20),
|
|
23
|
+
count,
|
|
24
|
+
ui.Button("Increment", on_click=bump),
|
|
25
|
+
],
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
Demo().run()
|
|
30
|
+
|
|
31
|
+
--------------------------------------------------------------------------------
|
|
32
|
+
STATUS: working proof of concept — verified end-to-end on Windows 11 against
|
|
33
|
+
winui3 3.2.1 / Python 3.13. A real window opens, native controls realize, click
|
|
34
|
+
handlers mutate them, and off-thread writes marshal back via the DispatcherQueue.
|
|
35
|
+
|
|
36
|
+
The import paths are confirmed: `winui3.microsoft.ui.xaml` is correct (the
|
|
37
|
+
PyWinRT docs' `winui3.microsoft.windows.ui.xaml` was indeed a typo).
|
|
38
|
+
|
|
39
|
+
The async bridge is verified too: `async def` handlers, awaiting WinRT async
|
|
40
|
+
operations, and off-thread writes marshalling back to the UI. No `# VERIFY`
|
|
41
|
+
tags remain — `TextBox` and `Grid` are the only controls not yet instantiated,
|
|
42
|
+
and they follow the same pattern as the verified ones.
|
|
43
|
+
--------------------------------------------------------------------------------
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
import asyncio
|
|
48
|
+
import contextlib
|
|
49
|
+
import contextvars
|
|
50
|
+
import inspect
|
|
51
|
+
import threading
|
|
52
|
+
from typing import Any, Callable, Optional
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# =============================================================================
|
|
56
|
+
# NATIVE GLUE — the only place that imports winui3.
|
|
57
|
+
# Swap/verify these against the installed packages; the rest of the file is
|
|
58
|
+
# plain Python and never mentions WinRT directly.
|
|
59
|
+
# =============================================================================
|
|
60
|
+
class _Native:
|
|
61
|
+
"""Lazy holder for winui3 modules. Imported on first use so that importing
|
|
62
|
+
`pywinui` on a non-Windows box (e.g. for tests of the pure-Python parts)
|
|
63
|
+
doesn't explode."""
|
|
64
|
+
|
|
65
|
+
_loaded = False
|
|
66
|
+
|
|
67
|
+
def load(self) -> None:
|
|
68
|
+
if self._loaded:
|
|
69
|
+
return
|
|
70
|
+
try:
|
|
71
|
+
# Import paths confirmed against winui3 3.2.1 on Windows 11.
|
|
72
|
+
from winui3.microsoft.ui.xaml import Application, Window, Thickness
|
|
73
|
+
from winui3.microsoft.ui.xaml import Visibility
|
|
74
|
+
from winui3.microsoft.ui.xaml.controls import (
|
|
75
|
+
Button, TextBlock, TextBox, StackPanel, Grid,
|
|
76
|
+
)
|
|
77
|
+
# Separate package from the Xaml one — the dispatcher lives in
|
|
78
|
+
# Microsoft.UI.Dispatching, not under Xaml.
|
|
79
|
+
from winui3.microsoft.ui.dispatching import DispatcherQueue
|
|
80
|
+
from winui3.microsoft.windows.applicationmodel.dynamicdependency import (
|
|
81
|
+
bootstrap,
|
|
82
|
+
)
|
|
83
|
+
# WinRT object-typed properties (Button.Content etc.) won't accept a
|
|
84
|
+
# bare Python str — it has to be boxed into an IInspectable first.
|
|
85
|
+
from winrt.windows.foundation import PropertyValue
|
|
86
|
+
except ImportError as exc: # give a useful message, not a raw traceback
|
|
87
|
+
raise RuntimeError(
|
|
88
|
+
"PyWinUI requires the winui3 bindings and the Windows App "
|
|
89
|
+
"Runtime.\n"
|
|
90
|
+
# ASCII only: this prints to consoles still using cp1252, where
|
|
91
|
+
# non-ASCII punctuation shows up as mojibake.
|
|
92
|
+
" 1) pip install the namespace packages (each namespace is its\n"
|
|
93
|
+
" own wheel - .Controls and .Bootstrap are NOT included by\n"
|
|
94
|
+
" their parent):\n"
|
|
95
|
+
" winui3-Microsoft.UI.Xaml\n"
|
|
96
|
+
" winui3-Microsoft.UI.Xaml.Controls\n"
|
|
97
|
+
" winui3-Microsoft.UI.Dispatching\n"
|
|
98
|
+
" winui3-Microsoft.Windows.ApplicationModel.DynamicDependency\n"
|
|
99
|
+
" winui3-Microsoft.Windows.ApplicationModel."
|
|
100
|
+
"DynamicDependency.Bootstrap\n"
|
|
101
|
+
" winrt-Windows.Foundation (boxing + event delegates)\n"
|
|
102
|
+
" 2) Install the Windows App Runtime: "
|
|
103
|
+
"https://aka.ms/windowsappsdk/runtime\n"
|
|
104
|
+
" 3) Use python.org Python, NOT the Microsoft Store build "
|
|
105
|
+
"(the Store build fails bootstrap with ERROR_NOT_SUPPORTED).\n"
|
|
106
|
+
" 4) If the cause above is 'DLL load failed ... filename or "
|
|
107
|
+
"extension is too long', your environment sits too deep: the\n"
|
|
108
|
+
" winui3 DLL names are long enough to exceed Windows'"
|
|
109
|
+
" 260-char MAX_PATH. Use a shorter venv path or enable long\n"
|
|
110
|
+
" paths (LongPathsEnabled)."
|
|
111
|
+
) from exc
|
|
112
|
+
|
|
113
|
+
# Stash the pieces the wrapper needs.
|
|
114
|
+
self.Application = Application
|
|
115
|
+
self.Window = Window
|
|
116
|
+
self.Thickness = Thickness
|
|
117
|
+
self.Visibility = Visibility
|
|
118
|
+
self.Button = Button
|
|
119
|
+
self.TextBlock = TextBlock
|
|
120
|
+
self.TextBox = TextBox
|
|
121
|
+
self.StackPanel = StackPanel
|
|
122
|
+
self.Grid = Grid
|
|
123
|
+
self.DispatcherQueue = DispatcherQueue
|
|
124
|
+
self.bootstrap = bootstrap
|
|
125
|
+
self.PropertyValue = PropertyValue
|
|
126
|
+
self._loaded = True
|
|
127
|
+
|
|
128
|
+
def box(self, value: Any):
|
|
129
|
+
"""Box a Python primitive into an IInspectable for object-typed
|
|
130
|
+
properties (``Button.Content``, ``ContentControl.Content``, ...).
|
|
131
|
+
WinRT objects and realized controls pass through untouched."""
|
|
132
|
+
if isinstance(value, str):
|
|
133
|
+
return self.PropertyValue.create_string(value)
|
|
134
|
+
if isinstance(value, bool):
|
|
135
|
+
return self.PropertyValue.create_boolean(value)
|
|
136
|
+
if isinstance(value, int):
|
|
137
|
+
return self.PropertyValue.create_int32(value)
|
|
138
|
+
if isinstance(value, float):
|
|
139
|
+
return self.PropertyValue.create_double(value)
|
|
140
|
+
return value
|
|
141
|
+
|
|
142
|
+
def thickness(self, value: Any):
|
|
143
|
+
"""Turn a Pythonic padding/margin (number or 4-tuple) into a WinRT
|
|
144
|
+
Thickness struct."""
|
|
145
|
+
if isinstance(value, (int, float)):
|
|
146
|
+
l = t = r = b = float(value)
|
|
147
|
+
else:
|
|
148
|
+
l, t, r, b = (float(v) for v in value)
|
|
149
|
+
return self.Thickness(l, t, r, b)
|
|
150
|
+
|
|
151
|
+
def visibility(self, value: bool):
|
|
152
|
+
"""Map a Pythonic bool to WinUI's Visibility enum (Visible/Collapsed —
|
|
153
|
+
note WinUI has no 'Hidden'; Collapsed removes it from layout)."""
|
|
154
|
+
return self.Visibility.VISIBLE if value else self.Visibility.COLLAPSED
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
_native = _Native()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# =============================================================================
|
|
161
|
+
# CONTEXT-MANAGER TREE BUILDING
|
|
162
|
+
# `with SomeContainer():` opens a scope; any Widget constructed inside it
|
|
163
|
+
# auto-attaches to that container. Implemented with a ContextVar (async-safe)
|
|
164
|
+
# instead of threading.local, so it survives an asyncio/dispatcher layer later.
|
|
165
|
+
# Nesting works via the token stack: each __enter__ set()s and each __exit__
|
|
166
|
+
# reset()s, so `_open_parent.get()` is always the innermost open container.
|
|
167
|
+
# =============================================================================
|
|
168
|
+
_open_parent: contextvars.ContextVar[Optional["Widget"]] = contextvars.ContextVar(
|
|
169
|
+
"pywinui_open_parent", default=None
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@contextlib.contextmanager
|
|
174
|
+
def detached():
|
|
175
|
+
"""Suppress auto-attach inside a `with` block. Widgets constructed in this
|
|
176
|
+
scope are NOT added to the enclosing container, so you can build one for
|
|
177
|
+
manual placement later:
|
|
178
|
+
|
|
179
|
+
with ui.StackPanel() as panel:
|
|
180
|
+
ui.TextBlock("in tree") # attaches
|
|
181
|
+
with ui.detached():
|
|
182
|
+
loose = ui.Button("later") # does NOT attach
|
|
183
|
+
panel.add(loose) # place it explicitly
|
|
184
|
+
"""
|
|
185
|
+
token = _open_parent.set(None)
|
|
186
|
+
try:
|
|
187
|
+
yield
|
|
188
|
+
finally:
|
|
189
|
+
_open_parent.reset(token)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# =============================================================================
|
|
193
|
+
# DISPATCHER + ASYNC BRIDGE
|
|
194
|
+
# WinUI has ONE UI thread; touching a control from another thread throws, and
|
|
195
|
+
# WinRT I/O returns IAsyncOperation objects instead of plain values. This
|
|
196
|
+
# section hides both so handlers can be ordinary `async def`:
|
|
197
|
+
# * run_on_ui(fn) -> marshal a call back onto the UI thread (any thread ok)
|
|
198
|
+
# * as_future(op) -> await a WinRT async op like a normal coroutine
|
|
199
|
+
# * async handlers -> auto-scheduled on a background loop
|
|
200
|
+
# * off-thread widget writes -> auto-marshalled (see Widget._set_native)
|
|
201
|
+
#
|
|
202
|
+
# ARCHITECTURE (v1 — simple + robust): Application.start owns the UI thread with
|
|
203
|
+
# the WinUI message pump, so we can't also run asyncio there. Instead asyncio
|
|
204
|
+
# runs on a separate daemon thread; async handlers run there, and anything that
|
|
205
|
+
# mutates UI hops back via the DispatcherQueue. The nicer alternative — one
|
|
206
|
+
# event loop *driven by* the DispatcherQueue so handlers run on the UI thread —
|
|
207
|
+
# is noted as future work; it removes the marshalling but is far more code.
|
|
208
|
+
# =============================================================================
|
|
209
|
+
class _Dispatcher:
|
|
210
|
+
def __init__(self) -> None:
|
|
211
|
+
self._queue: Any = None
|
|
212
|
+
self._ui_thread_id: Optional[int] = None
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def bound(self) -> bool:
|
|
216
|
+
return self._queue is not None
|
|
217
|
+
|
|
218
|
+
def bind_current_thread(self) -> None:
|
|
219
|
+
"""Call once from the UI thread during app startup."""
|
|
220
|
+
self._queue = _native.DispatcherQueue.get_for_current_thread()
|
|
221
|
+
self._ui_thread_id = threading.get_ident()
|
|
222
|
+
|
|
223
|
+
def on_ui_thread(self) -> bool:
|
|
224
|
+
return threading.get_ident() == self._ui_thread_id
|
|
225
|
+
|
|
226
|
+
def post(self, fn: Callable[[], Any]) -> None:
|
|
227
|
+
if self._queue is None:
|
|
228
|
+
fn() # no UI yet (e.g. building before start) — just run inline
|
|
229
|
+
return
|
|
230
|
+
self._queue.try_enqueue(fn) # a plain Python callable satisfies the delegate
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
_dispatcher = _Dispatcher()
|
|
234
|
+
_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def run_on_ui(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
|
|
238
|
+
"""Run `fn` on the UI thread. Safe from any thread."""
|
|
239
|
+
_dispatcher.post(lambda: fn(*args, **kwargs))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def as_future(winrt_async_op: Any) -> "asyncio.Future":
|
|
243
|
+
"""Adapt a WinRT IAsyncOperation / IAsyncAction into an asyncio Future.
|
|
244
|
+
|
|
245
|
+
text = await ui.as_future(FileIO.read_text_async(file))
|
|
246
|
+
|
|
247
|
+
NOTE: as of PyWinRT 3.2 the projected async types implement ``__await__``
|
|
248
|
+
already, so plain ``await op`` works and is the idiomatic path (errors
|
|
249
|
+
surface as normal Python exceptions either way). This wrapper is kept for
|
|
250
|
+
the cases where you want a real Future — to pass to ``asyncio.gather``,
|
|
251
|
+
``wait_for``, or to hold and cancel later — and as a stable seam if the
|
|
252
|
+
projection ever changes. It no longer hand-rolls the Completed delegate.
|
|
253
|
+
"""
|
|
254
|
+
return asyncio.ensure_future(winrt_async_op)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _schedule_coro(coro: Any) -> None:
|
|
258
|
+
"""Schedule an async-handler coroutine on the background loop."""
|
|
259
|
+
if _loop is None:
|
|
260
|
+
raise RuntimeError("async handler fired before the app loop started")
|
|
261
|
+
asyncio.run_coroutine_threadsafe(coro, _loop)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _wrap_async(handler: Callable[..., Any]) -> Callable[..., Any]:
|
|
265
|
+
"""If a handler is `async def`, return a sync shim that schedules it and
|
|
266
|
+
returns immediately, so the WinRT event callback doesn't block the UI."""
|
|
267
|
+
# inspect, not asyncio: asyncio.iscoroutinefunction is deprecated in 3.14.
|
|
268
|
+
if inspect.iscoroutinefunction(handler):
|
|
269
|
+
def _fire(sender: Any, args: Any, _h: Callable[..., Any] = handler) -> None:
|
|
270
|
+
_schedule_coro(_h(sender, args))
|
|
271
|
+
return _fire
|
|
272
|
+
return handler
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# =============================================================================
|
|
276
|
+
# BASE WIDGET
|
|
277
|
+
# Declaration and native realization are deliberately separate: you build a
|
|
278
|
+
# cheap Python tree first, then `_realize()` walks it and creates native
|
|
279
|
+
# objects (which must happen after bootstrap, on the UI thread).
|
|
280
|
+
# =============================================================================
|
|
281
|
+
class Widget:
|
|
282
|
+
# Attribute names that live on the Python object, never forwarded to native.
|
|
283
|
+
_INTERNAL = {"_native", "_props", "_events"}
|
|
284
|
+
|
|
285
|
+
def __init__(self, **props: Any) -> None:
|
|
286
|
+
# Set internals via object.__setattr__ so our custom __setattr__ below
|
|
287
|
+
# doesn't try to forward them to a native object that doesn't exist yet.
|
|
288
|
+
object.__setattr__(self, "_native", None)
|
|
289
|
+
# `attached` is a construct-time-only flag (NOT a live property): it
|
|
290
|
+
# controls the one-shot auto-parenting below. Consume it so it never
|
|
291
|
+
# gets forwarded to native.
|
|
292
|
+
attached = props.pop("attached", True)
|
|
293
|
+
# Split "on_*" handlers out from ordinary properties.
|
|
294
|
+
events = {k[3:]: props.pop(k) for k in list(props) if k.startswith("on_")}
|
|
295
|
+
object.__setattr__(self, "_events", events)
|
|
296
|
+
object.__setattr__(self, "_props", props)
|
|
297
|
+
|
|
298
|
+
# If we're being constructed inside a `with container:` block, attach
|
|
299
|
+
# ourselves to that container. Merely constructing the widget is the
|
|
300
|
+
# side effect that adds it to the tree (nicegui-style). `attached=False`
|
|
301
|
+
# (or a surrounding ui.detached()) opts out for manual placement.
|
|
302
|
+
if attached:
|
|
303
|
+
parent = _open_parent.get()
|
|
304
|
+
if parent is not None:
|
|
305
|
+
parent._adopt(self)
|
|
306
|
+
|
|
307
|
+
# ---- container protocol -------------------------------------------------
|
|
308
|
+
def _adopt(self, child: "Widget") -> None:
|
|
309
|
+
"""Attach a child constructed inside this widget's `with` block.
|
|
310
|
+
Leaf controls reject children; containers override this."""
|
|
311
|
+
raise TypeError(f"{type(self).__name__} cannot contain children")
|
|
312
|
+
|
|
313
|
+
def __enter__(self) -> "Widget":
|
|
314
|
+
# Push self as the current parent; remember the token so nested blocks
|
|
315
|
+
# unwind correctly on exit.
|
|
316
|
+
object.__setattr__(self, "_ctx_token", _open_parent.set(self))
|
|
317
|
+
return self
|
|
318
|
+
|
|
319
|
+
def __exit__(self, *exc: Any) -> bool:
|
|
320
|
+
token = self.__dict__.pop("_ctx_token", None)
|
|
321
|
+
if token is not None:
|
|
322
|
+
_open_parent.reset(token)
|
|
323
|
+
return False # never swallow exceptions
|
|
324
|
+
|
|
325
|
+
# ---- override points for subclasses -------------------------------------
|
|
326
|
+
def _construct(self) -> Any:
|
|
327
|
+
"""Return a freshly created native WinRT control."""
|
|
328
|
+
raise NotImplementedError
|
|
329
|
+
|
|
330
|
+
def _apply(self) -> None:
|
|
331
|
+
"""Apply queued properties to the native control. Default: forward each
|
|
332
|
+
kwarg by name, since PyWinRT already exposes properties as snake_case."""
|
|
333
|
+
for name, value in self._props.items():
|
|
334
|
+
self._set_native(name, value)
|
|
335
|
+
|
|
336
|
+
# ---- property forwarding ------------------------------------------------
|
|
337
|
+
def _set_native(self, name: str, value: Any) -> None:
|
|
338
|
+
# Map Pythonic property names/values onto the WinRT surface.
|
|
339
|
+
if name in ("padding", "margin"):
|
|
340
|
+
value = _native.thickness(value)
|
|
341
|
+
elif name == "visible":
|
|
342
|
+
name, value = "visibility", _native.visibility(value)
|
|
343
|
+
elif name == "content":
|
|
344
|
+
# Object-typed slot: primitives must be boxed, controls must not.
|
|
345
|
+
value = _native.box(value)
|
|
346
|
+
if _dispatcher.bound and not _dispatcher.on_ui_thread():
|
|
347
|
+
# Write coming from an async handler on the background loop: hop it
|
|
348
|
+
# onto the UI thread so the caller never has to think about threads.
|
|
349
|
+
_dispatcher.post(lambda: setattr(self._native, name, value))
|
|
350
|
+
else:
|
|
351
|
+
setattr(self._native, name, value)
|
|
352
|
+
|
|
353
|
+
# ---- realization machinery ----------------------------------------------
|
|
354
|
+
def _realize(self) -> Any:
|
|
355
|
+
if self._native is not None:
|
|
356
|
+
return self._native
|
|
357
|
+
self._native = self._construct()
|
|
358
|
+
self._apply()
|
|
359
|
+
for event, handler in self._events.items():
|
|
360
|
+
# PyWinRT projects WinRT events as add_<event>/remove_<event>.
|
|
361
|
+
# _wrap_async lets handlers be `async def` transparently.
|
|
362
|
+
getattr(self._native, f"add_{event}")(_wrap_async(handler))
|
|
363
|
+
return self._native
|
|
364
|
+
|
|
365
|
+
# ---- the escape hatch ---------------------------------------------------
|
|
366
|
+
@property
|
|
367
|
+
def native(self) -> Any:
|
|
368
|
+
"""The underlying WinUI 3 control — the blessed way to reach anything
|
|
369
|
+
the wrapper doesn't cover. Realizes on access (idempotent), so advanced
|
|
370
|
+
users can grab and tweak the real object even mid-build:
|
|
371
|
+
|
|
372
|
+
btn = ui.Button("Save")
|
|
373
|
+
btn.native.background = some_brush # raw WinUI, fully supported
|
|
374
|
+
"""
|
|
375
|
+
return self._realize()
|
|
376
|
+
|
|
377
|
+
# ---- Pythonic attribute access ------------------------------------------
|
|
378
|
+
# Lets you write `label.text = "hi"` before OR after realization and read
|
|
379
|
+
# `label.text` back, transparently forwarding to the native control.
|
|
380
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
381
|
+
if name in self._INTERNAL or name.startswith("_"):
|
|
382
|
+
object.__setattr__(self, name, value)
|
|
383
|
+
return
|
|
384
|
+
native = self.__dict__.get("_native")
|
|
385
|
+
if native is not None:
|
|
386
|
+
self._set_native(name, value)
|
|
387
|
+
else:
|
|
388
|
+
self._props[name] = value
|
|
389
|
+
|
|
390
|
+
def __getattr__(self, name: str) -> Any:
|
|
391
|
+
# __getattr__ only fires when normal lookup fails, so methods/internals
|
|
392
|
+
# are unaffected. Forward reads to the native control once realized,
|
|
393
|
+
# otherwise return whatever was queued.
|
|
394
|
+
native = self.__dict__.get("_native")
|
|
395
|
+
if native is not None:
|
|
396
|
+
return getattr(native, name)
|
|
397
|
+
props = self.__dict__.get("_props", {})
|
|
398
|
+
if name in props:
|
|
399
|
+
return props[name]
|
|
400
|
+
raise AttributeError(name)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# =============================================================================
|
|
404
|
+
# CONTROLS
|
|
405
|
+
# Adding a new control is the whole point of the design: subclass Widget,
|
|
406
|
+
# implement `_construct`, and (optionally) map a positional arg to a property.
|
|
407
|
+
# =============================================================================
|
|
408
|
+
class TextBlock(Widget):
|
|
409
|
+
def __init__(self, text: str = "", **props: Any) -> None:
|
|
410
|
+
props.setdefault("text", text)
|
|
411
|
+
super().__init__(**props)
|
|
412
|
+
|
|
413
|
+
def _construct(self):
|
|
414
|
+
return _native.TextBlock()
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class TextBox(Widget):
|
|
418
|
+
def __init__(self, text: str = "", **props: Any) -> None:
|
|
419
|
+
props.setdefault("text", text)
|
|
420
|
+
super().__init__(**props)
|
|
421
|
+
|
|
422
|
+
def _construct(self):
|
|
423
|
+
return _native.TextBox() # not yet exercised on Windows (same pattern)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class Button(Widget):
|
|
427
|
+
def __init__(self, label: Optional[str] = None, **props: Any) -> None:
|
|
428
|
+
if label is not None:
|
|
429
|
+
props.setdefault("content", label) # WinUI Button uses Content
|
|
430
|
+
super().__init__(**props)
|
|
431
|
+
|
|
432
|
+
def _construct(self):
|
|
433
|
+
return _native.Button()
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class _Panel(Widget):
|
|
437
|
+
"""Base for layout containers that hold a list of child Widgets."""
|
|
438
|
+
|
|
439
|
+
def __init__(self, children: Optional[list["Widget"]] = None, **props: Any) -> None:
|
|
440
|
+
super().__init__(**props)
|
|
441
|
+
object.__setattr__(self, "_child_widgets", children or [])
|
|
442
|
+
|
|
443
|
+
def _adopt(self, child: "Widget") -> None:
|
|
444
|
+
# Panels hold many children; `with`-adopted ones append to whatever the
|
|
445
|
+
# `children=[...]` kwarg already seeded, so the two styles compose.
|
|
446
|
+
self._child_widgets.append(child)
|
|
447
|
+
|
|
448
|
+
def add(self, *children: "Widget") -> "_Panel":
|
|
449
|
+
"""Attach children manually — e.g. ones built inside ui.detached().
|
|
450
|
+
Returns self for chaining."""
|
|
451
|
+
self._child_widgets.extend(children)
|
|
452
|
+
return self
|
|
453
|
+
|
|
454
|
+
def _apply(self) -> None:
|
|
455
|
+
super()._apply() # spacing, padding, orientation, ...
|
|
456
|
+
container = self._native.children
|
|
457
|
+
for child in self._child_widgets:
|
|
458
|
+
container.append(child._realize())
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class StackPanel(_Panel):
|
|
462
|
+
def _construct(self):
|
|
463
|
+
return _native.StackPanel()
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class Grid(_Panel):
|
|
467
|
+
# Row/column definitions and Grid.Row/Grid.Column attached properties are
|
|
468
|
+
# left as an exercise — attached properties need their own helper. Kept here
|
|
469
|
+
# so the container hierarchy is visible.
|
|
470
|
+
def _construct(self):
|
|
471
|
+
return _native.Grid() # not yet exercised on Windows (same pattern)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# =============================================================================
|
|
475
|
+
# WINDOW + APP LIFECYCLE
|
|
476
|
+
# This is the least-trodden part of the binding; treat _on_start as the piece
|
|
477
|
+
# most likely to need adjustment against the real packages.
|
|
478
|
+
# =============================================================================
|
|
479
|
+
class Window(Widget):
|
|
480
|
+
def __init__(self, content: Optional[Widget] = None, **props: Any) -> None:
|
|
481
|
+
super().__init__(**props)
|
|
482
|
+
object.__setattr__(self, "_content", content)
|
|
483
|
+
|
|
484
|
+
def _adopt(self, child: "Widget") -> None:
|
|
485
|
+
# A Window has a single content slot, so a second child is an error —
|
|
486
|
+
# the "slot" semantics (single vs list) are what let one protocol serve
|
|
487
|
+
# both container kinds.
|
|
488
|
+
if self._content is not None:
|
|
489
|
+
raise TypeError("Window already has content; it holds a single child")
|
|
490
|
+
object.__setattr__(self, "_content", child)
|
|
491
|
+
|
|
492
|
+
def _construct(self):
|
|
493
|
+
return _native.Window()
|
|
494
|
+
|
|
495
|
+
def _apply(self) -> None:
|
|
496
|
+
super()._apply() # title, etc.
|
|
497
|
+
if self._content is not None:
|
|
498
|
+
self._native.content = self._content._realize()
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class App:
|
|
502
|
+
"""Subclass and implement `build()` returning a Window, then call `run()`."""
|
|
503
|
+
|
|
504
|
+
def build(self) -> Window:
|
|
505
|
+
raise NotImplementedError("App subclasses must implement build() -> Window")
|
|
506
|
+
|
|
507
|
+
def run(self) -> None:
|
|
508
|
+
_native.load()
|
|
509
|
+
self._start_background_loop()
|
|
510
|
+
boot = _native.bootstrap
|
|
511
|
+
# Start the Windows App Runtime; prompt the user to install it if missing.
|
|
512
|
+
opts = boot.InitializeOptions.ON_NO_MATCH_SHOW_UI
|
|
513
|
+
try:
|
|
514
|
+
with boot.initialize(options=opts):
|
|
515
|
+
# Application.start blocks and pumps the UI message loop until exit.
|
|
516
|
+
# The callback runs on the freshly created UI thread.
|
|
517
|
+
_native.Application.start(self._on_start)
|
|
518
|
+
finally:
|
|
519
|
+
self._stop_background_loop()
|
|
520
|
+
|
|
521
|
+
def _on_start(self, _params: Any) -> None:
|
|
522
|
+
# We're on the UI thread now: capture it + the dispatcher queue so
|
|
523
|
+
# off-thread widget writes and run_on_ui() can marshal back here.
|
|
524
|
+
_dispatcher.bind_current_thread()
|
|
525
|
+
# Keep the window on the app: it's the root of the live tree, and both
|
|
526
|
+
# user code and tooling need a handle on it after startup.
|
|
527
|
+
self.window = self.build()
|
|
528
|
+
self.window._realize()
|
|
529
|
+
self.window._native.activate()
|
|
530
|
+
|
|
531
|
+
# -- background asyncio loop (see DISPATCHER + ASYNC BRIDGE) ---------------
|
|
532
|
+
def _start_background_loop(self) -> None:
|
|
533
|
+
global _loop
|
|
534
|
+
_loop = asyncio.new_event_loop()
|
|
535
|
+
self._loop_thread = threading.Thread(
|
|
536
|
+
target=_loop.run_forever, name="pywinui-async", daemon=True
|
|
537
|
+
)
|
|
538
|
+
self._loop_thread.start()
|
|
539
|
+
|
|
540
|
+
def _stop_background_loop(self) -> None:
|
|
541
|
+
global _loop
|
|
542
|
+
if _loop is not None:
|
|
543
|
+
_loop.call_soon_threadsafe(_loop.stop)
|
|
544
|
+
_loop = None
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
__all__ = [
|
|
548
|
+
"App", "Window",
|
|
549
|
+
"TextBlock", "TextBox", "Button",
|
|
550
|
+
"StackPanel", "Grid",
|
|
551
|
+
"Widget",
|
|
552
|
+
"detached", "run_on_ui", "as_future",
|
|
553
|
+
]
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pywinui
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Build native WinUI 3 (Windows App SDK) desktop apps in idiomatic Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/israel-dryer/pywinui
|
|
6
|
+
Project-URL: Repository, https://github.com/israel-dryer/pywinui
|
|
7
|
+
Project-URL: Issues, https://github.com/israel-dryer/pywinui/issues
|
|
8
|
+
Author-email: Israel Dryer <israel.dryer@gmail.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Israel Dryer
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: desktop,gui,windows,windows-app-sdk,winrt,winui,winui3
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 10
|
|
37
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
44
|
+
Classifier: Typing :: Typed
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
|
+
Requires-Dist: winrt-windows-foundation<4,>=3.2; sys_platform == 'win32'
|
|
47
|
+
Requires-Dist: winui3-microsoft-ui-dispatching<4,>=3.2; sys_platform == 'win32'
|
|
48
|
+
Requires-Dist: winui3-microsoft-ui-xaml-controls<4,>=3.2; sys_platform == 'win32'
|
|
49
|
+
Requires-Dist: winui3-microsoft-ui-xaml<4,>=3.2; sys_platform == 'win32'
|
|
50
|
+
Requires-Dist: winui3-microsoft-windows-applicationmodel-dynamicdependency-bootstrap<4,>=3.2; sys_platform == 'win32'
|
|
51
|
+
Requires-Dist: winui3-microsoft-windows-applicationmodel-dynamicdependency<4,>=3.2; sys_platform == 'win32'
|
|
52
|
+
Provides-Extra: dev
|
|
53
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
54
|
+
Provides-Extra: examples
|
|
55
|
+
Requires-Dist: winrt-windows-storage<4,>=3.2; (sys_platform == 'win32') and extra == 'examples'
|
|
56
|
+
Description-Content-Type: text/markdown
|
|
57
|
+
|
|
58
|
+
# PyWinUI
|
|
59
|
+
|
|
60
|
+
Build **native WinUI 3** (Windows App SDK) desktop applications in idiomatic Python.
|
|
61
|
+
|
|
62
|
+
Not a themed look-alike and not a reimplementation — these are real WinUI 3
|
|
63
|
+
controls, wrapped so that you never have to touch WinRT unless you want to.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import pywinui as ui
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CounterApp(ui.App):
|
|
70
|
+
def build(self):
|
|
71
|
+
count = ui.TextBlock("0", font_size=32)
|
|
72
|
+
|
|
73
|
+
def bump(sender, args):
|
|
74
|
+
count.text = str(int(count.text) + 1)
|
|
75
|
+
|
|
76
|
+
return ui.Window(
|
|
77
|
+
title="PyWinUI Counter",
|
|
78
|
+
content=ui.StackPanel(
|
|
79
|
+
spacing=12, padding=24,
|
|
80
|
+
children=[
|
|
81
|
+
ui.TextBlock("Counter", font_size=20),
|
|
82
|
+
count,
|
|
83
|
+
ui.Button("Increment", on_click=bump),
|
|
84
|
+
],
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
CounterApp().run()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
> **Status: early alpha.** The architecture is verified end-to-end against the
|
|
93
|
+
> real bindings on Windows 11, but only five controls are wrapped so far. The API
|
|
94
|
+
> may change. See [Current scope](#current-scope) before depending on it.
|
|
95
|
+
|
|
96
|
+
## Why now
|
|
97
|
+
|
|
98
|
+
The WinUI 3 projections for Python ([PyWinRT](https://github.com/pywinrt/pywinrt))
|
|
99
|
+
only landed in March 2025. Before that, a Python WinUI 3 app meant hand-rolling
|
|
100
|
+
raw WinRT. The bindings now exist and work; PyWinUI is the ergonomics layer on
|
|
101
|
+
top of them.
|
|
102
|
+
|
|
103
|
+
## Requirements
|
|
104
|
+
|
|
105
|
+
- **Windows 10/11** with the [Windows App Runtime](https://aka.ms/windowsappsdk/runtime)
|
|
106
|
+
- **python.org Python 3.10+** — *not* the Microsoft Store build, which is a
|
|
107
|
+
packaged app and fails the runtime bootstrap with `ERROR_NOT_SUPPORTED`
|
|
108
|
+
|
|
109
|
+
## Install
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
pip install pywinui
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The `winui3-*` dependencies are Windows-only and install automatically there.
|
|
116
|
+
On other platforms the package still installs and imports (the native layer is
|
|
117
|
+
loaded lazily), so the test suite and editor tooling work anywhere — but
|
|
118
|
+
anything that realizes a control needs Windows.
|
|
119
|
+
|
|
120
|
+
## Two ways to build a tree
|
|
121
|
+
|
|
122
|
+
Both are first-class and they compose.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# Flutter style — the tree is a value. Best for reusable components.
|
|
126
|
+
ui.StackPanel(children=[ui.TextBlock("a"), ui.TextBlock("b")])
|
|
127
|
+
|
|
128
|
+
# With style — handles loops, conditionals and local references far better.
|
|
129
|
+
with ui.StackPanel() as panel:
|
|
130
|
+
ui.TextBlock("Items")
|
|
131
|
+
for name in items:
|
|
132
|
+
ui.Button(name, on_click=make_handler(name))
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Async without the ceremony
|
|
136
|
+
|
|
137
|
+
Handlers may be `async def`. They're scheduled off the UI thread automatically,
|
|
138
|
+
WinRT async operations are awaited like any coroutine, and writes back to
|
|
139
|
+
widgets are marshalled onto the UI thread for you.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
async def read_file(sender, args):
|
|
143
|
+
status.text = "Reading..."
|
|
144
|
+
file = await StorageFile.get_file_from_path_async(path)
|
|
145
|
+
text = await FileIO.read_text_async(file) # real WinRT I/O
|
|
146
|
+
status.text = f"{len(text)} chars" # marshalled back to the UI
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Failures arrive as ordinary Python exceptions — a missing path raises
|
|
150
|
+
`FileNotFoundError`.
|
|
151
|
+
|
|
152
|
+
## The escape hatch
|
|
153
|
+
|
|
154
|
+
The curated surface covers the common path with type hints, validation and value
|
|
155
|
+
conversions. Anything not wrapped still forwards by name, and `widget.native`
|
|
156
|
+
gives you the raw WinUI control:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
btn = ui.Button("Save")
|
|
160
|
+
btn.native.background = some_brush # raw WinUI, fully supported
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Current scope
|
|
164
|
+
|
|
165
|
+
| Working | Not yet |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `TextBlock`, `TextBox`, `Button`, `StackPanel`, `Grid`, `Window` | The other ~100 WinUI controls |
|
|
168
|
+
| Both tree-building styles, attach/detach | Data binding, styles, resource dictionaries |
|
|
169
|
+
| `async def` handlers, awaiting WinRT ops | Control templates, virtualized lists |
|
|
170
|
+
| Off-thread writes auto-marshalled | Off-thread *reads* (wrap in `run_on_ui`) |
|
|
171
|
+
| `padding`/`margin`, `visible`, content boxing | `Grid.Row`/`Grid.Column` attached properties |
|
|
172
|
+
| Running from a Python environment | Packaging a double-clickable app (MSIX) |
|
|
173
|
+
|
|
174
|
+
**Packaging is unproven.** Shipping an app to end users means bundling Python and
|
|
175
|
+
the Windows App Runtime, likely as MSIX, and that path has not been prototyped.
|
|
176
|
+
Today this is a library for developers who already have Python installed.
|
|
177
|
+
|
|
178
|
+
## Troubleshooting
|
|
179
|
+
|
|
180
|
+
**`DLL load failed ... The filename or extension is too long`** — your virtual
|
|
181
|
+
environment path is too deep. The winui3 extension modules have very long file
|
|
182
|
+
names (`_winui3_microsoft_windows_applicationmodel_dynamicdependency_bootstrap`),
|
|
183
|
+
and a nested venv can push them past Windows' 260-character `MAX_PATH`. Use a
|
|
184
|
+
shorter path, or [enable long paths](https://learn.microsoft.com/windows/win32/fileio/maximum-file-path-limitation).
|
|
185
|
+
|
|
186
|
+
**`ERROR_NOT_SUPPORTED` during bootstrap** — you're on the Microsoft Store build
|
|
187
|
+
of Python, which is itself a packaged app. Use the python.org installer.
|
|
188
|
+
|
|
189
|
+
## Examples
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
pip install -e ".[examples,dev]"
|
|
193
|
+
python examples/counter.py
|
|
194
|
+
python examples/async_file_read.py
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Tests
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
pytest
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The suite runs **anywhere** — no Windows required. It swaps in a fake native
|
|
204
|
+
layer to lock down the tree model, both building styles, attach/detach, property
|
|
205
|
+
forwarding, value conversions and thread marshalling. What it deliberately
|
|
206
|
+
cannot cover is the binding boundary itself; that is verified by running the
|
|
207
|
+
examples on Windows.
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
pywinui/__init__.py,sha256=fUhGBee1BVCVBxd-4acHEbghaUPlOWU3LlPqvrNFY38,23678
|
|
2
|
+
pywinui-0.1.0a1.dist-info/METADATA,sha256=CfNZbSV3TTM6nVSG7LtOjhwo3z5Eip9VPZCth5mvRD4,8378
|
|
3
|
+
pywinui-0.1.0a1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
4
|
+
pywinui-0.1.0a1.dist-info/licenses/LICENSE,sha256=RU7fnTshexL9FueMYVgwu_WNKsNlw1hP0roNhPZaqS8,1069
|
|
5
|
+
pywinui-0.1.0a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Israel Dryer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|