[C++] C++에서 Window Form을 생성하는 방법(Win32Api, MFC)


Study/C , C++ , MFC  2020. 5. 1. 12:14

안녕하세요. 명월입니다.


이 글은 C++에서 Window Form을 생성하는 방법(Win32Api, MFC)에 대한 글입니다.


프로그램 개발을 할 때는 보통 프로그램의 목적과 실행되는 플렛폼을 설정하고 개발을 합니다. 최근에는 웹 개발이 많아져서 예전보다는 적어졌지만, 예전에는 C/S(Client/Server) 형식의 개발이 대부분이었습니다.

단순히 게임을 생각해도 괜찮겠네요. Client는 사용자가 실행하는 프로그램(실제 게임이 실행되는 게임)이고 Server는 그런 Client을 하나의 공간에서 활동을 가능하게끔 계산 및 데이터를 관리하는 프로그램입니다.

그런 목적을 가지고 있으면 Client는 대부분 Window 환경입니다. 예전보다는 플랫폼이 다양해져서 꼭 MS의 윈도우가 아닌 맥PC, 안드로이드 폰등으로 사용을 많이 합니다.

Client의 플랫폼은 정해지게 되면 어떤 프로그램이 나오는 지를 생각하게 되는데, 기본적인 Window form으로 작성하게 됩니다. 물론 콘솔로 작성해서 사용할 수 없는 건 아닙니다만, 개인이 스크립트 식으로 사용하는 것이 아닌 판매하는 물품이라면 절대적으로 Client는 window form을 생각하지 않을 수 없습니다.


반대로 Server의 경우는 조작을 위한 프로그램으로 Window Form을 작성하는 경우도 많지만 Server 메인 기능은 Client와의 데이터 송수신과 계산, 보관이 제일 큰 목표이기 때문에 꼭 Window Form을 고집하지 않고 콘솔로 사용하는 경우도 드물게 있습니다.

예로 Tomcat의 경우도 services.msc에 프로그램을 등록시켜서 사용하는 게 정석이지만, startup.bat로 그냥 콘솔 화면으로 서버를 사용하는 경우도 많이 있습니다. 또 Linux의 경우는 아예 UI를 처음부터 설치하지 않는 경우도 많기 때문에 Server는 Window Form을 사용하지 않는 경우도 많이 있습니다.


그렇게 Window form을 C++로 작성해 보겠습니다.

#include <stdio.h>
// Window를 실행하기 위한 라이브러리
#include <Windows.h>
// Window는 한번의 실행으로 끝나는 것이 아니라 윈도우가 종료될 때까지의 무한 루프의 메시지가 필요하기 때문에 callback 함수를 사용한다. 메시지 처리 함수.
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
// 실행 함수
int main()
{
  // 프로그램 인스턴스 핸들 취득.
  // Window 프로그램은 하나의 인스턴스 윈도우를 OS에 등록을 해야 한다.
  HINSTANCE hInstance = GetModuleHandle(NULL);
  // 윈도우 핸들이다
  HWND hWnd;
  // 윈도우 메시지이다.
  MSG Message;
  // 윈도우를 정의하는 구조체이다.
  WNDCLASS WndClass;
  // 윈도우의 초기 설정하는 값이다.
  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  // 윈도우의 메인 바탕색
  WndClass.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);
  // 윈도우의 기본 마우스 커서 형태
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  // 윈도우의 아이콘
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  // 윈도우의 인스턴스
  WndClass.hInstance = hInstance;
  // 윈도우의 메시지 callbak 함수
  WndClass.lpfnWndProc = WndProc;
  // 윈도우를 인식하기 위한 클래스 명
  WndClass.lpszClassName = L"Main";
  // 윈도우 메뉴 이름
  WndClass.lpszMenuName = NULL;
  // 윈도우 스타일
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  // OS에 윈도우를 등록한다.
  // 윈도우라는 것은 결국 Window OS에서 실행하는 것인데 다른 프로그램들과 동시에 관리하기 위해서는 OS가 Window의 정보를 알고 있어야 한다.
  // 그를 위해 등록한다.
  RegisterClass(&WndClass);
  // 윈도우 생성(CreateWindow의 함수와 차이는 첫번째 dwExStyle의 파라미터가 있느냐 없느냐의 차이이다. 저는 0을 사용했다.)
  // 1번째 파리미터는 확장 스타일로는 WS_EX_WINDOWEDGE, WS_EX_CLIENTEDGE, WS_EX_OVERLAPPEDWINDOW 등등인데 별 쓸모없다.
  // 2번째 파리미터는 윈도우 객체를 지정하는 문자열(예를 들면 static, button, scrollbar), 메인 윈도우는 WndClass.lpszClassName의 등록된 값을 넣는다.
  // 3번째 파라미터는 윈도우 메뉴 이름
  // 4번째 파라미터는 윈도우의 스타일이다. 닫기 버튼 x박스를 표시하는지 최소화, 최대화 버튼의 여부이다. WS_OVERLAPPEDWINDOW가 무난하다.
  // 5,6번째 파라미터는 윈도우 생성 시의 초기 위치 설청, 5번째는 x좌표(모니터에서 가로), 6번째는 y좌표(모니터에서 세로)
  // 7,8번째 파라미터는 윈도우 생성 시의 초기 크기 설정, 7번째는 너비(width), 8번째는 높이(height)
  // 9번째는 파라미터는 부모 윈도우 여부, 메인 윈도우는 부모 윈도우가 없기 때문에 NULL이다.
  // 10번째 파라미터는 메뉴,
  // 11번째는 프로그램 인스턴스
  // 12번째는 여분의 파라미터로 WndProc안에서 다른 윈도우들과 데이터를 공유하고자 할 때 사용된다.
  hWnd = CreateWindowEx(0, WndClass.lpszClassName, WndClass.lpszClassName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 200, 200, NULL, (HMENU)NULL, hInstance, NULL);
  // 이제 윈도우를 띄운다.
  ShowWindow(hWnd, SW_SHOWDEFAULT);
  // 윈도우를 띄우고 마로 main thread가 종료되면 안된다. 그럼 프로그램이 종료하는 것.
  // 문한 루프의 형태를 만든다.
  // GetMessage로 Window 메시지에 값을 취득하고 값이 있으면 계속 true를 리턴할 것이다. 종료가 된다면 false되어 프로그램이 종료가 된다.
  while (GetMessage(&Message, NULL, 0, 0))
  {
    // 키보드 메시지를 받고 프로그램에서 사용할 수 있게 변환한다.
    TranslateMessage(&Message);
    // 메시지를 WndProc로 보내는 함수.
    DispatchMessage(&Message);
  }
  return 0;
}
// 메시지 처리 함수, 메시지를 처리하는 함수로 메시지란 유저의 입력 또는 OS에서 요구하는 메시지 등을 처리하는 함수
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  // 윈도우를 생성할 때의 구조, main에서 CreateWindowEx로 넣었던 파라미터 값이 있다.
  // 다른 윈도우를 생성하려면 인스턴스가 필요한데 여기서 취득한다.
  CREATESTRUCT *cs = (CREATESTRUCT*)lParam;
  switch (iMessage)
  {
  // 최초 윈도우가 생성되면 호출된다.
  case WM_CREATE:
  {
    // 파라미터는 위와 같다.
    // 2번째 파라미터에 button을 설정 했고, 부모는 Window Form에 클릭시 발생하는 메시지 번호
    CreateWindowEx(0, L"button", L"Click me", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 20, 20, 100, 25, hWnd, (HMENU)999, cs->hInstance, NULL);
  }
  return 0;
  // 윈도우에서 명령이 발생하면 여기로 호출 
  case WM_COMMAND:
  {
    switch (LOWORD(wParam))
    {
    // 이 999는 위 버튼을 눌렀을 때 발생하는 메시지
    case 999:
      // 메시지 박스 창이 뜰 것이다.
      MessageBox(hWnd, L"Hello world", L"Button", MB_OK);
    }
  }
  return 0;
  // OS에서 윈도우를 그리는 명령을 할 떄 발생하는 이벤트.
  case WM_PAINT:
  {
    // PAINTSTRUCT를 받고
    PAINTSTRUCT ps;
    // 그린 후
    HDC hdc = BeginPaint(hWnd, &ps);
    // 리소스 반환
    EndPaint(hWnd, &ps);
  }
  return 0;
  // 윈도우가 종료하라는 메시지가 발생하면
  case WM_DESTROY:
  {
    // 종료 메시지를 보낸다. 위 GetMessage가 False가 호출이 되고 main함수가 종료가 된다.
    PostQuitMessage(0);
  }
  return 0;
  default:
    // 위에 포함되어 있지 않는 메시지를 콘솔에 표시힌다.
    printf("0x%04X\n", iMessage);
  }
  // 그외의 메시지는 기본 처리를 하도록 설정한다.
  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}

위의 형태가 아주 기본적인 형태입니다.

중요한 흐름은 WndClass 정의와 OS에 Window 구조체를 등록하는 것, WndProc로 메시지를 받아 윈도우를 그리고 처리하는 것입니다.

이제 실행하겠습니다.

실행하면 뒤에 콘솔창이 나오고 앞에 윈도우가 실행되는 것을 확인할 수 있습니다.

콘솔 창에서는 WM_CREATE, WM_COMMAND, WM_PAINT, WM_DESTROY의 외의 메시지의 번호가 나오는 것입니다.


Visual Studio를 사용하는 유저라면 위 WM_CREATE에 포커스를 두고 F12를 누르면 메시지가 나타내는 번호를 찾아 볼 수 있습니다.

이런 식으로 말입니다.


콘솔에 찍힌 메시지를 한번 찾아보겠습니다.

제가 윈도우 폼 안에서 마우스를 움직였나 보네요... 이런 식으로 메시지를 잡아서 처리할 수 있습니다.


이제 다시 소스로 돌아오면 우리가 윈도우를 Release한다고 하면 윈도우 프로그램에서 보통 콘솔 창은 보이지 않습니다. 윈도우 창만 있으면 되지 솔직히 콘솔 창은 개발할 때나 필요하지 실제 프로그램에서는 필요하지 않는 내용들입니다.

그럼 콘솔 창을 없애보겠습니다.

먼저 프로젝트의 Properties로 갑니다.

그리고 Linker -> System -> SubSystem이 Console로 되어 있을 텐데 이걸 Window로 변경합니다.


그리고 윈도우로 변경을 하게 되면 메인 함수가 바뀌게 됩니다. console에서는 int main으로 실행하던 실행 함수가 int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR LpszCmdParam, int nCmdShow) 식으로 바뀌게 됩니다.

다시 소스를 작성하면 다음과 같습니다.

#include <stdio.h>
// Window를 실행하기 위한 라이브러리
#include <Windows.h>
// Window는 한번의 실행으로 끝나는 것이 아니라 윈도우가 종료될 때까지의 무한 루프의 메시지가 필요하기 때문에 callback 함수를 사용한다. 메시지 처리 함수.
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
// 실행 함수
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR LpszCmdParam, int nCmdShow)
{
  // 프로그램 인스턴스 핸들 취득.
  // Window 프로그램은 하나의 인스턴스 윈도우를 OS에 등록을 해야 한다.
  // 인스턴스를 파라미터로 구해주기 때문에 굳이 구할 필요가 없어진다.
  // HINSTANCE hInstance = GetModuleHandle(NULL);
  // 윈도우 핸들이다
  HWND hWnd;
  // 윈도우 메시지이다.
  MSG Message;
  // 윈도우를 정의하는 구조체이다.
  WNDCLASS WndClass;
  // 윈도우의 초기 설정하는 값이다.
  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  // 윈도우의 메인 바탕색
  WndClass.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);
  // 윈도우의 기본 마우스 커서 형태
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  // 윈도우의 아이콘
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  // 윈도우의 인스턴스
  WndClass.hInstance = hInstance;
  // 윈도우의 메시지 callbak 함수
  WndClass.lpfnWndProc = WndProc;
  // 윈도우를 인식하기 위한 클래스 명
  WndClass.lpszClassName = L"Main";
  // 윈도우 메뉴 이름
  WndClass.lpszMenuName = NULL;
  // 윈도우 스타일
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  // OS에 윈도우를 등록한다.
  // 윈도우라는 것은 결국 Window OS에서 실행하는 것인데 다른 프로그램들과 동시에 관리하기 위해서는 OS가 Window의 정보를 알고 있어야 한다.
  // 그를 위해 등록한다.
  RegisterClass(&WndClass);
  // 윈도우 생성(CreateWindow의 함수와 차이는 첫번째 dwExStyle의 파라미터가 있느냐 없느냐의 차이이다. 저는 0을 사용했다.)
  // 1번째 파리미터는 확장 스타일로는 WS_EX_WINDOWEDGE, WS_EX_CLIENTEDGE, WS_EX_OVERLAPPEDWINDOW 등등인데 별 쓸모없다.
  // 2번째 파리미터는 윈도우 객체를 지정하는 문자열(예를 들면 static, button, scrollbar), 메인 윈도우는 WndClass.lpszClassName의 등록된 값을 넣는다.
  // 3번째 파라미터는 윈도우 메뉴 이름
  // 4번째 파라미터는 윈도우의 스타일이다. 닫기 버튼 x박스를 표시하는지 최소화, 최대화 버튼의 여부이다. WS_OVERLAPPEDWINDOW가 무난하다.
  // 5,6번째 파라미터는 윈도우 생성 시의 초기 위치 설청, 5번째는 x좌표(모니터에서 가로), 6번째는 y좌표(모니터에서 세로)
  // 7,8번째 파라미터는 윈도우 생성 시의 초기 크기 설정, 7번째는 너비(width), 8번째는 높이(height)
  // 9번째는 파라미터는 부모 윈도우 여부, 메인 윈도우는 부모 윈도우가 없기 때문에 NULL이다.
  // 10번째 파라미터는 메뉴,
  // 11번째는 프로그램 인스턴스
  // 12번째는 여분의 파라미터로 WndProc안에서 다른 윈도우들과 데이터를 공유하고자 할 때 사용된다.
  hWnd = CreateWindowEx(0, WndClass.lpszClassName, WndClass.lpszClassName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 200, 200, NULL, (HMENU)NULL, hInstance, NULL);
  // 이제 윈도우를 띄운다.
  ShowWindow(hWnd, nCmdShow);
  // 윈도우를 띄우고 마로 main thread가 종료되면 안된다. 그럼 프로그램이 종료하는 것.
  // 문한 루프의 형태를 만든다.
  // GetMessage로 Window 메시지에 값을 취득하고 값이 있으면 계속 true를 리턴할 것이다. 종료가 된다면 false되어 프로그램이 종료가 된다.
  while (GetMessage(&Message, NULL, 0, 0))
  {
    // 키보드 메시지를 받고 프로그램에서 사용할 수 있게 변환한다.
    TranslateMessage(&Message);
    // 메시지를 WndProc로 보내는 함수.
    DispatchMessage(&Message);
  }
  return 0;
}
// 메시지 처리 함수, 메시지를 처리하는 함수로 메시지란 유저의 입력 또는 OS에서 요구하는 메시지 등을 처리하는 함수
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  // 윈도우를 생성할 때의 구조, main에서 CreateWindowEx로 넣었던 파라미터 값이 있다.
  // 다른 윈도우를 생성하려면 인스턴스가 필요한데 여기서 취득한다.
  CREATESTRUCT *cs = (CREATESTRUCT*)lParam;
  switch (iMessage)
  {
  // 최초 윈도우가 생성되면 호출된다.
  case WM_CREATE:
  {
    // 파라미터는 위와 같다.
    // 2번째 파라미터에 button을 설정 했고, 부모는 Window Form에 클릭시 발생하는 메시지 번호
    CreateWindowEx(0, L"button", L"Click me", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 20, 20, 100, 25, hWnd, (HMENU)999, cs->hInstance, NULL);
  }
  return 0;
  // 윈도우에서 명령이 발생하면 여기로 호출 
  case WM_COMMAND:
  {
    switch (LOWORD(wParam))
    {
    // 이 999는 위 버튼을 눌렀을 때 발생하는 메시지
    case 999:
      // 메시지 박스 창이 뜰 것이다.
      MessageBox(hWnd, L"Hello world", L"Button", MB_OK);
    }
  }
  return 0;
  // OS에서 윈도우를 그리는 명령을 할 떄 발생하는 이벤트.
  case WM_PAINT:
  {
    // PAINTSTRUCT를 받고
    PAINTSTRUCT ps;
    // 그린 후
    HDC hdc = BeginPaint(hWnd, &ps);
    // 리소스 반환
    EndPaint(hWnd, &ps);
  }
  return 0;
  // 윈도우가 종료하라는 메시지가 발생하면
  case WM_DESTROY:
  {
    // 종료 메시지를 보낸다. 위 GetMessage가 False가 호출이 되고 main함수가 종료가 된다.
    PostQuitMessage(0);
  }
  return 0;
  default:
    // 위에 포함되어 있지 않는 메시지를 콘솔에 표시힌다.
    printf("0x%04X\n", iMessage);
  }
  // 그외의 메시지는 기본 처리를 하도록 설정한다.
  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}

hInstance를 main에서는 GetModuleHandle함수를 통해서 구했는데 WinMain은 파라미터로 구해다 주니 필요가 없어집니다.

2번째 파라미터는 이전 프로그램의 핸들이라고 하는 데, 제가 볼때 항상 NULL입니다. 필요없는 파라미터 인 것 같습니다.

3번째는 실행할 떄의 파라미터이고, 네번째는 실행 시의 화면의 크기, 최소화 또는 보통등의 옵션이 있는데 그걸 ShowWindow로 넘기면 윈도우 폼의 초기 설정을 할 수 있습니다.

콘솔 창은 없어지고 윈도우만 실행 되었습니다.


이번에는 버튼을 클릭해 보겠습니다.

Hello world라는 메시지 박스가 나왔습니다.

이는 WM_CREATE 메시지에서 작성한 button을 클릭할 때 999 메시지를 보내고 WM_COMMAND에서 999 메시지를 받으면 MessageBox를 실행하여 "Hello world"가 나오게 처리를 하였기 때문입니다.


여기까지가 Win32Api로 윈도우 폼을 만드는 방법입니다.


C#으로 윈도우를 만들어 보신분들은 Win32Api로 프로그램을 작성하는게 상당히 복잡하구나 싶으실 것입니다. 사실 이렇게 Window를 작성하면 엄청 힘듭니다. 버튼이 하나이지만 메뉴도 있고 여러 컨트롤이 있는 상황이라면 어머어마하게 복잡해 집니다.

그래서 C++로 작성하는 Window를 간단하게 작성하는 프로그램이 MFC입니다.


사실 Window.h의 라이브러리가 리눅스에서는 사용 안되는 ms전용 라이브러리입니다. 즉, 윈도우 환경에서만 사용할 수 있는 라이브러리인데 저 WinMain에서 WndProc를 처리하는 Win32Api도 g++라 다른 c++라이브러리에서는 사용이 안됩니다.

그래도 이 Win32Api로 작성하는게 워낙 힘들고 노가다이다보니 MFC(Microsoft Foundation Classes)가 생긴 것 같습니다.

이 MFC는 C++에서 Window를 작성을 할 수 있게 최적화된 라이브러리 집합이라고 생각하면 됩니다.


MFC를 사용하기 위해서는 먼저 Visual Studio를 설치 환경에서 MFC 개발 라이브러리가 설치 되어 있어야 합니다.

그럼 프로젝트 생성시에 MFC 프로젝트를 만들 수 있는 항목이 생깁니다.

그럼 적당히 프로젝트를 생성합니다.

그럼 이것 저것의 메뉴가 생기는데 간단히 에로 Dialog를 만들 것이기 때문에 Application Type을 Dialog로 설정합니다.

그럼 이것 저것 C#과 C++의 중간 정도의 모습의 소스가 자동 생성됩니다.

실행하면 위처럼 기본 다이아로그가 만들어집니다.


MFC의 라이브러리도 Win32Api만큼이나 방대합니다. 따로 MFC를 공부하지 않으면 MFC로 작성하기 어려울 정도입니다.

학교다닐때 C++를 익히고 MFC를 공부했지만 C++은 거의 기초만 공부하고 MFC로 바로 점프했습니다. 즉, Win32Api를 알고 MFC 라이브러리를 사용해도 좋지만 굳이 Win32Api를 몰라도 MFC를 사용할 수 있을 만큼 잘 되어 있습니다.

근데... C#이든 MFC든 윈도우 깊이 들어가고 원리를 공부하게 되면 결국 Win32Api로 돌아오더라구요.. 요즘엔 윈도우를 손 떼어서 이제는 옛날 말입니다.. 최근 블로그 때문에 다시 책 뒤지고 있습니다.ㅜㅜ


여기까지 C++에서 Window Form을 생성하는 방법(Win32Api, MFC)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.