MSDN Magazine > Home > All Issues > 2008 > June >  GUI Library: Windows Forms의 간편함을 네이티브 응용 프로그램에 ...
GUI Library
Windows Forms의 간편함을 네이티브 응용 프로그램에 구현
John Torjo

이 기사에서는 다음 내용에 대해 설명합니다.
  • GUI 프로그래밍의 문제점
  • 창 개체 만들기
  • 이벤트 및 알림 처리
  • 폼 및 컨트롤
이 기사에서 사용하는 기술:
Win32 API, C++
C++에서 GUI 프로그래밍의 문제점은 대부분의 라이브러리 수준이 너무 낮아 프로그래머의 작업 부담이 크다는 것입니다. 이러한 라이브러리는 C와 유사한 구조체에 의존하며 라이브러리의 래퍼 클래스는 복잡성을 제대로 숨기지 못합니다. 또한 라이브러리를 통해 이벤트 프로그래밍이 충분히 단순화되는 것도 아니기 때문에 기본 WM_ 메시지에 대한 지식이 필요합니다.
이 기사에서는 GUI 응용 프로그램을 다루기 위한 고수준 인터페이스를 클라이언트 프로그래머에게 제공하도록 필자가 작성한 eGUI++라는 C++ 라이브러리를 소개합니다. 이 라이브러리는 복잡성을 숨깁니다. 즉, WM_ 메시지에 대한 지식이 전혀 필요 없으므로 이벤트 프로그래밍을 매우 쉽게 수행할 수 있습니다. C와 유사한 원시 구조를 다룰 필요 없이 항상 클래스를 처리하면 됩니다. 대체로 eGUI++ 클라이언트 코드는 간단히 읽고 쓸 수 있습니다.
eGUI++는 Windows® 전용입니다. 필자는 매우 간단한 응용 프로그램이거나 단순한 테스트 프레임워크, 프로토타입 또는 교육용이 아닌 이상 플랫폼 간 호환이 가능한 GUI 응용 프로그램을 그다지 신뢰하지 않습니다. 무엇보다 필자는 기본 OS에서 제공하는 모든 이점을 활용하는 것이 바람직하다고 생각합니다. Windows XP와 Windows Vista®의 경우 그 이점은 상당합니다.

이식 가능한 네이티브 코드
CLR 코드를 찾는 개발자라면 이미 관리되는 C++가 있는 상태입니다. 이는 훌륭한 플랫폼이므로 개선할 필요가 없습니다. 그러나 Windows 2000 이상 버전의 운영 체제를 위한 네이티브 Windows 코드를 생성하기에 적절한 라이브러리를 찾고 있는 개발자라면 이 기사를 계속 읽어 보시기 바랍니다. 대상 OS의 이점을 활용하는 사용이 간편한 라이브러리를 얻게 될 것입니다. 또한 Microsoft® .NET Framework가 필요 없습니다. 여러분이 작성할 코드는 C++와 유사합니다. 또한 이 코드는 Visual C++ 컴파일러에만 국한되지 않습니다. 원한다면 g++ (GNU C++ Compiler) 4.1을 사용하여 컴파일할 수도 있습니다. 기본적으로 Win32® API를 래핑할 경우 이식 가능한 코드를 작성하지 못할 이유는 없습니다.
복잡한 GUI의 경우 Visual Studio® 2005나 Visual Studio 2008 Express과 같은 우수한 IDE가 필요합니다. 필자는 Visual Studio 2005 Express 이상 버전에 통합하고 더 향상된 GUI 환경을 제공하기 위해 라이브러리를 손질했습니다. 이 라이브러리에서 필자는 새 GUI 클래스를 만들거나 기존 클래스를 확장할 때 IDE가 최대한 도움이 되도록 코드 완성 기능에 집중했습니다.
필자의 희망 사항은 GUI 응용 프로그램을 즐겁게 작성하는 것이므로 쉽게 읽고 쓸 수 있는 GUI 코드를 목표로 하여 eGUI++를 만들었습니다. 예를 들어 가능한 모든 곳에 코드 완성 기능을 구현했습니다. 따라서 이제 GUI 프로그래밍을 안전하게 수행할 수 있습니다(오류가 있으면 가능한 한 컴파일 시에 포착하며 그렇지 않은 경우 런타임 예외 발생). 또한 eGUI++는 리소스 편집기 사용에 유리합니다(Visual Studio 2005 이상 버전의 리소스 편집기와 통합됨).

windows.h는 그만
windows.h를 포함할 때의 가장 큰 문제는 오류 발생의 소지가 크다는 것입니다. 도착하지 않는 이벤트를 모니터링하는 문제는 어떻게 막아야 할까요? 예를 들어 키보드 이벤트를 기다리는 단추 클래스가 있지만 키보드 이벤트가 발생하지 않는 경우를 생각해 보십시오.
그렇다면 windows.h가 굳이 왜 필요할까요? C++에서 일반적인 C++ 클래스를 사용하여 Windows 응용 프로그램을 작성할 수 있습니다. 따라서 windows.h의 내부 사항, 즉 WM_LBUTTONDBLCLK, WM_LBUTTONUP을 비롯한 장황한 이벤트 이름, LPNMITEMACTIVATE, NMHDR 또는 기타 모호한 C 구조에 대해서는 몰라도 됩니다. C 스타일 캐스트도 더 이상 사용되지 않습니다. 우수한 C++ GUI 라이브러리의 가장 중요한 기능은 Win32 API를 추상화하여 개발자가 클래스만 다루도록 하는 것입니다.
그림 1과 같은 원시 C 구조를 지겹도록 다루어 보셨겠지요. 필자 역시 마찬가지입니다. 그래서 이제는 다음과 같이 코드를 작성합니다.
wnd<rebar> w = new_(parent);
rebar::item i(rebar::item::color | rebar::item::text);
w->add(i); 
// 이전 방식 
hwndRB = CreateWindowEx(WS_EX_TOOLWINDOW,
  REBARCLASSNAME, NULL,
  WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|
  WS_CLIPCHILDREN|RBS_VARHEIGHT|
  CCS_NODIVIDER,
  0,0,0,0, hwndOwner, NULL, g_hinst, NULL);
...
rbi.cbSize = sizeof(REBARINFO);  
rbBand.cbSize = sizeof(REBARBANDINFO);
rbBand.fMask  = RBBIM_COLORS | RBBIM_TEXT |
  RBBIM_BACKGROUND;
rbBand.fStyle = RBBS_CHILDEDGE;
여기에서 볼 수 있듯이 eGUI++는 복잡성을 숨깁니다. 즉, 개발자는 C++ 클래스만 다루면 됩니다. CreateWindowEx와 같은 복잡한 API 함수, 상수 이름(WS_* 상수) 또는 REBARPARAMINFO 같은 복잡한 C 구조를 기억할 필요가 없습니다.
eGUI++는 Windows 2000 이상 버전을 대상으로 합니다. 기본적으로 Windows XP SP2를 대상으로 하지만 사용 가능한 기능이 더 많거다 적은 다른 OS를 대상으로 선택할 수 있습니다. 다른 OS를 대상으로 지정하려면 eGUI++ 헤더를 추가하기 전에 #define EGUI_OS를 다른 OS 상수로 정의하기만 하면 됩니다(그림 2 참조).
// eGUI++ 코드
struct os {
 typedef enum type {
 win_2k,
 win_2k_sp4,
 win_xp,
 win_xp_sp2,
 win_vista
 };
};

#ifndef EGUI_OS
#define EGUI_OS os::win_xp_sp2
#endif
코드에 #ifdef가 거의 없기 때문에 읽기가 쉽습니다. 대신 일부 속성이 OS별로 고유한 것을 알 수 있습니다.
property<int,os::win_xp> some_prop;
예를 들어 이전 OS를 대상으로 하고 위의 속성을 사용하려고 시도하면 컴파일 시 오류가 발생합니다.

각 창 처리
대부분의 경우 개체의 수명은 프로그래머가 결정합니다. 하지만 GUI 프로그래밍에서 한 가지 중요한 문제는 시각적 창과 창 개체 사이에 차이가 존재하며, 창이 닫히는 시점을 개발자가 아닌 사용자가 결정한다는 점입니다. 따라서 코드에는 사용자에 의해 창이 소멸된 창 개체에 대한 유효한 포인터 또는 참조가 있을 수 있습니다. 이로 인해 화면에 표시된 창이 개체 인스턴스를 나타내고, 개체 인스턴스가 화면에 표시된 창을 나타내는 일 대 일 관계를 유지하기 어렵게 됩니다. 이 시나리오에서 한 가지 분명한 제약은 로컬 창 인스턴스를 사용할 수 없다는 점입니다(로컬 창 인스턴스는 범위가 존재할 때 소멸될 수 있음).
{
form f(...);
f.show();
...
}
화면에 f라는 폼이 있다고 가정해 보십시오. f의 범위를 벗어날 때 어떤 동작이 수행되도록 해야 할까요? 두 가지 중 하나를 선택할 수 있습니다. 즉, 화면에서 폼을 소멸시키거나 해당 C++ 인스턴스가 소멸되더라도 폼을 화면에 그대로 두는 방법이 있습니다. 그러나 두 가지 방법 모두 마땅치 않습니다. 전자의 경우 폼이 갑자기 사라져 사용자가 혼란스러울 수 있고, 후자의 경우 폼은 화면에 그대로 있지만 해당 C++ 인스턴스가 소멸되었기 때문에 폼이 이벤트에 반응하지 않습니다. 또한 f가 아직 범위 내에 있는 상태에서 사용자가 이미 화면의 폼을 닫았을 수도 있습니다. 따라서 화면에 존재하지 않는 개체에 대해 작업을 수행하는 결과를 낳을 수 있습니다.
이 문제의 해결 방법은 항상 포인터(간접 포인터)를 통해 창에 액세스하는 것입니다. 그러면 사용자가 화면의 창을 닫을 때 해당 C++ 인스턴스는 유효하지 않은 것으로 표시되며, 해당 인스턴스에 액세스하려고 하면 예외가 발생합니다. 다음과 같이 창이 유효한지 여부를 항상 확인할 수 있고 직접 창을 소멸시킬 수 있습니다.
wnd<> w = ...;
// 창이 유효한가?
if ( is_valid(w) ) w->do_something();
// 창이 유효한가?
if ( w) w->do_something();

// 창 닫기
delete_(w);
화면의 각 창에는 해당 C++ 인스턴스가 있으므로 창에 대한 공유(참조 횟수 계산됨) 포인터를 나타내는 wnd<> 템플릿 클래스를 사용하여 창을 처리하면 됩니다. wnd<> 클래스에는 하나의 옵션 인수로 창 유형이 있습니다. 이 옵션 인수가 제공되는 것은 당연합니다. 기본 유형은 window_base입니다. 또는 text, label, rebar, edit 등의 창 클래스가 될 수도 있습니다. 그림 3에는 창 개체 사용과 이러한 개체 간 캐스팅을 보여 주는 몇 가지 예가 나와 있습니다.
// 창을 만들 때 유형을 지정할 수 있음
wnd<> w = new_<form>(parent);
w->bg_color( rgb(0,0,0));

// 창을 만들 때 유형을 지정하지 않으면
// 유형을 지정하도록 할당한 사람을 기반으로 유형을 유추함
wnd<button> b = new_(w, rect(10,10,200,20) );
b->events.click += &my_func;

// 창 닫기
delete_(b);

// 캐스팅 – 실패할 경우 예외 발생
wnd<form> f = wnd_cast(w);

// 캐스팅 – 실패할 경우 Null 반환
if ( wnd<edit> e = try_wnd_cast(e) )
  e->text = "not nullio";
창은 new_ 함수를 사용하여 만들고 delete_ 함수를 사용하여 삭제합니다. 역시 창은 대부분 사용자가 닫습니다. 또한 유형이 X인 창(기본 유형은 window_base)이 있는데 Y 유형에도 해당되는지 확인하려는 경우에는 캐스팅을 사용하면 됩니다. 캐스팅은 항상 명시적입니다. 캐스팅에는 실패하면 예외를 발생시키는 wnd_cast와 실패하면 Null 창을 반환하는 try_wnd_cast의 두 가지 유형이 있습니다.
eGUI++ 클래스를 개발할 때 기본 클래스에서 파생하는 것 외에 크기 조정 기능, 스킨 적용 기능 등의 부가적인 동작을 상속해야 하는 경우도 있습니다. 이러한 경우 재사용 가능한 동작 클래스를 여러 개 만들어 여기에서 추가로 파생하면 됩니다.

알기 쉬운 코드
쓰기와 읽기과 쉬운 GUI 코드는 보는 사람의 기분을 상쾌하게 합니다. 필자는 코드 완성 기능을 최대한 활용하기 위해 책자에 소개된 모든 팁을 이용했습니다. 크기가 큰 GUI 라이브러리와 씨름하다 보면 속성 이름, 이벤트 이름, 플래그 등을 잊기 쉽습니다. 필자는 이 문제를 해결했습니다. 문서화를 위해 doxygen을 사용했는데, 그 효과는 상당히 뛰어납니다. 사용할수록 마음에 드는 도구입니다.
문서는 매우 쉽게 탐색할 수 있습니다. 속성 이름의 경우 그림 4와 같이 w->를 입력하기만 하면 메서드와 속성이 표시됩니다. 속성은 멤버 변수로 표시되므로 구분하기 쉽습니다. 이벤트 이름의 경우도 클래스에서 처리할 수 있는 이벤트를 쉽게 기억할 수 있습니다. class_name::ev::만 입력하면 코드 완성 기능이 작동하여 범위 연산자 뒤에 이벤트를 표시합니다. eGUI++의 진정한 장점은 플래그 처리에 있습니다. 플래그의 조합으로 지정될 수 있는 각 속성의 경우 플래그 속성에 "."만 추가하면 역시 코드 완성 기능이 사용 가능한 플래그 옵션을 표시합니다. 보너스로 필자는 속성에 대한 연산자 오버로드를 추가했습니다. 따라서 다음과 같은 코드가 가능합니다.
w->text = "hello";
w->text += " world";
w->style |= w->style.tiled;
그림 4 코드 완성
또한 사용 가능한 컨트롤 목록이 기억나지 않을 경우에 대비하여 egui::ctrl이라는 네임스페이스를 추가했습니다.

컨트롤 대 폼
Win32 GUI 프로그래밍 경험이 있다면 대화 상자에 익숙할 것입니다. 또한 API에서 대화 상자 생성(::CreateDialog)을 처리하는 방식이 창 생성(::CreateWindow[Ex])과는 전혀 다르다는 사실도 알고 있을 것입니다. 프로그래머는 사용법이 서로 전혀 다른 복잡한 이 두 함수를 기억할 필요가 없습니다. 두 가지 모두 창의 한 유형이며 eGUI++에서 창은 한 가지 방법, 즉 new_ 함수를 통해서만 만들어집니다.
여러 가지 창 유형을 다루는 경우에는 이름 대화 상자보다 이름 폼이 더 적절합니다. 이름 폼은 데이터가 포함된 다른 컨트롤을 표시하는 창을 기술합니다. 두 가지 모두 괜찮지만 필자는 폼을 선호합니다. 실제 코드에서는 다음과 같이 나타납니다.
typedef form dialog;
개념적으로 보면 창의 유형에는 컨트롤과 폼, 두 가지밖에 없습니다. 컨트롤은 사용자가 수정할 수 있는 데이터를 표시하는 창입니다. 모든 컨트롤 클래스는 "control" 클래스에서 파생됩니다. 폼은 하나 이상의 컨트롤과 자체 논리(예: 일부 데이터의 사용자 조작을 허용하는 논리)가 포함된 창입니다.
각 창 유형의 사용 가능한 기능은 창의 용도에 따라 다릅니다. 예를 들어 폼에서는 자식 컨트롤을 열거할 수 있지만 컨트롤에서는 불가능합니다. 이를 통해 코드에서 오류가 발생할 가능성이 줄어듭니다. 또한 컨트롤을 만들어야 하는 경우는 극히 드뭅니다. 리소스 편집기를 사용하여 이미 폼에 컨트롤을 배치한 경우가 대부분이기 때문입니다.
폼 자체에는 모달 대화 상자와 메시지 상자의 두 가지 형태가 있습니다. 모달 대화 상자를 만들려면 폼을 만들 때 form::style::modal만 추가하면 됩니다. 메시지 상자를 만들려면 msg_box<> 함수를 사용하고 단추를 템플릿 인수로 지정하면 됩니다.
if ( msg_box<mb::ok | mb::cancel>("q") == mb::ok)
    std::cout << "ok pressed";
또한 msg_box<>는 컴파일 시에 단추 조합이 작동하는지 여부를 인식합니다.
// ok
   msg_box<mb::yes | mb::no>("q");
   // 컴파일 타임 오류
   msg_box<mb::ok | mb::yes>("q");

폼 프로그래밍
다시 한 번 말하지만 폼은 Win32 API의 대화 상자에 해당합니다. Windows Forms에서는 폼 프로그래밍이 더 나은 전략으로 입증되었습니다. 각 폼에는 몇 개의 컨트롤이 있으며 각 폼은 하나의 작업을 처리합니다. 구식이고 복잡한 SDI(단일 문서 인터페이스)나 MDI(다중 문서 인터페이스) 대신 컨트롤 또는 다른 폼을 포함할 수 있는 탭을 사용할 수 있습니다. 따라서 CFrameWnd, CMDIChildWnd 또는 이와 비슷한 어떤 것도 사용할 필요가 없습니다. 하나의 폼에 여러 개의 폼을 호스팅해야 하는 경우에는 tab_form 클래스를 사용합니다. 이 클래스를 사용하면 자식 폼을 각각 고유한 탭에 추가할 수 있습니다.

폼 처리
필자는 마법사를 싫어하지만 가끔은 프로그래밍 작업에 도움이 된다고 생각합니다. 여기에서는 폼을 만들기 위해 New Class Wizard(새 클래스 마법사)를 만들었습니다. Class View(클래스 뷰)에서 Add Class(클래스 추가)를 선택하고 Categories(범주)에서 eGUI를 선택합니다. 왼쪽에서 eGUI Form(eGUI 폼)을 선택하고 Add(추가)를 클릭합니다. 그리고 클래스 이름을 지정하면 됩니다(그림 5). 그러면 마법사가 <dlgname>.h라는 헤더 파일, <dlgname>.cpp라는 소스 파일 및 <dlgname>_form_resource.h라는 추가 헤더 파일(eGUI++가 내부적으로 유지하는 파일)을 만듭니다.
그림 5 클래스 추가(더 크게 보려면 이미지를 클릭하십시오.)
마지막 헤더 파일에는 폼에 사용되는 컨트롤의 이름이 모두 들어 있습니다. 따라서 MFC에서와 같이 컨트롤 변수를 추가로 만들고 데이터 교환을 사용할 필요가 없습니다. 직접 컨트롤을 사용하면 됩니다. 그림 6과 같이 편집 상자 두 개(User Name 및 Password)와 단추 두 개(OK 및 Cancel)가 있는 로그인 대화 상자를 가정해 보겠습니다.
그림 6 편집 상자와 단추
다음 파일이 생성됩니다.
// login.h
#pragma once
#include "login_form_resource.h"
struct login : form, 
 private form_resource::login {};

// login.cpp
#include "stdafx.h"
#include "login.h"
코드는 매우 간단합니다. "enum {IDD = ... }" 같은 마법사 스타일의 코드나 메시지 맵이 없습니다. 원하지 않으면 사용자 지정 생성자를 제공하지 않아도 됩니다. 기본 생성자로도 충분합니다.
login 클래스는 login_form_resource.h(이 파일은 eGUI++ 라이브러리에 의해 유지됨)에 구현된 form_resource::login에서 전용으로 파생됩니다. form_resource::login 클래스에는 폼의 컨트롤에 대한 정보(컨트롤 이름 및 형식, 컨트롤의 알림을 포착하는 기능)가 포함되어 있습니다. 파생의 액세스 가능성 유형을 변경할 수도 있지만 이는 필자의 생각으로는 바람직하지 않습니다. 클래스의 멤버 데이터가 보통 전용인 것과 마찬가지로 폼의 컨트롤도 대개 전용이어야 합니다.
생성된 form_resource::login은 다음과 유사합니다.
// login_form_resource.h
#pragma once
struct form_resource::login {
 // ... (통지 처리를 허용하는 코드)
 wnd<edit> username;
 wnd<edit> passw;
 wnd<button> ok, cancel;
};
이를 통해 폼의 컨트롤을 손쉽게 조작할 수 있습니다. 암호가 "secretword"인지 확인하려는 경우를 예로 들어 보겠습니다.
void login::on_button_click(ev::button_click &, ok_) {
 if ( passw->text == "secretword")
 { pass_ok = true; visible = false; }
}
여기에서 볼 수 있듯이 Visual Basic®에서와 마찬가지로 폼을 숨기려면 visible 속성을 false로 설정하기만 하면 됩니다.

구식 ID의 제거
개발자라면 리소스 편집기를 다루어 본 경험이 있을테고, 따라서 리소스 편집기에서 ID_, IDD_, IDC_, IDR_, IDS_ 등의 다양한 리소스 접두사 형식이 사용된다는 사실을 알고 있을 것입니다. 접두사는 리소스 편집기에서는 유용하지만 코드에서는 기억하거나 신경 쓸 필요가 없는 부가적인 정보일 뿐입니다. eGUI++ 응용 프로그램에서는 이러한 접두사가 완전히 무시되므로 기억할 필요가 없습니다.
예를 들어 이전 이름(username, passw, ok, cancel)은 리소스 편집기의 바로 가기이며 eGUI++ 라이브러리는 ID* 접두사를 자동으로 제거합니다. 아마 원래 이름은 IDC_username, IDC_passw, IDOK 및 IDCANCEL 정도였겠지요.

이벤트 및 알림
앞서 설명했듯이 WM_ 메시지는 전혀 기억할 필요가 없습니다. 사실 이벤트는 익숙해지기 어렵습니다. 그 숫자가 워낙 많기 때문에 응답 가능한 이벤트를 알아내고 이러한 이벤트에 응답할 수 있는 쉬운 방법이 필요합니다. 또한 손쉽게 폼의 컨트롤에서 발생하는 이벤트에 대해 알림을 수신하고 컨트롤을 확장하고 사용자 고유의 이벤트를 추가할 수 있는 방법이 필요합니다.
모든 창 클래스(컨트롤 또는 폼)는 이벤트를 생성할 수 있습니다. 각 클래스에는 모든 이벤트를 포착하는 이벤트 처리기가 있습니다. 각 이벤트에는 이벤트를 처리하기 위한 함수가 정의됩니다. 이 함수는 가상 함수이며 그 구현에서는 아무런 작업도 수행하지 않습니다. 각 이벤트 처리기 함수의 유일한 인수는 이벤트 데이터입니다.
기존 컨트롤의 경우 해당하는 이벤트 클래스는 handle_events::control_name입니다. 각각의 기존 eGUI++ 창 클래스 wnd_name은 이미 handle_events::wnd_name에서 파생됩니다. 기존 창 클래스를 확장하면 언제든지 해당 이벤트를 처리할 수 있습니다. 여기에서는 단순하게 구현하기 위해 모든 이벤트 처리기 함수는 "on_"으로 시작합니다. 예를 들면 다음과 같습니다.
struct my_btn : button {
 void on_char(ev::char& e) {
 cout << "typed " << e.ch;
 }
};
다른 GUI 라이브러리를 사용해 본 경험이 있다면 실제로는 눈에 보이는 것만큼 간단하지 않다는 사실을 알 것입니다. 기존 컨트롤은 이벤트를 보내지 않고 대신 알림을 보냅니다. 알림의 형태는 WM_COMMAND/WM_NOTIFY 메시지이며 컨트롤 자체가 아니라 컨트롤의 부모에게 전송됩니다. 알림을 받아야 하는 대상이 컨트롤의 부모(폼)이므로 이 동작은 타당한 것처럼 보입니다. 그러나 이 경우 컨트롤 클래스를 확장하기가 매우 어렵게 됩니다. 현재 파일 시스템을 시각화하기 위해 트리를 만든다면 어떨까요? 컨트롤의 부모에 전송되는 이벤트(예: 항목 확장(TVN_ITEMEXPANDING))를 포착해야 합니다. 그런 다음에는 알림을 컨트롤 자체로 내려 보낼 방법이 필요합니다.
eGUI++에서 알림은 이벤트입니다. 따라서 알림은 항상 컨트롤로 전송된 후 컨트롤의 부모에게 전송됩니다. 상속을 통해 컨트롤 클래스를 확장할 때 각 알림은 서로 다른 이벤트로 변환됩니다. 예를 들어 사용자가 첫 번째 열을 편집할 때 편집 컨트롤 대신 콤보 상자를 표시하는 목록 컨트롤을 만들려는 경우 코드는 다음과 같습니다.
struct list_with_combo : list {
 ...
 void on_begin_label_edit(
  ev::begin_label_edit & e) {
 e.allow_default = false;
 combo->rect(...);
 combo->visible = true;
 }
 wnd<combo_box> combo;
};
이벤트를 처리하기 위해 다음과 같이 이벤트 처리기 함수를 오버로드합니다.
struct my_btn : button {
 void on_char(ev::char& e);
};
여기서는 문자 입력 이벤트에 응답하거나, Win32 API를 선호한다면 WM_CHAR 메시지에 응답합니다.
on_my_event 이벤트 처리기 함수의 경우 이벤트 인수의 형식은 항상 ev::my_event입니다. 클래스에서 처리 가능한 모든 이벤트는 ev:: 구조체입니다. ev::만 입력하면 코드 완성 기능에 의해 클래스에서 처리 가능한 모든 이벤트가 표시됩니다(그림 7 참조). 이벤트 정보를 확인하는 가장 쉬운 방법은 e.를 입력하고 코드 완성 기능으로 이벤트와 관련된 모든 데이터가 표시되도록 하는 것입니다(그림 8 참조).
그림 7 코드 완성 기능으로 이벤트 표시
그림 8 이벤트 정보 확인
문서에서 컨트롤의 이벤트를 찾을 수 있습니다. 컨트롤을 선택한 다음 ev 클래스를 선택하기만 하면 모든 해당 이벤트가 표시됩니다. 라이브러리는 여러 이벤트 처리기에 같은 이벤트를 보낼 수 있습니다. 예를 들어 알림은 컨트롤에 전송된 후 해당 컨트롤의 부모에 전송됩니다.
모든 이벤트에는 .sender 속성이 있습니다. 이 속성은 이벤트를 보낸 컨트롤이며 알림에 유용합니다(특히 알림을 누가 보냈는지 확인하려는 경우). 모든 이벤트에는 .handled 속성이 있습니다. 이 속성은 handled_partially(기본값)와 handled_fully, 두 개의 값을 가질 수 있습니다. 이 속성을 handled_fully로 설정하면 이벤트 처리를 중지할 수 있습니다. 이 경우 이벤트 처리기가 더 있더라도 호출되지 않습니다. 예를 들어 edit 클래스를 확장하는 경우 텍스트 변경 사항을 부모에 알리지 않으려면 다음과 같은 코드를 작성합니다.
struct independent_edit : edit {
                      void on_change(ev::change &e) {
                      e.handled = handled_fully;
                      }
                     };
위에서 볼 수 있듯이 컨트롤 확장은 간단합니다. 그런데 폼의 알림 처리도 마찬가지로 간단해야 합니다. 알림을 처리할 때는 해당 알림을 보낸 주체(e.sender)를 알아야 합니다. 더 중요한 점은 특정 컨트롤에서 전송된 알림을 처리할 수 있어야 한다는 것입니다. 따라서 이벤트 처리기 함수는 컨트롤 이름을 추가 인수로 받습니다. 컨트롤 이름 뒤에는 밑줄(_)이 붙습니다. 예를 들어 사용자가 사용자 이름 편집 상자에 입력하는 내용을 확인하려면 다음 코드를 실행합니다.
void login::on_change(
 edit::ev::change &e, username_) {
 cout << "name=" << e.sender->text;
}
미국 달러와 유로화 간에 통화를 변환하는 경우를 가정해 보겠습니다. EUR 상자에 값을 입력하고 키를 누르면 USD 상자의 값이 업데이트됩니다. USD 상자에 값을 입력하고 키를 누르면 EUR 상자의 값이 업데이트됩니다(그림 9 참조). 이를 구현하는 코드는 다음과 같습니다.
struct convert : form, form_resource::convert {
 double rate; 
 convert() : rate(1.5) {}
 int mul_str(const string& a, double b) { ... }
 void on_change(edit::ev::change&, eur_) {
 usd->text = mul_str ( eur->text, rate); }
 void on_change(edit::ev::change&, usd_) {
 eur->text = mul_str ( usd->text, 1/rate); }
};
그림 9 통화 변환기
이 코드는 쉽게 이해할 수 있습니다. mul_str은 string을 double로 변환하고 여기에 환율을 곱해 double과 string의 곱을 구합니다.
위와 같이 이벤트를 처리하려면 상당한 작업이 필요합니다. 편집 상자가 3개 있는 폼을 생각해 보십시오. 각 편집 상자는 특정 이벤트 집합을 생성합니다. 각 이벤트(예: on_change)에 대해 각각의 컨트롤별로 다시 정의할 수 있는 함수를 하나씩 만들 수 있습니다.
void on_change(edit::ev::change& e, ctrlname_);
또는 다음과 같이 다시 정의할 수 있는 함수를 하나만 만들 수도 있습니다.
void on_change(edit::ev::change& e);
필자는 전자를 선호합니다. 클라이언트 코드가 훨씬 더 간단하고 Visual Basic 방식과 더 비슷하기도 하기 때문입니다. 처리하는 대상도 쉽게 확인할 수 있습니다. 반면 후자의 경우 이벤트 구현 내에서 e.sender를 통해 이벤트를 생성한 컨트롤을 수동으로 확인해야 합니다.
이러한 이유로 필자는 첫 번째 방법을 구현했습니다. 그러나 내부적으로는 많은 작업이 수행됩니다. eGUI++는 리소스 편집기를 모니터링하면서 새 컨트롤이 추가되거나 컨트롤의 이름이 바뀌면 모든 <dlgname>_form_resource.h 파일을 업데이트합니다. form_resource::<dlgname> 클래스의 각 <dlgname>_form_resource.h 파일에 대해 기존 컨트롤의 모든 알림을 다시 정의하고, 이렇게 다시 정의한 각 알림에 대해 이를 보낼 수 있는 컨트롤을 찾아야 합니다. 다음 단계는 각 컨트롤에 대해 다시 정의가 가능한 다른 함수로 진행하는 구현을 생성하는 것입니다. 예를 들어 그림 10에는 편집 상자와 단추가 두 개씩 있는 로그인 폼의 코드가 나와 있습니다.
struct form_resource::login {
  wnd<edit> name;
  wnd<edit> passw;
  wnd<button> ok, cancel;

  typedef ... ok_;
  typedef ... cancel_;
  typedef ... name_;
  typedef ... passw_;

  virtual void on_change(edit::ev::change& e, name__) {}
  virtual void on_change(edit::ev::change& e, passw__) {}

  virtual void on_change(edit::ev::change& e) {
    if ( e.sender == name) on_change(e, name__());
    else if ( e.sender == passw) on_change(e, passw__());
  }
  // ... 다른 편집 통지에도 동일

  virtual void on_click(button::ev::click & e, ok__) {}
  virtual void on_click(button::ev::click & e, cancel__) {}
  virtual void on_click(button::ev::click & e) {
    if ( e.sender == ok) on_click(e, ok__());
    else if ( e.sender == cancel) on_click(e, cancel__() );
  }
  // ... 다른 단추 통지에도 동일
};
마지막으로 new_event<>에서 파생시키는 방법으로 사용자 고유의 이벤트를 만들 수 있습니다. 기존 이벤트를 전송하든 사용자 고유의 이벤트를 전송하든 과정은 동일합니다. 즉, send_event 함수를 사용합니다.
struct hover : new_event<hover> {
 int x,y; // 위치
 hover(int x,int y) : x(x),y(y) {}
};

w->send_event( hover(x,y) );
이 라이브러리는 스레드로부터 안전합니다. 주제와 다소 벗어난 이야기지만 각 창에는 m_cs 뮤텍스 변수(기본적으로 CRITICAL_SECTION)가 있습니다. 필자는 각 메서드 액세스가 스레드로부터 안전하도록 하기 위해 이 변수를 사용했습니다. 창 클래스를 확장하는 경우 m_cs 변수를 재사용할 수도 있고 직접 만들 수도 있습니다.

메뉴, 바로 가기 등
GUI 프로그래밍 경험이 있다면 메뉴 명령과 키를 모두 누를 경우 WM_COMMAND가 전송된다는 사실을 알 것입니다. 따라서 WM_COMMAND를 수신하는 경우 이 이벤트가 컨트롤에서 전송된 것인지 아니면 메뉴(또는 바로 가기 키)에서 전송된 것인지를 확인하기가 어렵습니다. eGUI++는 폼(대화 상자)에 직접 메뉴를 배치함으로써 일단 첫 번째 문제를 해결합니다. 즉, 폼에 전송된 명령이 단추에서 전송된 것이 아니라면 메뉴에서 전송된 것입니다.
이로써 메뉴 명령에 대한 문제는 해결되었습니다. 다음으로 바로 가기의 경우를 살펴보겠습니다. 바로 가기에서 문제는 아무 때나 입력될 수 있다는 점입니다. 예를 들어 편집 상자 안에서 입력되는 경우도 있습니다. 바로 가기 키(단축 키)는 먼저 직접 관련된 창, 이 창을 호스팅하는 폼, 폼의 부모 등의 순서에 따라 최상위 창에 도달할 때까지 계속 라우팅됩니다. 바로 가기에 대한 이벤트 처리기가 처음 발견되면 처리가 중지됩니다(하나의 바로 가기는 두 개 이상의 창에 의해 처리되지 않음).
이제 도구 모음이 남았습니다. 도구 모음은 메뉴 및 바로 가기와 함께 처리됩니다. 사용자가 도구 모음 단추를 누르면 이벤트는 메뉴 명령으로 변환되어 호스팅하는 폼으로 직접 라우팅됩니다. 명령의 출처가 메뉴인지, 바로 가기인지 또는 도구 모음 단추인지는 관계없습니다.
다음과 같이 메뉴 명령을 처리하는 폼을 구현한다고 가정해 보십시오.
void on_menu_command( ev::menu&, 
 menu::some_menu_id) { ... }
new_file과 open_file, 두 가지의 메뉴 명령을 처리하기 위해 다음 처리기를 만듭니다.
void on_menu_command( ev::menu&,
    menu::new_file) { ... }
   void on_menu_command( ev::menu&,
    menu::open_file) { ... }

탭 컨트롤 및 폼
탭은 보편적으로 사용되는 GUI 패러다임입니다. 필자는 탭 컨트롤을 확장하여 tab_type 속성이 normal 또는 one_dialog_per_tab(이 경우 컨트롤이 다른 폼을 호스팅함) 값을 갖도록 했습니다. 후자의 경우 다음과 같이 새 폼을 추가할 수 있습니다.
tab->add_form<form_type>( new_([args]) );
앞서 언급한 로그인 폼을 추가하려면 다음과 같은 코드를 작성합니다.
tab->add_form<login>( new_() );
폼을 하나 이상 추가한 후에는 이 탭 폼이 보유할 탭의 수를 지정할 수 있습니다.
tab->count = 5;
여기에서 탭 수는 5개입니다. 이 코드는 마지막으로 추가된 폼을 가져와 필요한 수만큼 복제합니다. 이전에 탭이 하나뿐이었다면 첫 번째 탭의 폼이 4번 더 복제됩니다. 이와 같이 기존 창을 자유롭게 복제할 수 있습니다.
지금까지 간섭적인(intrusive) 이벤트 처리에 대해 설명했습니다. 즉, 창 클래스를 확장하고 최종적으로 해당 이벤트에 응답하거나 폼을 구현하는 경우에는 알림에 응답하게 됩니다. 그러나 서로 별 관계가 없는 여러 창에 적용되는 하나의 동작을 구현해야 하는 경우도 있습니다.
크기 조정 기능과 스킨 적용 기능을 예로 들어 보겠습니다. 이 두 기능은 간섭적 방식으로 구현할 수 있습니다(동작을 구현하는 클래스를 만든 다음 여기에서 GUI 클래스를 파생). 그러나 이 방법은 코드가 복잡해지며 적합하지 않은 경우도 있습니다(예: 스킨 적용 기능). 비간섭적인 방식으로 동작을 구현하면 다른 응용 프로그램에서 이 동작을 재사용할 수 있으며 동작을 해제하기도 쉽습니다.
예를 들어 비간섭적인 이벤트 처리 클래스를 만든 다음 이 클래스의 인스턴스를 만들어 등록합니다. 새 창이 만들어지면 처리기 인스턴스에 그 사실이 알려지며 이 인스턴스를 모니터링하도록 선택할 수 있습니다. 인스턴스를 모니터링하는 경우 모니터링할 이벤트를 다음과 같이 수동으로 지정해야 합니다.
// 모니터 단추 클릭
struct btn_handler : non_intrusive_handler {
 void on_new_window_create(wnd<> w) {
 if ( wnd<button> b = try_cast(w)) {
  b->events.on_click += mem_fn(&on_click,this);
 }
 }
 void on_click(button::ev::click&) { ... }
};
이벤트 처리기를 등록하기는 쉽습니다. 다음과 같이 하면 됩니다.
btn_handler bh;
window_base::add_non_intrusive_handler(bh);

크기 조정 기능은 응용 프로그램에 따라 다양한 방법으로 구현할 수 있습니다. 예를 들어 각 폼에 대한 on_size 이벤트를 다시 정의하고 새 폼 크기를 기준으로 컨트롤의 위치를 업데이트할 수 있습니다. 그러나 별로 좋은 생각이 아니고 작업량도 많습니다. 또는 각 폼에서 "a.x = b.x + b.width + 4"와 같이 컨트롤 사이의 관계를 만들 수 있습니다. 이 방법은 매우 유연하지만 역시 작업량이 많습니다.
이러한 방법 대신 각 폼에서 컨트롤을 각 축을 따라 이동이 가능하거나 크기 조정이 가능한 것으로 표시할 수 있습니다. 컨트롤이 특정 축을 따라 크기 조정 가능한 것으로 표시되면 폼 크기가 변경될 때 해당 컨트롤 크기가 업데이트됩니다. 컨트롤이 특정 축을 따라 이동 가능한 것으로 표시되면 폼 크기가 변경될 때 컨트롤이 이동됩니다. 이 방법은 대부분의 응용 프로그램에 충분히 사용할 수 있습니다. 이 아이디어는 WTL의 CResizeWindow에서 착안하여 비간섭적 처리기로 구현한 것입니다. 그림 11과 같은 대화 상자를 가정해 보겠습니다. 크기를 조정했을 때 그림 12와 같은 모양이 되도록 하려면 다음 코드를 사용합니다.
resize(name, axis::x, sizeable);
resize(desc, axis::x | axis::y, sizeable);
resize(ok, axis::x | axis::y, moveable);
resize(cancel, axis::x | axis::y, moveable);
그림 11 대화 상자
그림 12 크기 조정된 대화 상자
실패한 GUI 작업은 모두 예외를 트리거합니다. 이를 통해 여러분은 문제가 발생했음을 알게 됩니다. 디버그 모드에서는 이 경우 실패한 어설션이 생성되고 프로그램이 디버그 모드로 진입합니다. 이렇게 하면 문제가 발생했다는 사실을 시각적으로 확인한 후 버그를 찾을 수 있기 때문에 오류를 무시하고 넘어가는 것보다 훨씬 더 바람직합니다.

Visual Studio 2005와의 통합
Visual Studio는 훌륭한 IDE이며 확장이 가능하다는 큰 장점을 갖고 있습니다. eGUI++는 이러한 장점을 활용하여 New Form Class Wizard(새 폼 클래스 마법사)를 제공하는 추가 기능을 제공합니다. 또한 폼의 컨트롤 알림을 처리할 수 있도록 Visual Basic과 유사한 표시줄을 제공합니다. 컨트롤을 선택하기만 하면 해당 컨트롤에서 생성할 수 있는 알림의 목록이 표시됩니다. 이 목록에서 이미 처리된 알림은 굵게 표시됩니다. 이벤트를 클릭하면 처리기가 없는 경우 처리기가 추가됩니다. 이에 대한 예는 그림 13에 나와 있습니다.
그림 13 폼의 컨트롤 알림 처리(더 크게 보려면 이미지를 클릭하십시오.)
내부적으로 eGUI++는 변경 사항이 발생할 때마다 필요한 경우 _form_resource.h를 업데이트할 수 있도록 리소스 편집기를 모니터링합니다. 앞서 설명했듯이 이 경우 코드 완성 기능이 함께 사용됩니다.

동작 구현
GUI를 구축한 후에는 동작을 구현하고 데이터 바인딩을 허용해야 합니다. 폼은 데이터 수집 전용인 경우가 많습니다. 우선 generic 폼 클래스를 구현할 수 있습니다. 이 클래스는 생성 시에 조작할 데이터를 가져와 폼의 컨트롤에 바인딩합니다. 그런 다음 데이터 유효성 검사에 사용할 규칙 집합을 지정할 수 있습니다. 소멸 시에 유효성 규칙이 충족되면 원래 데이터가 컨트롤에서 가져온 값으로 업데이트되며 그렇지 않은 경우 원본 데이터는 변경되지 않습니다. 따라서 각각의 새 폼에 대해 리소스 편집기에서 폼을 만든 후 데이터 유효성 검사에 사용할 규칙 집합만 지정하면 됩니다(새 폼 클래스를 만들고 논리를 복제할 필요가 없음).
더 나아가 STL(표준 템플릿 라이브러리) 배열 및 컬렉션과 목록 컨트롤 및 트리 컨트롤 사이의 격차를 메울 수 있습니다. 직원으로 구성된 배열과 목록 컨트롤을 예로 들어 보겠습니다. 다음과 같이 배열을 컨트롤에 바인딩할 수 있습니다.
list_ctrl->bind(employees);
예상할 수 있듯이 이 경우 목록 컨트롤이 업데이트됩니다. 또한 목록 컨트롤의 셀이 변경되면 해당 내용이 자동으로 직원 배열과 동기화됩니다.
필자가 eGUI++를 구축한 목적은 GUI 프로그래밍을 즐겁게 해 주는 괜찮은 라이브러리를 만드는 것이었습니다. C++ 프로그래머라면 아마 필자의 의도에 공감할 것입니다. 소스와 바이너리는 torjo.com에서 다운로드할 수 있습니다.

John Torjo는 10년째 프로그래밍을 하고 있지만 C++에 대한 열정은 여전한 프로그래머입니다. John은 로깅과 GUI, 그리고 도전을 즐깁니다. 도전 과제가 있다면 전자 메일로 John에게 알려 주시기 바랍니다. 자세한 정보는 torjo.com을 참조하십시오.

Page view tracker