본문 바로가기

Library/Windows Programming

CView 파생 클래스를 사용하기

MFC의 Doc / View 구조를 번거로워 하는 사람들이, 그냥 바닥에서 바로 코드를 쌓아올리다가 만나는 가장 귀찮은 문제는 이것이다. CView의 파생클래스들은 그야말로 편리한 클래스들이 많다. 대표적으로, 스크롤 기능을 제공하는 CScrollView가 있는데, 스크롤 기능을 추가하고자 할 때 CScrollView 클래스를 사용하지 않는다면 WS_HSCROLL이나 WS_VSCROLL 스타일을 줘서 윈도우를 생성하거나, 스크롤바를 생성해서 윈도우에 붙여줘야 하고, 더구나 화면의 스크롤를 생각해서 WM_PAINT 메세지를 처리해야 한다. 참으로 귀찮은 일이 아닐 수 없다.

그렇다면, CScrollView를 평범하게 그냥 상속받아 사용할 수 있을까?


결론부터 말하자면 어렵다. 매우 힘들다. Doc / View 구조로 붙지 않은 CView 파생 클래스들은, 잘만 하면 사용할 수도 있지만, 완전한 기능을 발휘하지 못한다. 따라서, Doc / View 구조가 익숙하지 않은 사람은 이 기회에 Doc / View 구조를 한번 살펴보길 바란다. 간략히 설명하면, 다음과 같은 구조로 Document와 View, Frame이 연동된다.


먼저, 전역 변수로 선언되는 CWinApp 클래스의, InitInstance()에서 초기화가 이루어진다는 것을 알고 있을 것이다. 지금까지, InitInstance()에서 하나의 프레임 윈도우를 생성하고 그 프레임 윈도우 안에서 모든 것을 처리했다면, 이제는 여기서 Document Template을 생성하자. SDI와 MDI 어느 것을 사용할 것인가에 따라 어느 템플릿을 사용할지를 선택하라. 여기서는 SDI를 사용한다.

CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
    IDR_RESOURCE,
    RUNTIME_CLASS(DerivedDocumentClass),
    RUNTIME_CLASS(DerivedFrameWindow),
    RUNTIME_CLASS(DerivedViewClass));

이 코드에서, IDR_RESOURCE란 부분은, 프레임 윈도우에 붙을 각종 리소스 ID를 말한다. AppWizard를 통해서 코드를 생성했다면, IDR_MAINFRAME으로 되어 있는 것을 볼 수 있을 것이다. 그렇지만, 하나만 정의하도록 되어 있는데.. 메뉴, 엑셀러레이터와 기타 잡다한 리소스들은 이 대표이름으로 설정되어야 한다. 하지만, 실제로 필요한 것은 메뉴에 관련된 리소스이다. 정확한 이유는 알 수 없지만, 새로운 Document를 생성할 때 필요하기 때문인 것으로 보인다. 물론, 여기서 말하는 Document란 것이 문서라는 그 자체를 의미하는 것이 아님을 알고 있을 것이다. 이름은 Document지만, 이것은 DataSet이나 DataContainer 정도가 어울릴 것이다. 여튼, 최소한 리소스 편집기에서 빈 깡통 메뉴라도 하나 생성해두어야 하며, 최소한 하나 이상의 더미 항목이 있어야 나중에 프레임 윈도우에 메뉴를 붙이는 과정에서 FALSE를 리턴하지 않는다.

그리고, 프레임 윈도우는 반드시 CFrameWnd에서 상속 받아야 하며, CWnd에서 상속받아서는 안된다. CWnd에서 상속받은 윈도우를 프레임 윈도우로 사용하려고 했다가는 런타임 에러가 발생할 것이다. CFrameWnd는 이미 정해진 스타일이 있어서 꼭 CWnd를 사용해야겠다고? CFrameWnd에서 상속받은 클래스를 사용자에게 보여지기 전에 입맛대로 생성 스타일을 지정하고 싶다면, PreCreateWindow()를 재지정하여 원하는대로 바꾸면 된다. 여튼, 프레임 윈도우는 반드시 CFrameWnd를 상속받아야 한다.

AddDocTemplate(pDocTemplate);

CSingleDocTemmplate을 정상적으로 생성했다면, AddDocTemplate()을 사용하여 생성된 템플릿을 CWinApp에 추가해준다. 물론, CWinApp은 이것을 단일 항목으로 가지고 있는 것이 아니라, CDocManager 클래스를 사용해서 리스트로 관리한다. CDocManager는 문서화되어 있지 않은 클래스이다. 즉, 내부를 블랙박스로 생각하면되고, 이것을 건드릴 일은 없을 것이다.

CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);

이 부분은, 프로그램을 실행할 때 주는 인자값을 해석하는 부분인데, 다음에 ProcessShellCommand()라는 함수를 실행하기 위해서 필요하다. API로 작업할 때는 거의 신경쓰지 않았던 명령줄이지만, ProcessShellCommand()라는 함수를 실행하지 않으면 MFC Framework 내부에서 Document, View, FrameWindow를 생성하는 코드를 부르지 않는다. 자세하게 설명하자면, MFC의 Doc/View 구조에서 Document라는 것은 반드시 문서라는 것을 의미하지 않는다. Document는 Application이 사용할 데이터를 추상화한 것인데, Application이 시작할 때 이 Document를 새롭게 생성하며 시작할 것인지, 아니면 Document를 생성하지 않고 그냥 Application을 시작할 것이지 따위를, CCommandLineInfo의 인자를 통해 결정하게 되며, 그렇기 때문에 ParseCommandLine()라는 메서드를 호출하여 인자값을 파싱한다.

if(!ProcessShellCommand(cmdInfo)) return FALSE;

이 부분이 문제의 부분인데, ProcessShellCommand() 함수 내부에서 Document와 View를 생성하고, FrameWindow를 생성하고 이들을 연관짓는다. 먼저 FrameWindow가 생성되며, 다음은 View가 생성되고, 마지막으로 CDocument의 가상함수인 OnNewDocument() 함수가 호출된다. 위에서도 설명했듯이, 반드시 새로운 Document를 생성하는 것은 아니고, CCommandLineInfo의 인스턴스 개체값에 따라 결정된다. 새로운 Document가 생성되지 않도록 설정했다면 OnNewDocument()는 호출되지 않을 수도 있다.


AppWizard를 사용하지 않고 Document, View, FrameWindow를 생성했다면, 반드시 클래스 선언에 DECLARE_MESSAGE_MAP()과 함꼐 DECLARE_DYNCREATE() 매크로를 사용하여 이들 클래스가 동적 생성을 지원하게끔 해주어야 하며, 구현부에서는 IMPLEMENT_DYNCREATE() 매크로를 포함해주어야 한다.

즉. 클래스 선언에서는,

class CContainer : public CFrameWnd
{
protected:
    DECLARE_MESSAGE_MAP();
    DECLARE_DYNCREATE(CContainer);
};

위의 매크로가 포함되어야 하며, 구현부에서는

IMPLEMENT_DYNCREATE(CContainer, CFrameWnd)

Container::Container()
{
....
}

IMPLEMENT_DYNCREATE() 매크로가 반드시 포함되어야 한다. 이것은, BEGIN_MESSAGE_MAP(DerivedClass, BaseClass)처럼 동적 생성을 선언한 클래스를 구현한다면 해당 클래스에 모두 포함해주어야 한다.


CDocument를 상속받는 클래스는, 가상 함수로 지정된 OnNewDocument()를 반드시 재지정해야 하며, CView를 상속받는 클래스는, 역시 가상 함수로 지정된 OnDraw() 함수를 반드시 재지정해야 한다.


사실, Doc / View 구조는 그렇게 귀찮기만 한 것은 아니다. 프로그램에서 필요한 모든 데이터는 Document로 밀어넣고, View는 UI 핸들링, 데이터 렌더링만 해주도록 구조를 분리할 수 있으므로 잘 정리된 디자인이 가능하도록 해준다. 물론, 작은 프로그램을 만들 때는 이것이 상당히 귀찮은 일이지만, 프로그램의 덩치가 커지고 필요한 기능이 많아질수록 Doc / View 구조는 빛을 발할 것이다.

물론, MFC 자체가 미리 정의된 프레임워크로 디자인을 강요하는 것은 좋지 않은 면이기도 하지만 말이다.