設置 | 登錄 | 註冊

目前共有10篇帖子。

【代碼範例】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

內容轉換:

回覆帖子
內容:
用戶名: 您目前是匿名發表。
驗證碼:
看不清?換一張
©2010-2025 Purasbar Ver3.0 [手機版] [桌面版]
除非另有聲明,本站採用知識共享署名-相同方式共享 3.0 Unported許可協議進行許可。