WIN32 PeekMessage

Date:     Updated:

카테고리:

태그:

3일차

// Client.cpp : 애플리케이션에 대한 진입점을 정의합니다.
//

#include "framework.h"
#include "Client.h"

#define MAX_LOADSTRING 100
//WCHAR은 2바이트, wchar_t형임. (16비트)
//그거 이외엔 다른 게 없음(유니코드형을 위한 것)
// 전역 변수:
HINSTANCE hInst;                                // 현재 인스턴스입니다.
//메인함수가 처음 시작할 때 받아오는 값. 

HWND g_hWnd; //메인 윈도우 핸들. 
WCHAR szTitle[MAX_LOADSTRING];                  // 제목 표시줄 텍스트입니다.
WCHAR szWindowClass[MAX_LOADSTRING];            // 기본 창 클래스 이름입니다.
//100길이로 그냥 선언해놓은 것. 

// 이 코드 모듈에 포함된 함수의 선언을 전달합니다:
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);
//함수들은 전방 선언해놓은 것.(함수 프로토타입)

#include <vector>

using std::vector;

struct tObjInfo {
    POINT g_ptObjectPos;
    POINT g_ptObjScale;
};

vector<tObjInfo> g_vecInfo;

//좌 상단
POINT g_ptLT;

//우 하단. 
POINT g_ptRB;
bool bLbtnDown = false;

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_CLIENT, szWindowClass, MAX_LOADSTRING);

    MyRegisterClass(hInstance);

    // 애플리케이션 초기화를 수행합니다:
    // 여기서 윈도우 창을 생성함. 유저와 프로그램 사이의 인터페이스, 접점이라는 의미도 됨.
    if (!InitInstance(hInstance, nCmdShow))
    {
        return FALSE;
    }

    //단축키 정보. 
    //리소스 뷰 -> Accelerator -> table 
    //특정 키와 조합했을 때, 키값. 
    //테이블 정보 불러와서. 
    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_CLIENT));

    MSG msg;

    //타이머도 부착된 커널 오브젝틍임. 
    //두 번째 매개변수가 ID.
    //SetTimer(g_hWnd, 0, 0, nullptr);
    //GetMessage때문에 메시지 기반 처리 방식임. 메시지가 올 때까지 기다림...
    //PM_REMOVE 확인한 메시지가 있으면, 그 메시지를 제거함. -> 안 그러면 메시지가 계속 쌓임...
    
    DWORD dwPrevCount = GetTickCount();
    DWORD dwAccCount = GetTickCount();
    // msg.message == WM_QUIT이면 FALSE반환함.
    //그 이외에는 TRUE 당연히 계속 유지되어야 하니까. 
    // 
    //PeekMessage는 언제 반환되느냐? -> 메시지의 유무와 관계없이 반환. 그러면 언제 끝나고, 언제 끝나는지 모르는데 조건이 될 수가 없다. 
    //True 반환 경우 -> 꺼내온 게 있었다.
    //False 반환 경우 -> 꺼내온 게 없었다. 
    while (true)
    {
        //무한 반복 -> 메시지가 없으면 if문 바깥으로 무시. 
        if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
            DWORD iTime = GetTickCount();
            //메시지를 처리하기 전에, 

            if (WM_QUIT == msg.message) {
                break;
            }

            //hAccelTable로 단축키를 불러와서 도움말의 ID정보가 IDM_ABOUT임.
            //테이블의 단축키를 쓰지 않으면 필요없는 부분. 
            if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
            {
                //해당 윈도우에서 그 메시지를 처리해라 
                //WndProc 함수 포인터로 그걸 처리함. 
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
            dwAccCount = (GetTickCount() - iTime);
        }
        else {
            //메시지가 없는 동안 호출. 
            //메시지를 처리하는 호출시간의 비율은 굉장히 적음. 
            //메시지가 발생하지 않는 99.xx%의 시간. Game 코드 수행. 

            //GetMessage방식은 거의 메시지를 처리하는 시간이 적으니까, 나머지 시간은 그대로 놀게 하는 방식
            //그래서, 굉장히 비효율적인 방식이라고 할 수 있겠다. 
            
            //비동기 함수로, 이 순간에 마우스가 눌려있는지 안 눌려있는지 알 수 있음. 
            DWORD dwCurCount = GetTickCount();
            if (dwCurCount - dwPrevCount > 1000) {
                float fRatio = (float)dwAccCount / 1000.f;
                wchar_t szBuff[50] = {};
                //wsprintf(szBuff, L"비율 : %f", fRatio);

                swprintf_s(szBuff, L"비율 : %f", fRatio);
                SetWindowText(g_hWnd, szBuff);

                dwPrevCount = dwCurCount;
            }
            //Game 코드 수행
            //디자인 패턴(설계 유형)
            //다른 패턴은 잘 몰라도, 싱글톤 패턴은 너무나도 기본적인 것. 
        }
    }
    //두 번재 매개변수가 ID.
    //KillTimer(g_hWnd, 0);
    return (int)msg.wParam;
}

//
//  함수: MyRegisterClass()
//
//  용도: 창 클래스를 등록합니다.
//  창 클래스를 우리가 정의하고, 등록하는 절차는 윈도우 OS에서 제공하는 함수. 
ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEXW wcex;
    //구조체 채워서 함수에 전달하기. 
    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style = CS_HREDRAW | CS_VREDRAW;
    //WndProc이란?  전방 선언 되어 있는 위 함수. 
    //함수 포인터. 
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_CLIENT));
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    //처음 생성되는 메뉴바 null 값을 넣으면 메뉴바 사라짐. 
    wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_CLIENT);

    //이거의 Key값을 szWindowClass로 사용함. 이 MyRegisterClass의 Key값임. 
    wcex.lpszClassName = szWindowClass;
    wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

    //이건 윈도우 OS에서 제공하는 함수임... 
    return RegisterClassExW(&wcex);
}

//
//   함수: InitInstance(HINSTANCE, int)
//
//   용도: 인스턴스 핸들을 저장하고 주 창을 만듭니다.
//
//   주석:
//
//        이 함수를 통해 인스턴스 핸들을 전역 변수에 저장하고
//        주 프로그램 창을 만든 다음 표시합니다.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
    hInst = hInstance; // 인스턴스 핸들을 전역 변수에 저장합니다.
    //szTitle 대신 "한재훈~" 넣어도 됨. 2번째 변수는 이름 말하는 거임. 
    //szWindowClass 키 값대로 만들어달라. 
    g_hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

    if (!g_hWnd)
    {
        return FALSE;
    }

    ShowWindow(g_hWnd, nCmdShow);
    UpdateWindow(g_hWnd);

    return TRUE;
}

//
//  함수: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  용도: 주 창의 메시지를 처리합니다.
//
//  WM_COMMAND  - 애플리케이션 메뉴를 처리합니다.
//  WM_PAINT    - 주 창을 그립니다.
//  WM_DESTROY  - 종료 메시지를 게시하고 반환합니다.
//
// 메시지가 발생한 윈도우 id값, 메시지등등이 같이 들어옴. 
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); //Devicde Context.(그리기)

        //CreatePen 쓸 때, 다 찾아보면서 해라. 검색하면 다 나옴. 
        //내부적으론 BGR인데 <<로 밀어서 순서 맞춰서 처리해줌. 
        HPEN hRedPen = CreatePen(PS_SOLID, 1, RGB(255, 0, 0));

        //자주 쓰는 펜, 자주 쓰는 오브젝트는 이미 다 만들어놨음. 
        HBRUSH hBlueBrush = CreateSolidBrush(RGB(0, 0, 255));

        //hdc보고, hRedPen을 선택하라. 라고 주는거임. 
        //기존에 있던 defaultPen은 붕 떠버림. 
        //HHDIOBJ를 SelectObject가 반환함. 그래서 HPEN으로 줄 수 없으니, 캐스팅을 해줘야만 함. 
        //기본 펜 ID값(기존에 사용하고 있던 ID값을 받아온다)
        HPEN hDefaultPen = static_cast<HPEN>(SelectObject(hdc, hRedPen));
        HBRUSH hDefaultBrush = static_cast<HBRUSH>(SelectObject(hdc, hBlueBrush));

        //3일차. 여러 개의 물체가 있다면???
        // 물체마다, 좌표가 달라서... 한 물체가 움직이면 다른 물체에게 영향을 미칠 수 있어서 전체적으로 재갱신을 해줘야만 함. 

        //게임이란 렌더링, 화면을 매 순간 적용하기 위해 화면을 계속 그리는 반복의 연속임.(프레임이라는 개념)
        
        //변경된 펜으로 사각형 그림. 
        if (bLbtnDown) {
            Rectangle(hdc, g_ptLT.x, g_ptLT.y, g_ptRB.x, g_ptRB.y);
        }
        
        //물체가 늘어나면 늘어날수록 화면 깜빡임이 되게 심해짐...
        //왜 그럴까? 마우스만 움직여도 InvalidateRect로 계속 다시 그림. 깜빡임 현상은 컴퓨터의 속도가 중요한 게 아님...

        //이건 속도의 문제가 아니라 타이밍의 문제임. 사람의 눈이 60프레임이라고 할때, 우리가 눈을 깜빡이는 0.16초 이후 0.01초 사이에 총알 지나가면 총알 못봄
        //우리는 그걸 그리는 중간 중간에 봐서 깜빡이는 것 처럼 보임

        //현재 1번 문제. 
        //그래서 우리는 픽셀 데이터(도화지)를 2개를 관리할 것임. 완성된 그림만을 보여주기 위함. 

        //현재 프로그램 2번 문제.
        //또 메시지가 존재하지 않으면 프로그램이 돌지 않는다는 문제도 존재함. 인공지능이나 AI가 움직이는 건 어떻게 하려고?
        //계속 계속 돌아가게 해줘야함. 

        // 우회 방법 -> 일정 시간마다 일부로 메시지를 발생시키는 방법이 있긴 한데
        // 정석 방법 -> 프로그램의 구조자체를 변경하는 것. 
        for (auto& info : g_vecInfo) {
            Rectangle(hdc, 
                info.g_ptObjectPos.x - info.g_ptObjScale.x / 2, 
                info.g_ptObjectPos.y - info.g_ptObjScale.y / 2, 
                info.g_ptObjectPos.x + info.g_ptObjScale.x / 2, 
                info.g_ptObjectPos.y + info.g_ptObjScale.y / 2);
        }

            

        //빨간 펜 다 썼으니, 기본 펜으로 다시 바꿔줌. 
        SelectObject(hdc, hDefaultPen);
        SelectObject(hdc, hDefaultBrush);

        //그리고 빨간 펜을 지워준다. 
        DeleteObject(hRedPen);
        DeleteObject(hBlueBrush);

        //그리기 종료. 
        EndPaint(hWnd, &ps);
    }
    break;
    case WM_LBUTTONDOWN:
    {
     //마우스 왼쪽 누르기. 
     //4바이트 param인데, 2바이트 | 2바이트 이렇게 나눠서 비트마스킹... 비슷하게 해서 마우스 좌표를 보냄. 
     //코드보면 비트연산(<<, >>)으로 밀어서 가져옴. 
     //마우스의 x좌표, y좌표. 
        g_ptLT.x = LOWORD(lParam);
        g_ptLT.y = HIWORD(lParam);
        bLbtnDown = true;
    }
        break;
    case WM_MOUSEMOVE:
        g_ptRB.x = LOWORD(lParam);
        g_ptRB.y = HIWORD(lParam);
        //InvalidateRect(hWnd, nullptr, true);
        break;

    case WM_LBUTTONUP:
    {
        tObjInfo info = {};
        info.g_ptObjectPos.x = (g_ptLT.x + g_ptRB.x) / 2;
        info.g_ptObjectPos.y = (g_ptLT.y + g_ptRB.y) / 2;

        info.g_ptObjScale.x = abs(g_ptRB.x - g_ptLT.x);
        info.g_ptObjScale.y = abs(g_ptRB.y - g_ptLT.y);

        g_vecInfo.push_back(info);
        bLbtnDown = false;
    }
        break;

    case WM_TIMER:
    {
        int a = 0;
    }
        break;

    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    case WM_KEYDOWN:
    {
        //키가 눌리는 걸 감지하는 것. 
        //무슨 키냐, 라는 걸 알아야 하지 않나? 
        switch (wParam) {
        case VK_UP:
            //위쪽 화살표. 
            //g_ptObjectPos.y -= 10;
            //누를때마다 화면 갱신을 해줘야지. 
            // 이렇게 하면, 기존에 그려진 부분은 그대로 남아있슴... 그래서 기존에 있던 걸 지워줘야함. 
            //InvalidateRect(hWnd, nullptr, false);
            //true로 바꾸면, 기존에 그려져 있던 픽셀들 전부 깔끔하게 청소함. 
            InvalidateRect(hWnd, nullptr, true);

            break;
        case VK_DOWN:
            //g_ptObjectPos.y += 10;
            InvalidateRect(hWnd, nullptr, true);
            break;
        case VK_LEFT:
            //g_ptObjectPos.x -= 10;
            InvalidateRect(hWnd, nullptr, true);
            break;
        case VK_RIGHT:
            //g_ptObjectPos.x += 10;
            InvalidateRect(hWnd, nullptr, true);
            break;

        //꾹 누르고 있으면 한템포 느림... 
        //KEYDOWN은 한 번 눌리고, 어느정도 딜레이 이후에 계속 들어오는 방식이라 그럼.

        case 'W':
        {
            int a = 0;
        }
        //일반 키. 대문자 기준으로만 됨. 
        break;
        }
    }
        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;
}

오늘도 중요한 것만 정리해보자.

#include <vector>

using std::vector;

struct tObjInfo {
    POINT g_ptObjectPos;
    POINT g_ptObjScale;
};

vector<tObjInfo> g_vecInfo;

//좌 상단
POINT g_ptLT;

//우 하단. 
POINT g_ptRB;
bool bLbtnDown = false;
  • 처음에 이건, 마우스를 드래그하여 만든 사각형들의 벡터이다.
  • 또, g_ptLT, g_ptRB로 마우스의 움직임을 받는다.
  • bLbtnDown은 변수이름을 보다시피, 마우스 왼쪽 버튼을 누를 시에 true되는 변수.
    //타이머도 부착된 커널 오브젝틍임. 
    //두 번째 매개변수가 ID.
    //SetTimer(g_hWnd, 0, 0, nullptr);
    //GetMessage때문에 메시지 기반 처리 방식임. 메시지가 올 때까지 기다림...
    //PM_REMOVE 확인한 메시지가 있으면, 그 메시지를 제거함. -> 안 그러면 메시지가 계속 쌓임...
    
    DWORD dwPrevCount = GetTickCount();
    DWORD dwAccCount = GetTickCount();
    // msg.message == WM_QUIT이면 FALSE반환함.
    //그 이외에는 TRUE 당연히 계속 유지되어야 하니까. 
    // 
    //PeekMessage는 언제 반환되느냐? -> 메시지의 유무와 관계없이 반환. 그러면 언제 끝나고, 언제 끝나는지 모르는데 조건이 될 수가 없다. 
    //True 반환 경우 -> 꺼내온 게 있었다.
    //False 반환 경우 -> 꺼내온 게 없었다. 
    while (true)
    {
        //무한 반복 -> 메시지가 없으면 if문 바깥으로 무시. 
        if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
            DWORD iTime = GetTickCount();
            //메시지를 처리하기 전에, 

            if (WM_QUIT == msg.message) {
                break;
            }

            //hAccelTable로 단축키를 불러와서 도움말의 ID정보가 IDM_ABOUT임.
            //테이블의 단축키를 쓰지 않으면 필요없는 부분. 
            if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
            {
                //해당 윈도우에서 그 메시지를 처리해라 
                //WndProc 함수 포인터로 그걸 처리함. 
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
            dwAccCount = (GetTickCount() - iTime);
        }
        else {
            //메시지가 없는 동안 호출. 
            //메시지를 처리하는 호출시간의 비율은 굉장히 적음. 
            //메시지가 발생하지 않는 99.xx%의 시간. Game 코드 수행. 

            //GetMessage방식은 거의 메시지를 처리하는 시간이 적으니까, 나머지 시간은 그대로 놀게 하는 방식
            //그래서, 굉장히 비효율적인 방식이라고 할 수 있겠다. 
            
            //비동기 함수로, 이 순간에 마우스가 눌려있는지 안 눌려있는지 알 수 있음. 
            DWORD dwCurCount = GetTickCount();
            if (dwCurCount - dwPrevCount > 1000) {
                float fRatio = (float)dwAccCount / 1000.f;
                wchar_t szBuff[50] = {};
                //wsprintf(szBuff, L"비율 : %f", fRatio);

                swprintf_s(szBuff, L"비율 : %f", fRatio);
                SetWindowText(g_hWnd, szBuff);

                dwPrevCount = dwCurCount;
            }
            //Game 코드 수행
            //디자인 패턴(설계 유형)
            //다른 패턴은 잘 몰라도, 싱글톤 패턴은 너무나도 기본적인 것. 
        }
  • 오늘의 핵심부분이다. 기본 WINAPI는 메시지 기반 처리라서 게임에 적합하지가 않다.
  • 이유는 메시지가 올 때까지 기다리니까. 저 위의 시간 재는 코드로 해보면, 99%이상의 시간이 else문. 즉, 메시지를 받지 않을 때의 시간이다.
  • 따라서 굉장한 비효율 방식으로, 이후 비동기 방식을 사용하여 메시지만 독립적으로? 처리하는 방식으로 구조를 변경하겠다.
  • 그래서 GetMessage대신, PeekMessage로 메시지를 가져오는 것으로 변경. 내부는 주석을 읽어보길.
        //3일차. 여러 개의 물체가 있다면???
        // 물체마다, 좌표가 달라서... 한 물체가 움직이면 다른 물체에게 영향을 미칠 수 있어서 전체적으로 재갱신을 해줘야만 함. 

        //게임이란 렌더링, 화면을 매 순간 적용하기 위해 화면을 계속 그리는 반복의 연속임.(프레임이라는 개념)
        
        //변경된 펜으로 사각형 그림. 
        if (bLbtnDown) {
            Rectangle(hdc, g_ptLT.x, g_ptLT.y, g_ptRB.x, g_ptRB.y);
        }
        
        //물체가 늘어나면 늘어날수록 화면 깜빡임이 되게 심해짐...
        //왜 그럴까? 마우스만 움직여도 InvalidateRect로 계속 다시 그림. 깜빡임 현상은 컴퓨터의 속도가 중요한 게 아님...

        //이건 속도의 문제가 아니라 타이밍의 문제임. 사람의 눈이 60프레임이라고 할때, 우리가 눈을 깜빡이는 0.16초 이후 0.01초 사이에 총알 지나가면 총알 못봄
        //우리는 그걸 그리는 중간 중간에 봐서 깜빡이는 것 처럼 보임

        //현재 1번 문제. 
        //그래서 우리는 픽셀 데이터(도화지)를 2개를 관리할 것임. 완성된 그림만을 보여주기 위함. 

        //현재 프로그램 2번 문제.
        //또 메시지가 존재하지 않으면 프로그램이 돌지 않는다는 문제도 존재함. 인공지능이나 AI가 움직이는 건 어떻게 하려고?
        //계속 계속 돌아가게 해줘야함. 

        // 우회 방법 -> 일정 시간마다 일부로 메시지를 발생시키는 방법이 있긴 한데
        // 정석 방법 -> 프로그램의 구조자체를 변경하는 것. 
        for (auto& info : g_vecInfo) {
            Rectangle(hdc, 
                info.g_ptObjectPos.x - info.g_ptObjScale.x / 2, 
                info.g_ptObjectPos.y - info.g_ptObjScale.y / 2, 
                info.g_ptObjectPos.x + info.g_ptObjScale.x / 2, 
                info.g_ptObjectPos.y + info.g_ptObjScale.y / 2);
        }
  • case WM_PAINT: 의 내부부분. 화면깜빡임이 굉장히 심한데, 그 이유는 성능이 부족하기 떄문이 아님.
  • 컴퓨터가 그리는 도중에 우리의 눈이 보기 때문. 30개를 그려야 하는데, 10개만 그린 걸 보여서…
  • 그래서 현재 구조는 2가지 문제가 있는데 1번째 구조는 픽셀 데이터(도화지)를 2개를 관리하여, 완성된 그림만을 보여줄 것.
  • 2번째 구조는 메시지 기반의 처리구조를 바꿀 것(위에서 바꾸었다.)

WINAPI 카테고리 내 다른 글 보러가기

댓글 남기기