. MDI와 정렬 생략

당근은 한 번에 여러 개의 문서를 동시에 편집할 수 있는 MDI 프로그램이다. 그래서 전체적인 구조가 MDI로 되어 있는데, MDI는 시스템이 제공하는 편리한 기능인 반면 몇 가지 문제점이 있다. 아무리 잘 만든 루틴이라 하더라도 모든 경우에 다 문제없이 적용되기는 무척 어려운데 시스템의 MDI 기능과 당근과의 문제점을 점검해보고 해결하도록 하자. 여기서 점검하는 문제점은 프로그램의 동작을 자세히 관찰하지 않으면 잘 보이지 않는다.

보통의 경우는 잘 보이지 않지만 아주 큰 파일을 편집할 때는 문제점이 확실히 드러난다. 용량이 아주 큰 파일 두 개를 최대화 상태로 동시에 열어 놓고 <Ctrl+Tab>으로 스위칭을 해보자. 증세를 분명히 보기 위해서는 가급적이면 큰 파일을 여는 것이 좋으며 자동개행 옵션을 선택해놓는다. 이때 MDI는 다음 두 가지 단계를 통해 자식창을 스위칭한다.

 

새로 활성화되는 창이 최대화된다.

비활성화되는 창이 노말 상태가 된다.

 

최대화 상태에서 창을 스위칭할 때 두 개의 윈도우 크기가 모두 변하는데 비활성화되는 창이 굳이 노말 상태로 다시 돌아갈 필요가 없음에도 불구하고 그렇게 되어 있다. 그래서 스위칭이 발생할 때 두 윈도우 모두 재정렬되며 따라서 스위칭 속도가 떨어진다. 비활성화되는 창은 보이지 않는 뒤쪽으로 이동하므로 당장 재정렬할 필요가 없지만 WM_SIZE 메시지가 발생하므로 불필요한 정렬을 하게 되는 것이다. 이번에는 최대화된 상태에서 새로운 창을 만들 때를 보자. 이때는 다음 두 동작으로 창을 생성한다.

 

활성창이 노말 상태가 된다.

새로 생성되는 창이 최대화된다.

 

새로운 창을 만들면 이 창이 최대화되어 기존의 열려 있던 창을 덮어버리므로 굳이 활성창을 노말 상태로 만들 필요가 없음에도 그렇게 하고 있다. 따라서 이 경우도 번의 재정렬은 당장 필요치 않는 동작이며 스위칭 속도를 떨어뜨리는 요인이 된다. 문제를 요약하자면 최대화된 창이 비활성화될 때 불필요하게 윈도우 크기가 변경되며 그래서 재정렬이 한 번 더 필요하다는 점이다.

최대화된 창이 비활성화될 때 노말 상태로 미리 만들지 말고 최대화 상태의 크기와 동일한 크기를 유지한다면 다음 스위칭할 때도 재정렬을 할 필요가 없으므로 스위칭 속도는 아주 빨라질 것이다. 당근이 바라는 바는 이런 스위칭 방식이나 윈도우즈의 MDI는 이렇게 동작하지 않는다. MDI가 이런 식으로 자식창을 관리하는 것은 엄밀히 말해 버그는 아니다. 당근이 바라는 대로 동작한다면 최대화가 풀릴 때 열린 모든 창의 크기를 한꺼번에 바꾸어야 하는 문제가 있기 때문에 스위칭할 때마다 자식창의 크기를 조정하는 정책을 취하고 있는 것뿐이다.

문제의 핵심은 MDI가 자식창을 관리하는 방법에 있는 것이 아니라 노말 상태를 지원한다는 점이다. 아예 노말 상태라는 것이 없다면 모든 창은 MDI 클라이언트와 똑같은 크기를 가질 것이고 스위칭할 때 재정렬을 할 필요가 없으므로 속도가 아주 빨라질 것이다. 비주얼 스튜디오 7.0이 정확하게 이 방식대로 동작하고 있다.

이 문제를 해결하려면 스위칭될 때 불필요한 정렬을 최대한 막아야 하는데 그 방법이 그리 쉽지 않다. 일단 다음 전역변수를 하나 선언한다.

 

BOOL NewPhase;

 

이 변수는 New 함수와 WM_SIZE와의 통신에 사용되는데 이 값이 TRUE이면 새로 창이 만들어지고 있는 중이라는 뜻이다. New 함수에서 새로 창을 만들기 전에 이 변수에 값을 적절히 설정하며 WM_SIZE에서는 이 변수값을 참고하여 불필요한 정렬을 생략한다.

 

HWND New()

{

     MDICREATESTRUCT mcs;

     HWND hActive, hChild;

     BOOL bMax;

 

     hActive=(HWND)SendMessage(g_hMDIClient, WM_MDIGETACTIVE,0,(LPARAM)&bMax);

 

     g_NewNo++;

     mcs.szClass="DGChild";

     mcs.szTitle="";

     mcs.hOwner=g_hInst;

     mcs.x=mcs.y=CW_USEDEFAULT;

     mcs.cx=mcs.cy=CW_USEDEFAULT;

     mcs.style=MDIS_ALLCHILDSTYLES | WS_CLIPCHILDREN;

     if ((hActive == NULL && Option.bMaxFirstChild==TRUE) || bMax == TRUE) {

           mcs.style |= WS_MAXIMIZE;

     }

    if (hActive && bMax==TRUE) {

        NewPhase=TRUE;

    } else {

        NewPhase=FALSE;

    }

    hChild=(HWND)SendMessage(g_hMDIClient, WM_MDICREATE, 0,

        (LPARAM)(LPMDICREATESTRUCT)&mcs);

    NewPhase=FALSE;

    return hChild;

}

 

활성창이 있고 최대화된 상태이면 NewPhase TRUE로 변경해놓고 새 창을 만든다. 새 창을 만든 후에 NewPhase는 다시 FALSE가 되는데 따라서 이 변수는 새 창을 만드는 순간에만 TRUE가 된다. 문서창의 윈도우 프로시저는 다음과 같이 수정한다.

 

LRESULT CALLBACK DGChildProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

    HWND hActive;

    BOOL bMax;

    RECT crt,art;

 

     switch(iMessage) {

     case WM_CREATE:

          ....

     case WM_COMMAND:

          ....

     case WM_SIZE:

        hActive=(HWND)SendMessage(g_hMDIClient, WM_MDIGETACTIVE,0,(LPARAM)&bMax);

          pSi=(SInfo *)GetWindowLong(hWnd,0);

        if (bMax==FALSE || (hActive == hWnd && NewPhase==FALSE)) {

           MoveWindow(pSi->Ae.hWnd,0,0,LOWORD(lParam),HIWORD(lParam),TRUE);

        }

          break;

     case WM_SETFOCUS:

          pSi=(SInfo *)GetWindowLong(hWnd,0);

        GetClientRect(hWnd,&crt);

        GetWindowRect(pSi->Ae.hWnd,&art);

        if (art.right-art.left != crt.right-crt.left) {

           MoveWindow(pSi->Ae.hWnd,0,0,crt.right,crt.bottom,TRUE);

        }

          SetFocus(pSi->Ae.hWnd);

          SetStatusText(0xffff);

          idx=FindFileTab(pSi->NowFile);

          TabCtrl_SetCurSel(hFileTab,idx);

          return 0;

 

문서창의 WM_SIZE에서는 ApiEdit 컨트롤의 크기를 작업영역 크기로 변경하고 있는데 이때 ApiEdit는 자신의 크기가 변했으므로 재정렬을 할 것이다. 이 코드를 수정하여 문서창의 크기가 변했다고 해서 ApiEdit의 크기를 무조건 같이 변경하는 것이 아니라 재정렬이 필요할 때만 ApiEdit의 크기를 변경하도록 수정하였다.

재정렬이 필요한가 아닌가는 활성창의 상태와 NewPhase변수값에 따라 달라진다. 활성창이 최대화 상태가 아니면, 즉 노말 상태일 때는 무조건 재정렬한다. 노말 상태에서 윈도우 크기가 변경되었다는 것은 사용자가 직접 문서창의 크기를 조정하고 있다는 뜻이다. 활성창이 최대화되어 있을 때는 활성창이 자기 자신이고 새로운 창이 만들어지고 있는 중이 아닐 때만 재정렬한다. 이 조건은 좀 해부해서 살펴 볼 필요가 있다.

활성창이 최대화되어 있기는 하지만 자기 자신이 활성창이 아니라는 조건은 앞에서 예로든 번 경우이다. 즉 활성창이 비활성화되면서 노말 상태로 돌아가고 있는 중이며 이때는 재정렬할 필요가 없다. 자기 자신이 활성화되어 있지만 새로운 창이 만들어지고 있는 상태라는 조건은 앞에서 예로든 번 경우이다. 즉 새로운 창이 생성됨에 따라 활성창이 노말 상태로 돌아가고 있는 중이며 이때도 재정렬할 필요가 없다.

WM_SIZE에서 이런 식으로 꼭 필요할 때만 재정렬을 하게 되면 최대화 상태에서 노말 상태로 돌아갈 때 ApiEdit의 크기가 변경되지 않으므로 스위칭 속도가 빨라진다. 뿐만 아니라 이 상태에서 비활성화된 창이 다시 최대화될 때 재정렬하지 않아도 되는데 ApiEdit의 크기가 최대화 상태일 때 크기를 그대로 유지하고 있기 때문이다. 결국 비활성창의 ApiEdit는 작업영역 크기와 맞지 않는 상태이더라도 그냥 내버려 두는 것이다. 하지만 이렇게 되면 갑자기 노말 상태로 돌아간다거나 메인 윈도우의 크기를 바꾼 후 스위칭을 할 때 문제가 된다.

그래서 문서창이 포커스를 받을 때, WM_SETFOCUS 메시지를 받을 때 이 창이 재정렬이 필요한가 아닌가를 보고 이때 재정렬을 한 번 더 하도록 하였다. 이 판단은 아주 간단하게 할 수 있는데 문서창의 작업영역 폭과 ApiEdit의 윈도우 폭을 비교해보고 두 폭이 일치하지 않으면 재정렬이 필요하다는 뜻이다. 이때 ApiEdit를 문서창의 작업영역 폭에 맞추어주면 ApiEdit WM_SIZE 메시지가 전달되고 사용자 눈에 보이기 전에 재정렬을 마칠 것이다.

MDI는 이 문제 외에도 XP에서 스위칭시에 다른 문제가 또 있다. XP에서는 문서창을 스위칭할 때 새로 활성화되는 문서창의 테두리가 먼저 그려진 후 작업영역이 그려지기 때문에 깜박거림이 무척 심하다. XP의 디폴트 테마는 표준 윈도우즈의 타이틀바나 경계선이 상당히 예쁘게 장식되어 있는데 이 경계선을 그리는데 시간이 많이 걸리므로 스위칭되는 것이 보인다. 이는 XP MDI의 문제라기 보다는 XP의 테마 버그인 듯 하다.

이 문제를 해결하려면 MDI 클라이언트를 서브클래싱하여 스위칭될 때의 처리를 수정하면 될 듯하나 무리하게 수정하려고 하지 않았다. 왜냐하면 당근은 장기적으로 MDI 인터페이스를 탈피할 계획을 가지고 있으며 노말 상태를 지원하지 않는 새로운 인터페이스를 직접 만들어 쓸 계획을 가지고 있다. 그래서 이 문제는 당분간 노운 버그로 남겨둘 것이다.