Hello @Omar Mohamed ,
In Win32 there is an important distinction between a true child window and a separate top-level window. A real child window created with WS_CHILD lives inside the parent window’s client area and behaves more like a control. That is usually not the same thing as the WinUI 3-style experience you described.
So, the Win32 concept that matches it is usually not a traditional child window. In practice, the workaround is to create a modeless secondary top-level window and let the main window manage its lifetime. That gives you the behavior of a separate window while still allowing the main window to stay interactive.
To illustrate this approach, I put together a small Win32 sample that opens a secondary window from the main window like this:
void CreateOwnedModelessWindow(HWND hwndOwner)
{
if (g_ownedWindow && IsWindow(g_ownedWindow))
{
ShowWindow(g_ownedWindow, SW_SHOWNORMAL);
SetForegroundWindow(g_ownedWindow);
DebugLog(L"[Main] Owned window already exists");
return;
}
g_ownedWindow = CreateWindowExW(
0,
CHILD_CLASS_NAME,
L"Owned Modeless Secondary Window",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
450, 150, 500, 260,
hwndOwner,
nullptr,
GetModuleHandleW(nullptr),
nullptr
);
if (!g_ownedWindow)
{
DebugLog(L"[Main] Failed to create owned window");
MessageBoxW(hwndOwner, L"Failed to create owned window.", L"Error", MB_ICONERROR);
return;
}
DebugLog(L"[Main] Owned modeless window created");
}
With this approach, the main window remains interactive. In the sample, that can be verified because the main window continues to process commands while the secondary window is open:
case WM_COMMAND:
{
if (LOWORD(wParam) == ID_BTN_OPEN)
{
DebugLog(L"[Main] Open button clicked");
CreateOwnedModelessWindow(hwnd);
return 0;
}
if (LOWORD(wParam) == ID_BTN_PING_MAIN)
{
DebugLog(L"[Main] Ping Main clicked");
MessageBoxW(hwnd, L"Main window is still interactive.", L"Main", MB_OK);
return 0;
}
if (LOWORD(wParam) == ID_BTN_STOP_LOOP)
{
DebugLog(L"[Main] Stop Secondary Loop clicked");
if (g_loopRunning)
{
RequestStopWorker();
MessageBoxW(hwnd, L"Stop requested for secondary loop.", L"Main", MB_OK);
}
else
{
MessageBoxW(hwnd, L"Secondary loop is not running.", L"Main", MB_OK);
}
return 0;
}
break;
}
If you also want the secondary window to keep doing work while the main window is still usable, the work should not run on the UI thread. In the sample, the secondary window starts a worker thread instead:
DWORD WINAPI SecondaryWorkerThread(LPVOID)
{
DebugLog(L"[Worker] Thread started");
g_loopRunning = true;
g_stopRequested = false;
int counter = 0;
while (!g_stopRequested)
{
wchar_t buffer[128];
wsprintfW(buffer, L"[Worker] Tick %d", counter++);
DebugLog(buffer);
Sleep(500);
}
DebugLog(L"[Worker] Thread stopping");
g_loopRunning = false;
g_workerThread = nullptr;
return 0;
}
The secondary window can start that work without blocking either window:
case WM_COMMAND:
{
if (LOWORD(wParam) == ID_BTN_CLOSE_OWNED)
{
DebugLog(L"[Owned] Close button clicked");
DestroyWindow(hwnd);
return 0;
}
if (LOWORD(wParam) == ID_BTN_START_LOOP)
{
if (!g_loopRunning && g_workerThread == nullptr)
{
DebugLog(L"[Owned] Start Loop clicked");
g_workerThread = CreateThread(
nullptr,
0,
SecondaryWorkerThread,
nullptr,
0,
nullptr
);
if (!g_workerThread)
{
DebugLog(L"[Owned] Failed to create worker thread");
MessageBoxW(hwnd, L"Failed to start worker thread.", L"Error", MB_ICONERROR);
}
}
else
{
DebugLog(L"[Owned] Loop already running");
MessageBoxW(hwnd, L"Loop is already running.", L"Owned", MB_OK);
}
return 0;
}
break;
}
The main window can also control that work:
void RequestStopWorker()
{
if (g_loopRunning)
{
DebugLog(L"[App] Stop requested");
g_stopRequested = true;
}
}
To keep the lifetime relationship consistent, the sample requests the worker to stop when the main window is destroyed:
case WM_DESTROY:
DebugLog(L"[Main] WM_DESTROY");
RequestStopWorker();
PostQuitMessage(0);
return 0;
And the secondary window can also do the same when it is destroyed:
case WM_DESTROY:
DebugLog(L"[Owned] WM_DESTROY");
RequestStopWorker();
if (g_ownedWindow == hwnd)
g_ownedWindow = nullptr;
return 0;
Also, I attached a few screenshots from my testing in case that helps illustrate more clearly
The main window and the secondary window are both open and interactive at the same time.
The secondary window shows that its loop is running after clicked, confirming it can manage its own background activity.
The main window successfully sends a stop request to the secondary window’s loop while both windows remain open.
Please have a look and see whether this approach fits your framework design. If the information above is useful, you can follow this guide to give feedback.
Thank you.