Пошаговое руководство. Размещение содержимого WPF в Win32
Клиент Windows Presentation Foundation (WPF) предоставляет среду с широкими возможностями для создания приложений. Однако, если имеются существенные преимущества в коде Win32, возможно, более эффективным будет добавление функциональности WPF в приложение вместо переписывания первоначального кода. WPF предоставляет простой механизм для размещения содержимого WPF в окне Win32.
В этом руководстве показано, как написать пример приложения, Пример размещения содержимого WPF в окне Win32, в котором содержимое WPF размещается в окне Win32. Вы можете расширить этот пример, чтобы разместить любое окно Win32. Поскольку он включает в себя смешение управляемого и неуправляемого кода, приложение пишется в C++/CLI.
В этом разделе содержатся следующие подразделы.
- Требования
- Основная процедура
- Реализация ведущего приложения
- Реализация WPF страницы
- Связанные разделы
Требования
Это руководство предполагает начальное знакомство с программированием WPF и Win32. Введение в программирование WPF см. в разделе Приступая к работе (WPF). Для знакомства с программированием Win32 следует обратиться к любой из многочисленных книг по этой теме, например, к Programming Windows, автор Charles Petzold.
Поскольку образец, сопровождающий это руководство реализован в C++/CLI, это руководство предполагает знакомство с использованием C++, чтобы программировать Win32 API, плюс понимание программирования управляемого кода. Знакомство с C++/CLI является полезным, но не необходимым.
![]() |
---|
Это руководство включает ряд примеров кода из сопровождающего его образца.Тем не менее, для удобства чтения, он не включает в себя полный код примера.Полный код примера см. в разделе Пример размещения содержимого WPF в окне Win32. |
Основная процедура
В данном разделе описывается основная процедура, которую вы используете для размещения содержимого WPF в окне Win32. В остальных разделах описаны подробности каждого шага.
Ключом к размещению содержимого WPF в окне Win32 является класс HwndSource. Этот класс переносит содержимое WPF в окне Win32, позволяя ему присоединиться к вашему user interface (UI) в качестве дочернего окна. Следующий подход объединяет Win32 и WPF в одном приложении.
Реализуйте содержимое WPF в виде управляемого класса.
Реализуйте приложение Win32 с C++/CLI. Если вы запускаете с существующим приложением и неуправляемый C++ код, то обычно вы можете позволить ему вызывать управляемый код, изменив параметры проекта, чтобы включить флаг компилятора /clr.
Установите потоковую модель в однопотоковое подразделение (single-threaded apartment, STA).
Обработайте уведомление WM_CREATE в процедуре окна и выполните следующие действия.
Создайте новый объект HwndSource с родительским окном в качестве его параметра parent.
Создайте экземпляр класса содержимого WPF.
Назначьте ссылку в объекте содержимого WPF на объект RootVisual свойства HwndSource.
Получить HWND для содержимого. Свойство Handle объекта HwndSource содержит описатель окна (HWND). Для получения HWND, который можно использовать в неуправляемой части приложения, преобразуйте Handle.ToPointer() в HWND.
Реализовать управляемый класс, содержащий статическое поле для размещения ссылки на ваше содержимое WPF. Этот класс позволяет вам получить ссылку на содержимое WPF из вашего кода Win32.
Присвоить содержимое WPF статическому полю.
Получить уведомления от содержимого WPF путем присоединения обработчика к одному или нескольким событиям WPF.
Установить связь с содержимым WPF, используя ссылку, которую вы поместили в статическое поле для задания свойств и т. д.
![]() |
---|
Вы можете использовать Extensible Application Markup Language (XAML) для реализации вашего содержимого WPF.Однако вам надо будет скомпилировать его отдельно как dynamic-link library (DLL) и сделать ссылку на эту DLL из вашего приложения Win32.Оставшаяся часть процедуры аналогична описанной выше. |
Реализация ведущего приложения
В этом разделе описывается как разместить содержимое WPF в основном приложении Win32. Содержимое само реализуется в C++/CLI как управляемый класс. В большинстве случаев это простое программирование WPF. Ключевые аспекты реализации содержимого обсуждаются в разделе Реализация содержимого WPF.
Основное Приложение
Размещение содержимого WPF
Хранение ссылки на содержимое WPF
Установка связи с содержимым WPF
Основное Приложение
Создание шаблона Microsoft Visual Studio 2005 является начальной точкой для главного приложения.
Откройте Visual Studio 2005 и выберите Новый Проект из меню Файл.
Выберите Win32 из списка типов проектов Visual C++. Если выбранный по умолчанию язык отличен от C++, то эти типы проектов будут располагаться под пунктом Другие Языки.
Выберите шаблон Проект Win32, введите имя проекта и нажмите кнопку ОК для запуска Мастера Приложений Win32.
Примите параметры мастера по умолчанию и нажмите кнопку Готово для запуска проекта.
Этот шаблон создает базовое приложение Win32, включающее:
Точку входа для приложения.
Окно со связанной процедурой окна (WndProc).
Меню с заголовками Файл и Справка. В меню Файл присутствует элемент Выход, который закрывает приложение. Меню Справка содержит элемент О Программе который запускает просто диалоговое окно.
Перед началом написания кода для размещения содержимого WPF вам необходимо внести два изменения в базовый шаблон.
Во-первых, скомпилировать проект как управляемый код. По умолчанию проект компилируется как неуправляемый код. Однако, поскольку WPF реализован в управляемом коде, проект должен быть скомпилирован соответствующим образом.
Щелкните правой кнопкой мыши имя проекта в Обозреватели Решений и выберите Свойства из контекстного меню для запуска диалогового окна Страницы свойств.
Выберите Свойства Конфигурации из иерархического дерева в левой области.
Выберите поддержку Среда CLR из списка значений по умолчанию для проекта в правой области.
Выберите Поддержка среды CLR (/clr) из раскрывающегося списка.
![]() |
---|
Этот флаг компилятора позволяет использовать управляемый код в вашем приложении, но ваш неуправляемый код будет по-прежнему компилироваться, как и до этого. |
WPF использует потоковую модель однопотокового подразделения (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. Когда пользователь щелкает Отмена, обработчик просто вызывает 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 в этом пространстве имен и структурой MSG, объявленной в winuser.h.Вместо этого необходимо использовать полные имена для доступа к содержимому этого пространства имен. |
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 содержат похожую информацию, которую вы будете передавать CreateWindow при создании дочернего окна Win32.
Затем создайте экземпляр объекта содержимого WPF. В этом случае содержимое WPF реализуется как отдельный класс WPFPage с помощью C++/CLI. Также вы можете реализовывать содержимое WPF с XAML. Однако для этого вам необходимо настроить отдельный проект и построить содержимое WPF как DLL. Можно добавить ссылку на DLL в ваш проект и использовать эту ссылку для создания экземпляра содержимого WPF.
Вы отображаете содержимое WPF в вашем дочернем окне путем установки ссылки на содержимое WPF на свойство RootVisual из HwndSource.
В следующей строке кода присоединяется обработчик событий WPFButtonClicked к событию OnButtonClicked содержимого WPF. Этот обработчик вызывается, когда пользователь нажимает кнопку ОК или Отмена. Дальнейшее рассмотрение этого обработчика событий содержится в разделе communicating_with_the_WPF content
Последняя строка кода показывает возврат дескриптора окна (HWND), который связан с объектом HwndSource. Вы можете использовать этот дескриптор из вашего кода Win32 для отправки сообщений основному окну, хотя пример это не делает. Объект HwndSource вызывает событие каждый раз, когда он получает сообщение. Для обработки сообщений, вызовите метод AddHook, чтобы присоединить обработчик сообщений и затем обрабатывать сообщения в этом обработчике.
Хранение ссылки на содержимое WPF
Во многих приложениях вам будет необходимо установить связь с содержимым WPF позже. Например, вам может потребоваться изменить свойства содержимого WPF, или, возможно чтобы объект HwndSource располагал различное содержимое WPF. Для этого вам потребуется ссылка на объект HwndSource или на содержимое WPF. Объект HwndSource и связанное с ним содержимое WPF остаются в памяти до тех пор, пока вы не уничтожите дескриптор окна. Тем не менее, переменная, которой вы присвоите объект HwndSource уйдет из области видимости сразу по возврату из процедуры окна. Обычным способом решить эту проблему с приложениями Win32 является использование статической или глобальной переменной. К сожалению, вы не можете присвоить управляемый объект к этим типам переменных. Вы можете присвоить дескриптор окна, связанный с объектом HwndSource, глобальной или статической переменной, но которой не предоставляется доступа к самому объекту.
Самым простым решением этой проблемы является реализация управляемого класса, содержащего набор статических полей для хранения ссылок на любые управляемые объекты, к которым вам необходим доступ. Образец использует класс 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 являются формой управляемой строки, они должны быть маршалированы для использования элементом управления Win32. Если была нажата кнопка Отмена, обработчик удаляет данные из статических элементов управления.
Приложение 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;
Чтобы задать цвет фона, получите ссылку на содержимое WPF (hostedPage) из WPFPageHost и установите свойство цвета фона в соответствующий цвет. Образец использует три параметра цвета: исходный цвет, светло-зеленый или оранжево-розовый. Исходный цвет фона хранится в виде статического поля в классе WPFPageHost. Чтобы задать другие два, создайте новый объект SolidColorBrush и передайте в конструктор значение статического цвета из объекта Colors.
Реализация WPF страницы
Вы можете размещать и использовать содержимое WPF без каких-либо знаний о фактической реализации. Если содержимое WPF было упаковано в отдельное DLL, оно может быть встроено на любом языке common language runtime (CLR). Ниже приведен краткий пример реализации C++/CLI, который используется в образце. Этот подраздел состоит из следующих пунктов.
Макет
Возврат Данных Основному Окну
Установка свойств WPF
Макет
Элементы UI содержимого WPF состоят из пяти элементов управления TextBox со связанными элементами управления Label: Name, Address, City, State и Zip. Существуют также два элемента управления Button: ОК и Отмена.
Содержимое WPF реализовано в классе WPFPage. Макет обрабатывается с элементом Grid. Класс наследуется от Grid, который фактически делает его корневым элементом содержимого WPF.
Конструктор содержимого WPF принимает требуемые ширину и высоту и, соответственно, размеры Grid. Затем она определяет базовый макет, создавая набор из объектов ColumnDefinition и RowDefinition и добавляя их к базовым коллекциям ColumnDefinitions и RowDefinitions объекта Grid соответственно. Это определяет сетку из пяти строк и семи столбцов с размерами, определяемыми содержимым ячеек.
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 Name и связанный с ним элемент управления TextBox. Поскольку один и тот же код используется для каждой пары метка/текстовое поле, он помещается в паре закрытых методов и используются для всех пяти пар метка/текстовое поле. Методы создают соответствующий элемент управления, и вызывают статические методы SetColumn и SetRow класса Grid для размещения элементов управления в соответствующую ячейку. После создания элемента управления пример вызывает метод 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. Образец использует немного менее прямой подход. Он обрабатывает Click внутри содержимого WPF и затем вызывает пользовательское событие OnButtonClicked, чтобы уведомить содержимое WPF. Это позволяет содержимому WPF выполнить некоторую проверку параметров перед уведомлением узла. Обработчик получает текст из элементов управления TextBox и присваивает его общим свойствам, из которых узел может получить сведения.
Объявление событий в WPFPage.h:
public:
delegate void ButtonClickHandler(Object ^, MyPageEventArgs ^);
WPFPage();
WPFPage(int height, int width);
event ButtonClickHandler ^OnButtonClicked;
Обработчик событий Click в WPFPage.cpp:
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 является более сложной, так как нету ни одного глобальный свойства, которое управляло бы шрифтами для всех элементов управления. Вместо этого, соответствующее свойство для каждого элемента управления изменяется в методах доступа набора свойств. В следующем примере показан код для свойства 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;
}