C/C++와 GUI 프로그래밍

일반입력 :2002/10/11 00:00

장현준

마이크로소프트(이하 MS)의 윈도우는 GUI(Graphic User Interface) 기반의 편리한 인터페이스를 기반으로 윈도우 3.0에서부터 윈도우 95/98을 거쳐 XP에 이르는 PC 환경에 커다란 변화를 가져온 운영체제라 할 수 있다. 윈도우는 개발자에게도 많은 변화를 줬는데 그 이전의 DOS 프로그래밍에서는 볼 수 없었던 메시지 기반(message-driven 또는 event-driven) 방식을 제공하고 DOS에서는 기본 C 함수 외에 인터럽트(interrupt) 기반의 시스템 함수를 이용해 프로그래밍을 했다면 윈도우에서는 자체적으로 제공하는 수백 개의 API(Application Programming Interface)에 익숙해야 프로그래밍할 수 있다.

개발자는 윈도우가 DOS와는 설계 개념부터가 다른 운영체제이었기 때문에 같은 프로그래밍 언어를 사용한다 하더라도 기본 문법만 같을 뿐 많은 새로운 부분을 익혀야 하는 어려움은 있었다. 그러나 DOS나 유닉스 같은 명령어 라인 기반 운영체제에서 응용 프로그래밍 개발시 애로사항인 사용자 인터페이스 부분을 운영체제에서 지원해준다는 것은 상당히 매력적이다.

특히 윈도우 프로그래머로의 전환은 윈도우 95가 소개되는 시기를 생각하면 거의 대세였고, MS에서 제공하는 통합 환경의 편리함과 MFC(Microsoft Foundation Class)라는 편리한 라이브러리는 C/C++ 개발자가 4GL 못지않게 윈도우에 대한 깊은 이해 없이 프로그래밍할 수 있게 해줬다. 이번 호에서는 이러한 윈도우 기반의 GUI 프로그래밍에 대해서 살펴보고자 한다. 윈도우 프로그래밍의 기본에서부터 MFC를 이용한 방법과 응용 프로그램을 좀더 멋지게 꾸밀 수 있게 해주는 몇몇 공개 및 사용 라이브러리를 소개할 것이다.

윈도우 프로그래밍의 기본

C/C++ 언어를 이용한 윈도우 프로그래밍은 PC 환경의 DOS나 유닉스에서와 약간 다른 구조를 갖는다(물론 윈도우에서도 DOS에서와 같은 콘솔 기반 프로그래밍이 가능하다). 전통적인 시작 포인트(entry point)인 main 함수 대신 WinMain 함수를 사용하고, 표준 입출력 함수 대신에 윈도우(window) 제어와 GDI(Graphics Device Interface) API를 이용해서 입출력을 처리한다. 또한 메시지 루프(message loop)가 존재해 윈도우로 전달되는 메시지를 처리하는 역할을 한다.

윈도우 프로그램의 기본적인 목적은 하나 이상의 윈도우를 생성하고 사용자로부터 입력되는 입력 정보에 해당하는 이벤트를 처리하는데 있다. 이러한 동작을 하기 위해서 개발자는 윈도우에게 생성하려는 윈도우에 대한 몇 가지 정보를 전달해야 하고 그 정보를 기준으로 자신이 원하는 윈도우를 생성할 수 있다. 그 절차는 다음과 같다.

① 윈도우를 생성하려는 윈도우 클래스를 등록한다.

② 메인 윈도우를 생성한다.

③ 메시지 루프를 실행한다.

윈도우 생성에 첫 번째 단계는 윈도우에 윈도우 클래스 정보를 등록하는 것이다. 윈도우 클래스(window class) 정보 등록을 위해서 <리스트 1>의 WNDCLASS 구조체가 사용된다.

<리스트 1> WNDCLASS 구조체 정보

typedef struct _WNDCLASS {

UINT style; // 클래스 스타일 지정

WNDPROC lpfnWndProc; // 윈도우 프로시져의 포인터

int cbClsExtra; // 클래스 엑스트라 데이터 크기

int cbWndExtra; // 윈도우 엑스트라 데이터 크기

HINSTANCE hInstance; // 윈도우 인스턴스 핸들

HICON hIcon; // 아이콘 핸들

HCURSOR hCursor; // 커서 핸들

HBRUSH hbrBackground; // 백그라운드 브러시 핸들

LPCTSTR lpszMenuName; // 메뉴 리소스 명

LPCTSTR lpszClassName; // 클래스 명

} WNDCLASS, *PWNDCLASS;

WNDCLASS 구조체를 보면 알겠지만 윈도우 출력 형태와 관련된 다양한 정보를 넣게 되어있다. style 항목에는 double click 메시지 사용 여부 또는 크기가 변경되거나 위치가 변경됐을 때, 어떻게 동작할 것인지에 대한 정보가 전달되고, menu, icon, string 등의 정보가 들어있는 인스턴스(instance) 핸들, 윈도우 우측 상단에 출력돼는 icon handle에 대한 정보와 클라이언트 영역에 기본으로 칠해질 배경색 등에 대한 정보가 전달된다. WNDCLASS 정보는 RegisterClass 함수에 의해서 윈도우에 등록되는데, lpszClassName을 키 값으로 등록된다. 만일 이미 중복된 값이 전달된다면, 오류가 발생한다. 윈도우 클래스에 등록된 정보는 윈도우 생성 함수인 CreateWindow 함수의 인자로 전달된다. <리스트 2>는 CreateWindow 함수의 원형이다.

<리스트 2> CreateWindow 함수 원형

HWND CreateWindow(

LPCTSTR lpClassName, // 클래스 이름

LPCTSTR lpWindowName, // 윈도우 이름

DWORD dwStyle, // 윈도우 스타일

int x, // 윈도우 위치(x좌표)

int y, // 윈도우 위치(x좌표)

int nWidth, // 윈도우 폭

int nHeight, // 윈도우 높이

HWND hWndParent, // 부모 윈도우 핸들

HMENU hMenu, // 메뉴 핸들

HINSTANCE hInstance, // 인스턴스 핸들

LPVOID lpParam // WM_CREATE 메시지로 전달시킬 데이터

);

CreateWindow 함수의 첫 번째 인자가 RegisterClass 함수로 등록한 윈도우 클래스 이름이다. 클래스 이름은 사용자가 RegisterClass 함수로 직접 등록시킨 것 이외에 윈도우에 기 등록된 클래스 이름을 전달할 수도 있다. <표 1>이 윈도우에 기 등록된 클래스 이름의 목록이다.

<표 1> 기 등록된 윈도우 클래스 목록

CreateWindow 함수가 정상적으로 실행되면 윈도우 핸들이 전달된다. 윈도우가 생성되면 윈도우 내부적으로는 클래스에 등록된 윈도우 프로시저(window procedure)로 윈도우 생성에 필요한 메시지들이 전송된다. <리스트 3>은 윈도우 프로시저의 예이다.

<리스트 3> 윈도우 프로시저 예

// hWnd : 윈도우 핸들

// message : 메시지 ID

// wParam : 첫 번째 파라미터

// lParam : 두 번째 파라미터

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

{

int wmId, wmEvent;

PAINTSTRUCT ps;

HDC hdc;

LPCREATESTRUCT pCreateStrc;

switch (message) {

case WM_CREATE:// CreateWindow 함수가 호출됐을 때 전달된다.

pCreateStrc = (LPCREATESTRUCT)lParam;

// 만일 오류가 발생해서 윈도우 생성이 불가능하면, -1값을 리턴해야 한다.

break;

case WM_COMMAND:

wmId = LOWORD(wParam); // Menu 또는 Control ID

wmEvent = HIWORD(wParam); // Notification Code 또는 accelerator에서 전달되면 1,

// 메뉴로부터 전달되면 0값을 가진다.

//메뉴 대한 처리

...........................

break;

case WM_PAINT:// 클라이언트 영역을 그릴 필요가 있을 때, 전달된다.

hdc = BeginPaint (hWnd, &ps);

// hdc를 이용해서 화면 출력 부분을 처리한다.

EndPaint (hWnd, &ps);

break;

case WM_DESTROY:

// 윈도우가 제거될 때, 발생한다.

// PostQuitMessage를 호출해서 WM_QUIT 메시지가 발생하게 해야한다.

PostQuitMessage(0);

break;

default:// 사용자 윈도우에서 처리가 불필요할 경우

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

}

return (0);

}

윈도우 프로시저의 message로는 발생된 메시지의 ID가 전달되고 wParam과 lParam으로는 메시지와 관련된 인자가 전달된다. WM_CREATE 메시지의 경우 lParam으로는 CREATESTRUCT 구조체의 포인터가 전달된다. CREATESTRUCT 구조체에는 CreateWindow 함수로 전달된 윈도우 생성 정보가 들어있다. 윈도우 프로시저로 전달되는 메시지에는 다양한 것들이 있는데, 그중 많이 사용되는 메시지를 살펴보면 <표 2>와 같다.

<표 2> 자주 사용되는 윈도우 메시지 목록

앞서 설명한 것처럼 윈도우는 메시지 기반 구조이기 때문에 DOS처럼 사용자로부터 키보드 또는 마우스 등의 입력을 얻기 위해서 직접적으로 특정 함수를 호출하는 것은 불가능하다. 대신에 시스템으로부터 입력이 전달되기를 대기해야 한다. 시스템은 응용 프로그램에 발생하는 모든 입력 데이터를 응용 프로그램에 존재하는 여러 윈도우들로 전달한다. 입력 데이터는 각 윈도우에 등록된 윈도우 프로시저로 메시지 형태로 전달돼 응용 프로그램에서 입력에 대한 처리 후 그 결과를 시스템으로 전달하게 되는데 이렇게 응용 프로그램으로 전달되는 메시지를 받아서 해당 윈도우로 전달해주는 역할을 수행하는 것인 WinMain 함수에 존재하는 메시지 루프이다(실제로 Win32 환경에서 메시지에 대한 처리는 응용 프로그램 단위가 아니라 쓰레드 단위로 처리된다). 시스템은 응용 프로그램의 쓰레드로 메시지 전달을 위해서 쓰레드별로 구성된 메시지 큐(message queue)로 발생된 메시지를 전송하면 메시지 루프에서는 메시지 큐로 전달된 메시지를 가져다가 해당 윈도우의 윈도우 프로시저를 호출한다. <리스트 4>는 메시지 루프의 예이다.

<리스트 4> 메시지 루프

MSG msg;

BOOL bRet;

while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)

{

if (bRet == -1)

{

//오류에 대한 처리

}

else

{

TranslateMessage(&msg);

DispatchMessage(&msg);

}

}

메시지 루프는 간단히 GetMessage, TranslateMessage 그리고 DispatchMessage 함수로 구성되는데, GetMessage 함수에서는 -1 값이 리턴되는지 검사해야 한다. GetMessage 함수는 메시지 큐로부터 메시지를 구한 후, MSG 구조체로 값을 복사한다. GetMessage 함수는 프로세스 종료를 의미하는 WM_QUIT 메시지를 전달받으면 0 값을, 그렇지 않은 경우에는 0이 아닌 값을 전달한다. <리스트 3>을 보면 응용 프로그램은 프로그램의 종료를 위해 메인 윈도우의 WM_DESTROY 메시지 처리 부분에서 PostQuitMessage 함수를 호출하는데, 이 함수가 WM_QUIT 메시지를 해당 응용 프로그램의 메시지 큐에 전달해서, 메시지 루프를 종료하는 역할을 수행한다. <리스트 4>에서의 GetMessage 함수의 두 번째 인자는 NULL 값이 지정되는데 만일 특정 윈도우의 핸들을 지정하면 해당 윈도우의 메시지만 구할 수 있다.

메시지 루프의 TreanslateMessage 함수는 키보드로부터 발생한 문자에 대한 처리를 위한 함수이다. 시스템은 키보드로부터 이벤트가 발생했을 때, WM_KEYDOWN와 WM_KEYUP 메시지를 발생시키는데 TranslateMessage 함수는 이 메시지로부터 WM_CHAR 메시지를 발생하게 한다. DispatchMessage 함수는 메시지를 MSG 구조체에 지정된 윈도우 핸들의 윈도우 프로시저로 전달하는 동작을 수행한다. 만일 응용 프로그램에서 단축키(accelerator key)를 사용한다면 <리스트 4>의 메시지 루프에 TranslateAccelerator 함수를 추가해 단축키에 눌려졌을 때 대응되는 WM_COMMAND 메시지가 발생하도록 해야 한다.

MFC를 이용한 윈도우 프로그래밍

MS는 초기 윈도우 개발 도구로 MSC(마이크로소프트 C)를 제공한다. 비주얼 C++라는 개발 도구를 선보이면서 기존의 C 언어 기반의 API 프로그래밍 외에 MFC라는 C++언어 기반의 클래스 라이브러리는 같이 제공했다. MFC는 기존의 Win32 API를 클래스 기반으로 계층화해 놓은 라이브러리로 API기반 프로그래밍시에 행하던 기본 프로그래밍의 구조적인 틀을 논리적인 클래스와 API 기반 클래스로 추상화해 놓음으로써 복잡한 형식의 윈도우 프로그램을 쉽게 개발할 수 있다. 또한 MFC에서는 Win32 시스템에서는 기본으로 제공하지 않는 UI 설계를 위한 다양한 클래스들을 추가해 제공하고 있음은 물론 C++ 언어의 개체 상속 개념 등을 이용한 기존 클래스의 확장 가능함은 응용 프로그램 개발시에 많은 편리함을 제공하고 있다.

MFC에서는 한 응용 프로그램을 관리하기 위해서 <그림 1>과 같이 CWinApp, CFrameWnd, CView, 그리고 CDocument 클래스를 사용한다. C 언어 기반의 API 프로그래밍과 해당 클래스들의 역할을 비교해 보면 <표 3>과 같다. <표 3>에서는 CWinApp 클래스와 CWinThread 클래스는 API 기반 프로그래밍시의 WinMain 함수와 대응되지만 100% 대응되는 것은 아니다. MFC로 작성된 응용 프로그램은 _tMainWnd 함수라는 WinMain과 동일한 엔트리 포인트가 존재하고 이 함수는 내부적으로 AfxWinMain 함수를 호출한다. AfxWinMain 함수가 실제로 API에서의 WinMain에 대응된다. 즉, 윈도우 클래스를 등록하고 윈도우를 생성하고 메시지 루프를 수행한다. AfxWinMain 함수는 CWinApp 클래스와 CWinThread 클래스의 멤버 함수를 호출해서 WinMain의 역할을 수행하는데 CWinThread 클래스는 API 레벨의 역할을 수행하고 CWinApp 클래스는 MFC에서 새롭게 도입된 개념인 도큐멘트 템플릿(document template) 정보를 관리해 준다.

CFrameWnd 클래스는 API 방식에서의 메인 윈도우에 해당된다. CView 클래스는 CFrameWnd의 클라이언트 영역을 처리하기 위한 클래스로, API 방식에서는 존재할 수도 있고 존재하지 않을 수도 있다. 즉, API 방식에서는 메인 윈도우의 클라이언트 영역의 WM_PAINT 메시지를 받아 처리할 수도 있고, 메인 윈도우의 WM_CREATE 메시지에서 클라이언트 영역을 처리를 전담하는 새로운 윈도우를 하나 생성해서 클라이언트 영역 처리를 수행할 수 있다. 후자의 경우가 MFC의 CView와 같은 경우이다.

API 방식에서는 윈도우 제어 기반의 코딩이 수행되어지기 때문에 프로그래밍의 중요 부분인 데이터에 대한 처리가 미약하다고 할 수 있다. API 방식에서는 이를 보완하기 위해서 클래스 등록시에 지정하는 Window Extra Data 영역을 활용해서 윈도우마다 독립적인 데이터를 손쉽게 관리하는 방법이 소개되기도 했는데 MFC에서는 응용 프로그램에서의 데이터 관리를 쉽게 하기 위해서 CDocument 클래스를 제공하고 있다.

<표 3> MFC 클래스와 API 프로그래밍 비교

이렇듯 MFC에서 한 응용 프로그램을 처리하기 위해서, CWinApp, CWinThread, CFrameWnd, CView, 그리고 CDocument 클래스를 기본적으로 관리하고 있는데, 이중 데이터의 입출력과 관계되는 CFrameWnd, CView, CDocument를 묶어서 CDocTemplate 클래스로 관리한다. MFC에서는 클래스 라이브러리 이외에 여러 전역 자원을 관리하기 위한 전역 함수들이 존재한다. 이 함수는 Afx 접두어로 시작하는데, <표 4>는 자주 사용되는 함수를 정리해 놓은 것이다.

<표 4> 중요 MFC 전역 함수

<리스트 3>의 API 방식의 윈도우 프로시저를 보면 개발자는 자신이 처리하기 원하는 메시지만 처리하고 나머지는 기본 윈도우 프로시저로 전달하는 구조로 돼 있다. MFC에서는 이러한 윈도우 메시지 처리를 위해서 윈도우 핸들을 가지는 클래스들 즉, CWnd 클래스로부터 파생된 클래스들은 WindowProc 멤버 함수에서 기본 윈도우 메시지에 대한 처리를 전담하고 있다. 그런데 MFC에서도 API와 마찬가지로 WindowProc를 상속해서 메시지마다 일일이 조건 분기를 해 가면서 코딩을 한다는 것은 효율성이 떨어질 것이다. MFC는 이를 위해서 메시지 맵(message map)을 도입해 메시지 ID와 클래스의 멤버 함수를 연결 정보를 관리해 해당 메시지가 발생했을 때 해당 구성원 함수가 호출되게 구성돼 있다. <리스트 5>는 메시지 맵의 예이다. 메시지 맵을 사용하기 위해서는 클래스 선언시에 DECLARE_MESSAGE_MAP() 라인이 존재해야 하는데, DECLARE_MESSAGE_MAP()는 static 멤버 데이터인 _messageEntries와 messageMap를 선언하고 GetMessageMap 멤버 함수를 선언하는 것이다. 결국 메시지 맵은 이 두 변수와 함수를 정의하는 부분이다. afxwin.h를 확인하면 알 수 있을 것이다.

<리스트 5> 메시지 맵의 예

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)

//{{AFX_MSG_MAP(CMainFrame)

ON_WM_CREATE()

ON_WM_PAINT()

//}}AFX_MSG_MAP

END_MESSAGE_MAP()

<리스트 5>의 예를 보면, 메시지 맵을 구성하는 부분의 ON_WM_CREATE는 WM_CREATE 메시지가 발생했을 CMainFrame의 OnCreate 멤버함수를 호출하라는 의미이다. 마찬가지로 ON_WM_PAINT는 WM_PAINT가 발생했을 때, OnPaint 멤버함수를 호출하라는 의미이다. MFC는 이러한 메시지 맵의 관리를 쉽게 하기 위해서 ClassWizard를 제공한다. ClassWizard는 특정 메시지를 지원하기 위한 클래스의 멤버함수를 자동 추가하고, 해당 메지지의 메시지 맵을 갱신하는 작업을 수행해 준다. ClassWizard 이외에 비주얼 C++에서는 AppWizard를 제공하는데 AppWizard는 비주얼 C++로 개발할 수 있는 모든 종류의 프로젝트를 간단한 UI로 쉽게 생성할 수 있게 한다. MFC 응용 프로그램 개발시에 AppWizard를 사용하면 선택한 형태에 맞는 기본 소스를 자동으로 생성하기 때문에 AppWizard로 프로젝트 생성 후에 ClassWizard를 사용해서 원하는 메시지에 대한 처리를 수행할 멤버함수를 추가하고 구성원 함수에 필요한 코드를 삽입하는 식으로 하면 윈도우 응용 프로그램을 쉽게 개발할 수 있다.

기존의 DLL은 API 함수레벨의 export만 가능한데 MFC에서는 확장 라이브러리(extension library)라는 MFC 클래스 라이브러리를 DLL로 제공할 수 있는 라이브러리가 존재한다. MFC 확장 라이브러리 덕분에 MFC 기반의 다양한 라이브러리 나왔는데 특히 UI 부분의 라이브러리 중 대표적인 것을 몇 가지 소개하고자 한다.

BCG ControlBar

BCG ContraolBar 라이브러리는 BCGSoft(www.bcgsoft.com)에서 유료로 판매하고 있는 라이브러리로 현재 v5.9까지 소개됐는데 간단한 이미지 버튼에서부터 MS 오피스, XP, 비주얼 스튜디오 또는 아웃룩 등에서 선보이고 있는 대부분의 최신 UI를 지원하고 있다. 이 라이브러리는 언어팩을 지원해 다국어 지원이 가능하고(한글도 지원한다), 응용 프로그램 레벨에서 사용자 취향에 따라 새로운 스킨을 적용할 수 있는 Visualization Manager 기능을 제공하고 있다.

BCG ControlBar의 또 다른 장점이 있다면 비주얼 스튜디오와의 통합환경의 제공이다. AppWizard 지원은 물론 Skin Wizard, Build Wizard 그리고 MSDN과의 통합 도움말을 지원하고 있다. <그림 2>는 BCG ControlBar에서 지원하고 있는 AppWizard의 예이고 <그림 3>은 BCG ControlBar를 이용해 구축한 프로그램의 예이다(BCG ControlBar는 2001년 1월 30일자 MSDN에 소개된 바 있다).

CJLibrary와 Xtreme 툴킷

CJLibrary와는 Xtreme Toolkit은 codejock software에서 제작된 MFC 지원 UI 라이브러리로 특히 CJLibrary는 소스까지 공개된 공개 라이브러리로 초기에 선보였을 때 많은 개발자들에 의해 사용됐다. 하지만 CJLibrary는 codejock 사이트에서 현재는 찾아볼 수 없는데 codejock software가 CJLibrary를 상용 버전인 Xtreme 툴킷 라이브러리를 판매하고 있기 때문이다. CJLibrary의 마지막 버전은 V6.09로, 비주얼 스튜디오와 아웃룩 UI 지원은 물론 AppWizard 통합 기능까지 지원하는 등 공개인 점을 감안하면 앞서 설명한 BCG Controlbar 못지 않은 기능을 제공하고 있다. 이 라이브러리는 codejock 사이트에서 찾아볼 수 없지만, 인터넷을 찾아보면 이 버전을 구할 수 있다. 제공되는 UI는 최신 것은 아니지만, MFC를 좀 심도있게 공부하고 싶은 독자라면 소스를 구해서 참고하기 바란다. BCG ControlBar와 Xtreme 툴킷 라이브러리 모두 해당 사이트에 가면 평가판을 구할 수 있으니 직접 사용해보기 바란다.

ITCLib

ITClib는 CodeJock에서 CJLibrary가 선보일 당시, 그러니까 대략 1998년 당시에 Dev Central에서 소개한 MFC 기반의 UI 지원 라이브러리로 1998년 이후로 버전 업은 되고 있지 않지만 Dev Central 사이트에 가면 아직까지도 소스와 라이브러리를 구할 수 있다. 기능은 CJLibrary와 거의 유사하다. @