본문 바로가기

프로그래밍/VC/VC.NET/Cs

[Control] CSplitterWnd 클래스 변화시키기

 블로그 > 정서기의 블로그입니다.
원본 http://blog.naver.com/hanjeongseok/20035979136
CSplitterWnd 클래스 변화시키기
 
이 내용은 마이크로소프트웨어 1999년 6월 내용중 "원리구현, UI를 강화하면 컴퓨터가 즐겁다[1], 특수한 UI를 적용한 스플리터 만들기"의 내용을 참고한 것입니다.
1. CSplitterWnd 클래스란?
 CSplitterWnd 클래스는 SDI에서 한번에 둘 이상의 뷰를 보여주는 것을 가능하게 하는 클래스입니다. MDI의 경우 child 윈도우를 여러개 만들어서 둘 이상의 뷰를 보여주지만, CSplitterWnd 클래스를 사용하면 한 화면을 두 개 내지는 그 이상 분할해서 한꺼번에 보여줍니다.
 
2. CSplitterWnd 의 파생클래스 만들기
 CSplitterWnd를 변화시키려면 그 클래스에서 상속받은 새로운 클래스를 만들어야 합니다. 이건 뷰나 도큐먼트, 메인 프레임 클래스도 마찬가지죠.
 클래스뷰에서 생성하면 되는데, 조금 문제가 있습니다. 바로 클래스 위저드에서 지원하는 베이스 클래스에는 CSplitterWnd 는 빠져있기 때문입니다.
 물론 이 경우에도 방법은 있습니다. 몇가지가 있는데요. 하나씩 장단점과 함께 소개하기로 하죠.
    1) ClassView의 가장 위쪽 항목에서 마우스 왼쪽버튼을 클릭하여 나오는 New Class에서 Class type을 Generic Class로 선택합니다. 그리고, 아래쪽에 있는 Base class(es) 부분에 직접 CSplitterWnd를 써넣습니다.
     이렇게 만든 클래스는 클래스 위저드에서 지원되지 않습니다. 실제 코드 내용을 보면 클래스 위저드에서 지원되는 형식의 코드는 전혀 포함되어 있지 않은 것을 알 수 있습니다. 실제로 이 방식으로 다중상속을 구현할 수 있습니다.
     클래스 위저드에서 지원되지 않기 때문에 새로운 함수를 추가할 때 조금 문제가 있습니다. 물론 손으로 직접 추가하면 되지만, 콜백함수의 경우 이것저것 추가할 것이 많기 때문에 손이 많이 가죠. 하나만 빼먹어도 제대로 돌아가지 않구요.
    2) 위와 같은 방법 말고, 다른 방법을 사용해 보죠. 위와 같은 위치에서 Class type을 MFC Class로 선택합니다. Base class를 generic CWnd로 선택하고, Class 이름을 입력한 다음에 OK 버튼을 누릅니다. 클래스 이름은 CMySplitterWnd 라고 하는게 무난하겠죠.
     그런 다음에 생성된 헤더파일과 cpp 파일에서 CWnd를 CSplitterWnd로 모두 바꿉니다. Edit 메뉴의 Replace 기능을 이용하면 편리할 겁니다. 이렇게 하면 클래스 위저드에서도 문제없이 지원이 됩니다.
     이렇게 해서 CSplitterWnd 의 파생클래스 파일을 만들었습니다.
    3)  그리고 MainFrm.h 파일에서 다음부분을 바꿉니다. 이렇게 해서 CSplitterWnd 대신에 우리가 만든 CMySplitterWnd 클래스로 대체하는 것이죠. 이 CMySplitterWnd 클래스의 내용을 조금씩 바꾸어가면서 우리가 원하는 부분을 만들어가는 거죠.
     #include "MySplitterWnd.h"

    ..............................

     // Attributes
    protected:
            CMySplitterWnd m_wndSplitter;
 
3. 정적 스플리트 만들기
여기서는 동적 스플리트를 정적 스플리트로 바꾸어 보겠습니다.
    1) 새로운 SDI 프로젝트를 만듭니다. "step 4 of 6" 에서 아래쪽의 Advanced 버튼을 눌러 나오는 다이얼로그에서 Window Styles 탭을 선택하고 "Use split window" 항목에 체크표시를 해줍니다.
    2) 이대로 계속 진행해서 컴파일을 하면 동적 스플리트 윈도우가 만들어집니다. 이것을 조금 고쳐서 정적 스플리트 윈도우를 만들 겁니다. 그리고 거기서 쓰인 CSplitterWnd형의 변수를 새로 만들 파생클래스의 변수로 바꾸어서 원하는 형태로 바꿀 계획입니다.
    3) ClassView에서 CMainFrame 클래스에서 다음 함수를 봅시다.
    BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT /*lpcs*/,
            CCreateContext* pContext)
    {
            return m_wndSplitter.Create(this,
                    2, 2,               // TODO: adjust the number of rows, columns
                    CSize(10, 10),      // TODO: adjust the minimum pane size
                    pContext);
    }
     이 함수에서 m_wndWndSplitter 변수는 CSplitterWnd 클래스의 멤버변수입니다. 이 클래스의 Create 멤버변수는 동적 스플리트를 만들어주는 함수인데, 정적 스플리트를 만들기 위해서는 CreateStatic 함수를 사용합니다. 위의 내용을 다음처럼 고쳐주면 되죠.
    BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT /*lpcs*/,
            CCreateContext* pContext)
    {
            if(!m_wndSplitter.CreateStatic(this, 1, 2))
            {
                    TRACE0("Failed to CreateStatic Splittern");
                    return FALSE;
            }

            if(!m_wndSplitter.CreateView(0, 0,
                    RUNTIME_CLASS(CMySplitView), CSize(200, 50), pContext))
            {
                    TRACE0("Failed to Crate First Panen");
                    return FALSE;
            }

            if(!m_wndSplitter.CreateView(0, 1,
                    RUNTIME_CLASS(CMySplitView), CSize(0, 0), pContext))
            {
                    TRACE0("Failed to Crate Second Panen");
                    return FALSE;
            }

            return TRUE;
    }
     
     이렇게 하고 컴파일을 하면 동적 스플리트에서 정적 스플리트로 바뀐 것을 볼 수 있을 겁니다. 여기서는 양쪽 pane에 편의상 같은 뷰를 보여주도록 했지만, 보통 새로운 뷰를 추가해서 양쪽에 서로 다른 뷰를 보여주는 경우가 대부분입니다.
 
4. CSplitterWnd 클래스의 헤더파일 내용
  CSplitterWnd 클래스의 헤더파일의 내용은 AfxExt.h 파일에, cpp 의 내용은 WinSplit.cpp 파일에 있습니다.
 다음은 AfxExt.h 파일에서 CSplitterWnd 클래스의 헤더파일 내용만 뽑아놓은 겁니다.
/////////////////////////////////////////////////////////////////////////////
// Splitter Window

#define SPLS_DYNAMIC_SPLIT  0x0001
#define SPLS_INVERT_TRACKER 0x0002  // obsolete (now ignored)

class CSplitterWnd : public CWnd
{
        DECLARE_DYNAMIC(CSplitterWnd)

// Construction
public:
        CSplitterWnd();
        // Create a single view type splitter with multiple splits
        BOOL Create(CWnd* pParentWnd,
                                int nMaxRows, int nMaxCols, SIZE sizeMin,
                                CCreateContext* pContext,
                                DWORD dwStyle = WS_CHILD | WS_VISIBLE |
                                        WS_HSCROLL | WS_VSCROLL | SPLS_DYNAMIC_SPLIT,
                                UINT nID = AFX_IDW_PANE_FIRST);

        // Create a multiple view type splitter with static layout
        BOOL CreateStatic(CWnd* pParentWnd,
                                int nRows, int nCols,
                                DWORD dwStyle = WS_CHILD | WS_VISIBLE,
                                UINT nID = AFX_IDW_PANE_FIRST);

        virtual BOOL CreateView(int row, int col, CRuntimeClass* pViewClass,
                        SIZE sizeInit, CCreateContext* pContext);

// Attributes
public:
        int GetRowCount() const;
        int GetColumnCount() const;

        // information about a specific row or column
        void GetRowInfo(int row, int& cyCur, int& cyMin) const;
        void SetRowInfo(int row, int cyIdeal, int cyMin);
        void GetColumnInfo(int col, int& cxCur, int& cxMin) const;
        void SetColumnInfo(int col, int cxIdeal, int cxMin);

        // for setting and getting shared scroll bar style
        DWORD GetScrollStyle() const;
        void SetScrollStyle(DWORD dwStyle);

        // views inside the splitter
        CWnd* GetPane(int row, int col) const;
        BOOL IsChildPane(CWnd* pWnd, int* pRow, int* pCol);
        BOOL IsChildPane(CWnd* pWnd, int& row, int& col); // obsolete
        int IdFromRowCol(int row, int col) const;

        BOOL IsTracking();  // TRUE during split operation

// Operations
public:
        virtual void RecalcLayout();    // call after changing sizes

// Overridables
protected:
        // to customize the drawing
        enum ESplitType { splitBox, splitBar, splitIntersection, splitBorder };
        virtual void OnDrawSplitter(CDC* pDC, ESplitType nType, const CRect& rect);
        virtual void OnInvertTracker(const CRect& rect);

public:
        // for customizing scrollbar regions
        virtual BOOL CreateScrollBarCtrl(DWORD dwStyle, UINT nID);

        // for customizing DYNAMIC_SPLIT behavior
        virtual void DeleteView(int row, int col);
        virtual BOOL SplitRow(int cyBefore);
        virtual BOOL SplitColumn(int cxBefore);
        virtual void DeleteRow(int rowDelete);
        virtual void DeleteColumn(int colDelete);

        // determining active pane from focus or active view in frame
        virtual CWnd* GetActivePane(int* pRow = NULL, int* pCol = NULL);
        virtual void SetActivePane(int row, int col, CWnd* pWnd = NULL);

protected:
        CWnd* GetActivePane(int& row, int& col); // obsolete

public:
        // high level command operations - called by default view implementation
        virtual BOOL CanActivateNext(BOOL bPrev = FALSE);
        virtual void ActivateNext(BOOL bPrev = FALSE);
        virtual BOOL DoKeyboardSplit();

        // synchronized scrolling
        virtual BOOL DoScroll(CView* pViewFrom, UINT nScrollCode,
                BOOL bDoScroll = TRUE);
        virtual BOOL DoScrollBy(CView* pViewFrom, CSize sizeScroll,
                BOOL bDoScroll = TRUE);

// Implementation
public:
        virtual ~CSplitterWnd();
#ifdef _DEBUG
        virtual void AssertValid() const;
        virtual void Dump(CDumpContext& dc) const;
#endif

        // implementation structure
        struct CRowColInfo
        {
                int nMinSize;       // below that try not to show
                int nIdealSize;     // user set size
                // variable depending on the available size layout
                int nCurSize;       // 0 => invisible, -1 => nonexistant
        };

protected:
        // customizable implementation attributes (set by constructor or Create)
        CRuntimeClass* m_pDynamicViewClass;
        int m_nMaxRows, m_nMaxCols;

        // implementation attributes which control layout of the splitter
        int m_cxSplitter, m_cySplitter;         // size of splitter bar
        int m_cxBorderShare, m_cyBorderShare;   // space on either side of splitter
        int m_cxSplitterGap, m_cySplitterGap;   // amount of space between panes
        int m_cxBorder, m_cyBorder;             // borders in client area

        // current state information
        int m_nRows, m_nCols;
        BOOL m_bHasHScroll, m_bHasVScroll;
        CRowColInfo* m_pColInfo;
        CRowColInfo* m_pRowInfo;

        // Tracking info - only valid when 'm_bTracking' is set
        BOOL m_bTracking, m_bTracking2;
        CPoint m_ptTrackOffset;
        CRect m_rectLimit;
        CRect m_rectTracker, m_rectTracker2;
        int m_htTrack;

        // implementation routines
        BOOL CreateCommon(CWnd* pParentWnd, SIZE sizeMin, DWORD dwStyle, UINT nID);
        virtual int HitTest(CPoint pt) const;
        virtual void GetInsideRect(CRect& rect) const;
        virtual void GetHitRect(int ht, CRect& rect);
        virtual void TrackRowSize(int y, int row);
        virtual void TrackColumnSize(int x, int col);
        virtual void DrawAllSplitBars(CDC* pDC, int cxInside, int cyInside);
        virtual void SetSplitCursor(int ht);
        CWnd* GetSizingParent();

        // starting and stopping tracking
        virtual void StartTracking(int ht);
        virtual void StopTracking(BOOL bAccept);

        // special command routing to frame
        virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam);
        virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult);

        //{{AFX_MSG(CSplitterWnd)
        afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
        afx_msg void OnMouseMove(UINT nFlags, CPoint pt);
        afx_msg void OnPaint();
        afx_msg void OnLButtonDown(UINT nFlags, CPoint pt);
        afx_msg void OnLButtonDblClk(UINT nFlags, CPoint pt);
        afx_msg void OnLButtonUp(UINT nFlags, CPoint pt);
        afx_msg void OnCancelMode();
        afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
        afx_msg void OnSize(UINT nType, int cx, int cy);
        afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
        afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
        afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt);
        afx_msg BOOL OnNcCreate(LPCREATESTRUCT lpcs);
        afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
        afx_msg void OnDisplayChange();
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
};
 
5. 스플릿바의 크기변경
CSplitterWnd를 보면 스플릿바의 폭 계산과 패널의 배치를 위한 몇 개의 멤버변수를 가지고 있는데, 그 내용은 다음과 같습니다.
protected:
        ............................

        // implementation attributes which control layout of the splitter
        int m_cxSplitter, m_cySplitter;         // size of splitter bar
        int m_cxBorderShare, m_cyBorderShare;   // space on either side of splitter
        int m_cxSplitterGap, m_cySplitterGap;   // amount of space between panes
        int m_cxBorder, m_cyBorder;             // borders in client area
 
변수명
변수 타입
사용 용도
m_cxSplitter
정수형
수직,수평 분할 바의 두께를 정한다.
m_cySplitter
m_cxBorderShare
정수형
분할 바의 양끝이 테두리(Border)와 겹쳐지는 정도
m_cyBorderShare
m_cxSplitterGap
정수형
분할 바를 사이에 둔 양쪽 뷰 사이의 공간
m_cySplitterGap
m_cxBorder
정수형
분할 창의 테두리값, 이 값만큼 뷰가 안쪽에 위치한다.
m_cyBorder
 
 실제 CSplitterWnd의 생성자에선 Windows 95 이상일 경우, 위의 값들은 다음과 같이 정해집니다.
                m_cxSplitter = m_cySplitter = 4;
                m_cxBorderShare = m_cyBorderShare = 1;
                m_cxSplitterGap = m_cySplitterGap = 4 + 1 + 1;
                ASSERT(m_cxBorder == 0 && m_cyBorder == 0);
위의 값들을 조정하면 스플릿바의 두께를 조절할 수 있습니다.
그런데 실제의 경우 m_cxSplitter 와 m_cxSplitterGap 이 같지 않으면 문제가 생기죠.
  그런 현상이 생기는 원인은 이 두 개의 서로 다른 변수가 고유의 목적없이 혼용되어 사용되었기 때문입니다.
m_cxSplitter는 분할창 이동시 반전되는 부분의 폭 계산과 이동후 다시 무효화되는(그려지는) 영역의 계산에만 사용될 뿐 실제 분할 창의 폭 계산에는 사용되지 않지요. 그리고 m_cxSplitterGap 은 분할바의 폭 계산과 양쪽창의 위치계산에는 사용되지만 실제 분할 창을 그리는 데는 사용되지 않습니다. 즉, 위의 표에서 설명한 내용이 제대로 지켜지고 있지 않다는 것이죠.
하여간 이 두 값을 같게 정해주면 됩니다.
 지금까지의 내용을 참고하여, CMySplitterWnd 의 생성자를 다음처럼 고치면 스플릿바의 폭을 조절할 수 있습니다. 여기서는 폭을 24 pixel로 정했습니다. 물론 필요에 따라 이 값을 다르게 변화시키면 됩니다. 여기서 24 pixel로 정한 이유는 스플리터를 나타내기 위한 바인더 그림의 폭이 24 pixel 이기 때문입니다.
 
CMySplitterWnd::CMySplitterWnd()
{
        AFX_ZERO_INIT_OBJECT(CWnd);

        m_cxSplitter = m_cySplitter = 24;
        m_cxBorderShare = m_cyBorderShare = 0;
        m_cxSplitterGap = m_cySplitterGap = 24;
        // ASSERT(m_cxBorder == 0 && m_cyBorder == 0);
        m_cxBorder = m_cyBorder = 2;  
}
 
6. 스플릿바의 모양변경
 윈도우에 무언가를 그리기 위해서는 WM_PAINT 메시지의 핸들러인 OnPaint() 함수를 처리하면 됩니다. CSplitterWnd 클래스도 마찬가지로 OnPaint() 함수에서 OnDrawSplitter() 라는 가상함수를 호출해서 스플릿바를 그리게 됩니다. 이 함수의 원형은 다음과 같습니다.
virtual void OnDrawSplitter( CDC* pDC, ESplitType nType, const CRect& rect );
>> 파라미터 :
◆ pDC : 디바이스 컨텍스트에 대한 포인터, 만약 이 값이 NULL이면 분할창이 그려지기 전에 Framework에 의해 CWnd의 RedrawWindow 함수가 불리어진 것이다.
◆ nType : enum 타입의 ESplitType에 대한 값. 다음의 예 중 하나이다.
 splitBox
 분할 끌기 상자
 splitBar
 두 개의 분할 창 사이에 보여지는 바
 splitIntersection
 분할 창의 교차점. 위도우95에서 실행될 때는 사용되지 않는다
 splitBorder
 분할 창의 테두리
 ◆ rect : 분할 창의 크기와 모양을 충족시키기 위한 CRect의 레퍼런스값
 
 CSplitterWnd 클래스를 오버라이드해도 스플릿바의 모양을 바꾸는 것이 가능하지만, 이 함수는 스플릿바 뿐만이 아니라 테두리(Border)까지 그리기 때문에, 이 함수를 고쳐서 사용하기에는 불편한 점이 있습니다. 여기서는 가운데 스플릿바 하나를 사이에 둔 양쪽 페인이 있는 형태이므로 그냥 OnPaint() 함수를 고쳐서 작업을 하는 것이 더 낫겠네요.
다음과 같이 OnPaint() 함수 외에 두 개의 함수를 더 추가합니다.
 
 void CMySplitterWnd::OnPaint()
{
        ASSERT_VALID(this);
        CPaintDC dc(this); // device context for painting
        // TODO: Add your message handler code here
        CRect rcClient;
        GetClientRect(&rcClient);
       
        // 종이 모양의 배경 화면을 그린다.
        OnDrawBackground(&dc, rcClient);

        // 바인더 모양의 스플릿바를 그린다.
        OnDrawBindSplitter(&dc, rcClient);

        // CSplitterWnd에 있는 함수를 사용해 테두리를 그린다.
        OnDrawSplitter(&dc, splitBorder, rcClient);
        // Do not call CSplitterWnd::OnPaint() for painting messages
}

void CMySplitterWnd::OnDrawBackground(CDC *pDC, const CRect &rectArg)
{
        pDC->FillSolidRect(rectArg, ::GetSysColor(COLOR_APPWORKSPACE));

        CRect rcPage = rectArg;
        rcPage.top += 10;
        rcPage.bottom -= 10;
        rcPage.left += 10;
        rcPage.right = rectArg.left + m_pColInfo[0].nCurSize + m_cxSplitterGap/2 - 2;
       
        // 왼쪽 패널 영역을 그린다.
        pDC->FillSolidRect(rcPage, ::GetSysColor(COLOR_WINDOW));

        rcPage.left = rcPage.right + 4;
        rcPage.right = rectArg.right - 10;
       
        // 오른쪽 패널 영역을 그린다.
        pDC->FillSolidRect(rcPage, ::GetSysColor(COLOR_WINDOW));
}

void CMySplitterWnd::OnDrawBindSplitter(CDC *pDC, const CRect &rectArg)
{
        CBitmap splitBmp;
        splitBmp.LoadBitmap(IDB_BINDSPLIT);

        CDC dcBitmap;
        dcBitmap.CreateCompatibleDC(pDC);

        CBitmap *pOldBitmap = (CBitmap *)dcBitmap.SelectObject(&splitBmp);  

        CRect rcSplit = rectArg;
        rcSplit.left += m_pColInfo[0].nCurSize;
        rcSplit.right = rcSplit.left + m_cxSplitterGap;

        // 일정한 간격을 유지하며 바인더 모양의 그림을 그린다.
        for(int y=rectArg.top+20; y<rectArg.bottom-20; y+=16) {
                rcSplit.top = y;
                rcSplit.bottom = rcSplit.top + 16;
                pDC->BitBlt(rcSplit.left, rcSplit.top, rcSplit.Width(), rcSplit.Height(),
                        &dcBitmap, 0, 0, SRCCOPY);
        }

        dcBitmap.SelectObject(pOldBitmap);
        dcBitmap.DeleteDC();

        splitBmp.DeleteObject();                
}
 
 여기까지 작업을 하고 컴파일을 해서 결과를 봅니다. 컴파일되고 나서 처음 보이는 결과는 제대로 된 것처럼 보이는군요. 스플릿바가 제대로 나타나는 것을 보면 제대로 된 것 같기도 합니다. 하지만 우선 계속 가보기로 하죠.
여기서 OnDrawBackground() 함수를 보면 다음과 같은 코드를 볼 수 있습니다. 이 코드가 의미하는 바를 한 줄씩 해석해보면...
        // 배경을 COLOR_APPWORKSPACE 색으로 그린다.
        pDC->FillSolidRect(rectArg, ::GetSysColor(COLOR_APPWORKSPACE));

        CRect rcPage = rectArg;     // 위, 아래, 왼쪽을 10 픽셀씩
        rcPage.top += 10;               // 안쪽으로 위치시킨다.
        rcPage.bottom -= 10;        
        rcPage.left += 10;
        rcPage.right = rectArg.left + m_pColInfo[0].nCurSize + m_cxSplitterGap/2 - 2;
       
        // 왼쪽 패널 영역을 그린다.
        pDC->FillSolidRect(rcPage, ::GetSysColor(COLOR_WINDOW));
 
위와 같이 배경을 그리게 됩니다. 그리고, OnDrawBindSplitter() 함수에서 스플릿바를 그리게 됩니다.
 여기까지 설명을 듣고 나서 다시 컴파일되어 실행된 프로그램을 보면, 무언가 틀린 것을 알 수 있을 겁니다. 분명 코드에서는 10 픽셀씩 안쪽으로 들어가서 종이모양의 바탕이 그려지도록 만들었는데, 실행된 코드에서는 뷰가 화면에 꽉 차 있고, 스플릿바만이 제대로 보일 뿐입니다.
 스플릿바를 클릭해 보면 10 픽셀씩 안으로 들어가 보이긴 합니다만, 스플릿바를 움직이면 다시 원래대로 꽉 찬 화면으로 돌아가는 것을 알 수 있습니다. 그 원인은 CSplitterWnd 클래스가 자신이 소유한 뷰를 스플릿바가 이동된 만큼만 다시 그리고, 자기자신을 또 다시 그리기 때문에 잠시 그렇게 보이는 것이죠.
 결국 해결책은 내부 뷰의 위치를 조절하는 방법뿐입니다.
 
7. 뷰의 위치 조정
 내부의 뷰 위치를 조절하기 위해서, CSplitterWnd 클래스의 RecalcLayout() 이라는 함수를 이용합니다. 이 함수는 분할창에서 내부의 창 크기가 변화되었을 때 창을 다시 배열하기 위해 불려지는 함수입니다. RecalcLayout() 함수는 윈도우 관리를 위해 존재하는 클래스인 CFrameWnd에서 상속받은 클래스와 CSplitterWnd에만 있는데, 자신이 소유한 윈도우를 재배치하는 역할을 하죠.
 분할 창을 이동하거나 프레임 사이즈가 변경된 경우, 동적 분할창에서처럼 새로운 뷰가 생성되는 경우 등에 불려지는 CSplitterWnd::OnRecalcLayout() 함수는 내부에 존재하는 윈도우들의 위치를 재배치하고 이동된 윈도우와 스플릿바를 다시 그립니다.
 
void CMySplitterWnd::RecalcLayout()
{
        ASSERT_VALID(this);

        CSplitterWnd::RecalcLayout();

        // 모든 패널 위치를 재배치한다.
        CRect rcClient;

        GetClientRect(&rcClient);

        CRect rcPage = rcClient;
        rcPage.top += 10;
        rcPage.bottom -= 10;
        for(int col=0; col<m_nCols; col++) {
                CWnd* pWnd = (CWnd *)GetPane(0, col);
                ASSERT_VALID(pWnd);

                if(col == 0) {
                        // 오른쪽 패널 위치조정
                        rcPage.left += 10;
                        rcPage.right = rcClient.left + m_pColInfo[col].nCurSize +
                                m_cxSplitterGap/2 - 2;
               
                } else {
                        // 왼쪽 패널 위치 조정
                        rcPage.left = rcPage.right + 4;
                        rcPage.right = rcClient.right - 10;
                }

                CRect rect = rcPage;
                rect.DeflateRect(10, 10);

                if(col == 0)
                        rect.right -= 10;
                else
                        rect.left += 10;

                pWnd->SetWindowPos(NULL, rect.left, rect.top,
                        rect.Width(), rect.Height(), SWP_NOZORDER);
        }
}
 
 지금까지 코딩을 하고 컴파일 해서 결과를 보면, 스플릿바가 약간 왼쪽으로 치우친 것을 알 수 있습니다. 움직여보거나 스플릿바를 더블클릭해보면 왼쪽으로 움직인다는 걸 알 수 있죠. 이것을 보정하기 위해 StopTracking() 함수를 추가시킵니다.
 마우스 버튼을 놓는 순간 불려지는 이 함수는, 각 창들의 변경된 크기를 계산하기 위해 TrackColumnSize() 라는 함수에 이동된 스플릿바의 왼쪽 좌표를 인자로 넣어 호출합니다. 이 함수는 이 좌표를 받아 각각의 뷰에 대한 이상적인 크기(스플릿바의 크기를 고려하지 않은 크기)를 계산하는데, 후에 RecalcLayout() 함수에서 이 이상적인 크기에서 실제크기를 구하기 위해 다시 스플릿바와 양쪽 테두리 크기만큼의 값을 감소시키므로, 결국 스플릿바의 폭만큼 왼쪽으로 이동된 위치로 모든 것이 정렬됩니다. 이러한 오차를 보상하기 위해 결국 가상함수인 StopTracking() 함수를 오버라이드해서 미리 이 값만큼을 조정해 기반 클래스의 함수를 불러주면 모든 것이 해결되죠.
 
void CMySplitterWnd::StopTracking(BOOL bAccept)
{
        ASSERT_VALID(this);

        // 스플릿바의 위치를 이동하는 중이라면 다시 되돌린다.
        if(!m_bTracking)
                return;

        if(bAccept) {
                // m_htTrack는 수직 스플릿바의 경우 201과 215사이의 값을 가짐
                if(m_htTrack >= 201 && m_htTrack <= 215)
                        m_rectTracker.OffsetRect(m_cxSplitter - m_cxBorder * 2, 0);
        }

        CSplitterWnd::StopTracking(bAccept);
}