初學 Windows GDI 繪圖,用 for 循環改變圓的座標實現動畫,但畫面要么閃爍嚴重,要么卡死不動,完全不知道如何控制幀率。
Sleep 會阻塞消息泵:窗口無法處理 WM_PAINT、WM_TIMER、移動/縮放等消息,導致「未響應」WM_PAINT 繪圖必然閃爍WM_PAINT 內:繪製與狀態更新應分離SetTimer 替代循環// 創建窗口時設置定時器,約 60 FPS (16ms)
SetTimer(hwnd, TIMER_ANIMATION, 16, NULL);
// WM_TIMER 處理
case WM_TIMER:
if (wParam == TIMER_ANIMATION) {
UpdateCirclePosition(); // 更新邏輯座標
InvalidateRect(hwnd, NULL, FALSE); // 請求重繪
}
break;
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// 1. 創建兼容 DC 與位圖
HDC memDC = CreateCompatibleDC(hdc);
RECT rect; GetClientRect(hwnd, &rect);
HBITMAP hBmp = CreateCompatibleBitmap(hdc, rect.right, rect.bottom);
HBITMAP hOldBmp = SelectObject(memDC, hBmp);
// 2. 在記憶體 DC 繪製(背景清除 + 圓)
FillRect(memDC, &rect, (HBRUSH)GetStockObject(WHITE_BRUSH));
Ellipse(memDC, circle.x - circle.r, circle.y - circle.r,
circle.x + circle.r, circle.y + circle.r);
// 3. 一次性 BitBlt 到屏幕 DC
BitBlt(hdc, 0, 0, rect.right, rect.bottom, memDC, 0, 0, SRCCOPY);
// 4. 清理
SelectObject(memDC, hOldBmp);
DeleteObject(hBmp);
DeleteDC(memDC);
EndPaint(hwnd, &ps);
return 0;
}
// 僅供教學理解消息機制,實際專案請用 SetTimer
for (int frame = 0; frame < totalFrames; frame++) {
UpdateCirclePosition();
InvalidateRect(hwnd, NULL, FALSE);
// 關鍵:處理待處理消息,保持窗口響應
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Sleep(16); // 粗略控制幀率
}
關鍵點:PeekMessage 非阻塞檢查消息隊列,PM_REMOVE 取出並移除,DispatchMessage 分發給窗口過程,這樣 WM_PAINT 才能被及時處理。
// 使用 QueryPerformanceCounter 實現精準幀率
LARGE_INTEGER freq, lastTime, currentTime;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&lastTime);
const double targetFrameTime = 1.0 / 60.0; // 60 FPS
while (running) {
QueryPerformanceCounter(¤tTime);
double elapsed = (double)(currentTime.QuadPart - lastTime.QuadPart) / freq.QuadPart;
if (elapsed >= targetFrameTime) {
UpdateCirclePosition();
InvalidateRect(hwnd, NULL, FALSE);
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
lastTime = currentTime;
} else {
// 精確睡眠剩餘時間(需考慮 Sleep 精度 ~1ms)
DWORD sleepMs = (DWORD)((targetFrameTime - elapsed) * 1000);
if (sleepMs > 1) Sleep(sleepMs);
}
}
| 技術 | 優勢 | 適用場景 |
|---|---|---|
| Direct2D | 硬體加速、原生雙緩衝、高效能 | Win32 原生高效能繪圖 |
| WPF / WinUI 3 | 保留模式、聲明式動畫、GPU 加速 | .NET 桌面新專案 |
| SDL2 / SFML | 跨平台、遊戲級循環、音頻輸入整合 | 遊戲/多媒體應用 |
| Skia / Cairo | 跨平台 2D 繪圖引擎 | 自定義渲染管線 |
Windows 繪圖動畫的正確姿勢:定時器驅動邏輯更新 → InvalidateRect 請求重繪 → WM_PAINT 雙緩衝繪製。初學階段理解 PeekMessage + Sleep 消息泵機制有助於掌握 Windows 事件驅動本質,但實際專案請拥抱 SetTimer 或現代圖形 API。記住:繪製與邏輯分離,消息泵不可阻塞,雙緩衝消除閃爍,三條鐵律走遍天下。