3.출력

.GDI 오브젝트

윈도우즈는 화면이나 프린터로 출력할 때 GDI 오브젝트를 사용하는데 핸들로 관리를 하다 보니 사용하기 불편하다. 그래서 MFC는 GDI 오브젝트를 클래스로 래핑하여 관련 함수들을 캡슐화하고 자동화된 파괴를 지원한다. WTL도 비슷한데 GDI 오브젝트를 클래스로 직접 래핑하지 않고 템플릿으로 래핑한다는 점이 조금 다르다.

대표적인 GDI 오브젝트인 펜을 래핑하는 CPenT 템플릿을 연구해 보자. 이 템플릿에 대해서는 별도의 문서를 읽을 필요도 없이(문서도 없지만) 헤더 파일의 템플릿 선언문을 읽어 보면 대충의 구조를 빠르게 이해할 수 있다. 프로그래머에게는 글로 된 설명보다 때로는 소스 코드가 더 명쾌한 정보를 제공한다. 소스를 보는 것을 두려워하지 말고 항상 소스 보기를 즐기도록 하자.

 

template <bool t_bManaged> class CPenT

{

public:

     HPEN m_hPen;

     CPenT(HPEN hPen = NULL) : m_hPen(hPen) { }

     ~CPenT()

     {

          if(t_bManaged && m_hPen != NULL)

              DeleteObject();

     }

 

보다시피 멤버는 HPEN 핸들 딱 하나밖에 없으며 단순히 HPEN을 래핑할 뿐이다. 생성자는 래핑할 HPEN 핸들을 전달받되 디폴트 값이 NULL로 되어 있어 핸들을 전달하지 않으면 빈 객체로 생성된다. MFC처럼 생성자를 통해 펜을 바로 생성할 수는 없으므로 CreatePen 따위의 생성 함수를 따로 호출해야 한다.

PenT 클래스는 bool형의 비타입 인수 하나를 취한다. WTL은 비타입 인수를 타입 인수와 쉽게 구분하기 위해 t_ 접두를 붙인다. 혹시 비타입 인수가 뭔지 모른다면 C++ 문법서를 잠시 참조하도록 하자. 흔하게 쓰지 않는 문법이라 생소할 것이다. t_bManaged가 true이면 파괴자가 핸들을 알아서 파괴하지만 t_bManaged가 false이면 단순히 래핑만 할 뿐이며 자동화된 파괴를 하지 않는다. 예제를 만들어 보자.

 

: GDIObject

뷰에 OnPaint 핸들러가 미리 작성되어 있으므로 여기에 출력 코드를 작성하면 된다. dc까지 선언되어 있으므로 dc의 멤버 함수를 호출하기만 하면 된다. 다음 코드는 사각형을 그린다.

 

LRESULT CGDIObjectView::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     CPaintDC dc(m_hWnd);

 

     dc.Rectangle(10,10,100,100);

 

     return 0;

}

 

디폴트 속성대로 그렸으므로 검정색의 사각형이 그려질 것이다. 선의 색상을 파란색으로 바꾸고 싶으면 파란색 펜을 만들어 DC로 선택하면 된다.

 

LRESULT CGDIObjectView::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     CPaintDC dc(m_hWnd);

 

     CPenT<true> Pen, OldPen;

     Pen.CreatePen(PS_SOLID,3,RGB(0,0,255));

     OldPen=dc.SelectPen(Pen);

     dc.Rectangle(10,10,100,100);

     dc.SelectPen(OldPen);

     return 0;

}

 

DC에 GDI 오브젝트를 생성, 선택 및 해제하는 방법은 Win32나 MFC나 모두 동일하다. 두 코드의 실행 결과는 다음과 같다. 펜을 바꿈으로써 사각형의 외곽선이 달라진다.

 

CPenT 타입의 객체를 선언하되 인수로 true를 주어 관리되는 펜 객체를 만들었다. 다 사용한 후 Pen을 파괴하지 않아도 파괴자가 알아서 정리할 것이다. 이에 비해 템플릿 인수로 false를 줄 때는 다 사용한 후 DeleteObject를 반드시 호출해야 한다.

 

LRESULT CGDIObjectView::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     CPaintDC dc(m_hWnd);

 

     CPenT<false> Pen, OldPen;

     Pen.CreatePen(PS_SOLID,3,RGB(0,0,255));

     OldPen=dc.SelectPen(Pen);

     dc.Rectangle(10,10,100,100);

     dc.SelectPen(OldPen);

     Pen.DeleteObject();

 

     return 0;

}

 

제일 마지막에 DeleteObject 호출이 있다는 점이 다르다. 이 호출을 생략하면 당장의 동작에는 이상이 없는 것처럼 보이지만 리소스가 새므로 불안정해진다. MFC의 CPen은 파괴자가 무조건 동작하지만 WTL의 CPenT는 파괴자의 동작 여부를 템플릿 인수로 선택하도록 되어 있다. 대부분의 경우 파괴자가 자동으로 파괴하는 것이 편리하고 안전하지만 때로는 이런 파괴자의 동작이 방해가 될 수도 있다.

예를 들어 스레드간에 펜 객체를 공유한다거나 함수 내부에서 펜 객체를 생성하여 전역 구조체 내에 저장해 놓고자 할 때는 지역 객체라도 함부로 파괴되어서는 안된다. 그래서 비관리 래퍼도 따로 제공되며 자주는 아니지만 종종 필요할 때가 있다. 비관리 래퍼는 단순히 핸들을 래핑만 할 뿐 관리는 하지 않는 것이다. 그런데 CPenT<true> 따위의 이름이 너무 쓰기 불편하다. 그래서 WTL은 관리 여부에 따른 별칭을 정의해 놓았다.

 

typedef CPenT<false>   CPenHandle;

typedef CPenT<true>    CPen;

 

관리되는 펜 객체를 선언하려면 CPen이라는 명칭을 사용하면 되고 관리할 필요없이 단순히 핸들만 래핑하고 싶다면 CPenHandle을 사용하면 된다. 타입 정의에 의해 다음 두 선언문은 완전히 동일하다.

 

CPenT<true> Pen;

CPen Pen;

 

아무래도 CPen이라고 쓰는 것이 더 편리하다. 다른 GDI 오브젝트도 똑같은 방식이므로 도표로 간략하게 정리해 보자. 이름을 붙이는 방법에 일관성이 있어 외우기 쉽다. 관리되는 타입은 MFC와 명칭이 같아 이미 익숙하기도 하다.

 

GDI 오브젝트

템플릿

관리 래퍼

비관리 래퍼

선택 함수

DC

CDCT

CDC

CDCHandle

 

CPenT

CPen

CPenHandle

SelectPen

브러시

CBrushT

CBrush

CBrushHandle

SelectBrush

폰트

CFontT

CFont

CFontHandle

SelectFont

비트맵

CBitmapT

CBitmap

CBitmapHandle

SelectBitmap

리전

CRgnT

CRgn

CRgnHandle

SelectRgn

 

DC에 오브젝트를 선택하는 함수들도 따로 정의되어 있어 리턴값을 대입받을 때 캐스팅할 필요가 없다. 심지어 스톡 오브젝트를 선택하는 함수들도 오브젝트 타입별로 따로 정의되어 있다.

 

SelectStockPen

SelectStockBrush

SelectStockFont

 

스톡 오브젝트 이름에 WHITE_PEN, BLACK_BRUSH 등의 오브젝트 타입이 포함되어 있는데 이렇게까지는 할 필요가 없어 보인다. 타입에 따라 함수를 구분해서 써야 하므로 오히려 더 불편해 보인다. 다음 코드는 파란색으로 사각형 외곽을 그리고 빨간색으로 내부를 채운다. 펜과 브러시 오브젝트를 동시에 사용해야 한다.

 

LRESULT CGDIObjectView::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     CPaintDC dc(m_hWnd);

 

     CPen Pen, OldPen;

     CBrush Brush, OldBrush;

     Pen.CreatePen(PS_SOLID,3,RGB(0,0,255));

     Brush.CreateSolidBrush(RGB(255,0,0));

     OldPen=dc.SelectPen(Pen);

     OldBrush=dc.SelectBrush(Brush);

 

     dc.Rectangle(10,10,100,100);

    

     dc.SelectPen(OldPen);

     dc.SelectBrush(OldBrush);

 

     return 0;

}

 

, 브러시를 선택하는 함수가 SelectPen, SelectBrush로 구분되어 있음을 주의하자. 실행 결과는 다음과 같다.

항상 같은 타입의 Old 핸들을 선언하여 DC의 상태를 원래대로 돌려 놓아야 하므로 상당히 번거롭다. 이 점은 Win32에서도 마찬가지고 MFC에서도 동일한데 파괴자는 스스로를 파괴할 수는 있지만 DC의 상태까지 복원해 주지는 못하기 때문이다. 불편하기도 하지만 복구를 제대로 하지 못하면 위험해지기도 한다. 적어도 이 문제에 있어서만큼 객체 지향이 별 도움이 안된다. 반면 GDI+는 상태가 없는 라이브러리이기 때문에 이런 문제가 없으며 그래서 닷넷의 편의성이 더 높다고 하는 것이다.

다음은 DC에 대해 정리해 보자. CDC를 루트로 하여 다음 파생 클래스들이 제공된다. 이름으로부터 용도를 쉽게 짐작할 수 있을 것이다.

CPaintDC는 BeginPaint, EndPaint를 래핑하며 CClientDC는 GetDC, ReleaseDC를 래핑한다고 굳이 설명하지 않아도 추측 가능할 것이다. CMemoryDC는 비트맵을 생성 및 선택하여 간단한 더블 버퍼링 기능을 제공한다. 간편하기는 하지만 비트맵을 매번 생성 및 파괴하기 때문에 효율은 별로 좋지 못하다.

CDC에는 그리기와 관련된 멤버 함수들이 대거 포함되어 있다. Ellipse, BitBlt, TextOut, DrawText 등의 출력 함수와 SetTextColor, SetBkMode 등의 상태 변경 함수 등 Win32에서 HDC를 인수로 취하는 모든 함수들이 CDC의 멤버라고 보면 된다. MFC에 익숙한 사람은 MFC에서 CDC 클래스를 쓰는 방법과 똑같은 방법대로 CDC를 사용하면 된다.

.비트맵 출력

비트맵은 다른 도형과는 달리 리소스로 포함해야 한다. 리소스를 작성하고 사용하는 방법은 Win32나 MFC와 똑같은데 왜냐하면 모두 비주얼 스튜디오라는 개발 환경을 공유하고 있기 때문이다. 마법사로 프로젝트를 만들면 리소스 스크립트가 생성되고 리소스 뷰를 통해 리소스를 만들거나 외부의 파일을 가져올 수 있다.

적당한 비트맵 이미지를 준비하여 프로젝트의 res 폴더로 복사해 둔다. 잘 알겠지만 윈도우즈가 직접 지원하는 비트맵은 bmp밖에 없다. jpg나 png는 bmp로 바꾼 후 사용해야 한다. 리소스 뷰에서 리소스 추가를 선택한 후 가져오기 버튼을 누르고 미리 복사해 둔 비트맵 파일을 선택한다. IDB_BITMAP1이라는 이름으로 임포트될 것이다. 이제 이 리소스를 로드하여 출력하면 된다.

 

LRESULT CGDIObjectView::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     CPaintDC dc(m_hWnd);

 

     CBitmap Bit,OldBit;

     BITMAP BitInfo;

     Bit.LoadBitmap(IDB_BITMAP1);

     CDC MemDC;

     MemDC.CreateCompatibleDC();

     OldBit=MemDC.SelectBitmap(Bit);

     Bit.GetBitmap(BitInfo);

     dc.BitBlt(0,0,BitInfo.bmWidth,BitInfo.bmHeight,MemDC,0,0,SRCCOPY);

     MemDC.SelectBitmap(OldBit);

     return 0;

}

 

실행해 보면 멋진 사진이 나타날 것이다. 책 쓸 때마다 왜 내 사진은 안나오냐고 찡얼대길레 이번 강좌에 한 컷 넣었다. 이효리나 메이비 사진을 쓰고 싶지만 사진에도 초상권이 있어 초상권 문제가 없는 색시 사진을 쓴 것이다. 아니! 그렇게 말하면 색시가 서운해하지 않느냐고? 걱정 마시라. 우리 색시는 옥션질하느라 워낙 바쁘셔서 이런 글 절대로 안 읽는다.

비트맵을 출력하는 절차는 나름대로 복잡하다. 먼저 출력할 비트맵을 로드해야 한다. LoadBitmap 함수로 문자열 또는 정수형의 ID를 전달하는데 리소스 편집기가 리소스를 정수로 관리하므로 우리는 IDB_BITMAP1이라는 매크로 상수를 전달하면 된다. LoadBitmap의 인수 타입은 다음과 같이 정의되어 있다.

 

class _U_STRINGorID

{

public:

     _U_STRINGorID(LPCTSTR lpString) : m_lpstr(lpString)

     { }

     _U_STRINGorID(UINT nID) : m_lpstr(MAKEINTRESOURCE(nID))

     { }

     LPCTSTR m_lpstr;

};

 

개념적인 공용체라고 할 수 있는데 문자열 또는 정수를 래핑하되 정수가 전달되면 문자열로 캐스팅하여 저장한다. 이 클래스가 정수 ID를 문자열로 바꿔 놓으므로 MAKEINTRESOURCE 매크로를 일일이 쓰지 않아도 된다. 짧지만 아주 유용한 클래스라고 할 수 있다.

메모리 DC 만들어 로드한 비트맵을 선택해 놓고 비트맵의 크기를 조사한다. 그리고 BitBlt 함수를 호출하여 화면으로 전송한다. 출력한 후에도 메모리 DC를 해제하고 비트맵을 파괴해야 하는데 다행히 이 동작은 파괴자가 알아서 처리한다. 속도 문제로 인해 비트맵 출력 절차는 Win32나 MFC나 여전히 불편하다. 하드웨어 환경이 좋아진만큼 좀 더 편리하게 쓸 수 있는 래퍼 함수를 제공해 주면 좋을텐데 아쉽다.

아이콘, 커서, 문자열, 대화상자 등의 리소스를 작성 및 활용하는 방법도 MFC와 거의 동일하다. 대화상자에 대해서는 다음 절에서 따로 실습을 해 볼 것이다.

.유틸리티 클래스

Win32는 좌표, 크기, 사각형 등을 POINT, SIZE, RECT 등의 구조체로 제공한다. MFC와 WTL은 편의성을 높이기 위해 이 구조체들을 CPoint, CSize, CRect 등의 클래스로 래핑해 놓았다. 이 외에 문자열을 관리하는 CString 클래스도 제공되어 가변 길이의 문자 배열을 제공한다. 이 클래스들을 사용하면 코드가 조금 더 짧아지고 안전성도 높아진다. 다음 코드는 사각 영역에 긴 문자열을 워드랩하여 출력한다.

 

#include <atlmisc.h>

LRESULT CGDIObjectView::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

     CPaintDC dc(m_hWnd);

 

     CRect rt;

     rt.SetRect(10,10,250,100);

     CString str=TEXT("청산은 나를 보고 말없이 살라 하고")

          TEXT("창공은 나를 잡고 티없이 살라 하네 사랑도 벗어 놓고 미움도 벗어 놓고")

          TEXT("물같이 바람같이 살다가 가라 하네");

     dc.DrawText(str,-1,&rt,DT_WORDBREAK);

     return 0;

}

 

CRect, CString, CBrush 등은 아주 친숙하지만 바로는 쓸 수 없고 이 클래스들이 정의되어 있는 atlmisc.h 헤더 파일을 인클루드해야 한다. 편의상 OnPaint 바로 위에서 인클루드했는데 stdafx.h에 인클루드하는 게 정석이다. 사각영역에 문자열이 자동 개행되어 출력될 것이다.

MFC에도 동일한 이름의 유틸리티 클래스들이 있는데 MFC 프로젝트에서 WTL의 클래스 대신 MFC의 클래스를 쓰고 싶다면 다음 매크로를 정의하면 된다. 클래스 선언문이 이 매크로에 따라 조건부 컴파일된다.

 

_WTL_NO_WTYPES : CSize, CPoint, CRect 정의하지 않음

_WTL_NO_CSTRING : CString 정의하지 않음

 

이 클래스들은 똑같은 이름으로 MFC에도 있고 WTL에도 있으므로 MFC로 프로젝트를 할 때는 굳이 양쪽을 다 사용할 필요가 없다.