Mesoscopic Programming

タコさんプログラミング専門

ウィンドウズプログラミング講座第8回:スクロールバー

概要

スクロールバーに対応します。
スクロールバーを表示するのは簡単だけど、ちゃんと対応するのはけっこう大変です。
今回はウィンドウのサイズ変更に合わせてスクロールバーを出したり消したり、スクロール位置を保存したりとちゃんとした対応処理を実装しました。


class Window(ウィンドウ基底クラス)

以下のように基底クラス側で既にスクロール関係のウィンドウメッセージ対応仮想関数を定義してありますので、派生クラス側で必要な関数をオーバーライドしてください。

LRESULT Window :: MessageProc()
{
    switch ( umsg )
    {
    case WM_HSCROLL : return OnWmHscroll();
    case WM_VSCROLL : return OnWmVscroll();
    }

    return DefaultProc();
}

LRESULT Window :: OnWmHscroll()
{
    switch ( LOWORD( wparam ) )
    {
    case SB_LINELEFT :      return OnHsbLineLeft();
    case SB_LINERIGHT :     return OnHsbLineRight();
    case SB_PAGELEFT :      return OnHsbPageLeft();
    case SB_PAGERIGHT :     return OnHsbPageRight();
    case SB_THUMBPOSITION : return OnHsbThumbPosition();
    case SB_THUMBTRACK :    return OnHsbThumbTrack();
    case SB_LEFT :          return OnHsbLeft();
    case SB_RIGHT :         return OnHsbRight();
    case SB_ENDSCROLL :     return OnHsbEndScroll();
    }

    return DefaultProc();
}

LRESULT Window :: OnWmVscroll()
{
    switch ( LOWORD( wparam ) )
    {
    case SB_LINEUP :        return OnVsbLineUp();
    case SB_LINEDOWN :      return OnVsbLineDown();
    case SB_PAGEUP :        return OnVsbPageUp();
    case SB_PAGEDOWN :      return OnVsbPageDown();
    case SB_THUMBPOSITION : return OnVsbThumbPosition();
    case SB_THUMBTRACK :    return OnVsbThumbTrack();
    case SB_TOP :           return OnVsbTop();
    case SB_BOTTOM :        return OnVsbBottom();
    case SB_ENDSCROLL :     return OnVsbEndScroll();
    }

    return DefaultProc();
}


class App(メインウィンドウクラス)

以下のように、ドキュメントウィンドウのプロファイル関数を呼んであげます。
スクロール処理そのものは、ドキュメントウィンドウ側で行いますのでメインウィンドウさんの方では気にしなくて良いです。

class App : public Window
{
    VOID GetProf();
    VOID SetProf();
}

VOID App :: GetProf()
{
    docView.GetProf();
}

VOID App :: SetProf()
{
    docView.SetProf();
}


class DocView(ドキュメントウィンドウ)

これが全部スクロール処理に必要な変数と関数です。
めちゃくちゃ多いですね。

class DocView : public Window
{
    static const LPCTSTR sectionDocView;
    static const LPCTSTR keyHorzPos;
    static const LPCTSTR keyVertPos;

    VOID        GetProf();
    VOID        SetProf();
    VOID        UpdateSize();
    VOID        UpdateScrollRange( int bar );
    VOID        Scroll( int bar, int pos, BOOL page = FALSE );
    LRESULT     OnWmSize();
    LRESULT     OnWmPaint();
    LRESULT     OnHsbLineLeft();
    LRESULT     OnHsbLineRight();
    LRESULT     OnHsbPageLeft();
    LRESULT     OnHsbPageRight();
    LRESULT     OnHsbThumbPosition();
    LRESULT     OnHsbThumbTrack();
    LRESULT     OnHsbLeft();
    LRESULT     OnHsbRight();
    LRESULT     OnVsbLineUp();
    LRESULT     OnVsbLineDown();
    LRESULT     OnVsbPageUp();
    LRESULT     OnVsbPageDown();
    LRESULT     OnVsbThumbPosition();
    LRESULT     OnVsbThumbTrack();
    LRESULT     OnVsbTop();
    LRESULT     OnVsbBottom();
};

const LPCTSTR DocView :: sectionDocView = "Document View";
const LPCTSTR DocView :: keyHorzPos     = "HorzPos";
const LPCTSTR DocView :: keyVertPos     = "VertPos";

プロファイル

以下のように水平垂直それぞれのスクロールバーの設定ポジションを読んだり書いたりします。

VOID DocView :: GetProf()
{
    SetScrollPos( hwnd, SB_HORZ, GetProfInt( sectionDocView, keyHorzPos, 0 ), FALSE );
    SetScrollPos( hwnd, SB_VERT, GetProfInt( sectionDocView, keyVertPos, 0 ), FALSE );
}

VOID DocView :: SetProf()
{
    SetProfInt( sectionDocView, keyHorzPos, GetScrollPos( hwnd, SB_HORZ ) );
    SetProfInt( sectionDocView, keyVertPos, GetScrollPos( hwnd, SB_VERT ) );
}

ウィンドウサイズ変更

ウィンドウサイズが変更されたら、スクロールバーが必要か否か判断して更新します。
これが結構大変。

VOID DocView :: UpdateSize()
{
    DWORD dwStyle   = GetWindowLong( hwnd, GWL_STYLE );
    DWORD dwExStyle = GetWindowLong( hwnd, GWL_EXSTYLE );
    RECT  rc        = { 0, 0, width, height };
    RECT  rc2;

    dwStyle &= ~WS_HSCROLL;
    dwStyle &= ~WS_VSCROLL;

    AdjustWindowRectEx( & rc, dwStyle, FALSE, dwExStyle );
    GetWindowRect( hwnd, & rc2 );

    if ( rc2.right - rc2.left < rc.right - rc.left )
    {
        dwStyle |= WS_HSCROLL;

        rc2.bottom -= GetSystemMetrics( SM_CYHSCROLL );
    }
    else
    {
        dwStyle &= ~WS_HSCROLL;
    }

    if ( rc2.bottom - rc2.top < rc.bottom - rc.top )
    {
        dwStyle |= WS_VSCROLL;

        rc2.right -= GetSystemMetrics( SM_CXVSCROLL );

        if ( rc2.right - rc2.left < rc.right - rc.left )
        {
            dwStyle |= WS_HSCROLL;
        }
    }
    else
    {
        dwStyle &= ~WS_VSCROLL;
    }

    SetWindowLong( hwnd, GWL_STYLE, dwStyle );
    SetWindowPos( hwnd, 0, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE );
    UpdateScrollRange( SB_HORZ );
    UpdateScrollRange( SB_VERT );
}
解説

まず AdjustWindowRectEx() 関数でスクロール必要なしなサイズを求めます。
次に GetWindowRect() 関数で実際のウィンドウサイズを取得します。
もしこれが必要なサイズ未満だったらスクロールバーが必要だっちゅうことになりますよね。
やっかいなのは、スクロールバーを表示させるとクライアント領域が狭くなっちゃうのでこれを考慮しないとならないこと。
だから水平スクロールバーが必要だと分かったら、現在のクライアントエリアの高さを減らし、同様に垂直スクロールバーについてもこれをします。
んで、最初必要無いと思ってた水平スクロールバーが、垂直スクロールバーがあると必要になったりするので再計算します。
スクロールバーの有無が決まったら SetWindowLong() 関数で設定するんだけども、これだけじゃ表示されないので SetWindowPos() 関数も実行します。
これでスクロールバーの表示については完了。
あとはスクロールレンジを更新するために UpdateScrollRange() 関数を呼んでおります。

スクロールレンジの更新

スクロールレンジをチンします。

VOID DocView :: UpdateScrollRange( int bar )
{
    DWORD dwStyle = GetWindowLong( hwnd, GWL_STYLE );
    INT   min     = 0;
    INT   max     = 0;
    INT   pos     = 0;
    RECT  rc;

    GetClientRect( hwnd, & rc );

    if ( ( bar == SB_HORZ && ( dwStyle & WS_HSCROLL ) != 0 ) || ( bar == SB_VERT && ( dwStyle & WS_VSCROLL ) != 0 ) )
    {
        if ( bar == SB_HORZ )
        {
            max = width  - ( rc.right  - rc.left );
        }
        else
        {
            max = height - ( rc.bottom - rc.top );
        }

        pos = GetScrollPos( hwnd, bar );
    }

    SetScrollRange( hwnd, bar, min, max, TRUE );
    SetScrollPos( hwnd, bar, pos, TRUE );
}
解説

現在のクライアントエリアが必要なサイズに足りない分をスクロールレンジとします。

スクロールする

スクロールメッセージ関数からコールされるスクロールさせる関数です。

VOID DocView :: Scroll( int bar, int pos, BOOL page )
{
    INT pos0 = GetScrollPos( hwnd, bar );
    INT div  = 5;
    INT dx   = 0;
    INT dy   = 0;
    INT min;
    INT max;

    GetScrollRange( hwnd, bar, & min, & max );

    if ( page )
    {
        pos = pos0 + pos * ( max - min + div - 1 ) / div;
    }

    if ( pos < min )
    {
        pos = min;
    }
    else if ( pos > max )
    {
        pos = max;
    }

    SetScrollPos( hwnd, bar, pos, TRUE );

    if ( bar == SB_HORZ )
    {
        dx = pos0 - pos;
    }
    else if ( bar == SB_VERT )
    {
        dy = pos0 - pos;
    }

    ScrollWindowEx( hwnd, dx, dy, NULL, NULL, NULL, NULL, SW_SCROLLCHILDREN | SW_ERASE | SW_INVALIDATE );
}
解説

最大値と最小値のチェックとか、ページ単位のスクロールとかを処理してスクロールポジションをセットします。
んで、前のスクロールポジションとの差を求めたら ScrollWindowEx() 関数で実際の画面をスクロールさせます。
スクロールで足んなくなった分は、インバリデートが発生するのでペイント関数が描いてくれます。

ペイント

スクロールポジションを考慮して画面を描きます。

LRESULT DocView :: OnWmPaint()
{
    PAINTSTRUCT ps;
    HDC         hdc = BeginPaint( hwnd, & ps );
    RECT        rc;
    TCHAR       text[ 80 ];

    GetClientRect( hwnd, & rc );

    sprintf( text, "%d x %d", rc.right - rc.left, rc.bottom - rc.top );

    rc.left   = - GetScrollPos( hwnd, SB_HORZ );
    rc.top    = - GetScrollPos( hwnd, SB_VERT );
    rc.right  = rc.left + width;
    rc.bottom = rc.top  + height;

    MoveToEx( hdc, rc.left, rc.top, NULL );
    LineTo( hdc, rc.right, rc.bottom );
    MoveToEx( hdc, rc.right, rc.top, NULL );
    LineTo( hdc, rc.left, rc.bottom );

    DrawText( hdc, text, -1, & rc, DT_SINGLELINE | DT_VCENTER | DT_CENTER );

    EndPaint( hwnd, & ps );

    return 0;
}
解説

スクロールポジションがゼロでないってことは、クライアントエリアの左上がゼロでないってことだから、マイナスしてます。
とりあえずそれだけ注意すれば、あとは通常通りのお絵かきで足りるみたいです。

スクロールメッセージ関数群

スクロールメッセージは山ほどあります。
縦スクロール、横スクロール、右と左、上と下、トラッキング、三角ボタン、ページスクロールなどなど。
これらにちゃんと真面目に対応するとこうなります。

LRESULT DocView :: OnHsbLineLeft()
{
    Scroll( SB_HORZ, GetScrollPos( hwnd, SB_HORZ ) - 1 );

    return 0;
}

LRESULT DocView :: OnHsbLineRight()
{
    Scroll( SB_HORZ, GetScrollPos( hwnd, SB_HORZ ) + 1 );

    return 0;
}

LRESULT DocView :: OnHsbPageLeft()
{
    Scroll( SB_HORZ, - 1, TRUE );

    return 0;
}

LRESULT DocView :: OnHsbPageRight()
{
    Scroll( SB_HORZ, 1, TRUE );

    return 0;
}

LRESULT DocView :: OnHsbThumbPosition()
{
    Scroll( SB_HORZ, HIWORD( wparam ) );

    return 0;
}

LRESULT DocView :: OnHsbThumbTrack()
{
    return OnHsbThumbPosition();
}

LRESULT DocView :: OnHsbLeft()
{
    INT min;
    INT max;

    GetScrollRange( hwnd, SB_HORZ, & min, & max );
    Scroll( SB_HORZ, min );

    return 0;
}

LRESULT DocView :: OnHsbRight()
{
    INT min;
    INT max;

    GetScrollRange( hwnd, SB_HORZ, & min, & max );
    Scroll( SB_HORZ, max );

    return 0;
}

LRESULT DocView :: OnVsbLineUp()
{
    Scroll( SB_VERT, GetScrollPos( hwnd, SB_VERT ) - 1 );

    return 0;
}

LRESULT DocView :: OnVsbLineDown()
{
    Scroll( SB_VERT, GetScrollPos( hwnd, SB_VERT ) + 1 );

    return 0;
}

LRESULT DocView :: OnVsbPageUp()
{
    Scroll( SB_VERT, - 1, TRUE );

    return 0;
}

LRESULT DocView :: OnVsbPageDown()
{
    Scroll( SB_VERT, 1, TRUE );

    return 0;
}

LRESULT DocView :: OnVsbThumbPosition()
{
    Scroll( SB_VERT, HIWORD( wparam ) );

    return 0;
}

LRESULT DocView :: OnVsbThumbTrack()
{
    return OnVsbThumbPosition();
}

LRESULT DocView :: OnVsbTop()
{
    INT min;
    INT max;

    GetScrollRange( hwnd, SB_VERT, & min, & max );
    Scroll( SB_VERT, min );

    return 0;
}

LRESULT DocView :: OnVsbBottom()
{
    INT min;
    INT max;

    GetScrollRange( hwnd, SB_VERT, & min, & max );
    Scroll( SB_VERT, max );

    return 0;
}
解説

それぞれ必要な方向やら移動量やらを判断してスクロール関数をコールします。
数が多いですがやってることは単純ですよね。


実行画面

実行ファイル
  1. Windows.zip

以上です。