Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / QT

Create An Awesome WPF UI for Your C++/QT Applications

Rate me:
Please Sign up or sign in to vote.
5.00/5 (32 votes)
6 Jan 2020CPOL15 min read 67.4K   2.6K   53   30
This article will teach you how to create an amazing, clean and smooth WPF/Winform UI for your native application without using any complex, unsafe, ActiveXish methods, etc.

Introduction

I haven't written an article in a long long time, so, I'm here today with an awesome one. :)

This article is for C++/QT programmers who want to use amazing features of WPF within their C++ code.

If you're a .NET programmer, just walk away, it's not yours. (You can Use Invoking & DllImport stuff :v)

Background & Backstory

When we talk about WPF, we talk about an easy-to-use, user-friendly & high performance framework created by Microsoft for .NET users.

It has the power of DirectX Renderer combined with HTML/CSS styling and it's open source! There are too many reasons which makes this little guy better than any other solution for Desktop Applications.

Better than QT, Winforms, HTML/CSS, Here's why...

  • QT Framework: It's not free if you need to use static linking. It has too many dependencies, also, it has poor component library and you need to buy third party libraries or you should create your components yourself.
  • Winforms: Very well designed, still a hero, but it's a little bit old and uses GDI+.
  • HTML/CSS: Needs heavy sized dependencies like chromium, etc. You should ship 60+ MB for a simple hello world app developed in HTML/CSS and if you choose Electron, it comes in 80+ MB.
  • WPF: Uses GPU Rendering, It has a amazing designer, is fully customizable and ships with Windows itself, there is no need for any dependencies, Microsoft is using it in Windows over and over again. Also, .NET Core 3.0 has support of WPF which means it can be used for cross-platform applications too!

But the issue is it's only for .NET and C/C++ or Delphi developers cannot use it for their GUI if they want to. They should switch on C# language which has low security and less performance than C++ and if they want to mix it up with current solutions on the internet, it becomes unsafe, unstable and not cool at all!

But sometimes doing something complex is so much simpler than it looks! How? Let me quote from a KING:

Heisenberg:

Awh nothing special... it's just the Basics!

I always wanted to make this work!... Using C++ as my backend application and WPF/Winform for GUI and frontend because it's safe/fast in backend and pretty/easy-to-use in frontend.

In this solution, you can use WPF/Winforms and QT and whatever else you want together.

Current Existing Methods

Okay before we start, let's take a look at the current methods on the market and make a brief review...

  • ActiveX: The first thing you always hear from embedding a WPF/Winform inside C++ ActiveX control but it's bad, really bad! Creating another platform for using two another platforms ... awh that's mixed up! and on top of everything, it's visible and accessible to everyone and they can use it too... an advice from your programmer friend... never use COM for UI...
  • CLRHosting: Second thing everyone finds on net is CLR Hosting Interface, yeah it's good and stable but not enough for mixing only UI and frontend, I prefer this to run a whole .NET app inside my native application.
  • C++/CLI: The last thing anyone wants to mess with. Needs invoking and lots of pain, also CLR/C++ assemblies are hard to integrate.
  • Noesis GUI: Another possible way, But eh ... needs implanting everything, blah blah ...
  • WPF Rendering Redirection: In this method, you run a hidden instance of WPF window and renders it in a texture2D, then you show it in a C++ window and redirect every received window messages to WPF hidden window. It works! but ... you know... we are here for a clean and professional way... :D

My Pretty Basic Method

This method I will teach you is a part of my big tutorial pack "Architecting Your Application Like A Boss". I will publish it part by part on CodeProject so ... take a seat and enjoy the show! :)

Steps we're going to follow are:

  1. Creating a host window in C++
  2. Creating our WPF GUI library in C#
  3. Using our library in C++ to create WPF GUI
  4. Exchanging functions with C# from C++ and reverse
  5. Creating callbacks and installing Windows hooks
  6. There's no step 6, it's pretty basic remember ? :D
In My Method ...

Only thing you need is a single handle...

Okay, enough talk, let's do this! :)

Creating a Host Window

Open Visual Studio and create a C++ Console Application in x64, use this basic code as your main cpp file:

C++
//// 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();
}

Now let's create our unmanaged Host Window ...

  1. Define global objects under //// Global Objects and define Configs in //// Global Configs:
    C++
    //// 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. Create window class using (After /// We Code Here ...):
    C++
    /// 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;

    Why not setting 'hbrBackground'? If you choose a background for your native window, it will flick over WPF rendering, just don't set it up.

  3. Add a callback to host window using:
    C++
    //// 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 host window class using:
    C++
    //// Register Window
    if (!RegisterClassEx(&HostWindowClass))
    {
      cout << "Error, Code :" << GetLastError() << endl;
      getchar(); return 0;
    }
  5. Ok time to create the window, but hidden...
    C++
    /// 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. [Optimal] If you want to make your window fixed size, use:
    C++
    /// Making Window Fixed Size
    ::SetWindowLong(cpphwin_hwnd, GWL_STYLE, 
        GetWindowLong(cpphwin_hwnd, GWL_STYLE) & ~WS_SIZEBOX);

Here we go, now we have a native host window...

Configuring and Showing Host Window

Ok, I know most of you know all these things, but there's very small details in parameters you need to be careful about, that's why I'm writing this code for you. ;)

  1. Center your host window using:
    C++
    /// 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. And finally, display the window using:
    C++
    /// Display Window
    ShowWindow(cpphwin_hwnd, SW_SHOW);
    UpdateWindow(cpphwin_hwnd);
    BringWindowToTop(cpphwin_hwnd);
    isHWindowRunning = true;
  3. Add message loop to avoid application freezing using:
    C++
    /// Adding Message Loop
    while (GetMessage(&loop_message, NULL, 0, 0) > 0 && isHWindowRunning)
    {
     TranslateMessage(&loop_message);
     DispatchMessage(&loop_message);
    }

Image 1

Main Unmanaged Application

Ok, time to develop your C++ application functionality, interfaces, etc. In this tutorial, I created a simple C++ application which compresses/decompresses files using LZ4 algorithm.

Of course! .NET can do LZ4 compression just as perfect as C++ but this is an example for using a native library (lz4 library) and the point is using WPF just as GUI, your native application can be anything... there's no limitation.

Also at the end of the article, I tell you how you can use this method on Plugins and SDKs for which you don't have full access to entire parts.

Here's the code of Main app but it's not a part of this tutorial, so I will not explain it... you can use anything you want. :)

C++ LZ4 Compressor/Decompressor Application

C++
//// 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

Quick Note: Original size can be appended in the beginning or end of your compressed file.

Creating WPF/Winform User Interface

Okay, Now it's time to create our WPF or Winform user interface, Create a x64 C# Library with your desired platform, I use WPF because it's what I promise in this tutorial, but you can use Winform as well.

The main key of this method is DllExport from your C# library. We run a C# application right inside the heart of a C++ unmanaged application using DllExports and everything will be done by the Operating System, there is no need to use CLRHosting and ActiveX.

For exporting your functions in a managed DLL, you need DllExport nuget package which you can grab from here.

Create a WPF/Winform window and design your layouts, skins and stuff.

Here's what I designed for my simple compressor with WpfPlus library.

Image 2

As you can see, there are some buttons and a list for storing files and a log view for data debug, etc.

Quick Note: Before building your GUI library, make sure you set your Window Style to None and Unresizable.

Magic of Connection

Well, it's time to do some magic (science) to connect our Unmanaged world to Managed one.

For creating a clean and good communication between our C++ and C# app, we're going to use function pointers and delegating.

We have two API functions:

  1. LZ4_Compress_File with boolean output and one filename input
  2. LZ4_Decompress_File with boolean output and two filename and size input

Now we have to get pointers of our functions, add this function pointers typedefs in your global objects:

C++
typedef void (*LZ4_Compress_File_Ptr)(void);
typedef void (*LZ4_Decompress_File_Ptr)(void);
  1. In C# project, add a class and name it UIBridge and add this function into it based on your platform:

    For WPF:

    C#
    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();
            }
        }
    }

    For Winform:

    C#
    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();
            }
        }
    }

    Why opacity is 0 in window creation? It's simple, because of flicking free window creation, if you don't use 0 opacity, it flicks randomly when you're trying to show it, we manually display the Window/Form when our host will be ready...

    What to do if I had too many functions? Just try to pass an IntPtr list or array or even simpler, a string or json data, whatever you want is possible, just find your desired way!

    Also, for printing log data from your native app to .NET GUI, use the same method to add PrintLog(string log_str) export and assign them in C++.

  2. Okay, now go in your window backend code and add delegates of your functions:
    C#
    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. Now we need to get functions in C# using pointers we passed from C++. We have something amazing in .NET known as Marshal.GetDelegateForFunctionPointer which does exactly what we need.

    Add this code to your window code right after InitializeComponent():

    C#
    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. And finally, it's time to pass the functions from C++. First of all, we need to Load our .NET GUI library, define these objects and values in your Global Objects:
    C++
    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;

    Quick Note: You can use DisplayUserInterfaceFunc type for both DisplayUserInterface and DestroyUserInterface because they're using the same type, but you know ...

  5. Use LoadLibrary and GetProcAddress to Load your .NET GUI library and functions:
    C++
    /// 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");
    < Add this code before ShowWindow(cpphwin_hwnd, SW_SHOW); >
  6. Use CreateUserInterface just before ShowWindow and pass function pointers into dotNet Library...
    C++
    /// Creating .Net GUI
    wpf_hwnd = CreateUserInterface(
        (LZ4_Compress_File_Ptr)&LZ4_Compress_File,
        (LZ4_Decompress_File_Ptr)&LZ4_Decompress_File);

Here we go... now build your application and library and give it a try. If you get error or crash, please review your code with more details...

IMPORTANT NOTE :

If you received any error about STA thread, just add CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); in your code before calling CreateUserInterface function...

WARNING :

If you're using WPF UI for your application don't use Double Buffering (WS_EX_COMPOSITED) feature on your c++ host window, WPF uses DirectX renderer and has already double buffering, If you use it in your host window it will break WPF rendering.

Good job! Now your unmanaged app is connected to your managed GUI under the skin. It's time to connect them in frontend too!

Displaying GUI in C++ Environment

Remember that I riped off the basic part from Heisenberg? Well... it's on ...

A) Displaying WPF Window as a Custom Control (child) in our Host Window

Here's the magic code to turn your WPF window to a child and host it perfectly in your native window:

C++
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();
}
This code should be between CreateUserInterface and ShowWindow

B) Post Control Updates And Messaging

If the user resizes the host window, our control needs to be resized too, go back in HostWindowProc callback and add resize event and close event into it:

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

Okay. :) Time to build your application and library ... and here you go ... you have a smooth and flicker free WPF UI inside your C++ application.

Issue 1: Application Runs But It Freezes

If you launched your EXE and it simply went frozen, don't panic... It's totally natural!

In some cases like Console Apps or Standalone apps in which threading is running as default, WPF UI runs in the same thread and its message loop breaks the main application thread.

In this case, you should Use Multi Threading and create a separate thread for your GUI which makes your application work in two different threads, Backend Thread and Frontend Thread.

It goes even more safer and standard as a complex application. Okay, go in your Managed Bridge and change the code to this:

C#
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();
                }));
            }
         }
}

Now build and try again... It's working now! :)

Issue 2: I Don't Have Access to Host Window Creation

Ok folks, there are some situations when we don't have full access to host window creation, like when we want to use our WPF UI in some native plugin for applications like 3ds Max, Photoshop, QT Apps, etc.

As a plugin, SDKs lets you create Child Windows, Panels, Rollups, Rollouts, Custom Controls and returns a handle to it, but underneath all of this guys are Window!

In this case, there are three issues:

  • We don't have access to the host window WndProc Callback
  • We don't have access to the host window Handle Pointer
  • We don't know the structure of host window in target application

1) How to Deal With Lack of WndProc Control

In this case, you need to set a Window Hook on your host window to catch its messages, this method has security side effects if you try to use it on another process window because this is what keyloggers do, but in our case it's a plugin and it's running inside the same process so... there's no problem and any issue to doing this and Windows defender doesn't bust your ass on this. :)

To setup a windows hook on your host window, you only need its handle and most of the SDKs return the handle of host window/panel and we use this handle via SetWindowSubclass to set an alternative message capture callback to our host window/panel.

Create the same HostWindowProc in your plugin code and add this code just after you showed your window/panel:

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

And don't forget to apply two changes in your WndProc:

  1. Add RemoveWindowSubclass(hWnd, &HostWindowProc, 1); in WM_DESTROY
  2. Change your HostWindowProc CALLBACK parameters to:
C++
LRESULT CALLBACK HostWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, 
                                LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)

Done! Now you can handle resizing and other message events on any kind of window and panel.

2) How to Deal With Missing Handles

In this case, you need to query your target application windows and find the created host window/panel to be able to do the rest. If SDK doesn't give you the handle, it lets you provide a title for it, you just need to query and find it by its title.

To find all child windows in your process, you need to use EnumChildWindows and GetWindowText or GetClassName:

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

And use this function to start the query ...

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

Optimization Tip: You can use GetWindowThreadProcessId and GetCurrentProcessId in an IF_ELSE to check if window belongs to your current process or not, if yes, then do the rest getting & comparing and stuff.

3) How to Deal Unknown Structures of Target Application

It's really simple! You can use a great and lightweight tool from Microsoft "Spy++". You can find this little guy in your Visual Studio folder Microsoft Visual Studio\20XX\Enterprise\CommonX\Tools.

Programming Frontend UI

Congratulations! You could make your way to the final stage... It's time to develop our managed frontend and make everything work just fine...

In Frontend development, we have two different strategies:

  • Do everything in backend and just use the C# to calling functions, displaying data or drawing custom things.
  • Use C# more than just a UI and gain more power in development!

Example: In my compressor, I can use buttons to add files in backend C++ list and do everything behind and just display data in WPF list and use buttons to call compress/decompress functions or I can use C# to manage files, compressing and decompressing processes which saves me a lot of time!

It completely depends on your decision and we're living in a free world, right? ;)

Using Your Native API in Your Managed App Is Like

C#
/// 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.");
}

After finishing your UI programming, build your application and library. Run your C++ application.

Now you're watching the magic of science... :)

Image 3

To watch a video of smoothness, click here to download 'CppWPFUI.mp4' (350 KB)

Optimal Solutions

Packing Your GUI in C++ Application

You can use an amazing free application "Enigma Virtual Box" which you can grab from here on the official site.

Select your C++ EXE and add your WPF GUI library .dll file in root of virtual box, then build your EXE to a single one and use compression in settings.

Now you have a clean, lightweight and single file C++ application with a beautiful, smooth, flicker free WPF user interface.

Improving Security of Your Native Functions

To secure your C++ API pointers from unofficial assemblies, you can use MD5 or SHA hashing technique to check if requester has been modified or not.

Let me introduce you to an amazing library Digest++ which gives you a perfect hashing methods and it's header-only!

To secure your native functions:

  1. After building the final version of GUI library, DLL get its hash in MD5, SHA512, SHA1, etc.
  2. Write it down and store the hash as XOR Obfuscated String (Or AES256) in your main C++ application.
  3. Before using LoadLibrary function to load your GUI library DLL, get its hash again and compare it to pre-calculated hash you generated before and if it matched, continue the processing...

Converting to Windows SubSystem

After you're done with debugging, you can turn your console application to Windows SubSystem following:

  1. replace int main() with int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd)
  2. Go in C++ Project Settings>Linker>System and change SubSystem from Console (/SUBSYSTEM:CONSOLE) to Windows (/SUBSYSTEM:WINDOWS)

Using this Method in QT Framework

Everything in QT is like Native WinAPI, you just need to follow these tips:
  • Use this function to get Native Handle of your QT Window/Widget:
C++
HWND GetQTNativeHwnd = (HWND)MyQTWindow->winId();
  • Instead of using Rect and GetClientArea, use:
C++
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);

Source Code

You can download the full source code and final binary from the links below:

What's Next?

In the next section of my article series, I will teach you how to directly use all of your C++ Memory and Values inside C# managed application so you don't have to build new values, pass them or allocate more memory.

This technique is a great help when you're dealing with huge amount of data!

I hope this tutorial has been helpful to you. :)

Also, feel free to follow my latest application under development in its official discord server.

Happy New Year!

History

  • 2nd January, 2020: First release
  • 6nd January, 2020: Qt fully-featured example source code added.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer MemarDesign™ LLC.
France France
This is your last chance. After this, there is no turning back. You take the blue pill - the story ends, you wake up in your bed and believe whatever you want to believe. You take the red pill - you stay in Wonderland and I show you how deep the rabbit-hole goes. - Matrix


Hamid.Memar

Comments and Discussions

 
QuestionMultiple wpf UI controls on different QWidgets at the same time Pin
Member 157333721-Sep-22 21:53
Member 157333721-Sep-22 21:53 
AnswerRe: Multiple wpf UI controls on different QWidgets at the same time Pin
The Ænema24-Sep-22 19:57
The Ænema24-Sep-22 19:57 
QuestionCan't make a winforms app with this Pin
Devilbager5-Jul-21 18:39
Devilbager5-Jul-21 18:39 
AnswerRe: Can't make a winforms app with this Pin
Rosseta Stoned7-Jul-22 15:02
Rosseta Stoned7-Jul-22 15:02 
QuestionDoubt Regarding the code ! Pin
Dikshit Sharma18-Nov-20 1:31
Dikshit Sharma18-Nov-20 1:31 
AnswerRe: Doubt Regarding the code ! Pin
Ultran Messiski26-Dec-20 21:36
Ultran Messiski26-Dec-20 21:36 
BugClick "Show Size of File", App crashes in Visual Studio 2019 Pin
Ethan KG11-Aug-20 7:42
Ethan KG11-Aug-20 7:42 
GeneralRe: Click "Show Size of File", App crashes in Visual Studio 2019 Pin
Ultran Messiski29-Aug-20 17:37
Ultran Messiski29-Aug-20 17:37 
PraiseLooking for the "Architecting Your Application Like A Boss" serial Pin
Ethan KG11-Aug-20 6:28
Ethan KG11-Aug-20 6:28 
QuestionProject contains malware Pin
JirkaRSV19-Mar-20 12:25
JirkaRSV19-Mar-20 12:25 
AnswerRe: Project contains malware Pin
Member 1473936823-Mar-20 12:09
Member 1473936823-Mar-20 12:09 
QuestionUnable to compile the ManagedUIKitWPF project Pin
Member 757801713-Jan-20 7:14
Member 757801713-Jan-20 7:14 
AnswerRe: Unable to compile the ManagedUIKitWPF project Pin
Member 1473936814-Feb-20 10:40
Member 1473936814-Feb-20 10:40 
AnswerRe: Unable to compile the ManagedUIKitWPF project Pin
SilverString17-Apr-20 9:38
SilverString17-Apr-20 9:38 
QuestionQML Pin
Member 118961274-Jan-20 13:31
Member 118961274-Jan-20 13:31 
AnswerRe: QML Pin
The Ænema4-Jan-20 16:07
The Ænema4-Jan-20 16:07 
Questionis this cross platform? Pin
aldo hexosa2-Jan-20 15:49
professionalaldo hexosa2-Jan-20 15:49 
AnswerRe: is this cross platform? Pin
The Ænema2-Jan-20 19:35
The Ænema2-Jan-20 19:35 
GeneralRe: is this cross platform? Pin
Member 102418457-Jan-20 2:31
Member 102418457-Jan-20 2:31 
GeneralRe: is this cross platform? Pin
The Ænema8-Jan-20 3:11
The Ænema8-Jan-20 3:11 
GeneralRe: is this cross platform? Pin
slator20138-Jan-20 4:18
slator20138-Jan-20 4:18 
GeneralRe: is this cross platform? Pin
The Ænema8-Jan-20 4:27
The Ænema8-Jan-20 4:27 
GeneralRe: is this cross platform? Pin
slator20138-Jan-20 4:53
slator20138-Jan-20 4:53 
GeneralRe: is this cross platform? Pin
The Ænema8-Jan-20 6:37
The Ænema8-Jan-20 6:37 
GeneralRe: is this cross platform? Pin
Olivier DALET12-Sep-20 10:11
Olivier DALET12-Sep-20 10:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.