目前共有10篇帖子。 字体大小:较小 - 100% (默认)▼  内容转换:不转换▼
 
点击 回复
557 9
【代码范例】Win32程序中使用滚动条在窗口中显示大尺寸位图,并可用鼠标滚轮滚动
一派护法 十九级
1楼 发表于:2016-2-15 23:03

【运行效果】

一派护法 十九级
2楼 发表于:2016-2-15 23:04
【C++代码】
#include <tchar.h>
#include <Windows.h>
#include "resource.h"

#define SCROLL_LINE 20 // 点击滚动条的箭头时, 位图滚动多少像素
//#define SCROLL_MAX(si) (si.nMax - (int)si.nPage + 1) // 获取滑块左端最大位置的宏

HBITMAP hbmp;
BITMAP bmp;

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HDC hdc, hdcMem;
    HINSTANCE hInstance = (HINSTANCE)GetWindowLongPtr(hWnd, GWLP_HINSTANCE);
    int nLines, nNewPos, nOldPos, nOldHorzPos, nOldVertPos;
    PAINTSTRUCT ps;
    RECT rect, rcClient;
    SCROLLBARINFO sbi;
    SCROLLINFO si;
    ULONG ulScrollLines;
   
    switch (uMsg)
    {
    case WM_CREATE:
        hbmp = LoadBitmap(hInstance, MAKEINTRESOURCE(IDB_BITMAP1)); // 加载位图资源
        GetObject(hbmp, sizeof(bmp), &bmp); // 获取位图尺寸

        // 为了方便测试, 可以手动将bmWidth和bmHeight改小, 模拟显示小图片
        //bmp.bmWidth = 800;
        //bmp.bmHeight = 600;

        // 设置滚动条的滚动范围
        // 只要位图的大小不变, 那么滚动范围就不会发生变化
        // 由于滚动位置的最小值为0, 而bmWidth和bmHeight的最小值却为1, 因此在指定滚动位置的最大值时必须减1
        // 比如一张10x10的图片显示到窗口中,图片最右下角点的显示坐标为(9, 9)
        // 因此此时滚动条滑块最右端只能滚动到9,不能滚动到10
        SetScrollRange(hWnd, SB_HORZ, 0, bmp.bmWidth - 1, FALSE);
        SetScrollRange(hWnd, SB_VERT, 0, bmp.bmHeight - 1, FALSE);
        break;
    case WM_DESTROY:
        DeleteObject(hbmp);
        PostQuitMessage(0);
        break;

    // 水平滚动条消息的处理
    case WM_HSCROLL:
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_PAGE | SIF_POS | SIF_RANGE;
        GetScrollInfo(hWnd, SB_HORZ, &si);
        // 下面的这些值无需作任何判断处理, 系统会自动纠正无效的值
        switch (LOWORD(wParam))
        {
        case SB_LEFT:
            nNewPos = si.nMin;
            break;
        case SB_LINELEFT:
            nNewPos = si.nPos - SCROLL_LINE;
            break;
        case SB_LINERIGHT:
            nNewPos = si.nPos + SCROLL_LINE;
            break;
        case SB_PAGELEFT:
            nNewPos = si.nPos - si.nPage;
            break;
        case SB_PAGERIGHT:
            nNewPos = si.nPos + si.nPage;
            break;
        case SB_RIGHT:
            nNewPos = si.nMax; // 执行SetScrollPos函数后会由系统自动调整为SCROLL_MAX
            break;

        // 下面两个case要二选一
        //case SB_THUMBPOSITION: // 仅当用户拖动滚动条完成并释放鼠标左键后才更新内容
        case SB_THUMBTRACK: // 只要用户拖动了滚动条就立即更新内容
            nNewPos = HIWORD(wParam);
            break;

        // 忽略其他滚动条消息
        default:
            return FALSE;
        }
        nOldHorzPos = SetScrollPos(hWnd, SB_HORZ, nNewPos, TRUE); // 这里设置的是滑块左端的位置
        nNewPos = GetScrollPos(hWnd, SB_HORZ);
        // 若设置后滚动条位置有变化就更新窗口内容
        if (nNewPos != nOldHorzPos)
            InvalidateRect(hWnd, NULL, FALSE);
        break;

    // 鼠标滚轮消息处理
    case WM_MOUSEWHEEL:
        SystemParametersInfo(SPI_GETWHEELSCROLLLINES, NULL, &ulScrollLines, NULL); // 获取用户在控制面板中设置的滚动速度
        nLines = GET_WHEEL_DELTA_WPARAM(wParam) / WHEEL_DELTA * ulScrollLines; // 计算要滚动多少单位

        sbi.cbSize = sizeof(SCROLLBARINFO);
        GetScrollBarInfo(hWnd, OBJID_VSCROLL, &sbi); // 获取垂直滚动条的状态信息
        if (sbi.rgstate[0] == STATE_SYSTEM_INVISIBLE || sbi.rgstate[0] == STATE_SYSTEM_OFFSCREEN)
        {
            // 如果没有垂直滚动条,就滚动水平滚动条
            nNewPos = GetScrollPos(hWnd, SB_HORZ) - nLines * SCROLL_LINE;
            nOldPos = SetScrollPos(hWnd, SB_HORZ, nNewPos, TRUE);
            nNewPos = GetScrollPos(hWnd, SB_HORZ);
        }
        else
        {
            // 默认滚动垂直滚动条
            nNewPos = GetScrollPos(hWnd, SB_VERT) - nLines * SCROLL_LINE;
            nOldPos = SetScrollPos(hWnd, SB_VERT, nNewPos, TRUE);
            nNewPos = GetScrollPos(hWnd, SB_VERT);
        }
        // 如果滚动条位置发生变化就刷新窗口
        if (nOldPos != nNewPos)
            InvalidateRect(hWnd, NULL, FALSE);
        break;

    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        SetWindowOrgEx(hdc, GetScrollPos(hWnd, SB_HORZ), GetScrollPos(hWnd, SB_VERT), NULL); // 告诉hdc现在滚动到什么位置上了

        // 将位图显示到(0, 0)处
        // 其中的坐标都无需根据滚动条的位置改变,该往哪儿画就往哪儿画
        hdcMem = CreateCompatibleDC(hdc);
        SelectObject(hdcMem, hbmp);
        BitBlt(hdc, 0, 0, bmp.bmWidth, bmp.bmHeight, hdcMem, 0, 0, SRCCOPY);
        DeleteDC(hdcMem);
        EndPaint(hWnd, &ps);
        break;
    case WM_SIZE:
        // 记录重设nPage值之前的滚动条位置
        nOldHorzPos = GetScrollPos(hWnd, SB_HORZ);
        nOldVertPos = GetScrollPos(hWnd, SB_VERT);

        // 根据新的窗口尺寸重新设置nPage值的大小
        // 如果设置后某个滚动条消失或重新出现,那么系统会自动调整滚动条的位置
        SetRect(&rcClient, 0, 0, LOWORD(lParam), HIWORD(lParam));
        do
        {
            rect = rcClient;
            si.cbSize = sizeof(SCROLLINFO);
            si.fMask = SIF_PAGE;
            si.nPage = rect.right - rect.left;
            SetScrollInfo(hWnd, SB_HORZ, &si, TRUE);
            si.nPage = rect.bottom - rect.top;
            SetScrollInfo(hWnd, SB_VERT, &si, TRUE);
            GetClientRect(hWnd, &rcClient);
        } while (memcmp(&rcClient, &rect, sizeof(RECT)) != 0); // 如果滚动条消失或重新出现导致客户区尺寸发生变化,则需要重新设置nPage

        // 设置了nPage的值后,若滚动条位置已经自动发生了变化,则必须刷新窗口内容
        // 如果在注册窗口类时指定了CS_HREDRAW | CS_VREDRAW,则可以删除下面这两行
        if (nOldHorzPos != GetScrollPos(hWnd, SB_HORZ) || nOldVertPos != GetScrollPos(hWnd, SB_VERT))
            InvalidateRect(hWnd, NULL, FALSE);
        // 注意: InvalidateRect并不会立即刷新窗口, 而是要等到消息队列中没有消息了才执行WM_PAINT
        // 所以执行两次InvalidateRect并不会影响性能
        break;

    // 垂直滚动条消息的处理,这个和上面的水平滚动条差不多
    case WM_VSCROLL:
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_PAGE | SIF_POS | SIF_RANGE;
        GetScrollInfo(hWnd, SB_VERT, &si);
        switch (LOWORD(wParam))
        {
        case SB_TOP:
            nNewPos = si.nMin;
            break;
        case SB_LINEUP:
            nNewPos = si.nPos - SCROLL_LINE;
            break;
        case SB_LINEDOWN:
            nNewPos = si.nPos + SCROLL_LINE;
            break;
        case SB_PAGEUP:
            nNewPos = si.nPos - si.nPage;
            break;
        case SB_PAGEDOWN:
            nNewPos = si.nPos + si.nPage;
            break;
        case SB_BOTTOM:
            nNewPos = si.nMax;
            break;
        case SB_THUMBTRACK:
            nNewPos = HIWORD(wParam);
            break;
        default:
            return FALSE;
        }
        nOldVertPos = SetScrollPos(hWnd, SB_VERT, nNewPos, TRUE);
        nNewPos = GetScrollPos(hWnd, SB_VERT);
        if (nNewPos != nOldVertPos)
            InvalidateRect(hWnd, NULL, FALSE);
        break;
    default:
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
    return FALSE;
}

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
    WNDCLASS wc;
    wc.cbClsExtra = wc.cbWndExtra = 0;
    wc.hbrBackground = GetSysColorBrush(COLOR_WINDOW);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wc.hInstance = hInstance;
    wc.lpfnWndProc = WndProc;
    wc.lpszClassName = TEXT("ImageViewer");
    wc.lpszMenuName = NULL;
    wc.style = NULL;
    RegisterClass(&wc);

    // 创建窗口时,指定WS_HSCROLL或WS_VSCROLL就可以开启窗口滚动条
    HWND hWnd = CreateWindow(wc.lpszClassName, TEXT("Image Viewer"), WS_OVERLAPPEDWINDOW | WS_HSCROLL | WS_VSCROLL, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
    if (!hWnd)
        return 1;
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return msg.wParam;
}
一派护法 十九级
3楼 发表于:2016-2-15 23:06
资源文件:

位图就是用的Windows 10的C:\Windows\Web\Screen\img101.png,只不过用画图软件转换成了BMP格式。
一派护法 十九级
4楼 发表于:2016-2-16 10:48
【讲解】
如果把当前窗口内容看作某种尺寸的位图的话,例如800x600,那么水平滚动条的滚动范围就是0~799,也就是说滚动条滑块的最左端的最小位置为0,最右端的最大位置为799。
因此si.nMin的值就应该设置为0,si.nMax应该为799。
si.nPage指定了滑块的大小,一般根据窗口的可视区域大小(也就是客户区大小GetClientRect)来设置。
si.nPos指定了当前滚动到的位置,也就是滑块最左端的位置。
注意si.nPos的范围为:
si.nMin ≤ si.nPos ≤ SCROLL_MAX(si)
其中SCROLL_MAX(si) = (si.nMax - (int)si.nPage + 1)
也就是说si.nPos不可能等于si.nMax

SetScrollPos函数用于设置滚动条的位置,函数返回设置前滚动条的位置。但是,最终滚动条的位置不一定就等于设置的值。
例如执行:
SetScrollPos(hWnd, SB_VERT, nNewPos, TRUE);
然后执行a = GetScrollPos(hWnd, SB_VERT);
结果就会发现a的值不一定等于nNewPos。
这样一来我们就可以在处理WM_*SCROLL消息时大胆地设置新的滚动条位置的值,程序本身不作任何判断和纠正,让系统自动纠正错误的值。
设置完之后将现在的滚动条位置与设置前的滚动条位置进行比较,如果不相等的话才重绘窗口内容。

所以,如果将si.nPos的值强行设置为si.nMax的话,系统会自动纠正为SCROLL_MAX(si)。

InvalidateRect(hWnd, NULL, FALSE);用于刷新整个窗口的内容。执行后系统并不会立即发送WM_PAINT消息重绘窗口,而是要等到消息队列中没有任何消息了的时候才重绘。
这样一来,即便是这条语句同时执行多次,也丝毫不会影响性能。
如果想要立即刷新窗口,不等待消息队列变空,那么可以在其后立即调用UpdateWindow函数。

在WM_PAINT消息中,获得hdc句柄后立即用SetWindowOrgEx函数告诉hdc现在窗口滚动到了什么位置上了,这样hdc才能知道现在坐标(0, 0)到底应该画在窗口的什么位置上。
不然的话hdc还会以为(0, 0)就在窗口左上角,导致滚动没有效果。

在WM_SIZE消息中,程序根据当前客户区的大小设置si.nPage的值。但是设置后可能因为滚动条的消失或重新出现导致客户区的大小发生变化,这个时候就必须返回来重新设置si.nPage的值,直到客户区的大小不再变化为止。
此外,如果滚动条自动消失或出现,系统可能会自动纠正滚动条的位置,这个时候程序就需要检测滚动条位置是否已经发生了变化。如果发生了变化就必须重绘窗口。

对于鼠标滚轮,如果向下滚动(也就是说让滑块向下移动),GET_WHEEL_DELTA_WPARAM(wParam)的值为负(向上滚动才是正),这点一定要注意。
处理滚轮消息时,最好先获取一下用户在控制面板——鼠标中设置的鼠标滚轮滚动速度,也就是滚动一次是多少行。
一派护法 十九级
5楼 发表于:2016-2-16 10:53
由于创建窗口时,系统会先发送WM_CREATE,然后发送WM_SIZE。因此可以在WM_CREATE中加载位图并设置好滚动条的范围,然后在WM_SIZE中设置好滑块的大小。滚动条位置保持默认值0。
即使窗口运行期间不改变大小,WM_SIZE也至少会在创建窗口时执行一次。
一派护法 十九级
6楼 发表于:2016-2-16 11:20

程序在XP系统下运行的效果:

一派护法 十九级
7楼 发表于:2016-2-16 11:23
一派护法 十九级
8楼 发表于:2016-2-16 12:36
程序在Windows 7系统下运行的效果:
一派护法 十九级
9楼 发表于:2016-3-11 17:13
【补充】
while (memcmp(&rcClient, &rect, sizeof(RECT)) != 0)
在这句代码中判断rcClientRect和rect是否相等,如果不相等则继续循环。
其实这个功能还可以通过EqualRect函数来实现:
while (!EqualRect(&rcClient, &rect))
一派护法 十九级
10楼 发表于:2016-4-26 20:31

回复帖子

内容:
用户名: 您目前是匿名发表
验证码:
(快捷键:Ctrl+Enter)
 

本帖信息

点击数:557 回复数:9
评论数: ?
作者:巨大八爪鱼
最后回复:巨大八爪鱼
最后回复时间:2016-4-26 20:31
 
©2010-2024 Arslanbar Ver2.0
除非另有声明,本站采用知识共享署名-相同方式共享 3.0 Unported许可协议进行许可。