3. 기본 그리기

안녕하세요. 언제나휴일입니다.

1. DC와 그리기 절차

Windows API에서 그리기는 DC(Device Context)를 이용합니다.

DC란 출력에 필요한 여러 정보를 가지고 있는 구조체로 라인이나 경계를 그릴 때 사용하는 펜, 면을 채울 때 사용하는 브러쉬 등의 정보를 갖고 있습니다.

DC를 사용하지 않고 그리기를 한다고 가정하면 선을 그리기 위해 두 점의 좌표 뿐만 아니라 선의 형태, 선의 두께, 선의 색상 정보들을 인자로 전달해야 합니다. 사각형을 그리기 위해서는 좌표 뿐만 아니라 경계 선의 형태, 선의 두께, 선의 색상 정보, 면을 채울 색상, 패턴 등의 인자가 필요하겠죠. 이처럼 그리기를 위해 전달해야 하는 인자를 단순화하기 위해 Windows API에서는 DC를 사용하고 있습니다.

Windows API에서는 그리기 위해 DC를 발급받습니다. 그리고 그리기에 사용할 펜이나 브러쉬 등의 그리기 개체를 생성합니다. 생성한 그리기 개체는 그리기 작업을 수행하기 전에 DC에 선택할 수 있습니다. 이 후 그리기 작업을 수행하면 DC에 선택한 그리기 개체를 이용하여 그리기를 수행합니다. 따라서 실제 그리기 작업을 할 때 사용하는 함수에는 DC에 선택한 그리기 개체 정보는 전달할 필요가 없으며 DC의 핸들을 전달하면 내부에서 DC에 선택한 그리기 개체를 이용하여 그리기 작업을 수행합니다.

다음은 그리기 작업을 할 때의 기본적인 수행 흐름입니다.

[그림] 그리기 작업 수행 절차

2. WM_PAINT 메시지

윈도우에 다른 윈도우에 의해 가려졌다가 보여지거나 최소화 후에 최대화를 하는 등의 작업을 수행하면 다시 그려주어야 하는 영역이 생깁니다. 윈도우즈 운영체제에서는 다른 창에 의해 가려지는 영역을 클리핑 영역으로 기억해 두었다가 해당 영역이 다시 보여지면 그 부분을 포함하는 최소한의 사각 영역을 무효화 영역이 발생한 것으로 처리합니다. 이 때 발생하는 윈도우 메시지가 WM_PAINT입니다.

그런데 윈도우즈 프로그램에서 무효화 영역이 생긴다고 바로 WM_PAINT 메시지를 발생하여 처리하는 것은 아닙니다. 일반적으로 윈도우즈 프로그램에서 그리기 작업은 다른 작업들보다 처리 우선순위가 낮습니다. 따라서 응용 메시지 큐에 처리할 윈도우 메시지가 없고 무효화 영역이 있을 때 WM_PAINT 메시지를 만들어 집니다. 만약 무효화 영역이 있지만 처리할 윈도우 메시지가 응용 메시지 큐에 남아있다면 응용 메시지 큐에 남아있는 메시지가 사라질 때까지 WM_PAINT 메시지는 만들어지지 않습니다. 이런 상태에서 다시 무효화 영역이 생기면 기존 무효화 영역과 합산하여 동시에 WM_PAINT가 두 번 발생하지 않게 처리하고 있습니다.

WM_PAINT 메시지 처리기에서는 무효화 영역을 계산하고 DC를 발급받는 것부터 시작합니다. 그리고 그리기 작업을 마친 후에 DC를 해제하고 무효화 영역을 유효화 영역으로 변경해 주어야 합니다. 만약 그리기 작업을 마친 후에 무효화 영역을 유효화 영역으로 변경하지 않으면 처리할 메시지가 없을 때 WM_PAINT가 계속 발생하여 시스템 성능이 떨어집니다.

HDC WINAPI BeginPaint(HWND hWnd,LPPAINTSTRUCT lpPaint);
BOOL WINAPI EndPaint(HWND hWnd,CONST PAINTSTRUCT *lpPaint);

Windows API에서는 무효화 영역을 계산하고 DC를 발급받는 것을 한꺼번에 처리하는 BeginPaint 함수를 제공하고 있습니다. 그리고 DC를 해제하고 무효화 영역을 유효화 영역으로 변경하는 것은 EndPaint 함수를 호출하면 내부에서 모든 처리를 해 줍니다

struct PAINTSTRUCT {
    HDC         hdc;
    BOOL        fErase;
    RECT        rcPaint;
    BOOL        fRestore;
    BOOL        fIncUpdate;
    BYTE        rgbReserved[32];
};

BeginPaint EndPaint에 사용하는 PAINTSTRUCT 구조체의 rcPaint에서 다시 그려줄 영역을 계산한 값이예요.

WM_PAINT 처리기에서는 BeginPaint 함수를 호출한 후에 그리기 작업을 수행하고 EndPaint 함수를 호출하여 작업을 마무리하세요.

void OnDraw(HWND hWnd,HDC hdc)
{
     //그리기 작업
}
void OnPaint(HWND hWnd)
{
    PAINTSTRUCT ps;
    BeginPaint(hWnd,&ps);//무효화 영역 계산 및 DC 핸들 발급
    OnDraw(hWnd,ps.hdc);   
    EndPaint(hWnd,&ps);//발급받은 DC 소멸 및 무효화 영역을 유효화 영역으로 갱신
}
LRESULT CALLBACK MyWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
    switch(iMessage)
    {
    case WM_PAINT: OnPaint(hWnd); return 0;
    ...중략...
    }
    return DefWindowProc(hWnd,iMessage,wParam,lParam);
}
  • BOOL WINAPI InvalidateRect(HWND hWnd, CONST RECT *lpRect, BOOL bErase);

그리고 프로그램 방식으로 무효화 영역을 만들어 다시 그리게 할 때는 Invalidate로 시작하는 함수를 호출합니다. 가장 많이 사용하는 함수는 InvalidateRect입니다.

InvalidateRect의 두 번째 인자는 다시 그려줄 사각 영역을 설정한 변수의 주소입니다. 그리고 세 번째 인자는 배경을 다시 그릴지 여부입니다. 만약 윈도우 전체 화면을 다시 그리게 하려면 두 번째 인자에 0을 전달하세요. 만약 특정 영역만 다시 그리게 할 때는 사각 영역을 계산하여 전달하세요. 전체 영역을 다시 그리면 화면이 깜빡거리는 현상이 발생할 수 있습니다.

보다 자세한 사용은 그리기 개체와 그리기 작업에 관한 함수를 다루면서 구체적으로 다루기로 할게요.

3. 그리기 예

이번에는 간단한 그리기 예제를 통해 Windows API를 이용하여 어떤 절차를 거치는지 살펴고기로 합시다.

[그림] 그리기 작업 수행 절차

앞에서 소개했듯이 Windows API의 그리기 기본 흐름은 [그림 2]와 같습니다. Windows API를 처음 학습할 때 새로운 형식 명과 긴 함수 이름과 생소한 흐름과 절차들 때문에 어떻게 이해하면서 다음으로 넘어가야 하는지 걱정하는 것이 대부분입니다.

지금에 와서 Windows API를 학습하는 이유는 윈도우즈 프로그램이 어떠한 원리로 동작하는지 이해하기 위한 부분이 실제 프로그래밍에 사용하기 위한 것보다 많다고 볼 수 있습니다. 이미 MFC나 Windosw Form, WPF 등의 보다 강력하고 개발 비용이 적게 드는 기술들이 많기 때문에 실제 Windows API를 이용하여 윈도우즈 프로그래밍을 주로 할 일은 많지 않습니다.

Windows API는 방대한 형식과 제공하는 기능과 목적이나 사용 개체에 따른 사용 절차들이 단기간에 익히기 힘들 정도로 많습니다. 하지만 이들을 모두 정확히 알고 형식 이름과 함수 명 등을 암기한 상태에서 프로그래밍을 해야 하는 것은 아닙니다.

사용 절차나 원리가 나온 부분은 전체적인 흐름을 살펴본 후에 예제 코드를 보며 이해를 하고 개발 도구로 직접 작성해 나가다 보면 컨트롤과 대화 상자를 다루는 시점에 도달하면 대략 윈도우즈 프로그래밍을 어떻게 하는 것이지 감을 잡을 수 있습니다.

기본적인 윈도우즈 프로그래밍 방식이 손에 잡혔을 때 다양한 소재로 프로젝트를 기획하고 개발하면서 필요한 부분이 있으면 책이나 관련 자료를 찾아서 보면서 하다보면 어느 새 기본적인 사항은 보지 않고 자연스럽게 작성하고 있는 자신을 볼 수 있습니다.

이번 그리기에서는 먼저 간략한 예를 보여준 후에 코드 속에 새로운 기능이나 사용 개체에 관한 설명을 하기로 할게요.

3.1 그리기 예제 코드

다음은 이번에 소개할 그리기에 관한 예제를 실행하였을 때의 실행 화면입니다.

[그림] 그리기 예제 프로그램 실행 화면

다음은 그리기 개체를 사용하는 예제 코드입니다. 이에 관한 설명은 바로 이어서 하기로 할게요. 먼저 앞에서 얘기한 그리기의 기본 흐름과 예제 코드를 상호 비교해 보세요. 그리고 개발 도구로 작성해 보신 후에 이에 관한 설명을 참고하세요.

#include <Windows.h>
#define MY_DRAW_WND (TEXT("ex_drawing"))

void RegWindowClass();
void MessageLoop();
INT APIENTRY WinMain(HINSTANCE hIns, HINSTANCE hPrev, LPSTR cmd, INT nShow)
{    
    RegWindowClass();//윈도우 클래스 속성 설정 및 등록
    
    //윈도우 인스턴스 생성
    HWND hWnd = CreateWindow(MY_DRAW_WND,//클래스 이름
        TEXT("그리기 예제"), //캡션 명
        WS_OVERLAPPEDWINDOW, //윈도우 스타일
        10,10,520,420,//좌,상,폭,높이
        0,//부모 윈도우 핸들
        0,//메뉴 핸들
        hIns,//인스턴스 핸들
        0);//생성 시 전달 인자
        
    ShowWindow(hWnd,nShow);//윈도우 인스턴스 시각화, SW_SHOW(시각화), SW_HIDE(비시각화)        
    MessageLoop();//메시지 루프
    return 0;
}

//윈도우 클래스 속성 설정 및 등록
LRESULT CALLBACK MyWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam);
void RegWindowClass()
{
    WNDCLASS wndclass={0};
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);//흰색 브러쉬 핸들
    wndclass.hCursor = LoadCursor(0,IDC_ARROW); //마우스 커서 핸들
    wndclass.hIcon = LoadIcon(0,IDI_APPLICATION); //아이콘 핸들
    wndclass.hInstance = GetModuleHandle(0);//자신 모듈의 인스턴스 핸들
    wndclass.lpfnWndProc = MyWndProc;//윈도우 콜백 프로시저
    wndclass.lpszClassName = MY_DRAW_WND;//클래스 이름 - 클래스 구분자
    wndclass.style = CS_DBLCLKS;//클래스 종류

    RegisterClass(&wndclass);//윈도우 클래스 등록
}

//메시지 루프
void MessageLoop()
{
    MSG Message;
    while(GetMessage(&Message,0,0,0))//메시지 루프에서 메시지 꺼냄(WM_QUIT이면 FALSE 반환)
    {
        TranslateMessage(&Message);//WM_KEYDOWN이고 키가 문자 키일 때 WM_CHAR 발생
        DispatchMessage(&Message);//콜백 프로시저가 수행할 수 있게 디스패치 시킴
    }
}

//윈도우 콜백 프로시저
void OnPaint(HWND hWnd);
void OnDestroy(HWND hWnd);
LRESULT CALLBACK MyWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
    switch(iMessage)
    {
    case WM_PAINT: OnPaint(hWnd); return 0;
    case WM_DESTROY: OnDestroy(hWnd); return 0;
    }
    return DefWindowProc(hWnd,iMessage,wParam,lParam);
}

#define STEP_WIDTH  100//출력할 폭
#define STEP_HEIGHT 100//출력할 높이
#define LEFT(pi)       (pi*STEP_WIDTH) //펜 인덱스에 따라 출력할 좌측 X좌표
#define RIGHT(pi)     ((pi+1)*STEP_WIDTH-20) //브러쉬 인덱스에 따라 출력할 상단 Y좌표
#define TOP(bi)        (bi*STEP_HEIGHT + 30) //펜 인덱스에 따라 출력할 우측 X좌표
#define BOTTOM(bi)  ((bi+1)*STEP_HEIGHT - 30) //브러쉬 인덱스에 따라 출력할 하단 Y좌표
enum EN_PEN { HP_RED, HP_GREEN, HP_BLUE, HP2_RED, HPD_RED, HP_MAX}; //펜 스타일 열거형
enum EH_BRUSH { HB_RED, HB_GREEN, HB_BLUE, HBS_RED, HB_MAX}; //브러쉬 스타일 열거형

LPCTSTR pstrs[HP_MAX] = {TEXT("R"), TEXT("G"), TEXT("B"),TEXT("T R"), TEXT("DD R")}; //펜 출력 문자열
LPCTSTR bstrs[HB_MAX] = {TEXT("R"), TEXT("G"), TEXT("B"),TEXT("C R")}; //브러쉬 출력 문자열
//그리기 작업
void DrawDiagram(HWND hWnd,HDC hdc,HPEN hPen, HBRUSH hBrush,LPRECT prt);
void OnDraw(HWND hWnd,HDC hdc)
{
    HPEN hPens[HP_MAX];
    //그리기에 사용할 펜 개체 생성 요청
    hPens[HP_RED] = CreatePen(PS_SOLID,1,RGB(255,0,0));
    hPens[HP_GREEN] = CreatePen(PS_SOLID,1,RGB(0,255,0));
    hPens[HP_BLUE] = CreatePen(PS_SOLID,1,RGB(0,0,255));
    hPens[HP2_RED] = CreatePen(PS_SOLID,4,RGB(255,0,0));
    hPens[HPD_RED] = CreatePen(PS_DASHDOT,1,RGB(255,0,0));

    HBRUSH hBrushes[HB_MAX];
    //그리기에 사용할 브러쉬 개체 생성 요청
    hBrushes[HB_RED] = CreateSolidBrush(RGB(255,0,0));
    hBrushes[HB_GREEN] = CreateSolidBrush(RGB(0,255,0));
    hBrushes[HB_BLUE] = CreateSolidBrush(RGB(0,0,255));
    hBrushes[HBS_RED] = CreateHatchBrush(HS_CROSS,RGB(255,0,0));

    TCHAR buf[256]= TEXT("");
    RECT rt;
    for(int pi = 0; pi<HP_MAX; pi++)
    {
        for(int bi=0; bi<HB_MAX; bi++)
        {
            //펜과 브러쉬 스타일 문자열 조합
            wsprintf(buf,TEXT("%s, %s"),pstrs[pi],bstrs[bi]);
            //츨력할 좌표 설정
            rt.left = LEFT(pi);
            rt.top = TOP(bi);
            rt.right = RIGHT(pi);
            rt.bottom = BOTTOM(bi);
            //펜과 브러쉬 스타일 문자열 출력
            TextOut(hdc,pi*STEP_WIDTH, bi*STEP_HEIGHT,buf,lstrlen(buf));
            //특정 펜과 브러쉬로 도형 출력
            DrawDiagram(hWnd,hdc,hPens[pi], hBrushes[bi],&rt);
        }
    }
    
    //펜 개체 소멸 요청
    for(int pi = 0; pi<HP_MAX; pi++)
    {
        DeleteObject(hPens[pi]);
    }
    //브러쉬 개체 소멸 요청
    for(int bi=0; bi<HB_MAX; bi++)
    {
        DeleteObject(hBrushes[bi]);
    }
}

//도형 그리기
void DrawDiagram(HWND hWnd,HDC hdc,HPEN hPen, HBRUSH hBrush,LPRECT prt)
{
    HPEN oPen;//DC에 기존 선택 펜 핸들 기억할 변수
    oPen = (HPEN)SelectObject(hdc,hPen); //입력 인자로 전달받은 펜을 DC에 선택

    HBRUSH oBrush;//DC에 기존 선택 브러쉬 핸들 기억할 변수    
    oBrush = (HBRUSH)SelectObject(hdc,hBrush);//입력 인자로 전달받은 브러쉬를 DC에 선택

    Rectangle(hdc,prt->left,prt->top,prt->right,prt->bottom);//사각형 그리기
    Ellipse(hdc,prt->left,prt->top,prt->right,prt->bottom); //타원 그리기
    
    SelectObject(hdc,oPen);//기존 선택 펜을 DC에 선택
    SelectObject(hdc,oBrush);//기존 선택 브러쉬를 DC에 선택
}

//WM_PAINT 메시지 처리기
void OnPaint(HWND hWnd)
{
    PAINTSTRUCT ps;
    BeginPaint(hWnd,&ps);//DC 발급 및 무효화 영역 계산
    OnDraw(hWnd,ps.hdc);//그리기 작업 요청
    EndPaint(hWnd,&ps); //DC 해제 및 무효화 영역을 유효화 영역으로 변경
}
//WM_DESTROY 메시지 처리기
void OnDestroy(HWND hWnd)
{
    PostQuitMessage(0);//메시지 큐에 WM_QUIT 메시지를 붙임
}

3.2 그리기 예제 설명

이제 앞에서 소개한 그리기 예제 코드에 관하여 설명하기로 할게요. 이미 1장에서 소개했던 윈도우 클래스 등록 및 윈도우 개체 생성과 메시지 루프에 관한 부분은 설명을 생략할게요. 여기에서는 윈도우 콜백 프로시저인 MyWndProc에서 WM_PAINT 메시지를 처리하는 메시지 처리기를 호출한 이후부터 설명할게요.

void OnPaint(HWND hWnd)
{
    PAINTSTRUCT ps;
    BeginPaint(hWnd,&ps);//DC 발급 및 무효화 영역 계산
    OnDraw(hWnd,ps.hdc);//그리기 작업 요청
    EndPaint(hWnd,&ps); //DC 해제 및 무효화 영역을 유효화 영역으로 변경
}

윈도우즈 프로그램은 윈도우가 다른 윈도우에 가려지면 가려진 부분은 그리지 않습니다. 이와 같은 영역을 클리핑 영역이라 불러요. 그리고 가려졌던 부분이 다시 보이면 해당 영역을 다시 그려주기 위해 무효화 영역이 발생하였다는 것을 알려줍니다. 이 외에도 프로그램에서 InvalidateRect 처럼 무효화 영역을 프로그램 방식으로 만들수도 있습니다. 윈도우즈 프로그램에서는 응용 메시지 큐에 처리할 작업이 없고 무효화 영역이 발생하면 윈도우 콜백 프로시저에 WM_PAINT 메시지를 처리하게 디스패치 시킵니다.

따라서 WM_PAINT 메시지 처리기에서는 무효화 영역에 그리기 작업을 하기 위해 DC를 발급받고 무효화 영역을 계산하는 작업을 수행해야 합니다. 이 때 사용하는 함수가 BeginPaint입니다. 그리고 그리기 작업을 마친 후에는 발급받은 DC를 반납하고 무효화 영역을 유효화 영역으로 변경해야 하는데 이 때 사용하는 함수가 EndPaint입니다.

  • HDC WINAPI BeginPaint(HWND hWnd, LPPAINTSTRUCT lpPaint);

BeginPaint 함수는 첫 번째 입력 인자로 윈도우 핸들을 받고 두 번째 인자로 PAINTSTRUCT 형식 변수의 주소를 받습니다. 그리기 작업은 특정 창에 그리는 것이어서 DC는 윈도우에 종속적입니다. 그리고 이 함수를 호출하면 내부에서 DC를 발급하고 다시 그려줄 무효화 영역을 계산하는데 수행 결과를 두 번째 입력 인자로 전달받은 변수의 메모리에 채워줍니다.

struct PAINTSTRUCT {
    HDC         hdc;
    BOOL        fErase;
    RECT        rcPaint;
    BOOL        fRestore;
    BOOL        fIncUpdate;
    BYTE        rgbReserved[32];
};

PAINTSTRUCT 구조체를 보면 발급받은 DC 핸들을 기억하는 멤버 hdc와 다시 그려줄 무효화 영역을 계산한 rcPaint가 있습니다. 그리고 fErase 멤버는 배경을 다시 그릴 것인지 여부를 나타내는 멤버입니다. 그 외의 멤버는 시스템 내부에서 사용하는 멤버입니다.

  • BOOL WINAPI EndPaint(HWND hWnd, CONST PAINTSTRUCT *lpPaint);

EndPaint 함수는 윈도우 핸들과 그리기 정보를 갖고 있는 PPAINTSTRUCT 구조체 변수의 주소를 전달하면 발급했던 DC를 해제하고 무효화 영역을 유효화 영역으로 변경하는 역할을 수행합니다. 만약 그리기 작업만 수행한 후에 무효화 영역을 유효화 영역으로 변경하지 않으면 응용 메시지 큐에 처리할 메시자가 없을 때마다 그리기 작업을 수행하여 시스템 성능이 떨어집니다.

이처럼 WM_PAINT 메시지 처리기에서는 그리기 전에 반드시 BeginPaint를 호출하세요. 그리고 그리기 작업 후에 EndPaint를 호출해야 합니다. 따라서 이 책에서는 WM_PAINT 메시지 처리기에서는 BeginPaint 호출과 그리기 작업을 실제 하는 OnPaint 호출과 EndPaint 호출로 전개하였습니다.

이 책에서는 프로그램 종류에 관계없이 앞으로 WM_PAINT를 처리할 필요가 있을 때 OnPaint 메시지 처리기의 코드는 언제나 같게 정의할 거예요.

이제 실제 그리기 작업을 수행하는 OnDraw 함수를 살펴봅시다.

여기에서는 다양한 펜과 브러쉬를 이용하여 사각형과 타원을 출력하고 사용한 펜과 브러쉬 정보를 출력하는 작업을 수행합니다. 이를 위해 필요한 상수를 먼저 얘기할게요.

#define STEP_WIDTH  100//출력할 폭
#define STEP_HEIGHT 100//출력할 높이
#define LEFT(pi)       (pi*STEP_WIDTH) //펜 인덱스에 따라 출력할 좌측 X좌표
#define RIGHT(pi)     ((pi+1)*STEP_WIDTH-20) //브러쉬 인덱스에 따라 출력할 상단 Y좌표
#define TOP(bi)        (bi*STEP_HEIGHT + 30) //펜 인덱스에 따라 출력할 우측 X좌표
#define BOTTOM(bi)  ((bi+1)*STEP_HEIGHT - 30) //브러쉬 인덱스에 따라 출력할 하단 Y좌표

출력할 때 특정 펜과 특정 브러쉬를 그리는 좌표를 다르게 하기 위해 폭과 높이는 100 픽셀을 사용할 거예요. 그리고 펜의 인덱스에 따라 사각형과 타원을 그릴 좌상단 좌표와 우하단 좌표를 계산하는 매크로 함수를 정의하였습니다.

enum EN_PEN { HP_RED, HP_GREEN, HP_BLUE, HP2_RED, HPD_RED, HP_MAX}; //펜 스타일 열거형
enum EH_BRUSH { HB_RED, HB_GREEN, HB_BLUE, HBS_RED, HB_MAX}; //브러쉬 스타일 열거형

그리고 이 프로그램에서 사용할 펜과 브러쉬 핸들은 배열에 저장하여 사용합니다. 이 때 각 배열의 인덱스에 사용할 펜과 브러쉬에 관한 열거형입니다.

LPCTSTR pstrs[HP_MAX] = {TEXT("R"), TEXT("G"), TEXT("B"),TEXT("T R"), TEXT("DD R")}; //펜 출력 문자열
LPCTSTR bstrs[HB_MAX] = {TEXT("R"), TEXT("G"), TEXT("B"),TEXT("C R")}; //브러쉬 출력 문자열

그리고 그리기에 사용한 펜과 브러쉬 정보를 출력하는 문자열 상수를 정의하였습니다. R은 RED, B는 BLUE, G는 GREEN을 의미하며 T는 THICK, DD는 DASH DOT, C는 CROSS를 의미합니다.

이제 OnDraw 함수를 보기로 해요.

HPEN hPens[HP_MAX];
//그리기에 사용할 펜 개체 생성 요청
hPens[HP_RED] = CreatePen(PS_SOLID,1,RGB(255,0,0));
hPens[HP_GREEN] = CreatePen(PS_SOLID,1,RGB(0,255,0));
hPens[HP_BLUE] = CreatePen(PS_SOLID,1,RGB(0,0,255));
hPens[HP2_RED] = CreatePen(PS_SOLID,4,RGB(255,0,0));
hPens[HPD_RED] = CreatePen(PS_DASHDOT,1,RGB(255,0,0));

그리기 작업을 수행하기 전에 그리기에 사용할 그리기 개체를 생성합니다. 위 코드는 그리기에 사용할 펜 개체를 생성하는 부분입니다.

HPEN WINAPI CreatePen(int iStyle, int cWidth, COLORREF color);

CreatePen은 그리기에 사용할 펜 개체를 생성하는 함수입니다. 첫 번째 인자로 펜의 스타일을 전달하고 두 번째 인자에는 펜의 두께, 세 번째 인자에는 펜의 색상 정보를 전달합니다.

#define PS_SOLID            0
#define PS_DASH            1       /* -------  */
#define PS_DOT             2       /* .......  */
#define PS_DASHDOT        3       /* _._._._  */
#define PS_DASHDOTDOT    4       /* _.._.._  */
#define PS_NULL             5

PS_SOLID는 실선이며 PS_DASHDOT은 대시와 점으로 그은 선입니다.

[그림] 펜 스타일
HBRUSH hBrushes[HP_MAX];
//그리기에 사용할 브러쉬 개체 생성 요청
hBrushes[HB_RED] = CreateSolidBrush(RGB(255,0,0));
hBrushes[HB_GREEN] = CreateSolidBrush(RGB(0,255,0));
hBrushes[HB_BLUE] = CreateSolidBrush(RGB(0,0,255));
hBrushes[HBS_RED] = CreateHatchBrush(HS_CROSS,RGB(255,0,0));

그리기에 사용할 브러쉬 개체도 생성합니다. 브러쉬 개체는 면 내부를 채우는 그리기 개체입니다. 내부를 꽉 차게 채우는 브러쉬는 CreateSolidBrush 함수 호출로 생성 요청합니다. 격자 모양 등의 형태로 채울 때는 CreateHatchBrush 함수 호출로 생성 요청합니다.

HBRUSH WINAPI CreateSolidBrush(COLORREF color);
HBRUSH WINAPI CreateHatchBrush(int iHatch, COLORREF color);

SOLID 브러쉬를 생성할 때는 브러쉬 색상만 전달합니다. HATCH 브러쉬를 생성할 때는 HATCH 스타일을 첫 번째 입력 인자로 두 번째 입력 인자로 색상 정보를 전달합니다.

#define HS_HORIZONTAL       0
#define HS_VERTICAL          1
#define HS_FDIAGONAL        2
#define HS_BDIAGONAL       3
#define HS_CROSS            4
#define HS_DIAGCROSS       5

HATCH 스타일에는 수평, 수직, 사선, 크로스, 사선 크로스 등이 있습니다.

[그림] HATCH 스타일
TCHAR buf[256]= TEXT("");
RECT rt;
for(int pi = 0; pi<HP_MAX; pi++)
{
    for(int bi=0; bi<HP_MAX; bi++)
    {
        //펜과 브러쉬 스타일 문자열 조합
        wsprintf(buf,TEXT("%s, %s"),pstrs[pi],bstrs[bi]);
        //츨력할 좌표 설정
        rt.left = LEFT(pi);
        rt.top = TOP(bi);
        rt.right = RIGHT(pi);
        rt.bottom = BOTTOM(bi);
        //펜과 브러쉬 스타일 문자열 출력
        TextOut(hdc,pi*STEP_WIDTH, bi*STEP_HEIGHT,buf,lstrlen(buf));
        //특정 펜과 브러쉬로 도형 출력
        DrawDiagram(hWnd,hdc,hPens[pi], hBrushes[bi],&rt);
    }
}

그리기 작업은 펜과 브러쉬에 따라 사용한 그리기 개체 정보 문자열을 출력하는 부분과 도형을 출력합니다. 이를 위해 이중 반복문을 사용하였습니다. OnDraw에서는 그리기에 사용한 그리기 개체 정보를 출력하는 부분은 직접 Windows API 함수 TextOut을 이용하였고 도형을 그리는 부분은 DrawDiagram을 만들어서 호출하는 방식으로 구현했어요.

  • int WINAPIV wsprintfW(LPWSTR buffer,LPCWSTR format, …);

먼저 출력할 정보 문자열 조합을 만들기 위해 wsprintf를 사용했습니다. 이 부분은 C언어 표준 라이브러리 함수인 sprintf와 유사합니다. 차이가 있는 부분은 유니코드 문자열이라는 점 뿐입니다.

  • BOOL WINAPI TextOut(HDC hdc, int x, int y, LPCWSTR lpString, int c);

TextOut함수는 DC 핸들 외에도 출력할 x, y 좌표와 출력할 문자열과 문자열 길이를 입력 인자로 받습니다.

//펜 개체 소멸 요청
for(int pi = 0; pi<HP_MAX; pi++)
{
    DeleteObject(hPens[pi]);
}
//브러쉬 개체 소멸 요청
for(int bi=0; bi<HP_MAX; bi++)
{
    DeleteObject(hBrushes[bi]);
}

그리기 작업을 마친 후에는 그리기에 사용한 개체를 소멸합니다. 이 때 사용하는 함수가 DeleteObject입니다.

이제 도형을 그리는 DrawDiagram 함수를 살펴 보아요.

HPEN oPen;//DC에 기존 선택 펜 핸들 기억할 변수
oPen = (HPEN)SelectObject(hdc,hPen); //입력 인자로 전달받은 펜을 DC에 선택
HBRUSH oBrush;//DC에 기존 선택 브러쉬 핸들 기억할 변수    
oBrush = (HBRUSH)SelectObject(hdc,hBrush);//입력 인자로 전달받은 브러쉬를 DC에 선택

그리기를 하기 전에 DC에 그리기에 사용할 개체를 선택합니다. 이 때 SelectObject 함수를 사용합니다.

  • HGDIOBJ WINAPI SelectObject(HDC hdc, HGDIOBJ h);

SelectObject 함수의 두 번째 인자는 DC에 선택할 그리기 개체입니다. 그리고 원래 선택했던 기존 그리기 개체를 반환합니다. SelectObject는 그리기 개체 종류에 관계없이 사용하는 함수입니다. 그리기 개체를 전달할 때는 형식 변환없이 전달할 수 있지만 반환 값을 받을 때는 목적에 맞는 형식으로 변환하세요.

Rectangle(hdc,prt->left,prt->top,prt->right,prt->bottom);//사각형 그리기
Ellipse(hdc,prt->left,prt->top,prt->right,prt->bottom); //타원 그리기

사각형을 그릴 때는 Rectange, 타원을 그릴 때는 Ellipse를 사용합니다. 두 함수 모두 DC핸들과 그릴 사각 영역의 좌상단, 우하단 좌표를 전달합니다.

SelectObject(hdc,oPen);//기존 선택 펜을 DC에 선택
SelectObject(hdc,oBrush);//기존 선택 브러쉬를 DC에 선택

그리기 작업을 수행한 후에는 DC에 기존 그리기 개체를 선택하여 원래 상태로 복원하세요.

이 외에도 다양한 그리기 개체가 존재하고 그리기 작업에 사용하는 함수를 제공하고 있습니다. 여기에서는 이 정도에서 설명을 마칠게요.