解析視窗類別並實作按鈕點擊功能

讀者將 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 開頭命名空間為 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 依靠此事件自動重新整理介面)

只有在 IDL 內宣告的成員,才能在 XAML 使用 {x:Bind MyProperty} 進行繫結;若僅在 .h 撰寫一般成員變數,XAML 完全無法辨識,繫結直接報錯。

對 IDL 檔案按右鍵可單獨編譯,底層由 midl.exe 工具處理,會產生 C# 使用的中繼資料檔,同時輸出 C++ 介面標頭檔。

簡單來說,IDL 用來產生介面以實現跨語言呼叫,同時提供 XAML 資料繫結所需的中繼資料。WinUI3 C# 專案不需要 IDL,因為 C# 屬於受控語言,原生支援反射與中繼資料;而 C++ 版 WinUI 原生不具備反射、中繼資料機制。

僅在 .h 宣告成員變數,XAML 引擎完全無法偵測;

唯有在 IDL 宣告 Int32 MyProperty;,編譯器才會自動產生:

  1. 屬性讀取/寫入介面
  2. 屬性變更通知事件 MyPropertyChanged
  3. 中繼資料,確保 XAML 靜態繫結驗證通過

就算專案全程只使用 C++,完全不與 C#、JS 等其他語言互動,只要使用 XAML,就必須撰寫 IDL。

WinRT 設計初衷是一套統一 ABI 標準,讓 C++ / C# / JS / Python 可互相呼叫元件:

IDL 為中立描述語言,不綁定任何程式語言;各語言編譯器均可讀取 IDL 輸出的 .winmd 中繼資料,解析類別、屬性、方法;

對 IDL 檔按右鍵即可編譯該 IDL

IDL 的核心作用是提供 XAML 介面繫結所需的中繼資料,跨語言互通是附加功能。

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<> CRTP 樣版基底類別、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 數值,供 XAML x:Bind 讀取資料
        int32_t MyProperty();
        // 對應 IDL 中 Int32 MyProperty; 自動產生的屬性設值介面
        // 修改屬性值,底層自動觸發 MyPropertyChanged 變更事件,供 x:Bind 重新整理介面
        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 事件重新整理繫結介面
    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 檔內的程式邏輯

本堂課到此結束,下一堂課我們實作這個按鈕點擊功能,並詳細認識屬性繫結。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *