逐步解說:在 Win32 中裝載 WPF 內容

Windows Presentation Foundation (WPF) 提供豐富的環境來建立應用程式。 不過,當您對 Win32 程式碼進行大量投資時,將 WPF 功能新增至應用程式,而不是重寫原始程式碼可能更有效率。 WPF 提供直接的機制,可在 Win32 視窗中裝載 WPF 內容。

本教學課程說明如何在 Win32 視窗範例 中撰寫裝載 WPF 內容的範例應用程式 、在 Win32 視窗中裝載 WPF 內容。 您可以擴充此範例來裝載任何 Win32 視窗。 因為它牽涉到混合 Managed 和 Unmanaged 程式碼,所以應用程式是以 C++/CLI 撰寫。

需求

本教學課程假設您已熟悉 WPF 和 Win32 程式設計。 如需 WPF 程式設計的基本簡介,請參閱 使用者入門 。 如需 Win32 程式設計的簡介,您應該參考有關該主題的任何書籍,特別是 Charles Petzold 的程式設計 Windows

由於本教學課程隨附的範例是在 C++/CLI 中實作,本教學課程假設熟悉使用 C++ 來設計 Windows API,以及瞭解 Managed 程式碼程式設計。 熟悉 C++/CLI 很有説明,但並非必要。

注意

本教學課程包含一些來自相關範例的程式碼範例。 不過,為了方便閱讀,並未包含完整的範例程式碼。 如需完整的範例程式碼,請參閱 Hosting WPF Content in a Win32 Window Sample (在 Win32 視窗中裝載 WPF 內容的範例)。

基本程序

本節概述您在 Win32 視窗中用來裝載 WPF 內容的基本程式。 其餘各節將說明每個步驟的詳細資訊。

在 Win32 視窗上裝載 WPF 內容的索引鍵是 類別 HwndSource 。 這個類別會將 WPF 內容包裝在 Win32 視窗中,讓它以子視窗的形式併入您的使用者介面(UI)。 下列方法結合單一應用程式中的 Win32 和 WPF。

  1. 將 WPF 內容實作為 Managed 類別。

  2. 使用 C++/CLI 實作 Windows 應用程式。 如果您從現有的應用程式和 Unmanaged C++ 程式碼開始,您通常可以藉由變更專案設定來包含 /clr 編譯器旗標來呼叫 Managed 程式碼。

  3. 將執行緒模型設為單一執行緒 Apartment (STA)。

  4. 處理視窗程序中的 WM_CREATE 通知,並執行下列動作:

    1. 以父視窗做為 HwndSource 參數,建立新的 parent 物件。

    2. 建立 WPF 內容類別別的實例。

    3. 將 WPF 內容物件的參考指派給 RootVisualHwndSource 屬性。

    4. 取得內容的 HWND。 Handle 物件的 HwndSource 屬性包含視窗控制代碼 (HWND)。 若要取得可用於應用程式 Unmanaged 部分的 HWND,請將 Handle.ToPointer() 轉型為 HWND。

  5. 實作 Managed 類別,其中包含靜態欄位,以保存 WPF 內容的參考。 這個類別可讓您從 Win32 程式碼取得 WPF 內容的參考。

  6. 將 WPF 內容指派給靜態欄位。

  7. 將處理常式附加至一或多個 WPF 事件,以接收來自 WPF 內容的通知。

  8. 使用您儲存在靜態欄位中的參考來設定屬性等等,與 WPF 內容通訊。

注意

您也可以使用 WPF 內容。 不過,您必須將它個別編譯為動態連結程式庫 (DLL),並從您的 Win32 應用程式參考該 DLL。 此程序的其餘部分與上述類似。

實作主應用程式

本節說明如何在基本的 Win32 應用程式中裝載 WPF 內容。 內容本身會在 C++/CLI 中實作為 Managed 類別。 在大多數情況下,它是簡單的 WPF 程式設計。 實作 WPF 內容將會討論內容實作的重要部分。

基本應用程式

主應用程式的起點是建立 Visual Studio 2005 範本。

  1. 開啟 Visual Studio 2005,然後從 [檔案 ] 功能表中選取 [ 新增專案 ]。

  2. 從 Visual C++ 專案類型清單中選取 [Win32 ]。 如果您的預設語言不是 C++,您會在 [其他語言 ] 底下 找到這些專案類型。

  3. 選取 [Win32 專案] 範本,並為專案指派名稱,然後按一下 [確定],以啟動 [Win32 應用程式精靈]

  4. 接受精靈的預設設定,然後按一下 [完成] 來啟動專案。

此範本會建立基本的 Win32 應用程式,包括:

  • 應用程式的進入點。

  • 視窗,具有相關聯的視窗程序 (WndProc)。

  • 具有 [檔案 ] 和 [說明 ] 標題的功能表。 [ 檔案 ] 功能表具有關閉應用程式的 [結束 ] 專案。 [說明] 功能表有可啟動簡單對話方塊的 [關於] 項目。

開始撰寫程式碼以裝載 WPF 內容之前,您需要對基本範本進行兩項修改。

第一個是將專案編譯成 Managed 程式碼。 根據預設,專案會編譯成 Unmanaged 程式碼。 不過,由於 WPF 是在 Managed 程式碼中實作,因此必須據以編譯專案。

  1. 以滑鼠右鍵按一下方案總管中的專案名稱,然後從內容功能表中選取 [屬性],以啟動 [屬性頁] 對話方塊。

  2. 在左窗格中,從樹狀檢視中選取 [組態屬性]

  3. 在右窗格中,從 [專案預設值] 清單選取 [Common Language Runtime] 支援。

  4. 從下拉式清單方塊中,選取 [Common Language Runtime 支援 (/clr)]

注意

此編譯器旗標可讓您在應用程式中使用 Managed 程式碼,但 Unmanaged 程式碼還是會編譯成和之前一樣。

WPF 使用單一執行緒 Apartment (STA) 執行緒模型。 若要正常使用 WPF 內容程式碼,您必須將屬性套用至進入點,將應用程式的執行緒模型設定為 STA。

[System::STAThreadAttribute] //Needs to be an STA thread to play nicely with WPF
int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{

裝載 WPF 內容

WPF 內容是簡單的位址輸入應用程式。 它包含數個 TextBox 控制項,以取得使用者名稱、位址等等。 另外還有兩 Button 個控制項: [確定 ] 和 [ 取消 ]。 當使用者按一下 [確定] 時,按鈕的 Click 事件處理常式會從 TextBox 控制項收集資料、將它指派給對應的屬性,並引發自訂事件 。 OnButtonClicked 使用者按一下 [Cancel] 時,處理常式只會引發 OnButtonClickedOnButtonClicked 的事件引數物件包含布林值的欄位,指出所點選的按鈕。

裝載 WPF 內容的程式碼會在主視窗上WM_CREATE 通知的 處理常式中實作。

case WM_CREATE :
  GetClientRect(hWnd, &rect);
  wpfHwnd = GetHwnd(hWnd, rect.right-375, 0, 375, 250);
  CreateDataDisplay(hWnd, 275, rect.right-375, 375);
  CreateRadioButtons(hWnd);
break;

方法 GetHwnd 會採用大小和位置資訊加上父視窗控制碼,並傳回裝載 WPF 內容的視窗控制碼。

注意

您不能將 #using 指示詞用於 System::Windows::Interop 命名空間。 這麼做會在該命名空間中的 MSG 結構與 winuser.h 中宣告的 MSG 結構之間,造成名稱衝突。 您必須改用完整的名稱來存取該命名空間的內容。

HWND GetHwnd(HWND parent, int x, int y, int width, int height)
{
    System::Windows::Interop::HwndSourceParameters^ sourceParams = gcnew System::Windows::Interop::HwndSourceParameters(
    "hi" // NAME
    );
    sourceParams->PositionX = x;
    sourceParams->PositionY = y;
    sourceParams->Height = height;
    sourceParams->Width = width;
    sourceParams->ParentWindow = IntPtr(parent);
    sourceParams->WindowStyle = WS_VISIBLE | WS_CHILD; // style
    System::Windows::Interop::HwndSource^ source = gcnew System::Windows::Interop::HwndSource(*sourceParams);
    WPFPage ^myPage = gcnew WPFPage(width, height);
    //Assign a reference to the WPF page and a set of UI properties to a set of static properties in a class
    //that is designed for that purpose.
    WPFPageHost::hostedPage = myPage;
    WPFPageHost::initBackBrush = myPage->Background;
    WPFPageHost::initFontFamily = myPage->DefaultFontFamily;
    WPFPageHost::initFontSize = myPage->DefaultFontSize;
    WPFPageHost::initFontStyle = myPage->DefaultFontStyle;
    WPFPageHost::initFontWeight = myPage->DefaultFontWeight;
    WPFPageHost::initForeBrush = myPage->DefaultForeBrush;
    myPage->OnButtonClicked += gcnew WPFPage::ButtonClickHandler(WPFButtonClicked);
    source->RootVisual = myPage;
    return (HWND) source->Handle.ToPointer();
}

您無法直接在應用程式視窗中裝載 WPF 內容。 相反地,您會先建立 HwndSource 物件來包裝 WPF 內容。 此物件基本上是設計用來裝載 WPF 內容的視窗。 HwndSource您可以將物件建立為屬於您應用程式的 Win32 視窗子系,以在父視窗中裝載物件。 建 HwndSource 構函式參數包含您在建立 Win32 子視窗時會傳遞至 CreateWindow 的相同資訊。

接下來,您會建立 WPF 內容物件的實例。 在此情況下,WPF 內容會使用 C++/CLI 實作為個別類別 WPFPage 。 您也可以使用 XAML 實作 WPF 內容。 不過,若要這樣做,您必須設定個別的專案,並將 WPF 內容建置為 DLL。 您可以將該 DLL 的參考新增至專案,並使用該參考來建立 WPF 內容的實例。

您可以將 WPF 內容的參考指派給 RootVisual 的 屬性 HwndSource ,以在子視窗中顯示 WPF 內容。

下一行程式碼會將事件處理常式 WPFButtonClicked 附加至 WPF 內容 OnButtonClicked 事件。 使用者按一下 [OK] 或 [Cancel] 按鈕時,就會呼叫此處理常式。 請參閱 communicating_with_the_WPF content,以取得此事件處理常式的進一步討論。

所顯示的最後一行程式碼會傳回與 HwndSource 物件相關聯的視窗控制代碼 (HWND)。 您可以從 Win32 程式碼使用此控制碼,將訊息傳送至託管視窗,不過範例不會這麼做。 HwndSource 物件每次收到訊息時,都會引發事件。 若要處理這些訊息,請呼叫 AddHook 方法來附加訊息處理常式,然後在該處理常式中處理訊息。

保存 WPF 內容的參考

對於許多應用程式,您稍後會想要與 WPF 內容通訊。 例如,您可能想要修改 WPF 內容屬性,或是讓 HwndSource 物件裝載不同的 WPF 內容。 若要這樣做,您需要物件或 WPF 內容的參考 HwndSource 。 物件 HwndSource 及其相關聯的 WPF 內容會保留在記憶體中,直到您終結視窗控制碼為止。 不過,一旦您從視窗程序返回,您指派給 HwndSource 物件的變數就會超出範圍。 處理 Win32 應用程式的此問題的慣用方式是使用靜態或全域變數。 可惜的是,您無法將 Managed 物件指派給這些類型的變數。 您可以將與 HwndSource 物件相關聯的視窗控制代碼指派給全域或靜態變數,但這樣並不能存取物件本身。

此問題最簡單的解決方案,就是實作 Managed 類別,其中包含一組靜態欄位來保存您需要存取之任何 Managed 物件的參考。 此範例會使用 類別 WPFPageHost 來保存 WPF 內容的參考,加上使用者稍後可能會變更之數個屬性的初始值。 這定義在標頭中。

public ref class WPFPageHost
{
public:
  WPFPageHost();
  static WPFPage^ hostedPage;
  //initial property settings
  static System::Windows::Media::Brush^ initBackBrush;
  static System::Windows::Media::Brush^ initForeBrush;
  static System::Windows::Media::FontFamily^ initFontFamily;
  static System::Windows::FontStyle initFontStyle;
  static System::Windows::FontWeight initFontWeight;
  static double initFontSize;
};

GetHwnd 函式的後半部會將值指派給這些欄位,以供日後使用,而 myPage 仍在範圍內。

與 WPF 內容通訊

WPF 內容有兩種類型的通訊。 當使用者按一下 [確定 ] 或 [取消 ] 按鈕時,應用程式會從 WPF 內容接收資訊。 應用程式也有 UI,可讓使用者變更各種 WPF 內容屬性,例如背景色彩或預設字型大小。

如上所述,當使用者按一下其中一個 OnButtonClicked 按鈕時,WPF 內容就會引發事件。 應用程式會將處理常式附加至這個事件,以接收這些通知。 如果按一下 [ 確定] 按鈕,處理常式會從 WPF 內容取得使用者資訊,並將其顯示在一組靜態控制項中。

void WPFButtonClicked(Object ^sender, MyPageEventArgs ^args)
{
    if(args->IsOK) //display data if OK button was clicked
    {
        WPFPage ^myPage = WPFPageHost::hostedPage;
        LPCWSTR userName = (LPCWSTR) InteropServices::Marshal::StringToHGlobalAuto("Name: " + myPage->EnteredName).ToPointer();
        SetWindowText(nameLabel, userName);
        LPCWSTR userAddress = (LPCWSTR) InteropServices::Marshal::StringToHGlobalAuto("Address: " + myPage->EnteredAddress).ToPointer();
        SetWindowText(addressLabel, userAddress);
        LPCWSTR userCity = (LPCWSTR) InteropServices::Marshal::StringToHGlobalAuto("City: " + myPage->EnteredCity).ToPointer();
        SetWindowText(cityLabel, userCity);
        LPCWSTR userState = (LPCWSTR) InteropServices::Marshal::StringToHGlobalAuto("State: " + myPage->EnteredState).ToPointer();
        SetWindowText(stateLabel, userState);
        LPCWSTR userZip = (LPCWSTR) InteropServices::Marshal::StringToHGlobalAuto("Zip: " + myPage->EnteredZip).ToPointer();
        SetWindowText(zipLabel, userZip);
    }
    else
    {
        SetWindowText(nameLabel, L"Name: ");
        SetWindowText(addressLabel, L"Address: ");
        SetWindowText(cityLabel, L"City: ");
        SetWindowText(stateLabel, L"State: ");
        SetWindowText(zipLabel, L"Zip: ");
    }
}

處理常式會從 WPF 內容接收自訂事件引數物件。 MyPageEventArgs 如果 按一下 [確定 ] 按鈕,而且 false 已按一下 [取消 ] 按鈕,則物件的 IsOK 屬性會設定 true 為 。

如果按一下 [ 確定] 按鈕,處理常式會從容器類別取得 WPF 內容的參考。 然後,它會收集相關聯 WPF 內容屬性所持有的使用者資訊,並使用靜態控制項在父視窗上顯示資訊。 因為 WPF 內容資料的格式為 Managed 字串,所以它必須封送處理以供 Win32 控制項使用。 如果按一下 [Cancel] 按鈕,則處理常式會清除靜態控制項中的資料。

應用程式 UI 提供一組選項按鈕,可讓使用者修改 WPF 內容的背景色彩,以及數個字型相關屬性。 下列範例摘錄自應用程式的視窗程序 (WndProc) 及其訊息處理作業,該作業在不同的訊息上設定各種屬性,包括背景色彩。 其他皆相似,所以不顯示。 請參閱完整範例,以了解詳細資料和內容。

case WM_COMMAND:
  wmId    = LOWORD(wParam);
  wmEvent = HIWORD(wParam);

  switch (wmId)
  {
  //Menu selections
    case IDM_ABOUT:
      DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
    break;
    case IDM_EXIT:
      DestroyWindow(hWnd);
    break;
    //RadioButtons
    case IDC_ORIGINALBACKGROUND :
      WPFPageHost::hostedPage->Background = WPFPageHost::initBackBrush;
    break;
    case IDC_LIGHTGREENBACKGROUND :
      WPFPageHost::hostedPage->Background = gcnew SolidColorBrush(Colors::LightGreen);
    break;
    case IDC_LIGHTSALMONBACKGROUND :
      WPFPageHost::hostedPage->Background = gcnew SolidColorBrush(Colors::LightSalmon);
    break;

若要設定背景色彩,請從 WPFPageHost 取得 WPF 內容的參考, hostedPage 並將背景色彩屬性設定為適當的色彩。 此範例使用三種色彩選項:原始色彩、淺綠色或淺橙紅。 原始背景色彩會在 WPFPageHost 類別中儲存為靜態欄位。 若要設定其他兩個色彩,您要建立新的 SolidColorBrush 物件,並從 Colors 物件傳遞靜態色彩值給建構函式。

實作 WPF 頁面

您可以裝載和使用 WPF 內容,而不需要任何實際實作的知識。 如果 WPF 內容已封裝在不同的 DLL 中,它可能已建置在任何 Common Language Runtime (CLR) 語言中。 以下是範例中使用的 C++/CLI 實作簡短逐步解說。 本節包含下列小節。

版面配置

WPF 內容中的 UI 元素包含五 TextBox 個控制項,其中包含相關聯的 Label 控制項:Name、Address、City、State 和 Zip。 另外還有兩 Button 個控制項: [確定 ] 和 [ 取消]

WPF 內容會在 類別中實作 WPFPage 。 配置是以 Grid 配置項目來處理。 類別繼承自 Grid ,有效地使其成為 WPF 內容根項目。

WPF 內容建構函式會採用所需的寬度和高度,並據此調整大小 Grid 。 然後,它會建立一組 ColumnDefinitionRowDefinition 物件,並將其分別新增至 Grid 物件基底 ColumnDefinitionsRowDefinitions 集合,以定義基本版面配置。 這會定義一個包含五個資料列和七個資料行的格線,維度則取決於儲存格的內容。

WPFPage::WPFPage(int allottedWidth, int allotedHeight)
{
  array<ColumnDefinition ^> ^ columnDef = gcnew array<ColumnDefinition ^> (4);
  array<RowDefinition ^> ^ rowDef = gcnew array<RowDefinition ^> (6);

  this->Height = allotedHeight;
  this->Width = allottedWidth;
  this->Background = gcnew SolidColorBrush(Colors::LightGray);
  
  //Set up the Grid's row and column definitions
  for(int i=0; i<4; i++)
  {
    columnDef[i] = gcnew ColumnDefinition();
    columnDef[i]->Width = GridLength(1, GridUnitType::Auto);
    this->ColumnDefinitions->Add(columnDef[i]);
  }
  for(int i=0; i<6; i++)
  {
    rowDef[i] = gcnew RowDefinition();
    rowDef[i]->Height = GridLength(1, GridUnitType::Auto);
    this->RowDefinitions->Add(rowDef[i]);
  }

接下來,建構函式會將 UI 元素新增至 Grid 。 第一個項目是標題文字,它是位於格線第一個資料列中間的 Label 控制項。

//Add the title
titleText = gcnew Label();
titleText->Content = "Simple WPF Control";
titleText->HorizontalAlignment = System::Windows::HorizontalAlignment::Center;
titleText->Margin = Thickness(10, 5, 10, 0);
titleText->FontWeight = FontWeights::Bold;
titleText->FontSize = 14;
Grid::SetColumn(titleText, 0);
Grid::SetRow(titleText, 0);
Grid::SetColumnSpan(titleText, 4);
this->Children->Add(titleText);

下一個資料列包含名稱 Label 控制項及其相關聯的 TextBox 控制項。 由於每一個標籤/文字方塊組都是使用相同的程式碼,所以程式碼會放在一個私用方法組中,然後用於所有五個標籤/文字方塊組。 這些方法會建立適當的控制項,並呼叫 Grid 類別靜態 SetColumnSetRow 方法,以將控制項放在適當的儲存格中。 建立控制項之後,此範例會在 AddChildren 屬性上呼叫 Grid 方法,以將控制項加入格線中。 用來加入其餘標籤/文字方塊組的程式碼很類似。 如需詳細資訊,請參閱範例程式碼。

//Add the Name Label and TextBox
nameLabel = CreateLabel(0, 1, "Name");
this->Children->Add(nameLabel);
nameTextBox = CreateTextBox(1, 1, 3);
this->Children->Add(nameTextBox);

這兩個方法的實作如下所示:

Label ^WPFPage::CreateLabel(int column, int row, String ^ text)
{
  Label ^ newLabel = gcnew Label();
  newLabel->Content = text;
  newLabel->Margin = Thickness(10, 5, 10, 0);
  newLabel->FontWeight = FontWeights::Normal;
  newLabel->FontSize = 12;
  Grid::SetColumn(newLabel, column);
  Grid::SetRow(newLabel, row);
  return newLabel;
}
TextBox ^WPFPage::CreateTextBox(int column, int row, int span)
{
  TextBox ^newTextBox = gcnew TextBox();
  newTextBox->Margin = Thickness(10, 5, 10, 0);
  Grid::SetColumn(newTextBox, column);
  Grid::SetRow(newTextBox, row);
  Grid::SetColumnSpan(newTextBox, span);
  return newTextBox;
}

最後,此範例會新增 [確定 ] 和 [取消 ] 按鈕,並將事件處理常式附加至其 Click 事件。

//Add the Buttons and atttach event handlers
okButton = CreateButton(0, 5, "OK");
cancelButton = CreateButton(1, 5, "Cancel");
this->Children->Add(okButton);
this->Children->Add(cancelButton);
okButton->Click += gcnew RoutedEventHandler(this, &WPFPage::ButtonClicked);
cancelButton->Click += gcnew RoutedEventHandler(this, &WPFPage::ButtonClicked);

將資料傳回主視窗

按一下任一按鈕,就會引發其 Click 事件。 主視窗只要將處理常式附加至這些事件,就可以直接從 TextBox 控制項取得資料。 此範例使用較不直接的方法。 它會處理 Click WPF 內容內的 ,然後引發自訂事件 OnButtonClicked ,以通知 WPF 內容。 這可讓 WPF 內容在通知主機之前執行一些參數驗證。 處理常式會從 TextBox 控制項取得文字,再將其指派給公用屬性,然後主機再從中擷取資訊。

WPFPage.h 中的事件宣告:

public:
  delegate void ButtonClickHandler(Object ^, MyPageEventArgs ^);
  WPFPage();
  WPFPage(int height, int width);
  event ButtonClickHandler ^OnButtonClicked;

WPFPage.cpp 中的 Click 事件處理常式:

void WPFPage::ButtonClicked(Object ^sender, RoutedEventArgs ^args)
{

  //TODO: validate input data
  bool okClicked = true;
  if(sender == cancelButton)
    okClicked = false;
  EnteredName = nameTextBox->Text;
  EnteredAddress = addressTextBox->Text;
  EnteredCity = cityTextBox->Text;
  EnteredState = stateTextBox->Text;
  EnteredZip = zipTextBox->Text;
  OnButtonClicked(this, gcnew MyPageEventArgs(okClicked));
}

設定 WPF 屬性

Win32 主機可讓使用者變更數個 WPF 內容屬性。 從 Win32 端,這只是變更屬性的問題。 WPF 內容類別別中的實作比較複雜,因為沒有任何單一全域屬性可控制所有控制項的字型。 相反地,每個控制項的適當屬性會在屬性的 set 存取子中變更。 下列範例顯示 屬性的程式 DefaultFontFamily 代碼。 設定屬性時會呼叫一個私用方法,它接著會設定各種控制項的 FontFamily 屬性。

從 WPFPage.h:

property FontFamily^ DefaultFontFamily
{
  FontFamily^ get() {return _defaultFontFamily;}
  void set(FontFamily^ value) {SetFontFamily(value);}
};

從 WPFPage.cpp:

void WPFPage::SetFontFamily(FontFamily^ newFontFamily)
{
  _defaultFontFamily = newFontFamily;
  titleText->FontFamily = newFontFamily;
  nameLabel->FontFamily = newFontFamily;
  addressLabel->FontFamily = newFontFamily;
  cityLabel->FontFamily = newFontFamily;
  stateLabel->FontFamily = newFontFamily;
  zipLabel->FontFamily = newFontFamily;
}

另請參閱