. 컨트롤화 후 추가 작업

컨트롤화가 된 ApiEditTest 예제를 실행해보자.

보다시피 타이틀바의 캡션이 바뀌었을 뿐이며 컨트롤화를 하기 전과 외형상으로는 전혀 달라진 것이 없다. ApiEdit에 있던 기능이 없어진 것도 아니고 없던 기능이 새로 생긴 것도 아니며 완전히 동일하다. 하지만 이 프로젝트는 메인 윈도우가 따로 있고 ApiEdit는 컨트롤로 존재하기 때문에 얼마든지 컨트롤의 배치상태를 바꿀 수 있으며 ApiEdit를 둘 이상 생성하는 것도 가능해졌다.

메인 윈도우였던 ApiEdit가 차일드 컨트롤이 되면 몇 가지 달라져야 할 부분이 있는데 이 점은 앞에서도 이미 설명을 했었다. 또한 꼭 바꾸지 않아도 상관없지만 호환성이나 자원 절약을 위해 약간의 코드 수정을 하도록 하자. 왜 이런 수정이 필요한지는 상식적으로 쉽게 이해될 것이다.

포커스 이동 및 종료 처리

마우스 왼쪽 버튼이 클릭됐을 때 SetFocus로 포커스를 가져와야 하고 WM_DESTROY 메시지를 받았을 때 PostQuitMessage를 호출해서는 안된다. OnLButtonDown 함수의 제일 처음에 SetFocus 함수 호출문을 추가하고 OnDestroy에 있는 PostQuitMessage 함수는 삭제하도록 하자. 컨트롤은 파괴될 때 자기 혼자만 죽어야지 응용 프로그램을 종료해서는 안된다.

 

void CApiEdit::OnLButtonDown(HWND hWnd, BOOL fDoubleClick, int x, int y, UINT keyFlags)

{

     bShift=((GetKeyState(VK_SHIFT) & 0x8000) != 0);

     bControl=((GetKeyState(VK_CONTROL) & 0x8000) != 0);

 

    SetFocus(hWnd);

     ....

}

 

최소폭 보장

컨트롤이 되고 난 후 또 달라져야 할 중요한 부분이 있는데 정렬루틴에 약간의 예외 처리가 필요하다. 메인 윈도우일 때는 타이틀바의 버튼들 때문에 윈도우의 최소폭이 보장되었지만 컨트롤은 그런 것이 없다. 메인 윈도우가 SetWindowPos MoveWindow 함수로 ApiEdit를 아주 작게 만들어버리면 정확하게 지정한 크기대로 바뀌어야 한다.

ApiEdit의 폭이 극단적으로 작아지면 폭이 0인 상태가 될 수도 있으며 이렇게 되면 한 줄에 단 한 글자도 못 들어가기 때문에 문서의 길이는 무한대가 되어 버릴 것이다. 그래서 문서를 정렬하는 GetLine 함수에 다음 예외 처리가 필요하다.

 

void CApiEdit::GetLine(int Line, int &s, int &e)

{

     ....

           if (acwidth > max(frt.right-2, MarginWidth+FontHeight*4)) {

                   break;

              }

     ....

 

원래 코드는 if (acwidth > frt.right-2)였으나 frt의 폭이 0이 될 수도 있기 때문에 최소값으로 폰트 높이의 네 배 정도를 주었다. , 아무리 윈도우 폭이 작아도 최소한 네 글자는 들어갈 수 있도록 해 줌으로써 문서 길이가 무한대가 되지 않도록 하였다. 이 처리를 하지 않으면 정렬에 실패하게 된다.

대화상자 지원

대화상자에서 컨트롤은 모든 키입력을 받을 수 없다. 왜냐하면 대화상자는 컨트롤보다 우선해서 몇 가지 키를 독점 처리하기 때문이다. 예를 들어 <Enter>키는 포커스에 상관없이 항상 디폴트 버튼을 클릭하는 것으로 되어 있고 <Esc>는 취소 버튼을 클릭하여 대화상자가 닫히도록 한다. 또한 <Tab>키는 컨트롤간의 포커스를 이동시키며 커서이동키는 그룹 내에서 컨트롤 사이를 이동하는 기능이 있다. 그래서 컨트롤이 아무리 키입력을 받고자 해도 시스템에 있는 대화상자의 윈도우 프로시저(대화상자 프로시저가 아님)에서 이 키를 가로채기 때문에 키입력을 받을 수 없다.

이런 식이라면 ApiEdit가 대화상자에 배치될 때는 <Enter>키로 개행을 할 수 없고 <Tab>키로 탭문자를 삽입하는 것도 불가능하며 키보드로 캐럿을 이동할 수도 없을 것이다. 시스템은 이런 상황에 대해서 물론 해결책을 제시한다. 대화상자는 키입력을 받았을  때 이 키를 무조건 디폴트 처리하지 않으며 먼저 포커스를 가진 컨트롤에게 이 키를 어떻게 처리할 것인가 질문을 하는데 이 질문이 바로 WM_GETDLGCODE 메시지이다.

즉 대화상자는 모든 키입력에 대해 포커스를 가진 컨트롤에게 메시지를 보내 우선권을 주도록 되어 있다. 이 메시지를 처리하지 않거나 0을 리턴하면 모든 키는 대화상자의 디폴트 정의대로 동작한다. 그렇게 하지 않으려면 이 메시지에 응답하여 어떤 키를 원한다는 적극적인 의사 표시를 해야 한다.

ApiEdit에서 특히 문제가 되는 키는 탭키이다. 탭으로 탭문자를 삽입할 것인지 아니면 다음 컨트롤에게 제어를 넘길 것인지를 결정해야 하는데 이는 ApiEdit를 사용하는 호스트가 결정할 일이다. 따라서 탭키입력 처리를 어떻게 할 것인가는 일종의 컨트롤 스타일값으로 정의되어야 하며 호스트가 이 스타일을 바꿀 수 있도록 해야 한다. CApiEdit에 다음 변수를 추가하고 일단 TRUE로 초기화하도록 하자.

 

class CApiEdit

{

private:

     ....

     BOOL bWantTab;

 

BOOL CApiEdit::OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

{

     ....

     bWantTab=TRUE;

 

컨트롤의 디폴트는 탭을 자기가 직접 처리하는 것으로 정의한다. 외부에서 bWantTab FALSE로 바꾸면 탭키입력을 처리하지 않고 부모 윈도우에게 양보하게 될 것이다. OnGetDlgCode 함수에서는 이 값에 따라 탭키를 직접 처리할 것인지 아니면 대화상자에게 넘길 것인지를 결정한다.

 

UINT CApiEdit::OnGetDlgCode(HWND hWnd, LPMSG lpmsg)

{

     if (lpmsg) {

          if (lpmsg->message == WM_KEYDOWN && lpmsg->wParam==‘\t’ && bWantTab==FALSE)

              return 0;

     }

     return DLGC_WANTARROWS | DLGC_WANTTAB | DLGC_WANTALLKEYS | DLGC_WANTCHARS;

}

 

탭키가 눌러졌을 때 bWantTab 변수의 값이 FALSE이면 0을 리턴하여 이 키입력을 대화상자가 알아서 처리하도록 했다. 오버랩드 윈도우의 차일드로 생성될 때는 모든 키입력이 포커스를 가진 컨트롤에게 바로 전달되므로 이 코드가 필요없으며 대화상자 내에서 사용될 때만 필요하다. 오버랩드 윈도우는 차일드에게 이 메시지를 보내지도 않는다.

정적 변수 제거

ApiEdit 4개의 정적 변수를 사용하고 있다. 정적 변수란 함수 내부에서 정의되는 지역변수이지만 항상 그 값을 유지하는 변수이다. static 키워드로 검색해보면 IsDelimiter, DisplayTab에 두 개의 배열이 있고 OnHScroll, OnMouseWheel에 각각 한 개씩 총 4개의 정적 변수가 사용되고 있다. ApiEdit가 하나밖에 실행되지 않을 때는 이 정적 변수들이 제대로 값을 기억하지만 복수 개의 인스턴스가 생기면 함수가 유지하는 정적 변수는 고유의 값을 유지할 수가 없다.

왜냐하면 멤버함수는 모든 인스턴스에 의해 공유되는 것이며 따라서 함수의 정적 변수도 모든 인스턴스가 공유하기 때문이다. 그래서 이 정적 변수들이 각각의 인스턴스에 대해 고유한 값을 저장할 수 있도록 멤버변수로 만들어야 한다. , IsDelimiter, DisplayTab에 있는 정적배열은 읽기전용이므로 그대로 두어도 상관없으며 OnHScroll, OnMouseWheel에 선언된 두 변수는 삭제하고 다음과 같이 클래스의 멤버로 포함시킨다.

 

class CApiEdit

{

     ....

     int SumDelta;

     BOOL bHideCaret;

 

이 두 값은 OnCreate에서 다음과 같이 초기화한다.

 

BOOL CApiEdit::OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct)

{

     ....

     SumDelta=0;

     bHideCaret=FALSE;

 

     return TRUE;

}

 

이 두 변수가 정적 변수로 선언되는 것과 멤버로 선언되는 것의 차이는 아주 미미해서 거의 느끼기 어렵다. 더구나 이 변수들이 기억하는 정보가 그다지 중요한 정보가 아니므로 값이 파괴되더라도 큰 문제가 발생하는 것은 아니다. 하지만 컨트롤의 완성도를 높이고 극단적으로 재수없는 경우를 방지하기 위해 멤버로 포함시키는 것이 안전하다.

리소스 정의

컨트롤이 됨으로써 인스턴스간의 구분을 위해 멤버에 새로 소속되어야 하는 변수가 있다면 반대로 복수 개의 인스턴스가 공유하기 위해 멤버에서 제외되어야 하는 변수도 있다. 인스턴스별로 다른 값을 가지지 않고 항상 동일한 값을 가지는 멤버는 정적 멤버로 선언함으로써 클래스의 크기를 작게 만들 수 있는데 커서 핸들이 그렇다. ApiEdit가 아무리 많이 만들어져도 각각 다른 커서를 사용하는 것이 아니므로 이 핸들을 개별 인스턴스가 따로 가지고 있을 필요가 없는 것이다. 물론 따로 핸들을 보유한다고 해서 문제가 되는 것은 아니지만 분명히 리소스와 메모리가 낭비될 것이다.

그래서 커서 핸들은 딱 한 번만 선언하고 읽기도 딱 한 번만 읽고 이후부터 생성되는 모든 ApiEdit가 이 핸들을 공유하면 된다. 그러면서도 이 핸들은 CApiEdit에 통합되어 있어야 하는데 이런 장치가 바로 정적 멤버, 정적 함수이다. 커서 외에 줄번호 출력에 사용되는 폰트도 모든 인스턴스가 공유할 수 있는 자원이다. CApiEdit의 커서 핸들 전부와 줄번호 폰트 핸들을 static으로 변경하고 이 핸들을 초기화 및 해제하는 LoadAeResource, UnLoadAeResource 정적 함수를 추가한다.

 

class CApiEdit

{

private:

     static HFONT hLineNumFont;

     static HCURSOR hCSel,hCCopy,hCMove,hCMargin,hCNoDrop;

     ....

public:

     static void LoadAeResource();

     static void UnLoadAeResource();

 

정적 멤버변수는 외부에서 한 번 더 선언해야 실제 메모리가 할당되므로 ApiEdit.cpp에서 이 멤버들을 다시 선언한다. 정적 멤버함수는 일반함수처럼 코드를 작성하면 된다.

 

HCURSOR CApiEdit::hCSel;

HCURSOR CApiEdit::hCCopy;

HCURSOR CApiEdit::hCMove;

HCURSOR CApiEdit::hCMargin;

HCURSOR CApiEdit::hCNoDrop;

HFONT CApiEdit::hLineNumFont;

 

void CApiEdit::LoadAeResource()

{

     hCSel=LoadCursor(NULL,IDC_ARROW);

     hCNoDrop=LoadCursor(NULL,IDC_NO);

     hCCopy=LoadCursor(GetModuleHandle(NULL),"ApiEditCopy");

     if (hCCopy==NULL)

          hCCopy=LoadCursor(NULL,IDC_APPSTARTING);

     hCMove=LoadCursor(GetModuleHandle(NULL),"ApiEditMove");

     if (hCMove==NULL)

          hCMove=LoadCursor(NULL,IDC_ARROW);

     hCMargin=LoadCursor(GetModuleHandle(NULL),"ApiEditMargin");

     if (hCMargin==NULL)

          hCMargin=LoadCursor(NULL,IDC_SIZENESW);

     hLineNumFont=CreateFont(12,0,0,0,0,0,0,0,HANGEUL_CHARSET,3,2,1,

          VARIABLE_PITCH | FF_MODERN,"굴림");

}

 

void CApiEdit::UnLoadAeResource()

{

     DeleteObject(hLineNumFont);

}

 

LoadAeResource 함수가 호출되면 정적 멤버변수에 커서 핸들이 읽혀지고 줄번호 출력에 사용될 폰트가 만들어질 것이다. UnLoadAeResource 함수는 공유되는 자원을 해제하는데 모든 ApiEdit가 파괴된 후에 호출하면 된다. 커서 핸들은 일부러 해제할 필요가 없으므로 줄번호 출력용 폰트만 삭제하도록 하자. 리소스를 읽어오는 별도의 함수가 만들어졌으므로 OnCreate에 있는 커서, 폰트 초기화 문장은 삭제한다. 또한 OnDestroy에 있는 폰트 해제문도 이제 삭제해야 한다.

공유 자원을 초기화하는 함수는 호스트가 ApiEdit를 사용하기 전에 먼저 호출해야 하며 또한 해제하는 함수는 ApiEdit 사용을 완전히 마친 후에 호출해야 한다. ApiEdit 스스로 이 공유 자원을 초기화할 수는 없으므로 호스트가 공유 자원의 초기화와 할당에 신경을 써야 하는 부담이 있다. 하지만 호스트에게 이런 부담을 지우는 것은 별로 좋은 방법이 아니므로 다른 방법을 사용하도록 하자. 이 예제의 경우는 RegisterHelper 클래스라는 전역 유일 도우미 객체가 있으므로 이 객체의 생성자에서 딱 한 번만 공유 자원을 초기화하고 파괴자에서 해제하면 된다.

 

CRegisterHelper::CRegisterHelper()

{

     WNDCLASS WndClass;

 

     ....

    CApiEdit::LoadAeResource();

}

 

CRegisterHelper::~CRegisterHelper()

{

     free(arObj);

     arObj=NULL;

    CApiEdit::UnLoadAeResource();

}

 

정적 멤버함수는 객체가 생성되기도 전에 호출할 수 있으므로 클래스 이름으로 호출 가능하다. 이제 이 함수는 ApiEdit 컨트롤이 생성되기 전에 먼저 호출되어 커서 핸들을 미리 읽어 놓게 된다. 각 커서의 핸들을 개별 객체가 가지지 않으므로 메모리가 절약되고 여러 번 읽지도 않으므로 리소스도 아낄 수 있고 여러 모로 좋다.

커서 핸들은 공유 자원으로 깔끔하게 처리했는데 아직 커서를 프로젝트에 포함시키지는 않았다. ApiEditTest.rc를 새로 추가하고 ApiEdit9에 있는 세 개의 커서 파일을 복사해 온다. 커서를 임포트한 후 ID를 문자열로 바꿔 주되 직접 리소스 스크립트를 열어서 편집하는 것이 더 편리하다. RC 파일에 다음 리소스 정의문을 추가한다.

 

ApiEditMove             CURSOR  DISCARDABLE     "ApiEditMove.cur"

ApiEditCopy             CURSOR  DISCARDABLE     "ApiEditCopy.cur"

ApiEditMargin           CURSOR  DISCARDABLE     "ApiEditMargin.cur"

 

이제 실행해보면 커서가 제대로 보일 것이다. 이 실습은 정적 멤버 사용의 가장 전형적인 예에 해당한다.

생성자 초기화

오브젝트가 됨으로써 생긴 중요한 변화는 생성자가 있다는 점이다. 생성자는 객체가 만들어질 때 호출되며 멤버들을 초기화하는 중요한 역할을 하는데 지금까지 이런 역할은 OnCreate가 해 왔다. 하지만 이제 생성자가 있으므로 일부 초기화 코드는 생성자로 옮기는 것이 논리적으로 맞다. 생성자에 다음 코드를 작성한다.

 

CApiEdit::CApiEdit()

{

     arHanWidth=NULL;

     buf=NULL;

     pLine=NULL;

     hBit=NULL;

}

 

핵심이 되는 주요 멤버함수들을 모두 NULL로 초기화하였다. 지금까지는 buf arHanWidth 같은 변수를 굳이 NULL로 초기화하지 않아도 잘 실행되었다. 왜냐하면 ApiEdit 윈도우가 정적으로 생성되었고 전역변수는 컴파일러에 의해 0으로 자동 초기화되기 때문이다. 하지만 오브젝트는 그렇지 않다. new CApiEdit연산문으로 동적 생성하면 멤버들이 자동으로 초기화되지 않으며 쓰레기값을 가지게 된다.

다른 변수들은 쓰레기값을 당분간 가지고 있더라도 상관이 없지만 상기 4변수는 객체 생성과 함께 반드시 NULL로 초기화되어야 한다. 그렇지 않으면 if (buf), if (arHanWidth) 같은 조건문이 잘못 판단을 하게 되므로 컨트롤 초기화가 제대로 되지 않는다. 이런 중요한 변수들이 쓰레기값을 가지게 되면 언제 말썽을 부릴지 알 수가 없는 것이다. 그래서 애초에 문제를 만들지 않기 위해서 생성자에서 이 값들을 깨끗하게 초기화하였다.