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

WinAPI:HBITMAP屏幕显示异常求助——无法实现预期闪烁效果

问题描述

编写了一个以60FPS频率调用的渲染函数,目标是切换显示两张HBITMAP图片,实现每秒各渲染30次的交替闪烁效果。但实际运行时,一张图片会长时间显示,另一张无法正常渲染,一段时间后才切换到另一张图片长时间显示,达不到预期的闪烁效果。注:已创建两个HDC,为未来支持多图显示做准备。

相关代码

渲染函数

void Game::render()
{
    static HBITMAP hBitMap1 = (HBITMAP)LoadImage(NULL, L"..\\images\\player01.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
    static HBITMAP hBitMap2 = (HBITMAP)LoadImage(NULL, L"..\\images\\player02.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);

    HDC memDC = CreateCompatibleDC(dc);
    HDC memDC1 = CreateCompatibleDC(dc);
    HBITMAP memBM = CreateCompatibleBitmap(dc, windowRect.right, windowRect.bottom);

    SelectObject(memDC, memBM);

    static int image_changing = 0;
    int x, y;

    HBITMAP hBitMap;
    if (image_changing < 1) {
        hBitMap = hBitMap1;
        x = 200;
        y = 200;
        image_changing++;
    } else {
        hBitMap = hBitMap2;
        x = 200;
        y = 50;
        image_changing = 0;
    }

    HBITMAP hbit = (HBITMAP)SelectObject(memDC1, hBitMap);
    BitBlt(memDC, x, y, IMAGE_SIZE_X, IMAGE_SIZE_Y, memDC1, 0, 0, SRCCOPY);
    SelectObject(memDC1, hbit);

    BitBlt(dc, 0, 0, windowRect.right, windowRect.bottom, memDC, 0, 0, SRCCOPY);

    DeleteDC(memDC1);
    DeleteDC(memDC);
    DeleteObject(memBM);
}

渲染循环

void Game::run()
{
    // Initialize previous time to current time
    DWORD prevTime = timeGetTime();

    // Initialize frame counter and FPS variables
    int frameCount = 0;

    // Initialize timer variables
    DWORD timer = timeGetTime();
    int elapsedTime = 0;

    MSG msg = { };
    while (true)
    {

        if (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE))
        {
            // Exit
            if (msg.message == WM_QUIT)
                break;
            
            // Add Bullets
            if (msg.message == WM_LBUTTONDOWN)
                if (player->getPlayerCanFire()) {
                    addBullet(LOWORD(msg.lParam), HIWORD(msg.lParam));
                    player->setPlayerCanFire(FALSE);
                    player->setPlayerFireElapsedTime(0);
                }
            
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else
        {
            // Get current time
            DWORD currentTime = timeGetTime();

            // Calculate difference between current time and previous time
            DWORD deltaTime = currentTime - prevTime;

            // If difference is less than desired time interval, sleep for the difference
            if (deltaTime < FRAME_INTERVAL) {
                Sleep(FRAME_INTERVAL - deltaTime);
            }

            prevTime = currentTime;

            // convert to seconds
            update(deltaTime / 1000.0);

            render();

            // Increment frame counter
            frameCount++;

            // Check if timer interval has elapsed
            elapsedTime = currentTime - timer;
            if (elapsedTime >= 1000) {
                // Calculate FPS
                fps = frameCount * 1000 / elapsedTime;

                // Reset timer and frame counter
                timer = currentTime;
                frameCount = 0;
            }
        }
    }
}
问题原因分析
  1. 视觉暂留效应:当前逻辑每帧切换一次图片(60FPS下每秒切换60次),每张图仅显示约16ms,远低于人类视觉暂留时间(约50ms),导致眼睛无法分辨单独的帧,看起来像是两张图叠加而非闪烁。
  2. GDI资源频繁创建销毁:每次render都创建/销毁memDC、memDC1和memBM,会导致性能开销和资源波动,可能引发帧率不稳定,进一步影响视觉效果。
  3. 渲染无清屏操作:渲染前未清空内存DC,前一帧的图像会残留,导致两张图叠加,模糊切换效果。
  4. 时间计算逻辑错误:prevTime在Sleep前更新,未考虑Sleep实际占用的时间,导致deltaTime计算不准,帧率控制失效,可能出现连续多帧显示同一张图的情况。
解决方案

1. 调整图片切换频率

根据视觉感知需求,修改切换逻辑,设置固定时间间隔让眼睛能分辨切换动作,比如每100ms切换一次:

static DWORD lastSwitchTime = timeGetTime();
const DWORD SWITCH_INTERVAL = 100; // 每100ms切换一次,每秒各5次
DWORD currentTime = timeGetTime();

static bool image_changing = false;
HBITMAP hBitMap;
int x, y;

if (currentTime - lastSwitchTime >= SWITCH_INTERVAL) {
    image_changing = !image_changing;
    lastSwitchTime = currentTime;
}

if (image_changing) {
    hBitMap = hBitMap1;
    x = 200;
    y = 200;
} else {
    hBitMap = hBitMap2;
    x = 200;
    y = 50;
}

2. 复用GDI资源

将memDC、memDC1、memBM改为成员变量,初始化一次避免频繁创建:

// 在Game类中声明成员变量
HDC m_memDC = nullptr;
HDC m_memDC1 = nullptr;
HBITMAP m_memBM = nullptr;

// 初始化(比如在构造函数或init方法中)
void Game::init() {
    m_memDC = CreateCompatibleDC(dc);
    m_memDC1 = CreateCompatibleDC(dc);
    m_memBM = CreateCompatibleBitmap(dc, windowRect.right, windowRect.bottom);
    SelectObject(m_memDC, m_memBM);
}

// 渲染函数中复用资源
void Game::render() {
    // ... 省略资源创建代码

    HBITMAP hbit = (HBITMAP)SelectObject(m_memDC1, hBitMap);
    BitBlt(m_memDC, x, y, IMAGE_SIZE_X, IMAGE_SIZE_Y, m_memDC1, 0, 0, SRCCOPY);
    SelectObject(m_memDC1, hbit);

    BitBlt(dc, 0, 0, windowRect.right, windowRect.bottom, m_memDC, 0, 0, SRCCOPY);
}

// 析构函数中释放资源
Game::~Game() {
    DeleteDC(m_memDC1);
    DeleteDC(m_memDC);
    DeleteObject(m_memBM);
}

3. 添加清屏操作

在渲染前清空内存DC,避免图像残留:

SelectObject(memDC, memBM);
// 用白色清空内存DC(可替换为窗口背景色)
PatBlt(memDC, 0, 0, windowRect.right, windowRect.bottom, WHITENESS);

4. 修正帧率控制逻辑

调整prevTime的更新时机,确保deltaTime计算准确:

// 替换原时间计算部分
DWORD currentTime = timeGetTime();
DWORD deltaTime = currentTime - prevTime;

if (deltaTime < FRAME_INTERVAL) {
    Sleep(FRAME_INTERVAL - deltaTime);
    // Sleep后重新获取当前时间
    currentTime = timeGetTime();
}

deltaTime = currentTime - prevTime;
prevTime = currentTime;

update(deltaTime / 1000.0);
render();

内容的提问来源于stack exchange,提问作者Alex

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 14:34:56