SoEnty 실습 프로젝트

개발 동기

개발자는 자신의 원하는 프로그램을 직접 만들어 쓰기도 한다. 필요한 모든 프로그램이 다 발표되어 있지만 그래도 딱 내가 원하는 형태의 프로그램을 입맛대로 만들어 쓸 수 있다는 것은 일반인이 누릴 수 없는 특권이다. 나도 그런 편인데 핸드폰에 나만 쓰는 프로그램이 몇 개 있고 데스크탑에도 이것 저것 만들어 쓰고 있다.

당근 텍스트 편집기는 내가 제일 즐겨 쓰는 프로그램이고 일정 관리는 글통으로, 이미지 뷰어도 직접 만들어 쓴다. 그 중에 이것 저것 잡다한 유틸리티를 모아 놓은 SahngHyungUtil이라는 프로그램이 있는데 수년간 무척 편리하게 잘 써 먹었다. 간단한 프로그램이 별도의 창은 없고 트레이에서 실행된다.

 

클립보드 자동 변환기는 서식을 싹 빼 버리고 텍스트만 추출해 주는데 집필 작업에 꼭 필요하다. 스크린 세이버 무력화 기능은 보안이 엄격한 회사에서 유용하며 장시간 통계를 돌릴 때 유용하다. 계산기나 진법 변환기도 개발자에게는 필수 유틸리티이다.

올 봄에 키보드 통계 프로그램을 작성했는데 CPU 점유율에 항상 관심을 가지고 쳐다 보게 되었다. 문제는 작업 관리자가 너무 거대해 화면을 꽤 많이 차지하며 불필요한 정보까지 보여 주어 불편한 면이 있었다. 이걸 좀 작게 만들어 볼까 하다가 그동안 모아 놓은 유틸리티를 하나로 합쳐 보자는 생각에 이르렀다.

노안이 오니 트레이의 시계는 너무 작아 잘 보이지 않아 좀 크게 보였으면 좋겠고 메모리나 하드 디스크의 상황도 한 눈에 보고 싶다. 이런 저런 여러 기능을 좁은 면적에 잘 구현해 놓으면 아기자기하게 귀여울 거 같고 실용성도 높을 것 같았다.

과거 윈도우7에 가젯이라는 기능이 있었는데 지금은 사라졌다. 이런 형태로 작은 앱을 구현해 볼까 하다가 아예 앱바 형태로 하나로 합쳐 버리자 싶어 개발을 시작했다. 처음에는 내가 필요로 하는 기능만 만들어 나 혼자 쓸려고 했는데 만들다 보니 공개해도 될 정도로 유용할 거 같아 본격적인 프로젝트로 만들기 시작했다.

정밀한 설계나 치밀한 기획없이 일단 짜고 보자는 식으로 만들다 보니 형식성이 떨어지고 퇴근 후 틈틈이 작업하다 보니 코드의 품질도 과히 좋지는 않다. 디자인도 직접 하니 UI의 어색함은 더 말할 필요도 없고 내가 필요한대로만 만들다 보니 사용법도 직관적이지 않다.

회사 프로젝트는 장기간에 걸쳐 회의도 하고 피드백도 받아 가며 만드는데 비해 취미로 만드는 프로젝트는 그럴 수가 없다. 한마디로 이 프로젝트에 임하는 나의 태도는 지극히 아마추어적일 수밖에 없다. 그래서 개발 실습 예제로 오히려 더 쉽고 유용할지도 모른다. 따라하는데 굉장한 시간과 노력이 드는데다 선행 지식도 많아 쉽지는 않다. 그러나 비록 대충이라도 선배 개발자가 만든 프로젝트가 신입 개발자나 후배들에게 어느 정도는 도움이 되지 않을까 하여 강좌로도 기획하게 되었다.

이 프로젝트의 개발자는 회사에서 실무에 시달리고 있는 상황이라 이 강좌가 꾸준히 진행되리라고 보장할 수 없으며 언제 완료될지도 알 수 없다. 그러나 언제건간에 평생 유지하고 관리할 프로젝트로 설정했으므로 계속 진행됨은 보장할 수 있다.

개발 방법 선택

뭔가를 만들기로 했으면 개발툴이나 개발 방법을 잘 선택해야 한다. 원하는 목적이 무엇인가를 분명히 하고 적절한 도구를 선택해야 이후의 개발이 원활하고 순조롭다. 중간에 바꾸기는 무척 어렵기 때문에 이 결정부터 신중하게 해야 한다. 먼저 개발 언어부터 선택하는데 대략 서너가지 후보가 있다.

 

C : 작고 빠른 유틸리티의 특성에는 가장 부합하지만 편의성이 떨어진다.

C++ : 약간 느리지만 객체 지향의 이점을 충분히 누릴 수 있다.

C# : UI 작업이 쉽고 컬렉션이나 라이브러리를 마음껏 사용할 수 있다.

자바 : 멀티 플랫폼을 구현할 수 있지만 느리고 가상머신상에서 실행되어 제약이 많다.

 

이 중 나는 C++을 선택했다. C는 너무 오래된 언어이고 코드를 관리하기가 번거롭다. C#은 업무에서도 쓰고 있고 편의성이 상당하지만 아무래도 속도를 걱정하지 않을 수 없다. 요즘 컴퓨터 성능은 유틸리티 정도는 충분히 커버하지만 문제는 CPU 점유율이다. 상시 실행할 프로그램은 CPU1~2% 정도로만 먹어야 부담없는데 대략 10%선까지 차지해 버리면 안된다. 개발자가 힘들어도 프로그램은 작고 빠르게 만드는게 장땡이다.

C++ 언어로 개발하는 방법도 여러 가지가 있다. Win32 API는 작고 빠르고 자유도가 높지만 편의성이 떨어지고 MFC는 정교한 라이브러리여서 개발 속도가 겁나게 빠르다는 이점이 있다. 이건 참 결정하기 어려운데 자유로운 개발을 위해 Win32를 직접 사용하되 MFC의 장점 일부를 흡수해서 메시지 핸들러 정도만 차용해서 쓰기로 한다. 둘 다 어차피 MS에서 버린 자식이기는 마찬가지이다.

다음은 비트수를 결정한다. 시대에 맞게 64비트로 개발하는게 맞을 거 같지만 의외로 64비트가 32비트에 비해 이점이 거의 없다. 속도가 더 빠를 거 같지만 포인터가 64비트여서 오히려 더 느리고 프로그램도 비대해진다. 메모리를 마음껏 쓸 수 있다는 장점이 있지만 유틸리티는 불과 10M 정도만 해도 차고 넘치니 그럴 필요가 없다. 32비트를 선택하되 64비트로도 컴파일 가능하게 준비 정도만 해 둔다.

C/C++은 문자 인코딩에 따라 프로젝트 구성이 달라지는데 C/C++은 아직도 ANSI/UNICODE를 동시에 지원한다. 이건 더 고민할 필요도 없이 UNICODE여야 한다. ANSI로 컴파일 할 일도 없고 두 인코딩을 동시에 컴파일하도록 호환성을 확보할 필요도 없다. 문자열은 L" "로 바로 기록하기로 하되 함수는 가급적 양쪽을 지원하는 것으로 쓰기로 한다.

윈도우의 기본 그래픽 엔진인 GDI는 오래된 라이브러리여서 불편하고 기능 제약도 많다. 반면 업그레이드 버전인 GDI+는 화려한 그래픽을 그릴 수 있고 JPG, PNG 이미지도 지원하여 활용성이 높지만 속도는 다소 느리다. 기본 입출력은 GDI를 사용하되 꼭 필요한 부분에서만 GDI+를 활용하기로 한다.

프로그램의 실행 정보를 저장하는 방법은 INI 파일과 레지스트리 두 가지가 있다. 공식적으로는 레지스트리를 권장하지만 이 프로젝트는 INI 파일을 사용하기로 결정했다. 구닥다리 방법이지만 굳이 INI 파일을 선택한 이유는 다음과 같다.

 

- 무설치 프로그램 제작 : 레지스트리에 초기 정보가 있어야 한다면 설치 프로그램이 필요하다. 복사하면 바로 쓸 수 있어야 하니 INI가 배포하기 편하다.

- 포터블 환경 : INI는 파일이어서 디스크 포맷이나 시스템 이동시에도 설정 정보를 그대로 가져갈 수 있는 이점이 있다. 컴퓨터를 바꿔도 설정은 그대로 유지된다.

- 속도상의 불이익 해소 : INI는 파일에 직접 입출력하기 때문에 레지스트리에 비해 많이 느리다는 결정적인 단점이 있다. 그러나 SSD의 보급으로 인해 지금은 이런 단점이 거의 없어졌다.

- 복수 인스턴스 지원 : 실행 파일 위치의 INI 파일을 읽도록 하면 버전별로 각각의 정보를 유지하기 쉽고 폴더를 구분하면 두 개 이상의 인스턴스를 서로 방해받지 않고 실행할 수 있다.

 

개발툴은 비주얼 스튜디오 2019를 사용하기로 한다. 무료 개발툴이지만 이 정도 유틸리티를 개발하기에는 그럭 저럭 괜찮다. 다만 Win32 프로젝트에 대한 지원이 부족하고 한글 버전에 심각한 버그가 많아 좀 불편한 면은 있다.

용어 선택

개발을 처음 시작할 때 이것 저것 이름 붙일게 많은데 이게 생각보다 어렵고 정확한 명칭을 부여하는데 시간이 오래 걸린다. 먼저 이 프로젝트의 이름부터 정해야 하는데 여러 가지 후보명을 생각했었다. 초기의 이 고민은 기획서에 그대로 남아 있다.

SoEnUtil, Board, Launcher 등등을 처음 생각했다가 작은 유틸리티라는 뜻의 SoEnTy로 이름을 결정했다. 대단한 의미가 있다기 보다는 그냥 발음하기 좋은 적당한 길이로 선택한 것이다. 최초 SoEnTy로 프로젝트를 만들었다가 대문자가 세 개나 있는데 싫어져 중간에 SoEnty로 변경했는데 이를 위해 프로젝트 전체를 새로 만들어야 했다. 이래서 처음에 이름을 잘 정하는게 중요한 것이다.

최초 생각했던 기능은 시계, CPU 점유율, 계산기 정도 였었고 좀 더 기획해 보니 그 외에도 포함할만한 기능이 많았다. 그동안 만들어 놓았던 코드오 있고 또 만들고 싶은 것도 많기 때문이다. 어떤 기능을 더 넣을지 연습장에 끄적거려 본 내역은 다음과 같다.

 

 

초기 기획을 보면 각 기능별로 DLL로 찢어 제작 API를 공개하도록 했는데 이렇게 되면 거의 플랫폼이 되는 셈이라 너무 거대해진다. 배포 계획은 일단은 공개용이며 소스와 제작 기법까지 다 공개하니 그럴 수밖에 없다. 그러나 차후 기능이 많이 업그레이드되면 셰워웨어나 기부금 정도는 받을 수 있지 않을까도 기대했었다. 물론 이건 잘 만들고 난 후의 일이다.

프로그램을 구성하는 작은 기능은 항상 보이는 것과 아이콘 형태로 하단에 있는 두 가지가 있는데 이 둘의 이름을 뭘로 붙일지도 고민을 많이 했다. 정보 제공 기능은 최초 패널, 칸 등으로 이름을 기획했고 실제로 코드에서도 Kan으로 칭했었다. 중간에 네모, Appy 등의 이름도 생각했으나 굳이 한글 이름을 붙일 필요도 없고 무난한게 좋아 보였다. 고민끝에 두 가지 단어를 골라냈다.

 

Widget : 작은 응용 프로그램이라는 뜻이며 영어 사전에도 '작은 창지'라는 뜻으로 등재되어 있다. PC, 웹용, 모바일용이 있으며 요즘은 핸드폰에도 있어 아주 친숙한 용어이다. 실행도 가능하지만 상시적으로 정보를 보여준다는 의미가 강하다.

Gadget : PC용 위젯을 특히 가젯이라고 부르는데 MS와 구글에서만 이 용어를 사용한다. 윈도우 7에 잠시 도입되었지만 지금은 쓰지 않는다. 사실 위젯과 가젯은 명확히 구분하기 어렵다. 참고로 원래 발음은 '개짓'인데 우리나라에서는 이렇게 부르지 않는다.

 

작은 앱을 위젯이라고 부르고 있으니 이 용어가 가장 적합한 것 같다. 초기의 Kan 용어를 모두 Widget으로 바꾸는데 꽤 시간이 걸렸다. 위젯은 그나마 좀 쉽게 이름을 결정했는데 아래쪽 트레이에 배치되는 더 작은 앱을 부를 마땅한 이름이 참 애매하다.

사실 트레이는 쟁반이라는 뜻인데 이 용어부터 부정확하다. 그러나 윈도우에서 그렇게 부르니 트레이는 일단 받아 들이기로 했다. 트레이 안의 작읍 앱은 아이콘이라고 부르며 최초 Icon으로 코딩을 했었다. 그러나 아이콘은 표시 형식의 하나일 뿐이어서 동작까지 포함하는 앱의 정확한 이름이 아니다. 적당한 이름을 찾기 위해 여러번의 개명을 거쳤다.

 

Gadget : 위젯보다 더 작다는 뜻으로 붙여볼까 했는데 오히려 더 헷갈릴 거 같고 공식적으로도 맞지 않는 표현이라 이내 포기했다.

Koma : 한동안 이 이름으로 개발을 했다. 한글 명칭을 쓰겠다고 트레이는 Kori, 그 안의 아이콘은 꼬마앱으로 칭했는데 이 둘만 해도 이름이 무척 헷갈린다. 좀 유치한 작명인 것 같아 결국 바꿨다.

Tidget : 이건 아예 내가 만든 용어이다. Tiny Widget의 합성어이며 작은 위젯이라는 뜻이되 발음은 타이젯이 아닌 티젯으로 부르기로 했다. Tiget도 고려했는데 이건 호랑이라는 뜻이 있어 제외했다.

 

앞으로도 이 용어가 바뀔 가능성은 얼마든지 있다. 그러나 프로젝트가 커질수록 명칭을 바꾸는 것은 점점 어려워진다. 당분간은 이 용어를 계속 쓰기로 한다.

CWindow

루트 클래스

원고는 나중에 쓰고 소스부터 나열해 놓기로 한다.

 

// 상호 참조하는 상황이라 전방 선언이 필요하다.

class CWindow;

 

// 윈도우 맵 관리 클래스

class CRegisterHelper

{

public:

    CRegisterHelper();

    ~CRegisterHelper();

 

    struct _arObj

    {

        CWindow* pObj;

        HWND hWnd;

    } *arObj;

    int arSize;

    int nReg;

 

    CWindow* FindObject(HWND hWnd);

    void AddObject(HWND hWnd, CWindow* pObj);

    void RemoveObject(HWND hWnd);

};

 

// 전역 유일 객체

CRegisterHelper _RegisterHelper;

HINSTANCE g_hInst;

 

// 루트 윈도우 클래스

class CWindow

{

public:

    HWND win_hWnd;

    // 이하는 런타임 데이터. 이름을 x, y 따위로 지어 놨더니 지역 변수랑 헷갈려 win_ 접두 붙임

    int win_x;                     // 현재 X 좌표

    int win_y;                     // 현재 Y 좌표

    int win_w;                     // 현재 너비

    int win_h;                     // 현재 높이

    COLORREF win_color;               // 현재 배경색

    HBRUSH win_backBrush;            // 배경 브러시

    HBITMAP win_dblBuffer;         // 더블버퍼링용 비트맵

 

    TCHAR win_name[32];               // 문자열로 된 위젯 설명. UI에 사용

    TCHAR win_id[32];                // 위젯의 고유 명칭. INI 파일의 섹션명에 사용

 

    CWindow(LPCTSTR name, LPCTSTR id) {

        lstrcpy(this->win_name, name);

        lstrcpy(this->win_id, id);

        win_backBrush = NULL;

        win_dblBuffer = NULL;

    }

    ~CWindow() {

        if (win_backBrush != NULL) DeleteObject(win_backBrush);

        if (win_dblBuffer != NULL) DeleteObject(win_dblBuffer);

        // 객체 파괴시 맵에서 스스로 제거한다.

        if (_RegisterHelper.arObj) _RegisterHelper.RemoveObject(win_hWnd);

    }

 

    // 평이한 차일드 윈도우로 생성하되 마지막 인수로 this를 전달하여 객체맵에 등록한다.

    // 이 형태와 다른 윈도우(확장 스타일, 좌표 지정) Create를 재정의하여 직접 정의한다.

    virtual void Create(HWND hParent, int child_id = 0, LPCTSTR title = L"", DWORD style = WS_CHILD | WS_VISIBLE) {

        CreateWindow(TEXT("CWindow"), title, style,

           0, 0, 0, 0, hParent, (HMENU)(ULONG_PTR)child_id, g_hInst, this);

    }

 

    virtual void LoadSetting() {}

    virtual void SaveSetting() {}

    virtual LRESULT OnMessage(UINT iMessage, WPARAM wParam, LPARAM lParam);

    virtual LRESULT OnCreate(WPARAM wParam, LPARAM lParam) { return 0; }

    virtual LRESULT OnDestroy(WPARAM wParam, LPARAM lParam) { return 0; }

    // OnPaint는 일단 루트가 받은 후 더블버퍼링 준비를 해서 가상 함수를 호출한다. 원하지 않을 경우 직접 재정의한다.

    virtual LRESULT OnPaint(WPARAM wParam, LPARAM lParam);

    virtual void OnDraw(HDC hdc, RECT crt) { }

    virtual LRESULT OnTimer(WPARAM wParam, LPARAM lParam) { return 0; }

    virtual LRESULT OnSize(WPARAM wParam, LPARAM lParam);

    virtual LRESULT OnCommand(WPARAM wParam, LPARAM lParam) { return 0; }

    virtual LRESULT OnContextMenu(WPARAM wParam, LPARAM lParam) { return 0; }

    virtual LRESULT OnKeyDown(WPARAM wParam, LPARAM lParam) { return 0; }

    virtual LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam) { return 0; }

    virtual LRESULT OnMouseMove(WPARAM wParam, LPARAM lParam) { return 0; }

    virtual LRESULT OnLButtonUp(WPARAM wParam, LPARAM lParam) { return 0; }

    virtual LRESULT OnLButtonDblClk(WPARAM wParam, LPARAM lParam) { return 0; }

    virtual LRESULT OnMouseWheel(WPARAM wParam, LPARAM lParam) { return 0; }

    virtual LRESULT OnUser1(WPARAM wParam, LPARAM lParam) { return 0; }

    virtual LRESULT OnUser2(WPARAM wParam, LPARAM lParam) { return 0; }

    virtual LRESULT OnDropFiles(WPARAM wParam, LPARAM lParam) { return 0; }

 

    static LRESULT CALLBACK CWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam);

};

 

상호 참조 관계라 선언 먼저 해야 함. 도우미의 구현 코드

 

CRegisterHelper::CRegisterHelper()

{

    WNDCLASS WndClass;

 

    WndClass.cbClsExtra = 0;

    WndClass.cbWndExtra = 0;

    // 더블버퍼링이 기본 제공되어 배경 브러시는 없다.

    WndClass.hbrBackground = NULL;

    WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);

    WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);

    WndClass.hInstance = GetModuleHandle(NULL);

    // 윈도우 프로시저와 윈도우 클래스명이 고정되어 있다.

    WndClass.lpfnWndProc = (WNDPROC)CWindow::CWindowProc;

    WndClass.lpszClassName = L"CWindow";

    WndClass.lpszMenuName = NULL;

    // 더블클릭을 기본 지원한다.

    WndClass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;

    RegisterClass(&WndClass);

 

    nReg = 0;

    arSize = 100;

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

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

}

 

CRegisterHelper::~CRegisterHelper()

{

    free(arObj);

    arObj = NULL;

}

 

CWindow* 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, CWindow* 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->win_hWnd = hWnd;

    nReg++;

}

 

void CRegisterHelper::RemoveObject(HWND hWnd)

{

    int i, j;

 

    // 객체 제거시 윈도우를 파괴하지 말아야 한다. 객체 파괴 전에 수동으로 윈도우를 직접 파괴해야 한다.

    // 상속받은 가상 파괴자는 다형적으로 동작하지 않아 파괴중의 WM_DESTROY를 처리할 OnMessage를 정확하게 찾지 못한다.

    // 이에 대한 문법적 이론에 대해서는 당근 1.2버전 강좌 21-1-사 항을 참고한다.

    // 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--;

}

 

RemoveObject에서 윈도우 파괴하지 않음을 주의

CWindow의 구현 코드

 

// 기본 메시지를 처리해 주며 차일드는 메시지 핸들러로 받는다. 그 외의 메시지는 차일드가 직접 처리해야 한다.

LRESULT CWindow::OnMessage(UINT iMessage, WPARAM wParam, LPARAM lParam) {

    switch (iMessage)

    {

    case WM_CREATE:

        return OnCreate(wParam, lParam);

    case WM_DESTROY:

        return OnDestroy(wParam, lParam);

    case WM_SIZE:

        return OnSize(wParam, lParam);

    case WM_PAINT:

        return OnPaint(wParam, lParam);

    case WM_TIMER:

        return OnTimer(wParam, lParam);

    case WM_COMMAND:

        return OnCommand(wParam, lParam);

    case WM_CONTEXTMENU:

        return OnContextMenu(wParam, lParam);

    case WM_KEYDOWN:

        return OnKeyDown(wParam, lParam);

    case WM_LBUTTONDOWN:

        return OnLButtonDown(wParam, lParam);

    case WM_MOUSEMOVE:

        return OnMouseMove(wParam, lParam);

    case WM_LBUTTONUP:

        return OnLButtonUp(wParam, lParam);

    case WM_LBUTTONDBLCLK:

        return OnLButtonDblClk(wParam, lParam);

    case WM_MOUSEWHEEL:

        return OnMouseWheel(wParam, lParam);

    case WM_USER + 1:

        return OnUser1(wParam, lParam);

    case WM_USER + 2:

        return OnUser2(wParam, lParam);

    case WM_DROPFILES:

        return OnDropFiles(wParam, lParam);

    }

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

}

 

void DrawBitmap(HDC hdc, int x, int y, HBITMAP hBit)

{

    HDC MemDC;

    HBITMAP OldBitmap;

    int bx, by;

    BITMAP bit;

 

    MemDC = CreateCompatibleDC(hdc);

    OldBitmap = (HBITMAP)SelectObject(MemDC, hBit);

 

    GetObject(hBit, sizeof(BITMAP), &bit);

    bx = bit.bmWidth;

    by = bit.bmHeight;

 

    BitBlt(hdc, x, y, bx, by, MemDC, 0, 0, SRCCOPY);

 

    SelectObject(MemDC, OldBitmap);

    DeleteDC(MemDC);

}

 

LRESULT CWindow::OnPaint(WPARAM wParam, LPARAM lParam)

{

    HDC hdc;

    PAINTSTRUCT ps;

    HDC hMemDC;

    HBITMAP oldBit;

 

    RECT crt;

    GetClientRect(win_hWnd, &crt);

    hdc = BeginPaint(win_hWnd, &ps);

    if (win_dblBuffer == NULL) {

        win_dblBuffer = CreateCompatibleBitmap(hdc, crt.right, crt.bottom);

    }

    hMemDC = CreateCompatibleDC(hdc);

    oldBit = (HBITMAP)SelectObject(hMemDC, win_dblBuffer);

    FillRect(hMemDC, &crt, win_backBrush);

 

    // 차일드에게 메모리 DC를 생성하여 전달한다.

    OnDraw(hMemDC, crt);

 

    SelectObject(hMemDC, oldBit);

    DeleteDC(hMemDC);

    DrawBitmap(hdc, 0, 0, win_dblBuffer);

    EndPaint(win_hWnd, &ps);

    return 0;

}

 

// 크기가 바뀌면 더블버퍼링 비트맵을 새로 만든다.

// 파생 클래스는 WM_SIZE를 받았을 때 CWindow::OnSize부터 먼저 호출해야 한다.

LRESULT CWindow::OnSize(WPARAM wParam, LPARAM lParam)

{

    if (win_dblBuffer != NULL) {

        DeleteObject(win_dblBuffer);

        win_dblBuffer = NULL;

    }

    return 0;

}

 

// 모든 윈도우의 메시지 프로시저는 이 함수 하나 뿐이다. CWindow의 정적 함수로 포함시켰다.

LRESULT CALLBACK CWindow::CWindowProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)

{

    CWindow* pWin;

 

    pWin = _RegisterHelper.FindObject(hWnd);

    if (pWin == NULL) {

        // WS_THICKFRAME 스타일이 있을 때 가장 먼저 오며 이때 lParam MINMAXINFO여서 객체를 찾을 수 없다.

        if (iMessage == WM_GETMINMAXINFO) {

           return 0;

        }

        pWin = (CWindow*)((LPCREATESTRUCT)lParam)->lpCreateParams;

        _RegisterHelper.AddObject(hWnd, pWin);

    }

 

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

}

 

더블버퍼링 기본 지원

윈도우 프로시저는 윈도우 핸들과 객체 맵을 중계해 주기만 한다.

, WM_GETMINMAXINFO 메시지는 약간 주의가 필요하다.

여기까지가 도우미 클래스들이다. 필요한 윈도우를 클래스로 선언한다.

 

// 메인 윈도우 클래스 선언

class CMainWindow : public CWindow

{

public:

    // 필요한 멤버를 선언한다.

    TCHAR sTime[128];

    HWND hBtn;

    enum { BTN };

 

    // 생성자에서 필요한 초기화 처리

    CMainWindow() : CWindow(L"main", L"main") {   }

 

    // CWindow Create와 다를 경우만 재정의한다. 대부분의 차일드는 재정의 불필요하다.

    void Create();

 

    // 관심있는 메시지의 핸들러만 재정의한다.

    LRESULT OnMessage(UINT iMessage, WPARAM wParam, LPARAM lParam);

    LRESULT OnCreate(WPARAM wParam, LPARAM lParam);

    LRESULT OnDestroy(WPARAM wParam, LPARAM lParam);

    LRESULT OnTimer(WPARAM wParam, LPARAM lParam);

    void OnDraw(HDC hdc, RECT crt);

    LRESULT OnCommand(WPARAM wParam, LPARAM lParam);

};

 

void CMainWindow::Create()

{

    // 윈도우 클래스는 무조건 CWindow로 지정한다.

    win_hWnd = CreateWindow(L"CWindow", L"main", WS_OVERLAPPEDWINDOW,

        10, 10, 640, 480, NULL, (HMENU)NULL, g_hInst, this);

}

 

LRESULT CMainWindow::OnMessage(UINT iMessage, WPARAM wParam, LPARAM lParam) {

    // 핸들러가 정의되지 않은 메시지만 직접 처리한다. 그럴게 없으면 OnMessage 자체가 불필요하다.

    switch (iMessage)

    {

    case WM_NCHITTEST:

        int nHit = (int)DefWindowProc(win_hWnd, WM_NCHITTEST, wParam, lParam);

        return nHit;

    }

 

    // 핸들러가 정의되어 있는 메시지는 핸들러를 찾아 호출한다.

    return CWindow::OnMessage(iMessage, wParam, lParam);

}

 

LRESULT CMainWindow::OnCreate(WPARAM wParam, LPARAM lParam)

{

    hBtn = CreateWindow(TEXT("button"), TEXT("Click Me"), WS_CHILD | WS_VISIBLE |

        BS_PUSHBUTTON, 10, 50, 100, 25, win_hWnd, (HMENU)BTN, g_hInst, NULL);

    SetTimer(win_hWnd, 1, 1000, NULL);

    return 0;

}

 

LRESULT CMainWindow::OnDestroy(WPARAM wParam, LPARAM lParam)

{

    KillTimer(win_hWnd, 1);

    PostQuitMessage(0);

    return 0;

}

 

LRESULT CMainWindow::OnTimer(WPARAM wParam, LPARAM lParam)

{

    SYSTEMTIME st;

 

    switch (wParam) {

    case 1:

        GetLocalTime(&st);

        wsprintf(sTime, TEXT("지금 시간은 %d:%d:%d입니다"),

           st.wHour, st.wMinute, st.wSecond);

        InvalidateRect(win_hWnd, NULL, TRUE);

        break;

    }

    return 0;

}

 

void CMainWindow::OnDraw(HDC hdc, RECT crt)

{

    // 더블 버퍼링되므로 출력만 하면 된다.

    TextOut(hdc, 10, 10, sTime, lstrlen(sTime));

}

 

LRESULT CMainWindow::OnCommand(WPARAM wParam, LPARAM lParam)

{

    switch (LOWORD(wParam)) {

    case BTN:

        MessageBox(win_hWnd, L"버튼을 클릭했습니다.", L"알림", MB_OK);

        break;

    }

 

    return 0;

}

 

// 메인 윈도우 전역 객체 선언

CMainWindow mainWindow;

 

차일드 하나 거느리고 타이머를 돌리는 간단한 윈도우이다.

WinMain은 다음과 같다.

 

// 메인 윈도우 생성하고 메시지 루프만 돌리면 된다.

int APIENTRY WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance

    , _In_ LPSTR lpszCmdParam, _In_ int nCmdShow)

{

    MSG Message;

    g_hInst = hInstance;

 

    mainWindow.Create();

    ShowWindow(mainWindow.win_hWnd, nCmdShow);

 

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

        TranslateMessage(&Message);

        DispatchMessage(&Message);

    }

    return (int)Message.wParam;

}

 

더 할게 없다. 나머지는 모두 객체에 코드를 작성하면 된다.