Normally, even when using double buffering, when resizing a window, it seems that it's inevitable that the flickering will happen.
Step 1, the original window.
Step 2, the window is resized, but the extra area hasn't been painted.
Step 3, the window is resized, and the extra area has been painted.
Is it possible somehow to hide setp 2? Can I suspend the resizing process until the painting action is done?
Here's an example:
#include <Windows.h>
#include <windowsx.h>
#include <Uxtheme.h>
#pragma comment(lib, "Uxtheme.lib")
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
BOOL MainWindow_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct);
void MainWindow_OnDestroy(HWND hWnd);
void MainWindow_OnSize(HWND hWnd, UINT state, int cx, int cy);
void MainWindow_OnPaint(HWND hWnd);
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
WNDCLASSEX wcex = { 0 };
HWND hWnd;
MSG msg;
BOOL ret;
wcex.cbSize = sizeof(wcex);
wcex.lpfnWndProc = WindowProc;
wcex.hInstance = hInstance;
wcex.hIcon = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
wcex.hCursor = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
wcex.lpszClassName = TEXT("MainWindow");
wcex.hIconSm = wcex.hIcon;
if (!RegisterClassEx(&wcex))
{
return 1;
}
hWnd = CreateWindow(wcex.lpszClassName, TEXT("CWin32"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, HWND_DESKTOP, NULL, hInstance, NULL);
if (!hWnd)
{
return 1;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
{
if (ret == -1)
{
return 1;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hWnd, WM_CREATE, MainWindow_OnCreate);
HANDLE_MSG(hWnd, WM_DESTROY, MainWindow_OnDestroy);
HANDLE_MSG(hWnd, WM_SIZE, MainWindow_OnSize);
HANDLE_MSG(hWnd, WM_PAINT, MainWindow_OnPaint);
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
BOOL MainWindow_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)
{
BufferedPaintInit();
return TRUE;
}
void MainWindow_OnDestroy(HWND hWnd)
{
BufferedPaintUnInit();
PostQuitMessage(0);
}
void MainWindow_OnSize(HWND hWnd, UINT state, int cx, int cy)
{
InvalidateRect(hWnd, NULL, FALSE);
}
void MainWindow_OnPaint(HWND hWnd)
{
PAINTSTRUCT ps;
HPAINTBUFFER hpb;
HDC hdc;
BeginPaint(hWnd, &ps);
hpb = BeginBufferedPaint(ps.hdc, &ps.rcPaint, BPBF_COMPATIBLEBITMAP, NULL, &hdc);
FillRect(hdc, &ps.rcPaint, GetStockBrush(DKGRAY_BRUSH));
Sleep(320); // This simulates some slow drawing actions.
EndBufferedPaint(hpb, TRUE);
EndPaint(hWnd, &ps);
}
Is it possible to eliminate the flickering?
Here is my POC of a non-flickering resizable Direct3D window. It is based on a solution posted here: https://stackoverflow.com/a/65522932/1454851, but with some improvements.
https://github.com/bigfatbrowncat/noflicker_directx_window
This is a Direct3D-specific solution, it contains dependency on DirectComposition (so, Windows 8, at least).
The repo contains a simple app supporting Windows 10 & 11 and DirectX 11 and 12. It paints a standard "colorful triangle" in a window. Please read the README.md for details.
The issue is rather complicated if you start digging into it and contains some non-obvious hacks (including Intel GPU-specific ones).
The overall ideas in the basement of the app:
With this flag window becomes completely transparent, unless...
Use DirectComposition API. Create a context, a Visual, prepare everything. In my app all related stuff is situated in the DCompContetxt class.
Initialize Direct3D device (D3DContext class) and make all the painting in it. Do the resize of the 3D Context properly.
Never use WM_PAINT event. It occurs too late, after the window is already resized. So you will have a frame of the size bigger than the painted image. It will look like a flicker.
Instead, initiate all the size changes inside WM_NCCALCSIZE. This is the best message to recalculate everything if you want to change size before any painting and in sync with it.
Here is the central part of the code - the window initialization and the event loop:
void updateLayout(int width, int height) {
// Uncomment this fake resizing load here to see how the app handles it
// 100ms is a huge time pretty enough to recalculate even a very complicated layout
//
// Sleep(100);
}
// Win32 message handler.
LRESULT window_proc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
std::mutex draw_mutex;
switch (message)
{
case WM_DESTROY: {
// Destroy the DirectComposition context properly,
// so that the window fades away beautifully.
dcompContext->unbind();
}
case WM_CLOSE: {
exitPending = true;
return 0;
}
case WM_SIZING: {
RECT& wr = *(RECT*)lparam;
updateLayout(wr.right - wr.left, wr.bottom - wr.top);
return 0;
}
case WM_NCCALCSIZE: {
// Use the result of DefWindowProc's WM_NCCALCSIZE handler to get the upcoming client rect.
// Technically, when wparam is TRUE, lparam points to NCCALCSIZE_PARAMS, but its first
// member is a RECT with the same meaning as the one lparam points to when wparam is FALSE.
DefWindowProc(hwnd, message, wparam, lparam);
if (RECT *rect = (RECT *) lparam; rect->right > rect->left && rect->bottom > rect->top) {
context->reposition(*rect);
}
// We're never preserving the client area, so we always return 0.
return 0;
}
default:
return DefWindowProc(hwnd, message, wparam, lparam);
}
}
// The app entry point.
int WinMain(HINSTANCE hinstance, HINSTANCE, LPSTR, int)
{
context = std::make_shared<D3DContext>();
// Register the window class.
WNDCLASS wc = {};
wc.lpfnWndProc = window_proc;
wc.hInstance = hinstance;
wc.hCursor = win32_check(LoadCursor(nullptr, IDC_ARROW));
wc.lpszClassName = TEXT("D3DWindow");
win32_check(RegisterClass(&wc));
std::wstring windowTitle = L"A Never Flickering DirectX Window";
// Create the window. We can use WS_EX_NOREDIRECTIONBITMAP
// since all our presentation is happening through DirectComposition.
HWND hwnd = win32_check(CreateWindowEx(
WS_EX_NOREDIRECTIONBITMAP, wc.lpszClassName, windowTitle.c_str(),
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, hinstance, nullptr));
// The DCompContext creation/destruction is fundamentally asymmetric.
// We are cleaning up the resources in WM_DESTROY, but should NOT create the object in WM_CREATE.
// Instead, we create it here between construction of the window and showing it
dcompContext = std::make_shared<DCompContext>(hwnd, context);
// Show the window and enter the message loop.
ShowWindow(hwnd, SW_SHOWNORMAL);
exitPending = false;
while (!exitPending)
{
MSG msg;
win32_check(GetMessage(&msg, nullptr, 0, 0) > 0);
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}