int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
콘솔 프로그램에서 main처럼 WINAPI에서의 main(진입점)이라고 보면 된다. 여기서 APIENTRY는 __stdcall 함수 호출규약을 의미하는데 #define으로 정의되어 있다.
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
WINAPI, APIENTRY, CALLBACK 모두 __stdcall로 정의되어 있는데 왜 여러개를 같은거로 정의해놨는지 의문이 든다. 이유는 코드를 분석할 때 가독성있게 작성하기 위해 진입점은 APIENTRY로 표현하고 콜백 프로시저는 CALLBACK, 기타 윈도우즈 API에서 약속한 함수 포인터 형식은 WINAPI로 표현한다. 참고로 윈도우즈 API에서는 표준 함수 호출 규약으로 PASCAL 방식을 따르고 있다고 한다. 인자들에 대해서 설명하자면
- HINSTANCE hInstance : 운영체제가 프로그램들을 구별할때 쓰는 인스턴스 핸들. 프로그램마다 고유한 값을 가지고 있다.
- HINSTANCE hPrevInstance : 현재는 사용이 안돼서 항상 NULL. 과거 16비트와 호환성때문에 사용됐었음.
- LPWSTR lpCMdLine :명령행으로 입력된 프로그램 인수. 도스의 argv인수에 해당하며 실행 직후에 파일 경로를 전달한다.
- int nCmdShow :프로그램이 실행될 형태. 최소화, 보통, 최대화 등을 전달.
참고로 wWinMain의 인자들 타입앞에 붙어있는 _In_이나 _In_opt_는 Microsoft의 SAL(Standard Annotation Language)로써, SAL은 소스코드의 결과에 영향을 끼치진 않으면서 동시에 코드 작성자의 의도나 코드 의미를 분명히 해줄 수 있다는 점에서 가독성을 높혀줄 수 있다. _In_과 _IN_OPT의 의미는 아래와 같다.
1. 초기화(Init) 관련 함수들
ATOM MyRegisterClass()
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = CCore::WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = m_hInst;
wcex.hIcon = LoadIcon(m_hInst, MAKEINTRESOURCE(IDI_ICON1));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL; //MAKEINTRESOURCEW(IDC_MY210111);
wcex.lpszClassName = L"Hello";
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_ICON1));
return RegisterClassExW(&wcex);
}
// typedef unsigned short WORD;
// typedef WORD ATOM;
// 함수 마지막에 return RegisterClassExW(&wcex); 호출함으로써
// MyRegisterClass 내부에서 설정한 class를 실제로 등록해줘야함
#define UNICODE가 되어 있다면
typedef WNDCLASSEXW WNDCLASSEX;
이렇게 typedef이 되어있고 UNICODE가 정의되어 있지 않다면
typedef WNDCLASSEXA WNDCLASSEX;
이렇게 되어 있다.
WNDCLASSEXW와 WNDCLASSEXA의 다른점은 아래 멤버중에 lpszMenuName와 lpszClassName이다. WNDCLASSEXW는 unicode 문자열을 담기 위해 두개의 멤버를 LPCWSTR타입(= long pointer constant wide string = const w_char *)으로 선언했고, WNDCLASSEXA는 두 멤버를 LPCSTR타입(= long pointer constant string = const char *)타입으로 선언해두었다.
- cbsize : WNDCLASSEXW 구조체의 크기. sizeof(WNDCLASSEX)로 할당해준다.
- style : CS_HREDRAW | CS_VREDRAW로 할당. 윈도우 크기를 변경하면 다시 그린다는 의미
- lpfnWndProc = 메세지 처리 함수. WndProc으로 할당
- cbClsExtra : 클래스를 위한 여분 메모리. 사용안하므로 0
- cbWndExtra : 윈도우를 위한 여분 메모리. 사용안하므로 0
- hInstance : wWinmain에서 받은 인스턴스 핸들. wWinmain에서 받은걸 따로 저장해뒀다가 여기에도 할당해주자
- hIcon : 기본 아이콘의 ID값. LoadIcon(인스턴스 핸들, MAKEINTRESOURCE(IDI_ICON1)); 과 같이 할당. 기본 아이콘을 만드는 방법은 리소스 파일 우클릭 > 추가 > 리소스 하면 다음과 같은 창이 나오는데 여기서 아이콘을 누른다
아이콘을 새로 만들면 아래와 같이 리소스 파일에 icon1.ico파일이 생기고 resource.h 파일에 들어가면 아래와 같이 방금 만든 아이콘에 대한 ID값이 정의되어 있다. resource.h 파일의 값을 사용했으므로 resource.h를 include 해주는 것도 잊지 말자.
- hCursor : 기본 커서 설정. LoadCursor(nullptr, IDC_ARROW);
- hbrBackground : (HBRUSH)(COLOR_WINDOW + 1); 뒤에 0~3을 더하는지에 따라 윈도우 배경이 달라진다고 한다.
COLOR_WINDOW는 매크로로 정의된 상수이며 5를 의미한다. 그러면 그냥 6을 의미하는 매크로를 넣으면 되는데 왜 5 + 1이렇게 넣어주는지 궁금해서 찾아봤다. 내가 찾은 바에 의하면 COLOR_WINDOW말고도 여러 매크로(COLOR_MENU, COLOR_WINDOWFRAME 등)가 있고 그 매크로들은 0부터 다양한 숫자를 의미하는데, 0을 의미하는 매크로를 넣으면 NULL과 구별할 수 없어서 관례상 6을 넣을 때도 5 + 1 이런식으로 넣어준다고 한다. 그러니 위에서 COLOR_WINDOW + 1은 실제로 COLOR_WINDOWFRAME(6으로 정의되어있음) 즉 6을 할당하는 것과 같다.
- lpszMenuName : 메뉴 설정, 사용하지 않을 거라면 NULL
- lpszClassName : 등록할 클래스 이름
- hIconSm : 작은 아이콘 등록. hIcon 멤버와 같은 방식으로 같은 아이콘으로 등록해주자. LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_ICON1));으로 등록
이렇게 wcex 구조체 멤버 설정을 다 했으면 마지막에 반드시 return RegisterClassExW(&wcex); 처럼 리턴하면서 설정한 클래스를 등록해줘야 한다.
HWND CreateWindowW(
LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCE hInstance,
LPVOID lpParam);
윈도우를 생성하는 함수이다
- lpClassName : 위에 MyRegisterClass에서 lpszClassName에 할당해줬던 클래스 이름을 쓰자
- lpWindowName : 윈도우가 생성되면 타이틀바에 적히는 이름
- dwStyle : 윈도우의 스타일(https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles?redirectedfrom=MSDN 참고). 나의 경우 WS_OVERLAPPEDWINDOW를 썼는데 가장 평범한 모양의 윈도우가 만들어진다.
- x, y : 윈도우가 만들어지게 될 시작 좌표
- nWidth, nHeight : 윈도우 가로, 세로 크기
- hWndParent : 생성될 이 윈도우를 가질 부모의 핸들. 없으면 NULL
- hMenu : 메뉴. 없으면 NULL
- hInstance : 이전에 wWinMain에서 받아두었던 인스턴스 핸들을 할당하자.
- lpParam : NULL
CreateWindowW를 리턴타입은 윈도우핸들(HWND)인데 반드시 리턴값을 받아서 전역변수든 클래스 멤버변수로든 저장해두자. 뒤에 SetWindowPos에서 클라이언트 영역 크기를 변경하거나 메세지를 처리하는 WndProc함수, 그외 다양한 부분에서 쓰인다.
그리고 필수적인건 아니지만 편의상 사용하면 나중에 편한 함수들이 있다.
BOOL AdjustWindowRect(LPRECT lpRect, DWORD dwStyle, BOOL bMenu);
- lpRect : 원하는 4개의 좌표로 클라이언트 영역을 담은 사각형 구조체 RECT의 주소값을 넣어주면 된다. 결과로 그에 대응되는 윈도우 크기가 RECT에 할당되어 있을 것이다.
- dwStyle : 윈도우 스타일. CreateWindowW에서 dwStyle에 준 것 처럼 맞춰서 주면 된다.
- bMenu : 메뉴 여부. 없으면 FALSE.
위 함수는 내가 원하는 크기의 클라이언트 영역을 1번째 인자로 주면 그 클라이언트 영역에 맞게 필요한 윈도우 창 크기를 1번째 인자에 다시 넣어준다. 우리가 흔히 말하는 해상도(ex. 1280X720)들은 이 클라이언트 영역을 보고 이야기하는 것이므로 꼭 클라이언트 영역을 맞게 설정해줘야 한다. 참고로 클라이언트 영역과 윈도우의 차이는 아래 그림을 보면 쉽게 이해된다.
왼쪽의 빨간색 영역이 클라이언트 영역(혹은 작업 영역이라고도 부른다)이라고 부르며 오른쪽이 윈도우 영역을 의미한다.
이제 내가 원하는 클라이언트 영역에 맞는 윈도우 크기를 얻었다면 그 크기만큼 윈도우 크기를 재설정해줘야 한다. 윈도우 크기를 재설정해주는 함수는 아래 SetWindowPos() 함수이다.
BOOL SetWindowPos(HWND hWnd, HWND hWndInsertAfter, int X, int Y, int cx, int cy, UINT uFlags);
- hWnd : CreateWindowW에서 받아둔 윈도우 핸들을 대입하자
- hWndInsertAfter : Z-order를 설정할 수 있다. 윈도우끼리 겹쳐질때 가장 위에 오게하고 싶으면 HWND_TOPMOST를 넣어주자
- X, Y : 윈도우 출력 좌표
- cx, cy : 위에 AdjustWindowRect에서 구조체 lpRect에 할당해준 윈도우 가로, 세로 크기
- uFlags : https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos 참고
BOOL UpdateWindow(HWND hWnd);
왜 ShowWindow()를 호출하고 뒤이어 바로 UpdateWindow()를 호출하냐면 ShowWindow()는 VM_PAINT메세지가 발생시키긴 하지만 바로 이것을 처리하진 않는다. 하지만 UpdateWindow()는 윈도우 프로시져를 직접 호출해서 곧바로 VM_PAINT메세지가 처리되도록 한다. CPU처리 속도가 빠르지 않았던 과거의 잔재라고 한다(출처 : http://egloos.zum.com/otisnate/v/673115).
2. 실행(Run) 관련 함수들
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
보통 이렇게 코드를 구성하며 이 세가지 함수로 이루어진 루프를 '메세지 루프'라고 부른다.
BOOL PeekMessage(
LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax,
UINT wRemoveMsg
);
- lpMsg : 메시지 정보를 담을 MSG타입 구조체 주소값
- hWnd : 이전에 갖고 있는 윈도우 핸들을 넘겨주자. hWnd파라미터에 정의된 윈도우와 그 자식윈도우와 관련된 메세지만 보게된다. 주로 nullptr을 넘기는데 이러면 현재의 thread를 호출한 윈도우와 관련된 메시지를 보게된다.
- wMsgFilterMin : 받을 메세지 타입의 시작(ex. WM_KEYFIRST (0x0100), WM_MOUSEFIRST (0x0200) ). 0을 넣으면 PeekMessage는 모든 종류의 메세지를 받는다
- wMsgFilterMax : 받을 메세지 타입의 끝(ex. WM_KEYLAST, WM_MOUSELAST). 0을 넣으면 PeekMessage는 모든 종류의 메세지를 받는다
- wRemoveMsg : 아래와 같은 옵션들을 쓸 수 있는데 보통 PM_REMOVE(메세지를 큐에서 가져온 후 큐에서 삭제)를 많이 쓴다
PeekMessage는 메세지 큐에서 메세지를 가져오는데, GetMessage함수와 다르게 큐에 메세지가 없으면 블로킹 상태가 되지 않고 바로 0을 리턴한다.
BOOL TranslateMessage(const MSG *lpMsg);
- lpMsg : MSG구조체 포인터. PeekMessage가 가져온 메세지를 가공한다. 가공한다는 의미는 예를 들어 키보드 A를 눌렀다 떼면 WM_KEYDOWN, WM_CHAR, WM_KEYUP 이 세 메세지가 동시에 발생하는데 WM_CHAR는 사용자에 의해 발생하는 메세지가 아니다. 키보드에 의해 전달되는 메세지는 WM_KEYDOWN, WM_KEYUP 둘 뿐이며, WM_CHAR는 TranslateMessage가 눌려진 키가 정말 문자키가 맞는지 검사해보고 맞다면 그때 메세지큐로 WM_CHAR를 넣어주는데 이게 TranslateMessage가 메세지를 가공하는 과정이다.
LRESULT DispatchMessage(const MSG *lpMsg);
- lpMsg : MSG구조체 포인터.
DispatchMessage() 함수는 시스템 메시지 큐에서 꺼낸 메시지를 프로그램의 메시지 처리 함수(WndProc)로 전달한다.
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
실제 메세지 큐에서 가져온 메세지를 처리하는 함수. 그런데 내가 추가해서 동작하는 코드들은 메세지를 처리하는 시간 외에 시간(흔히 데드타임이라고 부름)에 처리하도록 하기 때문에 WndProc에 추가적인 코드를 쓰거나 WndProc을 직접호출하는 경우는 없을 것이다. 다만 WndProc을 클래스(ex. Core클래스)의 멤버 함수로 선언할 수도 있을텐데, MyRegisterClass에서 wcex.lpfnWndProc 멤버는 전역함수 포인터만 받는다. 따라서 멤버 함수를 전역 함수 취급해주기 위해서 반드시 static으로 선언해주어야 한다.
Reference
https://docs.microsoft.com/ko-kr/cpp/code-quality/understanding-sal?view=msvc-160
http://ehpub.co.kr/tag/windows-h/
https://m.blog.naver.com/tipsware/221004018862
http://www.soen.kr/lecture/win32api/lec4/lec4-1-4.htm
https://skmagic.tistory.com/282
'공부 > WINAPI' 카테고리의 다른 글
인코딩 변환 함수(MultiByteToWideChar(), WideCharToMultiByte()) (0) | 2021.12.10 |
---|---|
lstrlen (0) | 2021.10.01 |
멀티 바이트/유니코드 문자열 만들어주기(sprintf_s, wsprintf) (0) | 2021.08.20 |
WINAPI 빌드 시 "LNK1120 : 확인할 수 없는 외부 기호라는 링크 에러" 발생 시 해결 방법 (0) | 2021.08.19 |
WINAPI 시작하기 - 초기 세팅 (0) | 2021.08.02 |