CSplitterWnd 클래스 변화시키기

 

이 내용은 마이크로소프트웨어 1999년 6월 내용중 "원리구현, UI를 강화하면 컴퓨터가 즐겁다[1], 특수한 UI를 적용한 스플리터 만들기"의 내용을 참고한 것입니다.

 이 글에서 소개한 예제의 소스         원본 글의 예제소스

1. CSplitterWnd 클래스란?

 CSplitterWnd 클래스는 SDI에서 한번에 둘 이상의 뷰를 보여주는 것을 가능하게 하는 클래스입니다. MDI의 경우 child 윈도우를 여러개 만들어서 둘 이상의 뷰를 보여주지만, CSplitterWnd 클래스를 사용하면 한 화면을 두 개 내지는 그 이상 분할해서 한꺼번에 보여줍니다.

 

2. CSplitterWnd 의 파생클래스 만들기

 CSplitterWnd를 변화시키려면 그 클래스에서 상속받은 새로운 클래스를 만들어야 합니다. 이건 뷰나 도큐먼트, 메인 프레임 클래스도 마찬가지죠.

 클래스뷰에서 생성하면 되는데, 조금 문제가 있습니다. 바로 클래스 위저드에서 지원하는 베이스 클래스에는 CSplitterWnd 는 빠져있기 때문입니다.

 물론 이 경우에도 방법은 있습니다. 몇가지가 있는데요. 하나씩 장단점과 함께 소개하기로 하죠.

 

3. 정적 스플리트 만들기

여기서는 동적 스플리트를 정적 스플리트로 바꾸어 보겠습니다.

 

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);
}

 

- the end of this article -