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.
And here is a small demo of this project. A Win32 dialog calls a WinUI
dialog:
WUI3 Project
- Create a new project in Visual Studio "Blank App, Packaged, WinUI 3" in Desktop.
- Close the project and open the .vcxproj with a text editor.
- Change
<AppxPackage>true</AppxPackage>
to <AppxPackage>false</AppxPackage>
. - Add after the first
<PropertyGroup Label="Globals">
the element <WindowsPackageType>None</WindowsPackageType>
. - Reopen the project with Visual Studio.
- 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
-
Create a new standard Win32 project.
-
Set the standard to C++ 17.
-
Optionally make it static (without the DLL runtime) and add the:
#pragma comment(linker,"\"/manifestdependency:type='win32'
\ name='Microsoft.Windows.Common-Controls' version='6.0.0.0'
\ processorArchitecture='*' publicKeyToken='6595b64144ccf1df'
language='*'\"")
for the styles.
-
Use Nuget to install WindowsAppSDK
and Microsoft.Windows.CppWinRT
.
-
Add a RC file which will contain the xbf file as a resource:
L1 DATA "..\\wui3\\x64\\debug\\embed\\MainWindow.xbf"
- Call the boostrapper indirectly:
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.
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:
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:
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:
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:
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
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:
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 HWND
s 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:
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:
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).
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:
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.
<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
RequestedTheme="Dark" KeyDown="KeyD2">
<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>
<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 StackPanel
s 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!):
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
:
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:
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
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
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
:
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);
}
}
}
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:
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:
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; }
if (wi != (WAIT_OBJECT_0 + 1))
{
SetOffWUI3();
}
break; }
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();
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:
HANDLE h2[2] = { u.hEventAux1,u.hEventAux2 };
for (;;)
{
auto wi = WaitForMultipleObjects(2, h2, false, 2000);
if (wi == WAIT_OBJECT_0)
{
wmsg(hh);
continue; }
if (wi != (WAIT_OBJECT_0 + 1))
{
SetOffWUI3();
}
break; }
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