Click here to Skip to main content
15,887,596 members
Articles / Desktop Programming / Win32

The Guide to WinUI3 for a C++ Win32 Programmer

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
1 Feb 2024CPOL7 min read 4.6K   127   6   1
Easily migrate to WinUI3 from plain Win32 while keeping all Win32 features intact
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:

XML
<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":

C++
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:

C++
namespace Project
{
    [default_interface]
    runtimeclass Item 
    {
        Int32 h;
    }
}

And then the item.h becomes:

C++
namespace winrt::Project::implementation 
{ 
struct Item : ItemT<Item> 
   { 
    int _h = 0;           // the actual variable, invisible externally
    int h() { return _h;} // visible methods get and set
    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:

C++
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

  1. You have a "resw" file which can contain translatable strings and stuff.
  2. You can embed "resource" items within the XAML to be reused later.

HWND

Windows are still HWNDs so you can get a native handle:

C++
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:

C++
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 strings. 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):

  1. 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:

    C++
    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.

  2. 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.
    XML
    <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.

  3. In your hosting Window, you set in IDL a function that will return a vector of items to populate the ListView:
    C++
    Windows.Foundation.Collections.IObservableVector<Item> Children{ get; };
    
     // And in the code:
       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:

    XML
    <ListView ItemsSource="{x:Bind Children}" ... />
  4. 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:

C++
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.

C++
runtimeclass Person : Microsoft.UI.Xaml.Data.INotifyPropertyChanged

Then I will notify when e.g., a property has changed:

C++
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:

XML
<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:

XML
<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:

C++
namespace App1
{
    [default_interface]
    runtimeclass Person : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
    {
        Person();
        String Last;
        String First;
    }
}

Person.h/Person.cpp implements:

C++
#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>
    {
    };
}
C++
#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:

C++
import "Person.idl";

namespace App1
{
    [default_interface]
    runtimeclass MainWindow : Microsoft.UI.Xaml.Window
    {
        MainWindow();
        Windows.Foundation.Collections.IObservableVector<Person> Persons{ get; };
    }
}

And the implementation:

C++
#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()
        {
            // Xaml objects should not call InitializeComponent during construction.
            // See https://github.com/microsoft/cppwinrt/tree/master/nuget#initializecomponent
        }

        // Called when button is clicked. Shows the content dialog
        void ShowDialog(const IInspectable&,const IInspectable&)
        {
            auto top = Content().as<StackPanel>();
            auto dialog = top.FindName(L"Input1_Name").as<ContentDialog>();
            auto result = dialog.ShowAsync();
        }

        // Called when OK in the ContectDialog is selected, changes the button1 text
        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()));
        }

        // Returns two elements to feed the ListView
        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;
        }

        // Called on About
        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:

Image 1

The example.zip contains all this simple project, ready for you to Build.

Have fun!

History

  • 1st February, 2014: First release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Greece Greece
I'm working in C++, PHP , Java, Windows, iOS, Android and Web (HTML/Javascript/CSS).

I 've a PhD in Digital Signal Processing and Artificial Intelligence and I specialize in Pro Audio and AI applications.

My home page: https://www.turbo-play.com

Comments and Discussions

 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA1-Feb-24 13:24
professionalȘtefan-Mihai MOGA1-Feb-24 13:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.