라벨, 에디트, 리스트 박스, 콤보 박스, 일반 버튼, 라디오/체크 박스.
이름만 들어도 친숙한 이 물건들은 응용 프로그램의 대화상자에 들어가는 그야말로 필수 GUI 구성요소들이다.

여기에다 리스트 컨트롤, 트리 컨트롤 같은 더 고급스러운 공용 컨트롤까지 있으니 실무에서는 사실 99%에 가까운 일은 기존 컨트롤만으로 다 해치울 수 있다.
사소하게 마음에 안 들고 동작 방식을 좀 고쳐야 하는 건, (1) 이들이 제공하는 owner-draw 옵션(주로 외형 외주) 내지 일부 메시지에 대한 (2) 윈도우 프로시저의 서브클래싱으로 customize 가능하다.

그러나 아주 가끔은, 기존 컨트롤과는 동작 방식이 완전히 다른 새로운 윈도우를 내가 머리부터 발끝까지 직접 구현하여 대화상자에다 집어넣어야 하는 경우가 있다. 기존 컨트롤을 약간만 고치는 정도로는 성이 안 찬다.

본인이 개발한 <날개셋> 한글 입력기의 GUI를 보면 이런 custom 컨트롤들이 적지 않게 보인다. 당장 비트맵 글꼴을 사용하는 자체 에디트 컨트롤은 편집기의 클라이언트 영역뿐만 아니라 대화상자에서 입력란으로도 쓰인다. 그리고 한글 낱자를 뽑아 오는 독특한 스타일의 콤보 박스라든가 문자표 리스트, 글쇠배열 편집 윈도우도 다 자체 개발한 custom 컨트롤이다.

꼭 그렇게까지 특이한 용도의 윈도우가 아니더라도.. 이걸 생각해 보자.
자체적인 키보드 포커스를 받으며, 화살표 key나 마우스 휠로 2차원적인 스크롤이 가능한 형태로 그림 같은 owner-draw 컨텐츠를 출력하는 컨트롤.
쉽게 말해 MFC로 치면 CScrollView에 해당하는 컨트롤이다. 특이할 것 없는 아주 기본적인 기능임에도 불구하고 기존 컨트롤의 서브클래싱만으로는 구현 가능하지 않다.

키보드 포커스를 받지 않으며 스크롤 같은 것도 없이 고정적인 owner draw 그림만을 화면에 달랑 그리는 거라면 쉽다. 기성 컨트롤인 STATIC 윈도우에다 owner draw 스타일을 줘서 WM_DRAWITEM 메시지를 처리하거나, 아예 서브클래싱을 해서 WM_PAINT 메시지를 통째로 가로채면 된다.

그러나 스스로 키보드와 마우스 입력을 처리하는 윈도우라면 독립된 형태로 자체 구현이 불가피하다. 여러 메시지들에 대한 동작을 인위로 고쳐야 하기 때문이다.
<날개셋> 편집기에서 화면 인쇄를 시켰을 때 화면 인쇄 결과를 출력하는 윈도우가 바로 이런 용도로 만들어진 custom control이다.

custom control을 MFC를 써서 개발한다면 응당 CWnd에서 파생된 새로운 클래스를 떠올리게 될 것이다. 이 C++ 클래스는 자신만의 윈도우 클래스 이름을 갖고 있을 것이고 자신을 운영체제에다 등록하는(RegisterClass 호출) 함수도 static 형태로 갖추고 있어야 할 것이다. (응용 프로그램이 실행 초기에 호출함)
그리고 대화상자 리소스에는 그 클래스 이름을 지정해 놓은 custom 컨트롤을 마음껏 생성해 놓는다.

그런데 대화상자의 컨트롤들은 내가 CWnd::Create를 호출하여 만드는 게 아니라 운영체제의 대화상자 관련 함수들이 CreateWindowEx에다가 윈도우 클래스 이름만 달랑 줘서 만들어진다. 이렇게 C++ 언어 체계의 통제를 받지 않고 밖에서 만들어진 HWND에다가 MFC 기반의 C++ 개체는 어떤 메커니즘을 통해 상호 연결시켜야 할까?

단순히 CWnd::FromHandle 함수 같은 걸로 임시 CWnd만 달랑 생성해서 연결하는 게 아니다. 이 custom 컨트롤은 구현체 클래스가 존재하니, 그냥 CWnd가 아니라 내가 만든 전용 CWnd 파생 클래스가 정확히 연결돼야 한다. 그렇게 해야 응용 프로그램 해당 윈도우끼리는 type-safe하지 않고 불편한 메시지를 쓸 필요 없이 C++ 멤버 함수/변수에 직통으로 가능해지니 좋다.

연결하는 방법은 크게 두 가지가 있다. 먼저, C++ 클래스의 생명 주기를 윈도우 자체의 생명 주기와 일치시키는 것이다. 만드는 윈도우 클래스에다가 윈도우 프로시저를 아래와 같이.. MFC의 AfxWndProc와 거의 똑같은 형태로 작성해 준다.

LRESULT CALLBACK CHeapCtrl::_MyWndProc(HWND hWnd, UINT msg, WPARAM w, LPARAM l)
{
    CWnd *pWnd = CWnd::FromHandlePermanent(hWnd);
    if (pWnd==NULL) { pWnd=new CHeapCtrl; pWnd->Attach(hWnd); }
    return AfxCallWndProc(pWnd, hWnd, msg, w, l);
}

이 개체는 언제나 new 연산자를 통해 heap에만 생성된다. 그러므로 PostNcDestroy 함수를 다음과 같이 구현하여 윈도우와 함께 C++ 개체도 같이 소멸되게 해 줘야 한다.

void CHeapCtrl::PostNcDestroy()
{
    delete this;
}

또한 대화상자 내부에서는 CHeapCtrl을 언제나 포인터를 통해 접근해야 한다.

CHeapCtrl *p = DYNAMIC_DOWNCAST(CHeapCtrl, GetDlgItem(IDC_MYCONTROL));

둘째 방법은.. 대화상자이니까 가능한 더 간단한 방법이다. 대화상자 클래스에다가 해당 컨트롤 클래스의 개체를 선언한다. 윈도우 클래스를 등록할 때 WNDCLASS 구조체에다가 윈도우 프로시저는 그냥 쿨하게 DefWindowProc이라고 주면 된다.

CStackCtrl m_wndCustomCtrl;

그리고 윈도우와 C++ 개체를 그냥 이렇게 컨트롤 변수로 연결해 버린다.

void CMyDialog::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_MYCONTROL, m_wndCustomCtrl);
}

이 경우, CStackCtrl은 스택, 아니 최소한 대화상자 클래스와 동일한 메모리에 생성된다. 그래서 개체를 포인터를 거치지 않고 더 간편하게 접근할 수 있을 뿐만 아니라, 대화상자 같은 윈도우 개체가 사라진 뒤에도 C++ 객체가 갖고 있던 내부 정보에 접근을 할 수 있다. PostNcDestroy 처리가 필요하지 않음은 두 말할 나위도 없고. 여러 모로 더 속 편하다.

윈도우 클래스상으로 이 윈도우는 프로시저가 아무 특수한 일도 하지 않는 DefWindowProc로 지정되어 있지만, DDX_Control 함수는 윈도우 프로시저를 서브클래싱하고 저 윈도우의 핸들을 우리가 준 CStackCtrl에다가 연결해 준다. 그래서 각종 메시지가 발생했을 때 CStackCtrl의 메시지 맵에 등록된 핸들러 함수가 호출되는 것이다.

윈도우 클래스와 MFC 클래스를 연결한 건 시작일 뿐이고 본격적인 코딩은 지금부터이다. 그야말로 산 넘어 산이다.
WM_PAINT와 필요하다면 WM_SIZE를 처리해야 할 것이고, 키보드 포커스를 받는 물건으로 계획을 했으니 WM_GETDLGCODE를 잡아서 스크롤을 위해 최소한 DLGC_WANTARROWS 정도는 되돌려야 할 것이다. 다른 단축키 같은 걸 추가로 인식하려면 DLGC_WANTCHARS도 필요할 테고.

스크롤이 되는 윈도우라면 요즘은 마우스 휠 인식은 필수다. WM_MOUSEWHEEL을 처리해야 한다. SystemParametersInfo(SPI_GETWHEELSCROLLLINES, ...) 을 호출하여 마우스 휠의 움직임 단위를 감지하여 동작할 필요가 있다. 특히 n줄이냐, 아예 페이지 단위냐(WHEEL_PAGESCROLL)를 잘 판단해야 한다.

만드는 컨트롤에 확대/축소 배율이 존재한다면 Ctrl+휠로 배율 조절을 시키는 게 요즘 트렌드다.
또한, 전통적인 세로 스크롤용 마우스 휠뿐만이 아니라 가로 마우스 휠 WM_MOUSEHWHEEL (wheel 앞에 H 추가)이라는 게 있다는 것도 알아 두면 좋다. 마우스 현물보다는 손가락 제스처를 이용한 스크롤 기능이 있는 노트북 터치패드로 생성되는 듯하다.

어디 그 뿐이랴? 표준 인터페이스에는 아예 마우스 휠을 눌러서 구동하는 '자동 스크롤' 모드도 있다. WM_MBUTTONDOWN(마우스 가운데 버튼) 되시겠다. 이건 아까와는 반대로 마우스 실물이 아닌 노트북 터치패드로는 구경하기 힘든 모드이다.

.마우스로 화면을 끌어서 스크롤이 되게 하려면 WM_LBUTTONDOWN, WM_MOUSEMOVE 같은 메시지들을 처리하면 될 것이고.
아, 이런 것보다 더 중요하면서도 스크롤 윈도우에서 상당히 번거로운 작업이 하나 있는데 그건 바로 WM_HSCROLL 및 WM_VSCROLL 메시지이다. 우리의 편견과는 달리,처음에 SetScrollInfo 함수 하나로 전체 스크롤 크기와 영역만 지정해 준다고 해서 그 다음부터 모든 스크롤 처리가 자동으로 되는 게 아니기 때문이다.

스크롤 메시지는 사용자가 스크롤 바의 화살표(한 칸씩)를 눌렀을 때, 스크롤 바를 드래그하고 있을 때, 스크롤 바 옆을 눌렀을 때(한 페이지 씩) 등등의 상황별로 서로 다른 정보가 담긴 채로 전달된다. 그리고 이때 실제로 화면을 얼마만치 스크롤시키고 어떤 처리를 할지는 전적으로 해당 응용 프로그램에 달려 있다. 운영체제가 자동으로 해 주는 일은 없다. 이걸 처리하지 않으면 사용자가 스크롤 바를 눌러도 다른 반응이 발생하지 않는다.

바로 이런 특성 때문에 사용자가 스크롤 바를 끌고 있는 동안 화면이 바로 갱신될지, 혹은 곧장은 아니고 스크롤 바의 드래그가 끝난 뒤에야 화면을 갱신할지 같은 것도 응용 프로그램마다 달리 동작할 수 있다. 물론 반응성이 좋은 프로그램이라면 어지간하면 즉각 화면이 갱신되는 게 좋겠지만 말이다.

그리고 요즘은 화면 캡처 유틸리티들이 스크롤 캡처를 지원하는데, 이 역시 생각보다 꼼수를 써서 구현돼 있다. 화면에다 별도의 표식을 그려 넣은 뒤, 캡처 대상 윈도우에다 스크롤 메시지를 보내고 그 표시가 얼마나 이동했는지를 수동으로 점검한다. 위와 같은 높은 자유도로 인해, 저렇게 하지 않으면 그 스크롤 분량을 정량적으로 알아낼 수 없기 때문이다. (스크롤 바가 이동한 양과 그에 따라 지금 화면이 실제로 이동한 픽셀수 사이의 인과관계)

화면이 스크롤되면 ScrollWindowEx 함수를 호출해서 이미 그려진 화면은 운영체제가 제공하는 스크롤 기능으로 넘기고 새로 칠해져야 하는 최소한의 부분만 새로 칠하는 '평범한 프로그램'이라면.. 저 꼼수가 통한다.
그러나 스크롤 될 때마다 화면을 몽땅 지우고 새로 그리는 프로그램이라면, 저 표식도 지워지기 때문에 스크롤 캡처를 할 수 없게 된다.

이상이다. 윈도우의 스크롤 기능까지 얘기하다 보니 말이 또 길어졌다. 여기에도 뭔가 정량적인 동작 패턴이 분명 있는 듯한데, 그것만 추려내기는 쉽지 않아 보인다.
공통된 기능을 운영체제가 API 함수로든, 공용 컨트롤 윈도우로든 뭘로든 좀 제공을 하지 않는다면 역시나 프로그래머들이 비슷한 기능을 여전히 자체적으로 중복 구현할 수밖에 없을 것이다.

Posted by 사무엘

2014/07/22 08:36 2014/07/22 08:36
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/987

컴퓨터 화면에 그려진 어떤 물건(주로 사각형 모양)의 경계 테두리에다 마우스를 갖다대면 포인터가 그 경계 테두리와 수직인 방향의 화살표로 변한다. 그 상태에서 마우스를 클릭하여 끌면, 그 물건의 크기를 마우스 포인터가 움직이는 방향으로 변경할 수 있다.
이것은 GUI에서 매우 흔히 볼 수 있는 기능이다. 특히 크기 조절 가능한 창--운영체제가 정식으로 제공하는 GUI 구성요소--의 경우 이런 기능은 운영체제가 non-client 영역에서 알아서 자동으로 처리해 준다.

그런 창이야 운영체제가 처리를 알아서 해 주지만, 대화상자 내부의 임의의 영역에 대해서, 혹은 클라이언트 영역에 내가 그려 주는 임의의 객체에 대해서 이런 처리를 구현하려면 어떻해야 할까? 뭔가 공통된 패턴의 알고리즘을 처리해야 할 텐데 코딩량이 적지는 않으며 왠지 귀찮고 번거로워 보인다.

크기 조절 내지 화면 분할 UI와 관련해서는 다음과 같은 여러 상황을 생각할 수 있다.

1. 한 윈도우 내부에 그려지는 개체의 크기 조절

2차원 벡터 그래픽 프로그램 내지 RAD 툴의 폼 에디터가 정확하게 여기에 속한다. 요즘은 워드 프로세서도 자체적인 벡터 그래픽이나 하다못해 OLE 개체라도 취급하니 마우스를 이용한 크기 조절 기능을 제공해야 한다. 개체를 클릭하면 8군데의 앵커 사각형이 생기며, 경계 아무 곳이라기보다는 그 앵커 사각형을 드래그했을 때 크기 조절이 된다. 다른 곳을 드래그하면 크기 조절이 아니라 이동이 되고. 그리고 요즘 MS 오피스 제품은 회전용 앵커까지 덤으로 제공한다.

비주얼 C++에는 CRectTracker라는 고전적인 클래스가 있어서 마우스 드래그로 임의의 사각형 영역을 화면에다 그리고 마우스가 클릭됐을 때의 일체의 처리를 알아서 해 준다. 이것만 쓸 줄 알아도 상당히 편리한데, 본인은 지금까지 그런 분야의 프로그램을 개발할 일이 없다 보니 실제로 써 본 적은 전혀 없다. 이런 게 있다는 것만 안다.

그리고, 지금도 있나 모르겠다만, 비주얼 C++에는 DrawCli라고 딱 개체 기반 벡터 드로잉과 드래그 드롭, 크기 조절과 이동을 모두 시연해 놓은 걸출한 예제 프로그램이 있다.
학창 시절 나의 친구였던 <비주얼 C++ 완벽 가이드>(김 용성)에도 '트래커'라는 예제 프로그램이 있으니 참고할 것.

사용자 삽입 이미지

참고로, 저렇게 테두리를 그리고 사각형 안의 내용물은 대각선 사선으로 칠하는 건.. embed된 OLE 개체를 외부 프로그램이 수정 중일 때 클라이언트 프로그램이 표시하는 표준 모양이기도 하다.

사용자 삽입 이미지

2. 부모 윈도우가 자식 윈도우를 동적으로 분할하고 관리

규모깨나 좀 있는 문서 편집 프로그램을 보면, view를 분할하는 기능이 있다. 이것은 한 문서 컨텐츠에 대해 뷰 윈도우를 여러 개 둬서 한 컨텐츠를 여러 가지 다른 방식, 다른 위치를 표시할 수 있게 한다. 사용자의 입장에서 매우 편리한 기능이지만, 컨텐츠와 뷰 윈도우가 완전히 일심동체라고 전제하고 만들어져 버린 프로그램이라면 추후에 이러 기능을 추가하기가 쉽지 않을 것이다. MDI 프로그램이라면 아예 별도의 독립된 창을 만드는 기능도 있겠지만 SDI에서는 한 창을 분할하는 것만 가능하다.

이건.. 생각보다 만들기 어려운 기능이다. 경계(splitter) 부분을 마우스로 끌었을 때의 처리도 처리거니와--이를테면 XOR 연산으로 자취를 그렸다가 지우는 것도..--, 내가 무엇보다도 힘들겠다고 느끼는 건 스크롤 바를 자체적으로 따로 만들어서 관리하는 부분이다. 이게 무슨 뜻인지를 설명하자면 이렇다.

Windows 운영체제에는 어떤 윈도우가 자체적으로 스크롤 바를 가질 수 있고, 한편으로 스크롤 바 자체가 별도의 컨트롤로 존재할 수 있다.
윈도우가 자체적으로 갖는 native 스크롤 바는 당연히 운영체제가 모든 처리를 알아서 해 준다. 창의 크기가 바뀌어도 스크롤 바를 자동으로 우측(상하) 내지 하단(좌우)에 배치해 주고, 스크롤을 할 필요가 없어지면 알아서 스크롤 바가 없어지고 그 영역까지 클라이언트 영역이 확대된다.

그러나 splitter가 존재하는 view를 보면, 스크롤 바가 있던 자리의 구석 일부에 창을 분할시키는 앵커가 자리잡고 있다. 이건 native 스크롤 바로 구현 가능하지 않기 때문에 스크롤 바 컨트롤을 따로 만들어서 앵커 밑이나 옆에다 두고, 스크롤 바의 위치· 크기와 관련된 모든 처리를 수동으로 해야 한다. 이 얼마나 복잡하고 손이 많이 갈까? 그러니 MFC가 CSplitterWnd라는 클래스에다 전부 구현해 놨다.

사용자 삽입 이미지

MFC AppWizard에서 '뷰 분할 기능 사용'을 체크하면, 프레임 윈도우는 밑에다 저 splitter 윈도우를 생성하고 걔가 또 자기 밑에다 어떤 view를 생성할지를 따로 지정해 준다. 다시 말해 프레임 윈도우와 view 사이에 splitter라는 중간 계층 윈도우가 하나 또 생긴다는 것이다. 그리고 요놈이 스크롤 바와 앵커의 위치를 관리하고 앵커 드래그에 대한 처리도 담당한다.

사용자 삽입 이미지

내가 MFC의 splitter 윈도우에 대해 꽤 놀란 것은, 분할을 2개씩뿐만 아니라 그 이상도 얼마든지 할 수 있다는 점이다. 위의 그림처럼 가로 3개, 세로 3개를 분할해서 무려 9개나 되는 view 윈도우를 뻥튀기시킬 수도 있다. 가로와 세로의 splitter 중 어느 것 하나의 위치가 바뀌었을 때, 혹은 창 전체의 크기가 바뀌었을 때 splitter가 총체적으로 조율하고 해야 할 일의 양도 그에 비례해서 많아질 것이다.

그런데 각각의 창들이 다 독자적인 가로· 세로 스크롤 바를 갖는 건 아니고.. 위의 스크린샷에 보듯이 한 column에 해당하는 view들은 가로 스크롤 바를 하나 다같이 공유한다. 그리고 한 row에 해당하는 view들은 세로 스크롤 바를 다같이 공유한다. 특이한 점임.

3. 부모 윈도우가 자식 윈도우를 '정적'으로 분할하고 관리

위의 2번과 마찬가지로 부모 윈도우가 자식 윈도우의 분할을 관리하는 경우이긴 한데, 위처럼 성격이 비슷한 윈도우가 아니라 별개의 윈도우를 고정적으로 관리하는 경우를 추가적으로 생각할 수 있다. 처음엔 1개였다가 2개 이상으로 자유자재로 분할되는 건 아니기 때문에, 스크롤 바나 앵커 같은 건 없다.

왼쪽에 트리 컨트롤, 오른쪽에 리스트 컨트롤을 두고 가로로 크기 조절이 가능한 탐색기 같은 프로그램도 좋은 예이고, 그보다 좀 더 복잡한 경우로는 개발자들의 친구인 Dependency Walker가 있다.

사용자 삽입 이미지

Spy++ 같은 프로그램으로 들여다보면, 창 구조가 생각보다 복잡하다는 걸 알 수 있다.
가장 겉에 있는 창은 화면을 상하로 나눈다. 위에 있는 창은 화면을 또 좌우로 나눠서 왼쪽은 모듈 트리 구조가 나오며, 오른쪽은 또 상하로 나누어서 각각 이 모듈이 import하는 심벌들, 그리고 대상 모듈이 export 하는 전체 심벌들이 표시된다.
한편, 아래의 화면은 또 상하로 나뉘어서 위에는 전체 모듈 리스트가 있고 아래에는 메시지 log가 있다.

즉, 한 윈도우에 여러 개의 컨트롤들이 sibling 관계로 대등하게 늘어서 있는 게 아니라, 또 분할 윈도우가 있고 그 아래에 또 분할 윈도우가 자식 윈도우로 있는 형태다. 한 분할 윈도우는 언제나 좌우로든 상하로든 2개의 윈도우만을 담당한다.

이렇게 이분법적으로 접근하면, 제아무리 복잡하게 화면이 좌우 상하로 분할되어 있는 창이라 해도 전체 크기가 바뀌었다거나 할 때 자기가 맡은 두 개의 창만 비율 분배를 잘 하고 나머지는 자가반복적인 재귀 처리에 맡기면 되니 문제가 단순해진다는 장점이 있다.

저런 윈도우가 활용의 자유도가 더욱 올라간다면 아예 별도의 창으로 분리하거나 docking까지 가능해진다. Visual Studio의 각종 보조 윈도우들처럼 말이다. 그건 우리 같은 평범한 프로그래머가 밑바닥부터 할 짓이 못 되며, 이미 있는 GUI 라이브러리의 사용법을 익히는 것만으로도 충분할 것이다. MFC 없이 Windows API만으로 docking toolbar를 구현한 소스를 외국 사이트에서 본 적이 있는데, 가히 근성이 느껴졌다.

사용자 인터페이스라는 건 컴퓨터로 하여금 의미 있는 작업을 하게 만드는 실질적인 알고리즘이 아니다. 없던 걸 처음 시도하여 만드는 게 아닌 이상, 베끼기만 하느라 시간 낭비할 필요는 없을 것이다.

4. 대등한 위상의 윈도우끼리 분할 관리를 해야 하는 경우

어휴, 글을 이렇게 길게 쓸 생각은 없었는데... 마지막 아이템도 언급을 안 할 수가 없구나. -_-;;
자, 지금까지 얘기한 것들을 정리하자면 1번은 그냥 한 윈도우 내부에서 그리기를 하는 것뿐이며, 2번과 3번은 부모 윈도우가 자식 윈도우들의 공간 관리를 하는 경우를 특별히 MFC의 document-view 아키텍처의 예를 들어 소개한 것이다.

그러나 실무에서는 그것만이 전부가 아니다. 대화상자처럼 document-view 아키텍처가 적용되지 않는 창에 대해서도 수평/수직 splitter 같은 물건을 만들어야 할 때가 있다.
예를 들어, <날개셋> 제어판 같은 경우, '분야'를 나타내는 왼쪽의 트리 컨트롤과, 오른쪽의 여타 컨트롤들 사이에 수직 splitter를 둬서, 트리 컨트롤의 폭을 좀 더 넓힌다거나 반대로 좁히는 경우를 생각할 수 있다.
이 경우 splitter는 여타 컨트롤들과 마찬가지로 대화상자 안에 존재하는 여러 자식 윈도우의 하나일 뿐이지 나머지 컨트롤들을 모두 통솔하는 부모 윈도우의 지위는 아니게 된다. Spy++로 들여다보면, 가로나 세로로 길쭉한 고유한 splitter 윈도우가 잡히는 걸 볼 수 있다.

이런 상황에 대해서는 MFC는 딱히 제공해 주는 있는 클래스가 없다. codeguru 같은 데서 splitter dialog 정도로 검색해 보면 예제 소스나 관련 튜토리얼들이 쭉 나온다. 예전에 아주 괜찮은 코드를 하나 구해서 유용하게 쓴 적이 있었는데 지금 다시 검색하려니까 못 찾겠다.

이런 일을 하는 범용적인 클래스를 만들 때 염두에 둬야 하는 사항으로는, 좌우나 상하든 보장해 줘야 하는 최소 크기를 인자로 받아야 할 것이고, 좌우 상하 중 한쪽에다 뒀으면 하는 윈도우를 배열 같은 자료구조로 관리해야 한다. 윈도우 핸들이 아닌 ID로 받으면.. ID로부터 실제 핸들값(HWND)을 얻어야 하는 번거로움이 있지만, 그 컨트롤이 중간에 재생성된다거나 해도 여전히 식별이 가능하기 때문에 범용성이 좀 더 향상된다.

다른 splitter조차 자기의 크기 조절에 영향을 받게 하고 WM_SIZE 메시지에 반응한다면, 아까 Dependency Walker 같은 복잡다단 splitter도 얼마든지 구현 가능하다.
splitter를 구현하려면 당장 크기를 조절하는 것 처리는 둘째치고라도, 창의 크기가 바뀌었을 때 각 분할 화면들의 공간 배분을 어떻게 할지 같은 것도 생각해야 하니 여러 모로 골치가 아픈 건 사실이다.

끝으로 언급하고 싶은 이슈가 있다. 1~4번들은 다 마우스가 클릭되었을 때 캡처를 잡고 마우스 움직임을 추적하다가 버튼이 떼졌을 때 마무리 처리를 한다는 공통점이 있다.
이 경우, WM_LBUTTONDOWN, WM_MOUSEMOVE, WM_LBUTTONUP을 모두 메시지 맵에다 등록하고 각 상황별 코드를 메시지 핸들러 함수에다 제각기 따로 작성하는 방법을 생각할 수 있지만..

좀 더 능숙한 프로그래머라면, 그런 드래그 드롭 처리 정도면 WM_LBUTTONDOWN에다가 아예 별도의 message loop을 만들어서 거기에다 WM_MOUSEMOVE와 WM_LBUTTONUP에 해당하는 코드를 다 집어넣는 방법을 선택한다. 한 함수에다가 한 기능에 대한 처리를 몰아서 넣는 게 훨씬 더 깔끔하기 때문이다.

Posted by 사무엘

2014/07/02 08:32 2014/07/02 08:32
, ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/980

MFC 프로그래밍 잡설

1. IE 웹브라우저 윈도우 삽입

내 프로그램에다가 로컬이든 웹이든 HTML 페이지 내용을 표시해야 할 일이 생겼다. 이 경우 가장 간단한 해결책은 Internet Explorer 웹브라우저 윈도우를 삽입하는 것이다.

그런데 얘는 ActiveX 컨트롤이다. 흔히 웹페이지 내부에 들어가는 각종 ActiveX 컨트롤들이 웹 표준을 위배하고 사용자 접근성을 저해한다는 식으로 말이 많지만, 사실은 Window의 웹브라우저 자체부터가 ActiveX 형태로 제공되는 컴포넌트인 것이다. 그리고 이미 다들 아시겠지만, 플래시도 기술적으로는 ActiveX이다. 단지 이건 너무 전세계적으로 널리 퍼진 관계로 반쯤 웹 표준인 것처럼 인정받고 있을 뿐이다. (뭐, 이것도 HTML5의 등장으로 인해 지위가 좀 위태로워지긴 했지만)

어쨌든 이런 구조적인 차이로 인해, 웹브라우저 윈도우는, 리치 에디트 같은 여느 custom control과는 달리 CreateWindowEx 함수에다가 클래스 이름만 달랑 넘겨 준다고 선뜻 만들 수 있는 물건이 아니다.
MFC에서 ActiveX 컨트롤을 생성하는 코드를 보면 CWnd::CreateControl로 내려가는데, 내부 메커니즘은 각종 COM API가 동원되며 미치도록 복잡하다. 사실, 난 MFC의 도움 없이 API만으로 ActiveX 컨트롤을 생성해 본 적이 없으며, 요즘 같은 세상에 굳이 그래야 할 필요도 없을 것이다.

예전에 비주얼 C++에는 Component Gallery라는 게 있어서 (1) 스플래시 윈도우나 '알고 계십니까' 팁 대화상자처럼 몇몇 자주 쓰이는 MFC 클래스를 프로젝트에다 자동으로 등록해 주는 템플릿, (2) 그리고 특정 ActiveX 컨트롤에 대한 wrapper 클래스를 자동 생성해 주는 기능이 있었다. 6.0의 이후 버전부터는 그런 걸 못 본 것 같다.
(1)은 그렇다 쳐도 (2)는 해당 ActiveX 컨트롤의 type library를 참고하여 이 컨트롤을 생성하는 함수, 그리고 걔가 원래 제공하는 속성과 메소드들을 그대로 C++ 클래스 형태로 옮겨 주는 기능이다. CWnd의 파생 클래스인 것은 두 말할 나위도 없고.

Component Gallery가 없으니 요즘 (2)를 수행하려면 좀 우회 경로를 가야 한다. 대화상자를 하나 만든 뒤 거기서 우클릭하여 원하는 ActiveX 컨트롤을 삽입하고, 그걸 또 우클릭하여 클래스를 추가하면 된다.

다른 것도 아니고 IE 웹브라우저 윈도우는 굉장히 유명한 ActiveX 컨트롤인 관계로, 사실은 MFC에도 이미 전용 클래스가 준비되어 있다. 바로 CHtmlView 되시겠다. 이름에서 알 수 있듯 얘는 CWnd가 아닌 CView로부터 상속을 받아서 MFC의 view-document 아키텍처에 최적화되어 있다.
즉, 대화상자의 여느 컨트롤들과는 달리 스택이 아닌 heap에 생성되고, PostNcDestroy 함수에 delete this가 구현되어 있다. 그래서 대화상자 같은 데에서 간단히 사용하기에는 어려움이 있다. (뭐, 불가능한 건 아니다. 대화상자 위에다 아예 CView를 만들지 말라는 법도 없으니)

한편, CHtmlEditCtrl이라는 클래스도 있다.
IE 윈도우는 단순히 HTML을 표시만 하는 게 아니라 위지윅 HTML 편집기 기능도 갖추고 있다. 얘는 IE 윈도우를 viewer가 아닌 editor 모드로 열어 준다.
IE가 여러 모로 리치 에디트 컨트롤과도 경쟁 구도가 된 듯하다. 물론 리치 에디트가 훨씬 더 빠르고 가볍지만, 텍스트에다 서식을 입히는 데 RTF보다야 HTML이 압도적으로 더 유명한 대세가 된 건 부인할 수 없다. 그래서 도움말조차 RTF 기반인 재래식 HLP는 진작에 밀려 사라지기도 했고 말이다.

이 CHtmlEditCtrl은 CView가 아닌 CWnd 기반이다. 그래서 CDialog 파생 클래스에다가 멤버로 선언하여 대화상자의 child control로도 비교적 쉽게 사용할 수 있다. view 버전은 CHtmlEditView와 CHtmlEditDoc이 따로 있는 듯.

하지만 에디트 기능이 없는 일반 IE 윈도우를 CWnd를 기반으로 간단히 스택에다가 생성하는 건 여전히 MFC의 기존 클래스로 가능하지 않은 것 같다. 그래서 본인은 그냥 ActiveX 컨트롤 type library로부터 CWnd 파생 클래스를 추출한 후 그걸 사용하는 재래식 방법을 동원했다.

2. MFC 액셀러레이터 버그(?)

Windows API에는 메뉴 단축키를 자동으로 처리해 주는 액셀러레이터라는 게 있다. MFC에서는 CFrameWnd::LoadFrame 함수에서 자기 프레임 윈도우 ID값에 해당하는 액셀러레이터를 불러들인다.

그런데 거기에 있는 단축키를 좀 수정하고, 메뉴에다 새로운 기능을 추가하여 단축키도 액셀러레이터 테이블에다가 배당했는데, 아무리 수정을 해 줘도 새로운 단축키가 동작하질 않고 단축키가 예전 방식으로만 동작한다.
혹시 액셀러레이터 리소스가 잘못 빌드됐나 싶어서 빌드된 EXE 파일의 내부 리소스를 살펴보기도 했지만 딱히 이상이 없다.

그렇다고 해서 해당 리소스를 아예 지워 버리면 모든 단축키가 먹통이 된다. 그러나 리소스가 있으면 단축키가 있는 그대로 인식되지 않는다. 어찌 된 영문일까?

이것은 비주얼 C++ 2008 이후부터 도입된 일명 feature pack의 추가 기능 때문에 벌어지는 현상으로, 엄밀히 말해 버그는 아니다.
알다시피 MFC feature pack에서는 CWinApp, CFrameWnd 같은 전통적인 클래스에 Ex가 붙었고, MS Office처럼 프로그램의 모든 기능의 단축키를 customize하는 기능이 추가되었다. 그래서 한번 프로그램을 사용하고 나면, 그 뒤엔 프로그램이 리소스에 있는 액셀러레이터 테이블을 참조하는 게 아니라 레지스트리에 저장된 단축키를 따라 동작하게 된다. CKeyboardManager라는 클래스를 보신 적이 있을 것이다.

그렇기 때문에 프로그램 개발 과정에서 새로운 메뉴 명령이나 단축키가 추가되어 이를 테스트하고 싶다면, 프로그램을 실행한 후에 Customize 대화상자를 꺼내서 단축키를 reset시키면 된다. 아니면 해당 레지스트리를 수동으로 날리거나 레지스트리를 날리는 코드를 추가해 주면 된다. 이에 대한 자세한 정보는 구글링하면 다 나온다.

단축키와 도구모음줄을 싹 다 customize하는 기능이 필요할 정도로 규모가 방대한 프로그램을 개발할 일은 사실 그리 많지 않다.
그러니 그냥 옛날처럼 feature pack 기능을 사용하지 않는 아주 간단한 프로그램만 만들고 싶은데 요즘 MFC 마법사는 그냥 선택의 여지가 없이 Ex 클래스만 사용하여 코드를 생성해 주는 듯하다.

요즘은 MFC DLL은 이제 ansi 버전은 기본 배포조차 안 해 준다고 하지?
그나저나 (1) DLL의 덩치가 커져도 너무 커진 것, 그리고 확장팩이 그나마 MS Office나 Visual Studio의 UI를 정확하게 고증하여 재연한 것도 아니고 (2) 동작 방식이나 글꼴, 색상이 들쭉날쭉 차이가 나면서 짝퉁 티가 팍팍 나는 것을 생각하면...
MFC의 변화 양상에 대해서 본인은 불만이 좀 있다. -_-;;

예전에도 말했지만, (1)은 걍 운영체제의 내장 mfc42.dll을 직통으로 사용하는 classic legacy 모드 같은 거 좀 넣어 주면 안 되나 싶고,
(2)는.. 운영체제의 보급 메뉴 말고 싸제 메뉴가 흔히 저지르는 실수 하나만 좀 지적하고 넘어가겠다. 업계 관계자가 내 글을 보게 될 가능성은 별로 없지만..;;

메뉴가 튀어나왔을 때는 프로그램이 자체적으로 IME를 꺼야 한다. 그래서 한글 모드일 때도 Alt를 누르지 않고 그냥 누르는 메뉴 항목에 대한 단축키(액셀러레이터 키)가 먹혀야 한다. 그 글쇠가 안 먹히고 화면 한 구석에 ㅇ, ㅂ 같은 조합 윈도우가 튀어나오는 건 프로그램의 버그이다.
이것도 MS 오피스의 싸제 메뉴는 처리를 한 반면에, 요즘 MFC가 라이선스한 싸제 메뉴는 그런 처리도 안 돼 있다. 보면 볼수록 품질이 실망스럽다. 아니, Visual Studio조차도 MS Office 라이브러리가 아니라 WPF 기반으로 새로 제작된 2010 이후의 IDE는 메뉴에 저 버그가 존재한다.

<날개셋> 한글 입력기야 MFC를 사용하지 않고, 그나마 타자연습은 나온 지 10년도 더 된 구닥다리 Visual C++ 2003을 아직도 사용하며 빌드되고 있다. MFC의 배포 방식과 덩치 때문에 업그레이드를 할 처지가 못 돼서 말이다. 아니면 차라리 WTL 같은 더 가벼운 프레임워크로 갈아타야 되나 싶다.
위의 두 아이템들은 내 개인 프로젝트가 아니라 회사 일을 하면서 발견하고 느낀 것들을 글로 옮긴 것이다. 이것 말고도 기억에 남는 게 좀 있는데.. 마저 나열하면서 글을 맺도록 하겠다.

3. ShowWindow(SW_HIDE) 하니까 창이 없어져 버렸던 것. 동일한 영역의 창에 IE ActiveX 컨트롤과 여타 윈도우를 상황에 따라 교대로 보이거나 숨기는 UI를 만들 일이 있었다. 그런데 프로그램이 자꾸 이상하게 동작하고 assertion failure가 나기에 디버깅을 해 봤더니, 이게 웬걸, IE 윈도우를 ShowWindow(SW_HIDE)를 해서 숨기는 순간 컨트롤 자체가 완전히 파괴되고 m_hWnd 값이 NULL이 되는 것이었다.

검색을 해 보니 이것은 아주 잘 알려진 문제. 처음에 Create로 생성을 할 때 WS_VISIBLE가 지정되지 않았던 IE 컨트롤은 나중에 또 ShowWindow를 통해 숨겨질 때 내부 로직에 의해 destroy되어 버리는 모양이었다.
이 문제를 피해 가려면 그 윈도우에 대해서 MFC의 CWnd::ShowWindow를 호출하지 말고 그냥 Windows API 함수를 쓰면 된다고 한다. 내부 사정은 알 수 없는 노릇. 스레드를 사용할 때 이래로 MFC 클래스 대신 Windows API의 사용이 강제되는 또 다른 상황을 만났다.

4. 내 프로그램에다 삽입시킨 IE 컨트롤로 각종 자바스크립트를 사용하는 웹페이지에 접속을 하다 보면.. 스크립트 오류가 난다. gmail만 해도 로그인을 하고 나면 동일 증상을 확인할 수 있음.
이것은 IE가 보안 때문에 취한 조치인 듯하다.
HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION

요 key를 만들어서 그 밑에 이름은 "자기 프로그램.exe"이고 데이터는 10진수로 IE 버전 곱하기 1000 (=0이 3개 붙은)인 REG_DWORD를 집어넣어 주면 된다.

5. MFC 라이브러리와 표준 C++ 라이브러리를 같이 사용한 상태로 프로그램을 static link 형태로 빌드하고 나면..
operator new/delete가 중복 정의되었다고 링크 에러가 나는 경우가 있다. (DLL link는 상관 없음)
이 역시 구글링을 하면 정보가 곧바로 걸려 나올 정도로 잘 알려진 문제이다. 귀찮지만 라이브러리를 링크하는 순서를 좀 바꿔 주면 해결 가능하다. 구체적인 해결책은 지금 이 개인용 컴퓨터에 들어있지 않아서 설명을 생략하겠다. -_-

Posted by 사무엘

2014/05/20 08:18 2014/05/20 08:18
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/965

컴퓨터 소프트웨어의 GUI 요소 중에는 잘 알다시피 체크 박스와 라디오 박스가 있다.
전자는 n개의 항목을 제각각 복수 선택할 수 있기 때문에 선택의 가짓수가 2^n개가 가능하다.
그 반면 후자는 n개의 항목 중 하나만 선택할 수 있기 때문에 선택의 가짓수가 딱 n이 된다.

그리고 이런 개념은 사실 메뉴에도 존재한다.
메뉴 항목은 사용 가능 여부(enabled)와 더불어 체크 여부(checked)라는 상태가 존재하여, 자신이 체크된 것처럼 보이는 시각적 피드백을 줄 수 있다.

Windows는 초창기엔(=16비트 시절) 말 그대로 √ 1종류만이 존재했다. 이를 제어하는 함수는 CheckMenuItem이다.
그러다가 Windows 95/NT4에서부터는 ● 모양의 체크를 표시해 주는 CheckMenuRadioItem 함수도 추가되었다. 이로써 각각의 항목들을 따로 체크할 수 있는 메뉴와, 여러 개 중 한 모드만 선택할 수 있는 메뉴의 구분이 가능해졌다.
CheckMenuRadioItem는 특정 메뉴 항목 하나의 속성을 바꾸는 여타 함수들과는 달리, 메뉴 항목들을 여러 개 한꺼번에 지정한 뒤 하나만 체크를 하고 나머지는 체크를 모두 자동으로 해제하는 형태로 동작한다.

그런데 재미있는 것은, MFC는 95/NT4 이전의 16비트 시절에서부터 메뉴에다 custom 비트맵을 지정하는 독자적인 방식으로 라디오 박스를 자체 지원해 왔다는 점이다.
운영체제에 CheckMenuRadioItem가 추가된 뒤에도 내부적으로 그 함수를 쓰지 않는다. 이것은 비주얼 C++ 2012의 최신 MFC도 변함이 없다.

MFC는 동일한 명령 ID에 대해서 메뉴, 도구모음줄 등 여러 GUI 요소에 대해 일관되게 checkd/enabled 상태를 관리할 수 있게 이 계층만을 CCmdUI라는 클래스로 따로 뽑아 냈다. 그리고 윈도우 메시지의 처리가 끝난 idle 시점 때 모든 GUI 상태들을 업데이트한다.
MFC 소스를 보면, CCmdUI::SetCheck는 CheckMenuItem 함수를 호출하는 형태이다. 그러나 CCmdUI::SetRadio는 운영체제의 API를 쓰는 게 아니라 자체 생성한 bullet 모양 비트맵을 SetMenuItemBitmaps로 지정하는 좀 더 힘든 방법을 쓴다.

고전 테마를 포함해 심지어 Windows XP의 Luna에서도 운영체제가 그려 주는 radio 그림과 MFC가 그려 주는 radio 그림은 차이가 거의 없었다. 둘 다 그냥 글자와 동일한 모양으로 동그란 bullet을 그리는 게 전부였다. 그렇기 때문에 두 구현이 따로 노는 건 그리 문제될 게 없었다.

그러나 문제는 Vista 이후에서부터이다. 운영체제가 그리는 radio 그림은 더 알록달록해지고 배경까지 가미되어 화려해진 반면, MFC가 그리는 radio 그림은 아직까지 단색의 단조로운 bullet이 전부이다. 그래서 시각적으로 이질감이 커졌다. 그것도 일반 체크(√) 항목은 괜찮은데 라디오(●) 그림만 차이가 생긴 것이다.

사용자 삽입 이미지사용자 삽입 이미지

이해를 돕기 위해 그림을 첨부한다. Windows Vista 이후에 운영체제가 메뉴에다 그려 주는 라디오 체크는 배경에 은은한 무늬가 생겨 있다(왼쪽). 그러나 MFC가 그리는 라디오 체크는 여전히 옛날 스타일대로 단색 동그라미밖에 없으며, 일반 체크와도 형태가 다르다(오른쪽). 오른쪽의 프로그램은 본인이 예전에 MFC 기반으로 개발했던 오목 게임이다. ㅋㅋ

MFC는 운영체제의 새로운 함수를 왜 쓰지 않는 걸까?
그냥 이런 사소한 데에까지 신경을 안 써서 그런 것일 수도 있고, 또 CCmdUI는 각각의 메뉴 항목에 대해 개별적으로 호출되는 반면 CheckMenuRadioItem는 그 자체가 여러 메뉴 항목의 상태를 한꺼번에 바꾸는 함수이기 때문에 기능의 구현 형태가 서로 맞지 않아서 도입하지 않은 것일 수도 있다.

물론, SetMenuItemInfo라는 만능 함수를 쓰면, 개별적으로 라디오 체크 상태를 바꾸는 것도 불가능하지는 않다. 다만, 구조체를 준비해야 하는 데다, 상태(state)만 옵션으로 간단히 바꾸면 되는 게 아니라 메뉴의 유형(type)까지 바꿔야 하니 일이 좀 번거로운 건 사실이다.

다만, 요즘은 MFC에도 잘 알다시피 MS Office나 Visual Studio의 모양대로 GUI 외형을 싹 바꿔 주는 툴킷이 도입되었고, 이런 상태에서는 어차피 메뉴의 요소들이 무조건 모조리 자체적으로 그려진다. 그러니 저런 SetRadio와 SetCheck의 동작 방식의 차이 같은 것도 존재하지 않으며, 그런 걸 논하는 게 아무 의미가 없다. 저건 오로지 운영체제 표준 GUI를 쓸 때만 발생하는 이슈이기 때문이다. ^^

* 글을 맺으며..

WinMain 함수를 포함해 윈도우 클래스 등록, 프로시저 구현을 전부 직접 하면서 Windows용 응용 프로그램을 밑바닥부터 만들어 본 사람이라면, MFC가 내부적으로 프로그래머에게 몰래 해 주는 일이 얼마나 많은지를 어렴풋이 짐작할 수 있다.

  • 대화상자를 창의 가운데에다 배치해 주는 것,
  • 프레임 윈도우와 뷰 윈도우 사이의 경계에 깔끔한 입체 모양 테두리 넣는 것,
  • 고대비 모드일 때 도구 아이콘의 검은색을 흰색으로 바꾸는 것,
  • 심지어 콤보 박스 내부에 디폴트 데이터(리소스 에디터에서 만들어 넣었던)들을 집어넣는 것,
  • 프레임 윈도우가 키보드 포커스를 얻었을 때 그 아래의 view 윈도우로 포커스를 옮기는 것,
  • 프로퍼티 시트의 내부에 들어가는 프로퍼티 페이지들의 글꼴을 운영체제 시스템 글꼴로 바꾸는 것 등..

이런 사소한 것들도 공짜가 아니라 죄다 MFC가 내부에서 해 주는 일들이다.
Windows API만 써서 프로그램을 만드는 방식은 최고의 작고 가볍고 성능 좋은 프로그램을 만들 수 있지만 생산성도 미칠 듯한 저질이기 때문에, 인제 와서 이런 불편한 방식으로 프로그램을 만들 프로그래머는 거의 없을 것이다. 요즘 세상에 C++도 아닌 C는 사실상 어셈블리나 마찬가지다.

Posted by 사무엘

2013/04/29 08:34 2013/04/29 08:34
, ,
Response
No Trackback , 8 Comments
RSS :
http://moogi.new21.org/tc/rss/response/824

MFC와 View 오브젝트 이야기

1. 들어가는 말: MFC에 대한 큰 그림

MFC는 Windows API를 단순히 C++ 클래스 형태로 재포장만 한 게 아닌 독창적인 기능이 다음과 같이 최소한 세 가지 정도는 있다.

  • 가상 함수가 아니라 멤버 함수 포인터 테이블을 이용하여 메시지 핸들러를 연결시킨 메시지 맵. MFC 프로그래머 치고 BEGIN/END_MESSAGE_MAP()을 본 사람이 없다면 간첩일 것이다.
  • 운영체제가 제공하는 핸들 자료형들과 C++ 개체를 딱 일대일로 연결시키고, 특히 MFC가 자체적으로 생성하지 않은 핸들이라도 임시로 C++ 개체를 생성해서 연결했다가 나중에 idle time 때 자동으로 소멸을 시켜 주는 각종 handle map 관리자들. 절묘하다.
  • 20년도 더 전의 MFC 1.0 시절부터 있었던 특유의 document-view 아키텍처. 상당히 잘 만든 디자인이다.

양념으로 CPoint, CRect, CString 같은 클래스들도 편리한 물건이긴 하지만, 그건 너무 간단한 거니까 패스.

사실, MFC는 Windows API를 객체지향적으로 재해석하고 포장한 수준은 그리 높지 않다. 본디 API가 prototype이 구리게 설계되었으면, MFC도 해당 클래스의 멤버 함수도 똑같이 구린 prototype을 답습하고 내부 디테일을 그대로 노출했다.

이와 관련하여 내가 늘 드는 예가 하나 있다. 당시 경쟁작 라이브러리이던 볼랜드의 OWL은 radio button과 check button을 별도의 클래스로 분리했다. 그러나 MFC는 그렇게 하지 않았다. 운영체제 내부에서 둘은 똑같은 버튼 윈도우이고 스타일값만 다를 뿐이기 때문이다. 그러니 MFC로는 동일한 CButton이다. 그리고 CStatic도 마찬가지.
아마 기존 응용 프로그램의 포팅을 용이하게 하려고 의도적으로 이런 식으로 설계한 것 같긴 하지만, 이것 때문에 MFC를 비판하는 프로그래머도 물론 적지 않았던 게 사실이다.

그러나 인간이 하루 하루 숨만 쉬고 똥만 만드는 기계가 아니듯, MFC는 단순한 API 포장 껍데기가 아니라 다른 곳에서 더 수준 높은 존재감을 보여준다. 오늘 이 글에서는 document-view 아키텍처 쪽으로 얘기를 좀 해 보겠다.

2. view가 일반적인 윈도우와 다른 점

MFC는 뭔가 문서를 생성하여 작업하고 불러오거나 저장하는 일을 하는 업무용 프로그램을 만드는 일에 딱 최적화되어 있다. 그렇기 때문에 MFC AppWizard가 FM대로 생성해 주는 기본 코드는 아주 간단한 화면 데모 프로그램만 만들기에는 구조가 필요 이상으로 복잡하고 거추장스러워 보인다.
그냥 프레임 윈도우의 클라이언트 영역에다 바로 그림을 그려도 충분할 텐데 굳이 그 내부에 View라는 윈도우를 또 만들었다. 그리고 View는 Document 계층과 분리돼 있기 때문에, 화면에 그릴 컨텐츠는 따로 얻어 와야 한다.

이런 계층 구분은 소스 코드가 몇십~몇백만 줄에 달하는 전문적인 대형 소프트웨어를 개발할 걸 염두에 두고 장기적인 안목에서 해 놓은 것이다.
먼저, View와 Document를 구분해 놓은 덕분에, 동일한 Document를 여러 View가 자신만의 다양한 설정과 방법으로 화면에 동시에 표시하는 게 가능하다. 텍스트 에디터의 경우, 한 문서의 여러 지점을 여러 창에다 늘어놓고 수시로 왔다 갔다 하면서 편집할 수 있다. 한 창에서 텍스트를 고치면 수정분이 다른 창에도 다같이 반영되는 것이 백미.

일례로, MS 워드는 기본, 웹, 읽기, 인쇄, 개요 등 같은 문서를 완전히 다른 방식으로 렌더링하는 모드가 존재하지 않던가(물론, MS 워드가 MFC를 써서 개발됐다는 얘기는 아님). 게다가 이 중에 실제로 위지윅이 지원되고 장치 독립적인 레이아웃이 사용되는 모드는 인쇄 모드뿐이다. 인쇄를 제외한 다른 모드들은 인쇄 모드보다 문서를 훨씬 덜 정교하게 대충 렌더링하는 셈이다.

이렇듯, view는 그 자체만으로 독립성이 충분한 특성을 가진 계층임을 알 수 있다. view는 프레임 윈도우와도 분리되어 있는 덕분에, 한 프레임 윈도우 내부에 splitter를 통해 하위 view 윈도우가 여러 개 생성될 수도 있다.
CWnd의 파생 클래스인 CView는 윈도우 중에서도 바로 저런 용도로 쓰이는 윈도우를 나타내는 클래스이며, 부모 클래스보다 더 특화된 것은 크게 두 가지이다. 하나는 CDocument와의 연계이고 다른 하나는 화면 출력뿐만 아니라 인쇄와 관련된 기능이다.

SDI형 프로그램에서는 view 윈도우 자체는 계속 생성되어 있고 딸린 document만 수시로 바뀌기 때문에, document를 처음 출력할 때 view가 추가적인 초기화를 하라고 OnInitalUpdate라는 유용한 가상 함수가 호출된다. 그리고 화면 표시와 프린터 출력을 한꺼번에 하라고 WM_PAINT (OnPaint) 대신 OnDraw라는 가상 함수가 호출된다. 하지만 프린터 출력이 화면 출력과 기능면에서 같을 수는 없으니 CDC::IsPrinting이라든가 OnPrepareDC 같은 추가적인 함수도 갖고 있다.

그러고 보니 MFC의 view 클래스는 운영체제에 진짜 존재하는 '유사품' 메시지인 WM_PRINT 및 WM_PRINTCLIENT와는 어떻게 연계하여 동작하는지 모르겠다. 화면의 invalidate 영역과 긴밀하게 얽혀서 BeginPaint와 EndPaint 함수 호출을 동반해야 하는 WM_PAINT와는 달리, PRINT 메시지는 invalidate 영역과는 무관하게 그냥 창 내용 전체를 주어진 DC에다가 그리면 된다는 차이가 존재한다. 거의 쓰일 일이 없을 것 같은 메시지이지만, AnimateWindow 함수가 창 전환 효과를 위해 창 내용 이미지를 미리 내부 버퍼에다 저장해 놓을 때 꽤 유용하게 쓰인다.

3. CView의 파생 클래스들

MFC에는 CView에서 파생된 또 다른 클래스들이 있다. 유명한 파생 클래스 중 하나인 CCtrlView는 MFC가 자체 등록하는 클래스 말고 임의의 클래스에 속하는 윈도우를 그대로 view로 쓰게 해 준다.
그래서 운영체제의 시스템 컨트롤을 view로 사용하는 CTreeView, CListView, CEditView, CRichEditView 등등은 다 CCtrlView의 자식들이다.

  • 프로그램의 클라이언트 영역에다 CTreeView와 CListView를 splitter로 나란히 배열하면 '탐색기' 내지 레지스트리 편집기 같은 외형의 프로그램을 금세 만들 수 있다.
  • <날개셋> 편집기가 MFC를 써서 개발되던 버전 2.x 시절에는 문서 창을 CCtrlView로부터 상속받아 만들었다.

CCtrlView 말고 CView의 또 다른 메이저 파생 클래스로는 CScrollView가 있다. 얘는 이름에서 유추할 수 있듯, view에다가 스크롤과 관련된 기본 구현들이 들어있다. 텍스트 에디터 같은 줄 단위 묶음 스크롤 말고, 픽셀 단위로 컨텐츠의 스크롤이 필요한 일반 워드 프로세서, 그래픽 에디터 같은 프로그램의 view를 만들 때 매우 유용하다. 마우스 휠과 자동 스크롤 모드(휠 클릭) 처리도 다 기본 구현돼 있다.

인쇄 미리 보기 기능은 온몸으로 scroll view를 써 달라고 외치는 기능이나 다름없으며, 실제로 MFC가 내부적으로 구현해 놓은 '인쇄 미리 보기' view인 CPreviewView 클래스도 CScrollView의 자식이다.
단, 요즘은 Ctrl+휠을 굴렸을 때 확대/축소 기능도 구현하는 게 대세인데 배율까지 관리하는 건 이 클래스의 관할이 아닌 듯하다. 그건 사용자가 직접 구현해야 한다.

그럼 스크롤 가능한 view로는 오로지 자체 윈도우만 설정할 수 있느냐 하면 그렇지는 않다. CFormView는 대화상자를 view 형태로 집어넣은 클래스인데 그냥 CView가 아니라 CScrollView의 파생 클래스이다. 워낙 설정할 게 많아서 환경설정 대화상자 자체가 세로로 쭈욱 스크롤되는 프로그램은 여러분의 기억에 낯설지 않을 것이다.

옛날에 윈도우 3.x 시절의 PIF 편집기처럼 클라이언트 영역에 대화상자 스타일로 각종 설정을 입력 받는 게 많은 프로그램을 만들 때 CFormView는 대단히 편리하다. 대화상자는 여느 윈도우들과는 달리, 자식으로 추가된 컨트롤들에 대해 tab 키 순환과 Alt+단축키 처리가 메시지 처리 차원에서 추가되어 있다.

4. CScrollView 다루기

처음에는 CView로부터 상속받은 view를 만들어서 프로그램을 열심히 만들고 있다가, 뒤늦게 view에다가 스크롤 기능을 추가해야 할 필요가 생기는 경우가 종종 있다.
이미 수많은 프로그래밍 블로그에 해당 테크닉이 올라와 있듯, 이것은 대부분의 경우 base class를 CView에서 CScrollView로 문자적으로 일괄 치환하고 몇몇 추가적인 코드만 작성하면 금세 구현할 수 있다.

클래스 이름을 치환한 뒤 가장 먼저 해야 할 일은 스크롤의 기준이 될 이 view의 실제 크기를 SetScrollSizes 함수로 지정해 주는 것이다. OnInitialUpdate 타이밍 때 하면 된다. 안 해 주면 디버그 버전의 경우 아예 assertion failure가 난다.

여기까지만 하면 반은 먹고 들어간다. OnDraw 함수의 경우, 전달되는 pDC가 아예 스크롤 기준대로 좌표 이동이 되어 있다! 즉, 내부적으로 (30, 50) 위치에다가 점을 찍는 경우, 현재 스크롤 시작점이 (10, 20)으로 잡혀 있으면 화면상으로 이 위치만치 뺀 (20, 30)에 점이 찍힌다는 뜻이다. 내가 수동으로 스크롤 좌표 보정을 할 필요가 없다. 아, 이 얼마나 편리한가! invalid 영역의 좌표도 화면 기준이 아닌 내부 기준으로 다 이동된 채로 전달된다.

그러니 CView 시절에 짜 놓은 그리기 코드를 어지간하면 수정 없이 CScrollView에다 곧바로 써먹을 수 있다. 다만, 최적화만 좀 신경 써 주면 된다. 당장 화면에 표시되는 영역은 수백 픽셀에 불과한데 수천 픽셀짜리의 전체 그림을 몽땅 불필요하게 계산해서 그리는 루틴을 OnDraw에다 때려박지 않도록 주의해야 한다.
이때 유용한 함수는 RectVisible이다. 이 영역이 invalidate되었기 때문에 반드시 그려 줘야 하는지의 여부를 알 수 있다.

그 다음으로 신경을 좀 써야 하는 부분은 마우스 클릭이다.
마우스 좌표는 화면 기준으로 오지 내부 기준으로 오지는 않으므로, 내부 개체에 대한 hit test를 하려면 마우스 좌표에다가 GetScrollPosition(현재 스크롤 위치) 함수의 값을 더하면 된다.
그리고 화살표 키로 무슨 아이템을 골랐다면, 그 아이템의 영역이 지금의 화면 범위를 벗어났을 경우 스크롤을 시켜 줘야 한다. 수동 스크롤은 ScrollToPosition 함수로 하면 된다.

화면의 일부 영역을 다시 그리도록 invalidate하는 것도 스크롤 위치 반영이 아닌 그냥 지금 화면 기준의 좌표를 지정하면 된다. 그러면 OnDraw 함수에서는 스크롤 위치가 반영된 내부 좌표 기준으로 refresh 위치가 전달된다.

끝으로, 마우스로 어떤 개체나 텍스트를 눌러서 끌든, 혹은 단순 selection rectangle을 만들든 그 상태로 포인터가 화면 밖으로 나갔을 때, 타이머를 이용한 자동 스크롤도 구현해야 할 것이다. 이 역시 자동화하기에는 customization의 폭이 너무 넓기 때문에 MFC가 알아서 해 주는 건 없다. 알아서 구현할 것. 이 정도면 이제 스크롤 기능을 그럭저럭 넣었다고 볼 수 있을 것이다.

이 정도면 어지간한 개발 이슈들은 다 나온 것 같다.
참, 혹시 재래식 GDI API가 아니라 GDI+를 쓰고 있는 프로젝트라면 CScrollView로 갈아타는 걸 신중히 해야 할 것 같다. GDI+는 MFC가 맞춰 놓은 GDI 방식의 기본 스크롤 좌표를 무시하고 DC의 상태를 난장판으로 만들어 버리기 때문이다. GDI+는 재래식 GDI보다 느리지만 곡선의 안티앨리어싱과 알파 블렌딩이 뛰어나니 아무래도 종종 사용되게 마련인데..

간단한 해결책 중 하나는, GDI+ 그래픽은 CreateCompatibleDC / CreateCompatibleBitmap을 이용한 메모리 DC에다가 따로 그리고, 본디 화면에다가는 그 결과를 Bitblt로 뿌리기만 하는 것이다. 그렇게 하면 아무 문제가 발생하지 않고, 심지어는 속도도 내 체감상으로는 더 빨라지는 것 같다.

Posted by 사무엘

2013/03/13 19:34 2013/03/13 19:34
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/806

※ 윈도우 프로그래머라면 누구나 다 알 만한 내용에 대한 정리이다.
보면 아시겠지만 1~5까지 등장하는 기술들은 서로 동등한 차원의 관계에 있는 것들이 아니다.

1. 윈 API

kernel32, gdi32, user32를 주축으로 운영체제가 응용 프로그램에다 자신의 기능을 제공하는 가장 원초적인 매체이다. 우리에게 친근한 CreateWindowEx, DispatchMessage, CreateFile 등등등! 20년에 달하는 역사를 자랑하며, Windows라는 운영체제와 PC 데스크톱 애플리케이션이라는 영역 자체가 존속하는 한 결코 없어지지 않는다. 과거의 도스 API는 그냥 인터럽트 호출을 그대로 노출하던 반면, 윈도우 API는 C언어 함수 호출 형태를 근간으로 만들어져 있다.

2. MFC

윈 API만 쓰면 생산성이 크게 떨어지고 불편한 관계로, 1990년대 초에 응용 프로그램의 주 개발 언어가 C에서 C++로 넘어가던 시기에 기존 API를 C++ 라이브러리 형태로 적당히 wrapping하기 위해 이 물건이 개발되었다.
생성자와 소멸자, 오버로딩과 상속, message map 같은 것들 덕분에 생API보다야 개발 생산성이 크게 향상되는 건 사실이나, 이걸 제대로 쓰려면 윈 API도 알아야 되고 객체지향 이념과 MFC가 새로 도입된 개념까지 다 알아야 하기 때문에 초기 학습자의 부담이 커진다. 또한 MFC 자체가 부과하는 오버헤드도 만만찮다.

MS C 7.0의 다음 버전인 비주얼 C++ 1.0때부터 application frameworks라는 이름으로 존재하고 있었다. 16비트 시절부터 존재했으니 역사가 제법 길다.

3. COM

함수 호출 규약, 메모리 할당과 해제 방식, 문자열의 처리 방식, 특정 기능이 담겨 있는 객체를 식별하고 외부에 노출하는 방식 같은 아주 기본적인 바이너리 수준에서의 소프트웨어 컴포넌트 제조 규격을 범언어적으로 통일하는 스펙이다. 가령, 윈API가 DLL 로딩을 위해 전통적으로 지저분한 LoadLibrary(파일명), GetProcAddress나 import library 같은 저수준 방법을 썼다면, COM의 사고방식으로는 CoCreateInstance와 깔끔한 class ID만으로 끝인 것이다.

이건 1990년대 중반의 32비트 윈도우 이래로 도입되었다. 지금은 옛날보다야 중요도가 크게 떨어진 게 사실이지만 DirectX, 탐색기 셸, 드래그 드롭 같은 일부 분야의 API는 이 COM 방식으로 제공되기 때문에 프로그래머아면 COM의 개발 취지와 기본 개념 정도는 알 필요가 있다. 한편, MFC도 이런 COM 규격을 만족하는 컴포넌트를 새로 구현하는 데 쓰이는 공통 필수 기능을 지원한다.

4. GDI+

클래식 윈 API 중에서 GDI 계층을 계승하는 그래픽 라이브러리로, MS가 제공하는 API로는 드물게 C와 더불어 순수 C++ 기반으로 만들어졌다. 또한 사용하는 자료형이나 명칭들이 윈 API와는 완전히 다르며 서로 관련이 없다는 특징이 있다. 비록 GDI+는 기존 GDI보다 느리고 오버헤드가 크지만, 알파 블렌딩, 그러데이션 같은 최신 그래픽 카드를 활용하는 고급 그래픽 기능에 더욱 특화되어 있으며, 일부 그리기 기능은 반드시 GDI+만 써야 가능한 것도 있다.

가령, 안티앨리어싱이 적용된 글자를 찍는 건 재래식 GDI로도 가능하지만 안티앨리어싱이 적용된 선을 그리는 건 GDI+를 써야만 가능하다. 그리고 윈도우 비스타/7의 glass 영역에다가 알파 채널이 적용된 그림/글자를 제대로 그리는 것도 역시 GDI+로만 가능하다.

5. .NET

기계어가 아닌 바이트코드 가상 기계(common language runtime)를 기반으로 하면서, 운영체제 API를 객체지향 위주로 완전히 새로 설계한 윈도우 프로그래밍 플랫폼이다. 예전에는 비주얼 베이직이 얼추 이런 개발 환경을 지향하고 있었지만 닷넷은 그보다 스케일이 범언어적으로 훨씬 더 커졌다. .NET 환경에서의 주력 개발 언어인 C#은 최신 언어답게 디자인이 깔끔하고 빌드 생산성이 우수하다. 하지만 네이티브 기계어 프로그램만치 빠르거나 운영체제 내부를 세밀하게 지어하지는 못하며, 닷넷 프레임워크 위에서만 돌아갈 수 있다는 한계도 있다.

.NET에서는 기본 그래픽 API가 GDI+이다. 둘 다 윈도우 XP부터는 기본 내장이고, 윈도우 98부터 2000/ME까지는 운영체제에 배포판을 추가 설치해서 쓸 수는 있다. 다만, 윈95는 지원을 끊었다.
윈도우 8에서는 닷넷조차도 다른 언어와 플랫폼으로 대체되었는지 WinRT라는 플랫폼이 등장하며, C++ 언어도 C++/CX라고 대대적으로 칼질이 가해졌다. 이게 앞으로 6번으로 추가되어야 할 듯하다.

맥 OS는 운영체제의 API가 저런 식의 내력을 거친 게 있으려나 궁금하다. 코코아, 카본 같은 건 어느 위상에 속할까?

Posted by 사무엘

2013/01/03 08:38 2013/01/03 08:38
, , ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/778

문자의 집합인 문자열(string)은 어지간한 프로그래밍 언어들이 기본으로 제공해 주는 기본 중의 기본 자료형이지만, 그저 기초라고만 치부하기에는 처리하는 데 내부적으로 손이 많이 가기도 하는 자료형이다.

문자열은 그 특성상 배열 같은 복합(compound) 자료형의 성격이 다분하며, 별도의 가변적인 동적 메모리 관리가 필요하다. 또한 문자열을 어떤 형태로 메모리에 저장할지, 복사와 대입은 어떤 형태로 할지(값 내지 참조?) 같은 전략도 구현체에 따라서 의외로 다양하게 존재할 수 있다.

그래서 C 언어는 컴퓨터 자원이 열악하고 가난하던 어셈블리 시절의 최적화 덕후의 정신을 이어받아, 언어 차원에서 따로 문자열 타입을 제공하지 않았다. 그 대신 충분히 크게 잡은 문자의 배열과 이를 가리키는 포인터를 문자열로 간주했다. 그리고 코드값이 0인 문자가 문자열의 끝을 나타내게 했다.

그 이름도 유명한 null-terminated string이 여기서 유래되었다. 오늘날까지 쓰이는 역사적으로 뿌리가 깊은 운영체제들은 응당 어셈블리나 C 기반이기 때문에, 내부 API에서 다 이런 형태의 문자열을 사용한다.
그리고 파일 시스템도 이런 문자열을 사용한다. 오죽했으면 이를 위해 MAX_PATH (=260)같은 표준 문자열 길이 제약까지 있을 정도이니 말 다 했다. 그렇기 때문에 null-terminated string은 앞으로 결코 없어지지 않을 것이며 무시할 수도 없을 것이다.

딱히 문자열만을 위한 별도의 표식을 사용하지 않고 그저 0 문자를 문자열의 끝으로 간주하게 하는 방식은 매우 간단하고 성능면에서 효율적이다. 지극히 C스러운 발상이다. 그러나 이는 buffer overflow 보안 취약점의 근본 원인을 제공하기도 했다.

또한 이런 문자열은 태생적으로 문자열 자기 내부엔 0문자가 또 들어갈 수 없다는 제약도 있다. 하지만 어차피 사람이 사용하는 표시용 문자열에는 코드 번호가 공백(0x20)보다 작은 제어 문자들이 사실상 쓰이지 않기 때문에 이는 그리 심각한 제약은 아니다. 문자열은 어차피 문자의 배열과는 같지 않은 개념이기 때문이다.

문자열을 기본 자료형으로 제공하는 언어들은 대개 문자열을 포인터 형태로 표현하고, 그 포인터가 가리키는 메모리에는 처음에는 문자열의 길이가 들어있고 다음부터 실제 문자의 배열이 이어지는 형태로 구현했다. 그러니 문자열의 길이를 구하는 요청은 O(1) 상수 시간 만에 곧바로 수행된다. (C의 strlen 함수는 그렇지 않다)

그리고 문자열의 길이는 대개 machine word의 크기와 일치하는 범위이다. 다만, 과거에 파스칼은 이례적으로 문자열의 크기를 16비트도 아닌 겨우 8비트 크기로 저장해서 256자 이상의 문자열을 지정할 수 없다는 이상한 한계가 있었다. 더 긴 문자열을 저장하려면 다른 특수한 별도의 자료형을 써야 했다.

과거에 비주얼 베이직은 16비트 시절의 버전 3까지는 “포인터 → (문자열의 길이, 포인터) → 실제 문자열”로 사실상 실제 문자열에 접근하려면 포인터를 이중으로 참고하는 형태로 문자열을 구현했다. 어쩌면 VB의 전신인 도스용 QuickBasic도 문자열의 내부 구조가 그랬는지 모르겠다.

그러다가 마이크로소프트는 훗날 OLE와 COM이라는 기술 스펙을 제정하면서 문자열을 나타내는 표준 규격까지 제정했는데, COM 기반인 VB 4부터는 문자열의 포맷도 그 방식대로 바꿨다.

일단 기본 문자 단위가 8비트이던 것이 16비트로 확장되었다. 마이크로소프트는 자기네 개발 환경에서 ANSI, wide string, 유니코드 같은 개념을 한데 싸잡아 뒤죽박죽으로 재정의한 것 때문에 문자 코드 개념을 좀 아는 사람들한테서 많이 까이고 있긴 하다. 뭐, 재해석하자면 유니코드 UTF16에 더 가깝게 바뀐 셈이다.

OLE 문자열은 일단 겉보기로는 null-terminated wide string을 가리키는 포인터와 완전히 호환된다. 하지만 그 메모리는 OLE의 표준 메모리 할당 함수로만 할당되고 해제된다. (아마 CoTaskMemAlloc) 그리고 포인터가 가리키는 메모리의 앞에는 문자열의 길이가 32비트 정수 형태로 또 들어있기 때문에 문자열 자체가 또 0문자를 포함하고 있을 수 있다.

그리고 문자열의 진짜 끝부분에는 0문자가 1개가 아니라 2개 들어있다. 윈도우 운영체제는 여러 개의 문자열을 tokenize할 때 double null-termination이라는 희대의 괴상한 개념을 종종 사용하기 때문에, 이 관행과도 호환성을 맞추기 위해서이다.

2중 0문자는 레지스트리의 multi-string 포맷에서도 쓰이고, 또 파일 열기/저장 공용 대화상자가 사용하는 확장자 필터에서도 쓰인다. MFC는 프로그래머의 편의를 위해 '|'(bar)도 받아 주지만, 운영체제에다 전달을 할 때는 그걸 다시 0문자로 바꾼다. ^^;;;

요컨대 이런 OLE 표준 문자열을 가리키는 포인터가 바로 그 이름도 유명한 BSTR이다. 모든 BSTR은 (L)PCWSTR과 호환된다. 그러나 PCWSTR은 스택이든 힙이든 아무 메모리나 가리킬 수 있기 때문에 그게 곧 BSTR이라고 간주할 수는 없다. 관계를 알겠는가? BSTR은 SysAllocString 함수를 통해 생성되고 SysFreeString 함수를 통해 해제된다.

'내 문서', '프로그램 파일' 등 운영체제가 특수한 용도로 예정하여 사용하는 디렉터리를 구하는 함수로 SHGetSpecialFolderPath가 있다. 이 함수는 MAX_PATH만치 확보된 메모리 공간을 가리키는 문자 포인터를 입력으로 받았으며, 특수 폴더들을 CSIDL이라고 불리는 일종의 정수값으로 식별했다.

그러나 윈도우 비스타에서 추가된 SHGetKnownFolderPath는 폴더들을 128비트짜리 GUID로 식별하며, 문자열도 아예 포인터의 포인터 형태로 받는다. 21세기에 도입된 API답게, 이 함수가 그냥 메모리를 따로 할당하여 가변 길이의 문자열을 되돌려 준다는 뜻이다. 260자 제한이 없어진 것은 좋지만, 이 함수가 돌려 준 메모리는 사용자가 따로 CoTaskMemFree로 해제를 해 줘야 한다. SysFreeString이 아님. 메모리만 COM 표준 함수로 할당했을 뿐이지, BSTR이 돌아오는 게 아닌 것도 주목할 만한 점이다.

예전에 FormatMessage 함수도 FORMAT_MESSAGE_ALLOCATE_BUFFER 플래그를 주면 자체적으로 메모리가 할당된 문자열의 포인터를 되돌리게 할 수 있는데, 이놈은 윈도우 NT 3.x 시절부터 있었던 함수이다 보니, 받은 포인터를 LocalFree로 해제하게 되어 있다.

이렇게 운영체제 API 차원에서 메모리를 할당하여 만들어 주는 문자열 말고, 프로그래밍 언어가 제공하는 문자열은 메모리 관리에 대한 센스가 추가되어 있다. 대표적인 예로 MFC 라이브러리의 CString이 있다.

CString 자체는 BSTR과 마찬가지로 언뜻 보기에 PCWSTR 포인터 하나만 멤버로 달랑 갖고 있다. 그래서 심지어 printf 같은 문자열 format 함수에다가 "%s", str처럼 개체를 명시적인 형변환 없이 바로 넘겨 줘도 괜찮다(권장되는 프로그래밍 스타일은 못 되지만).

그런데 그 포인터의 앞에 있는 것이 단순히 문자열 길이 말고도 더 있다. 바로 레퍼런스 카운트와 메모리 할당 크기. 그래서 문자열이 단순 대입이나 복사 생성만 될 경우, 그 개체는 동일한 메모리를 가리키면서 레퍼런스 카운트만 올렸다가, 값이 변경되어야 할 때만 실제 값 복사가 일어난다. 이것을 일명 copy-on-modify 테크닉이라고 하는데, MFC 4.0부터 도입되어 오늘날에 이르고 있다. 이는 상당히 똑똑한 정책이기 때문에 이것만 있어도 별도로 r-value 참조자 대입 최적화가 없어도 될 정도이다.

메모리 할당 크기는 문자열에 대해 덧셈 같은 연산을 수행할 때 메모리 재할당이 필요한지를 판단하기 위해 쓰이는 정보이다. MFC는 표준 C 라이브러리에 의존적이기 때문에 이때는 응당 malloc/free가 쓰인다. 재할당 단위는 보통 예전에 비해 배수 단위로 기하급수적으로 더 커진다.

CString이 그냥 포인터와 크기가 같은 반면, 표준 C++ 라이브러리에 존재하는 string 클래스는 비주얼 C++ 2010 x86 기준 개체 하나의 크기가 28바이트나 된다. 길이가 16 이하인 짧은 문자열은 그냥 자체 배열에다 담고, 그보다 긴 문자열을 담을 때만 메모리를 할당하는 테크닉을 쓰기 때문이다. 그리고 대입이나 복사를 할 때마다 CString 같은 reference counting을 하지 않고, 일일이 메모리 재할당과 값 복사를 한다.

글을 맺겠다.
C/C++이 까이는 여러 이유 중 하나는 라이브러리가 지저분하고 동일 기능의 중복 구현이 너무 많아서 혼란스럽다는 점이다. 문자열도 그 범주에 정확하게 속하는 요소일 것이다. 메모리 할당과 해제 자체부터가 구현체 중복이 한둘이 아니니... 어지간히 덩치와 규모가 있는 프레임워크 라이브러리는 그냥 자신만의 문자열 클래스 구현체를 갖고 있는 게 이상한 일이 아니다. 하지만 그건 C/C++이 쓰기 편리한 고급 언어와 시스템 최적화 오덕질이라는 두 토끼를 모두 잡으려다 어쩔 수 없이 그리 된 것도 강하다.

문자열에 대한 이야기 중에서 일부는 내가 예전 블로그 포스트에서도 한 것도 있지만, 이번 글에 처음으로 언급한 내용도 많을 것이다. 프로그래밍 언어 중에는 문자열을 다루기가 기가 막히게 편리한 것이 있는데, 그런 것도 내부적으로는 다 결국은 컴퓨터가 무진장 고생해서 결과물을 만들어 내는 것이다.
컴퓨터가 받아들이고 뱉어내는 문자열들이 내부적으로 어떤 구현체에 의해 어떤 처리를 거치는지를 생각해 보는 것도 프로그래머로서는 의미 있는 일일 것이다.

Posted by 사무엘

2012/10/13 08:26 2012/10/13 08:26
, , , ,
Response
No Trackback , 8 Comments
RSS :
http://moogi.new21.org/tc/rss/response/743

비주얼 C++ MFC의 역사 -- 下

현재 윈도우 운영체제의 GUI는 잘 알다시피 윈 2000/ME까지 유지되던 고전 테마, 그리고 XP 테마와 비스타/7 테마가 서로 따로 논다.
MS 오피스도 XP/2003/2007이 따로 놀고,
거기에다가 비주얼 개발툴의 디자인도 2005/2008/2010이 다 달라진다.

어디 그뿐인가. 현재 운영체제의 configuration에 따라 테마가 나타나는 모양도 달라져야 한다. 가령, 오피스 2003은 전반적으로 파란 색상이지만 운영체제가 회색 고전 테마일 때는 은색이 깔림. 그 반면 오피스 2007은 독자적인 배색 컨셉이기 때문에 운영체제의 상태에 관계 없이 여전히 은은한 하늘색 색상이 되는 게 맞다.

이런 세세한 알고리즘은 reverse engineering이라도 해서 알아내야 할 텐데, GUI 툴킷 만드는 일은 정말 장난 아니게 어려울 것 같다. 아래아한글의 스킨을 개발하는 팀도 고생 무지하게 했을 거라는 생각이 든다. 차라리 운영체제 GUI를 완전히 생까던 9x  시절이나, 반대로 잠시 100% 운영체제 표준 GUI만 따르던 워디안 시절이 나았겠다.

자, 이렇게 GUI 비주얼들이 난립하고 기존 MFC는 이런 수요를 충족하지 못한 채 시간만 자꾸 흐르고 있었다. 그러던 차에 MS는 결단을 내렸다. 바로 비주얼 C++ 2008 플러스 팩. MFC42 시절 이래로 MFC에 대대적으로 기능을 추가하여 역대 오피스와 비주얼 툴들의 UI, 그리고 심지어 리본 UI까지 MFC에다 넣어 줬다.

MFC의 새단장 소식이 전해지던 당시, 많은 개발자들은 MS가 자기네 오피스 팀이 개발한 오리지널 GUI 소스를 리팩터링해서 전해 줄지 무척 기대했었다. 하지만 아쉽게도 그건 아니고, 이미 BCG라는 러시아 회사에서 개발하여 판매 중이던 제3자 라이브러리를 MS라는 브랜드만 넣어서 제공하는 형태가 되었다. MS의 GUI는 MFC를 써서 개발된 것도 아니고, 모듈 구조를 보아하니 외부에 선뜻 떼어 줄 만한 상태가 아닌 모양이다.

이 작업의 결과로 MFC DLL은 덩치가 정말 크고 아름다워져서, 1MB 초반이던 게 4MB를 훌쩍 넘어갔다. CWinApp, CFrameWnd는 새로운 GUI 기능과의 연동을 위해, Ex라는 접미사가 붙은 CWinAppEx, 그리고 CFrameWndEx라는 파생 클래스가 추가되었다.

그런데, MS가 제3자 중에서도 왜 하필 BCG 라이브러리를 선택했는지에 대해서 불만을 제기하는 사람이 있다. 김 민장 님이 굉장히 옛날에 쓰신 개념글을 하나 읽어보자.

일단, 한눈에 BCG 제품의 메뉴 폰트가 한 픽셀 작음을 알 수 있다. 메뉴뿐만 아니라 툴바 아이콘도 가로 폭이 1픽셀 작은 것 같고, "Solution Explorer"의 글씨도 작다. (중략) 데모 프로그램 다운 받고 단 3분 동안 들여다 봤는데도 벌써 이상한 점 서너 개가 나왔다. 그러니 BCG 제품을 믿지 못하겠고 사용하기가 꺼려지는 것이다.


특히 메뉴의 글씨의 크기가 작은 것은 비주얼 C++ 2008/2010에서까지 그대로 이어져 오고 있는 걸 본인 역시 확인했다. 이건 꽤 심각한 문제인데? (왼쪽이 good, 오른쪽이 bad)

사용자 삽입 이미지사용자 삽입 이미지
도대체 왜 이렇게 동작하는 걸까? 궁금해서 MFC의 소스를 들여다봤다. 내가 발견한 문제점은 두 가지이다.

afxglobals.cpp를 보면 MFC의 GUI 구성요소들이 한데 공유하는 GDI 오브젝트들이 한데 모여 있으며, 그 중에는 글꼴 오브젝트도 응당 있다. 그런데 여기 생성자를 보면,

m_bUseSystemFont = FALSE;

로만 되어 있고, SystemFont라고 MFC 소스 전체를 검색해 봐도 이 값을 바꾸는 멤버 함수 같은 건 어디에도 나오지 않는다. 메뉴의 글씨체는 운영체제의 시스템 글꼴로 나오는 게 일반인데(한글 윈도우의 고전 테마에서는 그냥 ‘굴림’처럼), 저게 TRUE가 아닌 FALSE인 한은 운영체제 한글 윈도우 고전 테마 + 오피스 구버전 테마 모드에서도 메뉴의 글꼴은 Segoe 내지 Tahoma로 자기의 독자적인 글꼴로 출력된다.

그리고 더 큰 문제는 UpdateFonts 함수에 있다. Adjust font size라는 주석 하에

if (nFontHeight <= 12)
    nFontHeight = 11;

라는 글꼴 보정 코드가 있다. 그런데 이 잘못된 보정 때문에 BCG 툴킷이 정상보다 글씨가 작게 찍힌다. 도대체 이 코드가 왜 들어갔는지 모르겠다. 12픽셀이던 걸 11픽셀로 줄이면, 한글· 한자가 가장 잘 찍히는 9포인트보다 글자 크기가 한 단계 더 작아져 버린다. 아마 영문 오피스 2007의 글꼴이 여타 비주얼에도 획일적으로 적용되는 듯하다.

한눈에 봐도 MS 원본 프로그램과는 UI가 다르게 찍히는 GUI 툴킷을 최소한의 보정도 없이 MS가 자기 이름으로 어떻게 MFC에다 내장시켜서 내놓은 걸까? 물론 이런 시도조차 없는 것보다는 낫지만, 역시 네이티브 C++ 개발자로서 꽤 안타까운 일이다. 차라리 제3자 라이브러리 시절에는 소스를 고쳐서 라이브러리를 새로 빌드할 수라도 있지만, 그 거대한 MFC의 내부에 내장이 되어 버린 코드는 고칠 수도 없는 노릇이고.

김 민장 님의 블로그에 나와 있듯, BCG보다는 Codejock이라는 미국 회사에서 개발한 Extreme Toolkit이라는 GUI 라이브러리가 원본과의 고증(?)이 더 잘 돼 있고 품질이 좋다고 그런다.

이렇듯, MS는 자기네가 처음으로 구현한 기능을(뭐, 오피스 팀이니, VS 팀과는 다른 부서이지만) 외부 회사로부터 구현체를 사 오는 기묘한 방법으로 집어넣어서 MFC를 확장했다. 일단 MS 내부에서 MFC를 자기네 프로그램의 개발에 거의 안 쓴다는 점을 염두에 둘 필요가 있겠다. -_-;; 진짜로 워드패드와 그림판을 빼고는 거의 없다.

본인 역시 일단 중요 제품인 <날개셋> 한글 입력기를 MFC 없이 순수 API + 간단한 싸제 라이브러리만으로 개발하기 시작했으니, MFC와는 직접적으로 결별했다. 하지만 C++과 윈도우 플랫폼이 살아 있는 한, MFC의 중요성과 의의는 그리 금방 격하되지 않을 것이다.

다만, 덩치가 커져도 너무 커져 버린 건 진짜로 아쉬운 점이다. 그렇게 살이 뒤룩뒤룩 찐 네이티브 환경을 쓰느니, 차라리 훨씬 더 생산성 좋은 닷넷이 그 역할을 조금씩 대체하고 있는 게 아닐까 싶다.
그리고 여담이지만, 윈도우, 오피스, VS는 앞으로도 또 비주얼이 완전히 확 바뀌는 일이 있을지 궁금하고 기대-_-되기도 한다. ^^

Posted by 사무엘

2012/03/29 09:10 2012/03/29 09:10
, ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/661

비주얼 C++ MFC의 역사 -- 上

옛날에 비주얼 C++의 역사에 대해서는 거창한 글을 쓴 적이 있는데 MFC 자체에 대해서는 의외로 블로그 개설 이래로 글을 쓴 적이 없었던 것 같다. 그래서 오늘은 이 주제로 한번 칼럼을 써 보겠다.

MFC는 잘 알다시피 C 언어 기반인 윈도우 API를 C++로 얇게 포장하는 한편으로, C++의 특성을 살려 더욱 생산성 있고 편리한 윈도우 응용 프로그램 개발을 목표로 만들어진 C++ 라이브러리이다. C 언어로 API만 쓸 때에 비해 네이티브 코드 프로그램 개발이 넘사벽급으로 훨씬 더 편리해진다는 건 부인할 수 없는 사실이다(특히 message map 덕분에! ㅋㅋ).

이것의 역사는 윈도우 3.x 시절이던 1992년 무렵으로 거슬러 올라간다. 이때는 MS의 주력 운영체제가 도스에서 윈도우로 바뀌고, 주 개발 언어가 C에서 C++로 이제 막 바뀌던 매우 중요한 과도기였다. 당시 볼랜드의 터보 C의 인지도에 압도적으로 밀리고 있긴 했지만, 마이크로소프트도 MS C라는 컴파일러를 개발해서 팔고 있었고, 이게 7.0 버전부터는 C++ 언어도 지원하기 시작했다.

MFC는 바로 MS C/C++ 7.0부터 도입되었으며, 그 다음부터는 제품명이 그 이름도 유명한 비주얼 C++ 1.0으로 바뀌게 되었다. 당시에는 라이브러리에 Microsoft Foundation Classes라는 거창한 정식 명칭이 없었기 때문에, 그냥 Application Frameworks라고 불렸다. 이것이 바로 AFX의 어원이다.

그래서 오늘날까지도 MFC의 핵심 인클루드 파일 이름은 mfc.h가 아니라 afx.h / afxwin.h이다. 또한 AfxGetApp(), AfxMessageBox() 같은 함수명과, AfxWnd##, AfxFrameOrView##처럼 MFC가 자체적으로 생성하는 윈도우 클래스 이름에도 AFX라는 약어를 찾을 수 있다.

비록 16비트 시절부터 존재하긴 했지만 MFC가 본격적으로 볼랜드 사의 컴파일러와 걔네들 라이브러리를 누르고 인기를 누리기 시작한 건 32비트로 넘어오면서이다. MFC가 첫 도입되었을 때는 C++ 개념을 구현하는 데 드는 특유의 오버헤드가 성능 면에서 문제가 되지 않을까 하는 회의적인 시각이 많았었다. 가령, code bloat으로 인한 용량 증가를 비롯해, 가상 함수 호출이라든가, 윈도우 핸들을 C++ 개체로 연결하기 위한 비용 같은 것 말이다.

워낙 옛날에, C++이 지금과 같은 규격으로 확장되기 전에 개발되던 것이다 보니 MFC는 RTTI를 자체적인 메커니즘으로 구현했으며, 컨테이너 클래스를 템플릿 없이 자체적으로 여러 구현체로 만든 게 있다. CPtrList, CObList 같은 것 말이다. MFC가 처음 개발되었을 때는 C++에 아직 템플릿이란 게 없었기 때문이다. 그리고 그 흔한 namespace조차 쓰지 않았다. 당연히 그때는 namespace가 없었기 때문.

MFC가 드디어 어느 정도 안정화를 이뤄 낸 것은 비주얼  C++ 4.2에서 도입된 MFC 4.2이다(MFC42.DLL). 비록 후속 버전인 비주얼 C++ 5와 6에서 개선과 기능 추가가 있었지만, 이것은 하위 호환성이 완벽하게 유지되는 변화이기 때문에 파일 이름이 동일했고, 이것이 MFC42.DLL이 윈도우 98 이래로 모든 윈도우 운영체제가 기본 내장하고 있는 표준 MFC DLL이 되었다.

오늘날 운영체제가 제공하는 MFC42.DLL은 이제 비주얼 C++ 6.0이 제공하던 클래식 MFC42.DLL과 하위 호환성만 유지되는 superset으로, 비주얼 C++과는 별개로 운영체제가 관리하는 DLL이다. 가령, 윈도우 7의 워드패드와 그림판은 명목상 MFC42.DLL을 사용하지만, 원래 VC6에는 없던 리본 인터페이스를 자기네 MFC42.DLL로부터 가져와서 사용하고 있다.

16비트에서 32비트로 넘어가면서 MFC는 일부 내부 자료구조가 바뀌었다. 특히 handle map은 스레드마다 서로 따로 돌아가기 때문에, 서로 다른 스레드끼리 핸들을 주고받을 때는 MFC 개체가 아닌 핸들을 직통으로 주고받아야 안전하다.

뭐, 이뿐만이 아니라 자체 구현으로 표시하던 toolbar와 status bar가 윈도우 95/NT 3.5부터는 common control이라고 운영체제에 정식으로 편입되었기 때문에, 그걸 그냥 끌어다 쓰는 걸로 내부 구현이 바뀌었다는 것도 첨언하겠다.

비주얼 C++ 6에서 닷넷으로 넘어가면서는 CString 같은 기초 자료형이 MFC가 아닌 ATL이라는 다른 라이브러리로 내부 관할이 바뀌고, 완전히 템플릿 기반으로 바뀌어서 한 코드로 ansi string과 wide string을 모두 다룰 수 있게 되었다. 그리고 DLL의 배포 방식이 변화를 겪기도 했다.

그 뒤 MFC는 2000년대에 들어와서는 이렇다 할 큰 변화가 없었고, 하루가 다르게 발전하는 C# 언어와 닷넷에 비해 MS가 네이티브 개발을 너무 홀대하는 게 아니냐는 우려의 목소리가 나올 정도였다. 특히 MS는 오피스 제품을 주축으로 독자적인 화려한 GUI(메뉴, 툴바 등)를 선보였고 오피스 2007부터는 리본 UI라는 완전히 새로운 물건까지 선보여 왔는데, 이를 모방하여 구현해 주는 MFC 기반 싸제 GUI 라이브러리가 제3자에 의해 미들웨어로 전문적으로 개발되어 판매될 정도가 되었다. 이름하여 GUI 툴킷.

과거 오피스 97/2000까지만 해도 프로그램 비주얼이 운영체제의 그것과 그렇게 큰 차이는 없었다. 그러나 오피스 XP부터 운영체제 기본 프로그램의 비주얼과 그런 오피스(?)급 프로그램의 비주얼은 이질감이 확 생겨 버렸다. 사실은 오피스 XP의 그 하얗고 깔끔한 2D같은 GUI가 윈도우 XP의 전반적인 GUI 디자인이 될 예정이었는데, 그 계획이 수틀리는 바람이 그 오피스 계열하고 운영체제의 알록달록 둥글둥글(?)한 디자인 계열은 따로 놀게 된 거라고 한다.

다음 하편에서는 이 GUI 툴킷과 관련된 MFC의 변천사 이야기를 계속하겠다.

Posted by 사무엘

2012/03/27 08:35 2012/03/27 08:35
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/660

윈도우 프로그래머라면 이미 다 아시겠지만, 비스타에서부터 task dialog라는 아주 참신한 UI 기능이 추가되었다.
구닥다리 MessageBox를 쓰자니 뭐가 많이 부족하고,
그렇다고 해서 겨우 에러 메시지 하나 찍자고 별도의 대화상자를 또 만들자니 너무 번거로운데
task dialog는 가히 사막에 있는 오아시스 같은 존재가 아닐 수 없다.

사용자 삽입 이미지
위의 그림은 바로 task dialog의 뼈대. (출처: MSDN)

이제 당장 운영체제부터가 상당수의 UI를 task dialog으로 구현하고 있고,
메모장부터 워드패드까지 모든 기본 프로그램들의 “문서를 저장하시겠습니까?” 대화상자도 죄다 task dialog로 바뀌었다.
덕분에 Yes / No 일색이던 버튼이 Save / Don't save로 바뀐 걸 알 수 있다. task dialog는 각 버튼들에 들어가는 텍스트를 사용자가 자유롭게 지정 가능하기 때문이다.

Y/N이라고만 하면 이게 무슨 질문에 대한 “예/아니요”인지, 응답에 대한 결과를 사용자가 한 단계 더 추론을 해야 한다.
그러나 대놓고 “저장함/저장 안 함”이라고 표시를 해 주면, 이 선택으로 인해 야기되는 결과를 사용자가 더 직관적으로 알 수 있다. MS는 저런 UI 용어 하나하나까지 세심하게 검토를 해 온 것이다.

이것뿐만이 아니라 또 개인적으로 본인은 task dialog가 유용하다고 가장 먼저 느낀 면모가 뭐냐 하면,

“다음부터 이 확인 질문 안 하기” 부류의 체크 상자를 간단하게 추가할 수 있다는 점이었다. 과거의 MessageBox에서 진짜로 2% 부족한 면모였다.
그래픽 모드나 해상도를 바꾼 뒤에 타이머를 걸어서 “화면이 잘 나타나 보입니까? n초 이내에 응답이 없으면 원래 모드로 되돌립니다”를 구현하는 것도 이 task dialog로는 드디어 가능하다. 예전에는 그런 걸 구현하려면 전용 대화상자를 따로 만들어야 했다.

task dialog에는 인터넷 URL 링크를 넣을 수 있고, 라디오 버튼을 넣어서 사용자의 간단한 선택을 받을 수도 있다. 제목-본문 형태로 텍스트를 깔끔하게 배치할 수 있다는 것도 아주 좋은 점이다.
물론, 워낙 기능이 많기 때문에 사용하기가 다소 까다롭다는 건 어쩔 수 없다. 그래서 이를 간소화하기 위해, 비주얼 C++ 2008의 확장팩 내지 2010부터는 MFC에도 CTaskDialog라는 클래스가 추가되었다. 자료구조 관리는 이 클래스가 다 알아서 해 주기 때문에 사용자는 코드 한 줄로 간단하게 원하는 버튼, 원하는 컴포넌트들을 대화상자에다 추가할 수 있다.

그런데 task dialog로 할 수 있는 일은 단순히 메시지를 찍고 사용자로부터 간단한 피드백을 받는 일에 국한되지 않는다.
progress bar를 넣는 기능이 있고 bar의 상태를 일정 주기로 업데이트까지 가능하기 때문에, 이를 이용하면 진행 상황 표시 대화상자도 간단하게 구현 가능하다.

본인은 task dialog를 제어하는 코드와 스레드 작업 관련 코드를 한데 합쳐서 별도의 클래스를 만들어 이를 개인적으로 매우 즐겨 사용한다. task dialog를 사용하는 형태는 딱 정해져 있으니까 별로 customize를 하지 않고, 작업 상황 표시와 작업 스레드의 customization이 이 클래스의 존재 목표가 되는 셈이다.

task dialog 콜백과 스레드 콜백 함수는 내부의 private static 함수로 숨겨 놓는다. 스레드 콜백 함수는 this 포인터에 대해서 아래의 순수 가상 함수를 호출한다.

virtual UINT Work() = 0; //오버라이드 할 것
volatile int m_nCurPos, m_nPosMax; //현재/전체 진행 상황
volatile bool m_bCancel;

그리고 task dialog 콜백은 당연히.. 주기적으로 m_nCurPos 값을 체크하여 progress bar를 업데이트한다.
사용자가 도중에 취소 버튼을 눌러 버렸다면, m_bCancel 플래그가 설정된다. 작업 스레드는 이 값을 수시로 체크해서 사용자가 중단을 요청했다면 신속히 작업을 중단해야 할 것이다.

일이 언제 끝날지 모르는 작업에 대해서는 게이지가 marquee 형태로 뱅글뱅글 돌기만 하게 만들 수도 있다. 윈도우 부팅할 때처럼 말이다.

다만 한 가지 아쉬운 것은, task dialog는 진행 상황 표시만 전문으로 하는 녀석이 아니다 보니, progress bar를 두 개 표시해 주는 기능은 없다는 점이다.
설치 프로그램이라든가 압축/FTP 유틸리티처럼 파일을 다루는 프로그램들은 현재 처리하고 있는 파일의 진행률과 그리고 전체 작업의 진행률을 한데 표시하고 있으며, 이건 매우 흔한 관행이다. 이건 여전히 내가 직접 대화상자를 만들어야 할 것 같다.

.
.

그나저나 드디어 윈도우 7도 SP1이 정식 출시된 지 한 달쯤 됐다.
콘솔에서 세벌식으로 한글 입력할 때 한글+기호 입력이 제대로 안 되던 버그도 고쳐졌으려나? (난 7 안 써서 잘 모르겠다) 했는데
어느 지인의 얘기에 따르면 여전하다고 하네... -_- 어쩌라고.

Posted by 사무엘

2011/03/17 08:32 2011/03/17 08:32
, , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/481


블로그 이미지

그런즉 이제 애호박, 단호박, 늙은호박 이 셋은 항상 있으나, 그 중에 제일은 늙은호박이니라.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/04   »
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30        

Site Stats

Total hits:
2678786
Today:
870
Yesterday:
2484