解析窗口类和实现添加一个按钮点击执行

读者可以把vs 2022 升级到17.9.1或者以上版本后,创建的winui3 C++是默认的例子是有一个按钮的,点击按钮后台可以执行一些操作。

我们创建一个新的项目

上一节课,我们已经讲解了项目的各个文件和作用。这一节我们创建一个新的空白窗体,然后窗体上面添加一个按钮,点击按钮

我们前面说到,我们的入口类是APP这里

    void App::OnLaunched([[maybe_unused]] LaunchActivatedEventArgs const& e)
    {
        window = make<MainWindow>();
        window.Activate();
    }Code language: C++ (cpp)

从这里开始,创建主窗体,主窗体就是我们程序打开看到的那个窗体。如下是我们的主窗口,下面先解析一下,一个窗体一共有四个文件。

窗体

MainWindow.xaml(界面描述文件)

纯 XML 标记语言,专门写 UI 布局,只负责界面视觉,不写业务逻辑。

里面你可以写布局控件,各种窗体控件,例如Button TextBox等等,这是一个从WPF开始就一直使用的界面描述文件,本质是一个XML.需要配套另外的同名的.h .cpp文件。来处理后台事件。

在官方默认的例子中,xaml是一个空白的,什么控件都没有

<?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinuiCppDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinuiCppDemo"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="WinuiCppDemo">

    <Grid>

    </Grid>
</Window>Code language: YAML (yaml)

MainWindow.idl

接口定义文件,C++/WinRT 独有核心

IDL = Interface Definition Language 接口定义语言,定义窗口对外暴露的 API、属性、事件,是 C++/WinRT 跨语言互操作的桥梁。

  1. 规定窗口类的公开成员:自定义属性、自定义事件、可调用方法
  2. 编译器会根据 idl 自动生成 WinRT 标准 ABI 接口代码,让 C++/C#/JS 都能调用这个窗口
  3. 界面 XAML 里的数据绑定、自定义控件依赖 idl 声明属性才能生效

为什么 WinUI3 C++/WinRT 必须要有 .idl

因为,WinRT 是一套跨语言二进制 ABI 标准

Windows Runtime(WinRT)不是单纯 C++ 库,它是一套跨语言通用二进制接口规范: C++、C#、Python、JS 都能调用同一个 WinRT 组件,靠的就是统一接口描述,不能直接靠 C++ 头文件互通。

C++ 头文件只给 C++ 编译器识别,C#/JS 完全看不懂;而 IDL 是中立接口语言,编译器能把它翻译成所有语言都能识别的 ABI 胶水代码。

下面是我们的例子中的idl文件的内容

namespace WinuiCppDemo
{
    [default_interface]
    runtimeclass MainWindow : Microsoft.UI.Xaml.Window
    {
        MainWindow();
        Int32 MyProperty;
    }
}

我们说了 idl是描述语言,上面这个idl文件,前面是命名空间namespace WinuiCppDemo,然后[default_interface] 特性

runtimeclass 标记默认主接口,是 C++/WinRT 写窗口 / 自定义控件的强制标配

MIDL 编译器会自动生成一个 IMainWindow 纯虚接口,包含类所有成员(构造、属性、方法)

[default_interface] 指定这个 IMainWindow 是该类对外暴露的主 ABI 接口

大括号内的MainWindow();是无参构造函数声明,必须显式写出构造,XAML 引擎实例化窗口时会调用这个默认构造

对应 .xaml.h 里的构造函数实现。

Int32 MyProperty;

声明一个WinRT 依赖属性(可绑定属性),这就是 IDL 的核心价值体现:

Int32:WinRT 标准基础类型(对应 C++ int32_t);

MyProperty:自动生成一套完整属性体系:

  • Getter:int32_t MyProperty();
  • Setter:void MyProperty(int32_t value);
  • 属性变更事件:MyPropertyChanged(属性修改自动触发,XAML x:Bind 靠它自动刷新 UI)

只有在 IDL 里写的这种成员,才能在 XAML 中用 {x:Bind MyProperty} 绑定; 如果你只在 .h 里写一个普通成员变量,XAML 完全识别不到,绑定直接报错。

右键idl可以单独编译这个idl,它是midl.exe这个工具来处理的,回编译成C#用的元数据文件,也回生成C++的接口头文件。

说白了,idl,就是用来生成接口,为了跨语言之间的调用,同时也为了我们的XAML能进行数据绑定提供元数据。为什么winui 3 C# 没有这个idl,因为C#自带反射和元数据。它是托管语言,而C++ 版本的winui 因为C++ 本身没有反射、没有元数据;

你只在 .h 里写一个成员变量,XAML 引擎完全看不见;

只有在 IDL 声明 Int32 MyProperty;,编译器才会生成:

  1. 属性 Get/Set 接口
  2. 属性变更通知事件 MyPropertyChanged
  3. 元数据,让 XAML 静态绑定校验通过

哪怕你的项目从头到尾只用 C++,不跟 C#/JS 任何其他语言交互,只要你用 XAML,就必须写 IDL

WinRT 设计初衷是一套统一 ABI,让 C++ / C# / JS / Python 能互相调用组件:

IDL 是中立描述语言,不绑定任何编程语言; 不同语言编译器都能读取 IDL 生成的 .winmd 元数据,识别你的类、属性、方法;

右键IDL文件,可以编译这个idl

IDL 核心是给 XAML 提供识别 UI 绑定所需的元数据,顺带实现跨语言交互。

idl编译后回生成一个 xxxx.g.h 和xxxx.g.cpp 文件的,它就存放在项目目录下的Generated Files 文件里面

Generated Files 是 自动生成代码目录

  1. 由 midl.exe、cppwinrt.exe、XAML编译器 在编译项目时自动生成的各种文件。
  2. 不要手动修改里面任何文件,重新编译会全部覆盖
  3. VS项目配置默认把该目录加入头文件包含路径,#include “MainWindow.g.h” 才能找到文件

MainWindow.xaml.h

C++ 标准头文件,存放 MainWindow 类的声明

自动包含 idl 生成的基础接口;

类的字段、私有变量、前置定义都写在这里,它和我们学习C++语法中的.h头文件一样。

写明又那些变量和函数,但是不去实现,其他类可以通过include引用它。

// 头文件保护:防止该头文件被重复包含,避免重复定义编译错误
#pragma once

// 引入由 MainWindow.idl 编译自动生成的胶水头文件
// .g.h 内部包含 IDL 生成的 IMainWindow 接口、MainWindowT<> 模板基类、WinRT元数据相关定义
// 我们在idl声明的 MyProperty、构造函数接口都在这里定义
#include "MainWindow.g.h"

// 【实现命名空间】存放窗口业务逻辑的实现类,我们自己写的属性/函数都在这里实现
namespace winrt::WinuiCppDemo::implementation
{
    // MainWindowT<MainWindow> 是.g.h提供的CRTP模板基类
    // 1. 自动继承 IDL 定义的 IMainWindow 接口,强制实现idl声明的所有成员
    // 2. 封装WinUI窗口底层逻辑、XAML组件初始化、ABI接口转发逻辑
    struct MainWindow : MainWindowT<MainWindow>
    {
        // 窗口默认构造函数,对应idl里声明的 MainWindow();
        MainWindow()
        {
            // 官方规范:构造函数内禁止调用 InitializeComponent()
            // XAML控件初始化会由框架自动延迟执行,构造阶段调用会引发生命周期崩溃
            // 参考微软cppwinrt官方说明文档
            // See https://github.com/microsoft/cppwinrt/tree/master/nuget#initializecomponent
        }

        // 对应 IDL 中 Int32 MyProperty; 自动生成的属性读取接口
        // 返回属性的int32_t数值
        int32_t MyProperty();
        // 对应 IDL 中 Int32 MyProperty; 自动生成的属性写入接口
        // 修改属性值,内部自动触发 MyPropertyChanged 变更事件,供x:Bind刷新UI
        void MyProperty(int32_t value);
    };
}

// 【工厂命名空间】WinRT对象工厂类,负责对外暴露、实例化MainWindow窗口对象
namespace winrt::WinuiCppDemo::factory_implementation
{
    // MainWindowT第二个参数绑定上面implementation::MainWindow实现类
    // 作用:提供统一ABI创建入口,给XAML引擎、外部代码(C#/其他组件)创建窗口实例
    struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>
    {
    };
}Code language: C++ (cpp)

MainWindow.xaml.cpp

C++ 实现文件,所有业务逻辑代码写在这里

  • 实现 MainWindow.xaml.h 里声明的所有函数
  • 控件事件处理(按钮点击、窗口加载、输入响应)
  • 数据处理、网络、文件、业务计算
  • 操作 UI 控件、修改绑定属性、弹窗、页面跳转等交互逻辑
// 引入预编译头文件,统一包含项目公共头文件、WinRT基础投影头,加速编译
#include "pch.h"
// 引入当前窗口类的头文件,包含MainWindow实现类声明
#include "MainWindow.xaml.h"

// 条件编译:判断是否存在IDL自动生成的底层实现胶水文件MainWindow.g.cpp
// .g.cpp 由midl+cppwinrt工具根据.idl生成,封装属性变更事件、ABI底层转发逻辑
// 存在则引入,让属性变更通知(MyPropertyChanged)、XAML运行时绑定正常工作
#if __has_include("MainWindow.g.cpp")
#include "MainWindow.g.cpp"
#endif

// 全局命名空间简化,后续代码无需重复写winrt::前缀
using namespace winrt;
// WinUI Xaml核心控件、窗口、路由事件等API命名空间
using namespace Microsoft::UI::Xaml;

// 官方注释:WinUI项目结构、模板文档地址
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

// implementation命名空间:存放MainWindow窗口业务逻辑实现,对应头文件里的类声明
namespace winrt::WinuiCppDemo::implementation
{
    // IDL中声明的MyProperty属性【读方法】实现
    // 返回Int32类型的属性值XAML x:Bind读取数据时会调用此函数
    int32_t MainWindow::MyProperty()
    {
        // 默认模板抛出未实现异常,需要自行替换为真实存储变量的返回逻辑
        throw hresult_not_implemented();
    }

    // IDL中声明的MyProperty属性【写方法】实现
    // value:传入要修改的新属性值,修改后底层自动触发MyPropertyChanged事件刷新绑定UI
    void MainWindow::MyProperty(int32_t /* value */)
    {
        // 默认模板抛出未实现异常,需要自行补充变量赋值逻辑
        throw hresult_not_implemented();
    }
}Code language: PHP (php)

下面,我们在这个主窗体中,添加一个按钮,点击按钮,让里面的MyProperty属性自动+1,然后打开一个对话框,显示当前MyProperty的值。

首先是xaml中增加一个按钮

    <Grid>
        <Button Content="Click Me" HorizontalAlignment="Center" VerticalAlignment="Center" Click="OnButtonClick"/>
    </Grid>Code language: C++ (cpp)

按钮显示的名字是Click Me, 然后还绑定过来点击事件OnButtonClick

鼠标放到OnButtonClick 按住F12,VS自动创建这个事件的执行函数。

然后回在MainWindow.xaml.h MainWindow.xaml.cpp中看到

void OnButtonClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);Code language: C++ (cpp)

增加了一个函数的定义。

在MainWindow.xaml.cpp中有它的实现

然后我们修改cpp中的代码

本节课就先到这里,我们下节课来实现这个点击按钮的功能。了解一下属性绑定。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注