2つのCListCtrlのスクロール同期

CListCtrlのスクロールの同期方法です。

今回の説明は同じクラスを2つ張ってあることを前提としますが、応用次第で別コントロールも可能です。
(ちなみにダイアログベースではないです。)

構成は
■CHogeView
├CHogeList m_List1
└CHogeList m_List2

となっています。
CHogeListはCListCtrlを継承するクラスです。

はじめに

まず、CListCtrlを継承するクラスを2つ作ります。
その2つのクラスにそれぞれ、WM_SYSCOMMANDをマップした関数OnSysCommand関数を作成します。
そして、親ウィンドウのヘッダを #include します。
親ではPreTranslateMessage関数を追加します。
同期はこの2つの関数を利用します。

WM_SYSCOMMANDとPreTranslateMessage

WM_SYSCOMMANDのWPARAMに渡される値を利用します。
マップされたOnSysCommand関数の引数nIDがその値です。
このnIDには何種類か値が入ってきますが、今回はSC_HSCROLLの場合の処理を行います(水平方向のみ同期させます)
WM_SYSCOMMANDが送られてくるタイミングは、スクロールバーによるスクロールが行われた場合のみのようです。
キーやマウスクリックの場合はどうするかというと、親ウィンドウのPreTranslateMessage関数で処理を行います。
先にソースを見ましょう。

同期スクロールの例(リスト側)
void CHogeList::OnSysCommand( UINT nID, LPARAM lParam ){

    CListCtrl::OnSysCommand(nID, lParam);

    if((nID & SC_HSCROLL) == SC_HSCROLL){
        CHogeManageView *pParent = (CHogeManageView*)GetParent();
        if(pParent == NULL){
            return;
        }
        CHogeList *tView1 = NULL;
        CHogeList *tView2 = NULL;
        CHogeList *pTarget = NULL;
        CHogeList *pFrom = NULL;
        tView1 = &(pParent->m_listTest);
        tView2 = &(pParent->m_listTest2);
        if(tView1 == this){
            pTarget = tView2;
            pFrom = tView1;
        }else if(tView2 == this){
            pTarget = tView1;
            pFrom = tView2;
        }
        SIZE size;
        int tx = pTarget->GetScrollPos(SB_HORZ);
        int fx = pFrom->GetScrollPos(SB_HORZ);
        size.cx = fx - tx;
        size.cy = 0;
        pTarget->Scroll(size);
    }
}
ここでは純粋に、スクロール位置を合わせています。
まず、SC_HSCROLLがnIDに入っているかを調査する必要があります。
会社でやったときは、SC_HSCROLL単体ではなく何かの値とorした値が入ってきたので、ANDで抽出してチェックしています。
WM_SYSCOMMANDが渡されたListがどのリストかを判断し、渡されていない方のリストをスクロールさせています。
もっとスマートなやり方があると思うのですが、まぁ、結果がよければいいということで^^;

次にPreTranslateMessage関数の処理のソースです。
同期スクロールの例(親側)
BOOL CHogeView::PreTranslateMessage( MSG* pMsg ){
    SHORT sCtrl = GetKeyState(VK_CONTROL);

    switch(pMsg->message){

        //---------------------------------------------------------------------
        //キー入力制御
        case WM_KEYDOWN:
            if((pMsg->wParam != VK_LEFT) && (pMsg->wParam != VK_RIGHT)){
                break;
            }

            if(pMsg->hwnd == m_listTest.m_hWnd){
                m_listTest2.PostMessage(pMsg->message, pMsg->wParam, pMsg->lParam);
                return FALSE;

            }else if(pMsg->hwnd == m_listTest2.m_hWnd){
                m_listTest.PostMessage(pMsg->message, pMsg->wParam, pMsg->lParam);
                return FALSE;

            }
            break;

        //---------------------------------------------------------------------
        //マウス左クリック制御
        case WM_LBUTTONDOWN:
            TLPOS pos;
            LVHITTESTINFO lvHitTest;
            POINT point;
            point.x =  LOWORD(pMsg->lParam);
            point.y =  HIWORD(pMsg->lParam);

            lvHitTest.flags = LVHT_ABOVE ;
            lvHitTest.pt = point;

            if(pMsg->hwnd == m_listTest.m_hWnd){
                /*位置を合わせる処理*/

            }else if(pMsg->hwnd == m_listTest2.m_hWnd){
                /*位置を合わせる処理*/

            }
        break;
    }
    return FALSE;
}
キー入力とマウスのクリックで処理を変えています。
キー入力があった場合、その入力を入力が無かったListにPostMessageで送ります。
このPostMessageがポイントです。
PreTranslateMessage関数はメッセージを処理する前にメッセージを処理できる関数です。
この関数はどうやらSendMessageで送られたもののみを処理できるようです。(きっとローカルフックしているに違いない)
PostMessageを利用することで、PreTranslateMessage関数に送ったメッセージを処理させないことができます。
もし、SendMessageでこれを行うと、キー入力が無限ループした状態になる可能性があります。

マウス入力の場合、処理を合わせる処理が必要です。
実は、私がこの手法を取ったときは、ListCtrlがすでにカスタマイズされていて、結構簡単にこの処理がかけました。
純粋なListCtrlで行う場合は、どのように行えばよいか・・・いくつか方法はありますが、あまり確実ではありません。
・WM_SYSCOMMANDのような方法を取る
・WM_LBUTTONDOWNを送る。座標等は自力で計算する。(PostMessageでうまくいくかはわかりません
・WM_KEYDOWNを送る。キーコードは自力で求める

の3方法です。
ここの処理は自力で考えましょう(爆)

これらの関数をうまく利用すればかなり確実なスクロールの同期ができると思います。


ここから下は不完全版です。

はじめに

まず、CListCtrlを継承するクラスを2つ作ります。
その2つのクラスにそれぞれ、WM_LBUTTONDOWNをマップした関数(OnLButtonDownとします)を作成します。
そして、親クラスのヘッダを #include します。

OnLButtonDown()

OnLButtonDown()関数内での処理は、渡されたnCodeによって若干変わってきます。
といっても気をつけたいのは SB_THUMBTRACK と SB_ENDSCROLL です。
SB_ENDSCROLL は縦スクロールバーを常に表示している場合のみ気をつければ良いでしょう。
SB_THUMBTRACK はマウスでスクロールバーをドラッグした場合に送られてくるコードです。
上記以外のコードはそのまま同期するコントロールにSendMessage()でメッセージを送ります。

nCodeに入ってくる値
フラグ 説明
SB_ENDSCROLL スクロールの終了
SB_LEFT 一番左にスクロール
SB_RIGHT 一番右にスクロール
SB_LINELEFT 1つ左にスクロール
SB_LINERIGHT 1つ右にスクロール
SB_PAGELEFT 1ページ左にスクロール
SB_PAGERIGHT 1ページ右にスクロール
SB_THUMBPOSITION 絶対位置へスクロール
SB_THUMBTRACK 指定位置へスクロール ボックスをドラッグ

SB_THUMBTRACKがnCodeで来た場合は、CListCtrlのScroll()関数を利用してコントロール内部をスクロールします。
ただし、これではスクロールバーがスクロールしないので、SetScrollPos()関数を利用して スクロールバーもスクロールさせる必要があります。

同期スクロールの例
void CHogeList::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) {

    CListCtrl::OnHScroll(nSBCode, nPos, pScrollBar);

    CHogeView *pParent = (CHogeView*)GetParent();

    CHogeList *pList1 = NULL;
    CHogeList *pList2 = NULL;
    CHogeList *pTarget = NULL;
    CHogeList *pFrom = NULL;
    CScrollBar *pTargetBar = NULL;
    int toPos;
    int fromPos;

    if(pParent != NULL){
        pList1 = &(pParent->m_List1);
        pList2 = &(pParent->m_List2);
    }else{
        return;
    }

    if((pList1 != NULL) && (pList2 != NULL)){
        int nPos1 = pList1->GetScrollPos(SB_HORZ);
        int nPos2 = pList2->GetScrollPos(SB_HORZ);
        CRect rect1;
        CRect rect2;
        pList1->GetWindowRect(&rect1);
        pList2->GetWindowRect(&rect2);

        //スクロール同期の準備
        if(nPos1 != nPos2){
            if(pList1 == this){
                //上部のリストをスクロールした場合
                pTarget = pList2;
                pFrom = pList1;
                toPos = nPos1;
                fromPos = nPos2;
                pTargetBar = pList2->GetScrollBarCtrl(SB_HORZ);

            }else if(pList2 == this){
                //下部のリストをスクロールした場合
                pTarget = pList1;
                pFrom = pList2;
                toPos = nPos2;
                fromPos = nPos1;
                pTargetBar = pList1->GetScrollBarCtrl(SB_HORZ);
            }

            //各種メッセージへの対応
            switch(nSBCode){
            case SB_THUMBTRACK:
                pTarget->Scroll(CSize(toPos - fromPos, 0));
                pTarget->SetScrollPos(SB_HORZ, toPos, TRUE);
                pTarget->UpdateWindow();
                break;
            case SB_ENDSCROLL:
                //本物の縦スクロールバーが表示された場合の対処
                if(toPos != 0){
                    nSBCode = SB_LINERIGHT;
                }
            default:
                //メッセージ送信の準備
                WPARAM wParam = MAKEWPARAM( nSBCode, nPos );
                pTarget->SendMessage(WM_HSCROLL, (WPARAM)wParam, (LPARAM)pTargetBar);
            break;
            }
        }
    }
}


完璧な同期はできませんが(縦スクロールバーがでた場合など。実際に試すとわかります。)
かなりいい感じに同期します。