. 도우미

오브젝트화의 핵심은 윈도우 핸들과 객체 포인터의 대응 관계를 적어놓는 객체 맵에 있다. 그렇다면 객체 맵을 누가 관리할 것인가 하는 문제가 생기는데 객체의 포인터를 다루는 일을 객체 자신이 직접 할 수는 없다. 객체 맵은 객체 외부에 있기 때문에 이 작업만 전문적으로 하는 도우미 클래스가 필요하다. 소스에 있는 CRegisterHelper 클래스가 바로 객체 맵을 관리하는 도우미 클래스이다. 이 클래스는 ShowMsg.cpp에 같이 선언 및 정의되어 있으며 전역 객체까지 만들어져 있다.

 

#include <windows.h>

#include "ShowMsg.h"

 

LRESULT CALLBACK ShowMsgProc(HWND,UINT,WPARAM,LPARAM);

 

class CRegisterHelper

{

public:

     CRegisterHelper();

     ~CRegisterHelper();

 

     struct _arObj

     {

          CShowMsg *pObj;

          HWND hWnd;

     } *arObj;

     int arSize;

     int nReg;

 

     CShowMsg *FindObject(HWND hWnd);

     void AddObject(HWND hWnd, CShowMsg *pObj);

     void RemoveObject(HWND hWnd);

 

};

 

CRegisterHelper::CRegisterHelper()

{

     WNDCLASS WndClass;

 

     WndClass.cbClsExtra=0;

     WndClass.cbWndExtra=0;

     WndClass.hbrBackground=(HBRUSH)(COLOR_WINDOW+1);

     WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);

     WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);

     WndClass.hInstance=GetModuleHandle(NULL);

     WndClass.lpfnWndProc=(WNDPROC)ShowMsgProc;

     WndClass.lpszClassName="ShowMsgCtrl";

     WndClass.lpszMenuName=NULL;

     WndClass.style=CS_HREDRAW | CS_VREDRAW;

     RegisterClass(&WndClass);

 

     nReg=0;

     arSize=10;

     arObj=(_arObj *)malloc(arSize*sizeof(_arObj));

     memset(arObj,0,arSize*sizeof(_arObj));

}

 

CRegisterHelper::~CRegisterHelper()

{

     free(arObj);

     arObj=NULL;

}

 

CShowMsg *CRegisterHelper::FindObject(HWND hWnd)

{

     int i;

 

     for (i=0;i<nReg;i++) {

          if (arObj[i].hWnd == hWnd)

              return arObj[i].pObj;

     }

     return NULL;

}

 

void CRegisterHelper::AddObject(HWND hWnd, CShowMsg *pObj)

{

     int i;

 

     if (nReg == arSize-1) {

          arSize++;

          arObj=(_arObj *)realloc(arObj,arSize*sizeof(_arObj));

          arObj[arSize-1].hWnd=NULL;

          arObj[arSize-1].pObj=NULL;

     }

 

     for (i=0;;i++) {

          if (arObj[i].hWnd == NULL)

              break;

     }

     arObj[i].hWnd=hWnd;

     arObj[i].pObj=pObj;

     pObj->hWnd=hWnd;

     nReg++;

}

 

void CRegisterHelper::RemoveObject(HWND hWnd)

{

     int i,j;

 

     if (IsWindow(hWnd)) {

          DestroyWindow(hWnd);

     }

     for (i=0;i<nReg;i++) {

          if (arObj[i].hWnd == hWnd)

              break;

     }

     for (j=i+1;j<arSize;j++) {

          arObj[j-1].hWnd=arObj[j].hWnd;

          arObj[j-1].pObj=arObj[j].pObj;

     }

     nReg--;

}

 

CRegisterHelper _RegisterHelper;

 

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

{

     CShowMsg *pSM;

 

     pSM=_RegisterHelper.FindObject(hWnd);

     if (pSM == NULL) {

          pSM=(CShowMsg *)((LPCREATESTRUCT)lParam)->lpCreateParams;

          _RegisterHelper.AddObject(hWnd,pSM);

     }

 

     return pSM->OnMessage(iMessage, wParam, lParam);

}

 

이 클래스를 같이 분석해보도록 하자. 멤버로 arObj 구조체 포인터를 가지는데 이 배열이 바로 객체 맵이다. 구조체의 멤버에는 CShowMsg 클래스의 포인터 pObj와 윈도우 핸들 hWnd가 포함되어 있어 어떤 객체가 어떤 윈도우 핸들과 대응되는지를 기억한다. arSize 변수는 객체 맵의 할당된 크기이며 nReg는 등록된 객체의 개수이다. 객체 맵을 관리하는 세 개의 멤버함수와 생성자 파괴자가 있는데 각 함수를 분석해보자.

 

FindObject

윈도우 핸들로부터 객체 포인터를 찾아준다. 만약 맵에 윈도우 핸들이 없으면 NULL을 리턴하여 아직 등록되지 않은 객체라는 것을 알려준다. 코드는 아주 간단한데 등록된 객체수만큼 루프를 돌면서 윈도우 핸들을 비교해보고 발견되면 맵의 객체 포인터를 리턴한다. 배열의 크기가 크지 않으므로 순차 검색을 해도 속도상의 불이익은 크지 않으며 윈도우 핸들이 정렬되어 있지 않기 때문에 순차 검색밖에 할 수가 없다.

AddObject

객체 맵에 새로운 객체를 등록한다. 인수로는 객체의 윈도우 핸들과 객체 포인터를 전달받는다. 맵 배열의 빈자리를 찾아 hWnd pObj를 대입하고 객체의 hWnd 멤버변수에도 윈도우 핸들을 대입하는 비교적 간단한 코드로 되어 있다. 객체를 등록한 후 nReg 1 증가한다.

이 함수가 해야 할 또 다른 작업은 객체 맵의 크기를 관리하는 것이다. 맵 배열은 최초 10의 크기를 가지는데 등록되는 객체 수가 늘어나면 이 배열크기도 같이 늘어나야 한다. 객체 맵이 고정된 크기의 배열이라면 생성할 수 있는 객체의 최대 개수가 제한되는 문제가 있으므로 반드시 동적으로 배열을 관리하도록 해야 한다. 이 방법은 앞에서도 실습해 본 적이 있는데 pLine 배열이 똑같은 방법으로 관리되고 있다. 도우미 클래스의 arSize 변수는 ApiEdit LineLen에 해당하는 변수이고 nReg TotalLine에 해당한다.

nReg arSize-1과 같으면 즉, 마지막 끝 표시를 나타내는 NULL요소를 제외하고 더 등록할 공간이 없으면 arObj 배열을 다시 재할당하여 크기를 늘려 준다. 재할당할 때 여유분을 둘 수도 있지만 객체 등록은 자주 있는 일이 아니므로 여유분없이 꼭 필요한 만큼만 재할당하도록 했다.

RemoveObject

객체 맵에서 객체를 제거한다. 인수로 전달된 윈도우 핸들을 비교하여 배열상의 인덱스를 찾고 검색된 요소를 삭제한다. 삭제할 때는 뒤쪽에 있는 값을 앞으로 복사해 배열 중간에 빈칸이 없도록 했다. 객체를 제거할 때 윈도우도 같이 파괴시켜 주는데 이 코드에 대해서는 잠시 후 부연 설명을 할 것이다.

생성자

생성자는 멤버변수들을 초기화하고 객체 맵을 초기 할당하는 일을 한다. 최초 등록된 객체가 없으므로 nReg 0으로 초기화되고 arSize 10으로 초기화한 후 이 크기만큼 객체 맵 배열을 할당한다. 즉 객체 맵의 초기 크기는 10이다. 하지만 AddObject 함수가 이 배열을 동적으로 관리하므로 등록할 수 있는 객체 개수에 제한은 없다.

생성자는 객체 맵을 초기화하는 일 외에도 아주 중요한 일을 하나 더 하고 있는데 바로 ShowMsg 컨트롤의 윈도우 클래스를 등록하는 것이다. 도우미 객체는 전역변수로 선언되었으므로 프로그램이 실행될 때 생성자가 반드시 호출되며 또한 도우미 객체가 유일하게 하나뿐이므로 정확하게 한 번만 호출된다. 그래서 도우미의 생성자에서 윈도우 클래스를 등록하도록 하면 윈도우 클래스가 아주 자연스럽게 등록되는 것이다.

이 방법은 InitShowMsg 함수를 제공하는 것보다 훨씬 더 발전된 방법이다. 클래스가 생성될 때 생성자가 반드시 호출되므로 이 모듈을 프로젝트에 포함시키는 것만으로도 이미 윈도우 클래스는 등록되는 것이다. 그래서 이 컨트롤을 사용하는 사용자는 InitShowMsg 같은 함수를 호출해야 할 필요가 없고 그 사실을 기억할 필요도 없어졌다. 사용자가 기억해야 할 내용이 많은 프로그램은 좋은 프로그램이 아니다. 생성자의 이런 특성을 이용하는 것은 잘 기억해 둘만한 유용한 방법인데 MFC CWaitCursor 클래스가 동일한 기법으로 커서를 변경한다.

컨트롤이 오브젝트화되면 객체의 멤버변수에 정보를 저장할 수 있으므로 여분 메모리를 사용할 필요가 없어졌다. 그래서 윈도우 클래스의 cbWndExtra 멤버는 0으로 정의되었다. 메시지 처리 함수는 ShowMsgProc이라는 일반함수로 지정되었는데 이 함수는 잠시 후 따로 분석해 볼 것이다.

파괴자

파괴자는 객체 맵을 파괴한다. arObj NULL을 대입하는 이유는 객체가 파괴되었음을 명확하게 표시해놓기 위해서이다.

메시지 처리 함수

ShowMsgProc ShowMsg 윈도우로 전달되는 메시지를 일차적으로 받아 주는 함수이지만 객체의 일부는 아니다. 오히려 도우미 클래스의 일부라고 보아야 한다. 이 함수가 하는 일은 메시지가 전달되는 즉시 메시지 맵에 객체를 등록하고 이미 등록된 객체에 대해서는 객체의 OnMessage 가상함수로 메시지를 전달하는 것이다.

윈도우가 생성될 때 가장 먼저 전달되는 메시지는 WM_NCCREATE이다. 이 메시지를 받았을 때 hWnd가 맵에 등록되어 있는지 FindObject 함수로 검사해 본다. 만약 등록되어 있지 않다면 CREATESTRUCT로 전달된 this 포인터를 조사하여 객체를 등록하는데 도우미의 AddObject가 이 일을 한다. 그리고 이 메시지를 그대로 객체의 OnMessage 함수로 전달한다.

이후부터 메시지가 전달되면 ShowMsgProc은 객체 맵에서 객체 포인터를 조사한 후 이 객체의 OnMessage로 처리를 넘겨준다.