将 WPF 窗口嵌入到 MFC 窗口中
背景
有一个现存的 MFC 项目,需要在里面添加新的 UI 界面,使用 MFC 开发太费劲,完全使用 WPF 再重写一遍,时间上不允许。
可以考虑直接将 WPF 窗口嵌入到 MFC 窗口中,以下是探索过程中的一些记录。
🍕 方案1 MFC + .NET Framework WPF
参照如下博客的说明,使用 MFC + .NET Framework WPF 的方式,实现嵌入功能。
MFC中调用WPF教程_system::windows::interop;-CSDN博客
MFC中调用WPF教程 | Microsoft Learn
Step1 当前 MFC 项目修改
为当前 MFC 项目添加公共语言运行时支持
这里只能使用 .NET Framework 的框架支持,而不能使用 .NET Core,因为使用 .NET Core 要求当前项目的输出类型为“类库”,而不能是可执行文件。编译时会报相关的错误。
为当前 MFC 项目添加 .NET Framework 框架的必要引用
Step2 新建 .NET Framework WPF 项目
新建 WPF 项目,并删除 WPF 工程中的 App.xaml 和 App.xaml.cs 两个源文件,修改项目 Output type (输出类型)为 Class Library (类库)。
并在 MFC 项目中,添加对 WPF 项目的引用。
Step3 添加托管 C++ 类
// CHostWPFWnd.h
#pragma once
using namespace System;
using namespace System::Windows;
using namespace System::Windows::Interop;
using namespace System::Runtime;
using namespace MyWpfApp;
public ref class CHostWPFWnd
{
public:
static HWND GetWPFWindowHwnd();
static void ShowWPFWindow(HWND hwnd);
};
// CHostWPFWnd.cpp
#include "pch.h"
#include "CHostWPFWnd.h"
// 获取 WPF 窗口的句柄
HWND CHostWPFWnd::GetWPFWindowHwnd()
{
MainWindow^ wpfWindow = gcnew MainWindow();
// 需要先将窗口显示出来(显示到屏幕外,避免在屏幕中间闪现一下)
// 才能拿到有效的窗口句柄
wpfWindow->Top = -100000;
wpfWindow->Show();
WindowInteropHelper^ wih = gcnew WindowInteropHelper(wpfWindow);
HWND h = (HWND)(wih->Handle.ToPointer());
return h;
}
// 以独立的弹窗直接显示 WPF 窗口
void CHostWPFWnd::ShowWPFWindow(HWND hwnd)
{
MainWindow^ wpfWindow = gcnew MainWindow();
WindowInteropHelper^ wih = gcnew WindowInteropHelper(wpfWindow);
wih->Owner = IntPtr(hwnd);
wpfWindow->Show();
}
Step4 在 MFC 代码中调用
先获取 MFC 窗口的句柄,然后调用 ShowWPFWindow 方法,显示独立的 WPF 弹窗
#include "CHostWPFWnd.h"
#include <Windows.h>
HWND cppWindowHwnd = this->GetSafeHwnd();
CHostWPFWnd::ShowWPFWindow(cppWindowHwnd);
获取到 WPF 窗口的句柄,然后将其嵌入到 MFC 窗口中。
为了避免初始化 WPF 窗口时,在系统任务栏上闪现 WPF 窗口的标题,可以在 WPF 中设置 ShowInTaskbar="False"
#include "CHostWPFWnd.h"
#include <Windows.h>
HWND cppWindowHwnd = this->GetSafeHwnd();
HWND wpfWindowHwnd = CHostWPFWnd::GetWPFWindowHwnd();
::SetParent(wpfWindowHwnd, cppWindowHwnd);
::SetWindowLongPtr(wpfWindowHwnd, GWL_STYLE, WS_CHILD | WS_VISIBLE);
::MoveWindow(wpfWindowHwnd, 300, 20, 400, 300, TRUE);
::BringWindowToTop(wpfWindowHwnd);
主体窗口是 MFC 的窗口和控件,有青色背景的是 WPF 嵌入到 MFC 中的窗口。
MFC + .NET Framework WPF 方案的问题
1 需要将现有 MFC 项目修改成 C++/CLI 项目(添加托管运行时支持)
这个需要根据实际情况,或许修改之后会有其它影响
2 在部分电脑上,WPF 嵌入 MFC 窗口中之后,会出现窗口中的控件渲染闪烁的问题
我这里两台电脑测试,有一台有问题,另一台 OK。不确定是哪里的问题。
🍕 方案2 MFC + .NET Core WPF
以上使用 .NET Framework 的方案,是参考上面搜索出来的博客来实现的。现在更推荐使用 .NET Core 版本。
具体实现时,不能直接为 MFC 项目添加 .NET Core 的运行时支持,因为添加 .NET Core 支持要求 C++ 项目是类库,而不是可执行文件。
这就需要一个 C++/CLI 的中间层项目,来进行中转,与 《C++ 调用 C# - C++/CLI 方案》 中提到的是一样的。
Step1 新建 .NET Core WPF 项目
新建基于 .NET Core(如 .NET8)的 WPF 项目,并删除 App.xaml 和 App.xaml.cs 两个源文件,修改项目类型为类库。
<OutputType>Library</OutputType>
通过上面的代码,其实可以看到,将 WPF 窗口嵌入到 MFC 中,重点就是拿到 WPF 窗口的句柄,然后使用 Windows API 就可以将窗口嵌入了。这部分代码,其实可以在 C#/WPF 项目中直接实现,避免在 C++/CLI 中间层写太多代码(因为不习惯写 C++)。
namespace MyWPFApp
{
public static class ExportWindowHelper
{
private static MainWindow? _mainWindow;
private static int _mainWindowPtr;
public static int GetMainWindow()
{
if (_mainWindow == null)
{
_mainWindow = new MainWindow { Top = -100000 };
_mainWindow.Show();
var interopHelper = new WindowInteropHelper(_mainWindow);
_mainWindowPtr = (int)(interopHelper.Handle);
}
return _mainWindowPtr;
}
}
}
Step2 新建 C++/CLI 项目
// HostWPFNative.h
#pragma once
#ifdef VIEW_BRIDGE_EXPORTS
#define VIEW_BRIDGE_API __declspec(dllexport)
#else
#define VIEW_BRIDGE_API __declspec(dllimport)
#endif
#include <string>
// 导出给原生 C++/MFC 项目使用的类
class VIEW_BRIDGE_API ViewBridgeWrapper
{
public:
int GetHwnd();
};
// HostWPFWnd.h
#pragma once
using namespace System;
using namespace System::Windows;
using namespace System::Windows::Interop;
using namespace System::Runtime;
using namespace MyWPFApp;
// 托管 C++ 实现,调用 C# 获取窗口句柄
public ref class HostWPFWnd
{
public:
int GetHwnd()
{
// 在 C# 中获取到窗口句柄
int ptr2 = ExportWindowHelper::GetMainWindow();
return ptr2;
// 在 C++/CLI 中获取窗口句柄
//MainWindow^ wpfWindow = gcnew MainWindow();
//WindowInteropHelper^ wih = gcnew WindowInteropHelper(wpfWindow);
//wpfWindow->Top = -100000;
//wpfWindow->Show();
//int ptr = (int)(wih->Handle.ToPointer());
//return ptr;
}
};
// HostWPFWnd.cpp
#include "pch.h"
#include "HostWPFWnd.h"
#include "HostWPFNative.h"
#define VIEW_BRIDGE_EXPORTS
// C++ 导出方法的实现,调用托管 C++ 方法,获取窗口句柄
int ViewBridgeWrapper::GetHwnd()
{
HostWPFWnd wpfWnd;
int ptr = wpfWnd.GetHwnd();
return ptr;
}
Step3 MFC 中添加对中间层 C++/CLI 的引用
与 《C++ 调用 C# - C++/CLI 方案》 中提到的一样,需要如下步骤:
-
项目->属性->配置属性->VC++ 目录-> 在 "包含目录" 里添加头文件
HostWPFNative.h
所在的目录 -
项目->属性->配置属性->VC++ 目录-> 在 "库目录" 里添加
ViewBridge.lib
所在的目录 -
项目->属性->配置属性->链接器->输入-> 在 "附加依赖项" 里添加
ViewBridge.lib
(若有多个 lib 则以空格隔开)
在 MFC 的业务代码中(窗口初始化代码等地方),调用上述方法,获取到 WPF 窗口的句柄,就可以嵌入到 MFC 窗口中了。
以下是代码参考
#include "HostWPFNative.h"
/// <summary>
/// SHOW WPF TEST
/// </summary>
void CMyMFCAppDlg::OnBnClickedButtonShowWpf()
{
ViewBridgeWrapper viewWrapper;
int ptr = viewWrapper.GetHwnd();
HWND cppWindowHwnd = this->GetSafeHwnd();
HWND wpfWindowHwnd = (HWND)ptr;
// 保存 WPF 窗口句柄
m_wpfMainWindowHwnd = wpfWindowHwnd;
// 获取客户区的大小
CRect clientRect;
GetClientRect(&clientRect);
int clientWidth = clientRect.Width();
int clientHeight = clientRect.Height();
::SetParent(wpfWindowHwnd, cppWindowHwnd);
::SetWindowLongPtr(wpfWindowHwnd, GWL_STYLE, WS_CHILD | WS_VISIBLE);
::MoveWindow(wpfWindowHwnd, 10, 72, clientWidth-20, clientHeight-80, TRUE);
::BringWindowToTop(wpfWindowHwnd);
}
void CMyMFCAppDlg::OnSize(UINT nType, int cx, int cy)
{
CDialogEx::OnSize(nType, cx, cy);
if (m_wpfMainWindowHwnd)
{
// 窗口大小变化时,调整 WPF 窗口的大小
::MoveWindow(m_wpfMainWindowHwnd, 10, 72, cx - 20, cy - 80, TRUE);
}
}
🍕 源码参考
https://gitee.com/Jasongrass/DemoPark/tree/master/Code/Embed_WPF_to_MFC