這個主題建立在 Windows 執行階段 元件和消費應用程式之上,而 Windows 執行階段 元件搭配 C++/WinRT 主題則教你如何編譯。
以下是這個主題新增的功能。
- 更新溫度計執行類別,當溫度低於冰點時觸發事件。
- 更新使用溫度計執行階段類別的核心應用程式,讓其能夠處理該事件。
Note
關於安裝及使用 C++/WinRT Visual Studio 擴充套件(VSIX)及 NuGet 套件(共同提供專案範本與建置支援)的資訊,請參閱 Visual Studio 對 C++/WinRT 的支援。
這很重要
關於支持你如何使用 C++/WinRT 撰寫執行時類別的基本概念與術語,請參見 Consume API with C++/WinRT 及 Author API with C++/WinRT。
建立 ThermometerWRC 與 ThermometerCoreApp
如果你想跟著本主題中示範的更新步驟進行,以便建置及執行程式碼,那麼第一步就是依照 Windows 執行階段 元件與 C++/WinRT 主題中的逐步解說進行。 這樣你就會擁有 ThermometerWRC Windows 執行階段 元件,以及會消耗這些元件的 ThermometerCoreApp 核心應用程式。
更新 ThermometerWRC 以引發事件
更新 Thermometer.idl 後會看起來像下面的列表。 這是如何宣告一個代理型別為 EventHandler 的事件,並以單精度浮點數為參數。
// Thermometer.idl
namespace ThermometerWRC
{
runtimeclass Thermometer
{
Thermometer();
void AdjustTemperature(Single deltaFahrenheit);
event Windows.Foundation.EventHandler<Single> TemperatureIsBelowFreezing;
};
}
儲存檔案。 專案目前的狀態無法完成建置,但無論如何,現在還是請先執行一次建置,以產生更新版本的 \ThermometerWRC\ThermometerWRC\Generated Files\sources\Thermometer.h 和 Thermometer.cpp 存根檔。 在這些檔案中,你現在可以看到 TemperatureIsBelowFreezing 事件的存根實作。 在 C++/WinRT 中,IDL 宣告事件實作為一組超載函式(類似於屬性實作為一對超載的 get 與 set 函式)。 一次過載會接收一個代理被註冊,並回傳一個令牌( 一個 winrt::event_token)。 另一方則收取代幣,撤銷該代表的登記。
現在打開 Thermometer.h 和 Thermometer.cpp,並更新 Thermometer 執行類別的實作。 在 Thermometer.h中加入兩個超載的 TemperatureIsBelowFreezing 函式,以及一個私有事件資料成員以實作這些函式。
// Thermometer.h
...
namespace winrt::ThermometerWRC::implementation
{
struct Thermometer : ThermometerT<Thermometer>
{
...
winrt::event_token TemperatureIsBelowFreezing(Windows::Foundation::EventHandler<float> const& handler);
void TemperatureIsBelowFreezing(winrt::event_token const& token) noexcept;
private:
winrt::event<Windows::Foundation::EventHandler<float>> m_temperatureIsBelowFreezingEvent;
...
};
}
...
如上所示,事件由 winrt::event struct 模板表示,該模板由特定的代理類型參數化(而該代理類型本身也可由 args 類型參數化)。
在 Thermometer.cpp中,實作兩個超載的 TemperatureIsUnderFreezing 函數。
// Thermometer.cpp
...
namespace winrt::ThermometerWRC::implementation
{
winrt::event_token Thermometer::TemperatureIsBelowFreezing(Windows::Foundation::EventHandler<float> const& handler)
{
return m_temperatureIsBelowFreezingEvent.add(handler);
}
void Thermometer::TemperatureIsBelowFreezing(winrt::event_token const& token) noexcept
{
m_temperatureIsBelowFreezingEvent.remove(token);
}
void Thermometer::AdjustTemperature(float deltaFahrenheit)
{
m_temperatureFahrenheit += deltaFahrenheit;
if (m_temperatureFahrenheit < 32.f) m_temperatureIsBelowFreezingEvent(*this, m_temperatureFahrenheit);
}
}
Note
如需了解自動事件撤銷器的詳細資訊,請參閱 撤銷已註冊的委派。 你的活動會免費獲得自動事件撤銷器的實作。 換句話說,你不需要實作事件撤銷器的超載——那是 C++/WinRT 投影提供的。
其他超載(註冊和手動撤銷超載) 則未 內建在投影中。 這是為了讓你有彈性,能根據你的情境最佳地實施它們。 呼叫 event:add 和 event::remove 如這些實作所示,是一種高效且並發/執行緒安全的預設。 但如果事件數量非常龐大,你可能不想為每個事件各自設置一個事件欄位,而會改採某種稀疏實作方式。
你也可以從上方看到,AdjustTemperature 函式的實作已更新,以便在溫度降到冰點以下時引發 TemperatureIsBelowFreezing 事件。
更新 ThermometerCoreApp 以處理此事件
在 ThermometerCoreApp 專案中,對 App.cpp程式碼進行以下修改,註冊事件處理程序,然後使溫度降至冰點以下。
WINRT_ASSERT 是一個巨集定義,並擴展為 _ASSERTE。
struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
winrt::event_token m_eventToken;
...
void Initialize(CoreApplicationView const &)
{
m_eventToken = m_thermometer.TemperatureIsBelowFreezing([](const auto &, float temperatureFahrenheit)
{
WINRT_ASSERT(temperatureFahrenheit < 32.f); // Put a breakpoint here.
});
}
...
void Uninitialize()
{
m_thermometer.TemperatureIsBelowFreezing(m_eventToken);
}
...
void OnPointerPressed(IInspectable const &, PointerEventArgs const & args)
{
m_thermometer.AdjustTemperature(-1.f);
...
}
...
};
請注意 OnPointerPressed 方法的變更。 現在,每次點擊窗戶,你都會從溫度計的溫度 中扣除 1華氏度。 現在,應用程式正在處理氣溫降到冰點以下時所觸發的事件。 為了示範該事件會如預期般被引發,請在處理 TemperatureIsBelowFreezing 事件的 lambda 運算式中設定中斷點,執行應用程式,然後在視窗內按一下。
跨 ABI 的參數化代理
如果你的事件必須透過應用程式二進位介面(ABI)存取——例如元件與其消費應用程式之間——那麼你的事件必須使用 Windows 執行階段 代理型態。 上述範例使用 Windows::Foundation::EventHandler<T> Windows 執行階段 代理型別。 TypedEventHandler<TSender、TResult> 是另一個 Windows 執行階段 代理型態的例子。
這兩個代理型別的型別參數必須跨越 ABI,因此型別參數也必須是 Windows 執行階段 型別。 這包括 Windows 執行時類別、第三方執行時類別,以及像數字和字串這樣的原始型別。 如果你忘記了這個限制,編譯器會幫你解決「T 必須是 WinRT 類型」的錯誤。
以下是一個以代碼列表形式呈現的範例。 從你之前在本主題中建立的 ThermometerWRC 和 ThermometerCoreApp 專案開始,並編輯這些專案中的程式碼,使其看起來與這些列表中的程式碼相似。
第一個列表項目是 ThermometerWRC 專案。 如下方所示編輯 ThermometerWRC.idl 之後,建置專案,然後將 MyEventArgs.h 和 .cpp 從 Generated Files 資料夾複製到專案中,就像你先前處理 Thermometer.h 和 .cpp 時一樣。 請記得從兩個檔案中刪除 static_assert。
// ThermometerWRC.idl
namespace ThermometerWRC
{
[default_interface]
runtimeclass MyEventArgs
{
Single TemperatureFahrenheit{ get; };
}
[default_interface]
runtimeclass Thermometer
{
...
event Windows.Foundation.EventHandler<ThermometerWRC.MyEventArgs> TemperatureIsBelowFreezing;
...
};
}
// MyEventArgs.h
#pragma once
#include "MyEventArgs.g.h"
namespace winrt::ThermometerWRC::implementation
{
struct MyEventArgs : MyEventArgsT<MyEventArgs>
{
MyEventArgs() = default;
MyEventArgs(float temperatureFahrenheit);
float TemperatureFahrenheit();
private:
float m_temperatureFahrenheit{ 0.f };
};
}
// MyEventArgs.cpp
#include "pch.h"
#include "MyEventArgs.h"
#include "MyEventArgs.g.cpp"
namespace winrt::ThermometerWRC::implementation
{
MyEventArgs::MyEventArgs(float temperatureFahrenheit) : m_temperatureFahrenheit(temperatureFahrenheit)
{
}
float MyEventArgs::TemperatureFahrenheit()
{
return m_temperatureFahrenheit;
}
}
// Thermometer.h
...
struct Thermometer : ThermometerT<Thermometer>
{
...
winrt::event_token TemperatureIsBelowFreezing(Windows::Foundation::EventHandler<ThermometerWRC::MyEventArgs> const& handler);
...
private:
winrt::event<Windows::Foundation::EventHandler<ThermometerWRC::MyEventArgs>> m_temperatureIsBelowFreezingEvent;
...
}
...
// Thermometer.cpp
#include "MyEventArgs.h"
...
winrt::event_token Thermometer::TemperatureIsBelowFreezing(Windows::Foundation::EventHandler<ThermometerWRC::MyEventArgs> const& handler) { ... }
...
void Thermometer::AdjustTemperature(float deltaFahrenheit)
{
m_temperatureFahrenheit += deltaFahrenheit;
if (m_temperatureFahrenheit < 32.f)
{
auto args = winrt::make_self<winrt::ThermometerWRC::implementation::MyEventArgs>(m_temperatureFahrenheit);
m_temperatureIsBelowFreezingEvent(*this, *args);
}
}
...
此列表為 ThermometerCoreApp 專案。
// App.cpp
...
void Initialize(CoreApplicationView const&)
{
m_eventToken = m_thermometer.TemperatureIsBelowFreezing([](const auto&, ThermometerWRC::MyEventArgs args)
{
float degrees = args.TemperatureFahrenheit();
WINRT_ASSERT(degrees < 32.f); // Put a breakpoint here.
});
}
...
ABI 上的簡單訊號
如果你不需要在事件中傳遞任何參數或參數,那你就可以自己定義簡單的 Windows 執行階段 代理型態。 以下範例展示了溫度 計 運行時類別的簡化版本。 它宣告了一個名為 SignalDelegate 的代理類型,然後用它來引發一個訊號型事件,而不是帶參數的事件。
// ThermometerWRC.idl
namespace ThermometerWRC
{
delegate void SignalDelegate();
runtimeclass Thermometer
{
Thermometer();
event ThermometerWRC.SignalDelegate SignalTemperatureIsBelowFreezing;
void AdjustTemperature(Single value);
};
}
// Thermometer.h
...
namespace winrt::ThermometerWRC::implementation
{
struct Thermometer : ThermometerT<Thermometer>
{
...
winrt::event_token SignalTemperatureIsBelowFreezing(ThermometerWRC::SignalDelegate const& handler);
void SignalTemperatureIsBelowFreezing(winrt::event_token const& token);
void AdjustTemperature(float deltaFahrenheit);
private:
winrt::event<ThermometerWRC::SignalDelegate> m_signal;
float m_temperatureFahrenheit{ 0.f };
};
}
// Thermometer.cpp
...
namespace winrt::ThermometerWRC::implementation
{
winrt::event_token Thermometer::SignalTemperatureIsBelowFreezing(ThermometerWRC::SignalDelegate const& handler)
{
return m_signal.add(handler);
}
void Thermometer::SignalTemperatureIsBelowFreezing(winrt::event_token const& token)
{
m_signal.remove(token);
}
void Thermometer::AdjustTemperature(float deltaFahrenheit)
{
m_temperatureFahrenheit += deltaFahrenheit;
if (m_temperatureFahrenheit < 32.f)
{
m_signal();
}
}
}
// App.cpp
struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
ThermometerWRC::Thermometer m_thermometer;
winrt::event_token m_eventToken;
...
void Initialize(CoreApplicationView const &)
{
m_eventToken = m_thermometer.SignalTemperatureIsBelowFreezing([] { /* ... */ });
}
...
void Uninitialize()
{
m_thermometer.SignalTemperatureIsBelowFreezing(m_eventToken);
}
...
void OnPointerPressed(IInspectable const &, PointerEventArgs const & args)
{
m_thermometer.AdjustTemperature(-1.f);
...
}
...
};
參數化代理、簡單訊號與專案內回調
如果你需要的是 Visual Studio 專案內部的事件(不是跨二進位檔),且這些事件不限於 Windows 執行階段 類型,那你仍然可以使用 winrt::event<Delegate> 類別範本。 只要使用 winrt::d elegate 代替實際的 Windows 執行階段 代理類型,因為 winrt::d elegate 也支援非Windows 執行階段參數。
以下範例首先展示了一個不接受任何參數的代理簽名(本質上是一個簡單的訊號),接著是一個接收字串的簽名。
winrt::event<winrt::delegate<>> signal;
signal.add([] { std::wcout << L"Hello, "; });
signal.add([] { std::wcout << L"World!" << std::endl; });
signal();
winrt::event<winrt::delegate<std::wstring>> log;
log.add([](std::wstring const& message) { std::wcout << message.c_str() << std::endl; });
log.add([](std::wstring const& message) { Persist(message); });
log(L"Hello, World!");
請注意,您可以將任意數量的訂閱委派加入事件中。 然而,活動會帶來一些額外的負擔。 如果你只需要一個簡單的回呼,且只有一個訂閱委派,那麼你可以直接單獨使用 winrt::delegate<... T>。
winrt::delegate<> signalCallback;
signalCallback = [] { std::wcout << L"Hello, World!" << std::endl; };
signalCallback();
winrt::delegate<std::wstring> logCallback;
logCallback = [](std::wstring const& message) { std::wcout << message.c_str() << std::endl; }f;
logCallback(L"Hello, World!");
如果你是從在專案內部使用事件和委派的 C++/CX 程式碼庫移植而來,那麼 winrt::delegate 可協助你在 C++/WinRT 中重現該模式。
可延後事件
Windows 執行階段 中常見的模式是可延緩事件。 事件處理常式會藉由呼叫事件引數的 GetDeferral 方法取得延後。 這樣做會讓活動來源知道,活動後的活動應該延後,直到延期完成。 這使得事件處理器能對事件執行非同步動作。
winrt::deferrable_event_args 結構範本是用於實作(產生)Windows 執行階段 延後模式的輔助類別。 以下是範例。
// Widget.idl
namespace Sample
{
runtimeclass WidgetStartingEventArgs
{
Windows.Foundation.Deferral GetDeferral();
Boolean Cancel;
};
runtimeclass Widget
{
event Windows.Foundation.TypedEventHandler<
Widget, WidgetStartingEventArgs> Starting;
};
}
// Widget.h
namespace winrt::Sample::implementation
{
struct Widget : WidgetT<Widget>
{
Widget() = default;
event_token Starting(Windows::Foundation::TypedEventHandler<
Sample::Widget, Sample::WidgetStartingEventArgs> const& handler)
{
return m_starting.add(handler);
}
void Starting(event_token const& token) noexcept
{
m_starting.remove(token);
}
private:
event<Windows::Foundation::TypedEventHandler<
Sample::Widget, Sample::WidgetStartingEventArgs>> m_starting;
};
struct WidgetStartingEventArgs : WidgetStartingEventArgsT<WidgetStartingEventArgs>,
deferrable_event_args<WidgetStartingEventArgs>
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
{
bool Cancel() const noexcept { return m_cancel; }
void Cancel(bool value) noexcept { m_cancel = value; }
bool m_cancel = false;
};
}
以下說明事件接收者如何使用可延後事件模式。
// EventRecipient.h
widget.Starting([](auto sender, auto args) -> fire_and_forget
{
auto deferral = args.GetDeferral();
if (!co_await CanWidgetStartAsync(sender))
{
// Do not allow the widget to start.
args.Cancel(true);
}
deferral.Complete();
});
作為事件來源的實作端(產生者),你會從 winrt::deferrable_event_args 衍生你的事件引數類別。 deferrable_event_args<T> 為你實作了 T::GetDeferral 。 它還提供了一個新的輔助方法 deferrable_event_args::wait_for_deferrals,該方法會在所有尚未完成的延後作業都完成時完成(如果沒有取得任何延後作業,則會立即完成)。
// Widget.h
IAsyncOperation<bool> TryStartWidget(Widget const& widget)
{
auto args = make_self<WidgetStartingEventArgs>();
// Raise the event to let people know that the widget is starting
// and give them a chance to prevent it.
m_starting(widget, *args);
// Wait for deferrals to complete.
co_await args->wait_for_deferrals();
// Use the results.
bool started = false;
if (!args->Cancel())
{
widget.InsertBattery();
widget.FlipPowerSwitch();
started = true;
}
co_return started;
}
設計指導方針
我們建議您將事件而非委派作為函式參數傳遞。 winrt::event 的 add 函數是唯一的例外,因為在這種情況下你必須通過代理。 制定此指引的原因在於,委派在不同的 Windows 執行階段 語言中可能會有不同的形式(就其是否支援單一用戶端註冊或多個用戶端註冊而言)。 事件採用多重訂閱者模型,因此構成一種更可預測且更一致的選項。
事件處理代理的簽章應包含兩個參數: 發送者 (IInspectable)與 args (某事件參數類型,例如 RoutedEventArgs)。
請注意,這些指引不一定適用於設計內部 API。 不過,內部 API 通常會隨時間公開。