You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Win32 API:循环绘制自定义窗口框架致贴靠布局菜单闪烁

自定义窗口贴靠布局菜单闪烁问题的原因及解决方法

问题描述

我正在开发一款需要绘制完整窗口(含自定义最小化/最大化/关闭按钮框架)的应用,已通过响应WM_NCHITTEST消息实现自定义标题栏交互,但发现Windows贴靠布局菜单出现闪烁问题:菜单会先在屏幕左上角短暂闪现,再移动到鼠标位置。为实现流畅动画,应用主循环每秒绘制60次,绘制逻辑放在主循环中。

(注:贴靠布局菜单指Windows系统中,点击最大化按钮或拖拽窗口到屏幕边缘时弹出的分屏布局选择菜单)

更新:即使不使用OpenGL,仅用Win32 API循环绘制也会出现相同的闪烁问题。

原因分析

闪烁的核心原因是窗口非客户区(NC Area)状态与系统预期不匹配:

  • 你的窗口使用WS_POPUP样式且未启用WS_CAPTION,但通过WM_NCHITTEST模拟了最大化按钮交互。系统触发贴靠菜单时,会默认基于标准窗口的非客户区位置计算菜单显示坐标,由于你的窗口没有标准标题栏,导致系统初始计算出错误坐标(屏幕左上角),之后才修正到正确位置,从而产生闪烁。
  • 主循环中频繁调用InvalidateRect触发全屏重绘,干扰了系统对贴靠菜单的绘制时序,加重了闪烁现象。

解决方法

  1. 修正窗口样式,保留兼容的非客户区结构
    • 启用WS_CAPTION样式,同时通过WM_NCCALCSIZE将标准标题栏高度设为0,既满足系统对窗口结构的预期,又不显示默认标题栏。
  2. 优化重绘逻辑,避免不必要的全屏重绘
    • 仅在内容真正需要更新时调用InvalidateRect,或使用UpdateWindow替代减少无效绘制;同时用高精度计时控制60FPS,避免过度重绘。
  3. 正确处理DWM相关消息
    • 确保WM_NCCALCSIZE的处理符合DWM要求,让系统正确识别窗口有效区域。

修改后的最小示例代码

#include "Windows.h"
#include "Windowsx.h"
#include "dwmapi.h"
#include "math.h"

#pragma comment(lib, "dwmapi.lib")

struct AppState {
  HINSTANCE hInstance;
  HWND hwnd;
  float time;
};
static AppState appState = {};

void fill(HDC hdc, RECT r, float color) {
  int c = (int)(color * 255.0f + 0.5f);
  c = max(0, min(255, c));
  HBRUSH br = CreateSolidBrush(RGB(c, c, c));
  FillRect(hdc, &r, br);
  DeleteObject(br);
}

void render() {
  PAINTSTRUCT ps;
  HDC hdc = BeginPaint(appState.hwnd, &ps);
  RECT r = {0, 0, 600, 600};
  fill(hdc, r, 0.5f + 0.2f * sinf(appState.time * 0.01f));

  RECT r2 = {450, 450, 600, 600};
  fill(hdc, r2, 0.0f);

  EndPaint(appState.hwnd, &ps);
}

LRESULT __stdcall Wndproc(
  HWND hwnd,
  UINT uMsg,
  WPARAM wParam,
  LPARAM lParam
) {
  switch (uMsg) {
    case WM_NCHITTEST: {
      POINT p = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
      ScreenToClient(hwnd, &p);
      // 测试用:右下角区域作为最大化按钮
      if (p.x >= 450 && p.x < 600 && p.y >= 450 && p.y < 600) {
        return HTMAXBUTTON;
      }
      // 其他区域返回HTCLIENT,避免系统误判非客户区
      return HTCLIENT;
    }
    case WM_DESTROY:
    case WM_CLOSE: {
      PostQuitMessage(0);
      return 0;
    }
    case WM_NCACTIVATE: {
      return 1;
    }
    case WM_NCCALCSIZE: {
      if (wParam) {
        NCCALCSIZE_PARAMS* params = reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam);
        // 将客户区扩展到整个窗口,隐藏标准标题栏
        params->rgrc[0] = params->rgrc[1];
        return WVR_REDRAW;
      }
      return 0;
    }
    case WM_ERASEBKGND: {
      return 1; // 禁止系统擦除背景,避免闪烁
    }
    case WM_SYNCPAINT:
    case WM_NCPAINT: {
      return 0;
    }
    case WM_PAINT: {
      render();
      return 0;
    }
  }
  LRESULT dwmRes;
  if (DwmDefWindowProc(hwnd, uMsg, wParam, lParam, &dwmRes)) {
    return dwmRes;
  }

  return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}

static bool createWindow(int nCmdShow) {
  const wchar_t CLASS_NAME[] = L"TestClass";
  const wchar_t TITLE[] = L"Title";
  SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

  WNDCLASSEXW wc = {};
  wc.cbSize = sizeof(WNDCLASSEXW);
  wc.style = CS_OWNDC;
  wc.lpfnWndProc = Wndproc;
  wc.hInstance = appState.hInstance;
  wc.hIcon = LoadIconW(NULL, (LPCWSTR)IDI_APPLICATION);
  wc.lpszClassName = CLASS_NAME;
  if (RegisterClassExW(&wc) == 0) {
    return false;
  }

  // 启用WS_CAPTION,同时通过WM_NCCALCSIZE隐藏标题栏
  DWORD style = WS_POPUP | WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_CAPTION;
  appState.hwnd = CreateWindowExW(
    WS_EX_APPWINDOW, CLASS_NAME, TITLE, style,
    500, 500, 600, 600,
    0, 0, appState.hInstance, 0
  );
  if (appState.hwnd == 0) {
    return false;
  }

  ShowWindow(appState.hwnd, nCmdShow);
  return true;
}

bool processMessages() {
  MSG message;
  while (PeekMessageW(&message, 0, 0, 0, PM_REMOVE)) {
    if (message.message == WM_QUIT) {
      return true;
    }
    TranslateMessage(&message);
    DispatchMessageW(&message);
  }
  return false;
}

int __stdcall wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, wchar_t* pCmdLine, int nCmdShow) {
  appState.hInstance = hInstance;

  if (!createWindow(nCmdShow)) {
    return 1;
  }

  LARGE_INTEGER freq, prev, curr;
  QueryPerformanceFrequency(&freq);
  QueryPerformanceCounter(&prev);

  for (;;) {
    if (processMessages()) {
      break;
    }

    QueryPerformanceCounter(&curr);
    double delta = (curr.QuadPart - prev.QuadPart) * 1000.0 / freq.QuadPart;
    if (delta >= 16.666) { // 控制60FPS
      appState.time += delta;
      InvalidateRect(appState.hwnd, NULL, FALSE);
      prev = curr;
    }
  }

  return 0;
}

内容的提问来源于Stack Exchange,提问作者Somnium

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 03:47:17