Current Entries | Archives   RSS

 


Scrollbar voodoo, the MFC way

Monday 13th December, 2004


There are some development tasks that - no matter how you look at them - you just know are going to be messy.

I've just finished an small piece of functionality in the forthcoming version of ResOrg which falls into exactly that category, and it all started with such a simple idea - add a dynamic splitter to the MFC child frame class used for Symbols Displays so that the user could open a split view of the same file easily and quickly. Although adding such a splitter is really easy with MFC (just configure it in the OnCreateClient() override of the MDI child frame class, and MFC's document-view architecture does the rest), there turned out to be a nasty catch.

Before I explain what that is, it's worth a little digression into what is (I think) one of the messiest parts of the MFC framework - the duplication of common control classes between those dervied from CWnd (e.g. CListCtrl) and those derived from CView (e.g. CListView). Although on the face of it this provision of both CWnd and CView derived common control classes offers a great deal of flexibility in the way the classes can be used, unfortunately, it's not that straightforward. If you have a CWnd derived control class you also want to use as a view, this design leads many to assume that the only way to do so is to duplicate its functionality in a CView derived class. Messy.

Incidentally, Paul DiLascia explains the reasoning behind this aspect of the MFC design (and more importantly, its severe limitations) rather well in his article C++ Q&A: Understanding CControlView, Changing Scroll Bar Color in MFC Apps. He makes one crucial point - although MFC uses CCtrlView to accomplish this trickery, you can't use it to use your own control classes as views if they have either virtual functions or member data.

Fortunately, there is a another approach, and one which I've been using for some time - simply create a CView derived class which hosts the CWnd derived control as a child window. ResOrg uses exactly this approach - the view class for the Symbols Display (CResOrgSymbolsListView) is a simple CView wrapper hosting a child control class (CResOrgSymbolsListCtrl). It's simple, effective and promotes reuseability...perfect, on the face of it.

Perfect that is, until you add a splitter window. Suddenly, I was confronted with the realisation that while the list control quite happily managed its scrollbars when necessary, the splitter also provides scrollbars...and they don't know about the list control, only the view that encloses it. The result is two sets of scrollbars, only one of which works:

Not only does the splitter provide scrollbars, but the list control does too
Not only does the splitter provide scrollbars, but the list control does too.

When I first saw this, it went firmly into the "fix it later" category - mostly because I wasn't sure exactly how to correct the problem.

I finally dived in and had a real go at it this weekend. The first job was to configure the splitter's scrollbars to work the same way as those belonging to the control itself. This turned out to be straightforward - simply use the settings from the list control's scrollbars:

void CResOrgSymbolsListView::ConfigureScrollbars()
{
    SCROLLINFO infoHorz;
    m_ctrlSymbols.GetScrollInfo(SB_HORZ, &infoHorz);
    SetScrollInfo(SB_HORZ, &infoHorz, true);


    SCROLLINFO infoVert;
    m_ctrlSymbols.GetScrollInfo(SB_VERT, &infoVert);
    SetScrollInfo(SB_VERT, &infoVert, true);
}

This reconfiguration needs to be performed whenever the size or contents of the control changes, so the obvious places to do it are within OnSize() (which is already overridden to resize the list control when the view size changes) and OnItemChangedListCtrl() (the LVN_ITEMCHANGED handler).

Once the OnVScroll() and OnHScroll() handlers were implemented (I used MSDN as a reference to make sure all cases were handled) the outer scroll bars worked, and tracked those belonging to the list control itself.

So far, so good - although there still remained the problem of duplicated scrollbars of course. At this point it also became obvious that there was another problem - manipulating the list control via the keyboard didn't update the scrollbars.

Hiding the list control's scrollbars should be easy, but it's not at all obvious, as removing the appropriate window styles (WS_HSCROLL and WS_VSCROLL) at the outset stops the header control from being painted (there's an MSDN article on this somewhere). I finally found a solution which worked for me in the article Hide scrollbars from a CListCtrl by Lars Werner - remove the scrollbar styles in a WM_NCCALCSIZE handler:

void CResOrgSymbolsListCtrl::OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS FAR* lpncsp)
{
    CWnd* pParent = GetParent();

    if ( (NULL != pParent) && IsViewContainedInSplitterWindow(pParent) )
    {
        // If we're running inside a view within a splitter, remove our own scrollbars
        // so the splitter scrollbars can do their thing instead
        ModifyStyle(WS_HSCROLL | WS_VSCROLL, 0, 0);
    }
    CResOrgSymbolsListCtrl_BASE::OnNcCalcSize(bCalcValidRects, lpncsp);
}

IsViewContainedInSplitterWindow() is a simple static function to determine whether a given view is contained within a splitter window:

static bool IsViewContainedInSplitterWindow(CWnd* pView)
{
    CWnd* pViewParent = pView->GetParent();
    if (NULL != pViewParent)
    {
        if (pViewParent->IsKindOf(RUNTIME_CLASS(CSplitterWnd) ) )
        {
            return true;
        }
    }
    return false;
}

The final task was to trap the keypresses which would potentially change the scrollbar state. A little thought confirmed that doing so in the view wouldn't help, since a keypress handed by the control could still affect the state of the scrollbars.

There seemed no choice but to handle the keypresses in CResOrgSymbolsListCtrl itself. The solution was to handle the WM_KEYDOWN message, call the base class implementation to allow the control to perform it's default action, then grab the settings of its (invisible) scrollbars and pass them to the view. To retain some decoupling between the two classes, this was implemented as a registered message (WM_SETSCROLLINFO):

void CResOrgSymbolsListCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    CResOrgSymbolsListCtrl_BASE::OnKeyDown(nChar, nRepCnt, nFlags);

    switch (nChar)
    {
        case VK_PRIOR:
        case VK_NEXT:
        case VK_DOWN:
        case VK_UP:
        case VK_HOME:
        case VK_LEFT:
        case VK_RIGHT:
        case VK_END:
            {
                // If we're running inside a view within a splitter, forward the settings from
                // our own scrollbars to it so that its internal state is maintained
                CWnd* pParent = GetParent();
                if ( (NULL != pParent) && IsViewContainedInSplitterWindow(pParent) )
                {
                    SCROLLINFO infoHorz;
                    GetScrollInfo(SB_HORZ, &infoHorz);
                    pParent->SendMessage(WM_SETSCROLLINFO, (WPARAM)SB_HORZ, (LPARAM)&infoHorz);

                    SCROLLINFO infoVert;
                    GetScrollInfo(SB_VERT, &infoVert);
                    pParent->SendMessage(WM_SETSCROLLINFO, (WPARAM)SB_VERT, (LPARAM)&infoVert);
                }
            }
            break;

        default:
            break;
    }
}


LRESULT CResOrgSymbolsListView::OnMsgSetScrollInfo(WPARAM wParam, LPARAM lParam)
{
    int eBar                 = (int)wParam;
    LPSCROLLINFO pScrollInfo = (LPSCROLLINFO)lParam;

    switch (eBar)
    {
        case SB_VERT:
            m_nLastVPos = pScrollInfo->nPos;
            SetScrollInfo(eBar, pScrollInfo);
            break;

        case SB_HORZ:
            m_nLastHPos = pScrollInfo->nPos;
            SetScrollInfo(eBar, pScrollInfo);
            break;

        default:
            ASSERT(false);
            break;
    }
    return 0L;
}

With these changes in place, the final result is exactly what you'd expect - normal looking, working scrollbars. In the image below, you can see a Symbols Display split into two panes - the lower one displaying conflicts only:

A Symbols Display, showing the splitter in action and scrollbars configured correctly
A Symbols Display, showing the splitter in action and scrollbars configured correctly.

Believe me, I'm rather glad to get this one out of the way!

Posted by Anna at 1:37pm | Get Permalink