WinUI3 的 C++ Win32 程序员指南





5.00/5 (5投票s)
轻松从纯 Win32 迁移到 WinUI3,同时保持所有 Win32 功能不变
引言
本文面向希望使用 WinUI3 的硬核 Win32 C++ 程序员。这是将 Win32 项目转换为 WinUI3 项目的续篇,您可以使用它将现有的 VCXPROJ 文件转换为 WinUI3 项目,或者创建可在 Windows < 10 和 Windows >= 10 上运行的应用。本文侧重于 Win32 编程与 WinUI3 编程之间的差异,并帮助 C++ 程序员理解相关概念。
WinUI3 是 Windows 10/11 的界面系统。它可以与完整的 Win32 API 结合使用,因此您不会错过任何功能。它具有非常广泛的新型灵活控件。
RC 与 XAML
您不再使用 RC 文件构建对话框,而是使用 XML(Android 程序员会看到相似之处)。这具有以下优点:
- 元素像 HTML 一样具有分层视图
- 多种布局选项
- 在一个 XML 文件中可以构建许多页面
- 可以在 XML 中设置运行时属性
- 数据绑定允许您的函数自动链接到 XAML 控件
因此,例如,您可能会有一个用 RC 构建的 YES/NO 对话框,如下所示:
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
您可以拥有这个 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>
XAML 解析器解析 XML 文件并生成代码,在需要时调用您的回调(例如,在本例中,当主按钮被点击时,会调用函数 Input1_AskChannelDone
),具体取决于您在 IDL 文件中设置的类型。每个控件都可以有一个 Name
,所有容器都有一个 FindName()
函数,该函数将返回一个 IInspectable
,您可以使用 as<>()
将其转换为适当的项目。
命名空间
控件位于 winrt::Microsoft::UI::Xaml::Controls
下。一些基本的 IInspectable
对象是 winrt::Windows
的成员,但有时与旧的 UWP 代码会发生冲突。您应该始终使用 winrt::Microsoft
。您自己的代码位于 winrt::YourProjectName
下。
IInspectable 与 COM
实际上,一切都基于 COM。您可以使用 IInspectable
而不是 CComPtr<IUnknown>
,它具有 as()
和 try_as()
成员来切换到另一个接口(类似于 QueryInterface
)。
所有对象都建立在此基础上。可见对象(控件)建立在FrameworkElement
之上,正如我所说的,它具有搜索和操作其“项目”的方法。您还可以使用 box_value
和 unbox_value
将标量值和一些数组放入 IInspectable
。
窗口 / 页面 / 对话框
所有这些都是可以包含其他元素的容器。winrt::Microsoft::UI::Xaml::Controls::Window
创建一个新窗口。Page
是可以在 Frame
中显示的内容,而 ContentDialog
可以在代码调用时显示。
文件
一组文件基本上是一个 IDL 文件、一个 XAML 文件和一个 C++ H/CPP 类。IDL 文件是必需的,这样您的窗口方法才能通过 COM 的类型库机制进行注册并对 WinRT 相关内容可见。XAML 文件描述界面,C++ 代码包含属性的 getter/setter 以及您的代码。每个对象并不直接看到“h”文件(就像我们在 C++ 中会做的那样),因为它实际上被 COM 包装了。例如,在一个名为“Item.h”的文件中:
namespace winrt::Project::implementation
{
struct Item : ItemT<Item>
{
int h = 0;
}
}
如果您在另一个代码中包含此 Item.h 并获得一个指向 Item
的指针,该指针将无法看到“h”成员,因为它是一个 COM 包装器,包装了一个类型库。您还必须将其放入 idl 文件中:
namespace Project
{
[default_interface]
runtimeclass Item
{
Int32 h;
}
}
然后 item.h 变成:
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
这些控件可以组合其他控件。Stackpanel
将它们一个接一个地放置(水平或垂直),Canvas
使用绝对定位,RelativePanel
使用相对定位,ViewBox
会缩放其内容,而 Grid
则创建一个 Grid
。许多容器只想要一个控件(例如,Page
),因此如果您要在 Page
中放置更多控件,您将选择这些面板之一。
回调
因此,例如,当按钮被点击时,您的函数将被调用。此函数始终具有相同的签名:
void fn(const IInspectable& sender,const IInspectable& i2);
第一个参数是生成消息的控件,因此您可以调用 as<Button>()
,例如,如果点击来自 Button
。第二个参数包含有关点击的信息,其类型取决于事件,例如,Button 的 Click 会生成一个“RoutedEventArgs
”作为第二个参数(您可以直接将其放在签名中,或使用 as<>()
进行转换)。
资源
- 您有一个“resw”文件,其中可以包含可翻译的
string
等内容。 - 您可以将“
resource
”项嵌入 XAML 中以供以后重用。
HWND
窗口仍然是 HWND
,因此您可以获取本机句柄。
auto n = window.as<::IWindowNative>();
if (n)
{ HWND hh;
n->get_WindowHandle(&hh);
}
您可以子类化 HWND
并正常发送消息。
线程
您无法从工作线程与 Window
交互,因此您可以强制在主线程上执行。
window.DispatcherQueue().TryEnqueue([&]()
{
...
});
数据绑定
这是 WinUI 中最核心的功能之一。Item
容器,如 ListView
、GridView
、TreeView
等,不使用普通 Win32 ListView
那样的普通“Strings
”,后者会使用 LVM_INSERTITEM
并显示 string
。WinUI 容器可以包含任何内容,而不仅仅是文本。此外,项可以具有不同的数据表示。例如,TreeView
项的一个元素可以是一个带有按钮的 StackPanel
,而另一个 TreeView
元素可以是一个图像。
因此,处理这些容器的过程如下(我将以 ListView
为例,但其他容器如 Tree
和 Grid
类似):
- 创建一组 IDL、H、CPP 类来描述您的项,以及 setter 和 getter。
例如,如果我想表示一个
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(); } } }
我会像平常一样创建一个基于 IDL 的类,暴露 setter/getter。
- 在 XAML 中,在资源中定义一个
<DataTemplate>
(例如,在<Page.Resources>
下),该模板描述了项的格式以及将要使用的函数。<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
因此,
x:DataType
是 IDL 文件中使用的local:Type
,而{x:Bind}
表示要调用哪个 getter。Mode=OneWay
表示框架将调用您的 getter 来查找值。如果您有一个<TextBox>
,您可以说“Mode=TwoWay
”,在这种情况下,框架将调用您的 getter 来初始化文本框,并且每次文本框更改时,框架都会调用您的 setter 来将值传输到您的变量。 - 在您的托管窗口中,您在 IDL 中设置一个函数,该函数将返回一个项的向量来填充
ListView
。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; }
此
Children
属性将在“ItemsSource
” XAML 条目中设置。<ListView ItemsSource="{x:Bind Children}" ... />
- 如果您想要多种数据格式(例如,在
TreeView
中,您可能有带子项和不带子项的项的不同格式),请定义多个<DataTemplate>
并设置一个ItemsSourceSelector
,该选择器将为每个项返回适当的DataTemplate
。Simon Mourier 在此处的示例演示了带ItemsSourceSelector
的TreeView
。
我的数据已更改
让我们通知 UI。在 Item
中,我将有一个成员和两个函数:
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);
}
这是因为在 IDL 中,我将实现了 Microsoft 希望通知更改的接口。
runtimeclass Person : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
然后,我将在属性更改时通知,例如:
m_propertyChanged(*this, Microsoft::UI::Xaml::Data::PropertyChangedEventArgs{ L"Prop_Name" });
传递一个空的 string
来通知所有属性都已更改。
开始吧
创建新的 WinUI 项目后,我的 MainWindow.xaml 如下所示:
<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>
我有一个菜单,包含“帮助”和“关于”(点击时会调用 About()
,也可以通过 ALT+A 点击)。我还有一个按钮,点击时会调用 ShowDialog
,其名称为 Button1
。我还一个 ListView
,它将从“Persons
”函数获取子项,并将使用“Data1
”格式。在 XAML 的更早部分,我定义“Data1
”绑定到一个本地类型“Person
”,该类型有 First 和 Last 作为 TextBlock
的 Text 属性。此外,我还为许多控件传递了“x:Uid
”以从资源中获取其值。我还定义了一个内容对话框,将在按钮点击时显示。
我添加了一个 resources.resw 文件,其中包含我的资源:
<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>
我创建了一个 Person.idl 来保存我的 Person
信息:
namespace App1
{
[default_interface]
runtimeclass Person : Microsoft.UI.Xaml.Data.INotifyPropertyChanged
{
Person();
String Last;
String First;
}
}
Person.h/Person.cpp 的实现:
#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);
}
}
我的 MainWindow
的 IDL 现在如下所示:
import "Person.idl";
namespace App1
{
[default_interface]
runtimeclass MainWindow : Microsoft.UI.Xaml.Window
{
MainWindow();
Windows.Foundation.Collections.IObservableVector<Person> Persons{ get; };
}
}
以及实现:
#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>
{
};
}
然后,我得到了我那漂亮、无聊的窗口,并且 ContentDialog
也显示出来了:
example.zip 包含所有这些简单的项目,您可以直接构建。
玩得开心!
历史
- 2014 年 2 月 1 日:首次发布