창 메시지(Win32 및 C++ 시작)
GUI 애플리케이션은 사용자와 운영 체제의 이벤트에 응답해야 합니다.
- 사용자의 이벤트에는 마우스 클릭, 키 스트로크, 터치 스크린 제스처 등 사용자가 프로그램과 상호 작용할 수 있는 모든 방법이 포함됩니다.
- 운영 체제의 이벤트에는 프로그램의 동작 방식에 영향을 줄 수 있는 프로그램의 "외부" 항목이 포함됩니다. 예를 들어 사용자가 새 하드웨어 디바이스를 연결하거나 Windows가 저전력 상태(절전 또는 최대 절전 모드)로 전환될 수 있습니다.
이러한 이벤트는 프로그램이 실행되는 동안 거의 모든 순서로 언제든지 발생할 수 있습니다. 실행 흐름을 미리 예측할 수 없는 프로그램을 구성하려면 어떻게 하나요?
이 문제를 해결하기 위해 Windows는 메시지 전달 모델을 사용합니다. 운영 체제는 메시지를 전달하여 애플리케이션 창과 통신합니다. 메시지는 단순히 특정 이벤트를 지정하는 숫자 코드입니다. 예를 들어 사용자가 마우스 왼쪽 단추를 누르면 창에 다음 메시지 코드가 포함된 메시지가 표시됩니다.
#define WM_LBUTTONDOWN 0x0201
일부 메시지에는 관련 데이터가 있습니다. 예를 들어 WM_LBUTTONDOWN 메시지에는 마우스 커서의 x 좌표와 y 좌표가 포함됩니다.
창에 메시지를 전달하기 위해 운영 체제는 해당 창에 등록된 창 프로시저를 호출합니다. (이제 창 프로시저가 무엇인지 알 수 있습니다.)
메시지 루프
애플리케이션은 실행되는 동안 수천 개의 메시지를 받게 됩니다. (모든 키 입력 및 마우스 단추 클릭은 메시지를 생성합니다.) 또한 애플리케이션에는 각각 고유한 창 프로시저가 있는 여러 창이 있을 수 있습니다. 프로그램에서 이러한 모든 메시지를 수신하고 올바른 창 프로시저로 전달하려면 어떻게 해야 할까요? 애플리케이션에는 메시지를 검색하고 올바른 창으로 디스패치하는 루프가 필요합니다.
창을 만드는 각 스레드에 대해 운영 체제는 창 메시지에 대한 큐를 만듭니다. 이 큐는 해당 스레드에서 만든 모든 창에 대한 메시지를 포함합니다. 큐 자체는 프로그램에서 숨겨집니다. 큐를 직접 조작할 수는 없습니다. 그러나 GetMessage 함수를 호출하여 큐에서 메시지를 끌어올 수 있습니다.
MSG msg;
GetMessage(&msg, NULL, 0, 0);
이 함수는 큐의 맨 처음에서 첫 번째 메시지를 제거합니다. 큐가 비어 있으면 함수는 다른 메시지가 큐에 대기될 때까지 차단됩니다. GetMessage 블록이 있으면 프로그램이 응답하지 않는 경우가 없습니다. 메시지가 없으면 프로그램에서 수행할 작업이 없습니다. 백그라운드 처리를 수행해야 하는 경우 GetMessage가 다른 메시지를 기다리는 동안 계속 실행되는 추가 스레드를 만들 수 있습니다. (창 프로시저에서 병목 현상 방지를 참조하세요.)
GetMessage의 첫 번째 매개 변수는 MSG 구조체의 주소입니다. 이 함수가 성공하면 메시지에 대한 정보로 MSG 구조체를 채웁니다. 여기에는 대상 창 및 메시지 코드가 포함됩니다. 다른 세 가지 매개 변수를 사용하면 큐에서 가져오는 메시지를 필터링할 수 있습니다. 거의 모든 경우에 이러한 매개 변수를 0으로 설정합니다.
MSG 구조체에는 메시지에 대한 정보가 포함되어 있지만 이 구조체를 직접 검사하지는 않습니다. 대신 두 개의 다른 함수에 직접 전달합니다.
TranslateMessage(&msg);
DispatchMessage(&msg);
TranslateMessage 함수는 키보드 입력과 관련이 있습니다. 키 입력(키 누르기, 키 떼기)을 문자로 변환합니다. 이 함수가 어떻게 작동하는지를 실제로 알 필요는 없습니다. DispatchMessage 전에 호출하기만 하면 됩니다.
DispatchMessage 함수는 운영 체제에 메시지의 대상인 창의 창 프로시저를 호출하도록 지시합니다. 즉, 운영 체제는 창 테이블에서 창 핸들을 조회하고, 창과 연결된 함수 포인터를 찾은 다음, 함수를 호출합니다.
예를 들어 사용자가 마우스 왼쪽 단추를 누르고 있다고 가정합니다. 이로 인해 다음 이벤트 체인이 발생합니다.
- 운영 체제는 메시지 큐에 WM_LBUTTONDOWN 메시지를 배치합니다.
- 프로그램에서 GetMessage 함수를 호출합니다 .
- GetMessage는 큐에서 WM_LBUTTONDOWN 메시지를 끌어와 MSG 구조체를 채웁니다.
- 프로그램에서 TranslateMessage 및 DispatchMessage 함수를 호출합니다.
- DispatchMessage 내에서 운영 체제는 창 프로시저를 호출합니다.
- 창 프로시저는 메시지에 응답하거나 무시할 수 있습니다.
창 프로시저가 반환되면 DispatchMessage로 돌아갑니다. 그러면 다음 메시지의 메시지 루프로 돌아갑니다. 프로그램이 실행되는 한, 메시지는 큐에 계속 도착합니다. 따라서 큐에서 메시지를 지속적으로 끌어와 디스패치하는 루프가 있어야 합니다. 이 루프는 다음을 수행하는 것으로 생각할 수 있습니다.
// WARNING: Don't actually write your loop this way.
while (1)
{
GetMessage(&msg, NULL, 0, 0);
TranslateMessage(&msg);
DispatchMessage(&msg);
}
물론 앞서 설명된 것처럼 이 루프는 결코 끝나지 않을 것입니다. 여기서 GetMessage 함수의 반환 값이 들어옵니다. 일반적으로 GetMessage는 0이 아닌 값을 반환합니다. 애플리케이션을 종료하고 메시지 루프를 중단하려면 PostQuitMessage 함수를 호출합니다.
PostQuitMessage(0);
PostQuitMessage 함수는 메시지 큐에 WM_QUIT 메시지를 배치합니다. WM_QUIT는 특별한 메시지입니다. 이 메시지로 인해 GetMessage가 0을 반환하여 메시지 루프가 끝났음을 알릴 수 있습니다. 다음은 수정된 메시지 루프입니다.
// Correct.
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
GetMessage가 0이 아닌 값을 반환하는 한, while 루프의 식은 true로 평가됩니다. PostQuitMessage를 호출하면 식이 false가 되고 프로그램이 루프에서 중단됩니다. (이 동작의 흥미로운 결과 중 하나는 창 프로시저가 WM_QUIT 메시지를 받지 않는다는 것입니다. 따라서 창 프로시저에 이 메시지에 대한 case 문이 있을 필요는 없습니다.)
다음에 발생하는 명백한 질문은 PostQuitMessage를 언제 호출해야 하는가 입니다. 창 닫기 항목에서 이 질문을 해결하게 되지만 먼저 창 프로시저를 작성해야 합니다.
게시된 메시지와 보낸 메시지 비교
이전 섹션에서는 큐로 가는 메시지에 대해 설명했습니다. 경우에 따라 운영 체제는 큐를 우회하여 창 프로시저를 직접 호출합니다.
다음과 같이 용어를 구분하는 것은 혼동을 일으킬 수 있습니다.
- 메시지를 ‘게시’하는 것은 메시지가 메시지 큐로 이동하고 메시지 루프(GetMessage 및 DispatchMessage)를 통해 디스패치되는 것을 의미합니다.
- 메시지를 ‘보내는’ 것은 메시지가 큐를 건너뛰고 운영 체제가 창 프로시저를 직접 호출한다는 것을 의미합니다.
현재로서는 그 차이가 별로 중요하지 않습니다. 창 프로시저는 모든 메시지를 처리합니다. 그러나 일부 메시지는 큐를 무시하고 창 프로시저로 직접 이동합니다. 그러나 애플리케이션이 창 간에 통신하는 경우에는 차이가 있을 수 있습니다. 이 문제에 대한 자세한 내용은 메시지 및 메시지 큐 정보 항목에서 확인할 수 있습니다.