A resume of what you need to learn to migrate from a Win32 app to a WinUI app as a C++ developer
Introduction
This article targets the hardcore Win32 C++ programmer that wants to use WinUI3. This is the sequel to Convert Win32 to WinUI3 project which you can use to convert an existing VCXPROJ file to a WinUI3 target or if you want to create apps that run on both Windows < 10 and Windows >= 10. This article focuses on the differences between Win32 programming and WinUI3 programming and helps the C++ programmer understand the concepts.
WinUI3 is Windows 10/11 interface system. This can be combined with full Win32 API so you don't miss anything in functionality. It features a very big range of new flexible controls.
RC vs XAML
Instead of building your dialogs with RC files, you do that in XML (Android programmers will see similarities). This has the following benefits:
- Hierarchical view of the elements like HTML
- Many layout availabilities
- Many pages can be built within one XML file
- Runtime properties can be set in the XML
- Data binding allows your functions to be linked to the XAML controls automatically
So, for example, while you'd have a YES/NO dialog in RC that way:
DIALOG_ASK DIALOGEX 0, 0, 409, 262
STYLE DS_SETFONT | DS_FIXEDSYS |z DS_CENTER | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
CAPTION "Ask me"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "", 101, "RichEdit20W", ES_MULTILINE | ES_AUTOHSCROLL |
ES_WANTRETURN | WS_BORDER | WS_TABSTOP, 0, 0, 309, 81
DEFPUSHBUTTON "OK", IDOK, 198, 90, 50, 14
PUSHBUTTON "Cancel", IDCANCEL, 252, 90, 50, 14
END
You could have this ContentDialog
:
<ContentDialog x:Name="Input1_AskChannel" IsPrimaryButtonEnabled="True"
PrimaryButtonText="OK" SecondaryButtonText="Cancel" IsSecondaryButtonEnabled="True"
PrimaryButtonClick="Input1_AskChannelDone">
<StackPanel Orientation="Vertical">
<InfoBar Name="Input2_AskChannel" IsOpen="True" Severity="Informational"
IsIconVisible="False" IsClosable="False" Margin="10,0,0,10" />
<TextBox Name="Input3_AskChannel" />
</StackPanel>
</ContentDialog>
The XAML interpreter parses the XML file and creates code which calls your callbacks when needed, (for example, in this case, when the Primary Button is clicked, the function Input1_AskChannelDone
is called) based on the type you have set in the IDL file. Each control may have a Name
and all containers have a FindName()
function that will return an IInspectable
which you can cast with as<>()
to the appropriate item.
Namespaces
Controls are under winrt::Microsoft::UI::Xaml::Controls
. Some basic IInspectable
objects are members of winrt::Windows
, however there's a collision sometimes between winrt::Windows
with older UWP code. You should always use winrt::Microsoft
. Your own code is under winrt::YourProjectName
.
IInspectable vs COM
All is COM-based, actually. Instead of CComPtr<IUnknown>
, you have the IInspectable
, which has the as()
and try_as()
member to switch to another interface (like QueryInterface
).
All objects are built upon it. Visible objects (controls) are build on FrameworkElement
which as I said has methods to search and manipulate its "items". You have also box_value
and unbox_value
to put scalars and some arrays into an IInspectable
.
Window / Page / Dialog
All these are containers that can contain other elements. A winrt::Microsoft::UI::Xaml::Controls::Window
creates a new Window. A Page
is content you can display within a Frame
and a ContentDialog
can be displayed when called by code.
Files
A file set is basically an IDL file, a XAML file and a C++ H/CPP class. The IDL file is required so your window's methods are registered via COM's Type Library mechanism and become visible to the WinRT Stuff. The XAML file describes the interface and the C++ code has getters/setters for the properties and your code. Each object doesn't directly see the 'h' file (as we'd do in C++) because it actually gets wrapped by COM. For example, at a file "Item.h":
namespace winrt::Project::implementation
{
struct Item : ItemT<Item>
{
int h = 0;
}
}
If you include this Item.h in another code and you get a pointer to an Item
, this pointer won't be able to see the 'h' member because it is a COM wrapper over a type library. You have also to put it in the idl:
namespace Project
{
[default_interface]
runtimeclass Item
{
Int32 h;
}
}
And then the item.h becomes:
namespace winrt::Project::implementation
{
struct Item : ItemT<Item>
{
int _h = 0; int h() { return _h;} void h(int j) { _h = j;};
}
}
StackPanel / Grid / Canvas / RelativePanel / ViewBox
These controls can group other controls. Stackpanel
puts them one after another (horz or vertical), Canvas
uses absolute position, RelativePanel
uses relative position, ViewBox
scales its content and Grid
creates a Grid
. Many containers want only one control (for example, a Page
), so if you are to put more controls inside a Page
, you will pick one of these panels.
Callbacks
So when a button is clicked, for example, your function is called. This function has always the same signature:
void fn(const IInspectable& sender,const IInspectable& i2);
The first argument is the control that generates the message, so you can call as<Button>()
, for example, if the click is from a Button
. The second parameter carries information about the click and its type depends on the event, for example, the Button's Click generates a "RoutedEventArgs
" second parameter (which you can either put in the signature directly or cast with as<>()
).
Resources
- You have a "resw" file which can contain translatable
string
s and stuff. - You can embed "
resource
" items within the XAML to be reused later.
HWND
Windows are still HWND
s so you can get a native handle:
auto n = window.as<::IWindowNative>();
if (n)
{ HWND hh;
n->get_WindowHandle(&hh);
}
You can subclass the HWND
and send messages normally.
Threads
You can't interact from worker threads with the Window
, so you can force execution on main thread:
window.DispatcherQueue().TryEnqueue([&]()
{
...
});
Data Binding
This is one of the most core features in WinUI. Item
containers like ListView
, GridView
, TreeView
, etc. does not work with plain "Strings
" like the normal Win32 ListView
which would use LVM_INSERTITEM
and display string
s. WinUI containers can contain anything inside, not just text. Additionally, items may have different data representations. For example, one element of a TreeView
item may be a StackPanel
with a button inside, where another TreeView
element may be an image.
Therefore, the procedure to work with these containers is as follows (I will use, for reference, a ListView
, but other containers like Tree
and Grid
are similar):
- Create a set of IDL, H, CPP of a class that would describe your item, along with setters and getters.
For example, if I want to represent a Person
:
namespace MyApp
{
[default_interface]
runtimeclass Item : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
{
String LastName;
String FirstName;
}
}
namespace winrt::MyApp::implementation
{
struct Item : ItemT<Item>
{
std::wstring _ln,_fn;
winrt::hstring LastName()
{
return _ln;
}
winrt::hstring FirstName()
{
return _fn;
}
void LastName(winrt::hstring s)
{
_ln = s.c_str();
}
void FirstName(winrt::hstring s)
{
_fn = s.c_str();
}
}
}
I would create an IDL-based class as normal, exposing setters/getters.
- Define in XAML a
<DataTemplate>
within a resource (for example, under <Page.Resources>
which describes the format for the item and the functions that will be used to.
<DataTemplate x:Key="Template1" x:DataType="local:Item">
<StackPanel>
<TextBlock Name="ln" Text="{x:Bind LastName,Mode=OneWay}" />
<TextBlock Name="fn" Text="{x:Bind FirstName,Mode=OneWay}" />
</Stackpanel>
</DataTemplate
So the x:DataType
is the local:Type
used in the IDL file, and the {x:Bind}
indicates which getter to call. Mode=OneWay
means that the framework will call your getters to find the values. If you have a <TextBox>
, you can e.g., say "Mode=TwoWay
" in which case the framework will call your getter to initialize the text box and each time the text box changes, the framework will call your setter to transfer the value to your variables.
- In your hosting Window, you set in IDL a function that will return a vector of items to populate the
ListView
:
Windows.Foundation.Collections.IObservableVector<Item> Children{ get; };
winrt::Windows::Foundation::Collections::IObservableVector<winrt::Item>
MainWindow::Children()
{
auto children = single_threaded_observable_vector<App::Item>();
...
return children;
}
This Children
attribute will be set in a "ItemsSource
" XAML entry:
<ListView ItemsSource="{x:Bind Children}" ... />
- If you want multiple data formats (for example, in a
TreeView
, you may have different formats for items with children and for items without), define more than one <DataTemplate>
and set an ItemsSourceSelector
which will return the appropriate DataTemplate
for each item. Simon Mourier's example here has demonstrated the TreeView
with an ItemsSourceSelector
.
My Data Has Changed
Let's notify the UI. In the Item
, I'd have a member and two functions:
winrt::event<Microsoft::UI::Xaml::Data::PropertyChangedEventHandler> m_propertyChanged;
winrt::event_token Item::PropertyChanged
(winrt::Microsoft::UI::Xaml::Data::PropertyChangedEventHandler const& handler)
{
return m_propertyChanged.add(handler);
}
void Item::PropertyChanged(winrt::event_token const& token) noexcept
{
m_propertyChanged.remove(token);
}
And that's because in the IDL, I will have implemented the interface Microsoft wants to notify for changes.
runtimeclass Person : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
Then I will notify when e.g., a property has changed:
m_propertyChanged(*this, Microsoft::UI::Xaml::Data::PropertyChangedEventArgs{ L"Prop_Name" });
Pass an empty string
to notify that all properties have changed.
Let's Go
After creating a new WinUI project, my MainWindow.xaml looks like this:
<StackPanel Orientation="Vertical" >
<StackPanel.Resources>
<DataTemplate x:DataType="local:Person" x:Key="Data1">
<StackPanel Orientation="Vertical">
<TextBlock Text="{x:Bind First}" FontSize="24" />
<TextBlock Text="{x:Bind Last}" />
</StackPanel>
</DataTemplate>
</StackPanel.Resources>
<MenuBar >
<MenuBarItem x:Uid="MenuHelp">
<MenuFlyoutItem x:Uid="MenuAbout" Click="About" >
<MenuFlyoutItem.KeyboardAccelerators>
<KeyboardAccelerator Key="A" Modifiers="Menu" />
</MenuFlyoutItem.KeyboardAccelerators>
</MenuFlyoutItem>
</MenuBarItem>
</MenuBar>
<ContentDialog x:Name="Input1_Name" IsPrimaryButtonEnabled="True"
PrimaryButtonText="OK" SecondaryButtonText="Cancel"
IsSecondaryButtonEnabled="True" PrimaryButtonClick="ContentDialogOk">
<StackPanel Orientation="Vertical">
<TextBox Name="Input2_Name" />
</StackPanel>
</ContentDialog>
<Button x:Uid="b1" x:Name="Button1" Click="ShowDialog" />
<ListView ItemsSource="{x:Bind Persons}" ItemTemplate="{StaticResource Data1}"/>
</StackPanel>
So I have a menu with a Help and an About (Which would call About()
when clicked and can be clicked also with ALT+A. I have also a button which calls ShowDialog
when clicked with a name of Button1
. I also have a ListView
which would get its' children from a "Persons
" function and will have the "Data1
" format. Earlier in the XAML, I define the "Data1
" to be bound to a local type "Person
" which has First and Last as TextBlock
Text properties. Also, I 've passed "x:Uid
" to many controls to take their value from resources. I also have a content dialog to be shown when the button is clicked.
I've added a resources.resw file which contains my resources:
<data name="b1.Content" xml:space="preserve">
<value>Hello There</value>
</data>
<data name="MenuAbout.Text" xml:space="preserve">
<value>About...</value>
</data>
<data name="MenuHelp.Title" xml:space="preserve">
<value>Help</value>
</data>
I've created a Person.idl to hold my Person
info:
namespace App1
{
[default_interface]
runtimeclass Person : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
{
Person();
String Last;
String First;
}
}
Person.h/Person.cpp implements:
#pragma once
#include "Person.g.h"
namespace winrt::App1::implementation
{
struct Person : PersonT<Person>
{
Person() = default;
hstring _last, _first;
hstring Last();
void Last(hstring const& value);
hstring First();
void First(hstring const& value);
winrt::event_token PropertyChanged
(winrt::Microsoft::UI::Xaml::Data::PropertyChangedEventHandler const& handler);
void PropertyChanged(winrt::event_token const& token) noexcept;
winrt::event<Microsoft::UI::Xaml::Data::PropertyChangedEventHandler>
m_propertyChanged;
};
}
namespace winrt::App1::factory_implementation
{
struct Person : PersonT<Person, implementation::Person>
{
};
}
#include "pch.h"
#include "Person.h"
#include "Person.g.cpp"
namespace winrt::App1::implementation
{
hstring Person::Last()
{
return _last;
}
void Person::Last(hstring const& value)
{
_last = value;
}
hstring Person::First()
{
return _first;
}
void Person::First(hstring const& value)
{
_first = value;
}
winrt::event_token Person::PropertyChanged
(winrt::Microsoft::UI::Xaml::Data::PropertyChangedEventHandler const& handler)
{
return m_propertyChanged.add(handler);
}
void Person::PropertyChanged(winrt::event_token const& token) noexcept
{
m_propertyChanged.remove(token);
}
}
My MainWindow
's IDL is now like that:
import "Person.idl";
namespace App1
{
[default_interface]
runtimeclass MainWindow : Microsoft.UI.Xaml.Window
{
MainWindow();
Windows.Foundation.Collections.IObservableVector<Person> Persons{ get; };
}
}
And the implementation:
#pragma once
#include "MainWindow.g.h"
#include "Person.h"
#include "Person.g.h"
using namespace winrt::Microsoft::UI::Xaml::Controls;
namespace winrt::App1::implementation
{
struct MainWindow : MainWindowT<MainWindow>
{
MainWindow()
{
}
void ShowDialog(const IInspectable&,const IInspectable&)
{
auto top = Content().as<StackPanel>();
auto dialog = top.FindName(L"Input1_Name").as<ContentDialog>();
auto result = dialog.ShowAsync();
}
void ContentDialogOk(const IInspectable&, const IInspectable&)
{
auto top = Content().as<StackPanel>();
auto tb = top.FindName(L"Input2_Name").as<TextBox>();
auto bu = top.FindName(L"Button1").as<Button>();
bu.Content(box_value(tb.Text()));
}
winrt::Windows::Foundation::Collections::IObservableVector<winrt::App1::Person> Persons()
{
auto children = single_threaded_observable_vector<App1::Person>();
App1::Person person1;
person1.First(L"Michael");
person1.Last(L"Chourdakis");
children.Append(person1);
App1::Person person2;
person2.First(L"My");
person2.Last(L"Father");
children.Append(person2);
return children;
}
void About(const IInspectable&, const IInspectable&)
{
MessageBox(0, L"Hello", 0, 0);
}
};
}
namespace winrt::App1::factory_implementation
{
struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>
{
};
}
And then I've got my nice, boring window when the ContentDialog
is also shown:
The example.zip contains all this simple project, ready for you to Build.
Have fun!
History
- 1st February, 2014: First release