This is a highly experimental and early stage extension to pymsbuild. It uses the Microsoft Windows App SDK to build GUI apps based on XAML.
The sample projects in testdata/app1 and testdata/BasicPhotoViewer are currently the best starting point.
Good luck!
(Expressions of interest are welcome right now, but not contributions.)
In your _msbuild.py
, import WinUIExe
, XamlApp
and XamlPage
from
pymsbuild_winui
(using import *
is okay).
The WinUIExe
element will take the XamlApp
and one or more
XamlPage
s and generate an executable file along with a lot of
support files. The package this element goes into should contain the
Python sources and any other content, but only XAML items should be in
the WinUIExe
itself.
The name of the WinUIExe
must match the namespaces used in the XAML
files x:Class
attributes, and must also be an importable Python
module that will contain the implementation of each page.
from pymsbuild import *
from pymsbuild_winui import *
METADATA = {...}
PACKAGE = Package(
'PhotoViewerApp',
PyFile("PhotoViewer.py"),
PyFile("image_info.py"),
WinUIExe(
"PhotoViewer",
XamlApp("app.xaml"),
XamlPage("MainWindow.xaml"),
IncludePythonRuntime=True, # these both default to True, they
IncludeAppRuntime=True, # are just here for the example
),
)
def init_PACKAGE(tag):
# WinUIExe needs to be passed the wheel tag to complete setup
PACKAGE.find("PhotoViewer").init_PACKAGE(tag)
Build the module using pymsbuild
as normal. The created executable will
be put in a directory following the main package name, containing an
executable following the WinUIExe
name. You can also produce an sdist
or wheel, which will be transferrable to other machines. (Note that the
wheel will be quite large, due to containing all the files needed to run
the application.)
$> python -m pymsbuild
< lots of build output >
$> .\PhotoViewerApp\PhotoViewer.exe
In the main Python module (PhotoViewer.py
in the above example), each
XAML page must have a class with matching name and the initializer
shown below.
class MainWindow:
def __init__(self, view, viewmodels):
...
The view
argument is a reference to the XAML instance. You should
keep this around as self.view
to use it later.
The viewmodels
argument has each defined viewmodel as an attribute.
You should now update the view.models
dict to map your model types to
their viewmodels. This will enable view.wrap()
and view.unwrap()
to
work.
self.view.models[image_info.ImageInfo] = viewmodels.ImageInfo
self.view.models[image_info.ImagesRepository] = viewmodels.ImageRepository
The view
object also contains any properties that were defined in the
XAML. Properties and viewmodels use a private XML namespace that is
processed at build time.
...
xmlns:py="http://schemas.stevedower.id.au/pymsbuild/winui"
...
<py:ViewModel Name="ImageInfo">
<py:Property Name="Name" Type="str" />
<py:Property Name="Path" Type="str" />
</py:ViewModel>
<py:ViewModel Name="ImageRepository">
<py:Property Name="Images" Type="list[PhotoViewer.ImageInfo]" />
</py:ViewModel>
<py:Property Name="ImagesRepository" Type="PhotoViewer.ImageRepository" />
Property types are very limited, try to stick to primitives or other viewmodels. Notice that the viewmodel type must include the namespace, which is implicitly added to the name specified when defining it.
The list[]
property type defines a readonly property containing an
observable list that can contain any object type. The type specifier is
for self-documentation only, it is never verified. You cannot assign to
list properties, but can use their append
, clear
, extend
,
insert
and replace_all
methods to modify the contents. Note that
each update to an observable list may trigger UI updates if it has been
bound, and so replace_all
is recommended for batching updates
together.
# Python list of Python objects
images = [ImageInfo(p.name, p) for p in paths]
# Update the viewmodel with wrapped instances
viewmodel.Images.replace_all(view.wrap(i) for i in images)
Event handlers for common types will be automatically detected from your XAML. However, it is also possible to explicitly define them in case of needing to override the sender or argument type.
<!-- Not required for these particular events, but just syntax examples! -->
<py:EventHandler Name="ImageClick" Sender="object" />
<py:EventHandler Name="OnElementPointerEntered" EventArgs="Microsoft.UI.Xaml.Input.PointerRoutedEventArgs" />
Arguments are passed through with exactly the type they are defined as,
which mean mean sender
will need to be down-cast in order to access
its members. The .as_()
function takes a fully qualified name as a
string, and will raise if the conversion fails. The e
argument will
already be usable as the type specified in the EventArgs
attribute.
Event handlers found automatically will pass in the type they were
defined on.
def ImageClick(self, sender, e):
sender = sender.as_("Microsoft.UI.Xaml.Controls.Button")
...
def OnElementPointerEntered(self, sender, e):
...
Finally, the view
object contains any controls that were named with
x:Name
in the XAML. In general, it is best to avoid working with
controls directly, and instead try to use XAML bindings ({x:bind}
) to
properties and viewmodels. All properties and viewmodels provide update
notifications, and so Text="{x:bind Message,Mode=OneWay}"
is more efficient
than writing self.view.StatusControl.Text = Message
.