관리 메뉴

Kim's Programming

WinApi - 출력(1/2) 본문

Programming/Windows API

WinApi - 출력(1/2)

Programmer. 2015. 8. 27. 00:31

DC(Device Context)


윈도우즈는 세 가지 동적 연결 하이브러리(DLL)로 구성되어 있는데 메모리를 관리하고 프로그램을 실행시키는 KERNEL, 유저 인터페이스와 윈도우를 관리하는 USER 그리고 화면 처리와 그래픽을 담당하는 GDI가 그것들입니다. 윈도우 API함수의 대부분은 이 세 가지 DLL에 의해 제공되고 있습니다. 출력을 하려면 우리는 GDI(Graphic Device Interface)모듈에 특별히 관심을 기울여야 하는데 화면으로 출력되는 모든 글자와 그림은 GDI를 통해야 하기 떄문입니다.


DC(Device Context)란 출력에 필요한 모든 정보를 가지는 데이터 구조체이며 GDI 모듈에 의해 관리됩니다. 문자열의 모양을 지정하는 폰트, 선의 색상과 굵기, 채움 무늬와 색상, 그리기 모드 등등이 모두 출력에 필요한 정보등입니다.



문자열 출력


이벤트 드리븐 시스템에서는 사건이 일어나야 코드가 실행되므로 먼저 문자열 출력 시점을 결정해야합니다. 변화가 없다면 코드가 실행되어야 할 이유가 없으므로 가장 발생시키기 쉬운 마우스 왼쪽 버튼이 눌러질 때를 사용하기로 합니다. 마우스 왼쪽 버튼이 눌러질 때 WM_LBUTTONDOWN 메세지가 발생하므로 WndProc에서 이 메세지를 받았을 때 문자열을 출력하면 됩니다. 소스는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include<Windows.h>
 
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPCTSTR lpszClass = TEXT("출력");
 
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 = CreateSolidBrush(RGB(255,255,255));
    WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
    WndClass.hIcon = LoadIcon(NULL, IDI_QUESTION);
    WndClass.hInstance = hInstance;
    WndClass.lpfnWndProc = WndProc;
    WndClass.lpszClassName = lpszClass;
    WndClass.lpszMenuName = NULL;
    WndClass.style = CS_HREDRAW | CS_VREDRAW;
    RegisterClass(&WndClass);
 
    hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW | WS_VSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, (HMENU)NULL, hInstance, NULL);
    ShowWindow(hWnd, nCmdShow);
 
    while (GetMessage(&Message, NULL, 00))
    {
        TranslateMessage(&Message);
        DispatchMessage(&Message);
    }
    return (int)Message.wParam;
}
 
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
 
    switch (iMessage)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
        break;
    case WM_LBUTTONDOWN:
        hdc = GetDC(hWnd);
        TextOut(hdc, 100100, TEXT("Hello World!!!"), 14);
        ReleaseDC(hWnd, hdc);
        return 0;
    }
    return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}
cs

WM_LBUTTONDOWN에서는 getDC함수로 DC를 얻고 TextOut 함수로 (100,100) 좌표에 15자리 문자열을 출력한 후 ReleaseDC함수를 이용하여 DC를 해제하였습니다. DC를 얻는 방법과 TextOut함수에 대해서는 잠시후 자세하게 알아보겠습니다. 실행시킨후 윈도우의 임의 영역에 클릭을 하면 100, 100좌표에  Hello Wordl!!!라는 문자열이 출력됩니다. 결과는 다음과 같습니다.



WM_PAINT 메세지


앞에서 만들었던 출력 예제는 이해를 쉽게 하기위해 오로지 문자열 출력 그 자체에만 역점을 두었으면 출력이 잘 되었습니다. 하지만 어디까지나 출력과정을 보이기 위한 예제일 뿐이며 정상적인 출력을 한것은 아니기 떄문에 문제점을 가지고 있습니다. 어떤 문제인가 하면 그 문자열이 한자리에 있지 않다는 것입니다. 문자열이 출력된 윈도우를 다른 윈도우로 살짝 가렸다가 다시 드러나도록 하거나 윈도우의 크기를 변경하면 출력되어 있던 문자열이 감쪽같이 가라지게 됩니다.


왜냐하면 운영체제가 개별 윈도우의 화면을 보관 및 복구해 주지 않기 때문입니다. 도스에서는 하나의 프로그램만 실행하며 화면을 혼자서 독점하므로 한 번 출력해 놓으면 자신이 일부러 지우지 않는 한 절대로 지워지지 않습니다. 하지만 멀티 태스킹 시스템인 윈도우즈에서는 여러 개의 윈도우가 겹칠 수 있으므로 한 번 출력해 놓은 문자열이 언제까지고 그대로 그 자리에 있따는 보장이 없습니다. 다른 윈도우에 가려졌다가 다시 나타나면(Uncover 되면)가려졌던 윈도우의 새로 드러난 부분은 지워져 있게 됩니다. 하지만 우리는 윈도우즈 사용중에 이런 현상을 전혀 목격할 수 없습니다. 그 이유는 다음에 설명하는 방법으로 끊임없이 지워진 부분을 복구하기 때문입니다.


윈도우즈는 가려졌던 윈도우의 화면을 보관 및 복구하는 책임을 지지 않으며 지워진 화면을 복구(Repaint)하면 책임은 전적으로 프로그램 자신에게 있습니다. 자신의 작업영역 일부가 지워졌다면 프로그램은 재빠르게 지워진 부분을 복구해야 합니다. 앞에서 만든 출력 예제는 이런 복구를 하지 않기 때문에 크기가 변하거나 다른 윈도우에 의해 언커버(Uncover)되면 출력해 놓은 문자열이 사라지는 것입니다. 물론 마우스 버튼을 또 누르면 문자열은 다시 출력되지만 이 문자열도 언젠가는 지워질 것입니다.


그렇다면 문제 해결을 위해 출력 예제를 수정하여 화면이 지워지면 다시 복구하도록 만들면 됩니다. 마우스 왼쪽 버튼을 누르면 문자열을 출력하는 것이 아니라 화면이 지워질 때마다 문자열을 출력해야합니다. 그럼 어느 시점에서 문자열을 출력해야 할까요? 그 어느 시점에 바로 WM_PAINT 메세지 입니다. 운영체제는 개별 윈도우의 화면을 보관해 주지는 않지만 대신 윈도우의 일부가 지워졌다는 사실을 프로그램으로 즉각 알리며 그 방법은 WM_PAINT 메세지를 보내주는 것입니다. WM_PAINT메세지는 작업영역이 지워졌으니 다시 그리라는 뜻입니다.


위의 소스를 수정하여 WM_LBUTTONDOWN 메세지에 있는 문자열 출력문을 WM_PAINT로 옮겨보겠습니다. 코드의 위치가 바뀌었고 DC를 얻는 함수도 달라졌습니다. WM_LBUTTONDOWN의 코드는 이제 필요없으므로 삭제해 버려도 되고 그냥 두어도 별 지장은 없습니다. 수정된 소스는 다음과 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
 
    switch (iMessage)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        TextOut(hdc, 100100, TEXT("Hello World!!"), 13);
        EndPaint(hWnd, &ps);
        return 0;
    }
    return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}
cs

코드를 이렇게 수정한 후 다시 테스트 해보겠습니다. 문자열 출력문이 WM_PAINT 메세지 처리 루틴에 있으므로 여기서 출력한 문자열은 지워져도 항상 다시 복구됩니다. 그래서 실제로는 지워졌다가 다시 그려지지만 항상 그 자리에 있는 것처럼 보이는 것입니다. 윈도우즈에서 화면을 보관, 복구하는 메커니즘이 이런 식이라면 모든 그래픽 출력은 WM_PAINT메세지 처리시 해야만 할까요? 그렇지 않으면 그려놓은 그림이 항상 그 자리에 있는다고 보장할 수 없을까요? 대답은 그렇다 입니다. 도스에서와 같이 아무렇게나 화면에 출력해 놓아서는 안됩니다. 그렇다면 모든 프로그램은 현재 화면에 그려진 내용을 철저하게 기억하거나 다시 그리기 위한 준비를 해놓아야 할까요? 그 대답도 그렇다 입니다.


DC를 얻는 방법



화면으로 출력을 하기 위해서는 반드시 DC가 있어야 하며 DC를 얻는 방법에는 두 가지가 있습니다. 첫 번째 방법은 앞의 소스에서  보인대로 GetDC함수를 사용하여 DC를 얻고 사용 후 ReleaseDC로 해제하는 것입니다.

1
2
HDC GetDC(HWND hWnd);
int ReleaseDC(HWND hWndm HDC hDC);
cs

DC는 주로 하나의 윈도우와 연관되는 출력 정보를 가집니다. 그래서 인수로 어떤 윈도우에 대한 DC가 필요한가를 밝혀야 합니다. getDC는 hWnd가 가리키는 윈도우에서 적당한 DC를 만들어 그 핸들을 리턴합니다. 앞의 예제에서는 이 핸들을 HDC형의 hdc변수에 대입한 후 TextOut함수의 첫 번째 인수로 사용하였습니다. GetDC에 의해 얻어진 핸들은 사용 후에 반드시 ReleaseDC함수로 해제해야 합니다. DC도 메모리를 차지 하므로 할당 후 해제 원칙이 반드시 준수되어야 합니다. GetDC로 DC를 구해 사용하는 일반적인 코드는 다음과 같습니다.

1
2
3
4
HDC hdc;
hdc=GetDC(hWnd);
각종 출력문에서 hdc를 사용합니다.
ReleaseDC(hWnd, hdc);
cs

DC를 얻는 두 번쨰 방법은 WM_PAINT 메세지 루틴에서만 사용할 수 있습니다. WM_PAINT 메세지 처리 루틴에서는 DC핸들을 GetDC로 얻지 않고 BeginPaint 함수로 얻으며 핸들을 해제할 때 EndPaint 함수를 사용합니다. GetDC는 DC 핸들을 얻는 일반적인 방법이며 BeginPaint는 WM_PAINT 메시지 내에서 그림 그리기 준비를 하는 좀 더 전문적인 함수이되 그 외의 메세지에서는 절대로 사용할 수 없습니다.

1
2
HDC BeginPaint(HWND hwnd,LPPAINTSTRUCT lpPaint)
BOOL EndPaint(HWND hWnd, CONST PAINTSTRUCT *lpPaint)
cs

BeginPaint 함수는 윈도우 핸들 외에도 페이트 정보 구조체를 인수로 요구하며 이 구조체에 그림그리기에 필요한 여러 가지 복잡한 정보를 리턴합니다. 이 구조체는 다음과 같이 선언되어 있습니다.

1
2
3
4
5
6
7
8
9
typedef struct tagPAINTSTRUCT
{
    HDC             hdc;
    BOOL            fErase;
    RECT            rcPaint;
    BOOL            fRestore;
    BOOL            fIncUpdate;
    BYTE            rgbReserved[16];
}PAINTSTRUCT;
cs

앞의 세 멤버는 사용자가 사용하는 멤버이며 나머지 세 멤버는 윈도우즈가 내부적으로 사용하므로 사용자가 건들여서는 안됩니다. WndProc선두에서 PAINTSTRUCT형의 구조체를 지역변수로 선언하고 다음 방법대로 이 두 함수를 사용합니다.

1
2
3
4
5
6
HDC hdc;
PAINTSTRUCT ps;
case WM_PAINT(hWnd,&ps);
    hdc=BeginPaint(hWnd,&ps);
    각종 출력문에서 hdc를 사용합니다. 
    EndPaint(&hWnd,&ps);
cs

PAINTSTRUCT 구조체에는 그리기 속도를 비약적으로 향상시킬 수 있는 정보들이 들어 있는데 이 정보를 활용하는 방법에 대해서는 다음에 자세하게 하겠습니다. 여기서는 DC를 얻어 사용하는 방법에 대해서 알아두겠습니다. GetDC는 ReleaseDC와 짝이며 BeginPaint는 EndPaint와 짝이므로 반드시 짝을 맞춰서 사용해야합니다.



TextOut



데이터를 가공하여 결과를 보여주는 일이 프로그램의 당연한 임무라고 할 때 외부로부터 데이터를 받아들이는 입력(input)과 처리한 결과를 외부로 보여주는 출력(output)은 가장 기본적인 절차에 해당합니다. 그 중에서도 출력이 좀 더 쉽습니다. 그래서 콘솔에서 printf를 가장 먼져 마주하듯이 윈도우 프로그래밍에서는 출력에서 TextOut함수가 출력을 담당하고 원형은 다음과 같습니다.

1
BOOL TextOut(HDC hdc, int nXStart, int nYStart, LPCTSTR lpString, int cbString);
cs

첫 번째 인수는 당연히 DC의 핸들인 hdc입니다. 이 함수뿐만 아니라 화면에 무엇인가를 출력하는 모든 함수들의 첫 번쨰 인수는 항상 hdc입니다. 이 DC는 GetDC나 BeginPaint 함수에 의해 구해졌을 것입니다. (nXStart, nYStart)는  문자열이 출력될 좌표이며 윈도우의 작업영역 원점을 기준으로 합니다. 세 번째 인수 lpString은 출력할 문자열을 담고 있는 문자열 포인터이며 마지막 인수 cbString이 출력할 문자열의 길이입니다. 예제에서는 다음 호출문을 사용하여 문자열을 출력하였습니다.

1
TextOut(hdc, 100100, TEXT("Hello World!!",13);
cs

TextOut 함수는 널 종료 문자열을 인식하는 않으므로 출력할 문자열의 길이를 인수로 반드시 밝혀야 합니다. 문자열 길이를 일일이 세는 것은 번거롭기도 하지만 이식성에도 불리합니다. 예를 들어 "대한민국"은 멀티바이트에서는 길이가 8바이트이지만 유니코드에서는 4문자로 계산됩니다. 그래서 통상 문자열 상수를 직접 출력하지 않고 문자 배열과 lstrlen 함수를 함께 사용하여 전체 문자열을 출력합니다. 이렇게 하면 출력 문자열이 바뀔 때마다 길이를 직접 세지 않아도 되므로 편리하고 문자 설정이 바뀌면 길이도 같이 바뀌므로 안전합니다.

1
2
TCHAR *str=TEXT("Hello World!");
TextOut(hdc, 100100, str, strlen)
cs

이 호출문을 말로 풀게 되면 "Hello World!"라는 13글자 길이의 문자열을 (100, 100)좌표에 출력하라는 뜻입니다. 문자열 변수를 선언하는 것이 귀 찮다면 다음과 같이 래퍼를 만들어 사용하면 됩니다. 문자열을 넘기기만 하면 길이를 알아서 계산해줍니다.

1
2
3
4
void M_TextOut(HDC hdc, int x, int y, LPCTSTR Text)
{
    TextOut(hdc,x,y,Text,lstrlen(Text));
}
cs

출력에 필요한 기타 정보들은 모두 hdc에서 지정하는 정보를 사용합니다. hdc의 정보를 변경하면 문자의 모양이나 크기, 색상, 정렬 상태, 좌표 해석 방법 등의 여러가지 변화도 줄 수 있지만 여기서는 정렬 상태를 변경하는 방법만 알아보겠습니다. 문자열의 정렬 방법을 변경하는 함수는 SetTextAlign이라는 함수입니다.

1
UINT SetTextAlign(HDC hdc, UINT fMode);
cs

보다시피 이 함수도 첫 번째 인수로 DC의 핸들, 즉 hdc를 받아들이고 있습니다. 출력 함수뿐만 아니라 출력에 관계된 모든 함수의 첫 번째 인수는 예외없이 hdc입니다. 두 번째 인수 fMode가 지정하는 정렬 정보에 따라 hdc의 정렬 상태를 변경하며 이후부터 hdc를 참조하여 출력되는 모든 문자열은 이 함수가 지정한 정렬 상태를 따릅니다. fMode의 값은 다음과 같으며 두 개 이상의 플래그를 OR로 연결하여 지정합니다.


 값 

 의미

 TA_TOP

 지정한 좌표가 상단 좌표가 됩니다.

 TA_BOTTOM

 지정한 좌표가 하단 좌표가 됩니다.

 TA_CENTER

 지정한 좌표가 수평 중앙좌표가 됩니다.

 TA_LEFT

 지정한 좌표가 수평 왼쪽 좌표가 됩니다.

 TA_RIGHT

 지정한 좌표가 수평 오른쪽좌표가 됩니다.

 TA_UPDATECP

 지정한 좌표대신 CP를 사용하며 문자열 출력 후에 CP를 변경합니다.

 TA_NOUPDATECP

 CP를 사용하지 않고 지정한 좌표를 사용하며 CP를 변경하지 않습니다.


디폴트 정렬 상태는 TA_TOP | TA_LEFT로 되어있으며 지정한 좌표를 좌 상단으로 하여 문자열이 출력됩니다. 물론 정렬상태를 변경하면 지정한 좌표를 문자열의 어느 지점으로 사용할 것인가를 변경할 수 있습니다.

수평 정렬 위치를 변경하는 플래그가 좌, 우, 중앙 세 개 있고 수직 정렬 위치를 변경하는 플래그가 상단, 하단 두 개 있습니다. 수직 중앙 정렬 기능은 없으므로 출력 좌표를 직접 조정하는 수 밖에 없습니다. 이런 정렬 옵션을 사용하면 다음과 같은 여러 개의 문자열을 중앙 정렬하여 출력 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
 
    switch (iMessage)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        SetTextAlign(hdc, TA_CENTER);
        TextOut(hdc, 20060, TEXT("Hello~"), 6);
        TextOut(hdc, 20080, TEXT("World~"), 6);
        TextOut(hdc, 200100, TEXT("!!!!!!!!!~"), 10);
        EndPaint(hWnd, &ps);
    }
    return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}
cs

TextOut 출력을 하기 전에 SetTextAlign 함수로 중앙 정렬하였습니다. 출력 결과는 다음과 같습니다.


수평좌표값으로 모두 200으로 주었지만 이 좌표를 문자열의 중앙 좌표로 해석하도록 지정했기 때문에 문자열이 모두 수평 위치 200을 기준으로 중앙 정렬되었습니다. 만약 이런 중앙 정렬없이 왼쪽 정렬만 가능하다면 문자열의 길이에 따라 출력 시작 위치를 일일이 계산하여 적당히 왼쪽으로 옮겨서 출력해야만 할 것입니다. DC에 대한 속성 변경은 항상 그 후의 출력에만 영향을 미칩니다. 위 코드에서 SetTextAlign문을 첫 번쨰 TextOut 아래로 옮기면 두 번째 출력 이후부터만 중앙으로 출력됩니다.


정렬 위치를 지정하는 플래그 외에 두 개의 플래그가 더 있는데 이 플래그들은 CP(Current Position)의 사용 여부를 결정합니다. 텍스트 모드에서 다음 문자가 출력될 위치를 커서(Cursor)가 표시하듯이 그래픽 모드에서는 다음 그래픽이 출력될 위치를 CP가 가지고 있습니다. 물론 CP는 내부적으로 유지되는 좌표일 뿐 커서처럼 화면에서 깜박거리지는 않습니다. TA_UPDATECP 플래그를 사용하면 출력 위치를 지정하는 인수를 무시하고 항상 CP의 위치에 문자열을 출력하며 출력 후에 CP를 문자열의 바로 다음 위치로 옮깁니다. 그래서 이 플래그를 사용하면 다음과 같이 여러 번 TextOut을 호출하여 문장을 이어서 출력할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
 
    switch (iMessage)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        SetTextAlign(hdc, UPDATECP);
        TextOut(hdc, 20060, TEXT("One "), 6);
        TextOut(hdc, 20080, TEXT("Two "), 6);
        TextOut(hdc, 200100, TEXT("Three"), 10);
        EndPaint(hWnd, &ps);
        return 0;
    }
    return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}
cs

TextOut의 인수에 좌표를 지정하는 값들이 있지만 이 좌표는 사용되지 않고 무시되며 무조건 CP위치에 출력하므로 하므로 일직선으로 세 개의 문자열이 출력됩니다. 이 기능을 사용하면 콘솔에 printf로 출력하듯이 문자열을 연속적으로 출력할 수 있습니다.  윈도우즈 환경에서 TextOut 출력문은 오로지 문자열만 다룰 수 있습니다. 정수나 실수를 출력하는 함수는 따로 없으므로 sprintf나 wsprintf 같은 함수로 서식화하여 문자열로 만들어서 출력해야합니다. 예를 들어 Score라는 정수형 변수의 값을 화면으로 출력하고 싶다면 다음과 같이 출력합니다.

1
2
3
4
TCHAR str[128];
int Score = 85;
wsprintf(str, TEXT("현재 점수는 %d점입니다.",Score);
TextOut(hdc,10,10,str,lstrlen(str));
cs

문자열 조립을 위한 임시 버퍼를 선언하고 wsprintf 함수로 이 버퍼에 정수값을 조립하여 문자열 형태로 만든 후 TextOut으로 출력하였습니다. 정수 출력이 다소 번거러운데 원한다면 서식 조립과 출력을 한꺼번에 할 수 있는 사용자 정의 함수를 만들어 쓰면 됩니다. wsprintf는 API함수이므로 용량상의 불이익이 없고 유니코드를 지원한다는 장점이 있기는 하지만 실수를 서식화할 수 없고 버퍼 길이가 이익이 없고 유니코드를 지원한다는 장점이 있기는 하지만 실수를 서식화할 수 없고 버퍼 길이가 1024까지만 지원되는 단점이 있습니다. 실수는 sprintf 표준 함수나 유니코드 버전의 swprintf를 사용해야합니다.





'Programming > Windows API' 카테고리의 다른 글

WinApi - 출력(2/2)  (0) 2015.08.29
WinApi - 분석(2/2)  (0) 2015.08.26
WinApi - 분석(1/2)  (1) 2015.08.26
WinAPI - 첫번째 시작 - 프로젝트 만들기  (0) 2015.08.25
WinAPI - 시작하기전에 알고 갑시다.  (0) 2015.08.24