Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Undocumented Corner

George Shepherd and Scot Wingo

, February 01, 1997


Dr. Dobb's Journal February 1997: Undocumented Corner

Scot is a cofounder of Stingray Software. He can be contacted at [email protected]. George is a senior computer scientist with DevelopMentor and can be contacted at [email protected]. They are the coauthors of MFC Internals (Addison-Wesley, 1996).


In our December 1996 column, we took a look at the internal structures of the popular CSplitterWnd class. This month, we'll show how you can apply knowledge of the CSplitterWnd internals to solve a CSplitterWnd problem that MFC programmers frequently encounter.

When using static or dynamic CSplitterWnds in applications, you may want to change the splitter position programmatically. CSplitterWnd provides one undocumented solution that is easy to use -- simply add a menu/toolbar item that has ID_WINDOW_SPLIT as the identifier. When a user selects this menu/toolbar item, it will place the splitter into a mode where the user has to change the position of the splitter bar. The mouse cursor moves to the splitter bar, and the splitter bar is also put into tracking mode: The user can choose its position by moving the mouse and clicking to lock in the new position. Let's see how this seemingly magical feature works.

ID_WINDOW_SPLIT is a global MFC resource ID defined in AFXRES.H. The resource ID is used in VIEWCORE.CPP; see Example 1. OnUpdateSplitCmd() disables the menu/toolbar item if the parent of the view is not a splitter window. The real work is done by CView::OnSplitCmd(). Listing One is the pseudocode for this member function.

OnSplitCmd() first verifies that the parent of the view handling the command is a CSplitterWnd by calling GetParent-Splitter(). If the view is inside a CSplitterWnd, it then ASSERTs that the splitter bar is not already in tracking mode, which should be impossible since the menu/toolbar item should be disabled by OnUpdateSplitCmd(). MFC has many such ASSERTs that check the validity of states that should be True. This catches the problem of a user mistakenly changing the behavior of OnUpdateSplitCmd() to allow the splitter bar to enter tracking mode twice. Once everything is validated,

OnSplitCmd() calls CSplitterWnd::DoKeyboardSplit(). Listing Two is the pseudocode for this member function.

First, DoKeyboardSplit() does some if/else checking to find out which part of the CSplitterWnd needs to be "activated." For example, if there are two splits open, DoKeyboardSplit() will need to move both bars, so it sets the value of ht to splitterIntersection1. Once the value of ht is determined, DoKeyboardSplit() calls StartTracking() -- the same function that is called when the user presses the mouse button to "grab" a splitter bar. Finally, after calling StartTracking(), DoKeyboardSplit() makes some calculations and then programmatically moves the mouse cursor over the splitter bar (or intersection) being manipulated by calling SetCursorPos().

While the ID_WINDOW_SPLIT trick is helpful and easy to use, there are common CSplitterWnd situations it does not address.

Not So Static CSplitterWnd

Many of today's applications have a Microsoft Explorer look-and-feel with a vertical static splitter that separates a tree view on the left, and list view on the right. The problem with using CSplitterWnd for this functionality is that in static mode you cannot change the orientation of the splitter bar. For example, what if you want to provide users with the ability to switch from a vertical presentation to a horizontal presentation?

In our previous column, we showed that CSplitterWnd has key data members that maintain the row/column information for the class; see Table 1. We'll need to manipulate these members to implement the static-bar flipping functionality. Since these CSplitterWnd data members are all protected, the first step is to create a CSplitterWnd derivative so that you can freely access the data members and encapsulate your CSplitterWnd enhancement in a stand-alone class. The enhanced CSplitterWnd is called "CDobbsSplitter," and its declaration is presented in Listing Three. From Listing Three you can see that a couple of data members have been added to CDobbsSplitter to help with the implementation of the FlipSplit() function. Table 2 lists these data members.

Listing Four is the implementation of the CDobbsSplitter class. The main functionality provided by the class comes from the FlipSplit() member function. This member function determines whether the splitterwindow is currently split horizontally or vertically, then calls either SplitVertically() or SplitHorizontally() to "flip" the split. The logic for detecting whether the current split is horizontal or vertical is based on the number of rows. For example, if the number of rows is greater than the number of columns, then the split is horizontal. If the number of columns is greater than the number of rows, then there is a vertical split.

The SplitHorizontally()/SplitVertically() functions actually perform the manipulations of the split. They do this by programmatically swapping the numbers of rows (m_nRows/m_nMaxRows) and columns (m_nCols/m_nMaxCols). After swapping the values of these data members, the row and column information arrays are also swapped (m_pColInfo and m_pRowInfo). Next, the ID given to each pane is swapped so that there are no focus problems using the SetDlgCtrlID() API. Finally, whenever you programmatically change a splitter window, you must call RecalcLayout() to cause the splitter window to update.

Notice also that SplitHorizontally() and SplitVertically() make a call to the UpdatePanes() member function. This member updates the m_pColInfo and m_pRowInfo size information arrays with new data based on the new split. This data is calculated using the previous split ratios and sizes of the panes.

Conclusion

By applying some of our undocumented CSplitterWnd knowledge, you have added new functionality to the CSplitterWnd and given end users the flexibility to change the appearance of a program that uses a static splitter. You can take this example and extend it to swap panes, or work in a CDialog. For a real challenge, you might even try to make a dynamic CSplitterWnd that can have more than two rows and columns. A sample application that further illustrates how this additional functionality can be implemented is available electronically (see "Availability," page 3).

DDJ

Listing One

BOOL CView::OnSplitCmd(UINT){
    CSplitterWnd* pSplitter = GetParentSplitter(this, FALSE);
    if (pSplitter == NULL)
        return FALSE;
    ASSERT(!pSplitter->IsTracking());
    pSplitter->DoKeyboardSplit();
    return TRUE;    
}


</p>

Back to Article

Listing Two

BOOL CSplitterWnd::DoKeyboardSplit(){
    int ht;
    if (m_nRows > 1 && m_nCols > 1)
        ht = splitterIntersection1; // split existing row+col
    else if (m_nRows > 1)
        ht = vSplitterBar1;         // split existing row
    else if (m_nCols > 1)
        ht = hSplitterBar1;         // split existing col
    else if (m_nMaxRows > 1 && m_nMaxCols > 1)
        ht = bothSplitterBox;       // we can split both
    else if (m_nMaxRows > 1)
        ht = vSplitterBox;          // we can split rows
    else if (m_nMaxCols > 1)
        ht = hSplitterBox;          // we can split columns
    else
        return FALSE;               // can't split


</p>
   // start tracking
    StartTracking(ht);


</p>
    CRect rect;
    rect.left = m_rectTracker.Width() / 2;
    rect.top = m_rectTracker.Height() / 2;
    if (m_ptTrackOffset.y != 0)
        rect.top = m_rectTracker.top;
    if (m_ptTrackOffset.x != 0)
        rect.left = m_bTracking2 ? m_rectTracker2.left :m_rectTracker.left;
    rect.OffsetRect(-m_ptTrackOffset.x, -m_ptTrackOffset.y);
    ClientToScreen(&rect);
    SetCursorPos(rect.left, rect.top);


</p>
    return TRUE;
}


</p>

Back to Article

Listing Three

class CDobbsSplitter : public CSplitterWnd{
// Construction
public:
    CDobbsSplitter();


</p>
// Attributes
public:
    BOOL m_bPanesSwapped;
    int m_nSplitRatio;
    int m_nSplitResolution;
// Operations
public:
  void SetSplitRatio( int nRatio );
  BOOL IsSplitHorizontally() const;
  BOOL IsSplitVertically() const { return !IsSplitHorizontally(); }
  BOOL ArePanesSwapped() const { return m_bPanesSwapped; }
protected:
    BOOL UpdateSplitRatio();
    BOOL UpdatePanes( int cx, int cy );
    BOOL UpdatePanes();
public: 
void FlipSplit();     //DDJ
    void SplitVertically();
    void SplitHorizontally();
    // Implementation
public:
    virtual ~CDobbsSplitter();
protected: // Generated message map functions
    afx_msg void OnSize(UINT nType, int cx, int cy );
    DECLARE_MESSAGE_MAP()
};


</p>

Back to Article

Listing Four

CDobbsSplitter::CDobbsSplitter(){
    //Intialize extended state.


m_nSplitRatio = -1; m_bPanesSwapped = FALSE; nSplitResolution = 1; } BEGIN_MESSAGE_MAP(CDobbsSplitter, CSplitterWnd) ON_WM_SIZE() END_MESSAGE_MAP() void CDobbsSplitter::SetSplitRatio( int nRatio ) { m_nSplitRatio = nRatio; } BOOL CDobbsSplitter::IsSplitHorizontally() const { ASSERT(( m_nRows > 1 ) != ( m_nCols > 1 )); ASSERT( max( m_nRows, m_nCols ) == 2 ); return ( m_nCols > 1 ); } void CDobbsSplitter::SplitHorizontally() { if( IsSplitHorizontally()) return; ASSERT( m_nCols = 1 ); ASSERT( m_nRows = 2 ); CWnd* pPane = GetDlgItem( IdFromRowCol( 1, 0 )); ASSERT( pPane ); // swap the H/V information m_nMaxCols = m_nCols = 2; m_nMaxRows = m_nRows = 1; CRowColInfo* pTmp = m_pColInfo; m_pColInfo = m_pRowInfo; m_pRowInfo = pTmp; // change the last pane's ID reference pPane->SetDlgCtrlID( IdFromRowCol( 0, 1 )); ASSERT( GetPane( 0, 1 )->GetSafeHwnd() == pPane->GetSafeHwnd() ); if( UpdatePanes()) RecalcLayout(); } void CDobbsSplitter::SplitVertically() { if( IsSplitVertically()) return; ASSERT( m_nCols = 2 ); ASSERT( m_nRows = 1 ); CWnd* pPane = GetDlgItem(IdFromRowCol( 0, 1 )); ASSERT( pPane ); // swap the H/V information m_nMaxCols = m_nCols = 1; m_nMaxRows = m_nRows = 2; CRowColInfo* pTmp = m_pColInfo; m_pColInfo = m_pRowInfo; m_pRowInfo = pTmp; // change last pane's ID reference (no need to change ID for first one) pPane->SetDlgCtrlID( IdFromRowCol( 1, 0 )); ASSERT( GetPane( 1, 0 )->GetSafeHwnd() == pPane->GetSafeHwnd() );

if( UpdatePanes()) RecalcLayout(); } void CDobbsSplitter::FlipSplit() { if( IsSplitHorizontally()) SplitVertically(); else { ASSERT( IsSplitVertically()); SplitHorizontally(); } } int CDobbsSplitter::UpdateSplitRatio() { CRowColInfo* pPanes; int czSplitter; if( IsSplitHorizontally()) { pPanes = m_pColInfo; czSplitter = m_cxSplitter; } else { pPanes = m_pRowInfo; czSplitter = m_cySplitter; } TRACE( "CDobbsSplitter::UpdateSplitRatio: 1:%i, 2:%i\n", pPanes[0].nCurSize, pPanes[1].nCurSize); if(( pPanes[0].nCurSize != -1 ) && ( pPanes[0].nCurSize + pPanes[1].nCurSize != 0 )) { m_nSplitRatio = nSplitResolution * pPanes[0].nCurSize / ( pPanes[0].nCurSize + pPanes[1].nCurSize + czSplitter ); } TRACE("m_nSplitRatio=%i\n", m_nSplitRatio ); return m_nSplitRatio; } BOOL CDobbsSplitter::UpdatePanes() { CRect rcClient; GetClientRect( rcClient ); return ( !rcClient.IsRectEmpty() && UpdatePanes( rcClient.Width(), rcClient.Height())); } BOOL CDobbsSplitter::UpdatePanes( int cx, int cy ) { TRACE("UpdatePanes: cx=%i, cy=%i\n", cx, cy ); CRowColInfo* pPanes; int cz; if( IsSplitHorizontally()) { pPanes = m_pColInfo; cz = cx; }

else { pPanes = m_pRowInfo; cz = cy; } BOOL bRes = UpdateSplitRatio(); if( m_nSplitRatio >= 0 ) { ASSERT( (ULONG)m_nSplitRatio * cz / nSplitResolution <= (ULONG)INT_MAX ); pPanes[0].nIdealSize = int( (ULONG)m_nSplitRatio * cz / nSplitResolution ); } return bRes; } void CDobbsSplitter::OnSize( UINT nType, int cx, int cy ) { TRACE("CDobbsSplitter::OnSize: cx:%i, cy:%i\n", cx, cy ); if(( nType != SIZE_MINIMIZED )&&( cx > 0 )&&( cy > 0 )) UpdatePanes( cx, cy ); CSplitterWnd::OnSize( nType, cx, cy ); }

Back to Article


Copyright © 1997, Dr. Dobb's Journal

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.