程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 借助 C++ 進行 Windows 開發:Windows 運行時應用程序模型

借助 C++ 進行 Windows 開發:Windows 運行時應用程序模型

編輯:關於C++

我們的生活充滿了抽象。作為開發人員,如果我們不了解抽象的定義就去使用它,則通常會讓我們 陷入困境。抽象有時是零散的,且無法完全隱藏基本復雜性。別誤解我的意思,其實抽象是很有用的。 它們能為用戶和開發人員提供幫助,而如果您深入研究您通常依賴的抽象來了解其運作方式,則會讓您 受益匪淺。此外,承認這一現實的庫通常比不承認這一現實的庫更為成功,部分是因為前者允許您在必 要時繞過抽象。

Windows 運行時 (WinRT) 就是一個這樣的抽象,在本月的專欄中,我將通過研究 WinRT 核心應用 程序模型來說明此抽象。此抽象以 CoreWindow 類為中心,並且每個“新型”Windows 應用商店和 Windows Phone 應用中都包含一個該類的實例。但是很少有開發人員知道該實例的存在,更不必說該實 例的運作方式了。這可能是對抽象成功的最好證明。

自 Windows 8 API 最初於 2011 年發布以來,已有大量關於通過 Windows 運行時提供抽象的各種 語言投射的報道和文章。但是,了解 Windows 運行時的最佳方式是避開各種語言投射(包括 C++/CX) 並使用標准 C++ 和經典 COM。只有 C++ 能讓您透過表象看到實際情況(從技術上說,C 也可以,但會 造成一些不必要的麻煩)。您仍可以選擇使用這樣或那樣的語言投射(但願是 C++/CX),因為您可能 應該這樣做,但您至少要更清楚地了解實際情況。

若要開始此操作,請打開 Visual Studio 2012 並為 Windows 應用商店或 Windows Phone 應用創 建新的 Visual C++ 項目。隨便您使用哪種模板。加載後,轉到解決方案資源管理器並刪除一切不重要 的內容。如果您選取了基於 XAML 的模板,則刪除所有 XAML 文件。您也可以刪除所有 C++ 源文件。 您可能需要保留預編譯頭,但請務必刪除其中包含的所有內容。應保留的內容是部署應用、圖像、證書 和 XML 清單所需的包資產。

接下來,打開項目的屬性頁並選擇編譯器屬性 - 樹中左側的 C/C++ 節點。找到稱作“使用 Windows 運行時擴展”的 /ZW 編譯器選項對應的行,並選擇“否”以禁用 C++/CX 語言擴展。這樣, 您就可以確保不會有比標准 C++ 編譯器更神秘的東西了。到了這一步,您也可以將編譯器的警告級別 設置為 /W4。

如果您嘗試編譯項目,則應會收到一個鏈接器錯誤,告知您找不到項目的 WinMain 入口點函數。將 新的 C++ 源文件添加到項目,您要做的第一件事是添加缺少的 WinMain 函數:

int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
}

如您所見,這是用於基於 C 運行時庫 (CRT) 的 Windows 應用程序的舊 WinMain 函數。 當然,HINSTANCE 和 PWSTR 不是基本 C++ 類型,因此您將需要包含 Windows 頭:

#include <windows.h>
           

如果您保留了項目的預編譯頭,則可在此處包含它。 此外,由 於我還將使用 Windows 運行時 C++ 模板庫 (WRL) 中的 ComPtr,因此現在最好是包含它:

#include <wrl.h>
           

在接下來的幾個專欄中,我將更詳細地介紹 WRL。 現在,我將 利用 ComPtr 類模板來維護 COM 接口指針。 在這一階段中,您只需記住 WRL ComPtr 只是一個 COM 接口智能指針。 盡管它提供了某些特定於 Windows 運行時的功能,但我不會在本月的專欄中探討這些 功能。 您可以很輕松地改用活動模板庫 (ATL) CComPtr 或您選擇的任何 COM 接口智能指針。 WRL ComPtr 是在 Microsoft::WRL 命名空間中定義的:

 using namespace Microsoft::WRL;
           

我還將使用 ASSERT 宏和 HR 函數來進行錯誤處理。 由於我之前已討論這些,因此在這 裡不再講述。 如果您對這些步驟不確定,請參閱我在 2013 年 5 月撰寫的專欄“Direct2D 1.1 簡介 ”(msdn.microsoft.com/magazine/dn198239)。

最後,若要使用本專欄中提到的任何 WinRT 函 數,您需要為鏈接器提供 .lib 文件的名稱:

#pragma comment(lib, "RuntimeObject.lib")

應用程序模型首先需要的是多線程單元 (MTA)。 是的,這就是 COM 單 元模型所在的位置。 Windows 運行時提供了 RoInitialize 函數,它是 CoInitializeEx 的薄包裝:

HR(RoInitialize (RO_INIT_MULTITHREADED));
           

盡管使用 CoInitializeEx 通常已足夠,但我還是建議您使用 RoInitialize。 利用此函數,您將來可以對 Windows 運行時進行改進而不用承擔破壞經典 COM 的風險。 RoInitialize 類似於 OleInitialize, 後者也稱為 CoInitializeEx 等等。 就我看來,您的應用程序的主線程沒什麼神秘的。 唯一可能有點 讓人吃驚的是它不是單線程單位 (STA)。 別擔心,您的應用程序的窗口仍將從 STA 線程中運行,但 Windows 運行時將是其創建者。 實際上,此 STA 是略微不同的應用程序 STA (ASTA),我以後會詳細 介紹它。

接下來的內容會有一些棘手。 Windows 運行時放棄了使用基於 GUID 的類標識符 的傳統 COM 激活模型,而采用了根據文本類標識符激活類的模型。 文本名稱基於 Java 和 Microsoft .NET Framework 提出的以命名空間為作用域的類名稱,但在您對注冊表嗤之以鼻並表示終於得到解脫 之前,請記住這些新的類標識符仍存儲在注冊表中。 從技術上說,僅第一方類型是在注冊表中注冊的 ,而第三方類型僅在每個應用程序清單中注冊。 這種變化有利也有弊。 缺點之一是,在調用 WinRT 函數時描述類標識符有點困難。 Windows 運行時定義了一個新的遠程字符串類型來替代傳統的 BSTR 字符串類型,任何類標識符都需要通過此新媒介提供。 調用 HSTRING 時出現的錯誤比調用 BSTR 時出 現的錯誤要少得多,這主要是因為前者是固定不變的。 使用 WindowsCreateString 函數創建 HSTRING 最為輕松:

wchar_t buffer[] = L"Poultry.Hatchery";
HSTRING string;
HR(WindowsCreateString(buffer,
                       _countof(buffer) - 1,
                       &string));

WindowsCreateString 將分配足夠的內存來存儲 源字符串的副本和終止 null 字符,然後將源字符串復制到此緩沖區。 若要釋放後備緩沖區,您必須 記住調用 WindowsDeleteString,除非字符串的所有權返回到調用函數:

HR(WindowsDeleteString(string));
           

如果使用的是 HSTRING,您可以使用 WindowsGetStringRawBuffer 函數獲取指向其後備緩沖區的指針:

wchar_t const * raw = WindowsGetStringRawBuffer(string, nullptr);
           

可選的第二個參數將返回字符 串的長度。 該長度與字符串存儲在一起,這樣您無需掃描字符串即可確定其長度。 在運行和寫入 C++ 包裝類之前,您需要注意的是,在為字符串文本或常量數組生成代碼時,C++/CX 編譯器不會干擾 WindowsCreateString 和 WindowsDelete­String。 相反,它會使用所謂的快速傳遞字符串,以避 免我之前提到的過度的內存分配和復制。 這還將避免內存洩漏的風險。 WindowsCreate­StringReference 函數將創建快速傳遞字符串:

          

HSTRING_HEADER header;
HSTRING string;
HR(WindowsCreateStringReference(buffer,
                                _countof(buffer) - 1,
                                &header,
                                &string));

此函數使用調用方提供的 HSTRING_HEADER 來避免堆棧分配。在這種情況下,HSTRING 的後備緩沖區是源字符串本身,因此您必 須確保源字符串(以及標頭)在 HSTRING 的整個生命周期內保持不變。當您需要將字符串返回到調用 函數時,此方法明顯沒有任何用處,但當您需要將字符串作為輸入傳遞到生存期受堆棧限制的其他函數 (非異步函數)時,此方法值得優化。WRL 還為 HSTRING 和快速傳遞函數提供了包裝。

RoGetActivationFactory 只是這樣一個函數,用於獲取給定類 的激活工廠或靜態接口。這與 COM CoGetClassObject 函數類似。如果此函數通常用於由 MIDL 編譯器 生成的常量數組,則寫入簡單函數模板來提供快速傳遞字符串包裝會很有用。圖 1 顯示了其可能的外觀。

圖 1 GetActivationFactory 函數模板

          template <typename T, unsigned Count>
auto GetActivationFactory(WCHAR const (&classId)[Count]) -> ComPtr<T>
{
  HSTRING_HEADER header;
  HSTRING string;
  HR(WindowsCreateStringReference(classId,
                                  Count - 1,
                                  &header,
                                  &string));
  ComPtr<T> result;
  HR(RoGetActivationFactory(string,
    __uuidof(T),
    reinterpret_cast<void **>(result.GetAddressOf())));
  return result;
}

GetActivationFactory 函數模板將自動推斷字符串長度,從而消除了易出錯的緩沖區長度 參數或成本高昂的運行時掃描。然後,該模板會在調用實際 RoGetActivationFactory 函數前准備快速 傳遞字符串。此時該函數模板將再次推斷接口標識符,並安全地返回在 WRL ComPtr 中包裝的生成的 COM 接口指針。

您現在可以使用此幫助程序函數來獲取 ICoreApplication 接口:

          using namespace ABI::Windows::ApplicationModel::Core;
auto app = GetActivationFactory<ICoreApplication>(
  RuntimeClass_Windows_ApplicationModel_Core_CoreApplication);

ICoreApplication 接口用來讓您的應用程序開始運行。 若要使用此 COM 接口,您需要包含應用程序模型標頭:

             #include <Windows.ApplicationModel.Core.h>
           

此標頭定義了 ABI::Windows::ApplicationModel::Core 命名空間內的 ICoreApplication,以及 Core­Application 的文本類標識符。 您真正需要考慮的唯一接口方法 是 Run 方法。

在繼續討論之前,有必要贊賞一下 Windows 運行時為您的應用程序帶來的靈活 性。 正如我前面提到的,Windows 運行時只不過將您視為您自己的進程中的一個來賓。 這與 Windows 服務多年來的工作原理類似。 對於 Windows 服務,Windows 服務控制管理器 (SCM) 使用 CreateProcess 函數或其變體之一啟動服務。 該管理器隨後會等待進程調用 StartServiceCtrlDispatcher 函數。 此函數將建立返回 SCM 的連接,該服務和 SCM 可通過它進行通 信。 例如,如果該服務未能及時調用 StartService­CtrlDispatcher,則 SCM 將認為出錯並終止 進程。 StartServiceCtrl­Dispatcher 函數僅在該服務終止後返回,因此 SCM 需要為服務創建輔 助線程以便接收回調通知。 該服務很少對事件進行響應且受 SCM 的支配。 正如您將發現的,這與 WinRT 應用程序模型極其相似。

Windows 運行時等待進程獲取 ICore­Application 接口並 調用其 Run 方法。 與 SCM 類似,如果進程未能及時執行此操作,則 Windows 運行時將認為存在錯誤 並終止進程。 幸運的是,如果連接了調試器,則 Windows 運行時將發出通知並禁用超時,這一點與 SCM 不同。 但是,其模型是相同的。 Windows 運行時負責應用程序並在事件發生時對運行時創建的線 程調用應用程序。 當然,Windows 運行時是基於 COM 的,它期望應用程序提供 Run 方法與可用於調 用應用程序的 COM 接口,而不是提供回調函數(這與 SCM 相同),並且 Windows 運行時依賴進程生 命周期管理器 (PLM) 做到這一點。

您的應用程序必須提供 IFramework­ViewSource 的實 現(也來自 ABI::Windows::ApplicationModel::Core 命名空間);在創建您的應用程序的 UI 線程後 ,CoreApplication 將調用其獨立的 CreateView 方法。 IFrameworkViewSource 不會真的將 CreateView 視為方法。 IFrameworkViewSource 派生自 WinRT 基接口 IInspectable。 反過來, IInspectable 又派生自 COM 基接口 IUnknown。

WRL 為實現 COM 類提供了廣泛的支持,不過 關於這方面的內容,我將留到接下來的專欄中講解。 現在,我想介紹 Windows 運行時如何真正來源於 COM,以及哪一種方法比實現 IUnknown 更能展示這一點。 對於我而言,記住這一點很有用:將實現 IFrameworkViewSource 和其他一些接口的 C++ 類的生存期通過堆棧來定義。 基本說來,應用程序的 WinMain 函數可以歸納為:

          HR(RoInitialize(RO_INIT_MULTITHREADED));
auto app = GetActivationFactory<ICoreApplication>( ...
          SampleWindow window;
HR(app->Run(&window));

剩下來要做的只是寫入 SampleWindow 類,以便讓它正確實 現 IFrameworkViewSource。盡管 CoreApplication 不關注自己的實現位置,但至少來說,您的應用程 序不但需要實現 IFrameworkViewSource,而且需要實現 IFrameworkView 和 IActivatedEventHandler 接口。在本示例中,SampleWindow 類可以將它們全部實現:

          struct SampleWindow :
  IFrameworkViewSource,
  IFrameworkView,
  IActivatedEventHandler
{};

此外,IFrameworkView 接口也是在 ABI::Windows::ApplicationModel::Core 命名空間 中定義的,但更難約束 IActivatedEvent­Handler。我在下面給出了自己所做的定義:

          using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::ApplicationModel::Activation;
typedef ITypedEventHandler<CoreApplicationView *, 
  IActivatedEventArgs *>
  IActivatedEventHandler;

如果您有一些使用 COM 的經驗,則可能會認為這看起來不夠正 統 - 您是對的。正如您期望的,ITypedEventHandler 只是一個類模板,而定義 COM 接口的方法非常 奇怪 - 最明顯的問題是您無法知道要將其歸於哪種接口標識符。幸運的是,所有這些接口都由 MIDL 編譯器生成,該編譯器將負責專用化每個接口,並基於這些專用化來附加表示接口標識符的 GUID。該 編譯器將定義直接派生自 IUnknown 的 COM 接口並提供一個稱為 Invoke 的方法,這與之前的 typedef 可能出現的情況一樣復雜。

我有幾個要實現的接口方法,那麼讓我們開始吧。首先是 IUnknown 和強大的 QueryInterface 方法。我在這裡不想在 IUnknown 和 IInspectable 上花太多的時間,因為我將在接下來的專欄中詳細 介紹它們。圖 2 提供了針對基於堆棧的類的簡單 QueryInterface 實現,如下所示 。

圖 2 SampleWindow QueryInterface 方法

          auto __stdcall QueryInterface(IID const & id,
                              void ** result) -> HRESULT
{
  ASSERT(result);
  if (id == __uuidof(IFrameworkViewSource) ||
      id == __uuidof(IInspectable) ||
      id == __uuidof(IUnknown))
  {
    *result = static_cast<IFrameworkViewSource *>(this);
  }
  else if (id == __uuidof(IFrameworkView))
  {
    *result = static_cast<IFrameworkView *>(this);
  }
  else if (id == __uuidof(IActivatedEventHandler))
  {
    *result = static_cast<IActivatedEventHandler *>(this);
  }
  else
  {
    *result = nullptr;
    return E_NOINTERFACE;
  }
  // static_cast<IUnknown *>(*result)->AddRef();
  return S_OK;
}

對於此實現,有一些方面值得注意。首先,該方法斷言其參數是有效的。更正確的實現可能 返回 E_POINTER,但人們認為此類錯誤是可在開發過程中解決的 Bug,因此無需在運行時浪費額外的周 期。這通過導致訪問沖突和非常易於分析的崩潰轉儲提供了可能最好的行為。如果您返回了 E_POINTER ,中斷的調用方可能會直接忽略它。最佳策略是提早失敗。實際上,很多實現都采用了該位置,包括 DirectX 和 Windows 運行時。對於正確實現 QueryInterface 進行了深入地研究。COM 規范很特別, 這使得 COM 類將始終正確和一致地提供某些對象標識保證。如果 if 語句鏈看起來令人生畏,別擔心 。我將在適當的時候介紹它。

關於此實現,值得提及的最後一點是調用 AddRef 不會很麻煩。通常,在返回前,QueryInterface 必須在生成的 IUnknown 接口指針上調用 AddRef。但是,由於 SampleWindow 類主流位於堆棧上,因 此沒有必要進行引用計數。同樣,實現 IUnknown AddRef 和 Release 方法也很簡單:

          auto __stdcall AddRef()  -> ULONG { return 2; }
auto __stdcall Release() -> ULONG { return 1; }

這些方法的結果只是建議,因此您 可以利用此事實,任何非零值都行。這裡需要注意的一點是: 您可能希望覆蓋運算符 new 和 delete 以便明確表示該類僅用於堆棧。或者,您也可以直接實現引用計數,以防萬一。

接下來,我需要實現 IInspectable,但由於它不會用於此簡單應用,我將略施小計,以使其方法不 實現,如圖 3 所示。這不是符合標准的實現,不能保證一定有效。再次聲明,我將 在接下來的專欄中介紹 IInspectable,但這還不足以讓 SampleWindow IInspectable 派生的接口啟動 和運行。

圖 3 SampleWindow IInspectable 方法

          auto __stdcall GetIids(ULONG *,
                       IID **) -> HRESULT
{
  return E_NOTIMPL;
}
auto __stdcall GetRuntimeClassName(HSTRING *) -> HRESULT
{
  return E_NOTIMPL;
}
auto __stdcall GetTrustLevel(TrustLevel *) -> HRESULT
{
  return E_NOTIMPL;
}

接下來,我需要實現 IFrameworkViewSource 及其 CreateView 方法。由於 SampleWindow 類也將實現 IFrameworkView,因此上述實現很簡單。再次提醒,在返回前,您通常需要在生成的 IUnknown 派生接口指針上調用 AddRef。您可能希望在下列函數的主體中調用 AddRef,以防萬一:

          auto __stdcall CreateView(IFrameworkView ** result) -> HRESULT
{
  ASSERT(result);
  *result = this;
  // (*result)->AddRef();
  return S_OK;
}

IFrameworkView 接口是讓應用程序最終變得有趣的地方。在調用 CreateView 以從應用程 序檢索接口指針後,Windows 運行時將依次快速調用其大多數方法。您必須快速響應這些調用,因為它 們在用戶等待您的應用程序啟動的這段時間內將進行計數。第一個稱為 Initialize,這是應用程序必 須注冊 Activated 事件的地方。Activated 事件表示應用程序已激活,但要由該應用程序激活其 CoreWindow。Initialize 方法非常簡單:

          auto __stdcall Initialize(ICoreApplicationView * view) -> HRESULT
{
  EventRegistrationToken token;
  HR(view->add_Activated(this, &token));
  return S_OK;
}

隨後將調用 SetWindow 方法,用來為應用程序提供實際的 ICoreWindow 實現。 ICoreWindow 僅對 Windows 運行時內的常規桌面 HWND 建模。與之前的應用程序模型接口不同。 ICoreWindow 是在 ABI::Windows::UI::Core 命名空間中定義的。在 SetWindow 方法中,您應復制接 口指針,因為您很快會用到它:

          using namespace ABI::Windows::UI::Core;
ComPtr<ICoreWindow> m_window;
auto __stdcall SetWindow(ICoreWindow * window) -> HRESULT
{
  m_window = window;
  return S_OK;
}

Load 方法是下一個方法,您應該將所有代碼粘貼到這裡,以便准備應用程序以進行初始呈 現:

          auto __stdcall Load(HSTRING) -> HRESULT
{
  return S_OK;
}

查看本欄目

您至少應注冊與窗口大小和可見性更改以及對 DPI 縮放的更改相關的事件。您也可以抓住機會來創 建各種 DirectX 工廠對象、加載與設備無關的資源等。此處之所以是完成所有這些操作的好地方,是 因為用戶在這裡通過您的應用程序的啟動畫面呈現。

當 Load 方法返回時,Windows 運行時將認為您的應用程序已作好激活准備並觸發 Activated 事件 ,我將通過實現 IActivatedEventHandler Invoke 方法處理該事件,如下所示:

          auto __stdcall Invoke(ICoreApplicationView *,
                      IActivatedEventArgs *) -> HRESULT
{
  HR(m_window->Activate());
  return S_OK;
}

激活窗口後,應用程序便做好了運行准備:

          auto __stdcall Run() -> HRESULT
{
  ComPtr<ICoreDispatcher> dispatcher;
  HR(m_window->get_Dispatcher(dispatcher.GetAddressOf()));
  HR(dispatcher->ProcessEvents(CoreProcessEventsOption_ProcessUntilQuit));
  return S_OK;
}

可通過很多方法實現這一點。我在這裡采用的辦法是檢索窗口的 ICoreDispatcher 接口,它表示窗 口的消息泵。最後還有一個 Uninitialize 方法,它可能偶然被調用,但在其他情況下可以被安全地忽 略:

          auto __stdcall Uninitialize() -> HRESULT
{
  return S_OK;
}

就是這樣。您現在可以編譯和運行應用程序。當然,您在這裡不會真的繪制什麼東西。您可以從 dx.codeplex.com 獲得 dx.h 的副本並開始添加一些 Direct2D 呈現代碼(有關更多信息,請參見我在 2013 年 6 月的專欄“ 一個用於 DirectX 編程的現代庫”,網址為 msdn.microsoft.com/magazine/dn201741),或者等到我的下一個專欄,我將為您演示如何通過最好的方式將 Direct2D 與 WinRT 核心應用程 序模型集成。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved