5장. 단축키와 비트맵 윈도우 프로그램에는 화면에서 사용자들의 입력을 받아 들이고 출력을 위한 코드 외 부분이 존재한다. 이 부분을 주로 리소스라고 부르고 이들은 주로 화면에 나타난다. 메뉴, 툴바, 비트맵, 단축키, 대화상자 등이 여기에 속한다. 이 부분들은 우리의 프로그램의 사용 편의성을 높여주어 프로그램 완성도를 높인다. 1/211 1
단축키 메뉴항목을 선택하지 않고 키보드 만으로 메뉴의 기능을 수행하게 하는 기능 설정방법 메뉴의 속성창에서 Caption에 단축키 표시 Resource 도구상자에서 “New accelerator” 선택 단축키 맵핑 단축키 설정 단축키는 메뉴에 있는 기능을 수행시키고자 할 때 마우스로 메뉴항목을 선택하는 방법이 아니고 약속된 키 입력으로 메뉴항목에 설정된 기능을 수행하게 만드는 방법을 말한다. 단축키를 설정하기 위해서 먼저 할 일은 메뉴에서 단축키를 설정하고자 하는 항목을 선정한다. 선정된 메뉴항목의 속성창에서 Caption부분에 단축키를 표시해 준다. 예를 들어, “편집”부메뉴의 “복사하기”메뉴항목에 단축키 Ctrl+C를 설정하기 원한다면 속성창을 열고 Caption에 “복사하기\tCtrl+C”라고 써 준다. ‘\t’는 탭문자이기 때문에 단축키를 표시하는 문자열들이 오른쪽에 정렬되어 나타난다. Caption에 써주는 것은 사용자에게 단축키를 알려주기 위한 것 뿐이다. 사실 이 부분을 하지 않아도 단축키 설정을 할 수 있다. 두 번째로 해야 할 일은 앞에서 본 Resource 도구상자에서 “New accelerator”버튼을 눌러서 단축키 입력 환경을 만든다. 그리고 나서, 각 메뉴항목에 대해 단축키를 맵핑해 주고 마지막으로 설정을 하면 끝나게 된다. 맵핑과 설정 부분은 시범을 보이는 화면을 보면서 배우는 것이 좋기 때문에 다음 페이지에서 다루도록 하겠다. 2/211 2
실습 5-1 실습제목 실습내용 기본 메뉴에 단축키 표시하기 다음 장에 있는 메뉴항목 설정표를 참고하여 메뉴항목 Caption에 단축키를 표시하시오. 앞에서 만들었던 메뉴에 단축키를 표시하는 실습을 해 보도록 한다. 앞에서 만든 메뉴에서 메뉴항목의 Caption은 한글로 된 명령 뿐이다. 각 메뉴항목에 설정하기 원하는 단축키를 각 메뉴항목의 Caption에 ‘\t’문자와 함께 표시하여 준다. 3/211 3
실습안내서(계속) Caption ID 속성 파일 Pop-up 새글\tCtrl+N ID_FILENEW 디폴트 열기\tCtrl+O ID_FILEOPEN 저장하기\tCtrl+S ID_FILESAVE 끝내기\tCtrl+E ID_EXIT 각 메뉴항목에 대한 단축키는 위의 테이블에서 보는 바와 같다. 단축키는 속성이 Pop-up인 메뉴항목에는 필요 없다. 그것은 선택 가능한 메뉴항목만 기능을 가지고 있고 단축키는 그 기능을 수행시키기 위한 방편이기 때문이다. 위에서 보는 바와 같이 모든 Caption에는 ‘\t’문자를 넣어 주었다. 그것은 한글로 표기된 명령어와 단축키 문자열 사이에 탭간격을 주기 때문에 단축키 문자열이 오른쪽에 정렬되는 효과를 볼 수 있다. 4/211 4
실습안내서(계속) Caption ID 속성 편집 Pop-up 취소하기\tCtrl+U ID_EDITUNDO 디폴트 복사하기\tCtrl+C ID_EDITCOPY 붙여넣기\tCtrl+V ID_EDITPASTE 여기서도 “편집”부메뉴의 메뉴항목들에 단축키를 설정하였다. 단축키를 설정할 때 주의할 것은 다른 프로그램에서 사용하는 단축키와 동일한 것을 사용하는 것이 좋다. 가령 “복사하기”는 대부분의 프로그램에서 “Ctrl + C”를 사용한다. 그렇기 때문에 특별한 이유가 없는 한 우리의 프로그램에서도 “복사하기”를 위해 “Ctrl + C”를 사용하는 것이 좋다. 메뉴항목의 속성창 Caption에 단축키를 설정해 주는 것은 사용자를 위한 것이다. 이것을 했다고 해서 자동으로 단축키가 설정될 것이라고 생각하면 안 된다. 사실 이 과정은 하지 않아도 상관없다. 단지 사용자가 불편할 뿐이다. 5/211 5
New Accelerator(Ctrl+7) 단축키를 설정하기 위해 반드시 해야 할 첫 번째 일은 새로운 accelerator를 추가해 주는 것이다. 이것은 Resource 도구상자에서 “New Accelerator”를 찾아서 클릭해 줌으로 가능하다. 위의 그림에서 보는 바와 같이 7번째 있는 버튼을 눌러주거나 단축키로 설정된 “Ctrl + 7”를 눌러줘도 된다. 그러면 왼쪽에 있는 Resource창에는 Accelerator폴더가 생기면서 하위 항목으로 IDR_ACCELERATOR1이 생긴다. IDR_ACCELERATOR1은 우리가 추가한 새로운 Accelerator의 ID인 것이다. 그리고, 오른쪽 편집창에는 ID, Key, Type 칼럼을 가진 테이블이 만들어 진다. 각 메뉴항목에 단축키를 설정하면 테이블에 한 줄씩 저장된다. 6/211 6
단축키 맵핑 이제 각 메뉴항목에 단축키를 맵핑하는 과정을 살펴 보겠다. 앞에서 만들어진 테이블에서 파란색으로 표시된 줄을 더블클릭한다. 더블클릭을 하면 위에서 보이는 Accel Properties라는 창이 등장한다. ID부분을 선택하면 메뉴에 등록된 메뉴항목들의 ID들을 볼 수 있다. 단축키를 맵핑하기 원하는 메뉴항목의 ID를 선택한다. 선택하면 그 뒤에 숫자가 나오는데 이것은 신경 쓰지 않아도 된다. “Next Key Typed”버튼을 누르면 “Press a key to be used as the accelerator”라는 창이 등장한다. 이것은 단축키로 사용하기 원하는 키를 누르라는 명령이기 때문에 단축키로 사용하기 원하는 키의 조합을 눌러준다. 오른편에 있는 체크버튼이나 라디오 버튼은 손대지 않아도 된다. 7/211 7
단축키 맵핑(계속) 속성창을 닫아주면 위에서 보는 바와 같이 테이블에 한 줄이 추가된다. 위의 테이블에는 지금까지 맵핑된 단축키 리스트를 보여주고 있다. 테이블의 ID칼럼은 단축키를 설정한 메뉴항목의 ID이다. Key칼럼은 단축키로 사용할 버튼의 조합이다. 8/211 8
단축키 설정 5-2 HACCEL hAcc; . . . 중략 . . . hAcc=LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1)); while (GetMessage (&msg, NULL, 0, 0)) { if(!TranslateAccelerator(hwnd,hAcc,&msg)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; } 마지막으로 단축키를 설정한다. 설정은 우리 프로그램의 메인 함수인 WinMain에서 처리한다. 이것은 메시지를 WndProc에 보내기 전에 메시지를 변경해야 하기 때문이다. 자원을 만들어서 프로그램에서 사용하는 방법은 언제나 비슷하다. 먼저 자원을 위한 핸들변수를 만들고 자원의 ID를 로드하여 만들어진 핸들변수에 저장하여 사용한다. Accelerator의 경우도 마찬가지이다. Accelerator의 핸들타입인 HACCEL의 변수를 선언한다. 그리고 만들어진 Accelerator 자원의 ID인 IDR_ACCELERATOR1를 이용하여 Accelerator를 로드 한다. 그리고, GetMessage함수를 이용해서 메시지큐의 메시지를 얻어온 다음 Accelerator에 설정된 단축키인지 체크한다. 체크하는 방법은 함수 TranslateAccelerator를 이용하는 것이다. 받아온 메시지와 Accelerator를 로드한 hAcc를 가지고 메시지가 단축키에 해당하는지 체크한다. 메시지가 단축키에 설정된 것과 같으면 메시지는 WM_COMMAND로 변경된다. 가령, ‘Ctrl+C’버튼이 눌러졌다고 가정을 하겠다. 그러면 키보드가 눌러진 것이기 때문에 WM_KEYDOWN이 발생하여 msg에는 (메시지번호: WM_KEYDOWN, wParam: ‘Ctrl+C’)의 값이 들어 있게 된다. 그것을 hAcc와 함께 TranslateAccelerator에 넘겨주면 Ctrl+C가 IDR_ACCELERATOR1에 맵핑된 단축키인지 체크한다. 맵핑된 단축키라면 msg의 내용은 (메시지번호: WM_COMMAND, LOWORD(wParam):ID_EDITCOPY)로 변하게 된다. 9/211 9
비트맵 새로운 비트맵 이미지 만들기 이미지 파일을 비트맵형태로 읽어오기 비트맵 출력하기 10/211 비트맵 이미지를 우리의 프로그램에서 사용하는 방법을 배운다. 비트맵 이미지도 앞에서 배운 메뉴나 단축키처럼 하나의 자원이다. 생성하고 사용하는 방법이 앞에서 했던 방법과 매우 유사하다. 먼저, 새로운 비트맵 이미지를 만들어야 한다. 직접 그려도 되고 파일에 있는 내용을 가져와도 된다. 이미지를 만든 후 이미지 파일을 비트맵 형태로 읽어온다. 그리고 비트맵을 화면에 출력해 주면 된다. 10/211 10
비트맵 출력하기 hBitmap hdc memdc 비트맵 가져오기 LoadBitmap() SelectObject() 비트맵을 출력하기 위한 과정을 설명한다. 그림을 비주얼 스투디오의 비트맵으로 불러와야 한다. 이 과정은 앞에서 이미 설명하였다. 비트맵의 ID과 함수LoadBitmap을 통해 비트맵을 HBITMAP변수인 hBitmap에 로드한다. 앞에서 메뉴나 Accelerator도 모두 ID와 Load함수를 이용하여 로드 했던 것과 같이 비트맵도 LoadBitmap함수를 이용해서 로드해야 한다. 화면을 디바이스 컨텍스트인 hdc로 얻어와야 한다. 이것은 앞에서 배운 바와 같이 BeginPaint나 GetDC함수를 이용하면 가능하다. hdc와 호환이 되는 디바이스 컨텍스트인 memdc를 CreateCompatibleDC를 이용하여 만든다. 도형을 그리거나 글씨를 쓰는 것은 화면 디바이스 컨텍스트인 hdc에 바로 쓰거나 그리면 되었지만 비트맵은 화면 디바이스 컨텍스트와 호환이 되는 메모리 디바이스 컨텍스트를 통해서 해야 한다. 메모리 디바이스 컨텍스트는 화면 디바이스 컨텍스트와 동일하지만 화면이 없고 메모리 상에만 존재한다는 것이 다르다. 화면 디바이스 컨텍스트나 메모리 디바이스 컨텍스트는 모두 HDC타입으로 선언된 변수이다. hBitmap에 로드된 이미지는 메모리 디바이스 컨텍스트인 memdc에 SelectObject함수를 이용하여 설정된다. 이것은 memdc에 그리는 것과 동일하다. 하지만 memdc는 메모리 상에만 존재하기 때문에 눈으로 확인되지는 않는다. 이제 memdc의 그림을 hdc에 옮겨 주기만 하면 된다. 옮길 때 사용하는 함수는 BitBlt이다. BitBlt() hdc memdc BeginPaint() 또는 GetDC() CreateCompatibleDC() 11/211 11
비트맵 나타내기 New Bitmap (Ctrl+5) 12/211 비트맵을 만드는 방법은 앞에서 보았던 메뉴 생성이나 Accelerator생성과 비슷하다. Resource도구상자에서 다섯번째 버튼을 누르면 위의 그림과 같이 새로운 비트맵이 생성된다. 새롭게 생성된 미트맵의 ID는 IDB_BITMAP1이다. 오른쪽 편집창은 두 부분으로 나뉘어서 이미지 작업을 할 수 있는 판과 전체를 볼 수 있는 화면이 생긴다. 이미지 작업을 할 수 있는 부분은 윈도우에서 제공되는 그림판과 비슷한 기능을 가지고 있다. 새롭게 추가된 비트맵도 하나의 자원이기 때문에 관련된 속성정보를 속성창에서 확인할 수 있다. 위에서와 같이 비트맵 ID인 IDB_BITMAP1이 선택된 상황에서 [View]->[Properties]를 선택하면 속성창을 볼 수 있다. 12/211 12
비트맵 속성 메뉴에서 View->Properties 선택 ID: 비트맵에 대한 식별자, 수정가능 Width, Height: 비트맵의 크기, 수정가능 Colors: 사용하는 컬러수로 256으로 조정하는 것이 좋음 File name: 비트맵파일을 저장할 파일이름 비트맵 속성창 예시이다. 비트맵 ID를 선택한 상황에서 메뉴의 View->Properties를 선택하면 속성정보를 확인할 수 있다. 위의 그림은 IDB_BITMAP2라는 비트맵에 대한 속성창이다. ID는 비트맵을 하나의 자원으로 식별하기 위한 식별자이다. 프로그램내에서는 이 식별자를 이용하여 비트맵을 화면에 나타낸다. Width, Height는 생성된 비트맵의 가로 세로 크기를 나타낸다. 이 값을 수정하면 비트맵의 크기도 달라지게 된다. Colors는 사용할 컬러수이다. 컬러수는 256으로 조정하는 것이 좋다. 마지막으로 File name에는 이미지가 저장될 비트맵 파일 이름을 쓸 수 있다. 13/211 13
이미지파일 불러오기 그림판이나 포토샵과 같은 그림편집 도구에서 그림의 원하는 영역을 선택하여 복사한다. 비트맵에서 붙여넣기를 수행한다. 그림의 크기가 비트맵크기와 맞지 않을 수 있지만 자동으로 조정된다. 편집창에서 이미지를 직접 그릴 수도 있지만 인터넷에서 다운 받은 이미지를 불러다 저장할 수도 있다. 인터넷에서 그림을 다운 받았다면 그림판이나 포토샵과 같은 그림 편집 도구에서 다운 받은 그림을 오픈한다. 오픈한 그림의 전체 또는 그림의 일부분을 이미지로 사용하기 원할 것이다. 본인이 원하는 영역을 선택하고 “복사하기”를 수행한다. 이제 비주얼 스투디오 환경으로 이동하여 비트맵을 선택한 후 “붙여넣기”를 수행한다. 그리기를 원하는 그림의 가로, 세로 크기와 비트맵의 Width, Height의 값이 서로 맞지 않을 수 있다. 맞지 않을 때 조정하겠느냐는 질문이 나오는데 그렇게 하겠다고 하면 그림의 크기에 맞게 비트맵의 Width, Height값은 조정된다. 14/211 14
이미지를 붙여 넣은 예 이것은 인터넷에서 다운 받은 그림의 일부를 복사하여 우리의 비트맵에 붙여넣기를 한 결과이다. 오른쪽은 편집을 할 수 있는 비트맵이고 왼쪽은 비트맵의 전체보기를 나타낸 것이다. 15/211 15
비트맵 출력 예제 HDC hdc, memdc ; PAINTSTRUCT ps ; static HBITMAP hBitmap; switch (iMsg) { case WM_CREATE: hBitmap = (HBITMAP)LoadBitmap(((LPCREATESTRUCT)lParam)->hInstance, IDB_BITMAP1); break; case WM_PAINT: hdc = BeginPaint(hwnd, &ps); memdc=CreateCompatibleDC(hdc); SelectObject(memdc,hBitmap); BitBlt(hdc,0,0,332,240,memdc,0,0,SRCCOPY); DeleteDC(memdc); EndPaint(hwnd, &ps); 비트맵 이미지를 화면에 출력하는 예제이다. 여기서 불러올 비트맵 이미지의 ID는 IDB_BITMAP1이라고 가정할 것이다. 비트맵을 다루기 때문에 메모리 디바이스 컨텍스트 변수가 필요하다. 따라서, 우리는 memdc변수를 HDC타입으로 선언하였다. 비트맵을 로드할 변수 hBitmap을 HBITMAP으로 선언하였다. 윈도우가 생성되어 WM_CREATE메시지가 도착하면 비트맵 이미지를 로드한다. 로드할 때 사용하는 함수는 LoadBitmap이다. 함수의 첫 번째 파라메터는 우리 어플리케이션의 인스턴스값이다. WinMain함수에서는 어플리케이션 인스턴스의 값이 hInstance값이기 때문에 쉽게 알 수 있지만 WndProc에서는 알 수 없다. 알아내는 방법은 위에서 와 같이 ((LPCREATESTRUCT)lParam)->hInstance라고 써주면 WinMain의 hInstance값과 동일하다. WM_CREATE메시지 후에 WM_PAINT메시지가 도착하면 화면 디바이스 컨텍스트를 hdc에 저장하고 그것을 기반으로 메모리 디바이스 컨텍스트인 memdc를 생성한다. 그리고, SelectObject함수를 이용하여 memdc에 hBitmap을 설정해 준다. 실제 그림을 그리는 것은 그 다음으로 BitBlt함수를 이용한다. 위의 의미를 설명하면 memdc에 있는 그림을 좌표 (0,0)부터 폭 332픽셀, 높이 240픽셀 만큼 복사하여 hdc의 좌표 (0,0)부터 그리라는 것이다. 그림 그리기를 마친 후 메모리 디바이스 컨텍스트인 memdc는 삭제한다. 16/211 16
BitBlt() BOOL BitBlt(HDC hdc, int nXD, int nYD, int nW, int nH, HDC memdc, int nXS, int nYS, DWORD dwRop ); hdc memdc nXD nYD nW nH nW nH nXS nYS 앞장에서 살펴보았던 BitBlt함수를 자세히 설명한다. hdc는 화면을 가진 디바이스 컨텍스트이기 때문에 윈도우를 그림으로 왼쪽과 같이 나타내었고 memdc는 메모리에만 존재하는 디바이스 컨텍스트이기 때문에 윈도우 없이 그림만 오른쪽과 같이 나타내었다. SelectObject를 이용하면 memdc에 비트맵 이미지를 설정할 수 있다. hdc에 그리기를 원하는 그림 영역을 오른쪽의 사각형 영역이라고 가정하겠다. 즉, 그림의 시작점 좌표는 (nXS, nYS)이고 폭은 nW, 높이는 nH라고 가정하겠다. 우리는 이 그림을 hdc의 좌표(nXD, nYD)부터 같은 크기로 그리기를 원한다. 그러면, BitBlt(hdc, nXD, nYD, nW, nH, memdc, nXS, nYS, SRCCOPY)라고 써 주면 된다. 마지막 파라메터 dwROP에는 래스터 연산과 같이 그림을 hdc에 복사할 때 hdc의 바탕색과 그리고자 하는 그림의 색상 사이에 어떤 연산을 해야 하는지를 쓰는 곳인데 SRCCOPY라고 써주면 바탕색을 무시하고 그리라는 것이다. 바탕색을 무시했기 때문에 그림의 색상이 그대로 나타나게 된다. 17/211 17
StretchBlt() BOOL StretchBlt(HDC hdc, int nXD, int nYD, int nW, int nH, HDC memdc, int nXS, int nYS, int nWS, int nHS, DWORD dwRop ); hdc memdc nXD nWS nHS nXS nYS nW nYD 이것은 그리고자 하는 그림을 확대해서 나타내거나 또는 축소해서 나타내고자 할 때 사용하는 함수이다. 위의 예에서는 memdc의 사각형 영역 그림을 확대하여 hdc에 그리고자 하는 것이다. 앞장의 BitBlt와 다른 점은 각 영역의 폭과 높이를 모두 표시해 주어야 한다는 것이다. 앞에서는 양쪽의 크기가 똑같았기 때문에 hdc영역의 폭과 높이만 써 주었다. 따라서, 그림을 2배 확대 하고 싶으면 nW값이 nWS*2이고 nH값이 nHS*2라야 한다. 만약 2배 축소하기를 원하면 nW값이 nWS/2이고 nH값이 nHS/2이면 된다. 그 외의 사항은 앞장과 동일하다. nH 18/211 18
더블 버퍼링 BitBlt() hBit2 mem1dc mem2dc hBit1 CreateCompatibleBitmap() CreateCompatibleDC() LoadBitmap() SelectObject() StretchBlt() TextOut() 여기서는 더블버퍼링의 메커니즘을 설명한다. 앞에서 말했던 것처럼 메모리 디바이스 컨텍스트는 2개가 필요하다. 먼저 화면 디바이스 컨텍스트에서 mem1dc를 호환이 되도록 만든다. mem2dc는 mem1dc에서 호환이 되게 만들어야 한다. 서로 호환이 되어야 그림을 옮길 수 있기 때문이다. 비트맵 이미지인 그림을 hBit2에 로드해주고 SelectObject함수를 이용해서 hBit2를 mem2dc에 설정한다. 화면에 시각적으로 나오지는 않지만 mem2dc에 그림을 그린 것과 같다. 또 다른 비트맵 핸들 변수인 hBit1이 필요하다. 여기는 화면 디바이스 컨텍스트와 호환이 되도록 CreateCompatibleBitmap함수를 이용하여 만들어 줘야 한다. 그 비트맵을 mem1dc에 설정한다. 이제 mem1dc에 mem2dc의 그림을 옮겨 놓는다. 그리고, TextOut을 이용해서 mem1dc에 글을 쓴다. 이제 화면에 출력할 내용은 mem1dc에 전부 출력되어 있다. mem1dc의 내용을 StretchBlt를 이용하여 화면 디바이스 컨텍스트에 옮기면 모든 과정은 끝이 난다. 19/211 19
배경화면위로 움직이는 글 HDC hdc, memdc; PAINTSTRUCT ps; static HBITMAP hBit, oldBit; static RECT rectView; static int yPos; char word[] = "대한민국 화이팅"; switch(iMsg) { case WM_CREATE: yPos=-30; GetClientRect(hwnd,&rectView); SetTimer(hwnd,1,70,NULL); hBit=LoadBitmap(((LPCREATESTRUCT)lParam)->hInstance, MAKEINTRESOURCE(IDB_BITMAP1)); break; case WM_TIMER: yPos += 5; if (yPos > rectView.bottom) yPos = -30; InvalidateRect(hwnd,NULL,TRUE); return 0; 윈도우상에 위에서 아래로 자동으로 움직이는 글을 나타내 보도록 한다. 비트맵 이미지를 이용하여 배경을 그려야 하기 때문에 memdc변수와 hBit변수, oldBit변수를 선언하였다. 윈도우의 크기를 측정하여 경계값을 저장할 변수로 rectView를 선언하였다. 글을 저장할 변수는 문자배열 word[]이고 초기값은 “대한민국 화이팅”이다. 글을 출력할 위치는 (200,yPos)이다. x좌표는 고정되어 있고 y좌표만 초기값으로 -30을 yPos라는 변수에 저장하여 5씩 증가될 것이다. 증가되다가 y좌표인 yPos가 윈도우 하단 경계값인 rectView.bottom보다 커지면 즉, 글이 화면 아래로 내려가서 사라지게 되면 다시 글이 위에서 내려오도록 -30값을 넣어줄 것이다. 윈도우가 생성되어 WM_CREATE메시지가 오면 yPos에 초기 y좌표인 -30을 넣어주고 초기 윈도우의 크기를 측정하여 rectView에 저장한다. 그 다음으로 1번 타이머에 0.07초 간격으로 WM_TIMER메시지를 보내도록 셋팅한다. 그리고, 배경 이미지로 사용될 IDB_BITMAP1을 로드하여 hBit에 저장한다. 그러면, 0.07초마다 WM_TIMER가 오게 되는데 이때마다 yPos의 값은 5씩 증가된다. 계속 증가되다가 윈도우 하단 경계값인 rectView.bottom을 넘으면 다시 yPos는 -30이 된다. 새롭게 정해진 y좌표 값에 글을 출력하기 위해 InvalidateRgn함수를 호출함으로 WM_PAINT를 발생시킨다. 20/211 20
배경화면위로 움직이는 글(계속) case WM_PAINT: GetClientRect(hwnd,&rectView); hdc=BeginPaint(hwnd, &ps); hBit=LoadBitmap(((LPCREATESTRUCT)lParam)->hInstance, MAKEINTRESOURCE(IDB_BITMAP1)); memdc=CreateCompatibleDC(hdc); oldBit=(HBITMAP)SelectObject(memdc, hBit); StretchBlt(hdc,0,0,rectView.right,rectView.bottom, memdc,0,0,819,614,SRCCOPY); SelectObject(memdc, oldBit); DeleteDC(memdc); TextOut(hdc, 200, yPos, word, strlen(word)); EndPaint(hwnd, &ps); return 0; case WM_DESTROY: DeleteObject(hBit); KillTimer(hwnd,1); PostQuitMessage(0); } WM_PAINT메시지가 발생하면 다시 윈도우의 크기를 측정하여 rectView구조체에 저장한다. 이것은 윈도우의 크기가 프로그램 실행 중에 변경될 수도 있기 때문이다. 윈도우의 크기가 변경되면 WM_PAINT메시지가 발생되기 때문이다. 화면 디바이스 컨텍스트를 hdc에 얻어오고 얻어온 hdc를 기반으로 호환이 되는 메모리 디바이스 컨텍스트인 memdc를 만든다. memdc에 배경 이미지인 hBit를 설정함으로 변경을 그릴 준비를 마친다. 이때, SelectObject는 memdc에 있던 비트맵을 리턴하는데 그것을 oldBit에 저장하고 있다. 앞에서 배운 바와 같이 StretchBlt를 이용하면 윈도우 크기에 맞추어 배경을 그릴 수 있다. memdc에는 원래 설정되어 있던 비트맵인 oldBit를 설정시켜 주고 memdc는 삭제된다. 배경이 hdc에 그려졌기 때문에 그 위에 글을 출력한다. 글의 위치는 (200, yPos)좌표이다. 앞장에 case WM_TIMER:에 있는 InvalidateRgn함수 때문에 WM_PAINT가 발생할 때는 yPos의 값이 변경된 상태이다. 즉 글을 출력할 위치가 변경된 상태라는 것이다. 마지막으로 윈도우가 종료되는 메시지 WM_DESTROY가 오면 로드했던 비트맵 hBit도 지우고 동작하던 1번 타이머도 끝내준다. 21/211 21
문제점과 해결방법 화면의 깜박임 발생 원인: 움직이는 글을 위해 WM_TIMER에서 WM_PAINT 주기적으로 발생시키고 화면을 전체 지우고 다시 그리기 때문 해결: 메모리 DC를 하나 더 이용하여 화면 DC에 옮기기전에 메모리 DC에 전부 그림 메모리 DC 내용을 화면 DC에 변경된 부분만 수정하는 InvalidateRgn(hWnd, NULL, FALSE) 수행 이방법을 더블버퍼링이라 함 앞에서 작성한 프로그램은 실행중에 화면이 깜박거리는 문제점을 가지고 있다. 이 현상이 발생하는 원인은 자주 발생하는 WM_PAINT메시지 때문이다. 프로그램에서 WM_PAINT메시지는 0.07초마다 WM_TIMER메시지가 발생하면 InvalidateRgn함수로 인해 화면을 지우고 WM_PAINT메시지가 발생하고 있다. 화면을 전체적으로 지우고 다시 그리다 보니 깜박거림으로 나타나는 것이다. 이것을 해결하기 위해서는 메모리 디바이스 컨텍스트를 하나 더 사용해야 한다. 추가된 메모리 디바이스 컨텍스트에 그리기를 원하는 그림과 글씨를 다 쓴 다음에 화면 디바이스 컨텍스트로 옮기는 방법을 이용해야 하는 것이다. 이 방법을 더블버퍼링이라 부른다. 22/211 22
더블버퍼링 예제 void TextPrint(HDC hdc, int x, int y, char text[]) { int i, j; SetTextColor(hdc,RGB(255,255,255)); for (i=-1;i<=1;i++) for (j=-1;j<=1;j++) TextOut(hdc, x+i, y+j, text, strlen(text)); SetTextColor(hdc, RGB(0,0,0)); TextOut(hdc, x, y, text, strlen(text)); } LRESULT CALLBACK WndProc(HWND hwnd,UINT iMsg,WPARAM wParam,LPARAM lParam) static HDC hdc, mem1dc, mem2dc; PAINTSTRUCT ps; static HBITMAP hBit1, hBit2, oldBit1, oldBit2; static RECT rectView; static int yPos; char word[] = "대한민국 화이팅"; 앞에서 본 배경위에 글이 위에서 아래로 내려오는 예제를 더블버퍼링을 이용하여 다시 구현하였다. 메시지를 처리하는 함수 WndProc에서 추가된 변수는 2번째 메모리 디바이스 컨텍스트인 mem2dc, 2번째 비트맵 핸들 hBit2, hBit2를 디바이스 컨텍스트에 설정하면 받는 비트맵 핸들 oldBit2 등이다. 위의 코드에서 보이는 TextPrint함수는 화면에 표시되는 글이 선명하게 보이도록 출력하는 함수이다. TextOut함수와 비슷하게 호출하도록 작성하였는데 첫 번째 파라메터는 글을 출력하기 위한 디바이스 컨텍스트이고 두 번째, 세 번째 파라메터는 글이 출력될 위치이고 네 번째 파라메터는 출력될 글이 저장된 문자 배열이다. 먼저 하얀색으로 글을 9번 출력한다. 출력하는 위치는 (x-1,y-1),(x-1,y),(x-1,y+1), (x,y-1),(x,y),(x,y+1), (x+1,y-1),(x+1,y),(x+1,y+1) 이다. 그리고 나서 검정색으로 글을 (x,y)에 출력한다. 즉, (x,y)를 중심으로 주위에 하얀색으로 글을 쓴 다음 (x,y)에 검정색으로 쓰는 것이다. 23/211 23
더블버퍼링 예제(계속) switch(iMsg) { case WM_CREATE: yPos=-30; GetClientRect(hwnd,&rectView); SetTimer(hwnd,1,70,NULL); hBit2=LoadBitmap(((LPCREATESTRUCT)lParam)->hInstance, MAKEINTRESOURCE(IDB_BITMAP1)); break; case WM_TIMER: yPos += 5; if (yPos > rectView.bottom) yPos = -30; hdc = GetDC(hwnd); if (hBit1==NULL) { hBit1=CreateCompatibleBitmap(hdc,rectView.right, rectView.bottom); } 앞의 더블버퍼링을 설명하는 그림에서와 같이 hBit2에 배경 그림을 로드하고 mem2dc에 hBit2의 그림을 설정한다. hBit1은 화면 디바이스 컨텍스트를 얻어오고 mem1dc에 hBit1을 설정한다. 윈도우가 생성되어 WM_CREATE메시지가 오면 하는 일은 앞의 예제와 똑 같다. 글을 출력할 좌표의 y좌표인 yPos변수를 초기화 하고 윈도우의 클라이언트 영역의 크기를 측정하여 rectView에 저장한다. 그 다음에 그림이 자동으로 내려오도록 타이머를 셋팅한다. 마지막으로 hBit2에 우리의 배경그림인 IDB_BITMAP1을 불러온다. 이제 타이머를 셋팅했기 때문에 0.07초마다 WM_TIMER메시지가 도착한다. 그때마다 하는 일은 다음과 같다. yPos값을 5씩 증가 시켜준다. yPos의 값이 rectView.bottom값보다 커지면 즉, 글을 출력할 위치가 윈도우 하단 경계를 넘어가면 yPos값은 다시 -30이된다. hBit1을 화면 디바이스 컨텍스트인 hdc와 호환이 되도록 만들어 준다. 이때 사용하는 함수의 이름은 CreateCompatibleBitmap이다. 먼저 대상 디바이스 컨텍스트를 첫 번째 파라메터로 주고 만들 비트맵의 크기를 정해 준다. 여기서는 우리 윈도우와 같은 크기이기 때문에 rectView의 right값과 bottom값을 이용하였다. 24/211 24
더블버퍼링 예제(계속) mem1dc=CreateCompatibleDC(hdc); mem2dc=CreateCompatibleDC(mem1dc); oldBit1=(HBITMAP)SelectObject(mem1dc, hBit1); oldBit2=(HBITMAP)SelectObject(mem2dc, hBit2); BitBlt(mem1dc,0,0,819,614,mem2dc,0,0,SRCCOPY); SetBkMode(mem1dc, TRANSPARENT); //TextOut(mem1dc, 200, yPos, word, strlen(word)); TextPrint(mem1dc, 200, yPos, word); SelectObject(mem2dc, oldBit2); DeleteDC(mem2dc); SelectObject(mem1dc, oldBit1); DeleteDC(mem1dc); ReleaseDC(hwnd, hdc); InvalidateRgn(hwnd,NULL,FALSE); return 0; WM_TIMER메시지에 대해 할 일을 계속하여 설명한다. 화면 디바이스 컨텍스트인 hdc에서 메모리 디바이스 컨텍스트 mem1dc를 호환이 되도록 만들어 준다. 연이어서, 메모리 디바이스 컨텍스트 mem1dc에서 mem2dc를 호환이 되도록 만들어 준다. 이렇게 만드는 이유는 나중에 mem2dc의 비트맵을 mem1dc에 옮기고 mem1dc의 내용을 hdc로 옮길 것이기 때문이다. 비트맵을 옮기기 위해서는 비트맵 정보가 동일해야 하기 때문에 이렇게 하는 것이다. mem1dc에는 hBit1을 설정하고 mem2dc에는 hBit2를 설정해 준다. 이제 그림을 옮길 수 있는 단계가 되었다. mem2dc에 있는 배경 그림을 mem1dc에 옮긴다. 여기서 숫자 819, 614는 앞에서 사용한 그림이 크기가 우연히 819 X 614 일 뿐이다. 그림 크기가 다르면 이 숫자 역시 바꿔줘야 한다. SetBkMode함수는 글씨를 쓸 때 글씨의 배경 부분을 무슨 색으로 칠할 것인지 지정하는 함수이다. 컬러 대신 TRANSPARENT라고 쓰면 글씨의 배경은 투명하여 지기 때문에 배경 그림을 볼 수 있게 된다. 이제, 앞장에서 만든 TextPrint함수를 이용하여 글을 mem1dc에 출력한다. 이제 mem1dc에는 배경과 글이 모두 출력되어 있다. 그 다음에 하는 일들은 저장했던 비트맵 핸들 값들을 디바이스 컨텍스트에 원상복귀 시키는 일과 생성된 메모리 디바이스 컨텍스트를 삭제 해 주는 것이다. mem1dc가 삭제 되었지만 그림과 글이 있는 비트맵은 hBit1에 저장되어 있기 때문에 걱정할 필요 없다. 마지막으로 InvalidateRgn함수를 호출함으로 WM_PAINT를 발생시킨다. 이때 FALSE값을 세 번째 파라메터로 지정함으로 화면을 전부 지우지 않고 변하는 부분만 지우게 만든다. 25/211 25
더블버퍼링 예제(계속) case WM_PAINT: GetClientRect(hwnd,&rectView); hdc=BeginPaint(hwnd, &ps); mem1dc=CreateCompatibleDC(hdc); oldBit1=(HBITMAP)SelectObject(mem1dc, hBit1); StretchBlt(hdc,0,0,rectView.right,rectView.bottom,mem1dc,0,0,819,614,SRCCOPY); SelectObject(mem1dc, oldBit1); DeleteDC(mem1dc); EndPaint(hwnd, &ps); return 0; case WM_DESTROY: if (hBit1) { DeleteObject(hBit1); } DeleteObject(hBit2); KillTimer(hwnd,1); PostQuitMessage(0); 앞장의 WM_TIMER메시지 처리 마지막 부분에서 InvalidateRgn함수를 호출했기 때문에 위의 WM_PAINT메시지 처리하는 부분은 WM_TIMER메시지 처리 후에 도달하게 된다. 먼저, 화면의 크기를 측정하여 rectView에 저장한다. 이것은 윈도우의 크기가 변경 될 수 있기 때문에 다시 측정하는 것이다. hdc를 얻어오고 이를 기반으로 mem1dc를 호환이 되도록 만들어 준다. hBit1에는 배경과 글씨가 출력된 비트맵이 저장되어 있다. 이것을 mem1dc에 설정하면 출력할 내용은 mem1dc에 있게 된다. 이것을 StretchBlt함수를 이용하여 hdc에 뿌려주면 된다. 여기서 StretchBlt를 이용한 이유는 그림의 원본 크기와 화면의 크기가 다를 수 있기 때문이다. 출력한 이후에는 앞에서와 마찬가지로 저장 해 둔 oldBit1을 mem1dc에 설정하고 mem2dc는 필요 없기 때문에 삭제한다. 마지막으로 프로그램이 종료되는 WM_DESTROY메시지가 오면 hBit1, hBit2를 지워주고 타이머를 멈추며 mem1dc를 삭제한다. 26/211 26
연습문제 1 실습제목 실습내용 메뉴에 단축키 붙이기 원 복사 프로그램에 취소하기 기능 추가 원을 선택하여 복사하기/붙여넣기 기능이 있는 프로그램을 불러오시오. 메뉴나 단축키를 이용해 마지막 붙여넣기를 취소할수 있는 기능을 추가하시오. 앞에서 작성했던 원 선택 후 복사하는 프로그램에 단축키를 설정한다. 여기에 특별히 취소하기 기능을 구현해 준다. 취소하기는 마지막에 붙여 넣었던 원을 화면에서 사라지게 하는 것이다. 27/211 27
연습문제 2 실습제목 실습내용 윈도우에 배경 그림 넣기 인터넷에서 원하는 그림을 다운받아 프로그램에 등록한다. StretchBlt()를 이용하여 윈도우에 빈공간 없이 배경그림을 그린다. 윈도우 크기가 변경되어도 윈도우 화면 전체에 배경그림이 나와야 한다. 여기서는 우리가 만든 윈도우 배경에 그림을 그리는 프로그램을 작성해 보겠다. 일단 인터넷에서 자신이 원하는 그림을 다운 받는다. 그리고, 우리 프로젝트의 새로운 비트맵에 붙여넣기를 수행한다. 그림의 크기는 얼마든지 상관없다. 그런데, 그림이 크기와 윈도우의 크기가 다를 것은 당연한 결과이기 때문에 우리는 StretchBlt함수를 이용하여 그림이 크면 축소를 작으면 확대를 하여 윈도우 크기에 맞춘다. 프로그램은 실행 중간에 윈도우 크기를 변경할 수도 있다. 그런 상황이 발생하면 윈도우 크기를 다시 측정하여 측정된 크기에 맞추어 다시 그림을 그려줘야 한다. 28/211 28
연습문제 3 실습제목 실습내용 타자게임 윈도우 위에서 여러 개의 단어가 다양한 속도로 내려오도록 구현한다. 윈도우 아래 텍스트입력 할 수 있는 영역을 만든다. 입력된 텍스트가 위에서 내려오는 단어중 하나와 일치하면 일치된 단어는 화면에서 사라지고 또다른 단어가 내려온다. 지금까지 배웠던 내용을 기반으로 타자게임을 작성해 본다. 먼저 배경 그림을 그리고 게임이 시작되면 여러 개의 단어들이 위에서 내려오기 시작한다. 윈도우의 하단에는 글 입력을 받을 수 있는 공간이 있어야 한다. 게이머가 입력한 단어가 내려오는 단어들 중에 하나와 일치하면 일치된 단어는 화면에서 사라진다. 그리고, 또 다른 단어가 추가로 위에서 내려온다. 게이머가 3개 이상의 단어를 맞추지 못하여 지나치게 되면 게임은 끝나게 되고 맞춘 단어의 개수를 점수로 보여준다. 내려오는 단어들은 랜덤하게 내려와야 하고 내려오는 위치도 그때그때 달라져야 한다. 29/211 29