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

使用ScrollWindowEx()实现GUI滚动视图时内容显示异常的问题

问题:自定义垂直滚动视图显示异常

创建了一个宽700、高600的窗口,使用ScrollWindowEx()处理WM_MOUSEWHEEL消息实现垂直滚动,绘制了一个高1000的矩形,但滚动时始终无法看到矩形底部,显示内容异常。

原代码

#include <stdio.h>
#include <windows.h>
#include <gdiplus.h>

int SCREEN_SIZE_WIDTH,
        SCREEN_SIZE_HEIGHT;

static unsigned long long GDI_PLUS_TOKEN;
static GdiplusStartupInput GDI_PLUS_STARTUP_INPUT = {
        .GdiplusVersion = 1,
        .SuppressExternalCodecs = FALSE,
        .SuppressBackgroundThread = FALSE };

static WNDCLASSEXW WINDOW_CLASS_DEFAULT = (WNDCLASSEXW) {
        sizeof(WNDCLASSEXW),
        CS_HREDRAW|CS_VREDRAW|CS_CLASSDC,
        NULL,
        0,
        16,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        L"MY_CLASS",
        NULL };

LRESULT CALLBACK
window_callback(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI
WinMain(HINSTANCE hInstance,
        HINSTANCE hPrevInstance,
        PSTR lpCmdLine,
        int nCmdShow) {
    HWND window;
    if((SCREEN_SIZE_WIDTH=GetSystemMetrics(SM_CXSCREEN))==0
       || (SCREEN_SIZE_HEIGHT=GetSystemMetrics(SM_CYSCREEN))==0) {
        printf("GetSystemMetrics() failed.\r\n");
        ExitProcess(EXIT_FAILURE);
    }
    if(GdiplusStartup(&GDI_PLUS_TOKEN, &GDI_PLUS_STARTUP_INPUT, NULL)!=Ok) {
        printf("GdiplusStartup() failed.\r\n");
        ExitProcess(EXIT_FAILURE);
    }
    WINDOW_CLASS_DEFAULT.hInstance=hInstance;
    WINDOW_CLASS_DEFAULT.hbrBackground= CreateSolidBrush(RGB(255,255,255));
    WINDOW_CLASS_DEFAULT.hCursor=LoadCursor(NULL, IDC_ARROW);
    WINDOW_CLASS_DEFAULT.lpfnWndProc=window_callback;
    if(!RegisterClassExW(&WINDOW_CLASS_DEFAULT)) {
        printf("RegisterClassExW() failed.\r\n");
        ExitProcess(EXIT_FAILURE);
    }
    window=CreateWindowExW(
            0, L"MY_CLASS",
            L"MY PROJECT",
            WS_POPUP|WS_OVERLAPPEDWINDOW,
            ((SCREEN_SIZE_WIDTH - 700) / 2), // Center of the screen
            ((SCREEN_SIZE_HEIGHT - 400) / 2), // Center of the screen
            700, 600, NULL,
            NULL, hInstance, NULL);
    if(window==NULL) {
        printf("CreateWindowExA() failed.\r\n");
        goto failure;
    }
    ShowWindow(window, SW_NORMAL);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    UnregisterClassA("DRAWPEN", hInstance);
    ExitProcess(EXIT_SUCCESS);
    failure:
    UnregisterClassA("DRAWPEN", hInstance);
    ExitProcess(EXIT_FAILURE);
}

LRESULT CALLBACK
window_callback(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    HDC hdc;
    PAINTSTRUCT ps;
    static int scroll_value = 0;
    switch (uMsg) {
        case WM_MOUSEWHEEL:
        {
            // On scroll wheel, let's use ScrollWindowEx() function
            // to scroll vertical only and here is my problem !
            ScrollWindowEx(
                    hwnd, 0, GET_WHEEL_DELTA_WPARAM(wParam),
                    NULL, NULL, NULL,
                    NULL, SW_INVALIDATE);
            UpdateWindow(hwnd);
        }
            break;
        case WM_PAINT:
            printf("WM_PAINT\r\n");
            hdc=BeginPaint(hwnd, &ps);
            GpGraphics *gfx;
            GdipCreateFromHDC(hdc, &gfx);
            GpPen *pen;
            GdipCreatePen1(0xFF4c4c4c, 15, UnitPixel, &pen);
            GdipDrawRectangle(gfx, pen, 20, 20, 100, 1000);
            EndPaint(hwnd, &ps);
            break;
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProcW(hwnd, uMsg, wParam, lParam);
    }
    return 0;
}

问题截图

  • 程序启动时
    程序启动时的截图

  • 向下滚动时(无法看到矩形底部)
    向下滚动时的截图(始终无法看到矩形底部)

  • 向上滚动时
    向上滚动时的截图


问题分析与修复

核心问题点

  1. 未维护滚动偏移量:代码中定义了scroll_value但未使用,绘制时没有根据滚动位置偏移内容坐标,导致滚动逻辑和绘制脱节。
  2. ScrollWindowEx参数错误:直接使用鼠标滚轮的原始delta值(±120)作为滚动步长,步长过大且方向不符合用户预期;同时未限制滚动范围,无法滚动到内容底部。
  3. 资源泄漏:GDI+创建的GpGraphics和GpPen未释放,会导致资源占用。
  4. 类名不匹配:UnregisterClassA使用了错误的类名DRAWPEN,但实际注册的是MY_CLASS。

修复后的代码

#include <stdio.h>
#include <windows.h>
#include <gdiplus.h>

int SCREEN_SIZE_WIDTH,
        SCREEN_SIZE_HEIGHT;

static unsigned long long GDI_PLUS_TOKEN;
static GdiplusStartupInput GDI_PLUS_STARTUP_INPUT = {
        .GdiplusVersion = 1,
        .SuppressExternalCodecs = FALSE,
        .SuppressBackgroundThread = FALSE };

static WNDCLASSEXW WINDOW_CLASS_DEFAULT = (WNDCLASSEXW) {
        sizeof(WNDCLASSEXW),
        CS_HREDRAW|CS_VREDRAW|CS_CLASSDC,
        NULL,
        0,
        16,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        L"MY_CLASS",
        NULL };

LRESULT CALLBACK
window_callback(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI
WinMain(HINSTANCE hInstance,
        HINSTANCE hPrevInstance,
        PSTR lpCmdLine,
        int nCmdShow) {
    HWND window;
    if((SCREEN_SIZE_WIDTH=GetSystemMetrics(SM_CXSCREEN))==0
       || (SCREEN_SIZE_HEIGHT=GetSystemMetrics(SM_CYSCREEN))==0) {
        printf("GetSystemMetrics() failed.\r\n");
        ExitProcess(EXIT_FAILURE);
    }
    if(GdiplusStartup(&GDI_PLUS_TOKEN, &GDI_PLUS_STARTUP_INPUT, NULL)!=Ok) {
        printf("GdiplusStartup() failed.\r\n");
        ExitProcess(EXIT_FAILURE);
    }
    WINDOW_CLASS_DEFAULT.hInstance=hInstance;
    WINDOW_CLASS_DEFAULT.hbrBackground= CreateSolidBrush(RGB(255,255,255));
    WINDOW_CLASS_DEFAULT.hCursor=LoadCursor(NULL, IDC_ARROW);
    WINDOW_CLASS_DEFAULT.lpfnWndProc=window_callback;
    if(!RegisterClassExW(&WINDOW_CLASS_DEFAULT)) {
        printf("RegisterClassExW() failed.\r\n");
        ExitProcess(EXIT_FAILURE);
    }
    window=CreateWindowExW(
            0, L"MY_CLASS",
            L"MY PROJECT",
            WS_POPUP|WS_OVERLAPPEDWINDOW,
            ((SCREEN_SIZE_WIDTH - 700) / 2), // Center of the screen
            ((SCREEN_SIZE_HEIGHT - 600) / 2), // 修正窗口垂直居中计算
            700, 600, NULL,
            NULL, hInstance, NULL);
    if(window==NULL) {
        printf("CreateWindowExW() failed.\r\n"); // 修正函数名打印错误
        goto failure;
    }
    ShowWindow(window, SW_NORMAL);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    GdiplusShutdown(GDI_PLUS_TOKEN); // 关闭GDI+
    UnregisterClassW(L"MY_CLASS", hInstance); // 修正类名和宽字符版本
    ExitProcess(EXIT_SUCCESS);
    failure:
    GdiplusShutdown(GDI_PLUS_TOKEN); // 关闭GDI+
    UnregisterClassW(L"MY_CLASS", hInstance); // 修正类名和宽字符版本
    ExitProcess(EXIT_FAILURE);
}

LRESULT CALLBACK
window_callback(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    HDC hdc;
    PAINTSTRUCT ps;
    static int scroll_value = 0;
    const int CONTENT_HEIGHT = 1000; // 内容高度常量
    switch (uMsg) {
        case WM_MOUSEWHEEL:
        {
            // 转换滚轮delta为合适的滚动步长,反转方向符合用户直觉
            int delta = GET_WHEEL_DELTA_WPARAM(wParam);
            int scroll_step = delta > 0 ? -30 : 30;

            // 获取客户区尺寸,计算最大滚动范围
            RECT client_rect;
            GetClientRect(hwnd, &client_rect);
            int client_height = client_rect.bottom - client_rect.top;
            int max_scroll = CONTENT_HEIGHT - client_height;
            if(max_scroll < 0) max_scroll = 0;

            // 计算新的滚动位置并限制范围
            int new_scroll = scroll_value + scroll_step;
            if(new_scroll < 0) new_scroll = 0;
            if(new_scroll > max_scroll) new_scroll = max_scroll;

            // 仅当滚动位置变化时执行滚动操作
            int scroll_delta = new_scroll - scroll_value;
            if(scroll_delta != 0) {
                // ScrollWindowEx的dy参数是内容移动的偏移:负数值表示内容向上移(用户向下滚)
                ScrollWindowEx(hwnd, 0, -scroll_delta, NULL, NULL, NULL, NULL, SW_INVALIDATE);
                scroll_value = new_scroll;
                UpdateWindow(hwnd);
            }
        }
            break;
        case WM_PAINT:
            printf("WM_PAINT\r\n");
            hdc=BeginPaint(hwnd, &ps);
            GpGraphics *gfx;
            GdipCreateFromHDC(hdc, &gfx);
            GpPen *pen;
            GdipCreatePen1(0xFF4c4c4c, 15, UnitPixel, &pen);
            // 根据滚动偏移绘制内容
            GdipDrawRectangle(gfx, pen, 20, 20 - scroll_value, 100, CONTENT_HEIGHT);
            
            // 释放GDI+资源
            GdipDeletePen(pen);
            GdipDeleteGraphics(gfx);
            EndPaint(hwnd, &ps);
            break;
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProcW(hwnd, uMsg, wParam, lParam);
    }
    return 0;
}

关键修改说明

  1. 滚动逻辑修正:

    • 将滚轮delta转换为30像素的步长,方向反转以符合用户直觉(向下滚时内容向上移动)。
    • 计算客户区高度,限制滚动范围为内容高度 - 客户区高度,确保能滚动到内容底部。
    • 使用scroll_value维护当前滚动位置,仅当位置变化时执行滚动操作。
  2. 绘制逻辑修正:

    • 绘制矩形时,y坐标减去scroll_value,实现内容随滚动偏移。
  3. 资源与细节修复:

    • 释放GDI+创建的GpPen和GpGraphics,避免资源泄漏。
    • 修正窗口垂直居中的计算错误(原代码用400代替窗口高度600)。
    • 修正UnregisterClass的类名和宽字符版本,匹配注册的类名。
    • 新增GdiplusShutdown关闭GDI+,释放相关资源。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 01:10:07