65.9K
CodeProject 正在变化。 阅读更多。
Home

WinUI3 的 C++ Win32 程序员指南

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2024 年 2 月 1 日

CPOL

7分钟阅读

viewsIcon

11819

downloadIcon

395

轻松从纯 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_valueunbox_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<>() 进行转换)。

资源

  1. 您有一个“resw”文件,其中可以包含可翻译的 string 等内容。
  2. 您可以将“resource”项嵌入 XAML 中以供以后重用。

HWND

窗口仍然是 HWND,因此您可以获取本机句柄。

auto n = window.as<::IWindowNative>(); 
if (n) 
{ HWND hh; 
  n->get_WindowHandle(&hh);
}

您可以子类化 HWND 并正常发送消息。

线程

您无法从工作线程与 Window 交互,因此您可以强制在主线程上执行。

window.DispatcherQueue().TryEnqueue([&]()
     {
         ...
     });

数据绑定

这是 WinUI 中最核心的功能之一。Item 容器,如 ListViewGridViewTreeView 等,不使用普通 Win32 ListView 那样的普通“Strings”,后者会使用 LVM_INSERTITEM 并显示 string。WinUI 容器可以包含任何内容,而不仅仅是文本。此外,项可以具有不同的数据表示。例如,TreeView 项的一个元素可以是一个带有按钮的 StackPanel,而另一个 TreeView 元素可以是一个图像。

因此,处理这些容器的过程如下(我将以 ListView 为例,但其他容器如 TreeGrid 类似):

  1. 创建一组 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。

  2. 在 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 来将值传输到您的变量。

  3. 在您的托管窗口中,您在 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}" ... />
  4. 如果您想要多种数据格式(例如,在 TreeView 中,您可能有带子项和不带子项的项的不同格式),请定义多个 <DataTemplate> 并设置一个 ItemsSourceSelector,该选择器将为每个项返回适当的 DataTemplate。Simon Mourier 在此处的示例演示了带 ItemsSourceSelectorTreeView

我的数据已更改

让我们通知 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 日:首次发布
© . All rights reserved.