. 윈도우 컨트롤

윈도우 컨트롤이란 버튼, 에디트 같은 표준 컨트롤처럼 CreateWindow로 생성하는 컨트롤이다. 그 전에 윈도우 클래스는 등록되어 있어야 하며 생성할 때 스타일로 컨트롤의 모양이나 동작을 지정할 수 있다. 부모 윈도우가 컨트롤에게 명령을 보낼 때는 SendMessage 함수로 미리 정의된 메시지를 보내며 반대로 컨트롤이 부모 윈도우에게 보고를 할 때는 통지 메시지를 사용한다. ShowMsg 컨트롤을 이 방식대로 사용할 수 있도록 컨트롤화 해보도록 하자.

컨트롤이란 재사용이 쉽도록 하는데 일차적인 목적이 있으므로 아무 프로젝트에서나 쉽게 가져가 쓸 수 있어야 한다. 특정한 조건이 만족될 때만 사용할 수 있어서는 곤란하며 컨트롤을 사용하는데 추가적인 작업은 가급적이면 최소화해야 하고 아무 추가 작업 없이 바로 사용할 수 있다면 이상적이다.

통상 컨트롤은 LIB DLL로 작성하고 이 모듈을 프로젝트에 추가함으로써 사용하도록 하는 것이 보통이다. 굳이 소스를 숨길 필요가 없고 개발자가 원하는 대로 소스를 수정해 사용할 수 있도록 하려면 소스파일을 배포할 수도 있다. 소스 배포시는 통상 .h 헤더 파일과 .cpp 구현 파일 둘을 배포하고 필요할 경우 리소스파일(bmp, cur)이 추가될 수 있다. 헤더 파일에는 메시지, 통지 메시지, 스타일 등의 매크로나 인터페이스 함수의 원형이 작성되고 구현 파일에는 함수들의 구현 코드와 메시지 처리 함수가 작성된다. 이 컨트롤을 사용하는 개발자는 컨트롤 작성자가 배포한 모듈을 자신의 프로젝트에 포함시켜 줌으로써 컨트롤을 사용한다.

ShowMsg 윈도우를 컨트롤화하여 ShowMsgCtrl 프로젝트를 작성해보자. 새로운 프로젝트를 작성하고 필요한 구성 파일들을 차례대로 작성해보자. 먼저 ShowMsg.h 파일을 다음과 같이 작성한다.

 

BOOL InitShowMsg();

#define SMM_CHANGESTRING WM_USER+1

 

컨트롤이 워낙 간단하기 때문에 헤더 파일도 아주 간단하다. InitShowMsg 인터페이스 함수의 원형이 정의되어 있고 SMM_CHANGESTRING이라는 메시지 상수가 정의되어 있다. 이 메시지는 ShowMsg 윈도우의 ChangeString 함수를 호출하기 위해 정의된 것인데 이 함수는 컨트롤 내부의 함수이므로 메시지를 통해 간접적으로 호출해야 한다. 메시지의 lParam으로 바꾸고자 하는 문자열의 포인터를 전달하도록 하였다. 컨트롤의 스타일이나 통지 메시지가 필요하다면 역시 헤더 파일에 정의하는데 ShowMsg 컨트롤은 스타일을 가지지 않는다.

다음은 컨트롤의 구현 파일 ShowMsg.cpp를 작성한다. 컨트롤화하기 전의 ShowMsg.cpp와 모양이 조금 달라지지만 사용하는 논리는 동일하다.

 

#include <windows.h>

#include "ShowMsg.h"

 

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

 

BOOL InitShowMsg()

{

     WNDCLASS WndClass;

    

     WndClass.cbClsExtra=0;

     WndClass.cbWndExtra=4;

     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;

     if (RegisterClass(&WndClass) == 0) {

          return FALSE;

     } else {

          return TRUE;

     }

}

 

struct tagSM

{

     int x;

     int y;

     TCHAR *str;

};

 

void ChangeString(HWND hWnd,TCHAR *nstr)

{

     tagSM *pSM=(tagSM *)GetWindowLong(hWnd,0);

 

     lstrcpy(pSM->str,nstr);

     InvalidateRect(hWnd,NULL,TRUE);

}

 

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

{

     HDC hdc;

     PAINTSTRUCT ps;

     RECT crt;

     tagSM *pSM;

 

     switch(iMessage) {

     case WM_CREATE:

          pSM=(tagSM *)malloc(sizeof(tagSM));

          pSM->x=50;

          pSM->y=50;

          pSM->str=(TCHAR *)malloc(128);

          lstrcpy(pSM->str,"String");

          SetWindowLong(hWnd,0,(LONG)pSM);

          return 0;

     case WM_KEYDOWN:

          pSM=(tagSM *)GetWindowLong(hWnd,0);

          GetClientRect(hWnd,&crt);

          switch (wParam) {

          case VK_LEFT:

              if (pSM->x > 0)

                   pSM->x--;

              break;

          case VK_RIGHT:

              if (pSM->x < crt.right-50)

                   pSM->x++;

              break;

          case VK_UP:

              if (pSM->y > 0)

                   pSM->y--;

              break;

          case VK_DOWN:

              if (pSM->y < crt.bottom-10)

                   pSM->y++;

              break;

          }

          InvalidateRect(hWnd,NULL,TRUE);

          return 0;

     case SMM_CHANGESTRING:

          ChangeString(hWnd,(TCHAR *)lParam);

          return 0;

     case WM_LBUTTONDOWN:

          pSM=(tagSM *)GetWindowLong(hWnd,0);

          if (lstrcmp(pSM->str,"String") == 0) {

              ChangeString(hWnd,"문자열");

          } else {

              ChangeString(hWnd,"String");

          }

          SetFocus(hWnd);

          return 0;

     case WM_PAINT:

          pSM=(tagSM *)GetWindowLong(hWnd,0);

          GetClientRect(hWnd,&crt);

          hdc=BeginPaint(hWnd, &ps);

          Rectangle(hdc,0,0,crt.right,crt.bottom);

          TextOut(hdc,pSM->x,pSM->y,pSM->str,lstrlen(pSM->str));

          EndPaint(hWnd, &ps);

          return 0;

     case WM_DESTROY:

          pSM=(tagSM *)GetWindowLong(hWnd,0);

          free(pSM->str);

          free(pSM);

          return 0;

     }

     return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

 

InitShowMsg라는 인터페이스 함수는 이 컨트롤의 윈도우 클래스를 등록하는 작업을 하는데 윈도우 클래스가 등록되어야 CreateWindow 함수로 컨트롤을 생성할 수 있다. 리스트 뷰나 트리 뷰를 사용하기 전에 InitCommonControls(Ex) 함수를 호출해야 하는 것과 마찬가지로 ShowMsg 컨트롤을 사용하고 싶으면 호스트 프로그램의 초기화 루틴에서 이 함수를 호출하여 윈도우 클래스를 등록해야 한다.

윈도우 클래스의 이름은 ShowMsgCtrl로 정했으며 cbWndExtra 4를 대입하여 4바이트의 여분 메모리를 준비하였다. 한 윈도우에 여러 개의 컨트롤을 동시에 생성할 수 있으며 이때 각 컨트롤의 인스턴스 별로 데이터를 저장할 수 있는 공간이 필요하다. 컨트롤의 전역변수는 인스턴스 별로 고유해야 한다. ShowMsg 컨트롤은 x, y, str 세 개의 전역변수를 가지는데 이 변수들은 인스턴스끼리 공유하는 것이 아니라 각 인스턴스가 개별적으로 값을 유지한다.

예를 들어 다음과 같이 한 대화상자에 4개의 에디트 컨트롤이 배치되어 있다고 하자. 각 에디트 별로 입력받는 정보의 의미는 다르다.

에디트 컨트롤은 편집중인 텍스트의 버퍼, 현재 캐럿 위치, 선택영역, 읽기전용 스타일값 등의 전역변수를 필요로 하는데 각 에디트마다 고유한 전역변수 집합을 가지고 있어야 한다. 이 정보들을 공유한다면 개별 컨트롤은 독립적으로 동작할 수가 없다. ShowMsg 컨트롤도 마찬가지로 각 인스턴스마다 x, y, str을 가져야 하며 이 정보 저장을 위한 공간으로 여분 메모리를 할당하는 것이다.

ShowMsg 컨트롤의 경우 전역변수가 세 개밖에 되지 않기 때문에 여분 메모리에 12바이트를 할당하고 직접 넣을 수도 있다. 그러나 이렇게 하면 각 변수의 이름은 없어져버리고 오프셋으로 액세스해야 하므로 불편할 뿐만 아니라 여분 메모리 공간이 무한하지 않다는 제약이 있어 전역변수를 많이 사용할 수가 없다. 그래서 보통 모든 전역변수를 포함하는 하나의 구조체를 선언하고 이 구조체의 포인터만 여분 메모리에 저장해놓는다. 그래서 ShowMsgCtrl 윈도우 클래스의 여분 메모리 크기가 4바이트로 할당되었다.

tagSM 구조체는 ShowMsgCtrl의 고유 데이터를 가지는 구조체이며 크기는 12바이트이다. WM_CREATE에서 이 구조체를 위한 메모리를 할당하고 구조체의 멤버를 초기화한다. 그리고 여분 메모리에 이 구조체의 포인터를 대입해놓았다. 인스턴스의 데이터가 여분 메모리에 들어 있으므로 이 컨트롤이 저장된 데이터를 액세스하려면 다음 순서대로 해야 한다.

 

tagSM *pSM;

pSM=(tagSM *)GetWindowLong(hWnd,0);

pSM->멤버=;

 

tagSM 구조체 포인터를 선언하고 여분 메모리에서 이 구조체의 포인터를 구한다. 그리고 구조체 포인터 연산자를 사용하여 구조체의 각 멤버에 액세스할 수 있다. 메시지 처리 함수나 유틸리티 함수에서도 이 방법대로 컨트롤의 데이터를 액세스해야 한다. 여분 메모리는 컨트롤이 실행되는 동안 인스턴스의 고유 데이터를 저장하는 역할을 하며 컨트롤이 종료될 때인 WM_DESTROY에서 해제된다.

일반함수 ChangeString은 첫 번째 인수로 윈도우 핸들을 받도록 원형이 수정되었다. 이 함수는 str 전역변수의 값을 변경하는데 str에 액세스하기 위해서는 여분 메모리에 들어 있는 pSM 구조체 포인터가 필요하고 윈도우 핸들이 있어야만 여분 메모리를 읽을 수 있다. 일반함수는 컨트롤의 모든 인스턴스가 공유하는 함수이며 작업할 대상 인스턴스를 찾아야 하므로 윈도우 핸들이 반드시 필요하다.

윈도우 프로시저인 ShowMsgProc 함수는 x, y, str을 액세스할 때 항상 pSM을 먼저 구하고 pSM->x, pSM->str과 같이 액세스하도록 수정되었다. 데이터를 액세스하는 구문이 조금 복잡해졌을 뿐이지 논리가 변한 것은 하나도 없다. 다만 컨트롤이 됨으로써 두 가지 부분에 변화가 생겼는데 WM_LBUTTONDOWN에서 SetFocus를 호출하여 강제로 포커스를 가져와야 한다는 점과 WM_DESTROY에서 PostQuitMessage(0) 함수를 호출하지 않는다는 점이 바뀌었다. 컨트롤은 메인 윈도우가 아니기 때문에 응용 프로그램을 종료할 권한이 없다.

ShowMsg 컨트롤은 다 작성했으며 이제 이 컨트롤을 사용할 메인 윈도우를 작성하도록 하자. 호스트 프로그램을 작성하는 것이다. 프로젝트에 ShowMsgTest.cpp 파일을 추가하고 다음과 같이 코드를 작성한다.

 

#include <windows.h>

#include "ShowMsg.h"

 

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

HINSTANCE g_hInst;

HWND hWndMain;

LPCTSTR lpszClass=TEXT("ShowMsgTest");

 

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance

       ,LPSTR lpszCmdParam,int nCmdShow)

{

     HWND hWnd;

     MSG Message;

     WNDCLASS WndClass;

     g_hInst=hInstance;

    

     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=hInstance;

     WndClass.lpfnWndProc=(WNDPROC)WndProc;

     WndClass.lpszClassName=lpszClass;

     WndClass.lpszMenuName=NULL;

     WndClass.style=CS_HREDRAW | CS_VREDRAW;

     RegisterClass(&WndClass);

 

     hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,

          CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,

          NULL,(HMENU)NULL,hInstance,NULL);

     ShowWindow(hWnd,nCmdShow);

     hWndMain=hWnd;

    

     while(GetMessage(&Message,0,0,0)) {

          TranslateMessage(&Message);

          DispatchMessage(&Message);

     }

     return (int)Message.wParam;

}

 

HWND hMsg1, hMsg2;

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

{

     HDC hdc;

     PAINTSTRUCT ps;

 

     switch(iMessage) {

     case WM_CREATE:

          if (InitShowMsg() == FALSE) {

              return -1;

          }

          hMsg1=CreateWindow("ShowMsgCtrl",NULL, WS_CHILD | WS_VISIBLE,

              10,10,100,100,hWnd,NULL,g_hInst,NULL);

          hMsg2=CreateWindow("ShowMsgCtrl",NULL, WS_CHILD | WS_VISIBLE,

              210,10,100,100,hWnd,NULL,g_hInst,NULL);

          return 0;

     case WM_LBUTTONDOWN:

          SendMessage(hMsg1,SMM_CHANGESTRING,0,(LPARAM)"test");

          return 0;

     case WM_PAINT:

          hdc=BeginPaint(hWnd, &ps);

          EndPaint(hWnd, &ps);

          return 0;

     case WM_DESTROY:

          PostQuitMessage(0);

          return 0;

     }

     return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

 

아주 일반적인 API 소스이다. ShowMsg.h를 인클루드하여 ShowMsg 컨트롤의 스타일, 메시지, 인터페이스 함수 등에 대한 정보를 먼저 구했다. 전역변수로 두 개의 윈도우 핸들을 정의했는데 이 핸들로 컨트롤을 조작하게 된다.

WM_CREATE에서는 먼저 InitShowMsg 인터페이스 함수를 호출하여 컨트롤에게 윈도우 클래스를 등록할 기회를 제공한다. 만약 어떤 이유로 윈도우 클래스 등록에 실패하면 프로그램이 제대로 실행될 수 없는 상황이므로 프로그램을 종료해야 한다. ShowMsg 컨트롤을 사용하는 호스트 프로그램은 InitShowMsg 함수를 먼저 호출해야 할 의무가 있다.

윈도우 클래스를 등록한 후 CreateWindow 함수로 표준 컨트롤을 생성하듯이 두 개의 컨트롤을 생성하였다. 마치 button 클래스로부터 버튼을 만들듯이 ShowMsgCtrl 클래스로부터 ShowMsg 컨트롤을 만들었다. 스타일은 정의되어 있지 않으므로 별도로 주지 않았는데 만약 있다면 SMS_로 시작되는 상수가 정의될 것이다. 컨트롤의 문자열을 부모 윈도우가 직접 변경하려면 lParam에 원하는 문자열 포인터를 대입하여 SMM_CHANGESTRING 메시지를 보내면 된다. 실행해보자.

두 개의 컨트롤이 생성되었으며 각각이 서로에게 영향을 미치지 않고 독립적으로 잘 동작하고 있다. 각 윈도우 별로 여분 메모리에 고유의 tagSM 구조체를 할당하여 사용하고 있기 때문이다. 원한다면 수십, 수백 개의 컨트롤을 생성할 수도 있다.

이 컨트롤을 다른 프로젝트에서 사용하고 싶다면 ShowMsg.h ShowMsg.cpp를 복사한 후 프로젝트에 포함시키고 표준 컨트롤을 사용하는 방법과 동일하게 사용하면 된다. InitShowMsg 함수를 호출하는 추가 작업이 필요한 것이 조금 흠이기는 하지만 사용하기에 특별히 어려운 점은 없다. 표준 컨트롤들은 모두 이 방식대로 작성되어 있고, 운영체제가 프로세스 시작과 동시에 윈도우 클래스의 사본을 복사하기 때문에 윈도우 클래스를 등록하지 않아도 된다는 점만 다를 뿐이다. 공통 컨트롤들은 이 방식과 완전히 동일하게 작성되어 있으며, 소스가 제공되지 않고 comctl32.dll로 제공된다는 점만 다르다.