본문으로 바로가기

5-4 액셀러레이터

액셀러레이터(Accelerator)는 아주 쉽게 말해서 단축키이다. 아래 한글을 예로 든다면 Alt+L을 누르면 문자 꾸미기 기능, Ctrl+P를 누르면 프린터 설정 기능이 곧바로 실행되도록 하는 키보드 조합키를 말한다. 그런데 왜 단축키라는 말을 쓰지 않고 어렵게시리 영어를 쓰는가 하면 윈도우즈에서 단축키(ShortCut)라는 말은 다른 의미로 사용되고 있기 때문이다. 메뉴 이름에 &를 넣어 Alt키와 함께 사용하는 키를 단축키라고 하며 여기서 말하는 액셀러레이터와는 의미가 조금 다르다. 단축키는 반드시 Alt키와 함께 사용해야 하며 메뉴에 있는 항목을 키보드로 선택하는 빠른 방법이지만 액셀러레이터는 메뉴와 상관없이 언제든지 사용할 수 있다는 점에 있어서 차이가 있다.

여기서는 Menu.dsw에서 만들었던 예제를 변형하여 메뉴 항목을 액셀러레이터로 곧바로 실행할 수 있도록 변형해 볼 것이다. 각 메뉴 항목에 대해 Ctrl+A, Ctrl+B, Ctrl+C의 액셀러레이터를 할당하여 메뉴를 선택하지 않고 키보드로 메뉴 기능을 실행하도록 해 본다. 단계가 조금 복잡하므로 다음 단계를 따라 Menu.dsw를 계속 만들어 보자.

1일단 Menu.dsw 프로젝트를 다시 연다. 단축키를 지원하기 위해 메뉴 리소스를 수정한다. 리소스 뷰에서 IDR_MENU1을 더블 클릭하면 메뉴 편집기가 열릴 것이다.

File 메뉴 아래 세 개의 메뉴 항목이 있는데 각 항목의 캡션을 다음과 같이 수정한다. 메뉴 편집기에서 메뉴 항목을 더블클릭하여 속성 윈도우를 연 다음 Caption 속성을 편집하면 된다.

수정전수정후
Menu1Menu&1\tCtrl+A
Menu2Menu&2\tCtrl+B
Exit&Exit\tCtrl+C

캡션에 포함된 &문자가 단축키를 지정하는데 &다음에 있는 문자가 단축키가 되며 단축키로 지정된 문자 밑에는 밑줄이 그어진다. 단축키는 그 메뉴 항목을 대표하는 문자 하나로 지정하는 것이 보통인데 Menu1은 1을 Menu2는 2를 Exit는 E를 단축키로 지정하였다. 앞 문자를 단축키로 지정하는 것이 보편적이지만 Copy, Cut 등과 같이 앞문자가 중복될 경우는 적당한 다른 문자를 지정해야 한다.

액셀러레이터는 캡션 뒤에 \t로 적당히 칸을 띄운 후 키조합을 써 주면 된다. 여기서 사용된 \t는 탭 문자인데 액셀러레이터가 메뉴 리스트의 오른쪽으로 가지런하게 정렬되도록 해 준다. 위에서부터 순서대로 Ctrl+A, Ctrl+B, Ctrl+C를 액셀러레이터로 지정하였다. 캡션을 변경한 후의 메뉴 모양은 다음과 같다.

2여기까지만 작업하고 예제를 다시 실행해 보자. 단축키가 지정되었으므로 이제 마우스를 사용하지 않고도 메뉴 항목을 선택할 수 있다. File 메뉴는 F문자가 단축키로 지정되어 있으므로 언제든지 Alt+F를 누르면 File 메뉴가 열린다. 메뉴 리스트에서 밑줄 그어진 문자를 누르면 해당 메뉴가 선택되는데 예를 들어 1을 누르면 Menu1이 선택되며 E를 누르면 Exit가 선택되어 프로그램이 종료된다.

그러나 액셀러레이터는 표시만 되어 있지 아직 동작하지는 않는다. Ctrl+A, Ctrl+B를 눌러도 프로그램은 아무 반응이 없을 것이다. 메뉴 리스트에 출력되는 액셀러레이터는 이 메뉴 항목의 액셀러레이터에 대한 설명을 해 주는 문자열일뿐이며 실제로 단축키로 동작하지는 않는다. 액셀러레이터를 만들려면 별도의 리소스를 만들어 주어야 한다.

3이제 액셀러레이터를 작성해 보자. 메뉴를 만들 때와 같은 방법으로 Insert/Resource를 선택하고 리소스 리스트에서 Accelerator를 선택한다. 다음과 같은 액셀러레이터 편집기가 열릴 것이다.

새로 만든 액셀러레이터 테이블이므로 아직 정의되어 있는 액셀러레이터가 없으며 빈칸만 하나 있다. 이 빈칸을 더블클릭하면 액셀러레이터를 편집할 수 있는 속성 편집기가 열린다. 각 란을 다음과 같이 입력한다.

ID

액셀러레이터의 ID이며 프로그램 소스에서 액셀러레이터를 참조할 때 이 값을 사용한다. 메뉴의 ID를 작성하는 방법과 동일한 규칙대로 ID를 작성하되 이 예제의 경우는 메뉴에 작성되어 있는 ID를 그대로 사용하면 된다. 드롭다운 리스트를 열어 ID에 ID_FILE_MENU1을 선택한다.

Key

액셀러레이터로 사용할 키를 선택한다. 펑션키나 특수키를 사용하려면 드롭다운 리스트를 열어 선택하고 알파벳키나 숫자키를 선택하려면 바로 아래쪽의 Next Key Typed버튼을 누른 후 원하는 키를 키보드에서 누르면 된다. Next Key Typed를 누른 후 키보드에서 "A"키를 누른다.

Modifier

Key와 함께 눌러질 조합키를 선택한다. Ctrl, Alt, Shift를 개별적으로 또는 여러개를 한꺼번에 선택할 수 있다. Ctrl키만 선택한다.

Type

Key값이 아스키 코드값인지 가상키 코드값인지를 설정한다. 보통 가상키 코드를 많이 사용한다.

이런식으로 다음과 같이 세 개의 액셀러레이터를 만든다. 액셀러레이터가 메뉴의 기능을 대신할 수 있도록 메뉴와 같은 ID를 사용하도록 해 두었다.

4다음은 만들어진 리소스를 코드에서 사용하도록 소스를 변경해 주어야 한다. 액셀러레이터의 ID를 메뉴의 ID와 같도록 해 두었기 때문에 WndProc의 WM_COMMAND는 수정할 필요가 없다. WinMain의 선두에 hAccel 변수를 선언해 주고 메시지 루프만 다음과 같이 수정해 주면 된다.

HACCEL hAccel;

hAccel=LoadAccelerators(hInstance,MAKEINTRESOURCE(IDR_ACCELERATOR1));
while(GetMessage(&Message,0,0,0)) {
	if (!TranslateAccelerator(hWnd,hAccel,&Message)) {
		TranslateMessage(&Message);
		DispatchMessage(&Message);
	}
}

못 보던 함수 두 개가 새로 등장했다. 우선 LoadAccelerators 함수부터 알아 보자.

HACCEL LoadAccelerators(HINSTANCE hInstance, LPCTSTR lpTableName ); 

이 함수는 리소스로부터 액셀러레이터 테이블을 읽어들인다. 두번째 인수 lpTableName은 액셀러레이터 테이블의 이름 문자열 포인터이되 우리가 작성한 액셀러레이터 테이블 IDR_ACCELERATOR1은 정수값이므로 MAKEINTRESOURCE 매크로를 사용해야 한다. 이 함수는 리소스에서 액셀러레이터 테이블을 읽은 후 그 핸들값을 리턴해 준다. 이 핸들값을 hAccel이라는 변수에 대입해 두면 다음부터 hAccel을 통해 액셀러레이터 테이블을 읽을 수 있다. 메시지 루프안에는 다음 함수가 삽입되었다.

int TranslateAccelerator(HWND hWnd, HACCEL hAccTable, LPMSG lpMsg ); 

이 함수는 키보드 메시지를 WM_COMMAND 메시지로 변경해 주어 액셀러레이터가 동작할 수 있도록 해 준다. 액셀러레이터 Ctrl+A가 입력되었다고 해 보자. Ctrl+A는 액셀러레이터이기 이전에 키보드로부터의 입력이므로 먼저 WM_KEYDOWN 메시지가 발생할 것이고 그대로 내버려 두면 WndProc의 WM_KEYDOWN 메시지 처리 루틴에서 먼저 이 키값을 처리해 버릴 것이다. 그래서 TranslateAccelerator 함수는 키보드 입력값을 읽어 이 키값이 지정한 액셀러레이터 테이블에 있는지를 먼저 살펴보고 있을 경우 WM_COMMAND 메시지를 발생시키고 TRUE를 리턴해 버린다. 그래서 액셀러레이터가 입력되었을 경우 TranslateMessage, DispatchMessage 함수가 실행되지 못하도록 막아 버리며 다음번의 WM_COMMAND메시지가 처리되도록 해 준다. 물론 액셀러레이터 입력이 아니면 FALSE를 리턴하여 다른 메시지들은 정상적으로 처리되도록 해 준다.

이제 예제를 완성했다. 컴파일시켜 보고 예제를 실행해보자. 프로그램 실행중의 모습은 다음과 같다.

단축키와 액셀러레이터가 정의되었으므로 다음 세가지 방법중 어떤 방법을 쓰더라도 Menu1메뉴 항목을 호출할 수 있게 되었다.

① 마우스로 File 메뉴를 열고 Menu1 메뉴 항목을 선택한다.

② Alt+F를 눌러 File 메뉴를 열고 1을 눌러 Menu1을 선택한다.

③ 곧바로 Ctrl+A를 누른다.


엑셀레이터는 단축키를 말한다. 다른거랑 비슷하다. 예제를 쳐보자. 

메뉴를 눌러도 WM_COMMAND 가 메시지 큐에 메시지가 쌓이고 단축키를 눌려도 메시지 큐에 WM_COMMAND가 쌓인다.


완성된 코드는 이러하며,

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"Menu";

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance
  , LPSTR lpszCmdParam, int nCmdShow)
{
  HWND hWnd;
  MSG Message;
  WNDCLASS WndClass;
  g_hInst = hInstance;
  HACCEL hAccel;

  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(hInstance, MAKEINTRESOURCE(IDC_CURSOR1));
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1);
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    100100500500,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1));
  while (GetMessage(&Message, 000)) 
  {
    if (!TranslateAccelerator(hWnd, hAccel, &Message)) 
    {
      TranslateMessage(&Message);
      DispatchMessage(&Message);
    }
  }

  while (GetMessage(&Message, 000))
  {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }

  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  switch (iMessage) 
  {
    case WM_COMMAND:
      switch (LOWORD(wParam))  
      {
        case ID_FILE_MENU1:
          MessageBox(hWnd, TEXT("첫번째 메뉴를 선택했습니다."), TEXT("Menu1 Demo"), MB_OK);
          break;
        case ID_FILE_MENU2:
          MessageBox(hWnd, TEXT("두번째 메뉴를 선택했습니다."), TEXT("Menu2 Demo"), MB_OK);
          break;
        case ID_FILE_EXIT:
          PostQuitMessage(0);
          break;
      }
    return 0;

    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}


WinMain에 

HACCEL hAccel;
.
.
hAccel=LoadAccelerators(hInstance,MAKEINTRESOURCE(IDR_ACCELERATOR1));
while(GetMessage(&Message,0,0,0)) {
	if (!TranslateAccelerator(hWnd,hAccel,&Message)) {
		TranslateMessage(&Message);
		DispatchMessage(&Message);
	}
}

이 부분이 수정 되었다라는 것을 볼 수 있다.



새로히 보이는 함수가 있는데LoadAccelerators() ,TranslateAccelerator()  이다. 어디 소스를 분석하며 살펴보자.


 HACCEL hAccel;

.

.

.

 hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1));
  while (GetMessage(&Message, 000)) 
  {
    if (!TranslateAccelerator(hWnd, hAccel, &Message)) 
    {
      TranslateMessage(&Message);
      DispatchMessage(&Message);
    }
  }

    

1. LoadAccelerators() 는 응용 프로그램의 리소스에 정의된 액셀러레이터 테이블을 읽어온다. (직접 정의한 엑셀레이터) 액셀러레이터 테이블은 응용 프로그램이

사용하는 단축키의 목록을 가지는 리소스이다. 이 함수로 읽어온 액셀러레이터 테이블은 메시지 루프에서 TranslateAccelerator() 에 의해 해석되어 

WM_COMMAND 메시지로 변환된다. 이 함수로 읽어온 액셀러레이터 테이블은 응용 프로그램이 종료될 때 자동으로 파괴되므로 직접 파괴해주지 않아도

된다. (Kill~() 동적 할당 해제하듯이 사용했던 애들.)

함수 원형

:HACCEL LoadAccelerators(HINSTANCE hInstance, LPCTSTR lpTableName ); 

여기 보이는 HINSTANCE 는 자료형의 일종이다. 16비트에서는 Uint 로써 32비트에서는 Ulong 으로써 작용한다. 알아두자.

또한, MAKEINTRESOURCE() 이것은 매크로 함수 인데,

말그대로 LPSTR로 형변환하는 겁니다
굳이 MAKEINTRESOURCE()만든 이유는 가독성이죠

그렇다.


2. TranslateAccelerator() 는 액셀러레이터 명령을 만든다. 이 함수는 hAccTable(LoadAccelerators() 로 부터 읽혀 들여진 액셀러레이터.을 참조하여

WM_KEYDOWN, WM_SYSKEYDOWN으로부터

WM_COMMAND, WM_SYSCOMMAND 메시지를 만들어 낸다. 눌러진 키가 액셀러레이터 테이블에 정의된 명령일 경우 명령 메시지로

변환하여 메시지 큐에 붙여주며 이 메시지는 다음번 GetMessage나 PeekMessage에 의해 읽혀져 처리되며 이 메시지가 완전히 처리되기 전에는

리턴하지 않는다.

함수 원형

:int TranslateAccelerator(HWND hWnd, HACCEL hAccTable, LPMSG lpMsg ); 


일반적으로 액셀러레이터는 메뉴 항목에 대한 단축키를 제공하기 위해 작성한다. 이 경우 액셀러레이터키가 눌러지면 마치 메뉴가 선택된 것처럼

WM_INITMENU, WM_INITPOPUPMENU 메시지가 전달된다. 단 윈도우가 사용금지 되어 있거나 메뉴 항목이 사용금지된 경우 , 마우스가 캡쳐된 경우는

제외된다. WM_COMMAND 메시지는 명령이 액셀러레이터로부터 온 경우 wParam의 상위 워드로 1이 전달 되며 메뉴로 부터 온 경우 0이 전달되는데

보통 이 구분은 무시하지만 메뉴로부터의 명령과 액셀러레이터로부터의 명령을 구분하려면 HIWORD(wParam)을 참고하도록 한다.


 hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1));
  while (GetMessage(&Message, 000)) 
  {
    if (!TranslateAccelerator(hWnd, hAccel, &Message)) 
    {
      TranslateMessage(&Message);
      DispatchMessage(&Message);
    }
  }


함수 분석이 끝났으니 소스 분석을 해보자.

GetMessage로 메시지를 조사한 후 먼저 TranslateAccelerator 함수가 이 메시지를 검사하여 액셀러레이터표에 있는 키보드 입력인지 조사한다.

만약 그렇다면 이 메시지는 WM_COMMAND로 변환되어 메시지 처리 함수로 보내지며 이 경우 TranslateMessage, DispatchMessage 함수는 호출되지

말아야 한다.

즉, 액셀러레이터 값이라면 TranslateMessage, DispatchMessage 함수 는 호출되지 말아야 한다. 그대로 while 문만 탈출한다.       


실행 화면은 전과 같을테니 생략하도록 하겠다. 물론 액셀~ 값과 마우스로 눌렀을 경우, 로 wParam의 상위 워드 0과 1로 나눌수야 있겠지만 우선 생략하도록 하자.





5-5 문자열 테이블

윈도우즈에서는 문자열들도 리소스의 일종으로 취급된다. 대량의 문자열을 사용하는 프로그램은 리소스에 문자열을 정의해 두고 필요할 때마다 리소스에 문자열을 읽어와 사용한다. 일단 문자열 리소스를 사용하는 간단한 예제를 같이 만들어 보도록 하자.

StrTable이라는 이름으로 프로젝트를 만들고 ApiStart.txt 파일을 복사해 온다. lpszClass 문자열을 "StrTable"로 변경한 후 소스 파일을 프로젝트에 포함시켜 표준적인 프로젝트를 먼저 만든다. 이 상태에서 문자열 리소스를 정의하기 위해 Insert/Resource 항목을 선택하고 리소스 종류에서 String Table을 선택한다.

다음과 같은 문자열 리소스 편집기가 열릴 것이다. 액셀러레이터 편집기와 유사하게 생겼는데 아직 정의된 문자열이 없으므로 비어 있다.

새 문자열을 삽입하기 위해 빈칸을 더블클릭하여 속성 편집기를 열고 다음과 같이 입력한다.

ID는 디폴트로 제시되는 IDS_STRING1을 선택하고 Caption에 적당한 문자열을 입력하였다. 리소스 스크립트를 StrTable.rc로 저장하고 프로젝트에 포함시키면 문자열 리소스는 다 만든 것이다. 이제 이 문자열 리소스를 소스에서 사용하기 위해 코드를 작성한다.

#include "resource.h"
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	char str[256];
	switch(iMessage) {
	case WM_PAINT:
		hdc=BeginPaint(hWnd, &ps);
		LoadString(g_hInst, IDS_STRING1, str, 256);
		TextOut(hdc,10,10,str,strlen(str));
		EndPaint(hWnd, &ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

리소스 ID를 인식하기 위해 resource.h를 먼저 포함시켰으며 WM_PAINT에서 문자열 리소스를 읽어와 화면으로 출력하였다. 문자열 리소스를 읽을 때는 다음 함수를 사용한다.

int LoadString( HINSTANCE hInstance, UINT uID, LPTSTR lpBuffer, int nBufferMax);

인수가 네 개나 되지만 별로 복잡하지 않다. 첫번째 인수는 문자열 리소스를 가진 인스턴스 핸들인데 이 값은 WinMain의 첫번째 인수로 전달되며 ApiStart.txt에서 이 인수를 전역 변수 g_hInst에 대입해 두었으므로 g_hInst를 써 주면 된다. 두번째 인수로 읽어올 문자열의 ID를 주고 세번재 인수로 문자열을 읽을 버퍼, 네번째 인수로 버퍼의 길이를 주면 된다.

LoadString(g_hInst, IDS_STRING1, str, 256);

이 함수 호출문의 의미를 해석해 보면 g_hInst 인스턴스에 정의된 IDS_STRING1 문자열을 길이 256의 str 문자 배열로 읽어 오라는 뜻이다. 문자열 리소스는 최대 255문자까지 가능하므로 버퍼 길이는 256이면 충분하다. 이렇게 읽은 문자열 str을 TextOut으로 화면에 출력하였다.

문자열 리소스에 정의된 문자열이 화면으로 출력되었다. 별로 어려운 내용은 없으므로 문자열 리소스를 정의하고 사용하는 방법에 대해서는 쉽게 이해가 갈 것이다. 그런데 이렇게 문자열 리소스를 정의해서 쓰는 이유는 뭘까? 문자열을 출력할 단순한 목적이라면 다음과 같이 코드를 작성하는 것이 훨씬 더 쉬워 보인다.

	char str[256]="String Resource Test";
	switch(iMessage) {
	case WM_PAINT:
		hdc=BeginPaint(hWnd, &ps);
		TextOut(hdc,10,10,str,strlen(str));
		........

문자 배열에 바로 문자열을 초기화시켜 이 문자열을 화면으로 출력하면 그만이다. 물론 단순한 문자열 출력이 목적이라면 이런 방법이 더 편하겠지만 문자열 리소스는 여러가지 면에서 이점을 준다.

우선 첫째로 문자열 자체가 코드와 분리됨으로써 문자열만 따로 관리할 수 있으며 프로젝트를 유지하는데도 큰 도움을 준다. 프로그램은 실행중에 사용자가 조작을 잘못하면 에러 메시지를 보여주며 간단한 안내문이나 도움말을 보여주기도 한다. 이런 메시지 문자열들이 많을 경우 수백개가 되는데 이 문자열들이 몽땅 소스 코드에 다 들어가 있다고 한다면 정말 끔직할 것이다. 메시지 문자열이나 안내문들도 사용자에 대한 세심한 배려이기 때문에 문장을 바르게 예의있게 작성해야 하는데 프로그래머가 코드에 신경쓰다 보면 이런 메시지들에 제대로 신경을 쓸 수가 없다. 문자열들이 리소스로 분리되어 있다면 전문 디자이너가 메시지들을 한꺼번에 모아 작성할 수 있을 것이다. 만약 어떤 이유로 메시지들을 전부 수정해야 한다고 해보자. 이때 코드의 여기저기에 박혀있는 메시지를 찾아 고치는 경우와 리소스 편집기에서 메시지를 일괄적으로 고치는 것 중 어떤 것이 더 빠르고 정확할 것인가는 쉽게 상상이 갈 것이다.

문자열 리소스를 사용하는 두번째 이점은 다국어 버전을 쉽게 만들 수 있다는 점이다. 리소스는 조건에 따라 교체할 수 있기 때문에 경우에 따라 다른 문자열을 사용할 수 있다. 그래서 영어 리소스 한벌, 한글 리소스 한벌을 각각 따로 만들어 놓으면 소스 코드는 건드릴 필요없이 리소스만 교체함으로써 영문 프로그램, 한글 프로그램을 쉽게 만들 수 있다. 리소스만 만들면 일본이나 중국 어디든지 팔아먹을 수 있게 되는 것이다. 물론 문자열이 소스에 작성되어 있어도 찾아서 고쳐주면 가능이야 하겠지만 무척 어려운 일이다.

또한 문자열이 소스와 분리되어 있으면 문자열을 고쳐도 소스를 다시 컴파일할 필요가 없어 개발 기간도 빨라진다. 이런 여러가지 장점이 있기 때문에 아주 간단한 문자열인 경우를 제외하고 좀 길다 싶은 문자열은 왠만하면 문자열 리소스로 만들어 쓰는 것이 좋다. 필자는 한때 이런 원칙을 무시한 나머지 "그라모 안돼는데예"라고 장난으로 메시지를 써 놨다가 파이널 베타에서 이 메시지를 찾아 고친 적이 있었는데 만약 그대로 제품으로 나갔다가는 큰 웃음거리가 될 뻔 했었다. 메모리 사용면에 있어서도 코드에 문자열을 바로 기입하는 것보다 문자열 리소스를 사용하는 것이 유리하다. 운영체제는 꼭 필요한 문자열만 메모리로 읽어들일만큼 지능적으로 메모리를 관리해주기 때문이다.

이상으로 윈도우즈에서 사용되는 몇가지 리소스에 대해 알아 보았는데 이외에도 중요한 리소스로 비트맵과 대화상자가 있다. 비트맵에 대해서는 다음장인 6장에서, 대화상자에 대해서는 8장에서 별도로 공부할 것이다.



(주쌤)

{

여기있는 내용을 수정을 한다면 번역을 할 때 많이 쓰인다. 

이 테이블만 고치면 프로그램 수정을 안해도 한글화가 된다라는 뜻이다.

프린트에프에 들어있는 메시지들 수정할 것들을 한글화로 전부 하는 식이다.

게임을 몰라도 테이블에 있는 것만 보고 바꿔도 수정이 손쉬워 지므로 

게임에 필요한 것들을 대부분 여기에 등록하고 쓰면 편하게 할 수 있다.

보통은 잘 안쓰지만 쓰일 수도 있으니 알아두자.

}


이번엔 문자열 리소스이다. 해보자.





#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"Menu";

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance
  , LPSTR lpszCmdParam, int nCmdShow)
{
  HWND hWnd;
  MSG Message;
  WNDCLASS WndClass;
  g_hInst = hInstance;
  HACCEL hAccel;

  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(hInstance, MAKEINTRESOURCE(IDC_CURSOR1));
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1);
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    100100500500,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1));
  while (GetMessage(&Message, 000)) 
  {
    if (!TranslateAccelerator(hWnd, hAccel, &Message)) 
    {
      TranslateMessage(&Message);
      DispatchMessage(&Message);
    }
  }

  while (GetMessage(&Message, 000))
  {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }

  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  char str[256];

  switch (iMessage) 
  {
    case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      LoadString(g_hInst, IDS_STRING104, str, 256);
      TextOut(hdc, 1010, str, strlen(str));
      EndPaint(hWnd, &ps);
      return 0;

    case WM_COMMAND:
      switch (LOWORD(wParam))  
      {
        case ID_FILE_MENU1:
          MessageBox(hWnd, TEXT("첫번째 메뉴를 선택했습니다."), TEXT("Menu1 Demo"), MB_OK);
          break;
        case ID_FILE_MENU2:
          MessageBox(hWnd, TEXT("두번째 메뉴를 선택했습니다."), TEXT("Menu2 Demo"), MB_OK);
          break;
        case ID_FILE_EXIT:
          PostQuitMessage(0);
          break;
      }
    return 0;

    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}



이거 뭔가 자꾸 깨지게 나온다..

해결방안은 없을까.

LPSTR, LPCSTR, LPTSTR, LPCTSTR , LPWSTR, LPCWSTR 의 의미

http://pelican7.egloos.com/1768951

LPSTR, LPCSTR, LPTSTR, LPCTSTR , LPWSTR, LPCWSTR 
뭔가 다 비슷 비슷해보이죠? 

원래 c와 c++은 string이라는 똑똑한 자료구조형을 compiler차원에서 지원하고 있지 않습니다. 

그대신 가장 많이 사용하는 string을 어떻게 저장해야 할지에 대해 고심한 결과... 
결국 배열의 끝에 '\0'또는 0 또는 NULL값을 넣어 string을 표현하도록 했습니다. 
결국 가장 적은 용량의 string처리와 가장 골치아픈 string처리가 탄생하는 순간이였죠. 

어쨌거나 요점은... 
Windows에서는 이런 string처리를 위해서 char* 형을 그대로 쓰기 보다는 LPCSTR등의 표현으로 대치해 사용함으로써, 개발의 편의성을 돕고 있습니다. 

자... 그럼 서론이 길었고... 
위의 골치아픈 형을 살펴보면.. 

같은 글자들이 여러번 반복해 나옴니다. 

LP, C, STR 등이 거의 자주 반복되고, 
어떤놈들은 T 나 W를 사용하기도 하죠. 

글자를 하나씩 살펴볼까요. 

LP는 long pointer를 나타내는 약어로서 16bit시절의 윈도우의 유산입니다. 
과거 windows3.1까지의 시절에는 포인터는 모두 16bit였고, 24bit 메모리를 long pointer라는 것을 통해서 extended memory라는 이름으로 관리했었거든요.. 
현재 LP(long pointer)는 .Net에서는 64bit pointer를, VC++6.0과 그 이전 버전에서는 32bit pointer를 나타냅니다. 

C는 constant, 즉 함수의 내부에서 인자값을 변경하지 말라는 뜻입니다. 

STR은 말그대로 string자료가 될것이라는 뜻으로 내부적으로는 char형 배열에 null값 종료를 의미하고 있죠. 

자... 그럼 해석해 봅시다.. 
LPSTR = long pointer string = char * 
LPCSTR = long pointer constant string = const char * 
결과적으로는 맨 마지막과 같은 형이라는 거죠. 

그런데... 
LPCTSTR!! 요넘은 무었이냐!! 
LPCTSTR = long pointer constant t_string = const tchar * 
앗! 오타입니다. t라는 놈이 들어갔네요.. 
오타일까요? ^^ 아닙니다. t라는 놈은 우리나라를 위해 아주 중요한 역할을 하는 놈이죠.. 이것은 잠시 이후에 살펴보겠습니다. 

그럼 먼저.. 
W라는 넘을 살펴보죠... 

W 이넘은 wide char를 나타냅니다. 쉽게 말하면 unicode죠.. 
win9x에서 사용하던 multibyte와는 다릅니다. 물론 한글 조합형 코드도 아니고... 
unicode를 나타냅니다. 

자 그럼 다시 해석을 해보죠. 
LPWSTR = long pointer wide string = w_char * 
LPCWSTR = long pointer constant wide string = const w_char * 

위와 같이 해석됩니다. 


그런데 t_char('티캐릭터'라고 읽습니다.)는 무었이냐!! 

마이크로소프트가 세계 각국에 제품을 판매하면서.. 
각국의 언어에 맞추어 개발하는 것에 환멸을 느끼다가.. 
드디어 windows를 unicode기반으로 개발하는 작업에 착수했습니다. 

그런데... 문제는 char는 1Byte이고 wide char는 2Byte이므로.. 
포인터 연산을 많이하는 c, c++코드는 호환성에 치명적인 문제가 있었죠. 
그래서 컴파일러가 precompile option을 보고. 환경에 맞게 동작하는 코드를 작성할 수 있는 새로운 변수 모양의 Macro를 선언하게 되었습니다. 
그것이 바로 TCHAR, t_char라는 변수죠. 
이놈들은 자신의 운영체제가 multi-byte환경이면, char형으로, 
unicode환경이면, w_char, wide char형으로 type casting됩니다. 

그래서... 보통 windows 9x, 2000계열의 환경이라면, 
LPTSTR = LPSTR = char * 
LPCTSTR = LPCSTR = const char *가 됩니다. 

그런데.. 
아마 저 코드에서.. 
(LPSTR)(LPCTSTR) 형변환을 할때 자세히 보면.. 
const 라는 키워드만 떼내는거지요... 
그러니까 사실은 (char *)(const char *)와 같은 말입니다. 
웃기는 형변환이죠.. 
그럼 없어도 될까요? 
^^ 

없으면 당연히 오류가 나게됩니다. 
왜냐면...(LPSTR)CString을 하면.... CString형 자료의 맨 처음 주소부터 char * 형으로 형변환하기 때문이죠. 
CString형은 앞의 16Byte를 자료형을 표현하기 위해서 사용하기 때문에, 여기서부터 형 변환을 해주면 엉뚱한 값이 표현되게 됩니다. 

따라서 MFC에서 지원하는 CString class는 LPCTSTR라는 함수를 통해서 일단 안전하게 const char * 형으로 바뀐 자료형을 얻어오게 하는거죠. 

CString myString; 
(LPCTSTR)myString;이라고 해주면.. 
myString내부의 string 값을 꺼내오게 도와주는 연산자 또는 함수를 사용하게 된겁니다. 
즉 (LPCTSTR)이란 놈이 반환값이 const char* 인 함수입니다. 
정확하게 표현하면 operator overloading이라는 거지요. 

결과적으로 (LPSTR)(LPCTSTR)myString은 
myString의 내부 string 자료를 함수를 통해 자료를 꺼내온뒤에, char* type으로 안전하게 바꾸어주는 역할을 하게 되는 거지요. 

참고로, 함수의 인자가 char * 인곳에 const char* 형을 넣으면 컴파일 오류가 발생하기 때문에 (LPSTR)을 한번더 앞에 써주어서 강제 type casting을 한 것입니다. 











6-1-가. GDI오브젝트

GDI 오브젝트(GDI Object)란 그래픽 출력에 사용되는 도구를 말하며 펜, 브러시, 비트맵, 폰트 등등이 모두 GDI 오브젝트이다. 사람이 그림을 그릴 때 연필, 붓 등의 도구를 사용하는 것과 마찬가지로 GDI가 그래픽을 출력할 때는 GDI 오브젝트를 사용한다. 즉 선을 그을 때는 펜을 사용하며 면을 채울 때는 브러시를 사용하고 문자열을 출력할 때는 폰트를 사용한다. GDI 오브젝트를 모아놓은 것이 DC이며 GDI는 현재 DC에 선택된 GDI 오브젝트를 사용한다. 그래서 사용자는 그래픽을 출력하기 전에 DC에 원하는 오브젝트를 선택해 줌으로써 그래픽을 다른 모양으로 변경할 수 있다.

예를 들어 그냥 선을 그으면 디폴트 펜인 검정색 펜으로 그려지지만 파란색 펜을 만들어 DC에 선택한 후 선을 그으면 GDI는 이 파란색 펜을 사용하여 선을 긋게 되므로 파란색 선이 그려지게 된다. 마찬가지로 브러시나 폰트를 변경하면 채워지는 색상이나 문자열의 글꼴 모양을 변경할 수 있다. GDI 오브젝트는 GDI가 그래픽 출력을 위해 사용하는 도구임과 동시에 사용자가 GDI의 출력을 조정할 수 있는 도구이기도 하다.

GDI 오브젝트는 내부적으로 일종의 구조체이겠지만 우리가 사용할 때는 모두 핸들로 관리된다. GDI 오브젝트를 만들 때 핸들을 발급받으며 선택하거나 삭제할 때는 이 핸들만 가지고 GDI 오브젝트를 사용하게 된다. DC가 BeginPaint나 GetDC 함수에 의해 처음 만들어졌을 때 디폴트로 선택된 GDI 오브젝트는 다음과 같다.

GDI 오브젝트핸들 타입설명디폴트
HPEN선을 그을 때 사용된다.검정색의 가는 선
브러시HBRUSH면을 채울 때 사용된다.흰색
폰트HFONT문자 출력에 사용되는 글꼴시스템 글꼴
비트맵HBITMAP비트맵 이미지선택되지 않음
팔레트HPALETTE팔레트선택되지 않음
영역HRGN영역선택되지 않음


(주쌤)

{

선을 그을떄 사용된다. 페인트통이라고 생각하면 된다.

비트맵을 들고올수도, 그릴수도 있다. 파일 읽어가 출력할 수도있다

윈api는 비트맵을 그리고 수동으로 점찍어주는것도 가능 하다. 윈도우가 다 자동으로 해준다. 사용방법을 익히자.

}





6-1-나. 스톡 오브젝트

스톡 오브젝트(Stock Object)는 윈도우즈가 기본적으로 제공해 주는 GDI 오브젝트를 말한다. 운영체제가 제공해 주므로 일부러 만들지 않아도 언제든지 사용할 수 있으며 사용하고 난 후에 파괴시켜 줄 필요도 없다. 다음 함수로 핸들을 얻어 사용하기만 하면 된다.

HGDIOBJ GetStockObject( int fnObject ); 

fnObject 인수에 사용하고자 하는 스톡 오브젝트를 기입해 주면 된다. 사용 가능한 스톡 오브젝트는 다음과 같다. 주로 브러시와 펜이 스톡 오브젝트로 제공된다. 2장에서 처음 공부했던 WHITE_BRUSH도 알고보면 스톡 오브젝트의 일종이다.

fnObject설명
BLACK_BRUSH검정색 브러시
GRAY_BRUSH회색 브러시
NULL_BRUSH투명 브러시
WHITE_BRUSH흰색 브러시
DKGRAY_BRUSH짙은 회색 브러시
LTGRAY_BRUSH옅은 회색 브러시
BLACK_PEN검정색 펜
WHITE_PEN흰색 펜
NULL_PEN투명 펜
ANSI_FIXED_FONT고정폭 폰트
ANSI_VAR_FONT가변폭 폰트
DEFAULT_PALETTE시스템 팔레트

그러면 스톡 오브젝트를 사용하는 예제를 만들어 보고 GDI오브젝트를 어떤 식으로 사용하는가를 살펴 보자. GdiObj.dsw라는 프로젝트를 만들고 다음과 같이 Gdiobj.cpp를 작성한다.

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	switch(iMessage) {
	case WM_PAINT:
		hdc=BeginPaint(hWnd,&ps);
		Rectangle(hdc,50,50,300,200);
		EndPaint(hWnd,&ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

4장 끝에서 말했지만 노파심에서 한번 더 얘기하자면 앞으로는 WndProc의 소스만 보인다. 이때까지의 실습을 통해 알수 있겠지만 WinMain은 아주 특별한 경우가 아니면 변하지 않으므로 굳이 소스 리스트를 보일 필요가 없을 것이다. 괜히 지면만 낭비하고 독자들의 머리와 눈만 괴롭힐 것 같으므로 앞으로도 꼭 필요한 경우가 아니면 WinMain은 보이지 않는다. WndProc외에는 WinMain 앞에 선언된 lpszClass 문자열값만 프로젝트의 이름으로 변경해 주면 된다.

LPSTR lpszClass="GdiObj";

이 문자열은 윈도우 클래스의 이름과 윈도우의 타이틀 바에 사용되는데 당연히 바꿔 주어야 할 부분이므로 앞으로 이 부분에 대해서도 설명을 생략하도록 한다.

이 소스에서 WM_PAINT 메시지를 보면 BeginPaint로 DC를 얻은 후 Rectangle 함수로 사각형을 그리고 EndPaint로 그리기를 종료하고 있다. DC를 만든 후 어떠한 GDI 오브젝트도 만들거나 선택하지 않고 곧바로 사각형을 그렸으므로 이 때 사용되는 GDI 오브젝트는 모두 디폴트 GDI 오브젝트이다. 즉 선은 검정색의 가는 선으로 그려지고 사각형 안쪽의 채워지는 면은 단순한 흰색일 뿐이다. 실행 결과는 다음과 같다.

보다시피 아주 썰렁하다. 이 상태에서 브러시를 변경하여 채워지는 면을 흰색이 아닌 다른 색으로 바꾸어 보도록 하자. 브러시를 직접 만들어 사용하는 실습은 잠시 후에 해 보기로 하고 일단 스톡 오브젝트를 사용한다. 다음 문장들을 추가해 보아라.

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	HBRUSH MyBrush,OldBrush;
	switch(iMessage) {
	case WM_PAINT:
		hdc=BeginPaint(hWnd,&ps);
		MyBrush=(HBRUSH)GetStockObject(GRAY_BRUSH);
		OldBrush=(HBRUSH)SelectObject(hdc,MyBrush);
		Rectangle(hdc,50,50,300,200);
		SelectObject(hdc,OldBrush);
		EndPaint(hWnd,&ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

4줄이 추가되었다. 이제 다시 컴파일하고 실행해 보자. 결과는 다음과 같다.

흰색 사각형 대신 회색의 사각형이 그려졌다. 어째서 이런 그림이 그려졌는지 코드를 살펴보도록 하자. 우선 브러시 핸들을 저장할 변수 두 개를 선언한다. 그리고 GetStockObject 함수로 회색의 브러시 핸들을 얻은 후 이 브러시를 DC에 선택하되 MyBrush 변수에 대입하기 위해 (HBRUSH)형으로 캐스팅 해 주어야한다. GDI 오브젝트를 DC에 선택할 때는 다음 함수를 사용한다.

HGDIOBJ SelectObject( HDC hdc, HGDIOBJ hgdiobj ); 

첫번째 인수로 DC의 핸들을 주고 두번째 인수로 GDI 오브젝트의 핸들을 주면 DC에 해당 오브젝트를 선택해 준다. 이후부터 GDI는 그래픽을 출력할 때 선택된 오브젝트를 사용하게 된다. SelectObject가 리턴하는 값은 새로 선택되는 오브젝트 이전에 선택되어 있던 같은 종류의 오브젝트 핸들이다. 이 핸들값은 복구를 위해 반드시 별도의 변수에 저장해 두어야 한다. 위 예제에서는 이전 브러시의 핸들을 OldBrush 변수에 대입해 두었다.

회색 스톡 브러시를 선택한 후 Rectangle 함수로 사각형을 그렸으므로 사각형의 내부는 회색으로 채워지게 된다. 사각형을 그리고 난 후는 SelectObject를 한번 더 호출하여 원래의 GDI 오브젝트인 OldBrush를 복구해 주어야 한다. 그 이유에 대해서는 잠시 후에 다시 논해 보도록 하자.




자동으로 다 해준다.


LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	HBRUSH MyBrush,OldBrush;
	switch(iMessage) {
	case WM_PAINT:
		hdc=BeginPaint(hWnd,&ps);
		MyBrush=(HBRUSH)GetStockObject(GRAY_BRUSH);
		OldBrush=(HBRUSH)SelectObject(hdc,MyBrush);
		Rectangle(hdc,50,50,300,200);
		SelectObject(hdc,OldBrush);
		EndPaint(hWnd,&ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}


(주쌤)

{

되돌리기 위해 OldBrush가 잠시 가지고 있는 것이다. 

기존것을 원래대로 되돌려 놔야지 , 콘텍스트 스위칭 할때도 레지스터 값들을 마치 호출안한듯이 전부 되돌리는 작업을 했듯이 이도 똑같다.

SelectObject   는 기존의 것들을 반환한다라는 것이다.

그레이로 잠시 바뀌어 졌다라는 것이다.

}


완성된 코드를 보자. 커서와 이것저것 설정이 되어있는 윈 메인은 잠시 눈길을 피해주길 바란다.


#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"Menu";

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 = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(hInstance, MAKEINTRESOURCE(IDC_CURSOR1));
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1);
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    100100500500,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000))
  {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }

  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  HBRUSH MyBrush;
  HBRUSH OldBrush;

  switch (iMessage) 
  {
    case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      //MyBrush = (HBRUSH)GetStockObject(GRAY_BRUSH);
      //OldBrush = (HBRUSH)SelectObject(hdc, MyBrush);
      Rectangle(hdc, 5050300200);
      EndPaint(hWnd, &ps);
      return 0;

    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}


현재 실행화면은, 




이와 같고 저기 주석이 되있는 부분을 풀면,



이와 같다.


case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      //MyBrush = (HBRUSH)GetStockObject(GRAY_BRUSH);
      //OldBrush = (HBRUSH)SelectObject(hdc, MyBrush);
      Rectangle(hdc, 5050300200);
      EndPaint(hWnd, &ps);
      return 0;


이 부분을 해석해보자.


1. BeginPaint() 로 드로잉 작업을 수행하기 위해서 CWnd를 준비한다. (DC 를 준비한다.)

2. GetStockObject() 는 스톡 오브젝트 로 운영체제가 기본적으로 제공해주는 GDI 오브젝트이다. 생성할 필요가 없으며 이 함수로 핸들을 구해 언제든지

사용할 수 있고 생성할 필요가 없으니 파괴시켜 주지 않아도 된다. 

우리가 봤던 WinMain이 이 부분이 있다.

WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);

기본적인 선 처리를 하는 중간 쯤에 이부분을 해석하자면

"흰색 스톡 브러시를 구해 윈도우 클래스의 배경 브러시로 지정한다" 라는 뜻이 된다.

이 윈도우는 배경을 지울 필요가 있을 때 흰색 브러시를 사용하므로 배경은 항상 흰색으로 채색된다.

3. SelectObject(), GDI는 그래픽 출력에 사용되는 도구이며 펜, 브러시, 비트맵, 리전, 패스, 팔레트, 폰트 등등이 있다.

그리기에 사용할 GDI 오브젝트를 변경하고자 할 때 이 오브젝트를 만든 후 이 함수로 DC에 선택해 주어야 한다. 

//MyBrush = (HBRUSH)GetStockObject(GRAY_BRUSH);
//OldBrush = (HBRUSH)SelectObject(hdc, MyBrush);

이 부분을 보면,

1. "그레이 브러시를 구해 MyBrush에 색을 지정해 놓는다."

2. "DC에 MyBrush의 색으로 그리기에 사용할 채색을 지정한다."

이렇게 된다.

또다른 예를 보자.

(다른 예의 설명) - 예는 적지 않았다.

즉, SelectObject() 가 리턴하는 이전 브러시의 핸들은 복구를 위해 OldBrush 등의 변수에 저장해 두어야 한다. 브러시를 선택한 후 모든 그리기 함수는

MyBrush로 면을 채색한다. 브러시를 사용한 후에는 반드시 DeleteObject 함수로 삭제해 주어야 하되 그전에 DC에 선택되어 있는 브러시를 선택 해제 해 주어야 한다.

DC에 선택된 브러시는 삭제할 수 없기 때문이다. 그래서 OldBrush를 다시 선택해 주어 MyBrush를 해제하도록 하였다. 이 코드는 다음과 같이 한줄로 작성될 수 있다.

DeleteObject(SelectObject(hdc, OldBrush));

SelectObject() 가 이전에 선택되어 있던 같은 타입의 GDI 오브젝트를 리턴해 주기 때문에 리턴되는 브러시를 DeleteObject()로 삭제하였다.


즉, 다시 정리해서 말하자면. SelectObject() 가 반환하는 핸들러 값은 이 전 핸들러 값을 반환하게 된다. 그것을 받아놨다가 나중에 다시

SelectObject()로 이전 상태를 복원하는 것이다. 이것을 한 줄로 나타내서 더욱 안전한 소스를 짜보자면,

이 설명에 의해서 다시 안전한 소스를 작성하면 이와 같다.

case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      MyBrush = (HBRUSH)GetStockObject(GRAY_BRUSH);
      OldBrush = (HBRUSH)SelectObject(hdc, MyBrush);
      Rectangle(hdc, 5050300200);
      DeleteObject(SelectObject(hdc, OldBrush));
      EndPaint(hWnd, &ps);
      return 0;

 DeleteObject(SelectObject(hdc, OldBrush));


이 부분을 해석하면 SelectObject() 로 예전 브러시로 바꿔준 뒤 바뀌었던 MyBrush 의 핸들러 값을 SelectObject() 가 반환하므로 받아서 바로

해제 시켜주는 부분이다.


(주쌤)

{

펌웨어는 레지스터 사용방법을 익히는게 핵심이고 WinAPI는 함수를 어떻게 활용할 줄 알고 그것들을 더 잘아는 것들이 더 세련되게 나오고 하는 것이다.

cpu도 똑같다. spi나 이런것들을 익히면 세련되어 진다.

(함수만 많이 알아두면 더 세련되게 나온다라는 소리, 실력은 곧 다양한 함수 싸움)

c가 기본으로 되었기 때문에 우리가 api를 잘 짤 수 있는 것이다. 

mfc 제대로 짜려고 한다면 C++은 되어야 하고 API 는 되어야 한다.

mfc 구조도 알고 있어야 한다.

(즉, mfc를 제대로 하려면 전제 조건으로 c, c++, WinAPI, MFC 구조 를 잘 알고 있어야 한다.)

api와 c++ 과의 연결고리는 mfc 구조에 나와있으므로 알고 있어야 짜기 쉽다.

MFC App 이런 책 하나 사서 공부하고 나면 이제 프로그램 짤만해진다.

MFC App 쉽게 배울수 있는 책이 있다. 나중에 따로 말하면 알려 주겠다.

 }







6-1-다. 색상

잠시 후에 다양한 색상과 모양의 펜, 브러시를 만들어 볼텐데 그 전에 윈도우즈에서 색상을 표현하는 방법에 대해 알아보자. 도스에서는 WHITE, YELLOW, RED 등의 매크로 상수로 색상을 표현했고 이 매크로들의 실제값은 0~15까지의 정수였었다. 도스에서야 기껏 16색상까지 사용할 수 있었으므로 각 색상마다 이름을 줄 수 있었지만 최대 천육백만가지 색상을 사용할 수 있는 윈도우즈 환경에서는 이런 간단한 방법을 쓸 수 없다. 윈도우즈에서는 색상값을 표현하기 위해 COLORREF라는 데이터형을 사용하는데 이는 다음과 같이 정의되어 있다.

typedef DWORD COLORREF;

보다시피 COLORREF 형은 부호없는 32비트 크기의 정수형이며 8비트씩 빨간색, 초록색, 파란색의 농도를 나타내며 상위 8비트는 사용되지 않는다. 각 색상 요소는 1바이트의 크기를 가지므로 0~255까지의 농도를 표현할 수 있다.

COLORREF형은 32비트 정수일 뿐이므로 직접 16진수로 표현할 수도 있다. 예를 들어 0는 검정색이 되며 0xff는 빨간색, 0xff0000은 파란색이 된다. 하지만 이 방법은 너무 기계적이고 초보자가 쓰기는 힘들므로 색상값을 만들 때는 통상 RGB 매크로 함수를 사용하며 이 매크로는 다음과 같이 정의되어 있다.

#define RGB(r,g,b)          ((COLORREF)(((BYTE)(r) | ((WORD)((BYTE)(g))<<8)) | (((DWORD)(BYTE)(b))<<16)))

세 개의 인수를 가지는데 각각 빨간색, 초록색, 파란색의 농도이며 이 세값을 조립하여 하나의 32비트 색상값을 만들어내는 간단한 비트 연산을 하고 있다. 비트 쉬프트, 비트 OR 연산자를 적절히 혼합한 문장인데 그리 어렵지 않게 이해가 될 것이다.

각 색상 요소가 얼마만큼 혼합되어 있는가에 따라 실제 색상이 결정되는데 RGB(255,0,0)는 빨간색, RGB(0,0,255)는 파란색이다. 세 요소가 모두 다 최대치인 RGB(255,255,255)는 흰색이며 반대로 RGB(0,0,0)는 검정색이다. 윈도우즈에서는 이와같이 RGB 매크로를 사용하여 색상값을 표현하며 COLORREF 형의 인수 자리에는 RGB 매크로를 사용하면 된다. 다음 세 매크로 함수는 COLORREF 형 변수값에서 각 색상요소의 농도를 분리해내는 함수이다.

#define GetRValue(rgb)      ((BYTE)(rgb))
#define GetGValue(rgb)      ((BYTE)(((WORD)(rgb)) >> 8))
#define GetBValue(rgb)      ((BYTE)((rgb)>>16))







6-1-라. 펜

펜은 선을 그을 때 사용되는 GDI 오브젝트이다. 펜을 변경하면 그려지는 선의 모양을 마음대로 변경할 수 있다. 그런데 윈도우즈가 제공하는 스톡 펜은 흰색, 검정색, 투명색 세 가지 뿐이며 파란색, 노란색 등의 원색 펜은 없다. 이런 펜을 사용하고자 할 때는 직접 만들어서 사용해야 한다. 펜을 만들 때는 다음 함수를 사용한다.

HPEN CreatePen( int fnPenStyle, int nWidth, COLORREF crColor ); 

세 개의 인수를 가지며 각 인수의 의미는 다음과 같다.

fnPenStyle

그려질 선의 모양을 정의한다. 이 값을 변경하면 실선뿐만 아니라 다양한 형태의 선을 만들 수 있다. 실선, 점선, 일점 쇄선 등등의 선 모양이 있다.

PS_SOLIDPS_DASHPS_DOTPS_DASHDOTPS_DASHDOTDOT
nWidth

선의 폭을 지정한다. 디폴트 선의 굵기는 1이지만 이 값을 2나 3으로 변경해 주면 두꺼운 선을 그릴 수 있다. 단 이 값이 0일 경우는 맵핑 모드에 상관없이 무조건 1픽셀 두께의 선이 만들어진다.

crColor

선의 색상을 지정한다. COLORREF 형이므로 RGB 매크로 함수를 사용하면 된다.

리턴값으로는 만들어진 펜의 핸들을 돌려주므로 이 값을 잘 보관해 두어야 만들어진 펜을 사용할 수 있다. 모양, 굵기, 색상 세 가지 속성을 조합하면 아주 다양한 형태의 펜을 만들 수 있을 것이다. 그럼 펜을 만들어 사용해 보도록 하자. 별도의 예제를 만들 필요없이 GdiObj 프로젝트를 다음과 같이 수정한다.

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	HPEN MyPen, OldPen;
	switch(iMessage) {
	case WM_PAINT:
		hdc=BeginPaint(hWnd,&ps);
		MyPen = CreatePen(PS_SOLID, 5, RGB(0,0,255));
		OldPen = (HPEN)SelectObject(hdc, MyPen);
		Rectangle(hdc,50,50,300,200);
		SelectObject(hdc, OldPen);
		DeleteObject(MyPen);
		EndPaint(hWnd,&ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

실행 결과는 다음과 같다.

CreatePen 함수를 호출하여 굵기 5의 파란색 실선 펜을 만든 후 이 펜의 핸들을 MyPen에 대입하였다. 그리고 SelectObject로 이 펜을 DC에 선택한 후 사각형을 그렸으므로 사각형의 테두리는 분명히 굵은 파란색으로 그려질 것이다. CreatePen 함수 자체가 그다지 어렵지 않으므로 이 코드는 아주 쉽게 이해가 갈 것이다.

GDI 오브젝트는 사용한 후 반드시 삭제해 주어야 한다. 왜냐하면 GDI 오브젝트도 메모리를 사용하기 때문이다. 메모리를 할당한 후 반드시 해제해 주어야 하는 것과 마찬가지로 GDI 오브젝트도 사용이 끝나면 해제해 주어야 하는 것이 원칙이다. 만약 해제해 주지 않으면 시스템의 메모리를 갉아먹게 될 것이다. GDI 오브젝트를 삭제할 때는 다음 함수를 사용한다.

BOOL DeleteObject( HGDIOBJ hObject ); 

삭제하고자 하는 GDI 오브젝트의 핸들만 인수로 넘겨주면 된다. 단, 이때 주의할 것은 DC에 현재 선택되어 있는 GDI 오브젝트는 삭제할 수 없다는 점이다. 현재 사용되고 있는 객체를 함부로 삭제하도록 내버려 둘 수는 없기 때문에 생긴 일종의 안전 장치 역할을 하는 규정이다. 그래서 삭제를 하기 전에 먼저 DC에 선택된 객체를 선택 해제해 주어야 하는데 선택을 해제시켜주는 별도의 함수는 제공되지 않으므로 다른 GDI 오브젝트를 선택해 주는 방법을 사용한다. 이런 이유로 OldPen이라는 핸들을 만든 후 이 핸들에 MyPen이 선택되기 전의 펜 핸들을 저장해 두고 MyPen을 삭제하기 전에 OldPen을 다시 선택해 주는 것이다.

SelectObject(hdc, OldPen);
DeleteObject(MyPen);

OldPen이 DC에 선택되면 자연히 현재 선택되어 있는 MyPen이 선택해제될 것이고 따라서 MyPen을 안전하게 삭제할 수 있다. 이 두 줄은 다음과 같이 한줄로도 작성할 수 있다.

DeleteObject(SelectObject(hdc, OldPen));

SelectObject가 이전 핸들값을 리턴해 주므로 OldPen을 선택함과 동시에 리턴되어져 나오는 MyPen을 삭제하는 것이다. 이때 사용되는 OldPen은 이전에 선택되어 있던 펜을 보존한다기보다는 단순히 MyPen을 선택해제하기 위한 용도로 사용된 것이다. GDI 오브젝트를 만들고 사용하는 일반적인 절차는 다음과 같다.

펜뿐만 아니라 브러시, 폰트 등 모든 GDI 오브젝트는 일반적으로 이런 절차를 거쳐 만들어지고 사용된다. 물론 속도를 위해 약간의 다른 형식을 사용하는 방법이 있기는 하지만 여기서는 원론적인 내용만 이해하도록 하자.




완성된 코드는 이러하며,

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"Menu";

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 = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(hInstance, MAKEINTRESOURCE(IDC_CURSOR1));
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1);
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    100100500500,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000))
  {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }

  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  HBRUSH MyPen;
  HBRUSH OldPen;

  switch (iMessage) 
  {
    case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      MyPen = CreatePen(PS_SOLID, 5, RGB(00255));
      OldPen = (HPEN)SelectObject(hdc, MyPen);
      Rectangle(hdc, 5050300200);
      SelectObject(hdc, OldPen);
      DeleteObject(MyPen);
      EndPaint(hWnd, &ps);
      return 0;

    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}


실행 화면은 이러하다. (PS_SOLID)




case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      MyPen = CreatePen(PS_SOLID, 5, RGB(00255));
      OldPen = (HPEN)SelectObject(hdc, MyPen);
      Rectangle(hdc, 5050300200);
      SelectObject(hdc, OldPen);
      DeleteObject(MyPen);
      EndPaint(hWnd, &ps);
      return 0;


자 이부분, 해석 해보자.


1. BeginPaint() 이제 슬슬 지겹다. DC를 구하느라 쓴다. 또한 WM_PAINT 내부에서만 사용해야 한다.

2. CreatePen() 는 펜은 GDI가 선을 그릴 때 사용하는 오브젝트 이며 DC에 선택된 펜의 속성대로 선이 그어진다. 디폴트 펜은 굵기 1의 검정색 실선이나

펜을 만들어 DC로 전송하면 만들어진 펜대로 선이 그어진다. 다 사용하고 난 후에는 브러쉬와 마찬가지로 DeleteObject()로 펜을 삭제해야 한다.

함수 원형

HPEN CreatePen( int fnPenStyle, int nWidth, COLORREF crColor ); 

첫 번째 인자에 어떤 펜의 형태를 선택할 것인가를 선택할 수 있는데, 디파인 되어있다.

fnPenStyle

그려질 선의 모양을 정의한다. 이 값을 변경하면 실선뿐만 아니라 다양한 형태의 선을 만들 수 있다. 실선, 점선, 일점 쇄선 등등의 선 모양이 있다.

PS_SOLIDPS_DASHPS_DOTPS_DASHDOTPS_DASHDOTDOT

3. Rectangle() 는 지정한 사각형을 그린다.

함수 원형

: BOOL Rectangle(HDC hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);

사각형의 변은 현재 DC에 선택된 펜으로 그려지며 내부는 현재 DC에 선택된 브러쉬로 채워진다. 터보C의 Rectangle() 와는 달리 내부를 채우므로

도스 프로그래밍을 해 본 사람은 주의해야 한다. 내부를 채우지 않으려면 NULL_BRUSH 스톡 오브젝트를 선택한 후 사각형을 그려야 한다.

Ellipse의 예제를 참고하면 더 자세히 알 수 있다.


<순서>










6-1-마. 브러시

브러시는 채워지는 면을 채색하는 용도로 사용된다. 사각형의 안쪽이나 원의 내부 또는 다각형의 내부를 채색할 때 현재 DC에 선택된 브러시가 사용된다. 스톡 브러시에는 회색, 흰색, 검정색 등의 단색 브러시가 있으므로 이 브러시들은 별도로 만들지 않아도 사용할 수 있다. 이 외의 브러시는 직접 만들어 사용해야 한다. 만드는 함수만 다를 뿐 사용 방법은 앞에서 살펴본 펜과 동일하므로 길게 설명하지 않기로 하자. 브러시를 만드는 함수는 다음과 같다.

HBRUSH CreateSolidBrush( COLORREF crColor ); HBRUSH CreateHatchBrush( int fnStyle, COLORREF clrref ); 

첫번째 함수는 단색의 브러시만을 만들 수있으며 브러시의 색상만 인수로 전달해 주면 된다. 두번째 함수는 색상뿐만 아니라 무늬도 같이 지정할 수 있다. 지정할 수 있는 무늬의 종류는 다음과 같다.

설명
HS_BDIAGONAL좌하향 줄무늬
HS_CROSS바둑판 모양
HS_DIACROSS좌하향 및 우하향 줄무늬
HS_FDIAGONAL우하향 줄무늬
HS_HORIZONTAL수평선
HS_VERTICAL수직선

두 함수 모두 리턴하는 값은 만들어진 브러시의 핸들이다. 그럼 이제 브러시를 만드는 함수도 배웠으니 펜과 브러시를 동시에 만들어서 사용해 보도록 하자. GdiObj 프로젝트의 WM_PAINT 메시지를 다음과 같이 수정한다.

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	HBRUSH MyBrush,OldBrush;
	HPEN MyPen, OldPen;
	switch(iMessage) {
	case WM_PAINT:
		hdc=BeginPaint(hWnd,&ps);
		MyBrush=CreateHatchBrush(HS_BDIAGONAL, RGB(255,255,0));
		OldBrush=(HBRUSH)SelectObject(hdc,MyBrush);
		MyPen=CreatePen(PS_SOLID, 5, RGB(0,0,255));
		OldPen=(HPEN)SelectObject(hdc, MyPen);
		Rectangle(hdc,50,50,300,200);
		SelectObject(hdc,OldBrush);
		SelectObject(hdc, OldPen);
		DeleteObject(MyBrush);
		DeleteObject(MyPen);
		EndPaint(hWnd,&ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

파란색의 굵은 MyPen과 노란색의 줄무늬를 가지는 MyBrush를 각각 만들고 DC에 선택해 준 후 사각형을 그렸다. 실행 결과는 다음과 같다.

이런 식으로 펜과 브러시를 바꾸어 가며 그림을 그리면 얼마든지 다양한 모양의 그래픽을 그릴 수 있다. 코드가 워낙 간단하기 때문에 더 이상의 설명이 필요없을 것이다. 이 외에도 비트맵, 패턴 브러시를 만드는 함수들과 브러시에 관련된 좀 더 복잡한 이론들이 있지만 다음 기회에 배우기로 한다.




소스코드는 이러하며,

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"Menu";

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 = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(hInstance, MAKEINTRESOURCE(IDC_CURSOR1));
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1);
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    100100500500,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000))
  {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }

  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  HBRUSH MyBrush;
  HBRUSH OldBrush;
  HBRUSH MyPen;
  HBRUSH OldPen;

  switch (iMessage) 
  {
    case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      MyBrush = CreateHatchBrush(HS_BDIAGONAL, RGB(2552550));
      OldBrush = (HBRUSH)SelectObject(hdc, MyBrush);
      MyPen = CreatePen(PS_SOLID, 5, RGB(00255));
      OldPen = (HPEN)SelectObject(hdc, MyPen);
      Rectangle(hdc, 5050300200);
      SelectObject(hdc, OldBrush);
      SelectObject(hdc, OldPen);
      DeleteObject(MyBrush);
      DeleteObject(MyPen);
      EndPaint(hWnd, &ps);
      return 0;

    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}


실행 화면은 이러하다.



(HS_CROSS)



case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      MyBrush = CreateHatchBrush(HS_BDIAGONAL, RGB(2552550));
      OldBrush = (HBRUSH)SelectObject(hdc, MyBrush);
      MyPen = CreatePen(PS_SOLID, 5, RGB(00255));
      OldPen = (HPEN)SelectObject(hdc, MyPen);
      Rectangle(hdc, 5050300200);
      SelectObject(hdc, OldBrush);
      SelectObject(hdc, OldPen);
      DeleteObject(MyBrush);
      DeleteObject(MyPen);
      EndPaint(hWnd, &ps);
      return 0;


이 부분을 보면, 브러쉬와 펜을 같이 이용하여 사각형을 만들고 있는 모습이다.

앞에서 설명이 되어있으니 넘어가도록 하겠다.







6-2-가. 흑백에서의 그리기 모드

화면에 무엇인가가 그려져 있는 상황에서 그 위에 다른 무엇인가를 출력하면 원래 그려져 있던 그림은 새로 그려지는 그림에 덮여 지워진다. 디스플레이 표면은 2차원적인 평면이므로 새로운 그림이 그려지면 그 아래에 있던 그림이 지워질 수밖에 없으며 아주 당연하게 생각될 것이다. 그러나 이런 당연한 현상도 그리기 모드를 변경하면 달라진다.

그리기 모드란 도형이 그려질 때 원래 그려져 있던 그림과 새로 그려지는 그림과의 관계를 정의하는 것이다. 그리기 모드를 개념적으로 이해하기 위해 아주 단순한 흑백의 그래픽 환경을 가정해 보자. 무엇인가를 화면에 그린다는 것은 비디오 메모리에 그림의 이미지를 기록해 넣는 동작을 말하며 이 때 원래 비디오 메모리에 있던 값과 새로 써지는 값 사이의 관계를 생각해 볼 수 있다. 가장 단순하게는 새로 그려지는 값으로 원래 있던 값을 덮어 버리는 경우가 있으며 비트 논리 연산에 의해 두 값을 적당히 혼합하는 방법들도 있다. 다음 그림은 두 개의 그림을 4가지 비트 연산으로 합쳐본 것이다.

여기서 가로로 놓인 막대가 원래 그려져 있던 그림이며 세로로 놓여져 있는 막대가 새로 그려지는 그림이다. 첫번째의 COPY는 새로 그려지는 그림이 기존 그림을 덮어 버리는 것이다. 두번째의 OR은 두 그림의 대응되는 비트를 OR연산하여 새로 값을 써 넣는다. 즉 두 비트가 모두 1이거나 둘 중 하나라도 1이면 1이 쓰여지며 둘 다 0일 경우에만 0을 써 넣는다. 그래서 마치 새로 그려지는 그림이 셀로판지에 그려져 기존 그림위에 얹히는 것과 같은 효과를 낸다. AND연산을 할 경우 두 그림의 교집합 영역만 그려지며 XOR연산을 할 경우 두 그림 중 겹쳐지는 부분이 반전되는 효과를 가져온다.

흑백에서의 비트 연산은 0(검정색) 또는 1(흰색)만 있기 때문에 이렇게 이해하기 쉽지만 여러 가지 색상을 사용하는 컬러 그래픽 환경에서의 비트 연산은 이보다 훨씬 더 복잡하다. 하지만 개념적으로는 대응되는 비트끼리 흑백에서와 같은 형태의 연산을 하기 때문에 엄격하게 계산해 본다면 결과를 예측해 볼 수도 있다.




6-2-나. 그리기 모드의 종류

윈도우즈에서 사용하는 디폴트 그리기 모드는 R2_COPY 모드이다. 그래서 그려지는 그림이 기존 그림을 덮어 버린다. 그리기 모드를 변경하는 함수와 현재 설정된 그리기 모드를 구하는 함수는 다음과 같다.

int SetROP2( HDC hdc, int fnDrawMode );
int GetROP2( HDC hdc ); 

첫번째 인수는 그리기 모드를 변경(또는 조사)하고자하는 DC의 핸들이며 SetROP2 함수의 두번째 인수에 다음과 같은 그리기 모드값을 넘겨준다.

그리기 모드설명
R2_BLACK항상 검정색이다.
R2_WHITE항상 흰색이다.
R2_NOP아무런 그리기도 하지 않는다.
R2_NOT원래의 그림을 반전시킨다.
R2_COPYPEN원래의 그림을 덮어버리고 새 그림을 그린다.
R2_NOTCOPYPEN새 그림을 반전시켜 그린다.
R2_MERGEPENOR연산으로 두 그림을 합친다.
R2_MASKPENAND연산으로 겹치는 부분만 그린다.
R2_XORPENXOR연산으로 겹치는 부분만 반전시킨다.

이 외에도 몇가지 그리기 모드가 더 있지만 주로 NOT연산자를 중간 중간에 넣은 것들이며 현실적으로 거의 사용되지 않는다. GetROP2 함수는 DC에 설정되어 있는 현재 그리기 모드값을 리턴해 준다.





6-2-다. Ropmode

<ㅖ>그러면 반전모드를 사용하는 예제를 만들어 보자. 마우스로 선을 그리도록 하되 선이 그려지는 중간 과정을 보여주도록 한다. 즉 마우스 버튼을 누른 위치에서부터 시작해서 다시 마우스 버튼을 놓는 자리까지 선을 긋되 버튼을 누른채로 마우스를 움직이면 중간에 그려지는 선의 모양을 보여주도록 하는 것이다. 페인트 브러시 등의 그래픽 프로그램을 써 본 사람이라면 무슨 말인지 쉽게 이해할 수 있을 것이다. 소스는 다음과 같다.

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	static int sx,sy,oldx,oldy;
	int ex,ey;
	static BOOL bNowDraw = FALSE;
	HDC hdc;
	switch(iMessage) {
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_LBUTTONDOWN:
		sx = LOWORD(lParam);
		sy = HIWORD(lParam);
		oldx = sx;
		oldy = sy;
		bNowDraw = TRUE;
		return 0;
	case WM_LBUTTONUP:
		bNowDraw = FALSE;
		hdc=GetDC(hWnd);
		MoveToEx(hdc,sx,sy,NULL);
		LineTo(hdc,oldx,oldy);
		ReleaseDC(hWnd, hdc);
		return 0;
	case WM_MOUSEMOVE:
		if (bNowDraw) {
			hdc=GetDC(hWnd);
			SetROP2(hdc,R2_NOT);
			MoveToEx(hdc,sx,sy,NULL);
			LineTo(hdc,oldx,oldy);
			ex = LOWORD(lParam);
			ey = HIWORD(lParam);
			MoveToEx(hdc,sx,sy,NULL);
			LineTo(hdc,ex,ey);
			oldx = ex;
			oldy = ey;
			ReleaseDC(hWnd,hdc);
		}
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

실행중의 모습을 보이면 다음과 같다.

마우스 버튼을 누른 위치에서부터 놓은 위치까지 선을 긋되 버튼을 놓기전까지 선의 중간 모양을 계속해서 보여준다. WndProc의 선두부터 분석을 해 보도록 하자. 이 프로그램에서 사용하는 변수들은 다음과 같다.

변수설명
sx,sy처음 마우스를 누른 위치를 가진다.
oldx,oldy지워져야 할 선의 끝 좌표를 가진다.
ex,ey다시 그려져야 할 선의 끝 좌표를 가진다.
bNowDraw현재 선을 그리고 있는 중인가에 대한 정보를 가진다.

이 중 ex,ey를 제외한 변수들은 계속 값을 보관해야 하므로 static 기억 부류로 선언되었다.

마우스 버튼을 누르면 bNowDraw를 TRUE로 만들어 선 그리기를 시작하며 버튼이 눌러진 좌표를 시작점, 끝점에 모두 대입해 준다. 즉 처음 선을 그리기 시작할 때는 시작점과 끝점이 같은 상태에서 시작하며 이 상태에서 마우스를 움직이면 끝점이 움직이면서 선이 늘어나게 된다.

마우스가 이동할 때는 먼저 bNowDraw값을 점검해 보고 이 값이 TRUE일 경우만 선을 긋는다. SetROP2 함수를 호출하여 그리고 모드를 R2_NOT로 변경하고 (sx,sy)-(oldx,oldy) 선을 먼저 지운다. 왜 이 좌표에 선을 그으면 선이 지워지는가 하면 이미 그려진 선 위에 반전 모드로 다시 선을 출력하면 선이 지워지는 XOR연산의 특수성때문이다. 이전 선을 지운 후 현 마우스 좌표를 구해 ex, ey에 대입하고 다시 새로운 선을 그린다. 새 선을 그린 후 oldx, oldy에 현재의 끝점을 대입해 주어 다음 마우스 이동시에 이 선을 지울 수 있도록 해준다.

마우스 버튼을 놓으면 확정된 선을 다시 그리고 bNowDraw를 FALSE로 변경하여 그리기를 끝낸다. 이렇게 설명을 읽어도 코드를 직접 분석해 보지 않으면 이해가 잘 되지 않으므로 코드를 자세히 뜯어 보기 바란다. WM_MOUSEMOVE 메시지의 SetROP2 함수 호출문을 삭제해 보면 왜 그리기 모드를 변경해야 하며, 그리기 모드가 왜 필요한가를 알 수 있을 것이다. 다음은 SetROP2 호출문을 삭제한 후의 실행 모습이다.

한번 출력한 선을 지우지 못하기 때문에 마우스가 움직이는 족족 새로운 선이 그려질 것이다. 이것은 분명히 우리가 바라는 결과와는 다른 것이다. 이동중에 계속 선의 모양을 보여주기 위해서는 그리기 모드라는 것이 반드시 필요하다. 선 하나가 그려진 상태에서 다음 위치로 마우스를 이동하는 과정을 그림으로 정리해 보았다.

이런 과정이 처음 마우스 버튼을 누르고부터 시작하여 bNowDraw가 FALSE가 될 때까지 계속 반복되며 마우스 버튼을 놓으면 비로소 종료된다. 주의하여 살펴볼 것은 새 위치에 선이 그려진 후 oldx, oldy가 ex,ey 즉, 현재 그려진 선의 끝 좌표를 대입받음으로써 다음번 마우스가 움직일 때 이 선이 지워지도록 해 준다는 점이다.



원점을 찍고  마우스를 움직였다. 선이 따라왔을 것이다.

위로 다시 움직으면 첫 번째 선은 지워진다.

그 부분에 xor을 하면 선이 지워진다.

기존에 있는 선을 한 번 더 긋는데 그을 떄 xor을 하고 움직이는 것이다. 그러므로 우리 눈에는 선을 지우면서 움직이게 되는 것이다.


XOR 연산은 두 값의 각 자릿수를 비교해, 값이 같으면 0, 다르면 1을 계산한다.

    0101
XOR 0011
  = 0110

C / C++

x = y ^ z;


그러하다.

즉, 선을 다시 그으니 xor 로 1과 1 값이 만나 0 이 나오는 것이다. 

그러므로 지워진다.




완성된 소스는 이러하며,

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"Menu";

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 = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(hInstance, MAKEINTRESOURCE(IDC_CURSOR1));
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1);
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    100100500500,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000))
  {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }

  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  static int sx, sy, oldx, oldy;
  int ex, ey;
  static BOOL bNowDraw = FALSE;
  HDC hdc;

  switch (iMessage) 
  {
    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;

    case WM_LBUTTONDOWN:
      sx = LOWORD(lParam);
      sy = HIWORD(lParam);
      oldx = sx;
      oldy = sy;
      bNowDraw = TRUE;
      return 0;

    case WM_LBUTTONUP:
      bNowDraw = FALSE;
      hdc = GetDC(hWnd);
      MoveToEx(hdc, sx, sy, NULL);
      LineTo(hdc, oldx, oldy);
      ReleaseDC(hWnd, hdc);
      return 0;

    case WM_MOUSEMOVE:
      if (bNowDraw) 
      {
        hdc = GetDC(hWnd);
        SetROP2(hdc, R2_NOT);
        MoveToEx(hdc, sx, sy, NULL);
        LineTo(hdc, oldx, oldy);
        ex = LOWORD(lParam);
        ey = HIWORD(lParam);
        MoveToEx(hdc, sx, sy, NULL);
        LineTo(hdc, ex, ey);
        oldx = ex;
        oldy = ey;
        ReleaseDC(hWnd, hdc);
      }
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}



자 분석해보자.


case WM_LBUTTONDOWN:
      sx = LOWORD(lParam);
      sy = HIWORD(lParam);
      oldx = sx;
      oldy = sy;
      bNowDraw = TRUE;
      return 0;


이 중 ex,ey를 제외한 변수들은 계속 값을 보관해야 하므로 static 기억 부류로 선언되었다.

이렇게 나와 있으므로, 변수 선언은

static int sx, sy, oldx, oldy;
int ex, ey;

이와 같이 해야 하겠다.

sx = LOWORD(lParam);
sy = HIWORD(lParam);

oldx = sx;
oldy = sy;

bNowDraw = TRUE;

1. WM_LBUTTONDOWN : wParam : 키보드와 다른 마우스 버튼의 현재 상태를 나타내는 값이며 다음 플래그들의 조합값이다.

LOWORD(lParam), HIWORD(lParam) : 마우스의 현재 x, y 좌표이다.  이 좌표는 작업 영역의 원점을 기준으로 한 좌표이다.

작업 영역 내부에서 마우스 왼쪽 버튼을 누를 때 이 메시지가 메시지 큐에 붙여진다. 마우스가 캡쳐되어 있으면 캡쳐한 윈도우로 메시지가 전달되며

그렇지 않으면 마우스 커서 아래의 윈도우로 전달된다.

모든 메시지 중에 가장 쉽게 받을 수 있는 메시지 이므로 실습용이나 간단한 테스트용으로 많이 사용된다.

2. sx, sy 에 현재 왼쪽 마우스를 누른 곳의 좌표 값 x, y 를 저장 하고 oldx, oldy 에 백업한다. 그 다음 이제 그리기를 시작할 테니.. 라는 플래그에

값을 Bool 값인 TRUE 로 바꾼다.


case WM_MOUSEMOVE:
      if (bNowDraw) 
      {
        hdc = GetDC(hWnd);
        SetROP2(hdc, R2_NOT);
        MoveToEx(hdc, sx, sy, NULL);
        LineTo(hdc, oldx, oldy);
        ex = LOWORD(lParam);
        ey = HIWORD(lParam);
        MoveToEx(hdc, sx, sy, NULL);
        LineTo(hdc, ex, ey);
        oldx = ex;
        oldy = ey;
        ReleaseDC(hWnd, hdc);
      }


1. GetDC() 로 DC 값을 얻어온다.

2. SetROP2() 로 현재의 드로잉 모드를 설정한다. 그리기 모드란 GDI 함수가 화면에 출력을 내보낼 때 화면에 이미 출력되어 있는 그림과 새로 그려지는 그림과의

관계를 정의하는 것이다. AND, OR, XOR 등 비트간의 이진 연상 방법과 NOT 연산의 조합으로 지정된다.

이 함수명의 ROP는 Rater OPeration의 약자이며 2는 화면 색상과 색상 2개를 피연자로 취한다. 라는 뜻이다.

그리기 모드설명
R2_BLACK항상 검정색이다.
R2_WHITE항상 흰색이다.
R2_NOP아무런 그리기도 하지 않는다.
R2_NOT원래의 그림을 반전시킨다.
R2_COPYPEN원래의 그림을 덮어버리고 새 그림을 그린다.
R2_NOTCOPYPEN새 그림을 반전시켜 그린다.
R2_MERGEPENOR연산으로 두 그림을 합친다.
R2_MASKPENAND연산으로 겹치는 부분만 그린다.
R2_XORPENXOR연산으로 겹치는 부분만 반전시킨다.

두 번째 인자에 이처럼 그리기 모드를 설정해 줄 수 있다. 지금 소스에는 R2_NOT 으로 드로잉 모드가 설정되어 있다라는 것을 볼 수 있는데,

'원래의 그림을 반전시킨다.' 라고 되어있다. 아까 주쌤이 설명하신 말씀이 이 부분이다. 원래라면 계속해서 이 그리는 함수가 실행되는 동안에는 

줄이 Paint 되며 이동 되어야 하는데 이 드로잉 모드를 인자 값으로 넣어주게 된다면 XOR 로써 그 전 상태로 남아 있는 애들을 XOR 시키며 0 값으로 

안 보이게 만들며 이동할 수 있는 것이다.

3. MoveToEx() 는 DC에 현재 좌표가 보관되어 있으며 현재 좌표는 LineTo, PolyLineTo 등의 함수가 선의 시작점으로 사용한다. MoveToEx() 는 현재 좌표를

(x, y)좌표로 이동시켜 준다. 현재 위치는 LineTo, PolyLine 등의 함수에 의해서도 변경된다.

이 함수의 16비트 버전은 MoveTo였으나 이 함수는 Win32 에서 제거되었다. Win32에서 좌표값이 32비트로 확장됨으로써 리턴값 하나로 이전 좌표를

리턴할 수가 없게 됨에 따라 lpPoint로 이전 좌표를 조사하도록 변경되었다. 이 함수에 대한 예제는 LineTo 함수의 예제를 참고하면 쉽게 이해 할수있다


소스 : MoveToEx(hdc, sx, sy, NULL);


즉, 현재 좌표를 이동 시켜 주는 함수이다.

4. LineTo() 는 현재 좌표에서 지정된 좌표까지 선을 그린다. 

함수 원형

: BOOL LineTo(HDC hdc, int nXEnd, int nYEnd);

현재 위치에서 (nXEnd, nYEnd) 끝 점 까지 선을 긋는다. 이 때 끝점은 선에서 제외된다. 선을 긋고 난 후 현재 위치를 끝점으로 옮겨주므로 LineTo함수를

계속 호출하면 선을 이어서 그릴 수 있다. 현재 위치는 DC에 보관되어 있으며 MoveToEx 함수로 변경할 수 있다.








실행 화면 이고,





SetROP2(hdc, R2_NOT); 을 주석 처리하고 난 뒤의 실행화면 이다.








6-3-가. 윈도우즈의 좌표체계

윈도우즈는 그래픽 기반의 GUI운영체제이며 모든 출력은 점 단위로 이루어진다. 픽셀(Picture Element)이란 그래픽을 이루는 최소단위이며 우리말로 번역하면 화소이다. 윈도우의 위치를 지정하거나 문자열을 출력하거나 반드시 출력 위치를 지정하는 좌표가 있어야 하며 좌표는 X,Y 두 축의 오프셋, 즉 원점으로부터의 거리로 구성된다.

예를 들어 화면의 (100,100)에 문자열을 출력한다면 이는 윈도우의 작업 영역 좌상단에서 X축으로나 Y축으로 100픽셀 만큼 떨어진 거리에 문자열이 출력된다는 뜻이며 원점과 출력된 문자열 사이에는 100개의 픽셀이 있다. 결론을 말하자면 윈도우즈의 좌표체계는 픽셀 단위를 사용하며 이는 실수 단위가 아닌 정수 단위로 계산이 이루어지는 디지털 컴퓨터의 특성상 불가피한 일이다.

그러나 때로는 이런 픽셀 단위의 좌표 체계가 프로그램에서 응용하기에 부적합할 수도 있다. 대표적으로 프린터로 출력을 보낼 때가 이에 해당한다. 화면은 72dpi의 낮은 저해상도를 가지지만 프린터는 보편적으로 600dpi의 높은 고해상도를 사용하며 어떤 프린터는 4800dpi까지 지원하기도 한다. 이런 해상도의 차이에 의해 두 장치의 출력 결과가 엄청나게 달라진다.

화면에 반지름 100픽셀의 원을 그렸을 때는 안경알만한데 이 원을 똑같은 반지름 100을 사용하여 프린터로 출력하면 콩알만하게 보일 것이다. 같은 100픽셀이라도 해상도가 낮은 화면에서는 크기가 크고 거칠게 그려지지만 해상도가 높은 프린터에서는 작고 섬세하게 그려지기 때문이다. 쉽게 이해가 가지 않으면 모눈 종이를 생각해 보면 된다. 눈이 큰 모눈 종이의 10칸과 눈이 작은 모눈 종이의 10칸의 크기가 같을 수 없지 않은가?

좌표의 증가 방향도 문제가 있다. 모니터의 좌표 체계는 좌상단이 (0,0)의 좌표가 되어 원점으로 사용되며 X축 좌표는 오른쪽으로 갈수록 증가하고 Y축 좌표는 아래쪽으로 갈수록 증가한다. 즉 모니터의 좌표계는 4/4분면에 위치하며 Y축의 증가 방향이 우리가 학교 다닐 때 배웠던 그래프와 거꾸로 되어 있다.

왜 이렇게 되어 있는지 굳이 이유를 댄다면 사람들은 오랫동안 글을 읽을 때 왼쪽에서 오른쪽으로, 위에서 아래로 읽는 습관을 가지고 있었기 때문에 여기에 맞추다 보니 그렇게 된 것이다. 이런 좌표계가 아주 자연스러워 보이겠지만 경우에 따라서는 4/4분면의 좌표 공간이 적합하지 못한 경우가 있는데 예를 들어 수학적인 그래프를 화면으로 출력하고자 할 경우가 이에 해당한다. 수학 좌표계는 대개 1/4분면에 그려지며 좌하단이 (0,0)의 원점이 되고 X축은 오른쪽으로 갈수록 증가하지만 Y축은 위쪽으로 갈수록 증가한다. 즉 모니터상의 좌표계와 수학 좌표계는 원점과 Y축 증가 방향이 다르다.

모니터에서의 좌표 단위와 좌표 공간이 실생활에서의 그것들과 차이가 있음으로 인해 좌표 체계를 변경해 주어야 할 경우가 있다. 이럴 때는 보통 출력 함수에서 Y축 좌표에 -1을 곱해 주어 증가 방향을 바꾸거나 원점에 일정한 값을 더해 평행 이동시켜 인위적으로 옮겨주는 방법을 사용했다. 하지만 윈도우즈는 이런 차이점을 맵핑 모드라는 매커니즘을 통해 쉽게 해결할 수 있도록 해 준다. 즉 좌표 체계를 바꾸는 방법을 운영 체제가 제공해 줌으로써 프로그래머의 부담을 덜어 주고 있는 것이다. 맵핑 모드를 꼭 변경해 주어야 할 경우는 그다지 흔하지 않지만 윈도우즈 시스템의 주요한 부분이므로 잘 알아두도록 하자.





(주 쌤)

{

아래로 내려오면서 증가한다. 윈도우즈 대부분이 이렇다.

}



6-3-나. 맵핑 모드

맵핑 모드(mapping mode)란 주어진 좌표가 화면상의 실제 어디에 해당하는지를 결정하는 방법을 말한다. 윈도우즈에서 사용하는 좌표는 논리 좌표와 물리 좌표 두가지가 있다.

논리 좌표 : 윈도우즈의 내부에서 사용되는 좌표를 말한다. TextOut (100,100,...)에서 지정한 (100,100)이 곧 논리 좌표이며 논리 좌표의 실제 위치는 경우에 따라 달라진다. 그래픽 함수들이 사용하는 모든 좌표는 논리 좌표이며 좀 더 현실적으로 얘기한다면 DC핸들을 인수로 받아들이는 모든 함수는 논리 좌표를 사용한다.

물리 좌표 : 실제 화면에 출력되는 좌표이며 픽셀 단위를 사용한다. 물리적인 모니터의 픽셀이 단위이므로 물리 좌표 (100,100)은 그 위치가 정해져 있다. 윈도우를 관리하는 함수(또는 메시지) 에서 사용하는 좌표는 물리 좌표이다.

이 두가지 좌표의 관계를 정의하는 것이 맵핑 모드이다. 맵핑(Mapping)이란 용어는 두가지 사물의 일대일 대응 관계를 정의하는 공식 내지는 함수라고 할 수 있으며 윈도우즈에서의 맵핑 모드는 논리 좌표를 물리 좌표로 변환하는 방법을 의미한다. 어떠한 맵핑 모드가 사용되는가에 따라 (100,100)의 논리 좌표는 물리적으로 (10,10)이 될 수도 있고 (20,30)이 될 수도 있다.

그러나 우리는 이때까지 프로그래밍을 하면서 이런 대응관계를 전혀 느끼지 못했다. TextOut(100,100,...)하면 화면에 출력되는 위치는 어김없이 X축으로 100픽셀만큼 떨어지고 Y축으로 100픽셀만큼 떨어진 위치였다. 왜 그런가하면 윈도우즈가 디폴트로 사용하는 맵핑 모드에서는 논리 좌표와 물리 좌표가 일치되어 있기 때문에 어떠한 변환도 일어나지 않았기 때문이다. 물론 맵핑 모드를 변경하면 화면에 출력되는 실제좌표는 달라진다. 윈도우즈에서 사용되는 맵핑 모드에는 다음과 같은 것들이 있다.

맵핑 모드단위X축 증가Y축 증가
MM_TEXT픽셀오른쪽아래쪽
MM_LOMETRIC0.1mm오른쪽윗쪽
MM_HIMETRIC0.01mm오른쪽윗쪽
MM_LOENGLISH0.01인치오른쪽윗쪽
MM_HIENGLISH0.001인치오른쪽윗쪽
MM_TWIPS1/1440인치오른쪽윗쪽
MM_ISOTROPIC가변가변가변
MM_ANISOTROPIC가변가변가변

디폴트 맵핑 모드는 픽셀 단위인 MM_TEXT이며 나머지 맵핑 모드는 밀리미터나 인치 등의 논리적인 단위를 사용한다. 끝에 있는 두 개의 맵핑 모드는 약간 특수한 맵핑 모드이며 별도로 연구해 볼 것이다. 맵핑 모드를 변경할 때는 SetMapMode 함수를 사용하며 현재 설정된 맵핑 모드를 알고 싶을 때는 GetMapMode함수를 사용한다.

int SetMapMode( HDC hdc, int fnMapMode );
int GetMapMode( HDC hdc ); 

첫번째 인수는 DC의 핸들이며 SetMapMode의 인수 fnMapMode로 변경하고자 하는 맵핑 모드를 지정해 주면 된다. GetMapMode 함수는 DC에 설정되어 있는 맵핑 모드를 조사해 준다.


(주쌤)

{

거의 안쓴다. 오락만들때 쓰인다.

]

나중에 써보자.




6-3-다. 윈도우와 뷰포트

맵핑 모드를 제대로 이해하려면 몇가지 용어에 대해 이해해야 한다. 우선 윈도우와 뷰포트에 대해 알아야 하며 원점과 확장에 대해서도 알아야 한다. 윈도우(Window)는 논리 좌표가 사용되는 표면을 말하며 그래픽 출력 함수는 윈도우에 그래픽을 출력한다. 뷰포트(Viewport)는 물리 좌표가 사용되는 영역을 말하며 실제로 사용자의 눈에 보이는 좌표 영역이다. TextOut(100,100,...) 함수는 윈도우 영역의 (100,100)에 문자열을 출력하며 이렇게 출력된 문자열은 뷰포트를 통해 우리 눈에 보이게 된다. 물론 뷰포트에서의 좌표는 맵핑 모드에 따라 달라진다. 요컨데 윈도우는 논리 좌표를 사용하는 영역이며 뷰포트는 물리 좌표를 사용하는 영역을 말한다.

원점(Origin)이란 좌표의 기준이 되는 점, 즉 (0,0)의 좌표를 말한다. 디폴트로 원점은 맵핑 모드에 상관없이 화면의 좌상단에 위치하고 있다. 그러나 수학좌표계를 표현하고자 할 경우는 원점이 화면의 좌상단에 있는 것보다 중앙에 있는 것이 더 적합하다. 그래서 필요에 따라 원점을 변경시킬 수 있도록 다음 두 함수를 제공한다.

BOOL SetViewportOrgEx( HDC hdc, int X, int Y, LPPOINT lpPoint );
BOOL SetWindowOrgEx( HDC hdc, int X, int Y, LPPOINT lpPoint ); 

각각 뷰포트와 윈도우의 원점을 인수로 지정한 좌표 (X,Y)로 이동시킨다. 4번째 인수 lpPoint는 변경하기 전의 원래 원점값을 돌려받기 위해 사용하는데 원래의 원점값이 필요없을 경우는 NULL을 사용하면 된다. 두 원점을 동시에 이동시킬 필요는 없으며 둘 중 하나의 원점만을 옮겨도 원하는 결과를 얻을 수 있지만 보통 뷰포트의 원점을 이동시키는 것이 더 편리하다. 왜냐하면 윈도우의 원점은 논리 단위로 지정되며 뷰포트의 원점은 픽셀 단위로 표현되기 때문이다.

아무래도 논리 단위보다는 픽셀 단위가 더 사용하기는 쉽다. 원점을 이동시켜 수학좌표계와 완전히 동일한 좌표계를 가지도록 만들어 보고 여기에 부드러운 사인 곡선을 그려보도록 하자. Sine.cpp의 소스는 다음과 같다.

#include <math.h>
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	double f;
	int y;
	switch(iMessage) {
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_PAINT:
		hdc=BeginPaint(hWnd,&ps);
		SetMapMode(hdc,MM_LOENGLISH);
		SetViewportOrgEx(hdc,200,150,NULL);
		MoveToEx(hdc, -2000, 0,NULL);
		LineTo(hdc, 2000, 0);
		MoveToEx(hdc,0,-2000,NULL);
		LineTo(hdc,0,2000);
		for (f=-500;f<1000;f++) {
			y=(int)(sin(f*3.14/180)*100);
			SetPixel(hdc, (int)f, y,RGB(0,0,0));
		}
		EndPaint(hWnd, &ps);
		return 0;
	default:
		return(DefWindowProc(hWnd,iMessage,wParam,lParam));
	}
}

사인값을 구하는 수학 함수를 사용하므로 math.h를 포함시켰다. 두개의 변수 f와 y를 가지고 f를 -500도에서 1000도까지 변화시키며 y좌표를 구한 후 이 좌표에 점을 찍어 사인 곡선을 만든다. 출력 결과는 다음과 같다.

모든 일은 WM_PAINT 안에서 일어나므로 WM_PAINT 메시지를 분석해 보면 된다. 우선 WM_PAINT의 선두에서 맵핑 모드를 MM_LOENGLISH로 변경하였다. 그래서 그래픽 출력 함수에서 지정하는 단위는 모두 0.01인치가 되며 Y축이 위로 증가하므로 수학 좌표계와 같은 증가방향을 가지게 된다. 바로 아래에서 뷰포트의 원점을 (200,150)으로 변경하였다. 그래서 우리가 보는 화면은 (200,150)이 원점이 되는 수학좌표계와 동일한 공간이 된다. 맵핑 모드를 변경한 후 선을 두 개 그어 수학 좌표계의 축을 표시하도록 하였다. 좌표값에 음수가 사용되는 것이 이상하게 보일지도 모르겠지만 맵핑 모드와 원점이 변경되면 공간 자체가 실수 공간이기 때문에 음수도 당연히 사용될 수 있다.

축을 그린 후 사인 함수의 입력값인 f를 -500에서 1000까지 루프를 돌며 각 값의 사인값을 구해 점을 찍는다. sin 함수가 받아들이는 값이 라디안 값이므로 이 값을 각도로 바꾸기 위해 3.14를 곱하고 180으로 나누었다. 간단한 수학 공식이므로 혹시 이 식이 이해가 되지 않으면 책꽃이에 꽂아둔 정석이나 해법등의 수학 참고서를 보기 바란다.

아뭏든 우리는 이 예제를 통해 수학 좌표계를 만들었고 사인 곡선을 그렸다. 그럼 이제 이 예제를 조금씩 수정해 가면서 결과가 어떻게 달라지는지 보자. 먼저 맵핑 모드를 MM_HIENGLISH로 변경해 보자. 그러면 단위가 0.001인치로 더 작아지며 따라서 그려지는 그림은 훨씬 더 작게 그려진다.

MM_TEXT나 MM_LOMETRIC 등의 맵핑 모드로도 바꾸어 보면 각 맵핑모드별로 그림의 크기가 달라진다는 것을 알 수 있다. 이번에는 원점을 변경해 보아라. SetViewportOrg 함수의 인수를 변경하면 원점이 변경한 곳으로 가 있게 될 것이다. 이때 원점이 변경되었다고 해서 축의 좌표가 달라지거나 점의 좌표가 달라져야할 필요는 전혀없다. 왜냐하면 모든 그래픽 함수들은 변경된 원점의 영향을 받기 때문이다. sin 함수를 cos난 tan 함수로도 변경해 보면 삼각함수 그래프들을 볼 수 있을 것이다.

 

맵핑 모드의 개념에 대해 알아 보았는데 지금까지 배운 내용들에 비해서는 다소 어렵다는 생각이 들 것이다. 오래전에 잊어버린 수학 얘기도 나오고 새로운 용어도 여러 개 등장해서 혼란스럽지나 않았는지 모르겠다. 아마 대부분의 사람이 "대충은 알겠는데... 글쎄" 하는 반응을 보일 것 같은데 그 정도면 충분하다. 다행히 맵핑 모드는 자주 사용되지 않으므로 당장 몰라도 되며 그다지 상세하게 알 필요도 없으므로 대충 읽어보고 다음에 필요할 때 더 공부해 보기 바란다.



새로히 등장한 함수는 세개다.

BOOL SetViewportOrgEx( HDC hdc, int X, int Y, LPPOINT lpPoint );
BOOL SetWindowOrgEx( HDC hdc, int X, int Y, LPPOINT lpPoint ); 

이것과

int SetMapMode(HDC hdc, int fnMapMode);


SetMapMode() 는 조금의 부가적인 설명이 뒷받침되어야 한다. 

윈도우즈에서 사용되는 좌표는 논리 좌표와 물리좌표 두가지가 존재한다.

※ 논리 좌표 : 윈도우즈 내부에서 사용되는 좌표를 말한다. TextOut(100, 100,...) 에서 지정한 (100, 100)이 곧 논리 좌표이며 논리 좌표의 실제 위치는

경우에 따라 달라진다. 그래픽 함수들이 사용하는 모든 좌표는 논리좌표이며 좀 더 현실적으로 얘기한다면 DC핸들을 인수로 받아들이는

모든 함수는 논리 좌표를 사용한다

※ 물리 좌표 :





완성된 소스는 이러하며,

#include <windows.h>
#include <math.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"Menu";

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 = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(hInstance, MAKEINTRESOURCE(IDC_CURSOR1));
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1);
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    100100500500,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000))
  {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }

  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  double f;
  int y;

  switch (iMessage) 
  {
    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;

    case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);

      SetMapMode(hdc, MM_LOENGLISH);
      SetViewportOrgEx(hdc, 200150, NULL);

      MoveToEx(hdc, -20000, NULL);
      LineTo(hdc, 20000);
      MoveToEx(hdc, 0, -2000, NULL);
      LineTo(hdc, 02000);

      for (f = -500; f<1000; f++) 
      {
        y = (int)(sin(f*3.14 / 180) * 100);
        SetPixel(hdc, (int)f, y, RGB(000));
      }

      EndPaint(hWnd, &ps);
      return 0;
  default:
    return(DefWindowProc(hWnd, iMessage, wParam, lParam));
  }
}



실행 화면은 이렇다.











6-3-라. 가변 비율

윈도우즈에 포함된 시계 프로그램을 보면 윈도우의 크기에 비해 항상 일정한 비율을 유지하며 윈도우의 크기가 변경되면 시계의 크기도 같이 변경되어 항상 윈도우의 일정 영역을 차지한다.

  

이런 프로그램은 도대체 어떤 방법으로 작업 영역에 그려지는 크기를 일정하게 유지하는 것일까? 윈도우의 크기가 변할 때마다 그림을 그리는 코드를 바꾸거나 좌표를 일정 비율로 곱해주는 것이 아닐까 하고 추측되겠지만 그렇지는 않다. 그림을 그리는 코드나 좌표는 항상 일정하지만 윈도우 확장을 변경함으로써 전체 좌표계의 범위를 조정해 주는 방법을 사용한다.

윈도우 확장을 조정할 수 있는 맵핑 모드에는 MM_ISOTROPIC과 MM_ANISOTROPIC 두 가지가 있으며 나머지 맵핑 모드에서는 윈도우 확장을 변경할 수 없다. 시계 프로그램과 똑같은 원리로 항상 일정한 크기를 유지하는 프로그램을 Aniso.dsw라는 이름으로 만들고 WndProc에 다음 코드를 작성해 보자.

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	POINT ar[]={25,10,125,10,140,30,10,30,25,10};
	HBRUSH BrR, BrB, BrY, OldBr;
	RECT rect;

	switch(iMessage) {
	case WM_PAINT:
		hdc=BeginPaint(hWnd, &ps);
		BrR=CreateSolidBrush(RGB(255,0,0));
		BrB=CreateSolidBrush(RGB(0,0,255));
		BrY=CreateSolidBrush(RGB(255,255,0));

		SetMapMode(hdc,MM_ANISOTROPIC);
		SetWindowExtEx(hdc,160,100,NULL);
		GetClientRect(hWnd, &rect);
		SetViewportExtEx(hdc,rect.right,rect.bottom,NULL);

		OldBr=(HBRUSH)SelectObject(hdc,BrR);
		Rectangle(hdc,20,30,130,90);
		SelectObject(hdc,BrB);
		Polygon(hdc,ar,5);

		SelectObject(hdc,BrY);
		Rectangle(hdc,30,40,60,70);
		Rectangle(hdc,90,40,120,70);
		Ellipse(hdc,135,5,155,25);

		SelectObject(hdc,OldBr);
		DeleteObject(BrR);
		DeleteObject(BrB);
		DeleteObject(BrY);
		EndPaint(hWnd, &ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

그리고 프로그램을 컴파일한 후 실행해 보면 이 프로그램도 과연 시계 프로그램과 마찬가지로 윈도우 크기를 변경함에 따라 그림의 크기도 변경된다. 프로그램 실행중의 모습은 다음과 같다.

 

이렇게  정말 작게 만들어도 그림의 크기는 윈도우의 크기에 비해 항상 일정하다. 코드를 살펴보면 맵핑 모드는 MM_ANISOTROPIC으로 되어 있으며 다음 두 함수를 사용하여 윈도우와 뷰포트의 확장을 설정한다.

BOOL SetWindowExtEx( HDC hdc, int nXExtent, int nYExtent, LPSIZE lpSize );
BOOL SetViewportExtEx( HDC hdc, int nXExtent, int nYExtent, LPSIZE lpSize ); 

윈도우 확장은 논리적인 좌표 범위를 지정하며 이 프로그램의 경우 (160,100)으로 설정하였다. 그래서 그림을 그리는 함수들은 모두 (160,100) 안쪽의 좌표를 사용하며 (160,100)이 우하단의 좌표가 된다. 뷰포트의 확장은 그림이 화면으로 출력되는 뷰포트의 좌표 범위를 말한다. 이 프로그램에서는 뷰포트 확장을 작업 영역의 우하단 점까지로 확장한다. 뷰포트의 우하단이 윈도우의 우하단과 맵핑되므로 윈도우의 (160,100)은 항상 뷰포트의 우하단과 대응되며 그 중간 점들은 적당한 비율의 좌표와 맵핑되어 윈도우의 크기에 상관없이 항상 일정한 비율의 크기를 유지하는 것이다.

MM_ANISOTROPIC 맵핑 모드는 이런식으로 X,Y 어느 방향으로나 확장을 임의 설정할 수 있는 맵핑 모드이다. 이에 비해 MM_ISOTROPIC은 확장을 마음대로 변경할 수 있도록 해 주기는 하되 항상 가로, 세로 종횡비를 일정하게 유지시켜준다. 그래서 확장에 따라 그림이 작아지거나 커지기는 하지만 찌그러지지는 않도록 해 준다. 위 코드에서 맵핑 모드를 MM_ISOTROPIC으로 변경해 보면 무슨 말인지 쉽게 이해할 것이다. 윈도우즈의 시계 프로그램도 사실은 MM_ISOTROPIC 맵핑 모드를 사용하기 때문에 시계의 크기가 변할지라도 항상 원모양을 유지하며 타원이 되지는 않는다.

여기까지 그리기 모드, 맵핑 모드, 윈도우와 뷰포트에 대해 알아보았고 관련 함수들에 대해서도 소개했다. 그런데 이 즈음에서 Win32 API 함수들의 이름을 잘 관찰해 보면 Set~ 함수가 있으면 Get~ 함수가 반드시 존재한다는 법칙을 발견할 수 있을 것이다. 즉 SetMapMode 함수가 있으면 GetMapMode 함수가 있고 SetROP2 함수가 있으면 GetROP2 함수도 존재한다. 물론 각 함수의 쌍은 같은 대상을 설정/조사하는 함수쌍인데 대부분의 API 함수들은 Get/Set 함수가 쌍으로 존재한다.



이건 지금은 넘어간다.




6-4-가. bitmap.dsw

점, 선, 원 등을 그리는 작도 함수를 사용하면 어떤 그래픽이든지 그릴 수 있다. 사실 극단적으로 말하자면 점을 찍는 SetPixel만 사용해도 못그릴 그림이 없는 셈이다. 그러나 작도로 그리는 그래픽은 그 속도는 둘째로 치더라도 복잡한 그림을 나타내기에는 무리가 많다. 특히 사진같은 정밀한 그래픽을 프로그램 실행중에 일일이 그린다는 것은 굉장히 비합리적이다. 다음과 같은 그림을 작도 함수로 직접 그린다고 생각해 보라. 과연 가능하겠는가?

그래서 복잡한 그림을 출력해야 할 경우는 미리 그려진 비트맵을 사용한다. 페인팅 툴을 사용해 출력하고자 하는 그림을 미리 그려 두거나 아니면 스케너와 같은 장비로 그림을 입력받아 두고 프로그램에서는 이 그림을 사용하는 것이 일반적이다. 복잡한 그림을 출력하는 용도 외에도 비트맵은 넓은 활용 범위를 가지고 있다. 화면의 일정영역을 복사해서 옮기기도 하고 잠시 보관해 두기도 하며 화려한 애니메이션에 활용되기도 한다.

그럼 일단 비트맵을 읽어와 화면으로 출력하는 프로그램을 한번 만들어 보자. 리소스를 임포트하는 생소한 실습을 해야 하므로 단계를 따라 프로젝트를 만들어 보자.

1Bitmap.dsw 프로젝트를 만들고 ApiStart.txt를 복사하여 Bitmap.cpp를 만든 후 프로젝트에 포함시킨다. 일단 Bitmap.cpp의 lpszClass만 "Bitmap"으로 변경시키고 컴파일해 보자. 그러면 First.exe와 같은 실행 파일이 만들어질 것이다.

2비트맵 출력에 사용될 실습용 비트맵을 준비한다. 아무 비트맵이나 상관없지만 여기서는 윈도우즈 3.1에 포함되어 있는 Arches.bmp 파일을 사용한다. 사정에 따라 아무 비트맵이나 사용해도 상관없다. 단 그래픽 카드에는 맞아야 하므로 16색상 비트맵을 준비하는 것이 좋을 것 같다. 준비한 비트맵 파일을 Bitmap 프로젝트 디렉토리에 복사해 둔다.

3비트맵 리소스를 만든다. 비트맵 편집기를 사용할 경우 직접 비트맵을 만들 수도 있지만 여기서는 이미 만들어져 있는 비트맵을 임포트(Import)해와 사용할 것이다. 임포트란 남이 만들어 놓은 리소스를 영원히 빌려오는 동작이다. Insert/Resource를 선택하면 다음 대화상자가 나타난다.

이 대화상자에서 리소스 타입을 선택하면 새로운 리소스를 만드는 것이므로 그렇게 하지말고 Import 버튼을 누른다. 그러면 다음과 같은 임포트 대화상자가 나타날 것이다.

이 대화상자에서 미리 준비해 두었던 비트맵 파일을 선택한다. 그러면 이 비트맵을 IDB_BITMAP1이라는 ID로 임포트해오며 Script1.rc라는 리소스 스크립트 파일을 만들어 줄 것이다.

이 스크립트 파일을 Bitmap.rc로 저장하고 Project/Add To Project/Files메뉴 항목을 사용하여 프로젝트에 포함시킨다. 비트맵 파일을 임포트하고 리소스 파일을 만든 것이다. 만약 직접 비트맵을 만드려면 Insert/Resource 메뉴에서 Bitmap을 선택한 후 New 버튼을 눌러 새 비트맵을 그리면 된다.

4비트맵 리소스를 읽어와 출력하도록 코드를 수정한다.

#include "resource.h"
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc,MemDC;
	PAINTSTRUCT ps;
	HBITMAP MyBitmap, OldBitmap;
	switch(iMessage) {
	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);
		MemDC=CreateCompatibleDC(hdc);
		MyBitmap=LoadBitmap(g_hInst, MAKEINTRESOURCE(IDB_BITMAP1));
		OldBitmap=(HBITMAP)SelectObject(MemDC, MyBitmap);
		BitBlt(hdc, 0,0,123,160,MemDC,0,0,SRCCOPY);
		SelectObject(MemDC,OldBitmap);
		DeleteObject(MyBitmap);
		DeleteDC(MemDC);
		EndPaint(hWnd, &ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

리소스를 만들었으므로 resource.h를 포함시켜 주어야 한다. 일단 실행 결과를 보이면 다음과 같다.

(0,0)위치에 비트맵이 출력되었다. 코드를 보면 다소 생소한 함수들이 사용되고 있는데 이 코드의 의미는 잠시 후에 알아보자.






메뉴와 같은 원리로 비트맵도 숫자로 디파인 되어있다.

101 하면 현재 그림이 뜬다. 라는 소리이다.



#include "resource.h"
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc,MemDC;
	PAINTSTRUCT ps;
	HBITMAP MyBitmap, OldBitmap;
	switch(iMessage) {
	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);
		MemDC=CreateCompatibleDC(hdc);
		MyBitmap=LoadBitmap(g_hInst, MAKEINTRESOURCE(IDB_BITMAP1));
		OldBitmap=(HBITMAP)SelectObject(MemDC, MyBitmap);
		BitBlt(hdc, 0,0,123,160,MemDC,0,0,SRCCOPY);
		SelectObject(MemDC,OldBitmap);
		DeleteObject(MyBitmap);
		DeleteDC(MemDC);
		EndPaint(hWnd, &ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}


(주쌤)

{

LoadBitmap() 가 메모리에 비트맵을 올리고

BitBlt() 로 화면에 표시하는 함수 이다. 용량이 크기 때문에 띄우는데 오랜 시간이 걸린다.

이 인자 네개는 0, 0 memDc를 찍어줄 시작 좌표이고 그 뒤의 0, 0 가로, 세로 크기 이다. 우리가 숫자 바꿔보자.


메모리 올리고 DC 화면에 출력하고 다시 딜리트 시키고. 느리다.

올리는 작업을 WM_PAINT에 보내고 딜리트 작업을 디스트로이에 보낸다.

즉, 종료할때까지 해제를 안시킨다. 즉, 프로그램이 실행될때 메모리는 잡아먹지만 속도를 얻는다.

}




완성된 코드는 이러하며,

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"First";

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 = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = NULL;
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000)) 
  {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }

  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  HDC hdc, MemDC;
  PAINTSTRUCT ps;
  HBITMAP MyBitmap, OldBitmap;
  
  switch (iMessage) 
  {
    case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      MemDC = CreateCompatibleDC(hdc);
      MyBitmap = LoadBitmap(g_hInst, MAKEINTRESOURCE(IDB_BITMAP1));
      OldBitmap = (HBITMAP)SelectObject(MemDC, MyBitmap);
      BitBlt(hdc, 0010001000, MemDC, 00, SRCCOPY);
      SelectObject(MemDC, OldBitmap);
      DeleteObject(MyBitmap);
      DeleteDC(MemDC);
      EndPaint(hWnd, &ps);
      return 0;

    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}


실행 화면은 이렇다.







 case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      MemDC = CreateCompatibleDC(hdc);
      MyBitmap = LoadBitmap(g_hInst, MAKEINTRESOURCE(IDB_BITMAP1));
      OldBitmap = (HBITMAP)SelectObject(MemDC, MyBitmap);
      BitBlt(hdc, 0010001000, MemDC, 00, SRCCOPY);
      SelectObject(MemDC, OldBitmap);
      DeleteObject(MyBitmap);
      DeleteDC(MemDC);
      EndPaint(hWnd, &ps);
      return 0;


이 부분을 해석해 보자.


1. DC를 BeginPaint 로 얻어온다.

2. CreateCompatibleDC() 로 다른 디바이스 컨텍스트와 호환이 가능한 메모리 디바이스 컨텍스트를 만든다. 이것은 메모리에 이미지를 미리 읽어 들이기

위해서 사용할 수 있다.

3. LoadBitmap() 로 비트맵 리소스를 읽어 온다. 리소스에 정의된 비트맵은 장치에 독립적인 DIB 포맷으로 저장되어 있으나 이 함수로 읽혀질 때 현재

화면 모드와 호환되는 DDB로 변환된다.

즉, 화면에 호환되는 DDB로 변환하기 위해 이 함수를 꼭 사용해줘야 화면에 출력이 가능 해진다.

이것도 브러쉬와 펜 이용 방법도 동일하게 DeleteObject() 로 삭제를 해주어야 한다.

BitBlt()로 화면에 출력할 수 있다.

4. BitBlt() 로 지정된 디바이스 컨텍스트에서 비트맵을 복사한다. 즉, 메모리에 저장되있는 비트맵을 복사해 와서 화면에 뿌려준다.

하나의 DC에 있는 비트맵을 다른 DC로 복사하는 비트맵 전송함수 이다. 이때 두 DC는 호환되어야 하나 만약 색상 포맷이 호환되지 않을 경우

BitBlt()는 복사원의 색상 포맷을 복사처의 포맷에 맞게 변경한다. 비트맵을 화면에 출력하기 위해서는 우선

CreateCompatibleDc()를 사용하여 메모리 DC를 만들어야 하며 SelectObject() 를 사용하여 메모리 DC에 출력하고자 하는 비트맵을 선택한 후

BitBlt()로 실제 화면 DC에 전송한다. 이 때 비트맵은 원본 그대로 복사가 되지만 ROP 코드에 따라 배경과 함께 논리 연산되어 변형될 수는 있다.

복사원의 비트맵은 복사처의 맵핑모드에 따라 크기가 커지거나 작아지기도 한다. 모든 장치가 BitBlt()를 지원하는 것은 아니므로 GetDeviceCaps를

사용하여 BitBlt() 를 쓸 수 있는 장치인가를 확인해 보아야 한다.


BitBlt(그림 x 좌표, 그림 y 좌표, 그림 넓이, 그림 높이, 그림 그려진 메모리 DC, 그림 시작 x 좌표, 그림 시작 y 좌표, 스타일);





 case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      MemDC = CreateCompatibleDC(hdc);
      MyBitmap = LoadBitmap(g_hInst, MAKEINTRESOURCE(IDB_BITMAP1));
      OldBitmap = (HBITMAP)SelectObject(MemDC, MyBitmap);
      BitBlt(hdc, 0010001000, MemDC, 00, SRCCOPY);
      SelectObject(MemDC, OldBitmap);
      DeleteObject(MyBitmap);
      DeleteDC(MemDC);
      EndPaint(hWnd, &ps);
      return 0;


즉, 다시 해석하면


1. DC를 얻어온다.

2. BitBlt() 로 화면에 비트맵을 복사해서 출력하기 위해 CreateCompatibleDC() 로 호환가능한 메모리 DC를 하나 더 생성해 준다.

3. LoadBitmap() 로 화면에 불러올 비트맵을 지정하여 DIB 포맷에서 현재 화면에 호환되는 DDB 포맷으로 변환 시켜준다.

4. SelectObject() 로 현재 MemDC에 있는 DC 메모리에 저장된 DDB 포맷으로 변환된 비트맵을 메모리에 그림 그린다.

5. BitBlt() 로 MemDC 에 그려진 비트맵을 복사해 와서 화면에 뿌려준다.

6.



(주쌤)

{

현재 우리는 캡스톤 프로젝트롤 하는 중이다.

}


6-4-나. 메모리 DC

윈도우즈는 비트맵을 곧바로 화면 DC로 출력하는 함수는 제공하지 않는다. 비트맵을 출력하는 함수라면 아마 다음과 같은 형태를 상상할 수 있을 것이다.

OutBitmap(hdc,x,y,비트맵 이름);

이런 함수가 있다면 무척 편리하게 비트맵을 출력할 수 있겠지만 안타깝게도 이런 함수는 없다. 왜냐하면 비트맵은 크기가 큰 데이터 덩어리이며 따라서 출력 속도가 형편없이 느리기 때문에 화면으로 곧바로 출력할 경우 여러 가지 꼴사나운 현상이 발생할 수 있기 때문이다. 게다가 다른 좋은 대안이 있기 때문에 직접 화면으로 출력하는 방법은 쓰지 않는다. 그 대안이 바로 여기서 논하고자 하는 메모리 DC이다.

메모리 DC란 화면 DC와 동일한 특성을 가지며 그 내부에 출력 표면을 가진 메모리 영역이다. 메모리에 있기는 하지만 화면 DC에서 사용할 수 있는 모든 출력을 메모리 DC에서도 할 수 있다. 선, 면, 원 등의 작도 함수는 물론 화면 DC에서는 불가능한 것까지도 가능하다. 그래서 메모리 DC에 먼저 그림을 그린 후 사용자 눈에 그려지는 과정은 보여주지 않고 메모리 DC에서 작업을 완료한 후 그 결과만 화면으로 고속 복사하는 방법을 많이 사용한다.

사용자 눈에 화면이 그려지는 과정을 보여주는 것은 그리 깔끔한 모양은 아니다. 여기서 우리가 하고자 하는 비트맵 출력을 위해서도 반드시 메모리 DC를 사용해야 한다. 비트맵도 일종의 GDI 오브젝트이지만 화면 DC에서는 선택할 수 없으며 메모리 DC만이 비트맵을 선택할 수 있다. 그래서 메모리 DC에서 먼저 비트맵을 읽어온 후 화면 DC로 복사하는 것이다. 메모리 DC를 만들 때는 다음 함수가 사용된다.

HDC CreateCompatibleDC( HDC hdc ); 

인수로 화면 DC의 핸들을 주면 이 화면 DC와 동일한 특성을 가지는 DC를 메모리에 만들어 그 핸들값을 리턴해 준다. 동일한 특성을 가진다(=호환된다)는 말은 사용하는 색상수, 색상면(plane)이 같다는 뜻이다. 호환되지 않는 DC끼리는 정보를 공유할 수 없기 때문에 화면 DC와 호환되는 메모리 DC를 만들어야 한다.

메모리 DC를 만든 후에는 비트맵을 읽어온 후 이 비트맵을 메모리 DC에 선택해 준다. 선택하는 방법은 여타의 GDI 오브젝트와 마찬가지로 SelectObject 함수를 사용하며 비트맵을 읽어올 때는 LoadBitmap 함수를 사용한다.

HBITMAP LoadBitmap( HINSTANCE hInstance, LPCTSTR lpBitmapName ); 

첫번째 인수는 비트맵 리소스를 가진 인스턴스의 핸들이며 두번째 인수는 비트맵 리소스의 이름이다. 읽어온 비트맵을 SelectObject 함수로 메모리 DC에 선택하면 메모리 DC의 표면에는 리소스에서 읽어온 비트맵이 그려져 있을 것이다. 이제 남은 일은 메모리 DC에 그려진 비트맵을 화면으로 복사하기만 하면 된다.


(주쌤)

{

메모리 속도가 굉장히 빠르므로 화면에 표시되는 속도는 굉장히 느린다ㅔ

메모리에 써지니까 굉장히 빠른거다,.

한꺼번에 메모리를 푸는 것이다.

그래픽이 개입하면 무조건 느리므로 이런식으로 조금이라도 느린것을 상쇄시킬수 있다라는 것이다.

일종의 버퍼링이라고 한다 이것을.

메모리에 비트맵을 복사해놓고 복사해오는 것이다.

}


(주쌤)

{

진짜 복사한다라는 말이 아니고,

가상의 공간을 현재 화면 크기에 맞춰 생성한다라는 뜻이다.

}


(주쌤)

{

로드 비트맵을 호출해야 비트맵이 진짜로 적재가 된다.

}

 앞에 분석에서 확인 되었다.



6-4-다. BitBlt

BitBlt 함수는 DC간의 영역끼리 고속 복사를 수행한다. 메모리 DC의 표면에 그려져 있는 비트맵을 화면 DC로 복사함으로써 비트맵을 화면으로 출력한다. 원형은 다음과 같다.

BOOL BitBlt( HDC hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, HDC hdcSrc, int nXSrc, int nYSrc, DWORD dwRop ); 

첫번째 인수는 복사 대상 DC이며 다음 네 개의 인수는 복사 대상의 XYWH이며 PSrcDC가 복사원의 DC이다. xSrc, ySrc는 복사원의 좌표이되 BitBlt는 비트맵의 크기를 변경시키지 않고 복사를 수행하므로 폭과 높이는 복사 대상에서 한번만 지정하고 복사원에서는 이 값을 그대로 사용한다. 소스를 보면 MemDC의 0,0위치를 복사 대상의 0,0위치에 폭 123, 높이 160만큼 복사한다.

BitBlt의 마지막 인수 dwRop는 레스터 연산 방법을 지정하며 SRCCOPY를 쓰면 복사원을 그대로 복사 대상으로 복사한다. dwRop에 다른 값을 사용하면 기존 그림에 겹친다거나 반전시킬 수도 있다. 앞에서 배운 그리기 모드와 개념적으로 유사하며 다음과 같은 값들이 가능하다.

설명
BLACKNESS대상영역을 검정색으로 가득 채운다.
DSTINVERT화면을 반전시킨다.
MERGECOPY소스 비트맵과 대상 화면을 AND 연산한다.
MERGEPAINT소스 비트맵과 대상 화면을 OR 연산한다.
SRCCOPY소스 영역을 대상 영역에 복사한다.
WHITENESS대상영역을 흰색으로 채운다.

비트맵 출력이 끝난 후에는 비트맵 자체와 메모리 DC를 해제해 주어야 한다. 비트맵은 GDI 오브젝트이므로 DeleteObject 함수로 지우면 되고 메모리 DC는 DeleteDC라는 별도의 함수를 사용하여 지운다.


 BitBlt(hdc, 0010001000, MemDC, 00, SRCCOPY);

BOOL BitBlt( HDC hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, HDC hdcSrc, int nXSrc, int nYSrc, DWORD dwRop ); 

BitBlt(그림 x 좌표, 그림 y 좌표, 그림 넓이, 그림 높이, 그림 그려진 메모리 DC, 그림 시작 x 좌표, 그림 시작 y 좌표, 스타일);


(주쌤)

{

확대 축소라는 개념은 없고 그냥 짤려서 복사 해오는 것이다.

확대 축소하는 함수는 뒤에 나온다.

BLACKNESS

화면에 안나오는 부분이 검은색으로 나온다라는 소리이다.

}



6-4-라. StretchBlt

BOOL StretchBlt( HDC hdcDest, int nXOriginDest, int nYOriginDest, int nWidthDest, int nHeightDest, HDC hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc, DWORD dwRop ); 

BitBlt와 마찬가지로 DC간의 복사를 수행하지만 복사 후에 크기가 변경된다는 점이 다르다. 인수를 보면 복사 대상과 복사원이 모두 폭과 높이를 가지고 있다. 복사원의 지정한 영역이 복사대상의 지정한 영역의 크기만큼 확대되어 출력된다. 물론 복사대상의 영역이 복사원보다 더 좁다면 축소가 발생할 것이다.

예제의 BitBlt 호출문을 다음과 같이 변경해 보자.

StretchBlt(hdc,0,0,246,320,MemDC,0,0,123,160,SRCCOPY);

이 코드에서는 123,160의 폭을 가지는 영역을 246, 320 영역에 복사했으므로 비트맵이 두배의 크기로 확장된다.


{

얘가 확대 축소하는 애다.

뒤에 있는 애가 원본 좌표다.

다섯개로 나누면된다.

맨앞에는 무조건 dc 고

그룹이

(dc, 5 개(화면) , 5 개(비트맵) , )

원본그림의 가로 세로 등


확대 축소가 가능하다.

}





완성된 코드는

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"First";

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 = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = NULL;
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000)) 
  {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }

  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  HDC hdc, MemDC;
  PAINTSTRUCT ps;
  HBITMAP MyBitmap, OldBitmap;
  
  switch (iMessage) 
  {
    case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      MemDC = CreateCompatibleDC(hdc);
      MyBitmap = LoadBitmap(g_hInst, MAKEINTRESOURCE(IDB_BITMAP1));
      OldBitmap = (HBITMAP)SelectObject(MemDC, MyBitmap);
      //BitBlt(hdc, 0, 0, 1000, 1000, MemDC, 0, 0, SRCCOPY);
      StretchBlt(hdc, 00200200, MemDC, 00400400, SRCCOPY);
      SelectObject(MemDC, OldBitmap);
      DeleteObject(MyBitmap);
      DeleteDC(MemDC);
      EndPaint(hWnd, &ps);
      return 0;

    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}


실행화면은




바꾸어서,

 StretchBlt(hdc, 00800800, MemDC, 00400400, SRCCOPY);











6-4-마.비트맵 만들기

비트맵은 보통 페인트 샵이나 포토샵 또는 그림판 등의 그래픽 편집 툴로 만들어 사용하거나 아니면 미리 만들어져 있는 이미지를 구해 사용하지만 간단한 비트맵이라면 개발자 스튜디오에서 직접 만들 수도 있다. Insert/Resource 항목을 선택한 후 리소스 목록에서 Bitmap을 선택하거나 리소스 뷰가 있을 경우 리소스 뷰의 팝업 메뉴에서 Insert Bitmap 항목을 선택하면 새 비트맵을 만들 수 있다.

IDB_BITMAP2라는 디폴트 ID로 48*48 크기의 16색상 비트맵을 만들 수 있도록 비트맵 편집기가 열리며 도구 툴바와 색상 팔레트가 같이 열릴 것이다.

펜, 브러시, 문자툴, 선택툴 등 그림판에서 볼 수 있는 각종 툴들이 준비되어 있어 마치 그림판을 사용하듯이 비트맵을 그리면 된다. 비트맵의 크기나 색상, ID를 변경하려면 팝업 메뉴 편집기의 빈 여백을 더블클릭하여 속성 편집 윈도우를 열어 수정하면 된다.

이렇게 리소스에 추가된 비트맵은 언제든지 LoadBitmap 함수로 읽어올 수 있고 BitBlt 함수로 화면으로 출력할 수 있다. 비트맵을 만드는데는 별다른 기술이 필요한 것은 아니며 단지 약간의 예술적 소질만 있으면 된다.

이상으로 비트맵 리소스를 화면으로 출력하는 방법에 대해 아주 간단하게 알아 보았다. 비트맵은 단순한 장식외에도 여러 가지 활용용도가 있으며 그 기법들도 아주 고난도에 해당한다. 화면 영역 보관, 가상화면, 화면 확대, 더블 버퍼링, 투명 비트맵, 그래픽 파일 입출력 등등의 기법들이 있는데 비트맵에 관한 고급 기법들에 대해서는 18장에서 본격적으로 연구해 볼 것이다.



내가 직접 비트맵을 그려서 윈도우 화면에 띄워보자.

완성 시킨 소스는 이러하며

#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"First";

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 = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = NULL;
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000)) 
  {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }

  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  HDC hdc, MemDC;
  PAINTSTRUCT ps;
  HBITMAP MyBitmap, OldBitmap;
  
  switch (iMessage) 
  {
    case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      MemDC = CreateCompatibleDC(hdc);
      MyBitmap = LoadBitmap(g_hInst, MAKEINTRESOURCE(IDB_BITMAP2));
      OldBitmap = (HBITMAP)SelectObject(MemDC, MyBitmap);
      //BitBlt(hdc, 0, 0, 1000, 1000, MemDC, 0, 0, SRCCOPY);
      StretchBlt(hdc, 00800800, MemDC, 00100100, SRCCOPY);
      SelectObject(MemDC, OldBitmap);
      DeleteObject(MyBitmap);
      DeleteDC(MemDC);
      EndPaint(hWnd, &ps);
      return 0;

    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}








픽셀단위로 그려서 굉장히 작으므로 8배 확대 시킨 화면이다.














폰트도 펜이나 브러시와 마찬가지로 GDI 오브젝트이다. 폰트에 대한 지정을 하지 않고 문자열을 출력하면 디폴트로 시스템 폰트를 사용하지만 폰트를 만들고 DC로 전송한 후 문자열을 출력하면 DC에 전송된 폰트를 사용하여 문자열을 출력한다. 그래서 문자열을 원하는 폰트로 출력하고자 한다면 먼저 폰트 오브젝트를 만들어 DC로 전송해 주어야 한다.

6-5-가. CreateFont

폰트를 만들기 위해서는 CreateFont 함수를 사용하며 이 함수가 리턴해 주는 핸들을 HFONT형의 변수에 대입해 주면 된다. CreateFont 함수는 다음과 같이 아주 복잡한 모양을 가지고 있다.

HFONT CreateFont( int nHeight, int nWidth, int nEscapement, int nOrientation, int fnWeight, DWORD fdwItalic, DWORD fdwUnderline, DWORD fdwStrikeOut, DWORD fdwCharSet, DWORD fdwOutputPrecision, DWORD fdwClipPrecision, DWORD fdwQuality, DWORD fdwPitchAndFamily, LPCTSTR lpszFace ); 

인수만 해도 자그마치 14개나 될 정도로 복잡하다. 그만큼 글꼴이라는 것이 복잡한 모양을 가질 수 있다는 뜻이다. 개별 인수의 의미를 보기 쉽게 표로 정리하였다.

인수설명
nHeight폰트의 높이를 논리적인 단위로 지정한다. 이 값이 0일 경우는 디폴트 크기가 사용된다.
hWidth폰트의 폭을 지정하되 이 값이 0이면 nHeight에서 지정한 높이에 따라 폭을 자동으로 결정한다. 즉 이 값이 0이면 폰트의 종횡비(장평)가 일정하다.
nEscapement폰트의 각도를 0.1도 단위로 설정한다. 이 각도는 문자가 출력될 X출과 문자열과의 각도이며 일상적인 360분법의 각도 체계를 사용한다. 즉 세시 방향이 0도로 사용되며 반시계 방향으로 각도가 증가한다.
nOrientation글자 한자와 X축과의 각도를 지정한다. nEscapement는 전체 문자열의 기울기를 지정하는데 비해 이 인수는 개별 문자의 기울기를 설정한다.
nWeight폰트의 무게를 0~1000까지의 값으로 설정한다. 쉽게 말해서 폰트의 두께를 설정한다. 0~1000까지의 값을 지정할 수 있으며 보통 굵기인 FW_NORMAL이 400이다.
bItalic, bUnderline, cStrikeOut기울임체, 밑줄, 관통선 속성을 설정한다. 데이터형이 BYTE이지만 불린형처럼 사용한다. 속성을 주고 싶으면 0 이외의 값(TRUE)를 주고 속성을 주지 않으려면 0(FALSE)를 준다.
nCharSet문자 셋을 설정한다. 여러 가지 값이 있지만 실제 사용될 수 있는 옵션은 ANSI_CHARSET과 OEM_CHARSET가 있다. ANSI_CHARSET가 윈도우즈에서 사용하는 문자셋이고 OEM_CHARSET가 도스에서 사용하는 문자셋이라고 생각하면 된다.
nOutPrecision출력 정확도를 설정한다.
nClipPrecision클리핑 정확도를 설정한다.
nQuality논리적 폰트를 물리적 폰트에 얼마나 근접시킬 것인가를 지정한다.
nPitchAndFamily폰트의 피치와 그룹을 설정한다.
lpszFacename글꼴의 이름을 나타내는 문자열을 설정한다.

인수가 많기도 하지만 개별 인수의 의미도 무척 복잡하다. 하지만 이중 실질적으로 변경해 주어야할 필요가 있는 인수는 문자의 크기를 지정하는 nHeight와 글꼴 모양을 지정하는 lpszFacename 정도이며 나머지 인수는 디폴트를 사용하면 일단은 큰 무리 없이 사용할 수 있다. 각 인수의 좀 더 정확한 의미에 대해서는 도움말을 참조하기 바란다.


이 부분은 넘어 간다.


리눅스 에서

gg = G

비쥬얼 스튜디오 에서





4-1-다. WM_KEYDOWN

키보드로부터 문자를 입력받고자 할 경우는 WM_CHAR 메시지를 사용하면 된다는 것을 배웠다. 문자 이외의 키를 입력 받으려면 WM_CHAR 메시지만으로는 입력을 받을 수 없다. 예를 들어 커서 이동키라든가 Ins, Del, PgUp, 펑션키 등의 키는 문자키가 아니기 때문에 WM_CHAR 메시지로는 검출해 낼 수 없다. 이때는 WM_KEYDOWN 메시지를 사용해야 한다.

WM_KEYDOWN 메시지는 wParam에 문자 코드가 아닌 가상 키코드라는 것을 전달해 준다. 가상키코드(Virtual Key Code)란 시스템에 장착된 키보드의 종류에 상관없이 키를 입력받기 위해 만들어진 코드값이며 다음과 같이 정의되어 있다.

가상키 코드
VK_BACK08Backspace
VK_TAB09Tab
VK_RETURN0DEnter
VK_SHIFT10Shift
VK_CONTROL11Ctrl
VK_MENU12Alt
VK_PAUSE13Pause
VK_CAPITAL14Caps Lock
VK_ESCAPE1BEsc
VK_SPACE20스페이스
VK_PRIOR21PgUp
VK_NEXT22PgDn
VK_END23End
VK_HOME24Home
VK_LEFT25좌측 커서 이동키
VK_UP26위쪽 커서 이동키
이하생략 이하생략

WM_KEYDOWN 메시지가 발생했고 wParam으로 VK_HOME이 전달되었으면 사용자가 Home키를 누른 것이다. 숫자 및 영문자의 가상 키코드는 아스키 코드와 같으며 매크로 상수는 정의되어 있지 않으므로 아스키 코드와 wParam을 바로 비교하면 된다.

가상 키 코드는 지금까지 나온 모든 키보드는 물론이고 앞으로 만들어질 키보드까지 고려하여 만들어진 범용적인 코드이다. 코드표를 보면 VK_SELECT, VK_EXECUTE, VK_HELP 등과 같이 현재 101키에 없는 키값도 미리 정의되어 있으며 펑션키도 F16까지 미리 만들어 놓았다. 이외 한국과 일본 등의 2바이트 문자를 지원하기 위한 특수한 가상키까지 포함되어 있는데 가상 키 코드를 이렇게 범용적으로 만들어 놓은 이유는 앞으로 윈도우즈를 다른 시스템으로 이식하더라도 키 코드를 그대로 쓸 수 있도록 하기 위한 배려이다.

WM_KEYDOWN 메시지 처리 루틴에서 wParam의 값과 가상 키코드값을 비교해 봄으로써 어떤 키가 눌러졌는지를 구분한다. lParam으로 전달되는 값은 WM_CHAR와 동일하나 역시 잘 사용되지 않는다.

다음의 KeyDown예제는 커서 이동키를 검출하여 문자 "A"를 화면에서 상하좌우로 이동하는 예제이다. 소스를 입력해볼 필요없이 눈으로 보기만 해도 이해할 수 있을 것이다.

#include <windows.h>

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass="KeyDown";

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=(HBRUSH)GetStockObject(WHITE_BRUSH);
	WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);
	WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);
	WndClass.hInstance=hInstance;
	WndClass.lpfnWndProc=(WNDPROC)WndProc;
	WndClass.lpszClassName=lpszClass;
	WndClass.lpszMenuName=NULL;
	WndClass.style=CS_HREDRAW | CS_VREDRAW;
	RegisterClass(&WndClass);

	hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,
		  CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
		  NULL,(HMENU)NULL,hInstance,NULL);
	ShowWindow(hWnd,nCmdShow);
	
	while(GetMessage(&Message,0,0,0)) {
		TranslateMessage(&Message);
		DispatchMessage(&Message);
	}
	return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	static int x=100;
	static int y=100;
	switch(iMessage) {
	case WM_KEYDOWN:
		switch(wParam) {
		case VK_LEFT:
			x-=8;
			break;
		case VK_RIGHT:
			x+=8;
			break;
		case VK_UP:
			y-=8;
			break;
		case VK_DOWN:
			y+=8;
			break;
		}
		InvalidateRect(hWnd,NULL,TRUE);
		return 0;
	case WM_PAINT:
		hdc=BeginPaint(hWnd,&ps);
		TextOut(hdc,x,y,"A",1);
		EndPaint(hWnd,&ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}

x,y라는 정수형 변수 두 개를 100으로 초기화 하고 WM_PAINT 메시지에서 이 변수의 위치에 문자 "A"를 출력한다. WM_KEYDOWN 메시지에서는 wParam을 읽어 커서 이동키일 경우 x,y값을 커서 이동키의 방향에 따라 조정함으로써 "A"를 화면에서 이동시킨다. 물론 WM_KEYDOWN에서 좌표를 조정한 후는 반드시 화면에 좌표 조정이 반영되도록 InvalidateRect 함수를 호출해 주어야 한다. 프로그램 실행중의 모습은 다음과 같다.

키보드를 누를 때마다 가상 키코드를 점검하여 커서 이동키에 따라 x,y 좌표값을 조정한 후 화면을 다시 그리고 있다. 이때 화면을 다시 그리기 위해 InvaludateRect 함수를 호출하는데 세번째 인수에 TRUE를 주어 배경을 지우도록 하였다. 만약 이 인수를 FALSE로 변경해 주면 어떻게 될까? 직접 실행해 보면 알겠지만 A문자가 이동은 하지만 기존 출력된 A문자가 지워지지는 않는다.

이 예를 통해 InvalidateRect의 세번째 인수가 어떤 역할을 하는지 쉽게 이해할 수 있을 것이다. WM_KEYDOWN의 반대 메시지는 WM_KEYUP이며 키가 떨어질 때 발생한다. wParam, lParam의 의미는 WM_KEYDOWN과 동일하다. 별로 잘 사용되지 않는 메시지이다.


난 이 수업을 제대로 못들었지만 아주 간단하므로 잘 따라만 하면 되겠다.














현재 우리가 그림 그림을 이동하는 즉, 게임을 만드렁 보려고 소스코드를 수정중이다.


#include <windows.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = L"First";

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 = (HBRUSH)GetStockObject(WHITE_BRUSH);
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  WndClass.hInstance = hInstance;
  WndClass.lpfnWndProc = (WNDPROC)WndProc;
  WndClass.lpszClassName = lpszClass;
  WndClass.lpszMenuName = NULL;
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
    NULL, (HMENU)NULL, hInstance, NULL);
  ShowWindow(hWnd, nCmdShow);

  while (GetMessage(&Message, 000))
  {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }

  return Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  static HDC MemDC;
  PAINTSTRUCT ps;
  HBITMAP MyBitmap; 
  HBITMAP OldBitmap;
  static int iXPos = 0;
  static int iYPos = 0;

  switch (iMessage)
  {//캐릭터를 움직여보자.
    case WM_KEYDOWN:
      switch (wParam)
      {
        case VK_LEFT:
          iXPos = iXPos - 8;
          break;

        case VK_RIGHT:
          break;
          iXPos = iXPos + 8;

        case VK_UP:
          iYPos = iYPos - 8;
          break;

        case VK_DOWN:
          iYPos = iYPos + 8;
          break;
      }
      return 0;

    case WM_CREATE:  
      hdc = GetDC(hWnd);
      MemDC = CreateCompatibleDC(hdc);
      ReleaseDC(hWnd, hdc);
      return 0;

    case WM_PAINT :
      hdc = BeginPaint(hWnd, &ps);

      MyBitmap = LoadBitmap(g_hInst, MAKEINTRESOURCE(IDB_BITMAP2));
      OldBitmap = (HBITMAP)SelectObject(MemDC, MyBitmap);
      //BitBlt(hdc, 0, 0, 1000, 1000, MemDC, 0, 0, SRCCOPY);
      StretchBlt(hdc, 00800800, MemDC, 00100100, SRCCOPY);
      SelectObject(MemDC, OldBitmap);
      DeleteObject(MyBitmap);
      DeleteDC(MemDC);
      EndPaint(hWnd, &ps);
      return 0;

    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }

  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}















1. 경계 검사

2. 창 모양 고정 시킨다. 스크롤바도 없애고 가로 세로 크기를 사용자가 바꾸지 못하도록 고정시킨다.