C#与C++交互开发系列(二十四):WinForms 应用中嵌入C++ 原生窗体

引言

欢迎关注dotnet研习社,今天我继续延续 "C#与C++交互开发系列(二十四):WinForms 应用中嵌入C++ 原生窗体"。

在 .NET 开发中,我们经常会遇到这样的场景:需要在 C# WinForms 应用程序中集成一些 C++ 编写的原生窗口。这种需求通常出现在以下情况:

  • 集成遗留系统:需要将旧的 C++ 应用程序界面嵌入到新开发的 C# 应用中
  • 利用 C++ 库的特殊功能:某些图形渲染、硬件交互等功能在 C++ 中实现更高效
  • 性能关键部分:对性能要求极高的界面部分使用 C++ 实现
  • 特殊 UI 控件:使用只有 C++ 版本的第三方控件库

你是否曾经面临过这样的问题处理?本文将一步步实现将 C++ 原生窗体嵌入到 C# WinForms 窗口中的完整解决方案。

技术背景

Windows 窗口系统基础

在深入实现之前,我们需要了解 Windows 窗口系统的几个关键概念:

  1. 窗口句柄 (HWND):Windows 中每个窗口都有一个唯一的句柄,它是一个指向窗口对象的指针
  2. 父子窗口关系:Windows 允许窗口之间建立父子关系,子窗口显示在父窗口的客户区内
  3. 窗口消息:Windows 使用消息机制进行窗口间通信,如大小调整、焦点变化等
  4. 窗口样式 :控制窗口外观和行为的标志,如 WS_CHILD(子窗口样式)

实现原理

将 C++ 窗口嵌入 C# WinForms 应用的核心原理是:

  1. 在 C# 中通过 P/Invoke 调用 Win32 API 的 SetParent 函数,将 C++ 窗口设置为 C# 控件的子窗口
  2. 在 C++ 中创建一个窗口,并提供设置其窗口句柄的方法
  3. 处理窗口消息同步,确保两个窗口协同工作

接下来,我们将详细介绍具体的实现步骤。

实现步骤

1. C++ 端实现

首先,我们需要创建一个 C++ DLL 项目,实现窗口创建和导出必要的函数。

1.1 创建 C++ DLL 项目

在 Visual Studio 中创建一个新的 DLL 项目,并添加以下头文件:

cpp:NativeWindow.h 复制代码
#pragma once

#include <Windows.h>

#ifdef NATIVEWINDOW_EXPORTS
#define NATIVEWINDOW_API __declspec(dllexport)
#else
#define NATIVEWINDOW_API __declspec(dllimport)
#endif

// 导出函数声明
extern "C" {
    // 创建窗口并返回窗口句柄
    NATIVEWINDOW_API HWND CreateNativeWindow(HWND parentHwnd, int x, int y, int width, int height);
    // 设置窗口内容(示例:设置文本)
    NATIVEWINDOW_API void SetNativeWindowText(HWND hwnd, const char* text);
    // 销毁窗口
    NATIVEWINDOW_API void DestroyNativeWindow(HWND hwnd);
}
1.2 实现窗口创建和消息处理

接下来,我们实现源文件:

cpp:NativeWindow.cpp 复制代码
#include "NativeWindow.h"
#include <string>
// 窗口类名
const char* WINDOW_CLASS_NAME = "NativeWindowClass";

// 窗口过程函数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);

        // 获取窗口客户区大小
        RECT rect;
        GetClientRect(hwnd, &rect);

        // 设置文本颜色和背景模式
        SetTextColor(hdc, RGB(0, 0, 0));
        SetBkMode(hdc, TRANSPARENT);

        // 获取窗口文本
        char buffer[256];
        GetWindowTextA(hwnd, buffer, 256);

        // 绘制文本
        DrawTextA(hdc, buffer, -1, &rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

        EndPaint(hwnd, &ps);
        return 0;
    }

    case WM_SIZE:
        // 处理大小调整消息
        InvalidateRect(hwnd, NULL, TRUE);
        return 0;

    case WM_DESTROY:
        // 清理资源
        return 0;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

// 注册窗口类
bool RegisterWindowClass()
{
    static bool registered = false;

    if (!registered)
    {
        WNDCLASSEXA wc = { 0 };
        wc.cbSize = sizeof(WNDCLASSEXA);
        wc.style = CS_HREDRAW | CS_VREDRAW;
        wc.lpfnWndProc = WindowProc;
        wc.hInstance = GetModuleHandleA(NULL);
        wc.hCursor = LoadCursor(NULL, IDC_ARROW);
        wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        wc.lpszClassName = WINDOW_CLASS_NAME;

        registered = (RegisterClassExA(&wc) != 0);
    }

    return registered;
}

// 导出函数实现
extern "C" {
    NATIVEWINDOW_API HWND CreateNativeWindow(HWND parentHwnd, int x, int y, int width, int height)
    {
        // 注册窗口类
        if (!RegisterWindowClass())
        {
            return NULL;
        }

        // 创建窗口
        HWND hwnd = CreateWindowExA(
            0,                      // 扩展样式
            WINDOW_CLASS_NAME,      // 窗口类名
            "Native Window",        // 窗口标题
            WS_CHILD | WS_VISIBLE,  // 窗口样式:子窗口且可见
            x, y, width, height,    // 位置和大小
            parentHwnd,             // 父窗口句柄
            NULL,                   // 菜单句柄
            GetModuleHandleA(NULL), // 实例句柄
            NULL                    // 额外参数
        );

        if (!hwnd)
        {
            DWORD err = GetLastError();
            char msg[256];
            sprintf_s(msg, "CreateWindowExA failed with error: %lu", err);
            MessageBoxA(NULL, msg, "Error", MB_OK | MB_ICONERROR);
        }

        if (hwnd)
        {
            // 显示窗口
            ShowWindow(hwnd, SW_SHOW);
            UpdateWindow(hwnd);
        }

        return hwnd;
    }

    NATIVEWINDOW_API void SetNativeWindowText(HWND hwnd, const char* text)
    {
        SetWindowTextA(hwnd, text);
        InvalidateRect(hwnd, NULL, TRUE);
    }

    NATIVEWINDOW_API void DestroyNativeWindow(HWND hwnd)
    {
        if (hwnd && IsWindow(hwnd))
        {
            DestroyWindow(hwnd);
        }
    }
}
1.3 编译设置

确保 DLL 项目的编译设置与 C# 项目兼容:

  • 平台设置:如果 C# 应用是 64 位的,C++ DLL 也必须是 64 位的
  • 运行时库:建议使用多线程 DLL (/MD) 设置
  • 字符集:使用 Unicode 字符集

2. C# 端实现

现在,我们需要在 C# WinForms 应用中集成这个 C++ 窗口。

2.1 P/Invoke 声明

首先,我们需要声明必要的 P/Invoke 函数:

csharp 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace WinFormsNativeWindow
{
    public class NativeWindowWrapper
    {
        // 导入 C++ DLL 函数
        [DllImport("NativeWindow.dll")]
        private static extern IntPtr CreateNativeWindow(IntPtr parentHwnd, int x, int y, int width, int height);

        [DllImport("NativeWindow.dll")]
        private static extern void SetNativeWindowText(IntPtr hwnd, string text);

        [DllImport("NativeWindow.dll")]
        private static extern void DestroyNativeWindow(IntPtr hwnd);

        // 窗口句柄
        public IntPtr NativeWindowHandle = IntPtr.Zero;

        // 创建并嵌入原生窗口
        public void CreateAndEmbedNativeWindow(Control parent, int x, int y, int width, int height)
        {
            // 创建原生窗口,直接使用 C# 窗体的句柄作为父窗口
            NativeWindowHandle = CreateNativeWindow(parent.Handle, x, y, width, height);

            if (NativeWindowHandle != IntPtr.Zero)
            {
                // 调整位置和大小
                SetWindowPos(NativeWindowHandle, x, y, width, height);
            }
        }

        // 设置窗口位置和大小
        [DllImport("user32.dll")]
        private static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);

        public void SetWindowPos(IntPtr hwnd, int x, int y, int width, int height)
        {
            MoveWindow(hwnd, x, y, width, height, true);
        }

        // 设置窗口文本
        public void SetText(string text)
        {
            if (NativeWindowHandle != IntPtr.Zero)
            {
                SetNativeWindowText(NativeWindowHandle, text);
            }
        }

        // 销毁窗口
        public void DestroyWindow()
        {
            if (NativeWindowHandle != IntPtr.Zero)
            {
                DestroyNativeWindow(NativeWindowHandle);
                NativeWindowHandle = IntPtr.Zero;
            }
        }
    }
}
2.2 窗体实现

接下来,我们创建一个 WinForms 窗体,并在其中嵌入 C++ 窗口:

csharp 复制代码
using System;
using System.Windows.Forms;

namespace WinFormsNativeWindow
{
    public partial class MainForm : Form
    {
        private NativeWindowWrapper _nativeWindow;
        
        public MainForm()
        {
            InitializeComponent();
            
            // 创建面板作为容器
            Panel panel = new Panel();
            panel.Dock = DockStyle.Fill;
            panel.Resize += Panel_Resize;
            this.Controls.Add(panel);
            
            // 创建按钮
            Button btnSetText = new Button();
            btnSetText.Text = "设置文本";
            btnSetText.Dock = DockStyle.Bottom;
            btnSetText.Click += BtnSetText_Click;
            this.Controls.Add(btnSetText);
            
            // 初始化原生窗口包装器
            _nativeWindow = new NativeWindowWrapper();
            
            // 窗体加载时创建并嵌入原生窗口
            this.Load += MainForm_Load;
            // 窗体关闭时销毁原生窗口
            this.FormClosing += MainForm_FormClosing;
        }
        
        private void MainForm_Load(object sender, EventArgs e)
        {
            Panel panel = this.Controls.OfType<Panel>().First();
            
            // 创建并嵌入原生窗口
            _nativeWindow.CreateAndEmbedNativeWindow(
                panel,
                0, 0,
                panel.ClientSize.Width,
                panel.ClientSize.Height
            );
            
            // 设置初始文本
            _nativeWindow.SetText("C++ 原生窗口已嵌入");
        }
        
        private void Panel_Resize(object sender, EventArgs e)
        {
            // 调整原生窗口大小以适应面板
            Panel panel = sender as Panel;
            if (panel != null && _nativeWindow != null)
            {
                _nativeWindow.SetWindowPos(
                    _nativeWindow._nativeWindowHandle,
                    0, 0,
                    panel.ClientSize.Width,
                    panel.ClientSize.Height
                );
            }
        }
        
        private void BtnSetText_Click(object sender, EventArgs e)
        {
            // 弹出输入对话框
            string text = Microsoft.VisualBasic.Interaction.InputBox(
                "请输入要显示的文本:",
                "设置文本",
                "Hello from C#!"
            );
            
            if (!string.IsNullOrEmpty(text))
            {
                _nativeWindow.SetText(text);
            }
        }
        
        private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            // 销毁原生窗口
            _nativeWindow.DestroyWindow();
        }
    }
}

执行结果

可以看到C++原生窗口已经嵌入了。

点击设置文本:

确定后:

交互也Ok。

常见问题与解决方案

在实现过程中,可能会遇到以下常见问题:

1. DLL 加载失败

问题:System.DllNotFoundException: 无法加载 DLL 'NativeWindow.dll'

解决方案

  • 确保 DLL 文件位于应用程序目录或系统路径中
  • 检查 DLL 和应用程序的平台是否匹配(x86/x64)
  • 使用 Dependency Walker 工具检查 DLL 依赖项
csharp 复制代码
<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<OutputType>WinExe</OutputType>
		<Nullable>enable</Nullable>
		<UseWindowsForms>true</UseWindowsForms>
		<ImplicitUsings>enable</ImplicitUsings>
	</PropertyGroup>

	<PropertyGroup>
		<TargetFramework>net8.0-windows</TargetFramework>
		<PlatformTarget>x64</PlatformTarget>
		<RuntimeIdentifiers></RuntimeIdentifiers>
		<OutputPath>$(SolutionDir)x64\Debug\</OutputPath>
		<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
	</PropertyGroup>

</Project>

其中【net8.0-windows】目录一直存在,需要调整AppendTargetFrameworkToOutputPath为false才能不生成目录。

2. 资源释放问题

问题:应用程序关闭时未释放原生窗口资源

解决方案

  • 在窗体的 FormClosing 事件中调用 DestroyWindow 方法
  • 实现 IDisposable 接口,确保资源正确释放

性能优化与最佳实践

性能优化

  1. 最小化跨进程通信

    • 减少 C# 和 C++ 之间的频繁调用
    • 批量处理数据交换
  2. 使用双缓冲绘图

    • 在 C++ 窗口中实现双缓冲绘图,避免闪烁
    • 使用 GDI+ 或 Direct2D 进行高效绘图
  3. 优化窗口消息处理

    • 只处理必要的窗口消息
    • 避免消息循环中的复杂计算
  4. 共享内存通信

    • 对于大量数据交换,考虑使用共享内存
    • 使用内存映射文件实现高效数据共享

最佳实践

  1. 明确资源所有权

    • 清晰定义 C++ 和 C# 端各自负责的资源
    • 确保资源在正确的时机释放
  2. 异常处理

    • 在 P/Invoke 调用周围添加异常处理
    • 确保即使发生异常,资源也能正确释放
  3. 线程安全

    • 注意 UI 线程和工作线程之间的交互
    • 使用 Invoke/BeginInvoke 在正确的线程上执行 UI 操作
  4. 接口抽象

    • 使用接口抽象隔离平台相关代码
    • 便于将来替换或扩展实现

扩展应用场景

这种技术不仅限于简单的窗口嵌入,还可以应用于更多高级场景:

  1. 嵌入 DirectX/OpenGL 渲染窗口

    • 在 C# 应用中嵌入高性能 3D 渲染窗口
    • 实现复杂的图形应用
  2. 集成第三方 C++ 库的 UI 组件

    • 嵌入只有 C++ 版本的专业控件
    • 集成特定行业的专用界面组件
  3. 嵌入特殊硬件驱动的视图窗口

    • 集成工业相机、医疗设备等专用显示窗口
    • 实现硬件加速的图像处理界面
  4. 混合使用现代 UI 框架和传统控件

    • 在现代 UI 应用中嵌入传统控件
    • 逐步迁移遗留系统

总结

在本文中,我们详细介绍了如何将 C++ 原生窗体嵌入到 C# WinForms 窗口中的完整解决方案。我们从技术背景入手,详细讲解了实现步骤,包括 C++ DLL 的创建、C# 端的集成以及窗口消息同步处理。同时,我们还提供了常见问题的解决方案、性能优化建议和最佳实践。

通过这种技术,可以充分利用 C++ 和 C# 各自的优势,实现更加灵活和高效的应用程序。无论是集成遗留系统,还是实现特殊功能,这种混合编程方式都能为你提供强大的技术支持。

参考资源

  1. Windows API 文档 - SetParent 函数
  2. P/Invoke - Windows API 在 .NET 中的使用
  3. Windows 窗口消息参考
  4. C# 与非托管代码交互最佳实践
相关推荐
唐青枫2 分钟前
C#.NET dapper 详解
c#·.net
死也不注释2 小时前
【鸡零狗碎记录】
unity·c#
Maybe_ch3 小时前
.NET-键控服务依赖注入
开发语言·c#·.net
QQ_4376643144 小时前
C++11 右值引用 Lambda 表达式
java·开发语言·c++
liulilittle6 小时前
C++/CLI与标准C++的语法差异(一)
开发语言·c++·.net·cli·clr·托管·原生
★YUI★6 小时前
学习游戏制作记录(剑投掷技能)7.26
学习·游戏·unity·c#
小狄同学呀6 小时前
VS插件报错,g++却完美编译?API调用错因分析
c++
程序员编程指南6 小时前
Qt 数据库连接池实现与管理
c语言·数据库·c++·qt·oracle
张北北.6 小时前
【深入底层】C++开发简历4+4技能描述6
java·开发语言·c++