65.9K
CodeProject 正在变化。 阅读更多。
Home

为你的 C++/QT 应用程序创建出色的 WPF UI

starIconstarIconstarIconstarIconstarIcon

5.00/5 (32投票s)

2020 年 1 月 2 日

CPOL

15分钟阅读

viewsIcon

78427

downloadIcon

2906

本文将教你如何为你的原生应用程序创建令人惊叹、干净流畅的 WPF/Winform UI,而无需使用任何复杂、不安全、ActiveX 式的方法等。

引言

我很久没有写文章了,今天我带来了一篇精彩的文章。:)

本文面向 **C++/QT 程序员**,他们希望在 **C++ 代码** 中使用 **WPF** 的强大功能。

如果你是 .NET 程序员,请绕道,这不是你的菜。(你可以使用 Invoking & DllImport 等技巧 :v)

背景与故事

当我们谈论 WPF 时,我们指的是微软为 .NET 用户创建的易于使用、用户友好且高性能的框架。

它拥有 **DirectX 渲染器** 的强大功能,结合 **HTML/CSS 样式**,并且是 **开源的!** 有太多理由让这个小家伙比其他任何 **桌面应用程序** 解决方案都出色。

比 QT、Winforms、HTML/CSS 更好,原因如下:

  • QT 框架:如果你需要静态链接,它并不免费。它有太多的依赖项,而且组件库很差,你需要购买第三方库或者自己创建组件。
  • Winforms:设计得非常好,至今仍是经典,但它有点老旧,使用 GDI+。
  • HTML/CSS:需要像 chromium 等体积庞大的依赖项。用 HTML/CSS 开发一个简单的“Hello World”应用,你需要打包 60MB 以上的文件,如果选择 Electron,则需要 80MB 以上。
  • WPF:使用 GPU 渲染,拥有出色的设计器,完全可定制,并且随 Windows 一起提供,无需任何依赖项,微软在 Windows 中反复使用它。此外,.NET Core 3.0 支持 WPF,这意味着它也可以用于跨平台应用程序!

但问题是它只适用于 .NET,而 C/C++ 或 Delphi 开发者如果想要的话也无法使用它来创建 GUI。他们应该切换到 C# 语言,但 C# 的安全性较低,性能不如 C++,如果他们想将它与互联网上现有的解决方案混合使用,就会变得不安全、不稳定,一点也不酷!

但有时做一些复杂的事情比看起来要简单得多!怎么做?让我引用一位国王的话

Heisenberg

没什么特别的……这 **只是基础知识!**

我一直想让这个方案奏效!……使用 C++ 作为我的后端应用程序,WPF/Winform 作为 GUI 和前端,因为它在后端安全/快速,在前端美观/易于使用。

在这个解决方案中,你可以同时使用 WPF/Winforms 和 QT 以及任何你想要的其他东西。

当前现有方法

好的,在我们开始之前,让我们看看市场上现有的方法并进行简要回顾……

  • ActiveX:从将 WPF/Winform 嵌入 C++ ActiveX 控件开始,你总是会听到这个方法,但它很糟糕,非常糟糕!为了使用另外两个平台,创建另一个平台……哦,太混乱了!最重要的是,它是可见的,并且对所有人开放,他们也可以使用它……你的程序员朋友给你的建议是……永远不要对 UI 使用 COM……
  • CLRHosting:大家在网上找到的第二件事是 CLR Hosting Interface,是的,它很好而且稳定,但不足以只混合 UI 和前端,我宁愿用它在我的原生应用程序中运行整个 .NET 应用。
  • C++/CLI:这是大家最不想碰的东西。需要调用大量的代码,并带来很多痛苦,而且 CLR/C++ 程序集很难集成。
  • Noesis GUI:另一种可能的方法,但是……需要实现所有东西,blah blah……
  • WPF 渲染重定向:在这种方法中,你运行一个隐藏的 WPF 窗口实例,将其渲染到 texture2D,然后在 C++ 窗口中显示它,并将所有接收到的窗口消息重定向到隐藏的 WPF 窗口。它奏效了!但是……你知道的,我们在这里是为了一个干净专业的方法……:D

我非常基础的方法

我将教你的这个方法是我大型教程包“**像老板一样架构你的应用程序**”的一部分。我将在 CodeProject 上分部分发布它,所以……坐好,享受表演吧!:)

我们将遵循的步骤是:

  1. 在 C++ 中创建一个宿主窗口
  2. 在 C# 中创建我们的 WPF GUI 库
  3. 在 C++ 中使用我们的库创建 WPF GUI
  4. 与 C++ 和 C# 之间交换函数,反之亦然
  5. 创建回调并安装 Windows 钩子
  6. 没有第 6 步,请记住,这非常基础?:D
在我这里……

你唯一需要的是一个句柄……

好了,废话少说,开始干吧!:)

创建宿主窗口

打开 Visual Studio,创建一个 x64 **C++ 控制台应用程序**,将此基本代码作为你的主 cpp 文件

//// Dependencies
#include <iostream>
#include <Windows.h>

using namespace std;

//// Global Objects

//// Global Configs

//// Our Application Entry Point
int main()
{
    cout << "C++ Main App Started..." << endl;

    /// We Code Here ...

    cout << "C++ Main App Finished." << endl;
    getchar();
}

现在让我们创建我们的非托管 **宿主窗口……**

  1. 在 `//// Global Objects` 下定义全局对象,在 `//// Global Configs` 中定义配置
    //// Global Objects
    WNDCLASSEX HostWindowClass; /// Our Host Window Class Object
    MSG loop_message; /// Loop Message for Host Window
    HINSTANCE hInstance = GetModuleHandle(NULL); /// Application Image Base Address
    HWND cpphwin_hwnd; /// Host Window Handle
    HWND wpf_hwnd; /// WPF Wrapper Handle
    
    //// Global Configs
    const wchar_t cpphwinCN[] = L"CppMAppHostWinClass"; /// Host Window Class Name
    bool isHWindowRunning = false; /// Host Window Running State
  2. 使用 (在 `/// We Code Here ...` 之后) 创建窗口类
    /// Creating Icon Object From Resources, Don't forget to include resource.h!
    HICON app_icon = LoadIcon(GetModuleHandle(0),MAKEINTRESOURCE(IDI_APPICON));
    
    /// Defining Our Host Window Class
    HostWindowClass.cbSize = sizeof(WNDCLASSEX); HostWindowClass.lpfnWndProc = HostWindowProc;
    HostWindowClass.hCursor = LoadCursor(NULL, IDC_ARROW);
    HostWindowClass.cbClsExtra = 0; HostWindowClass.style = 0;
    HostWindowClass.cbWndExtra = 0;    HostWindowClass.hInstance = hInstance;
    HostWindowClass.hIcon = app_icon; HostWindowClass.hIconSm = app_icon;
    HostWindowClass.lpszClassName = cpphwinCN; HostWindowClass.lpszMenuName = NULL;

    为什么不设置 'hbrBackground'? 如果为你的原生窗口选择背景,它会覆盖 WPF 渲染,所以不要设置它。

  3. 使用添加回调到宿主窗口
    //// Host Window Callback, NOTE :Define This Before Your Entrypoint Function
    LRESULT CALLBACK HostWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
    {
        switch (msg)
        {
        case WM_CLOSE:
            DestroyWindow(hwnd);
            break;
        case WM_DESTROY:
            isHWindowRunning = false;
            break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
        }
        return 0;
    }
  4. 使用注册宿主窗口类
    //// Register Window
    if (!RegisterClassEx(&HostWindowClass))
    {
      cout << "Error, Code :" << GetLastError() << endl;
      getchar(); return 0;
    }
  5. 好的,该创建窗口了,但要隐藏起来……
    /// Creating Unmanaged Host Window
    cpphwin_hwnd = CreateWindowEx(
      WS_EX_CLIENTEDGE,
      cpphwinCN,
      GetSTR_Res(APPDATA_HWINDOW_NAME),
      WS_THICKFRAME | WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, CW_USEDEFAULT, 800, 500,
      NULL, NULL, hInstance, NULL);
    
    /// Check if How Window is valid
    if (cpphwin_hwnd == NULL)
    {
      cout << "Error, Code :" << GetLastError() << endl;
      getchar(); return 0;
    }
  6. [可选] 如果你想让你的窗口固定大小,可以使用
    /// Making Window Fixed Size
    ::SetWindowLong(cpphwin_hwnd, GWL_STYLE, 
        GetWindowLong(cpphwin_hwnd, GWL_STYLE) & ~WS_SIZEBOX);

好了,现在我们有了一个原生宿主窗口……

配置和显示宿主窗口

好吧,我知道你们中的大多数人都知道这些,但参数中有些非常小的细节需要你注意,这就是为什么我为你编写这段代码。;)

  1. 使用居中你的宿主窗口
    /// Centering Host Window
    RECT window_r; RECT desktop_r;
    GetWindowRect(cpphwin_hwnd, &window_r); GetWindowRect(GetDesktopWindow(), &desktop_r);
    int xPos = (desktop_r.right - (window_r.right - window_r.left)) / 2;
    int yPos = (desktop_r.bottom - (window_r.bottom - window_r.top)) / 2;
    
    /// Set Window Position
    ::SetWindowPos(cpphwin_hwnd, 0, xPos, yPos, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
  2. 最后,使用显示窗口
    /// Display Window
    ShowWindow(cpphwin_hwnd, SW_SHOW);
    UpdateWindow(cpphwin_hwnd);
    BringWindowToTop(cpphwin_hwnd);
    isHWindowRunning = true;
  3. 添加消息循环以防止应用程序冻结
    /// Adding Message Loop
    while (GetMessage(&loop_message, NULL, 0, 0) > 0 && isHWindowRunning)
    {
     TranslateMessage(&loop_message);
     DispatchMessage(&loop_message);
    }

主非托管应用程序

好的,是时候开发你的 C++ 应用程序 **功能、接口等了。在本教程中,我创建了一个简单的 C++ 应用程序,它使用 LZ4 算法压缩/解压缩文件。**

**当然!** .NET 也可以像 C++ 一样完美地进行 LZ4 压缩,但这只是使用原生库(lz4 库)的一个例子,关键在于使用 WPF 作为 GUI,你的原生应用程序可以是任何东西……没有限制。

此外,在文章的最后,我将告诉你如何在 **插件和 SDK** 中使用这种方法,而你对它们的全部部分没有完全访问权限。

这是主应用程序的代码,但它不是本教程的一部分,所以我不会解释它……你可以使用任何你想要的。:)

C++ LZ4 压缩/解压缩应用程序

//// Main App Codes
#pragma region Main App Codes

/// IncludingLZ4 Library -> https://github.com/lz4/lz4
#include "SDK\\lz4.h"
#pragma comment(lib, "SDK\\liblz4_static_vc2019.lib")
#include <vector>
#include <fstream>
ofstream file;

/// Abstract Vector Data
using buffer = vector<char>;

/// Lz4 Methods
BOOL lz4_compress(const buffer& in, buffer& out)
{
    auto rv = LZ4_compress_default(in.data(), out.data(), in.size(), out.size());
    if (rv < 1) { return FALSE; }
    else { out.resize(rv); return TRUE;}
}
BOOL lz4_decompress(const buffer& in, buffer& out)
{
    auto rv = LZ4_decompress_safe(in.data(), out.data(), in.size(), out.size());
    if (rv < 1) { return FALSE; }
    else { out.resize(rv); return TRUE; }
}

/// Read File
std::vector<char> readFile(const char* filename)
{
    std::basic_ifstream<char> file(filename, std::ios::binary);
    return std::vector<char>((std::istreambuf_iterator<char>(file)),
                              std::istreambuf_iterator<char>());
}

/// MainApp API Functions
BOOL LZ4_Compress_File(char* filename) {
    buffer org_filedata = readFile(filename);
    if(org_filedata.size() == 0){ return FALSE; }
    const size_t max_dst_size = LZ4_compressBound(org_filedata.size());
    vector<char> compressed_data(max_dst_size);
    BOOL compress_data_with_lz4 = lz4_compress(org_filedata, compressed_data);
    if (!compress_data_with_lz4) { return FALSE; }
    string out_put_file_name = filename + string("_.lz4");
    file.open(out_put_file_name, ios::binary | ios::out);
    file.write((char*)compressed_data.data(), compressed_data.size());
    file.close();
    SecureZeroMemory(org_filedata.data(), org_filedata.size());
    return TRUE;
}

BOOL LZ4_Decompress_File(char* filename,long originalSize) {
    vector<char> decompressed_data;
    decompressed_data.resize(originalSize);
    buffer org_filedata = readFile(filename);
    if (org_filedata.size() == 0) { return FALSE; }
    BOOL decompress_data_with_lz4 = lz4_decompress(org_filedata, decompressed_data);
    string out_put_file_name(filename);
    out_put_file_name = out_put_file_name.replace
                        (out_put_file_name.find("_.lz4"), sizeof("_.lz4") - 1, "");
    file.open(out_put_file_name, ios::binary | ios::out);
    file.write((char*)decompressed_data.data(), decompressed_data.size());
    file.close();
    return TRUE;
}
#pragma endregion

快速提示:原始大小可以附加在压缩文件的开头或结尾。

创建 WPF/Winform 用户界面

好的,现在是创建我们的 WPF 或 Winform 用户界面的时候了,用你想要的平台创建一个 **x64 C# 库**,我使用 WPF 是因为这是我在本教程中承诺的,但你也可以使用 Winform。

此方法的核心是你的 C# 库中的 `DllExport`。我们使用 `DllExport` 在 C++ 非托管应用程序的核心中运行 C# 应用程序,一切都由 **操作系统** 完成,无需使用 CLRHosting 和 ActiveX。

要从托管 DLL 导出函数,你需要 `DllExport` nuget 包,你可以在 **这里** 找到它。

创建 WPF/Winform 窗口并设计你的布局、皮肤等。

这是我用 WpfPlus 库 为我的简单压缩器设计的。

正如你所见,有一些按钮,一个用于存储文件的列表,以及一个用于数据调试等的日志视图。

快速提示:在构建你的 GUI 库之前,请确保你将 **窗口样式设置为 None 和 Unresizable**。

连接的魔力

好了,是时候施展一些魔法(科学)来连接我们的 **非托管世界和托管世界** 了。

为了在我们的 C++ 和 C# 应用程序之间建立清晰良好的 **通信,我们将使用函数指针和委托。**

我们有两个 API 函数

  1. `LZ4_Compress_File`,返回布尔值,输入一个文件名
  2. `LZ4_Decompress_File`,返回布尔值,输入两个文件名和一个大小

现在我们必须获取函数指针,在你的全局对象中添加这些函数指针 `typedef`s

typedef void (*LZ4_Compress_File_Ptr)(void);
typedef void (*LZ4_Decompress_File_Ptr)(void);
  1. 在 C# 项目中,添加一个类并命名为 `UIBridge`,并根据你的平台将此函数添加到其中

    对于 WPF

    using System;
    using DllExportLib; /// This depends on your using library
    using System.Windows.Interop;
    
    namespace ManagedUIKitWPF
    {
        class UIBridge
        {
            public static MainView mainview_ui;
            [DllExport]
            static public IntPtr CreateUserInterface(IntPtr api_1_ptr, IntPtr api_2_ptr)
            {
                mainview_ui = new MainView(api_1_ptr, api_2_ptr)
                {
                    Opacity = 0,
                    Width = 0,
                    Height = 0
                };
                mainview_ui.Show();
                return new WindowInteropHelper(mainview_ui).Handle;
            }
    
            [DllExport]
            static public void DisplayUserInterface()
            {
                mainview_ui.Opacity = 1;
            }
    
            [DllExport]
            static public void DestroyUserInterface()
            {
                mainview_ui.Close();
            }
        }
    }

    对于 Winform

    using System;
    using DllExportLib; /// This depends on your using library
    using System.Windows.Forms;
    
    namespace ManagedUIKitWPF
    {
        class UIBridge
        {
            public static MainView mainview_ui;
            [DllExport]
            static public IntPtr CreateUserInterface(IntPtr api_1_ptr, IntPtr api_2_ptr)
            {
                mainview_ui = new MainView(api_1_ptr, api_2_ptr)
                mainview_ui.Opacity = 0f;
                mainview_ui.Show();
                return mainview_ui.Handle;
            }
    
            [DllExport]
            static public void DisplayUserInterface()
            {
                mainview_ui.Opacity = 1.0f;
            }
    
            [DllExport]
            static public void DestroyUserInterface()
            {
                mainview_ui.Close();
                mainview_ui.Dispose();
            }
        }
    }

    为什么在创建窗口时透明度为 0? 这很简单,因为免费创建窗口时会闪烁,如果你不使用 0 透明度,当你想显示它时,它会随机闪烁,我们会在宿主准备好时手动显示 Window/Form。

    如果我有太多函数怎么办? 尝试传递一个 `IntPtr` 列表或数组,甚至更简单,传递一个 `string` 或 JSON 数据,你想怎么都可以,找到你想要的方式!

    此外,要从原生应用程序打印日志数据到 .NET GUI,请使用相同的方法添加 **PrintLog(string log_str)** 导出并在 C++ 中分配它们。

  2. 好的,现在进入你的窗口后端代码并添加你的函数的委托。
    using System;
    using System.Windows;
    using System.Runtime.InteropServices; /// We need this...
    
    namespace ManagedUIKitWPF
    {
        public partial class MainView : Window
        {
            //// API Delegates
            delegate bool LZ4_Compress_File_Ptr(string filename);
            delegate bool LZ4_Decompress_File_Ptr(string filename,long filesize);
    
            //// Ported Functions
            LZ4_Compress_File_Ptr LZ4_Compress_File;
            LZ4_Decompress_File_Ptr LZ4_Decompress_File;
    
            public MainView(IntPtr api_1_ptr, IntPtr api_2_ptr)
            {
                InitializeComponent();
            }
        }
    }
  3. 现在我们需要使用 C++ 传递的指针在 C# 中获取函数。我们在 .NET 中有一个叫做 `Marshal.GetDelegateForFunctionPointer` 的绝妙工具,它正是我们所需要的。

    将此代码添加到你的窗口代码中,**紧随** `InitializeComponent()` 之后

    InitializeComponent();
    
    //// Recovering Native Functions
    LZ4_Compress_File = (LZ4_Compress_File_Ptr)Marshal.GetDelegateForFunctionPointer
                        (api_1_ptr, typeof(LZ4_Compress_File_Ptr));
    LZ4_Decompress_File = (LZ4_Decompress_File_Ptr)Marshal.GetDelegateForFunctionPointer
                          (api_2_ptr, typeof(LZ4_Decompress_File_Ptr));
  4. 最后,是时候从 C++ 传递函数了。首先,我们需要加载我们的 .NET GUI 库,在 **全局对象** 中定义这些对象和值。
    typedef HWND(*CreateUserInterfaceFunc)(LZ4_Compress_File_Ptr, LZ4_Decompress_File_Ptr);
    CreateUserInterfaceFunc CreateUserInterface;
    typedef void(*DisplayUserInterfaceFunc)(void);
    DisplayUserInterfaceFunc DisplayUserInterface;
    typedef void(*DestroyUserInterfaceFunc)(void);
    DestroyUserInterfaceFunc DestroyUserInterface;

    快速提示:你可以同时使用 `DisplayUserInterfaceFunc` 类型来处理 `DisplayUserInterface` 和 `DestroyUserInterface`,因为它们使用相同的类型,但你知道……

  5. 使用 `LoadLibrary` 和 `GetProcAddress` 来加载你的 .NET GUI 库和函数。
    /// Loading dotNet UI Library
    HMODULE dotNetGUILibrary = LoadLibrary(L"ManagedUIKitWPF.dll");
    CreateUserInterface = (CreateUserInterfaceFunc)GetProcAddress
                          (dotNetGUILibrary,"CreateUserInterface");
    DisplayUserInterface = (DisplayUserInterfaceFunc)GetProcAddress
                           (dotNetGUILibrary, "DisplayUserInterface");
    DestroyUserInterface = (DestroyUserInterfaceFunc)GetProcAddress
                           (dotNetGUILibrary, "DestroyUserInterface");
    < 将此代码添加到 `ShowWindow(cpphwin_hwnd, SW_SHOW);` 之前 >
  6. 在 `ShowWindow` 之前使用 `CreateUserInterface` 并将函数指针传递给 dotNet 库……
    /// Creating .Net GUI
    wpf_hwnd = CreateUserInterface(
        (LZ4_Compress_File_Ptr)&LZ4_Compress_File,
        (LZ4_Decompress_File_Ptr)&LZ4_Decompress_File);

好了……现在构建你的应用程序和库,然后试试。如果你遇到错误或崩溃,请仔细检查你的代码……

重要提示

如果你收到了关于 **STA 线程** 的任何错误,只需在调用 **CreateUserInterface** 函数之前在你的代码中添加 `CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);`……

警告

如果你为应用程序使用 WPF UI,请不要在你的 C++ 宿主窗口中使用 **双缓冲 (WS_EX_COMPOSITED)** 功能,WPF 使用 DirectX 渲染器并且已经具备双缓冲。如果你在宿主窗口中使用它,它会破坏 WPF 渲染。

**做得好!** 现在你的非托管应用程序已连接到你托管的 GUI 之下。是时候在前端也连接它们了!

在 C++ 环境中显示 GUI

还记得我从 Heisenberg 那里借鉴了基础部分吗?好吧……现在是时候了……

A) 将 WPF 窗口显示为自定义控件 (子控件) 在我们的宿主窗口中

这是将你的 WPF 窗口变成子窗口并完美地托管在你的原生窗口中的神奇代码。

RECT hwin_rect; /// Add this in global objects

/// Check if WPF Window is valid
if (wpf_hwnd != nullptr) {

        /// Disable Host Window Updates & Draws
        SendMessage(cpphwin_hwnd, WM_SETREDRAW, FALSE, 0);

        /// Disable Host Window Double Buffering
        long dwExStyle = GetWindowLong(cpphwin_hwnd, GWL_EXSTYLE);
        dwExStyle &= ~WS_EX_COMPOSITED;
        SetWindowLong(cpphwin_hwnd, GWL_EXSTYLE, dwExStyle);

        /// Set WPF Window to a Child Control
        SetWindowLong(wpf_hwnd, GWL_STYLE, WS_CHILD);

        /// Get your host client area rect
        GetClientRect(cpphwin_hwnd, &hwin_rect);

        /// Set WPF Control Order, Size and Position
        MoveWindow(wpf_hwnd, 0, 0, hwin_rect.right - hwin_rect.left, 
                   hwin_rect.bottom - hwin_rect.top, TRUE);
        SetWindowPos(wpf_hwnd, HWND_TOP, 0, 0, hwin_rect.right - hwin_rect.left, 
                     hwin_rect.bottom - hwin_rect.top, SWP_NOMOVE);

        /// Set WPF as A Child to Host Window...
        SetParent(wpf_hwnd, cpphwin_hwnd);

        /// Skadoosh!
        ShowWindow(wpf_hwnd,SW_RESTORE);

        /// Display WPF Control by resetting its Opacity
        DisplayUserInterface();
}
这段代码应该放在 **CreateUserInterface** 和 **ShowWindow** 之间。

B) 发布控件更新和消息

如果用户调整宿主窗口的大小,控件也需要调整大小。回到 `HostWindowProc` 回调,并在其中添加大小调整事件和关闭事件。

//// Host Window Callback
LRESULT CALLBACK HostWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_CLOSE:
        DestroyUserInterface(); //// Destroy WPF Control before Destroying Host Window
        DestroyWindow(hwnd);
        break;
    case WM_DESTROY:
        isHWindowRunning = false;
        break;
    case WM_SIZE: //// Resize WPF Control on Host Window Resizing
        if (wpf_hwnd!=nullptr) {
            GetClientRect(cpphwin_hwnd, &hwin_rect);
            MoveWindow(wpf_hwnd, 0, 0, hwin_rect.right - hwin_rect.left, 
                       hwin_rect.bottom - hwin_rect.top, TRUE);
        }
        break;
    default:
        return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

好的。:) 是时候构建你的应用程序和库了……瞧……你的 C++ 应用程序中拥有了一个流畅无闪烁的 WPF UI。

问题 1:应用程序运行但冻结了

如果你启动了你的 EXE,但它只是冻结了,不要惊慌……**这是完全正常的!**

在某些情况下,例如 **控制台应用程序或独立应用程序**,其中线程默认运行,WPF UI 在同一线程中运行,其消息循环会中断主应用程序线程。

在这种情况下,你应该使用 **多线程**,并为你的 GUI 创建一个单独的线程,这样你的应用程序就可以在两个不同的线程中运行:**后端线程** 和 **前端线程**。

这对于复杂的应用程序来说更加安全和标准化。好的,进入你的托管桥并修改代码如下:

using System.Threading; //// Add this in your file

class UIBridge
{
 public static MainView mainview_ui;
 public static Thread gui_thread;
 public static IntPtr mainview_handle = IntPtr.Zero;

 [DllExport]
 static public IntPtr CreateUserInterface
        (IntPtr api_1_ptr, IntPtr api_2_ptr) /// Multi-Threaded Version
        {
            gui_thread = new Thread(() =>
            {
                mainview_ui = new MainView(api_1_ptr, api_2_ptr)
                {Opacity = 0,Width = 0,Height = 0};
                mainview_ui.Show();
                mainview_handle = new WindowInteropHelper(mainview_ui).Handle;
                System.Windows.Threading.Dispatcher.Run();
            });
            gui_thread.SetApartmentState(ApartmentState.STA); /// STA Thread Initialization
            gui_thread.Start();

            while (mainview_handle == IntPtr.Zero) { }
            return mainview_handle;
        }

 [DllExport]
 static public void DisplayUserInterface() /// Multi-Threaded Version
        {
            try
            {
                mainview_ui.Opacity = 1;
            }
            catch /// Can't Access to UI Thread, So Dispatching
            {
                mainview_ui.Dispatcher.BeginInvoke((Action)(() => {
                    mainview_ui.Opacity = 1;
                }));
            }
        }

 [DllExport]
 static public void DestroyUserInterface() /// Multi-Threaded Version
        {
        try {
             mainview_ui.Close();
            }
            catch /// Can't Access to UI Thread, So Dispatching
            {
                mainview_ui.Dispatcher.BeginInvoke((Action)(()=> {
                    mainview_ui.Close();
                }));
            }
         }
}

现在构建并再次尝试……它现在可以工作了!:)

问题 2:我无法访问宿主窗口的创建

好的各位,有些情况我们无法完全访问宿主窗口的创建,例如当我们想在 3ds Max、Photoshop、QT 应用等应用程序的原生插件中使用我们的 WPF UI 时。

作为插件,SDK 允许你创建 *子窗口、面板、卷展栏、卷帘、自定义控件* 并返回一个句柄,但所有这些东西的底层都是窗口!

在这种情况下,有三个问题:

  • 我们无法访问宿主窗口的 **WndProc 回调**
  • 我们无法访问宿主窗口的 **句柄指针**
  • 我们不知道目标应用程序中宿主窗口的结构

1) 如何处理 WndProc 控制的缺失

在这种情况下,你需要为你的宿主窗口设置一个 *窗口钩子* 来捕获其消息。此方法有安全风险,如果你尝试在另一个进程的窗口上使用它,因为这是键盘记录器所做的,但在此场景下,这是一个插件,它运行在同一个进程中……这样做没有问题,也没有问题,Windows Defender 也不会因此而阻止你。:)

要为你的宿主窗口设置 Windows 钩子,你只需要它的句柄,并且大多数 SDK 都返回宿主窗口/面板的句柄,我们通过 `SetWindowSubclass` 使用这个句柄来为我们的宿主窗口/面板设置一个替代的消息捕获回调。

在你的插件代码中创建相同的 `HostWindowProc`,并在显示窗口/面板后立即添加此代码。

SetWindowSubclass(<Window/Panel Handle>, &HostWindowProc, <Subclass UID example '6663'>, 0);

并且不要忘记在你的 `WndProc` 中进行两个更改:

  1. 在 `WM_DESTROY` 中添加 `RemoveWindowSubclass(hWnd, &HostWindowProc, 1);`
  2. 更改你的 `HostWindowProc` `CALLBACK` 参数为:
LRESULT CALLBACK HostWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, 
                                LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)

完成!现在你可以处理任何类型的窗口和面板的大小调整和其他消息事件了。

2) 如何处理缺失的句柄

在这种情况下,你需要查询你的目标应用程序窗口并找到创建的宿主窗口/面板,以便能够完成其余工作。如果 SDK 没有给你句柄,它会让你提供一个标题,你只需要查询并按标题查找。

要查找你进程中的所有子窗口,你需要使用 `EnumChildWindows` 和 `GetWindowText` 或 `GetClassName`。

//// Global Objects
HWND host_window_handle;

//// Window Query to Find Host Window/Panel
BOOL CALLBACK HostWindowQueryCB(HWND hwnd, LPARAM lParam) 
{
    TCHAR* target_class_name = L"Window/Panel Class Name";
    TCHAR* target_text = L"Window/Panel Title Text";

    //// Get Class Name Of Window
    TCHAR wnd_class_name[MAX_PATH];
    GetClassName(hwnd, wnd_class_name, _countof(wnd_class_name));
    
    //// Get Title/Text Of Window
    TCHAR wnd_title_text[MAX_PATH]; 
    GetWindowText(hwnd,wnd_title_text,MAX_PATH);
    
    //// Compare Window Text
    if (_tcscmp(wnd_title_text, target_text) == 0) { 
        host_window_handle = hwnd;
        return FALSE; /// Found it 
    }

    //// Compare Window Class Name
    if (_tcscmp(wnd_class_name, target_class_name ) == 0) {
        host_window_handle = hwnd;
        return FALSE; /// Found it
    }

    return TRUE;
}

并使用此函数开始查询……

//// Query Child Windows to find target Host
EnumChildWindows(<Your Application Main Window Handle>, HostWindowQueryCB, 0);

优化提示:你可以在 `IF_ELSE` 中使用 `GetWindowThreadProcessId` 和 `GetCurrentProcessId` 来检查窗口是否属于你当前进程;如果是,则进行其余的获取和比较等操作。

3) 如何处理目标应用程序的未知结构

这真的很简单! 你可以使用微软的强大轻量级工具“Spy++”。你可以在你的 Visual Studio 文件夹中找到这个小工具,路径是 *Microsoft Visual Studio\20XX\Enterprise\CommonX\Tools*。

编程前端 UI

恭喜! 你已经走到了最后阶段……是时候开发我们的托管前端并让一切正常工作了……

在前端开发中,我们有两种不同的策略:

  • 在后端完成所有工作,只使用 C# 调用函数、显示数据或绘制自定义内容。
  • 更多地使用 C# 而不仅仅是 UI,并在开发中获得更多力量!

示例:*在我的压缩器中,我可以使用按钮将文件添加到后端 C++ 列表并处理所有后台操作,然后仅在 WPF 列表中显示数据,并使用按钮调用压缩/解压缩函数,或者我可以利用 C# 来管理文件、压缩和解压缩过程,这可以为我节省大量时间!*

这完全取决于你的决定,我们生活在一个自由的世界,不是吗?;)

在托管应用程序中使用你的原生 API 就像

/// Compression UI Function
private void Compress_Button_Click(object sender, RoutedEventArgs e)
{
     string file_path = get_selected_file();
     bool compression_process = LZ4_Compress_File(file_path); /// Native API

     if (compression_process)
     PrintLog($"File '{Path.GetFileName(file_path)}' has been compressed successfully!");
     else
     PrintLog($"File '{Path.GetFileName(file_path)}' compression failed.");
}

完成 UI 编程后,构建你的应用程序和库。运行你的 C++ 应用程序。

现在你将看到科学的魔力……:)

要观看流畅的视频,请**点击此处下载 'CppWPFUI.mp4' (350 KB)**

最佳解决方案

将你的 GUI 打包到 C++ 应用程序中

你可以使用一个名为“**Enigma Virtual Box**”的优秀免费应用程序,你可以在官方网站 **这里** 获取它。

选择你的 C++ EXE,并在虚拟框的根目录下添加你的 WPF GUI 库 * .dll 文件,然后将 EXE 构建为单个文件,并在设置中使用压缩。

现在你拥有一个干净、轻量级、单个文件的 C++ 应用程序,具有漂亮、流畅、无闪烁的 WPF 用户界面。

提高原生函数的安全性

为了保护你的 C++ API 指针免受非官方程序集的侵害,你可以使用 MD5 或 SHA 哈希技术来检查请求者是否被修改。

让我向你介绍一个很棒的库 Digest++,它提供了完美的哈希方法,并且是头文件式的!

保护你的原生函数

  1. 在构建 GUI 库的最终版本后,DLL 获取其哈希值,例如 **MD5、SHA512、SHA1** 等。
  2. 写下来,并将哈希值存储为 **XOR 混淆字符串** (或 AES256) 在你的主 C++ 应用程序中。
  3. 在使用 `LoadLibrary` 函数加载你的 GUI 库 DLL 之前,再次获取其哈希值,并将其与你之前生成的**预计算**哈希值进行比较,如果匹配,则继续处理……

转换为 Windows 子系统

完成调试后,你可以将控制台应用程序转换为 Windows 子系统,方法如下:

  1. 将 `int main()` 替换为 `int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd)`
  2. 转到 **C++ 项目设置 > 链接器 > 系统**,并将 **子系统** 从 `Console (/SUBSYSTEM:CONSOLE)` 更改为 `Windows (/SUBSYSTEM:WINDOWS)`。

在 QT 框架中使用此方法

QT 中的一切都与原生 WinAPI 类似,你只需要遵循以下提示:
  • 使用此函数获取 QT 窗口/控件的原生句柄。
HWND GetQTNativeHwnd = (HWND)MyQTWindow->winId();
  • 而不是使用 `Rect` 和 `GetClientArea`,使用:
QSize QTHostSize = MyQTWindow->size();
int win_w = QTHostSize.width();
int win_h = QTHostSize.height();

MoveWindow(wpf_hwnd, 0, 0, win_w, win_h, TRUE);
SetWindowPos(wpf_hwnd, HWND_TOP, 0, 0, win_w, win_h, SWP_NOMOVE);

源代码

你可以从下面的链接下载完整的源代码和最终二进制文件。

下一步?

在我文章系列的下一部分,我将教你如何在 C# 托管应用程序中直接使用你的所有 C++ 内存和值,这样你就无需构建新值、传递它们或分配更多内存。

当你处理大量数据时,这项技术非常有帮助!

我希望本教程对您有所帮助。:)

此外,请随时在它的官方 Discord 服务器上关注我正在开发的最新应用程序。

新年快乐!

历史

  • 2020 年 1 月 2 日:首次发布
  • 2020 年 1 月 6 日:添加了功能齐全的 Qt 示例源代码。
© . All rights reserved.