C++ 윈도우 프로그래밍의 고전은 Win32 GUI 다.
지금이야 웹과 모바일앱의 전성시대 유튜브 시대를 맞이하여 윈도우 프로그래밍에 대해 진지하게 생각하지 않아도 된다. 웬만한 기술들은 웹브라우저에서 거의 다 구현이 가능하다. 굳이 하나의 애플리케이션 기능만 사용하는 윈도우 GUI를, exe 파일을 더블클릭해서 실행하지 않아도 된다. 아니 까먹게 된다.
사실 애초에 컴퓨터를 사용한다는 말은 애플리케이션을 사용한다는 말이었다. 게임을 하고, 워드를 사용하고, 엑셀프로그램을 돌리고... 그런 일이었다.
(거의 밥먹고 게임만 하지 않았냐)
현재는 컴퓨터를 사용한다는 말의 의미는 웹을 사용하는 것의 동의어가 되가고 있다.
얼마전 조카가 돌이 지났다. 2020년에 태어난 그 아이는 아마 종이신문이란 것을 읽지 않겠지라는 생각이 들었다.
요즘 사람들에게 뉴스기사는 인터넷 포탈에서 보는 것이다.
그리고 그 애가 좀 더 자라면 유튜브로 키즈 채널을 보겠지. 다음 세대에는 Win32 GUI 같은 것을 쓸 것 같지가 않다.
하지만 컴퓨터 도입의 초창기에 애플리케이션은 그야말로 갑이었다. 프로그램 패키지는 지금처럼 다운로드 서비스가 아니라 손에 잡히는 패키지 였다. 패키지 언박싱의 감동이 있었다.
겜돌이 아재라면 스타 확장팩 패키지나 디아블로2 패키지를 한번쯤 집에 모셨던 경험이 있을 것이다.
소프트웨어가 직접 손으로 만져지는 시대에 중요한 역할을 한 것은 Win32 UI 였다.
기업에서는 마이크로 소프트 엑셀 같은 애플리케이션이 최고의 도구였다. 지금도 넘사벽이긴 하지만 당시에는 더 최신의 느낌이었다.
CLI 환경을 막 벗어난 쎄끈한 모습을 보여줬기 때문이다.
CLI 스프레드시트 엑셀 이전에 이것들을 기업에서 사용했었다.
흠... 지금보니 나쁘지 않은데... 그러나 엑셀은 그보다 많은 기능을 제공했다.
Win32 프로그래밍도 지금은 과거의 유산으로 가고 있나 조금은 생각이 든다. 무엇보다 국내에서의 개발자 수가 많이 줄어든 느낌이다.
자바 스프링을 국가 표준 프레임워크로 지정한 이유도 있을 것이고 지금은 사용하는 사람만 사용한다는 생각이 든다. 적어도 요새 핫한 파이썬이나 자바스크립트 웹개발에 비하면 화제성이 떨어지는 것도 사실이다.
이제 MS에서 더 지원할게 있나 싶기도 하고...
이 블로그는 사람들에게 관심이 덜한 포스팅도 하기로 했기 때문에 Win32도 다뤄본다.
가장 간단한 윈도우창을 만들어본다. 비주얼 스튜디오 2019에서 만들 것이다. 다운로드와 설치는 아래 사이트에서 한다.
https://visualstudio.microsoft.com/ko/downloads/
전체 코드는 아래와 같다. 하나씩 순차적으로 읽을 필요는 없다. 대부분 비주얼 스튜디오가 생성하는 코드이다.
// WindowProject1.cpp : 애플리케이션에 대한 진입점을 정의합니다.
//
#include "framework.h"
#include "WindowProject1.h"
#define MAX_LOADSTRING 100
// 전역 변수:
HINSTANCE hInst; // 현재 인스턴스입니다.
WCHAR szTitle[MAX_LOADSTRING]; // 제목 표시줄 텍스트입니다.
WCHAR szWindowClass[MAX_LOADSTRING]; // 기본 창 클래스 이름입니다.
// 이 코드 모듈에 포함된 함수의 선언을 전달합니다:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 여기에 코드를 입력합니다.
// 전역 문자열을 초기화합니다.
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_WINDOWPROJECT1, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// 애플리케이션 초기화를 수행합니다:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WINDOWPROJECT1));
MSG msg;
// 기본 메시지 루프입니다:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
//
// 함수: MyRegisterClass()
//
// 용도: 창 클래스를 등록합니다.
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WINDOWPROJECT1));
wcex.hCursor = LoadCursor(nullptr, IDC_HAND);
wcex.hbrBackground = (HBRUSH)(COLOR_HIGHLIGHTTEXT +1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WINDOWPROJECT1);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassExW(&wcex);
}
//
// 함수: InitInstance(HINSTANCE, int)
//
// 용도: 인스턴스 핸들을 저장하고 주 창을 만듭니다.
//
// 주석:
//
// 이 함수를 통해 인스턴스 핸들을 전역 변수에 저장하고
// 주 프로그램 창을 만든 다음 표시합니다.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 인스턴스 핸들을 전역 변수에 저장합니다.
HWND hWnd = CreateWindowW(szWindowClass,
L"Creating Windows", // Window Title Name
WS_OVERLAPPEDWINDOW,
300, 200, // x,y screen
800, 700, // width height
nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
//
// 함수: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 용도: 주 창의 메시지를 처리합니다.
//
// WM_COMMAND - 애플리케이션 메뉴를 처리합니다.
// WM_PAINT - 주 창을 그립니다.
// WM_DESTROY - 종료 메시지를 게시하고 반환합니다.
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 메뉴 선택을 구문 분석합니다:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
TCHAR greeting1[] = _T("Hello, Windows desktop!");
TCHAR greeting2[] = _T("WIN APPLICATION TEST");
TCHAR greeting3[] = _T("in Visual Studio 2019");
// TODO: 여기에 hdc를 사용하는 그리기 코드를 추가합니다...
FillRect(hdc, &ps.rcPaint, (HBRUSH)(COLOR_HIGHLIGHT+ 1));
TextOut(hdc, 5, 5, greeting1, _tcslen(greeting1));
TextOut(hdc, 5, 25, greeting2, _tcslen(greeting2));
TextOut(hdc, 5, 45, greeting3, _tcslen(greeting3));
MoveToEx(hdc, 100, 100, NULL);
LineTo(hdc, 500, 200);
Rectangle(hdc, 300, 100, 200, 200);
Ellipse(hdc, 300, 300, 450, 450);
EndPaint(hWnd, &ps);
break;
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// 정보 대화 상자의 메시지 처리기입니다.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
웹에서 검색을 하다보니 비주얼 스튜디오는 새로운 버전이 나올때마다 자료형이나 상수, 헤더파일 들을 조금씩 변경한다. 그래서 과거 버전의 비주얼 스튜디오로 설명한 웹페이지나 책들을 보고 있다면 코드를 약간씩 수정 해야한다.
(MS만의 특징이다. 오픈소스 진영은 이것을 싫어한다. 오픈소스로 가려고 하는 MS지만 과거에는 나름의 사정이 있었다... 고 말한다.)
ANSI 표준을 따르는 30년전 커니핸의 책을 봐도 C언어를 배울 수 있다. 그러나 비주얼 스튜디오는 과거의 책을 참고하다가는 골탕을 먹을 수 있다. 비주얼 스튜디오는 가끔 보면 프레임워크 그 자체 같다. 비주얼 스튜디오로 C++ 개발을 하다보면 다른 IDE로 갈아타기가 어려워진다(가두리 양식). 한편 VC++이 윈도우 프로그램을 개발하기 위한 최고의 개발환경인 것도 사실이다.
2000년대 초반의 컴퓨팅 환경은 지금과 많이 달랐다. 지금이야 오픈소스가 큰몫을 하지만 당시의 상업적 가치는 다르게 평가 받았다. 애플리케이션을 현금으로 구매하여 버스를 타고 집에 와서 스스로 컴퓨터에 설치해야 했다.
소프트웨어를 사용하기 위해서는 구글 스토어에서 손가락을 비벼 설치하는 것보다 많은 것을 요구했다. 용산에 직접 가서 비싼 돈을 주고 산 소프트웨어가 하드웨어의 호환성같은 문제로 실행이 불가능하다는 사실을 알았을 때는 절망했다. 할 수 있는게 없었다.
그런 시대상황을 고려하지 않고 지금의 기준으로 통일하여 비교할 수는 없을 것이다. MS를 적극적으로 이해할 필요는 없지만 흘러간 과거를 당시의 맥락에서 바라볼 필요는 있다.
(당시의 맥락 : 손님 맞을래요? 였는지도 모른다... 싫으면 윈도우를 쓰지 말던가 ㅉㅉ)
Win32 프로그래밍은 웬만하면 최신의 비주얼 스튜디오로 개발하도록 한다. MinGW 등의 C컴파일러로도 물론 가능하지만 설정하는게 조금 더 복잡하고 사용하는 자료형이 조금씩 차이가 있다.
더 많은 영어문서를 읽어야 한다
시작하면 프로젝트 -> 새로만들기에서 Windows 데스크톱 애플리케이션을 선택한다.
프로젝트 만들기를 클릭한다.
아래는 초기값 그대로 생성된 윈도우다.
// WindowsProject1.cpp : 애플리케이션에 대한 진입점을 정의합니다.
//
#include "framework.h"
#include "WindowsProject1.h"
#define MAX_LOADSTRING 100
// 전역 변수:
HINSTANCE hInst; // 현재 인스턴스입니다.
WCHAR szTitle[MAX_LOADSTRING]; // 제목 표시줄 텍스트입니다.
WCHAR szWindowClass[MAX_LOADSTRING]; // 기본 창 클래스 이름입니다.
// 이 코드 모듈에 포함된 함수의 선언을 전달합니다:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 여기에 코드를 입력합니다.
// 전역 문자열을 초기화합니다.
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_WINDOWSPROJECT1, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// 애플리케이션 초기화를 수행합니다:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WINDOWSPROJECT1));
MSG msg;
// 기본 메시지 루프입니다:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
//
// 함수: MyRegisterClass()
//
// 용도: 창 클래스를 등록합니다.
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WINDOWSPROJECT1));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WINDOWSPROJECT1);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassExW(&wcex);
}
//
// 함수: InitInstance(HINSTANCE, int)
//
// 용도: 인스턴스 핸들을 저장하고 주 창을 만듭니다.
//
// 주석:
//
// 이 함수를 통해 인스턴스 핸들을 전역 변수에 저장하고
// 주 프로그램 창을 만든 다음 표시합니다.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 인스턴스 핸들을 전역 변수에 저장합니다.
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
//
// 함수: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 용도: 주 창의 메시지를 처리합니다.
//
// WM_COMMAND - 애플리케이션 메뉴를 처리합니다.
// WM_PAINT - 주 창을 그립니다.
// WM_DESTROY - 종료 메시지를 게시하고 반환합니다.
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 메뉴 선택을 구문 분석합니다:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 여기에 hdc를 사용하는 그리기 코드를 추가합니다...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// 정보 대화 상자의 메시지 처리기입니다.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
그전 버전과 무엇이 바뀌었는지 보면 헤더파일이 바뀌었다.
전 버전의 sdstdafx.h 헤더 파일이 framework.h 로 바뀌었다. 들어가 보면 #pragma once 로 중복 전처리 방지를 하고 헤더들을 포함하고 있다. 이것들은 예전 비주얼 스튜디오에서 포함되던 헤더파일들이다.
실행시키면 민짜의 윈도우가 생성된다. 여기까지도 아주 좋다. 손하나 까딱하지 않아도 윈도우를 만들었다.
그럼 심심하니까 텍스트와 도형을 출력해보자.
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
TCHAR greeting1[] = _T("Hello, Windows desktop!");
TCHAR greeting2[] = _T("WIN APPLICATION TEST");
TCHAR greeting3[] = _T("in Visual Studio 2019");
// TODO: 여기에 hdc를 사용하는 그리기 코드를 추가합니다...
FillRect(hdc, &ps.rcPaint, (HBRUSH)(COLOR_HIGHLIGHT+ 1));
TextOut(hdc, 5, 5, greeting1, _tcslen(greeting1));
TextOut(hdc, 5, 25, greeting2, _tcslen(greeting2));
TextOut(hdc, 5, 45, greeting3, _tcslen(greeting3));
MoveToEx(hdc, 100, 100, NULL);
LineTo(hdc, 500, 200);
Rectangle(hdc, 300, 100, 200, 200);
Ellipse(hdc, 300, 300, 450, 450);
EndPaint(hWnd, &ps);
break;
}
case 가 들어있는 곳은 윈도우 이벤트 처리를 하는 곳이다.
WM_PAINT 는 윈도우에 그려주는 역할을 한다. 글씨를 쓰거나 그림을 그릴때 사용한다.
TCHAR 이런 자료형들이 막 나온다. TCHAR 는 유니코드 문자형 포인터이다. WIN32 프로그래밍에는 문자 자료형이 엄청나게 많다. 유니코드를 쓰기 위해서 이다. 확실히 자바나 C# 에는 이런 과정이 없는데 C++ 은 문자열 처리에 더 신경써야한다. _T 매크로도 마찬가지 이다. 넣으면 ASCII 코드가 아니라 UNICODE로 컴파일된다.
이 또한 매우 방대한 내용이니 아래의 자료를 참고하길 바란다.(원문임)
C++ 문자열 완벽 가이드
https://www.codeproject.com/Articles/2995/The-Complete-Guide-to-C-Strings-Part-I-Win32-Chara
https://www.codeproject.com/Articles/3004/The-Complete-Guide-to-C-Strings-Part-II-String-Wra
아래의 텍스트아웃은 문자열을 윈도우에 출력한다. 매개변수인 hdc는 장치 컨텍스트 핸들로 윈도우 인스턴스(메모리에 올라온 실제 윈도우)를 관리한다. 그 다음은 hdc 가 핸들하는 윈도우 안에서의 좌표 x, y , y는 행으로 보면 된다.
greeting은 유니코드 문자열의 포인터, 마지막 매개변수는 문자열의 개수이다. _t로 매크로 적용된 것은 기존 ASCII 자료형을 유니코드로 바꾼 것들이 많다.
TextOut(hdc, 5, 5, greeting1, _tcslen(greeting1));
TextOut(hdc, 5, 25, greeting2, _tcslen(greeting2));
TextOut(hdc, 5, 45, greeting3, _tcslen(greeting3));
이렇게 하면 기본 문자열을 출력할 수 있다.
다음은 도형을 그린다. 첫번 째 선을 그릴려면 MoveToEx 로 선의 시작점으로 위치한다. 여기서도 hdc를 줘야한다. hdc를 줘서 어느 윈도우에 그릴지 선택하는 것이다. 나머지는 좌표를 넣으면 된다.
MoveToEx(hdc, 100, 100, NULL);
LineTo(hdc, 500, 200);
Rectangle(hdc, 300, 100, 200, 200);
Ellipse(hdc, 300, 300, 450, 450);
프레임워크는 보통 동작방법이 정해져있어서 프레임워크가 정한 범위를 벗어나는 조작을 하는 것은 권장하지 않는다.
그게 한편으로 프레임워크의 안정성을 보장하기도 하지만 성능이나 기능의 제한을 걸기도 한다. 그러니까 사용하라는데로만 사용하면 된다. 텍스트 출력이나 도형을 그리는 방식은 웬만한 GUI 프레임워크에서 다 가지고 있다. 사용자는 이들을 조합해서 비즈니스 로직을 짜는데 집중하도록 한다.
새로 프로그래밍 언어를 배우는 사람들은 자바나 파이썬, 혹은 웹 개발환경으로 가고 있다. 이러다보니 점점 Win32 GUI 를 접하기가 어려워지기 때문에 이것들을 외우겠다는 생각을 할 필요가 없다. 또 IDE 에서 인텔리센스를 잘 활용하면 손가락이 편해지고 시간이 단축된다. 다만 비주얼 스튜디오는 계속 업데이트를 하기 때문에 변화에 대한 체크는 할 필요가 있다.
마지막으로 createWindow 만 잠깐 본다. 매개변수 중에 두번째 윈도우 타이틀을 바꾸면 창의 타이틀이 변경된다.
그 다음다음 숫자는 초기 윈도우 위치와 크기를 넣을 수 있다. 주의할 점은 크기가 커질 수록 x와 y는 왼쪽 상단에서 시작해서 오른쪽 하단으로 내려온다는 것이다. 윈도우를 다루는 GUI 프로그래밍은 거의 같다. 좌상단이 0,0이고 우하향한 좌표 x, y 가 width, height 너비, 높이 이다.
HWND hWnd = CreateWindowW(szWindowClass,
L"Creating Windows", // Window Title Name
WS_OVERLAPPEDWINDOW,
300, 200, // x,y screen
800, 700, // width height
nullptr, nullptr, hInstance, nullptr);
win32 프로그램은 비주얼 스튜디오가 업데이트 될때마다 코드가 조금씩 바뀌어서 빡치는 경우가 있다. 조금의 바뀐 부분을 찾기 위해 소스코드를 계속 읽어야 한다.
그래도 원리를 터득하면 적응하기는 어렵지 않을 것이다.
아래는 참고 사이트이다. MSDN은 설명도 번역도 좀 무성의하지만 역시 레퍼런스는 아직도 쓸만하다. C#과 닷넷은 MS공식 동영상 튜토리얼도 있던데... 이쪽은 방치한건가...
https://docs.microsoft.com/en-us/windows/win32/learnwin32/your-first-windows-program
소은 웹사이트는 아주 오래된 사이트지로써 2000년 이후에 1000만명이 방문한 성지다. 레퍼런스로써 WIN32를 공부하는 사람이 있다면 한번쯤 방문해보는 것을 추천한다. (웹의 디자인을 보면 인터넷의 추억에 빠질 수도 있다)