Поделиться через


Пошаговое руководство. Размещение содержимого WPF в Win32

Windows Presentation Foundation (WPF) предоставляет многофункциональную среду для создания приложений. Однако при наличии существенных инвестиций в код Win32 может оказаться более эффективным, чтобы добавить функциональные возможности WPF в приложение, а не переписать исходный код. WPF предоставляет простой механизм размещения содержимого WPF в окне Win32.

В этом руководстве описывается, как написать пример приложения (Размещение содержимого WPF в окне Win32), которое размещает содержимое WPF в окне Win32. Этот пример можно расширить для размещения любого окна Win32. Поскольку он включает в себя сочетание управляемого и неуправляемого кода, приложение записывается в C++/CLI.

Требования

В этом руководстве предполагается базовое знакомство с программированием WPF и Win32. Чтобы получить базовое введение в программирование WPF, см. Приступая к работе. Чтобы ознакомиться с программированием Win32, вам стоит обратиться к одной из многочисленных книг по этой теме, в частности, "Программирование Windows" Чарльза Петцольда.

Так как пример, сопровождающий это руководство, реализуется в C++/CLI, в этом руководстве предполагается знакомство с использованием C++ для программирования API Windows и понимания программирования управляемого кода. Знакомство с C++/CLI полезно, но не является важным.

Заметка

В этом руководстве содержится ряд примеров кода из связанного примера. Однако для удобства чтения он не включает полный пример кода. Полный пример кода см. в разделе Размещение содержимого WPF в примереокна Win32.

Базовая процедура

В этом разделе описана базовая процедура, используемая для размещения содержимого WPF в окне Win32. В остальных разделах описываются сведения о каждом шаге.

Ключ к размещению содержимого WPF в окне Win32 — это класс HwndSource. Этот класс упаковывает содержимое WPF в окно Win32, что позволяет включить его в пользовательский интерфейс в качестве дочернего окна. Следующий подход объединяет Win32 и WPF в одном приложении.

  1. Реализуйте содержимое WPF в виде управляемого класса.

  2. Реализуйте приложение Windows с помощью C++/CLI. Если вы начинаете с существующего приложения и неуправляемого кода C++, обычно можно включить его для вызова управляемого кода, изменив параметры проекта, чтобы включить флаг компилятора /clr.

  3. Установите модель потоков на однопоточную среду (STA).

  4. Обработайте уведомление WM_CREATEв процедуре окна и выполните следующие действия:

    1. Создайте новый объект HwndSource с родительским окном в качестве параметра parent.

    2. Создайте экземпляр класса контента WPF.

    3. Назначьте ссылку на объект содержимого WPF к свойству RootVisual в HwndSource.

    4. Получите HWND для содержимого. Свойство Handle объекта HwndSource содержит дескриптор окна (HWND). Чтобы получить HWND, который можно использовать в неуправляемой части вашего приложения, приведите Handle.ToPointer() к HWND.

  5. Реализуйте управляемый класс, содержащий статическое поле для хранения ссылки на содержимое WPF. Этот класс позволяет получить ссылку на содержимое WPF из кода Win32.

  6. Присвойте содержимое WPF статическому полю.

  7. Получение уведомлений из содержимого WPF путем подключения обработчика к одному или нескольким событиям WPF.

  8. Взаимодействие с содержимым WPF с использованием ссылки, хранящейся в статическом поле, для установки свойств и т. д.

Заметка

Вы также можете использовать содержимое WPF. Однако необходимо скомпилировать его отдельно в виде динамической библиотеки (DLL) и ссылаться на эту DLL из вашего приложения Win32. Оставшаяся часть процедуры аналогична приведенной выше процедуре.

Реализация ведущего приложения

В этом разделе описывается размещение содержимого WPF в базовом приложении Win32. Само содержимое реализуется в C++/CLI как управляемый класс. В большинстве случаев это простое программирование WPF. Основные аспекты внедрения содержимого обсуждаются в внедрение содержимого WPF.

Базовое приложение

Отправной точкой для ведущего приложения было создание шаблона Visual Studio 2005.

  1. Откройте Visual Studio 2005 и выберите Новый проект в меню Файл.

  2. Выберите Win32 из списка типов проектов Visual C++. Если язык по умолчанию не является C++, эти типы проектов будут находиться в других языках.

  3. Выберите шаблон проекта Win32 , назначьте имя проекту и нажмите ОК , чтобы запустить мастер приложений Win32 .

  4. Примите стандартные параметры мастера и нажмите кнопку Готово, чтобы запустить проект.

Шаблон создает базовое приложение Win32, в том числе:

  • Точка входа для приложения.

  • Окно со связанной процедурой окна (WndProc).

  • Меню с заголовком Файл и заголовком Справка. В меню файла есть элемент exit, который закрывает приложение. В меню Справка есть пункт "О программе", который запускает простое диалоговое окно.

Прежде чем приступить к написанию кода для размещения содержимого WPF, необходимо внести два изменения в базовый шаблон.

Первым является компиляция проекта в качестве управляемого кода. По умолчанию проект компилируется как неуправляемый код. Однако, так как WPF реализуется в управляемом коде, проект должен быть скомпилирован соответствующим образом.

  1. Щелкните правой кнопкой мыши имя проекта в обозревателе решений и выберите Свойства в контекстном меню, чтобы запустить диалоговое окно страниц свойств .

  2. Выберите свойства конфигурации в представлении дерева в левой области.

  3. Выберите поддержку Common Language Runtime в списке Project Defaults на правой панели.

  4. В раскрывающемся списке выберите поддержку Общей Среды Выполнения (/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, OK и Отмена. Когда пользователь нажимает ОК, обработчик событий 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к событию содержимого WPF OnButtonClicked. Этот обработчик вызывается, когда пользователь нажимает кнопку ОК или кнопку Отмена. Более подробную информацию об этом обработчике событий см. в содержимом communicating_with_the_WPF.

Последняя строка кода, показанная, возвращает дескриптор окна (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, когда пользователь нажимает кнопки OK или Отмена. Приложение также имеет пользовательский интерфейс, позволяющий пользователю изменять различные свойства содержимого 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, если кнопка OK была нажата, и false, если кнопка Отмена была нажата.

Если кнопка ОК была нажата, обработчик получает ссылку на содержимое WPF из класса контейнера. Затем он собирает сведения о пользователе, которые хранятся связанными свойствами содержимого WPF и используют статические элементы управления для отображения сведений в родительском окне. Поскольку данные содержимого WPF представлены в форме управляемой строки, ее необходимо маршалировать для использования элементом управления Win32. Если кнопка Отмена была нажата, обработчик очищает данные из статических элементов управления.

Пользовательский интерфейс приложения предоставляет набор переключателей, позволяющий пользователю изменять цвет фона содержимого 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, оно могло бы быть создано на любом языке среды CLR. Ниже приведено краткое пошаговое руководство по реализации C++/CLI, используемой в примере. Этот раздел содержит следующие подразделы.

Схема

Элементы пользовательского интерфейса в содержимом WPF состоят из пяти элементов управления TextBox с связанными элементами управления Label: Имя, Адрес, Город, Область и Почтовый индекс. Существует также два элемента управления Button, OK и Отмена

Содержимое WPF реализуется в классе WPFPage. Макет обрабатывается с помощью элемента макета Grid. Класс наследует от Grid, что фактически делает его корневым элементом содержимого WPF.

Конструктор содержимого WPF принимает необходимую ширину и высоту и соответственно изменяет размеры Grid. Затем он определяет базовый макет, создав набор объектов ColumnDefinition и RowDefinition и добавив их в базовые ColumnDefinitions объектов Grid и коллекции 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]);
  }

Затем конструктор добавляет элементы пользовательского интерфейса в 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);

Следующая строка содержит элемент управления Name Label и связанный с ним элемент управления TextBox. Так как один и тот же код используется для каждой пары меток и текстового поля, он помещается в пару частных методов и используется для всех пяти пар меток и текстовых ящиков. Методы создают соответствующий элемент управления и вызывают статические методы SetColumn и SetRow класса Grid для размещения элементов управления в соответствующей ячейке. После создания элемента управления образец вызывает метод Add в свойстве ChildrenGrid, чтобы добавить элемент управления в сетку. Код для добавления оставшихся пар метки и текстового поля аналогичен. Дополнительные сведения см. в примере кода.

//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;
}

Наконец, в примере добавляются кнопки OK и Cancel, и прикрепляются обработчики событий к их событиям 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;
}

См. также