逐步解說:在 Win32 中裝載 WPF 內容
Windows Presentation Foundation (WPF) 提供一個用來建立應用程式的豐富環境。 不過,如果您已長期開發 Win32 程式碼,將 WPF 功能加入應用程式,可能會比重寫原始程式碼更有效率。 WPF 提供了一個直覺化的機制,可以在 Win32 視窗中裝載 WPF 頁面。
本教學課程描述如何撰寫範例應用程式:Hosting WPF Content in a Win32 Window Sample(在 Win32 視窗中裝載 WPF 內容的範例),以在 Win32 視窗中裝載 WPF 內容。 您可以擴充這個範例,以裝載任何 Win32 視窗。 因為這牽涉到混合 Managed 和 Unmanaged 程式碼,所以是以 C++/CLI 撰寫應用程式。
需求
本教學課程假設您對 WPF 和 Win32 程式設計已有基本的熟悉度。 若需 WPF 程式設計的基本簡介,請參閱開始使用。 如需 Win32 程式設計的簡介,您應參考有關該主旨的任何書籍,這類書籍很多,請特別參考 Charles Petzold 的《Windows 程式設計》(Programming Windows)。
因為隨附本教學課程的範例是在 C++/CLI 中實作,所以本教學課程假設您已熟悉如何使用 C++ 來設計 Windows API 程式,並且了解 Managed 程式碼的程式設計。 熟悉 C++/CLI 會有幫助,但並非必要。
注意
本教學課程包含一些來自相關範例的程式碼範例。 不過,為了方便閱讀,並未包含完整的範例程式碼。 如需完整的範例程式碼,請參閱 Hosting WPF Content in a Win32 Window Sample (在 Win32 視窗中裝載 WPF 內容的範例)。
基本程序
本節概述在 Win32 視窗中裝載 WPF 內容的基本程序。 其餘各節將說明每個步驟的詳細資訊。
在 Win32 視窗上裝載 WPF 內容的索引鍵是 HwndSource 類別。 這個類別會包裝 Win32 視窗中的 WPF 內容,以將 WPF 內容併入使用者介面中作為子視窗。 下列方法會將 Win32 和 WPF 合併在單一應用程式中。
將 WPF 內容實作為 Managed 類別。
使用 C++/CLI 實作 Windows 應用程式。 如果您是使用現有的應用程式和 Unmanaged C++ 程式碼開始進行,通常只要變更專案設定來包含
/clr
編譯器旗標,該應用程式就可以呼叫 Managed 程式碼。將執行緒模型設為單一執行緒 Apartment (STA)。
處理視窗程序中的 WM_CREATE 通知,並執行下列動作:
以父視窗做為 HwndSource 參數,建立新的
parent
物件。建立 WPF 內容類別的實例。
將 WPF 內容物件的參考指派給 HwndSource 的 RootVisual 屬性。
取得內容的 HWND。 Handle 物件的 HwndSource 屬性包含視窗控制代碼 (HWND)。 若要取得可用於應用程式 Unmanaged 部分的 HWND,請將
Handle.ToPointer()
轉型為 HWND。
實作 Managed 類別,其中包含靜態欄位來保存 WPF 內容的參考。 這個類別可讓您從 Win32 程式碼取得 WPF 內容的參考。
將 WPF 內容指派至靜態欄位。
將處理常式附加到一或多個 WPF 事件,以接收 WPF 內容的通知。
使用儲存在靜態欄位中的參考來設定屬性等等,以與 WPF 內容通訊。
注意
您也可以使用 WPF 內容。 不過,您必須另外將它編譯成動態連結程式庫 (DLL),並從 Win32 應用程式參考該 DLL。 此程序的其餘部分與上述類似。
實作主應用程式
本節描述如何在基本 Win32 應用程式中裝載 WPF 內容。 內容本身實作在 C++/CLI 中,做為 Managed 類別。 大部分的情況下,這就是 WPF 程式設計。 實作 WPF 內容將會討論內容實作的重要部分。
基本應用程式
主應用程式的起點是建立 Visual Studio 2005 範本。
開啟 Visual Studio 2005,然後選取 [檔案] 功能表的 [新增專案]。
從 Visual C++ 專案類型的清單中,選取 [Win32]。 如果您的預設語言不是 C++,您會在 [其他語言] 底下找到這些專案類型。
選取 [Win32 專案] 範本,並為專案指派名稱,然後按一下 [確定],以啟動 [Win32 應用程式精靈]。
接受精靈的預設設定,然後按一下 [完成] 來啟動專案。
此範本會建立基本 Win32 應用程式,包括:
應用程式的進入點。
視窗,具有相關聯的視窗程序 (WndProc)。
含有 [檔案] 和 [說明] 標題的功能表。 [檔案] 功能表有可關閉應用程式的 [結束] 項目。 [說明] 功能表有可啟動簡單對話方塊的 [關於] 項目。
在您開始撰寫程式碼來裝載 WPF 內容之前,您必須先對基本範本進行兩次修改。
第一個是將專案編譯成 Managed 程式碼。 根據預設,專案會編譯成 Unmanaged 程式碼。 不過,因為 WPF 是以 Managed 程式碼實作,所以專案必須據以編譯。
以滑鼠右鍵按一下方案總管中的專案名稱,然後從內容功能表中選取 [屬性],以啟動 [屬性頁] 對話方塊。
在左窗格中,從樹狀檢視中選取 [組態屬性]。
在右窗格中,從 [專案預設值] 清單選取 [Common Language Runtime] 支援。
從下拉式清單方塊中,選取 [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] 時,處理常式只會引發 OnButtonClicked
。 OnButtonClicked
的事件引數物件包含布林值的欄位,指出所點選的按鈕。
裝載 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 內容的參考指派給 HwndSource 的 RootVisual 屬性,以在子視窗中顯示 WPF 內容。
下一行程式碼會將事件處理常式 WPFButtonClicked
附加至 WPF 內容 OnButtonClicked
事件。 使用者按一下 [OK] 或 [Cancel] 按鈕時,就會呼叫此處理常式。 請參閱 communicating_with_the_WPF content,以取得此事件處理常式的進一步討論。
所顯示的最後一行程式碼會傳回與 HwndSource 物件相關聯的視窗控制代碼 (HWND)。 您可以從 Win32 程式碼使用此控制代碼,將訊息傳送至裝載的視窗,但此範例並沒有這麼做。 HwndSource 物件每次收到訊息時,都會引發事件。 若要處理這些訊息,請呼叫 AddHook 方法來附加訊息處理常式,然後在該處理常式中處理訊息。
保存 WPF 內容的參考
就許多應用程式而言,您稍後會想要與 WPF 內容通訊。 例如,您可能會想要修改 WPF 內容屬性,或是讓 HwndSource 物件裝載不同的 WPF 內容。 若要這麼做,您需要 HwndSource 物件或 WPF 內容的參考。 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 內容屬性,例如背景色彩或預設字型大小。
如上所述,當使用者按一下任一按鈕時,WPF 內容就會引發 OnButtonClicked
事件。 應用程式會將處理常式附加至這個事件,以接收這些通知。 如果使用者按一下 [確定] 按鈕,處理常式就會從 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
接收自訂事件引數物件。 如果按一下 [確定] 按鈕,物件的 IsOK
屬性會設定為 true
;如果按一下 [取消] 按鈕,則會設定為 false
。
如果按一下 [確定] 按鈕,處理常式就會從容器類別取得 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 中,它可能是以任何通用語言執行平台 (CLR) 語言所建置。 以下是範例中所使用之 C++/CLI 實作的簡短逐步解說。 本節包含下列小節。
版面配置
WPF 內容中的 UI 元素包含五個 TextBox 控制項,並具有相關聯的 Label 控制項:名稱、地址、縣 (市)、州 (省) 和郵遞區號。 另外還有兩個 Button 控制項:[確定] 和 [取消]
WPF 內容會在 WPFPage
類別中實作。 配置是以 Grid 配置項目來處理。 類別繼承自 Grid,如此能有效使其成為 WPF 內容根項目。
WPF 內容建構函式會採用所需的寬度和高度,並視情況調整 Grid 大小。 然後它會建立一組 ColumnDefinition 和 RowDefinition 物件,將其分別加入 Grid 基底物件 ColumnDefinitions 和 RowDefinitions 集合中,以定義基本配置。 這會定義一個包含五個資料列和七個資料行的格線,維度則取決於儲存格的內容。
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 類別靜態 SetColumn 和 SetRow 方法,以將控制項放在適當的儲存格中。 建立控制項之後,此範例會在 Add 的 Children 屬性上呼叫 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 控制項取得資料。 此範例使用較不直接的方法。 它會處理 WPF 內容中的 Click,然後引發自訂事件 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;
}