Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / XAML

WinUI3 in Existing Win32 Applications

4.95/5 (18 votes)
6 May 2023CPOL8 min read 30.3K  
How to use the WinUI3 controls in a plain Win32 application without packaging
In this article, you will see how to use WinUI3 from a normal Win32 application.

Introduction

Let's face it. If Microsoft creates something that is compatible with the plain Win32 API, all will still use this old API and occasionally try the new interface. Therefore, they try hard to make it difficult for me to use whatever new thing they invent.

UWP failed to propagate within Win32. They tried with XAML Islands - I created a small library which (tries to) use it. It doesn't support all the controls and it is full of ugly bugs even with supported controls. Can't be used for serious production. Never mind.

I thought that the only way was to have two executables. However, a great article by Sota Nakamura guided me to the good way. You can have a still Windows 7 - compatible application which can use the WinUI3 right away.

So, let me show you how I did it. Here's a screenshot of my normal Win32 Direct2D app Turbo Play along with a WinUI3 'Yes No Cancel' dialog box.

Image 1

And here is a small demo of this project. A Win32 dialog calls a WinUI dialog:

Image 2

WUI3 Project

  1. Create a new project in Visual Studio "Blank App, Packaged, WinUI 3" in Desktop.
  2. Close the project and open the .vcxproj with a text editor.
  3. Change <AppxPackage>true</AppxPackage> to <AppxPackage>false</AppxPackage>.
  4. Add after the first <PropertyGroup Label="Globals"> the element <WindowsPackageType>None</WindowsPackageType>.
  5. Reopen the project with Visual Studio.
  6. Go to Tools -> NuGet Package Manager -> Manage NuGet Packages for Solution and update the four packages. This ensures the usage of the latest WinUI3 library version.

This ensures you are creating an 'unpackaged' project. The only use of this project is to compile your XAML files into XBF files. There is no more use for it.

Edit your MainWindow.xaml file (or any other XAML file you add to it) and compile. You should have MainWindow.xbf into \x64\Debug\embed.

The Normal Application

  1. Create a new standard Win32 project.

  2. Set the standard to C++ 17.

  3. Optionally make it static (without the DLL runtime) and add the:

    C++
    #pragma comment(linker,"\"/manifestdependency:type='win32' 
    	\ name='Microsoft.Windows.Common-Controls' version='6.0.0.0' 
    	\ processorArchitecture='*' publicKeyToken='6595b64144ccf1df' 
    	language='*'\"") 

    for the styles.

  4. Use Nuget to install WindowsAppSDK and Microsoft.Windows.CppWinRT.

  5. Add a RC file which will contain the xbf file as a resource:

    L1 DATA "..\\wui3\\x64\\debug\\embed\\MainWindow.xbf"
  6. Call the boostrapper indirectly:
    C++
    CoInitializeEx(0, COINIT_APARTMENTTHREADED);
    PACKAGE_VERSION pg = {};
    typedef HRESULT (__stdcall* mi)(
        UINT32 majorMinorVersion,
        PCWSTR versionTag,
        PACKAGE_VERSION minVersion);
    const wchar_t* dll = L"Microsoft.WindowsAppRuntime.Bootstrap.dll";
    auto hL = LoadLibrary(dll);
    mi M = (mi)GetProcAddress(hL, "MddBootstrapInitialize");
    if (!M)
        return E_FAIL;
    auto hr = M(0x00010003, L"", pg);
    

    Now the boostrapper is loaded.

Creating the "App" Class

Let us first see how we build the resources. You have to feed the loader with ms-appx://local/<path> file name with all the XBF resources you saved to the RC file.

C++
void BuildURLS()
    {
        urls.clear();
        wchar_t x[200] = {};
        auto tf = TempFile4(0,0,0);
        tf += L".xbf";
        ExtractResourceToFile(GetModuleHandle(0), L"L1", L"DATA", tf.c_str());
        swprintf_s(x, 200, L"ms-appx://local/%s", tf.c_str());
        urls.push_back(x);
    }

Now let's see the App class:

C++
bool FirstRun = 1;
class AppL : public ApplicationT<AppL, IXamlMetadataProvider>
{
    XamlControlsXamlMetaDataProvider provider;
public:
    std::vector<std::wstring> urls;
    std::vector<Window> windows;
    void BuildURLS() {...}
    void L2()
    {
        BuildURLS();
        if (FirstRun)
        {
            Resources().MergedDictionaries().Append(XamlControlsResources());
            for (size_t i = 0; i < urls.size(); i++)
            {
                windows.emplace_back(Window());
            }
            for (size_t i = 0; i < urls.size(); i++)
            {
                Application::LoadComponent(windows[i], Uri(urls[i].c_str()));
            }
            FirstRun = 0;
            windows[0].Activate();
        }
        else
        {
            for (size_t i = 0; i < urls.size(); i++)
            {
                windows[i].Activate();
                windows[i].Activated = 1;
            }
        }
    }
    void OnLaunched(LaunchActivatedEventArgs const&)
    {
        L2();
    }
    IXamlType GetXamlType(TypeName const& type)
    {
        return provider.GetXamlType(type);
    }
    IXamlType GetXamlType(hstring const& fullname)
    {
        return provider.GetXamlType(fullname);
    }
    com_array<XmlnsDefinition> GetXmlnsDefinitions()
    {
        return provider.GetXmlnsDefinitions();
    }
};

First, you use the callbacks for IXamlMetadataProvider to provide the "Visual Styles" for the WinUI3 (if you don't do that, you will get the old UWP styles).

Second, the first time OnLaunch is called, you must load all your windows because you cannot call Resources().MergedDictionaries().Append(XamlControlsResources()); twice. Whether you Activate() or Hide/Show your windows later is up to you, but loading must be done immediately to all of them.

In your WinMain now:

C++
auto app3 = make<AppL>();
Application::Start([&](auto&&) {
    app3;
    });

As long as there are windows visible, this won't return. If it returns, you can call it again.

Install the Redistributable

For an unpackaged app, you must, before running it, install a redistributable as admin. Get the redist from this link.

You are ready and running!

More Ideas

You can't yet define events in XAML. You have to manually set them up:

C++
Panel p = Content().as<Panel>();
p.FindName(L"myButton").as<Button>().Click([&]
          (IInspectable const&, RoutedEventArgs const&)
    {
        MessageBox(0, L"Clicked", 0, 0);
    });

You can subclass the WinUI windows for handling manually messages in your own Window PRoc:

C++
// Subclass
auto n = as <IWindowNative>();
if (n)
{
    n->get_WindowHandle(&hwnd);
    old = (WNDPROC)GetWindowLongPtr(hwnd, GWLP_WNDPROC);
    SetWindowLongPtrW(hwnd, GWLP_WNDPROC, (LONG_PTR)NEWP);
}

After that, you can use SendMessage as if it was any common Window.

Auto Resizing the Window

C++
auto co = Content();
Panel sp = FindElementByName(L"rsp2").as<Panel>();
if (!Activated)
{
    sp.SizeChanged([&](winrt::Windows::Foundation::IInspectable const& sender,
                           winrt::Windows::Foundation::IInspectable)
    {
        OnChangeSize(sender, false);
    });

You want the window to auto-fit, so I have a second StackPanel inside the first with the name rsp2 and I initiate an OnChangeSize on it:

C++
void OnChangeSize(winrt::Windows::Foundation::IInspectable const& sender, bool f)
{
    auto dlg = sender.as<winrt::Microsoft::UI::Xaml::Controls::StackPanel>();
    auto strn = dlg.Name();
    float xy = GetDpiForWindow(hwnd) / 96.0f;
    auto wi5 = dlg.ActualWidth() * xy;
    auto he5 = dlg.ActualHeight() * xy;
    wi5 += GetThemeSysSize(0, SM_CXBORDER);
    he5 += GetSystemMetrics(SM_CYCAPTION) + GetSystemMetrics(SM_CYSIZEFRAME) + 
       GetSystemMetrics(SM_CYEDGE) * 2;
    SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, (int)wi5, (int)he5, 
             SWP_SHOWWINDOW | SWP_NOMOVE);
CenterWindow2(hwnd);
}

Hosting Win32 controls into a WinUI3 Window

This one helped a lot. You have to set the Window with WS_EX_LAYERED, then call                 SetLayeredWindowAttributes(hwnd, 0, (BYTE)(255 * 100 / 100), LWA_ALPHA);

This still needs some work (for example, acceleration forwarding, etc.) but it works with a bug (WinUI3 controls can't draw over your Win32 window - problem for menus).

The basic rule is that the Win32 window will overlap any WinUI3 controls. That means that you have to put any WinUI at the top, left,right, bottom of the main window and have any Win32 HWNDs inside. What I did for menus is to handle the click on the menu item and display a normal HMENU below that - ugh, ugly, but I can't do anything more at the moment.

Here is Turbo Play with full WinUI3 mode:

 

Image 3

Older Article

Read this only for Information Purposes for the old method of using Winui3. This is now not needed.

Launching an Invisible App

WinUI3 apps can only have one window and once this is destroyed, you cannot create it again. Therefore, your WinUI3 app will only have one window that contains a big XAML of all your UIs and toggle them on demand.

In your OnLaunched event, do these things:

C++
auto ew = make<MainWindow>();
window = ew;
window.Activate();
if (window)
{
    auto n = window.as<IWindowNative>();
    if (n)
    {
        n->get_WindowHandle(&mw);
        if (mw)
        {
            ShowWindow(mw, SW_HIDE);
            auto ew2 = window.as<MainWindow>();
            ew2->Subclass();
            std::thread t(tx, this);
            t.detach();
        }
    }
}

You need to get a HWND to be used later in interprocess (saved in mw), then you need to subclass this Window with a new Window procedure (more on that later).

C++
Old = (WNDPROC)GetWindowLongPtr(mw, GWLP_WNDPROC);
SetWindowLongPtr(mw, GWLP_WNDPROC, (LONG_PTR)NEWW_WP);
mwt = this;

Then, you want to create a thread that waits for a trigger from your win32 app:

C++
void tx(App* app)
{
    if (!app)
        return;
    auto w = app->window.as<winrt::wui3::implementation::MainWindow>();
    w->Run();
}

We will take a look at that Run() function later.

Creating the XAML

As I said, you can only have one XAML. So I created here two entries in it with an infobox (to replace MessageBox) and an AskText dialog.

XAML
<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" 
RequestedTheme="Dark" KeyDown="KeyD2">

    <!-- Ask Text -->
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" 
    VerticalAlignment="Center" x:Name="AskText" Visibility="Collapsed" 
    SizeChanged="szch" Width="500">
        <StackPanel MinWidth="500">
            <InfoBar Name="AskText_Question" IsOpen="True" 
                Severity="Informational"  Title=""  IsIconVisible="False" 
                IsClosable="False"  Message="" />
            <TextBox Name="AskText_Response" Margin="15" 
                Text="" KeyDown="KeyD"/>
            <StackPanel Orientation="Horizontal" Margin="15" 
                HorizontalAlignment="Right">
                <Button Content=""  Margin="0,0,0,5" 
                    Name="AskText_OK"    Click="AskText_ClickOK" 
                    Style="{ThemeResource AccentButtonStyle}"  />
                <Button Content="" Margin="15,0,0,5" 
                Name="AskText_Cancel" Click="AskText_ClickCancel" />
            </StackPanel>
        </StackPanel>
    </StackPanel>

    <!-- Message -->
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" 
    VerticalAlignment="Center" x:Name="Message1" Visibility="Collapsed" 
    SizeChanged="szch" Width="500">
       <StackPanel MinWidth="500">
           <InfoBar MinHeight="100" Name="Message1_Question" 
               IsOpen="True"  Severity="Informational"  Title=""  
               IsIconVisible="False"  IsClosable="False"  Message="" />
           <StackPanel Orientation="Horizontal" Margin="15" 
               HorizontalAlignment="Left">
           <Button Content=""  Margin="0,0,0,5" 
               Name="Message1_OK"    Click="AskText_ClickCancel" 
               Style="{ThemeResource AccentButtonStyle}"  />
       </StackPanel>
       </StackPanel>
    </StackPanel>
    
</StackPanel>

Both StackPanels have Visibility="Collapsed" and SizeChanged="szch" because we will need to resize the main Window to have the same size of whatever the StackPanel resizes itself on first call.

Waiting for the Trigger

Now let's look at that Run function. It will wait for a notification from our main executable to show a dialog box.

First, we read the HwndOfWin32App to check it. If, for any reason, this goes invalid, the WinUI3 app terminates. Then we also return to the Win32 app our own HWND handle and trigger a wait event so our Win32 app knows the WinUI app is ready. For all this communication, I use my USM library (included in this repo).

Then we wait for a trigger from our main app. If this times out and the hwnd is not valid anymore, we exit, else wait.

If we get a trigger, we read a DIALOGID (found in common.h, I've defined DIALOGID_ASKTEXT and DIALOGID_MESSAGE) for now. This also includes reading a XML string (using my own xml3all.h library) so we know how to initialize our dialog. Then, we send a WM_APP message to our window to load it (must be from the main thread!):

C++
// Create the mutex 
u = std::make_shared<USMLIBRARY::usm<>>(usm_cid, 0, 1024 * 1024, 10);
u->Initialize();
u->ReadData((char*)&HwndOfWin32App, 8, 0);
SetTimer(mw, 1, 500, 0);
u->WriteData((char*)&mw, sizeof(HWND), 0, 0);
SetEvent(u->hEventAux1);
for (;;)
{
    auto j = u->NotifyWrite(true, 5000);
    if (j == WAIT_TIMEOUT)
    {
        if (!IsWindow((HWND)HwndOfWin32App))
            break;
    }
    else
    {
        auto id = DIALOGID_NONE;
        auto rd = u->BeginRead();
        if (!rd)
            continue;
        unsigned long long xlen = 0;
        memcpy(&id, rd + 0, sizeof(id));
        memcpy(&xlen, rd + sizeof(id), sizeof(xlen));
        std::vector<char> xmld;
        if (xlen < (1024 * 1024))
        {
            xmld.resize(xlen + 1);
            memcpy(xmld.data(), rd + sizeof(id) + sizeof(xlen), xlen);
        }
        u->EndRead();
        if (xlen < (1024 * 1024))
            SendMessage(mw, WM_APP, id, (LPARAM)xmld.data());
    }
}
if (mw)
    PostMessage(mw, WM_CLOSE, 0xFEFEFEFE, 0);

If we die, we post a WM_CLOSE along with 0xFEFEFEFE flag in order to terminate the app. We need this flag because if the user presses the X button, a WM_CLOSE will be sent without this flag and we must not pass it to the old window proc (or the app will die).

Running the Dialog Boxes

The WM_APP handler will call RunDialog() with the passed ID, along with any initialization XML string:

C++
unsigned long long id = mm - WM_APP;
XML3::XML xx;
if (ll)
    xx = (char*)ll;
current_id = id;
AskText().Visibility(winrt::Microsoft::UI::Xaml::Visibility::Collapsed);
Message1().Visibility(winrt::Microsoft::UI::Xaml::Visibility::Collapsed);
if (id == DIALOGID_ASKTEXT)
{
    XML3::XML* x = (XML3::XML*)&xx;
    Title(x->GetRootElement().vv("title").GetWideValue().c_str());
    AskText_Question().Title(x->GetRootElement().vv
                       ("t0").GetWideValue().c_str());
    AskText_Question().Message(x->GetRootElement().vv
                       ("t1").GetWideValue().c_str());
    AskText_Response().Text(x->GetRootElement().vv
                       ("t2").GetWideValue().c_str());
    AskText_Response().PlaceholderText
           (x->GetRootElement().vv("t3").GetWideValue().c_str());

    if (x->GetRootElement().vv("big").GetValueInt())
    {
        AskText_Response().TextWrapping
        (winrt::Microsoft::UI::Xaml::TextWrapping::Wrap);
        AskText_Response().AcceptsReturn(true);
        AskText_Response().MinHeight(100);
    }
    AskText_Response().SelectAll();
    AskText_OK().Content(box_value(x->GetRootElement().vv
                        ("tOK").GetWideValue().c_str()));
    AskText_Cancel().Content(box_value(x->GetRootElement().vv
                     ("tCancel").GetWideValue().c_str()));

    SetWindowPos(mw, HWND_TOPMOST, 0, 0, 0, 0, SWP_SHOWWINDOW |
                 SWP_NOMOVE | SWP_NOSIZE);
    auto dlg = AskText();
    dlg.Visibility(winrt::Microsoft::UI::Xaml::Visibility::Visible);
    if (ChangedSizeOnce[current_id])
    {
        OnChangeSize(dlg, 1);
    }
}

if (id == DIALOGID_MESSAGE1)
{
    XML3::XML* x = (XML3::XML*)&xx;
    Title(x->GetRootElement().vv("title").GetWideValue().c_str());
    Message1_Question().Title(x->GetRootElement().vv
                        ("t0").GetWideValue().c_str());
    Message1_Question().Message(x->GetRootElement().vv
                        ("t1").GetWideValue().c_str());
    Message1_OK().Content(box_value(x->GetRootElement().vv
                         ("tOK").GetWideValue().c_str()));
    SetWindowPos(mw, HWND_TOPMOST, 0, 0, 0, 0, SWP_SHOWWINDOW |
                 SWP_NOMOVE | SWP_NOSIZE);
    auto dlg = Message1();
    dlg.Visibility(winrt::Microsoft::UI::Xaml::Visibility::Visible);
    if (ChangedSizeOnce[current_id])
    {
        OnChangeSize(dlg, 1);
    }
}

What do we do here?

  • We hide everything.
  • We use the passed XML string to initialize the elements in the dialog.
  • We show the dialog:
C++
SetWindowPos(mw, HWND_TOPMOST, 0, 0, 0, 0, SWP_SHOWWINDOW |
             SWP_NOMOVE | SWP_NOSIZE);
auto dlg = AskText();
dlg.Visibility(winrt::Microsoft::UI::Xaml::Visibility::Visible);
if (ChangedSizeOnce[current_id])
{
    OnChangeSize(dlg, 1);
}

Yes, it's ugly that we need to show it in TopMost, but we want it to hide our main Win32 app. Also, if this is the first time we are showing (ChangedSizeOnce[current_id] == 0), then we will wait for the control to resize, so our szch will be called. If this is not the first one we call it, we must resize it ourselves by manually calling OnChangeSize with a force param.

Changing the Size

C++
void MainWindow::OnChangeSize(winrt::Windows::Foundation::IInspectable const& sender, 
                              bool f) {
  if (f)
            ChangedSizeOnce[current_id] = 0;

        if (ChangedSizeOnce[current_id])
            return;
        ChangedSizeOnce[current_id] = 1;

        auto dlg = sender.as<winrt::Microsoft::UI::Xaml::Controls::StackPanel>();
        float xy = GetDpiForWindow(mw) / 96.0f;
        auto wi5 = dlg.ActualWidth() * xy;
        auto he5 = dlg.ActualHeight() * xy;
        wi5 += GetThemeSysSize(0, SM_CXBORDER);
        he5 += GetSystemMetrics(SM_CYCAPTION) + 
               GetSystemMetrics(SM_CYSIZEFRAME) + GetSystemMetrics(SM_CYEDGE) * 2;
        SetWindowPos(mw, HWND_TOPMOST, 0, 0, (int)wi5, (int)he5, 
                     SWP_SHOWWINDOW | SWP_NOMOVE);
        CenterWindow(mw);

If we are forcing or this is the first time, we get the DPI and the theme border length and resize the main window of the WinUI3 app to match the size of the loaded dialog box.

User Presses X or ESC or Enter

C++
if (mm == WM_CLOSE && ww != 0xFEFEFEFE)
 {
     XML3::XML x;
     if (mwt->current_id == DIALOGID_ASKTEXT)
     {
         auto dlg = mwt->AskText();
         dlg.Visibility(winrt::Microsoft::UI::Xaml::Visibility::Collapsed);
     }
     if (mwt->current_id == DIALOGID_MESSAGE1)
     {
         auto dlg = mwt->Message1();
         dlg.Visibility(winrt::Microsoft::UI::Xaml::Visibility::Collapsed);
     }
     mwt->Cancel(x);
     return 0;
 }

Unless we used the 0xFEFEFEFE flag, we don't want the app to be closed when user presses X. Instead, we close the dialogs and call Cancel() which calls Off();

The same happens on keypresses, whether on general StackPanel (entire dialog), or when the cursor is inside a TextBox:

C++
void MainWindow::KeyD(winrt::Windows::Foundation::IInspectable const& sender, 
                      winrt::Microsoft::UI::Xaml::Input::KeyRoutedEventArgs r)
    {
        auto k = r.Key();
        if (k == winrt::Windows::System::VirtualKey::Enter)
        {
            if (current_id == DIALOGID_MESSAGE1)
            {
                Microsoft::UI::Xaml::RoutedEventArgs a;
                AskText_ClickCancel(sender, a);
            }
        }
        if (k == winrt::Windows::System::VirtualKey::Escape)
        {
            if (current_id == DIALOGID_MESSAGE1)
            {
                Microsoft::UI::Xaml::RoutedEventArgs a;
                AskText_ClickCancel(sender, a);
            }
        }
    }
C++
void MainWindow::KeyD2(winrt::Windows::Foundation::IInspectable const& sender, 
                       winrt::Microsoft::UI::Xaml::Input::KeyRoutedEventArgs r)
    {
        auto k = r.Key();
        if (k == winrt::Windows::System::VirtualKey::Enter)
        {
            if (current_id == DIALOGID_ASKTEXT)
            {
                Microsoft::UI::Xaml::RoutedEventArgs a;
                AskText_ClickOK(sender, a);
            }
            if (current_id == DIALOGID_MESSAGE1)
            {
                Microsoft::UI::Xaml::RoutedEventArgs a;
                AskText_ClickCancel(sender, a);
            }
        }
        if (k == winrt::Windows::System::VirtualKey::Escape)
        {
            XML3::XML x;
            Cancel(x);
        }
    }

Notifying the Caller

The Off() function writes to the shared memory any XML information:

C++
void MainWindow::Off(XML3::XML& x)
    {
        ShowWindow(mw, SW_HIDE);
        auto s = x.Serialize();
        auto rd = u->BeginWrite();
        if (!rd)
            return;
        unsigned long long xlen = s.length();
        memcpy(rd, &xlen, sizeof(xlen));
        memcpy(rd + sizeof(xlen), s.data(), s.length());
        u->EndWrite();
        current_id = DIALOGID_NONE;
        SetEvent(u->hEventAux2);
    }

The Win32 Project

Calling, for example, the AskText dialog:

C++
auto id = DIALOGID_ASKTEXT;
const char* x1 = R"(<?xml?><e title="Ask" t0="This is bold title" 
      t1="Enter value:" t2="100" t3="Placeholder text" 
      tOK="OK" tCancel="Cancel" />)";
unsigned long long wl = strlen(x1);
std::vector<char> what(1000);
memcpy(what.data(), &id, sizeof(id));
memcpy(what.data() + sizeof(id), &wl, sizeof(wl));
memcpy(what.data() + sizeof(id) + sizeof(wl), x1, wl);
ResetEvent(u.hEventAux1);
ResetEvent(u.hEventAux2);
u.WriteData((char*)what.data(), sizeof(id) + sizeof(wl) + wl, 0);

HANDLE h2[2] = { u.hEventAux1,u.hEventAux2 };
for (;;)
{
    auto wi = WaitForMultipleObjects(2, h2, false, 2000);
    if (wi == WAIT_OBJECT_0)
    {
        wmsg(hh);
        continue; // OK, dialog is showing
    }
    if (wi != (WAIT_OBJECT_0 + 1))
    {
        
        SetOffWUI3();
        // Fail here
    }
    break; // dialog ended
}

unsigned long long how = 0;
what.clear();
auto rd = u.BeginRead();
memcpy(&how, rd, sizeof(how));
if (how < 1024 * 1024)
{
    what.resize(how);
    memcpy(what.data(), rd + sizeof(how), how);
}
u.EndRead();
XML3::XML x;
what.resize(what.size() + 1);
x = what.data();

// Parse x to check return values of the dialogs

This is the ugly stuff of the code. You send the XML info with the aid of the USM library and now I have a waiting loop:

C++
HANDLE h2[2] = { u.hEventAux1,u.hEventAux2 };
for (;;)
{
    auto wi = WaitForMultipleObjects(2, h2, false, 2000);
    if (wi == WAIT_OBJECT_0)
    {
        wmsg(hh);
        continue; // OK, dialog is showing
    }
    if (wi != (WAIT_OBJECT_0 + 1))
    {
        
        SetOffWUI3();
        // Fail here
    }
    break; // dialog ended
}

void wmsg(HWND hh)
{
    MSG msg;
    if (GetMessage(&msg, hh, 0, 0))
    {
        if (msg.message == WM_LBUTTONDOWN || msg.message == WM_LBUTTONUP || 
            msg.message == WM_RBUTTONDOWN || msg.message == WM_RBUTTONUP || 
            msg.message == WM_MOUSEMOVE || msg.message == WM_KEYDOWN || 
            msg.message == WM_KEYUP || msg.message == WM_PAINT)
            return;
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

If the first event is triggered, that means that the dialog is still active. But in this case, we won't just wait, but also get a message so it appears that our waiting app is not stuck, but also we don't want to process any mouse/keyboard messages.

If the second event is triggered, the user has closed the dialog. Therefore, we can read the output from the shared memory.

If there's a timeout, the WinUI3 app has crashed/died/whatever. So we can fall back to the Win32 dialog boxes.

Packaging

In x64\release\wui3, there's the wui3.exe, the wui3.winmd, resources.pri, other assets and Microsoft.WindowsAppRuntime.Bootstrap.dll. Package and distribute the entire wui3 directory with your application.

The Code

The GIT repository contains the solution with the two executable projects. Enjoy!

History

  • 26th April, 2023: New article without second executable
  • 24th April, 2023: First release

License

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