윈도우 OS 에서 돌아가는 모든 어플리케이션은 '이벤트' 와 '메시지' 에 기반해서 실행된다.
예를 들어 왼쪽 마우스 버튼을 클릭하면, 윈도우는 왼쪽 마우스 버튼이 클릭되었다는 메세지를 어플리케이션에 보낸다.
그러면 어플리케이션은 이 메세지를 정해진 루틴대로 처리하는 방식이다.
그래서 윈도우 프로그래밍은 메세지를 만들고 핸들링하는 루틴 로직을 짜는 것이라고 말할 수 있다.
그래서 하드웨어를 조작하거나, 임의로 메세지를 만들면 만들어진 메세지는 message queue에 들어간다.
메세지 큐에 들어온 메세지는 어플리케이션에 있는 메세지 루프를 통해 하나씩 꺼내 일치하는 window procedure가 호출되고, 우리가 작성한 window procedure 로직에 의해 메시지를 처리해서 어플리케이션 화면에 필요에 따라 결과를 띄워주면 된다. (콜백 함수와 비슷한데, 조금 다르다고 한다.)
윈도우 프로그래밍은 크게 Win32 SDK 를 사용하는 방법과 MFC를 이용하는 방법으로 나눠진다.
Win32 SDK를 사용하면 표준 C 라이브러리를 사용해 어플리케이션을 개발하게 된다.
그리고 프로그래머가 메세지를 핸들링하고 화면에 보여주는 모든 루틴을 직접 프로그래밍해야한다.
대신 프로그램이 가볍다는 장점이 있다.
MFC는 C++ 클래스 라이브러리를 사용하여 개발한다.
메세지 핸들링 루틴같이 필수적인 코드는 자동으로 생성이 되어서 편리하지만, 코어한 로직이 프레임워크에 숨겨져 있어 프로그램이 무겁고 처음에 공부하기 힘들다는 단점이 있다.
Win32 SDK
먼저 Win32 SDK 를 간단하게 살펴보자.
#include <windows.h>
win32 sdk 는 windows.h 헤더를 사용하여 프로그래밍한다.
win32 sdk 프로그램은 WinMain() 이라는 함수로부터 시작된다.
int WINAPI WinMainWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpszCmdLine, int nCmdShow) {
// initialization
}
메인함수 내에서는 위와 같은 과정을 수행한다.
{
HWND hwnd; // window handle
MSG msg; // message structure
WNDCLASSEX WndClass; // Window Class structure
WndClass.cbSize = sizeof(WNDCLASSEX); // size of the struct
WndClass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; // class stype
WndClass.lpfnWndProc = WndProc; // window procedure
WndClass.cbClsExtra = 0; // window class data area
WndClass.cbWndExtra = 0; // window data area
WndClass.hInstance = hInstance; // instance handle
WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION); //icon handle
WndClass.hCursor = LoadCursor(NULL, IDC_ARROW); // cursor handle
WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); // background brush handle
WndClass.lpszMenuName = NULL; // menu name
WndClass.lpszClassName = "EasyText";
WndClass.hIconSm = 0; // basic small icon
// Regiters the window class
RegisterClassEx(&WndClass);
hwnd = CreateWindow( // window generating API function
"EasyText", // registered window class name
"Exercise1a", // string to be printed on the title bar
WS_OVERLAPPEDWINDOW, // widow style
CW_USEDEFAULT, // window uppper left corner x position
CW_USEDEFAULT, // window upper left corner y position
800,//CW_USEDEFAULT, // window width
600, //CW_USEDEFAULT, // window height
NULL, // parent window handle
NULL, // menu or child window handle
hInstance, // application instance handle
NULL // window generating ddata address
);
//displays frame window
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
// retrives messages from message cue and forward the message to the corresponding window
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
실제 코드로 보면 되게 긴데, 당연히 외워서 할 수 없다..
중요한 건 대충 윈도우 클래스 구조체를 만들어서 설정값을 세팅한다.
그리고 윈도우 클래스를 OS에 등록하고, 창을 화면에 띄운다.
창을 띄운 이후에는 메세지 루프를 돌면서 메세지를 받아 처리한다.
이 큰 흐름만 알면 될 것 같다.
메세지를 받으면 메세지를 번역하고 dispatch 하는데, 이렇게 디스패치된 메세지는 WndProc 함수에서 처리한다.
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
// handle message
}
이렇게 생겼다.
이 함수의 전체 코드를 보면 아래와 같다.
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc; // device context
RECT rect; // RECT structure
PAINTSTRUCT ps; // paint structure
LPCSTR szMsg1 = "Window Programming";
LPCSTR szMsg2 = " Keyboard is down ";
LPCSTR szMsg3 = " Keyboard is up ";
//handles message from the kernel
switch (message) {
case WM_CREATE:
break;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
TextOut(hdc, 10, 10, szMsg1, strlen(szMsg1));
EndPaint(hwnd, &ps);
break;
case WM_KEYDOWN:
hdc = GetDC(hwnd);
GetClientRect(hwnd, &rect);
DrawText(hdc, szMsg2, strlen(szMsg2), &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
ReleaseDC(hwnd, hdc);
break;
case WM_KEYUP:
hdc = GetDC(hwnd);
GetClientRect(hwnd, &rect);
DrawText(hdc, szMsg3, strlen(szMsg3), &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
break;
ReleaseDC(hwnd, hdc);
break;
case WM_LBUTTONDBLCLK:
MessageBox(hwnd, "Mouse double clicked", "Mouse message", MB_OK | MB_ICONASTERISK);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hwnd, message, wParam, lParam);
}
return 0;
}
switch 문을 사용해서 메세지 종류에 따라 다른 로직을 처리한다.
마법사로 만들면 기본 틀은 다 만들어줘서 코드를 외울 필요는 없다.
MFC
Win32 API를 기반으로 C++ 클래스를 이용하여 윈도우 프로그램을 만드는 프레임워크이다.
기본적인 코드 폼을 제공하므로 개발 시간이 단축된다.
또한 UI components, ActiveX, OLE, internet programming 도구들을 제공하기 때문에 데이터베이스와 네트워크 프로그래밍을 할 때 편하다. (ODBC/OLE DB 와 window socket 을 사용)
노테이션으로서, 모든 클래스 이름은 'C' 로 시작하고, 이후 이름은 Camal Case를 따른다.
멤버 변수는 'm_' 으로 시작하고 이후에도 Camal Case를 따른다.
글로벌 함수의 이름은 'Afx' 로 시작한다. (Application Framework 의 줄임)
MFC로 윈도우 앱을 만들 경우, 3가지 형태로 윈도우 프로그램을 만들 수 있다.
SDI (Single Document Interface), MDI (Multiple Document Interface), Dialog 이렇게 3가지 방식이 있다.
SDI vs MDI vs Dialog
SDI 는 하나의 Document frame window 를 가진 어플리케이션이다.
MDI 는 여러개의 Document frame window 를 가진 어플리케이션이다.
이 둘은 메인 프레임이 존재하고, 메인프레임만 있으면 SDI, 그 밑에 자식 창이 있으면 MDI, 메인 프레임조차 없이 간단하게 프로그래밍할 때는 Dialog 방식이 된다.
위 이미지는 SDI 어플리케이션의 기본적인 구조이다.
Template
템플릿은 윈도우 창 하나의 폼을 의미한다.
템플릿 안에는 윈도우의 메뉴, 툴바, 상태바 등을 포함하는 바깥 바운더리를 그리는 'Main Frame'
그 내부 흰색 스크린을 가리키는 'View Window' (이 곳에 텍스트, 그래픽 출력이 나타난다.)
디스크로부터 데이터를 읽고 쓰는 기능을 수행하는 'Document' 이렇게 3가지로 구성되어 있다.
그래서 위 클래스를 보면 이 템플릿 개념을 클래스로 구현하였음을 알 수 있다.
CWinApp 클래스가 템플릿을 가리키고
CFrameWnd 클래스가 템플릿의 메인 프레임
CView 클래스가 View Window
CDocument 클래스가 디스크와 데이터를 읽고쓰는 Document 역할을 수행한다.
MDI 는 이렇게 구성되어 있다.
메인 프레임 안에 자식 창이 존재한다.
CWinApp 클래스가 전체 어플리케이션을 나타내고
CMDIFrameWnd 클래스가 Main Frame을 나타낸다.
메인 프레임 안에 여러 child window 가 존재하는데, Child Window 각각마다 Document 와 View Window 가 따로 존재한다. child window는 CMDIChildWnd 클래스로 구현된다.
따라서 MDI 는 여러개의 SDI가 모여있는 형태를 갖는다.
ChildFrame이 Child Window 를 나타낸다.
Application Class
SDI 는 하나의 템플릿을, MDI는 여러개의 템플릿을 갖고 있다.
여러개의 템플릿은 하나의 Main Frame 안에 들어있어야 하기 때문에 Main Frame 으로 감싸져있다.
어플리케이션 클래스는 이 Main Frame을 감싸는 클래스이다.
MFC는 윈도우 SDK를 사용해서 클래스를 만들 때 발생하는 여러 문제들을 Visual C++ 를 이용해서 해결하며,
이를 도와주기 위해 초기 프로젝트를 생성할 때는 MFC 마법사를 이용하여 어플리케이션을 생성한다.
MFC Class 구조
CObject
메모리에 올라와있는 클래스에 대한 정보를 설정한다.
새 연산자를 오버로딩되어 있고, 현재 클래스의 기능과 타입을 식별하는 함수를 갖고 있다.
예를 들면 IsSerializeable() 함수는 현재 클래스가 디스크에 데이터를 쓸 수 있는지, 체크한다.
AssertValid() 함수는 현재 클래스가 유효한지 체크하고, Dump() 함수는 현재 클래스의 상태를 체크한다.
CWnd Class
스크린에 보이는 모든 윈도우 창이 상속하는 클래스이다.
MFC에서 가장 자주 사용하는 클래스로, '윈도우'를 나타내는 가장 상위 클래스이다.
보통 이 클래스를 직접 사용하지 않고, 이 클래스를 상속하는 CFrameWnd, CView, CDialog 클래스를 이용한다.
윈도우 창을 생성하고 실행하는 기능의 함수를 갖고 있다.
Initialization, Window State Functions, Window Size and Position, Coordinate Mapping Functions, Window Message Functions
CWinThread & CWinApp
CWinThread 클래스는 윈도우가 하나의 스레드에서 실행될 수 있도록 해준다.
MFC 어플리케이션을 실행하려면, 이 클래스의 객체가 최소 하나는 실행되어야 하고, 이 클래스가 다수 실행되면 멀티 스레딩이 된다.
CWinApp 클래스는 CWinThread 클래스를 상속하는 프로그램의 관리자 역할을 하는 클래스이다.
즉 이 클래스가 메인 스레드를 돌리는 클래스이다.
CWnd
CWnd 클래스는 윈도우 창에대한 클래스로, 아래와 같은 자식 클래스들이 있다.
실제 사용자 눈에 보이는 것과 관련된 클래스이다.
Message Handling
그렇다면 MFC는 어떻게 메세지를 핸들링할까?
우선 메세지에 대해 정리해보자.
Message 는 어떤 이벤트가 발생했을 때, 이 벤트의 타입과 정보를 갖고 있는 상수 값 데이터를 의미한다.
즉, 메세지는 Event를 번역한 것이다!
MFC는 Win SDK 와 다르게 메세지를 처리하는 과정이 눈에 보이지 않지만, 백그라운드에서 돌아가고 있다.
그래서 우리는 메세지를 처리할 함수만 잘 작성하면 된다.
윈도우 메세지는 WM_ 이라는 키워드가 앞에 붙는다.
(단 WM_COMMAND 는 메세지가 아니다.)
그리고 각각의 메세지를 구체적으로 어떻게 다뤄야할지에 관련된 파라미터를 갖는다.
(마우스 이벤트면 마우스의 좌표정보를 갖고 있다거나)
대표적인 메세지 종류는 아래와 같다.
- 윈도우 자체의 상태가 변하는 상황에서는 Window Administrative Message 가 발생한다.
- 어플리케이션 프로그램이 메뉴나 다이어로그를 설정하면 Initializing message가 발생한다.
- 마우스, 키보드 등으로 입력을 주면 Input Message 가 발생한다.
- 버튼, 콤보박스 같은 컨트롤 오브젝트는 Control Notification Message를 발생시킨다.
- 메뉴, 툴바, Accelerator key 같은 오브젝트 GUI 는 WM_COMMAND 메세지를 생성한다.
커맨드 메세지는 document, document template view, 다른 application object (window) 로부터도 생성될 수 있다.
이 메세지를 처리하는 핸들링에 대해서, Win32 SDK 는 switch - case 문으로 각 메세지의 타입별로 따로따로 처리를 했다.
반면 MFC는 Message Map 이라는 걸 사용해서 메세지를 핸들링한다.
메세지 맵은 메세지 번호와 각 메세지 별로 호출해야할 함수의 포인터를 매칭한 테이블을 말한다.
이를 이용해 메세지를 그 메세지에 해당하는 적절한 프로그램(함수)에 연결지어줄 수 있다.
구체적인 과정은 아래와 같다.
1. 메세지 핸들러 함수를 windows class 의 멤버 함수로 정의한다.
2. 메세지와 메세지 함수를 연결짓는 메세지 맵에 message macro를 추가한다.
3. message handler functionalities 를 구현한다.
사실 프로그래머는 3번, 메세지 핸들러 함수만 구현하면 된다. 나머지는 자동으로 생성된다.
Message Handler Function 은 On 이라는 이름으로 시작한다.
메세지의 WM_ 이 On 으로 바뀌었다고 생각하면 된다.
afx_msg 는 메세지 핸들러 함수를 의미한다.
헤더파일에 이렇게 메세지 핸들러 함수들이 정의되어 있다.
실제 코드는 이런식으로 되어 있다.
각각의 메세지 핸들러 함수가 이렇게 메세지 맵에 의해 바인딩 되어 있다.
CString
문자열을 다루는 클래스
Format 메서드를 자주 사용한다.
변수의 값을 문자열로 바꾼 뒤, 문자열을 컨트롤에 연결지어 쓰는데 활용할 수 있다.
Invalidate() 함수, OnDraw() 함수
CView 클래스에 있는 Invalidate() 함수를 호출하면, WM_PAINT 메세지가 생성된다.
이 메세지가 생성되면 OnDraw() 함수가 호출되어 화면을 새로 그리게 된다.
따라서 만약 어플리케이션 프로그램의 데이터가 바뀌어서 화면을 다시 그리고 싶다면 Invalidate() 함수를 호출하면 된다.
Invalidate(TRUE) 는 백그라운드를 삭제하고, BeguinPain() 함수를 이용하여 백그라운드를 다시 그린다.
Invalidate(FALSE) 는 백그라운드를 그대로 놔두고, 바뀐 부분만을 다시 그린다.
기본값은 TRUE이다.
OnDraw() 함수는 윈도우 화면이 바뀔 때마다 WM_PAINT 메세지가 생성되어 호출된다.
CWnd 클래스와 이를 상속한 클래스는 OnPaint() 라는 메세지 핸들러 함수를 갖는다.
CView 클래스와 이를 상속한 ㅡㅋㄹ래스는 OnPaint() 를 오버라이딩한 OnDraw() 를 사용한다.
WM_PAINT 가 호출될 때마다 OnDraw 와 OnPaint 가 모두 호출되지만, OnDraw 함수만으로 화면을 다시 그리는데는 충분하다.
CRect, GetClientRect
CRect 클래스는 사각형의 좌상단, 우하단 좌표를 저장하고 있는 클래스이다.
다음과 같은 속성값을 가지고 있다.
GetClientRect() 함수는 현재 윈도우의 사각형 영역 크기를 돌려준다.
CDC::DrawText()
화면의 특정 영역에 정해, 그 영역에 문자열을 출력한다.
Dialog
SDI, MDI 와 같이 윈도우 창을 만드는 클래스이다.
CDialogEx 클래스가 윈도우 메세지를 처리한다.
Dialog 를 기반으로 만든 윈도우 프로그램은 아래 3가지 클래스를 기반으로 만든다.
Dialog 프로그램은 아래와 같은 라이프사이클을 가진다.
데이터가 변경되는 것을 OnDataExchange() 로 감지하고, 이에 대해 이벤트를 처리한 뒤 다시 그림을 그린다.
Dialog Based MFC 어플리케이션은 다음과 같은 순서로 만든다.
1. 프로젝트 생성 후, Main Dialog의 폼에 컨트롤과 컨트롤에 맞는 설정을 추가한다.
2. 컨트롤과 멤버 변수를 연결한다.
3. 컨트롤에 대한 메세지 핸들러 함수들을 작성한다.
멤버 변수와 컨트롤 연결하기
DDX를 사용하여 멤버 변수와 컨트롤을 연결할 수 있다.
컨트롤에 값을 할당하고, DDX_Text 타입의 함수를 이용하여 리소스를 연결한다.
컨트롤은 DDX_Control 기반의 함수들을 사용하여 리소스를 연결한다.
컨트롤을 사용할 때는 컨트롤의 핸들을 가지는 포인터 변수를 선언하고, GetDlgItem() 함수로 해당 컨트롤을 얻어온다.
그리고 컨트롤을 조작할 때는 관련 멤버 함수와 변수를 조작하면 된다.
DoDataExchange() 함수
DDX_Text, DDX_Control 함수를 사용하여 다이어로그 박스에 있는 다양한 리소스들을 연결한다.
DDX_Control 에 의해 연결되면, 해당 컨트롤러 함수를 상속받게 되어 그 클래스의 멤버함수를 사용할 수 있다.
이렇게 컨트롤 또는 문자열을 저장하는 변수를 컨트롤에 연결할 수 있다.
UpdateData() 함수
DDX_Value 의 형태로 연결된 경우, 리소스를 업데이트할 때 UpdateData() 함수로 업데이트 해야한다.
컨트롤에 들어있는 값을 변수로 가져올 때는 UpdateData(TRUE), 변수에 있는 갑을 컨트롤에 출력할 때는 UpdateData(FALSE) 를 사용한다.