MFC로 '자동 들여쓰기' 노트패드 만들기

 

 역시 마이크로소프트 97년 3월호에 실린 내용입니다. 예제 프로젝트는 여기서..

 현재 커서의 라인과 컬럼, 수정된 문서 표시 기능, 자동 들여쓰기를 구현한 간단한 노트패드를 만들어 보기로  하지요. CWinApp의 OnIdle 함수를 오버라이드해서 위의 기능들을 구현한 겁니다. 다음은 본문 내용 중 일부를 인용한 겁니다.

 처리할 메시지가 없을 때 (예 : 메시지 큐가 비어있을 때) 프레임웍은 CWinApp의 멤버함수인 OnIdle()을 호출하기 때문에 백그라운드 작업을 수행하려면 이 함수를 오버라이드하면 된다. 기본적으로 OnIdle 함수는 툴바 버튼이나 메뉴 등 사용자 인터페이스에 관련된 객체 상태를 갱신하거나, 프레임웍이 생성한 임시 객체를 지우는 등 이것저것을 한다. 이때 주의할 점은 너무 긴 시간을 요하는 작업을 Idle 타임에 처리하면 절대 안된다는 것. Idle 타임 프로세싱이 끝날 때까지 프로그램은 아무 일도 할 수 없기 때문이다. 그렇게 되면 주객이 전도된 셈이 되고 만다.

~~~~~~~~~~~

 UI (User Interface)부분에서 Idle 타임 프로세싱을 응용할 수 있는 분야가 아주 많다. 가령 시계와 같이 프로그램이 실행되는 동안 지속적으로 상태를 갱신해 주어야 하는 작업에 유용하게 사용할 수 있으며, 각 개인의 창의력에 따라서 더 강력한 기능을 구사할 수 있다.

 

그럼 이제 해야 할 일에 대해 말해 드리죠.

1. MDI로 프로젝트를 만들되 AppWizard 마지막 단계에서 CViewCEditView로 바꾸어 주어야 합니다. 이렇게만 하고 컴파일을 해도 기본적인 에디터 기능은 가지고 있죠. 여기서 프로젝트 이름은 Pad 로 했습니다.

2. MainFrm.cpp에서 다음 부분을 찾아 굵은 글씨 부분을 추가합니다. 여기서 하는 작업은 상태바에 pane을 하나 추가하는 것이고, 여기서 라인과 컬럼을 출력합니다.

static UINT indicators[] =
{
        ID_SEPARATOR,           // status line indicator
        ID_INDICATOR_INFO,
        ID_INDICATOR_CAPS,
        ID_INDICATOR_NUM,
        ID_INDICATOR_SCRL,
};

3. MainFrm.cpp 파일의 다음부분도 고쳐줍니다. 아래의 굵은 글씨 부분 한 줄만 추가하면 됩니다.

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
        if (CMDIFrameWnd::OnCreate(lpCreateStruct) == -1)
                return -1;
        
        if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP
                | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
                !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
        {
                TRACE0("Failed to create toolbar\n");
                return -1;      // fail to create
        }

        if (!m_wndStatusBar.Create(this) ||
                !m_wndStatusBar.SetIndicators(indicators,
                  sizeof(indicators)/sizeof(UINT)))
        {
                TRACE0("Failed to create status bar\n");
                return -1;      // fail to create
        }

        m_wndStatusBar.SetPaneInfo(1, ID_INDICATOR_INFO, SBPS_NORMAL, 100);

        // TODO: Delete these three lines if you don't want the toolbar to
        //  be dockable
        m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
        EnableDocking(CBRS_ALIGN_ANY);
        DockControlBar(&m_wndToolBar);

        return 0;
}

4. 위의 2, 3 번에서 보면 ID_INDICATOR_INFO를 아무데도 정의도 하지 않고 그냥 써놨죠? 그럼 정의를 해주지요 뭐.. 리소스 뷰에서 String Table을 열어봅니다. 거기다 다음 내용을 추가하는 거죠.

      ID : ID_INDICATOR_INFO
      Caption : Ln 1, Col 1

Value 는 적당히 알아서 정해질 겁니다. 다른 id와 중복되지 않는 적당한 값 아무거나 상관은 없지요.

5. 뷰의 OnKeyDown을 오버라이드 합니다. 이 함수를 고치는 이유는 뷰에서 엔터가 눌렸을 때 자동 들여쓰기를 하도록 해주기 위해서이죠.

void CPadView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
        // TODO: Add your message handler code here and/or call default
        if(nChar == VK_RETURN) { // 엔터키가 눌려졌을 때
                ((CPadApp *)AfxGetApp())->m_bTabFlag = TRUE;
        }

                
        CEditView::OnKeyDown(nChar, nRepCnt, nFlags);
}

6. 애플리케이션 클래스에 다음 두 변수를 public 속성으로 정의합니다. m_nTabCount 는 현재 라인의 탭 수를 가지고 있는 변수이고, m_nTabFlag 는 엔터키가 눌려졌는지의 여부를 저장하는 플래그 변수입니다.

public:
        int m_nTabCount;
        BOOL m_bTabFlag;

7. 마지막으로 가장 핵심이 되는 OnIdle 함수 부분입니다.

BOOL CPadApp::OnIdle(LONG lCount)
{
        // TODO: Add your specialized code here and/or call the base class
        static CMainFrame*  pFrame = (CMainFrame*)AfxGetMainWnd();  // 프레임
        static CStatusBar*  pStatusBar =  // 상태바에 대한 포인터(CStatusBar*)pFrame->GetDescendantWindow(AFX_IDW_STATUS_BAR);  
        static CChildFrame* pChild;  // 차일드 프레임에 대한 포인터
        static CPadView*    pView;   // 뷰에 대한 포인터
        static CEdit*           pEdit;   // 에디트 컨트롤에 대한 포인터
        
        static CString str;       
        static int posStart;     // Sel의 시작 글자 위치
        static int posEnd;      // Sel의 끝 글자 위치
        static int posFirst;     // 라인의 첫 글자 위치
        static int posPrev;     // 이전 글자 위치, 문자열이 선택된 경우
                                       // 왼쪽 방향인지 오른쪽 방향인지 알기위해 필요  
        static int nCurLine;   // 현재 라인 수
        static int nCurCol;    // 현재 컬럼 수
        static char buf[1024];

        int i;
        int n;

        pChild  = (CChildFrame*)pFrame->GetActiveFrame();
        pView   = (CPadView*)pChild->GetActiveView();
        pEdit   = &(pView->GetEditCtrl());

        switch(lCount)
        {
        case 0:
        case 1:
                
                CWinApp::OnIdle(lCount);
                return TRUE;

        case 2:  // 상태바에 라인 수와 컬럼 수를 표시한다.

                if(!pStatusBar) return TRUE;
                if(!pEdit)
                {
                        str.Format("Ln ??, Col ??");
                        pStatusBar->SetPaneText(1, str);
                        
                        return TRUE;
                }
                
                pEdit->GetSel(posStart, posEnd);
                if(posStart == posEnd)        // 문자열이 선택되지 않았으면...
                {
                        posFirst = pEdit->LineIndex();
                        nCurLine = pEdit->LineFromChar() + 1;
                        nCurCol = posStart - posFirst + 1;
                        posPrev = posStart;
                }
                else                                  // 문자열이 선택돼 있으면
                {
                        if(posStart < posPrev)    // 왼쪽 방향으로 선택했으면
                        {
                                nCurLine = pEdit->LineFromChar(posStart) + 1;
                                posFirst = pEdit->LineIndex(nCurLine - 1);
                                nCurCol = posStart - posFirst + 1;    // 현재 컬럼은
                        }
// 시작 문자 위치
                        else     // 오른쪽 방향으로 선택했으면
                        {
                                nCurLine = pEdit->LineFromChar(posEnd) + 1;
                                posFirst = pEdit->LineIndex(nCurLine - 1);
                                nCurCol = posEnd - posFirst + 1;      // 현재 컬럼은
                        }
// 끝 문자 위치
                }
                
                str.Format("Ln %d, Col %d", nCurLine, nCurCol);
                pStatusBar->SetPaneText(1, str);
                // 상태바에 현재 라인, 컬럼 표시
                return TRUE;
        
        case 3:     // 자동 들여쓰기
                
                if(!pEdit) return TRUE;

                pEdit->GetSel(posStart, posEnd);
                posFirst = pEdit->LineIndex();

                if((m_bTabFlag == TRUE) && (posStart == posFirst))
                {
                        for(i = 0; i < m_nTabCount; i++)    // 이전 라인의 탭 수만큼
                        {                                               // 탭을 채운다.
                                pEdit->ReplaceSel("\t");
                        }
                        m_nTabCount = 0;
                        m_bTabFlag = FALSE;               // 다시 플래그를 거짓으로
                }
                else       // 탭 플래그가 거짓이면 현재 라인을 읽어와서
                {           // 앞의 탭 문자가 몇 개 인지를 계산한다.
                        n = pEdit->GetLine(pEdit->LineFromChar(), buf, 1024);
                        i = 0;
                        while(i < n && buf[i] == '\t') i++;
                        
                        m_nTabCount = i;
                }

                return TRUE;

        case 4:     // 타이틀에 '*' 표시
                
                if(!pEdit) return FALSE;

                if(pEdit->GetModify())       // 문서가 변경되었으면
                {
                        str = pView->GetDocument()->GetTitle();
                        if(str.Right(1) != '*')  // '*' 표시가 되어있지 않으면
                        {
                                str += '*';
                                pView->GetDocument()->SetTitle(str);
                        }
                }
                
                return FALSE;   // 더 이상 Idle 타임 작업이 없음
        }

        return FALSE;
}