ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [C++/Win API] 계산기 만들기 - WinAPI과 GUI편
    기타 코딩 2024. 7. 27. 17:42

    지난글에 이어서 이번에는 아래 계산기의 GUI를 Windows API를 이용해 만들어보자

    Windows API의 기본 패턴

    Windows API같은 경우 아래와 같은 패턴으로 구성되어있다.

    1. window class 설정

    2. window class를 os에 등록 (RegisterClass)

    3. window 생성및 업데이트(CreateWindow, UpdateWindow)

    4. message loop

    5. message 처리함수 작성 (WndProc)

     

    #ifndef UNICODE
    #define UNICODE
    #endif
    
    #pragma comment(linker, "/SUBSYSTEM:WINDOWS")
    
    #include <stdlib.h> //EXIT_FAILURE
    
    HINSTANCE hInstance; //WndProc에서 CreateWindow를 할때 필요
    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //메세지 처리 함수
    
    int APIENTRY wWinMain(HINSTANCE hInst, HINSTANCE hPrecInstance, PWSTR lpCmdLine, int nCmdShow) {
        //hInstance: handle Instance, 프로그램의 ID 값을 담고 있음
        //hPrevInstance 사용안함, legacy 호환성
        //lpCmdLine은 프로그램을 실행할 때 전달되는 문자열, int main(int argc, char*[] argv)의 argv의 역할
        //nCmdShow는 창을 보통상태로 열 것인지, 최대화, 최소화된 상태로 열 것인지
        
        hInstance = hInst;
        HWND hWnd; //handle windows
        MSG msg; //전송된 message를 저장할 구조체
        WNDCLASS wndclass = {};
        
        //window class 설정
        static LPCWSTR wndclass_name = L"Caculator";
        
        wndclass.style = CS_HREDRAW | CS_VREDRAW; //창 높이, 너비 변경시 redraw
        wndclass.lpfnWndProc = WndProc; //메세지 처리 함수
        wndclass.hInstance = hInstance;
        wndclass.hIcon = LoadIconW(NULL, IDI_APPLICATION); //기본 application icon
        wndclass.hCursor = LoadCursorW(NULL, IDC_ARROW); //기본 화살표 커서
        wndclass.hbrBackground = (HBRUSH)GetStockObject(COLOR_WINDOW + 1); //흰색
        wndclass.lpszClassName = wndclass_name; //등록할 class 이름
        if(!RegisterClassW(&wndclass)) return EXIT_FAILURE;
        
        hWnd = CreateWindowW(
            wndclass_name, //class 이름
            L"Caculator", //창 이름
            WS_OVERLAPPEDWINDOW, //기본 창 (최대, 최소, 닫기 버튼 등)
            CW_USEDEFAULT, //창을 그릴 x좌표
            CW_USEDEFAULT, //창을 그릴 y좌표
            410, //창 너비
            490, //창 높이
            NULL, //부모의 hWnd
            NULL, //자식 창을 구별하기 위한 HMENU값
            hInstance,
            NULL //lpParam
        );
        ShowWindow(hWnd, nCmdShow);
        UpdateWindow(hWnd);
        
        //Message 처리
        while(GetMessageW(&msg, NULL, 0, 0)) {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
       }
            
        return (int)msg.wParam;
    }
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    	switch(message) {
        	case WM_CREATE:
            	//메모리 동적 할당, 자식 윈도우 생성등
            	return 0;
            
            case WM_DESTROY:
            	//동적 할당된 메모리 정리
            	PostQuitMessage(1);
                return 0;
            
            case WM_SIZE:
            	//윈도우 사이즈가 변경되었을 때 호출
                return 0;
            
            case WM_PAINT:
            	//윈도우의 전체 또는 일부를 다시 그려야할 때
            	//do something
                return 0;
            
            case WM_COMMAND:
            	//자식 윈도우중 하나가 클릭되었을 때
                // 클릭된 윈도우의 HMENU 값이 wParam의 low bit에 담겨있음
                return 0;
        }
        return DefWindowProcW(hWnd, message, wParam, lParam);
    }

    윈도우는 메세지 기반의 운영체제로서, 프로그램의 직접적인 하드웨어 접근을 막는 대신, 마우스의 어떤 버튼이나 키보드의 어떤 키가 눌렸는 지 같은 것을 os에서 감지하여 메세지를 보내준다. 프로그램은 받은 메세지를 메세지 큐에서 한개씩 꺼내서 (GetMessage) WncProc에 전달하는 것으로 처리한다. 이때 현재 커서의 x 좌표는 lParam의 Low bit에 y좌표는 High bit에 담겨지며, 입력된 키보드나 마우스 버튼등에 대한 정보는 wParam에 담긴다. 메세지마다 wParam의 내용이 다르니 MS 홈페이지를 확인해보아야 한다. 

     

    API 함수는 크게 아스키 문자열(+UTF-8)을 입력받는 A버전의 함수와 wchar_t을 받는 W 함수가 있으며, 함수에 따라 확장 버전인 ExA, ExW가 존재한다. 여기서 A나 W가 붙지 않은 매크로 함수가 존재하는데, 이 함수는 UNICODE의 선언 여부에 따라 A버전의 함수와 W 버전의 함수를 선택하여 작동하게 된다.

    *참고로 아스키 버전의 경우 wWinMain이 아닌 WinMain으로 선언하여야 한다.

     

    C++ 에서 Windows API를 사용할때

    Windows API는 C언어로 되어있지만, C++에서도 사용할 수 있다. 다만 name mangling 때문에 아래와 같이 extern "C" 키워드를 메인 함수와 WndProc에 붙여야 한다.

    extern "C" LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
    
    extern "C" int APIENTRY wWinMain(HINSTANCE hInst, HINSTANCE hPrevInstance, PWSTR lpCmdLine, int nCmdShow) {
    
    }

    추가적으로 C에서는 구조체를 초기화할 때, 맴버의 순서를 마음대로 적어서 b=2, a=1 이런식으로 해도되지만, C++같은 경우 정의된 맴버의 순서를 지켜서 초기화해야한다는 차이를 조심하여야 한다.

     

    Windows API의 자료형

    윈도우에서는 typedef를 통해서 여러 자료형 별칭을 지정해서 사용하고 있다.

    LPSTR = Long Pointer STRing = char*

    LPCSTR = Long Pointer Constant STRing = const char*

    LPWSTR = Long Pointer Wide STRing = wchar_t*

    LPCWSTR = Long Pointer Constant Wide STRing = const wchar_t*

    *pointer 자료형의 크기가 32비트 시스템이면 32비트, 64비트 시스템이면, 64 비트인데, 과거 컴퓨터는 8비트 부터 시작하였으므로 16비트 이상의 포인터를 Long Pointer라고 불렀다.

     

    WORD = unsigned short (16 bit)

    DWORD = Double WORD = unsigned long (32 bit)

    QWORD = Quadruple WORD = unsigned long long (64 bit)

    *CPU가 한번에 처리할 수 있는 단위를 word라고 하는데 16 비트 CPU면 16 비트고, 32 비트 CPU면 32 비트이다. Win16 시절 WORD를 16 비트로 정의를 하였던 것의 흔적이다.

     

    기타

    H...  객체의 핸들

    WM_... Windows Message 

    Client Area

    https://beeleeong.wordpress.com/2017/04/23/want-a-better-window-frame/

    창의 Non-Client Area는 os에서 관리하는 부분이고 실질적으로 다루는 부분은 Client Area이다. 여기서 좌측 상단의 좌표가 (0, 0)이고, 왼쪽과 아래가 각각 x, y 좌표가 증가하는 방향이다.

     

    https://firststeps.ru/mfc/winapi/r.php?86

    typedef struct _RECT 
    { 
    	LONG left; 
    	LONG top; 
    	LONG right; 
    	LONG bottom; 
    } RECT, *PRECT;
    
    GetClientRect(HWND, LPRECT); //현재 창의 크기를 RECT 구조체에 저장

    이를 이용하여 WM_SIZE 메세지 발생시 GetClientRect 함수를 통해 현재 창의 크기를 받아온 뒤, 이를 바탕으로 각 객체들의 위치를 계산하여 재배치할 수 있다.

     

    GUI 코드

    위에서 Windows API에 대한 기본적인 내용은 설명하였으니, 아래 코드를 이해하는 데 그리 어렵지 않을 것이다. (그리고 GUI 작업이 상당한 노가다성 작업이라는 것도 쉽게 알 수 있을 것이다.) 여기에도 코드를 첨부하지만, 보기 좋지 않으니 github로 보거나 다운받아서 보는 것을 추천한다.

    https://github.com/sidreco214/caculator

     

    GitHub - sidreco214/caculator: caculator

    caculator. Contribute to sidreco214/caculator development by creating an account on GitHub.

    github.com

     

    #ifndef UNICODE
    #define UNICODE
    #endif
    
    #pragma comment(linker, "/SUBSYSTEM:WINDOWS")
    
    #include <stdlib.h>
    #include "windows.h"
    
    #include <string>
    #include <vector>
    
    #include "core/calc_tree.hpp"
    
    bool is_caculated = false;
    int num_op = 0;
    int num_cp = 0;
    std::string input_str;
    std::string result_str = "";
    
    extern "C" LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
    
    HINSTANCE hInstance;
    
    extern "C" int APIENTRY wWinMain(HINSTANCE hInst, HINSTANCE hPrevInstance, PWSTR lpCmdLine, int nCmdShow) {
        //hPrevInstance 사용안함, legacy 호환성
        //lpCmdLine은 프로그램을 실행할 때 전달되는 문자열, argv
        //nCmdShow는 창을 보통상태로 열 것인지, 최대화, 최소화된 상태로 열 것인지
        hInstance = hInst;
    
        HWND hWnd;
        MSG msg;
        WNDCLASS wndclass = {};
    
        static LPCWSTR wndclass_name = L"Caculator";
    
        wndclass.style = CS_HREDRAW | CS_VREDRAW; //창 높이, 너비 변경시 redraw
        wndclass.lpfnWndProc = WndProc;
        wndclass.hInstance = hInstance;
        wndclass.hIcon = LoadIconW(NULL, IDI_APPLICATION);
        wndclass.hCursor = LoadCursorW(NULL, IDC_ARROW);
        wndclass.hbrBackground = (HBRUSH)GetStockObject(COLOR_WINDOW + 1);
        wndclass.lpszClassName = wndclass_name;
        if(!RegisterClassW(&wndclass)) return EXIT_FAILURE;
        
        hWnd = CreateWindowW(
            wndclass_name,
            L"Caculator",
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            410,
            490,
            NULL,
            NULL,
            hInstance,
            NULL
        );
        ShowWindow(hWnd, nCmdShow);
        UpdateWindow(hWnd);
    
        //Message 처리
        try{
            while(GetMessageW(&msg, NULL, 0, 0)) {
                TranslateMessage(&msg);
                DispatchMessageW(&msg);
            }
            
        }
        catch(std::exception& e) {
            MessageBoxA(hWnd, e.what(), "error", NULL);
        }
        return (int)msg.wParam;
    }
    
    typedef enum _Bottons {
        B0 = 0,
        B1, B2, B3, B4, B5, B6, B7, B8, B9, BDOT, BADD, BSUB, BMUL, BDIV, BPOW,
        BSQR, BSQRT, BCALC, BAC, BBACKSPACE, BOPENPARENTHESIS, BCLOSEPARENTHSIS,
        BEXP
    } Bottons;
    
    void btn_fn(HWND hWnd, Bottons btn, std::string& input_str, std::string& result_str) {
        if(btn != BAC && btn != BBACKSPACE && is_caculated) return;
    
        switch(btn) {
            case B0:
                input_str.push_back('0');
                break;
    
            case B1:
                input_str.push_back('1');
                break;
    
            case B2:
                input_str.push_back('2');
                break;
    
            case B3:
                input_str.push_back('3');
                break;
    
            case B4:
                input_str.push_back('4');
                break;
    
            case B5:
                input_str.push_back('5');
                break;
    
            case B6:
                input_str.push_back('6');
                break;
    
            case B7:
                input_str.push_back('7');
                break;
    
            case B8:
                input_str.push_back('8');
                break;
    
            case B9:
                input_str.push_back('9');
                break;
    
            case BDOT:
                input_str.push_back('.');
                break;
    
            case BADD:
                input_str.push_back('+');
                break;
    
            case BSUB:
                input_str.push_back('-');
                break;
    
            case BMUL:
                input_str.push_back('*');
                break;
    
            case BDIV:
                input_str.push_back('/');
                break;
    
            case BPOW:
                input_str.push_back('^');
                break;
            
            case BSQR:
                input_str.push_back('^');
                input_str.push_back('2');
                break;
    
            case BSQRT:
                input_str = input_str + "sqrt(";
                ++num_op;
                break;
    
            case BCALC:
                if(input_str.empty()) return;
                if(*input_str.rbegin() == '(') return;
                if(num_op - num_cp > 0) 
                    for(int i=0; i<num_op-num_cp; ++i) input_str.push_back(')');
                [&]() {
                    auto func = Calc::calc_tree(input_str);
                    result_str = std::to_string(func.get());
                    for(auto iter = result_str.rbegin(); iter != result_str.rend(); ++iter) {
                        if(*iter == '0' || *iter == '.') result_str.pop_back();
                        else if(*iter == '.') {
                            result_str.pop_back();
                            break;
                        }
                        else break;
                    }
                }();
                is_caculated = true;
                input_str.push_back('=');
                break;
    
            case BAC:
                is_caculated = false;
                input_str = "";
                result_str = "";
                break;
    
            case BBACKSPACE:
                if(input_str.empty()) return;
                if(is_caculated) {
                    is_caculated = false;
                    result_str = "";
                }
                input_str.pop_back();
                break;
    
            case BOPENPARENTHESIS:
                input_str.push_back('(');
                ++num_op;
                break;
    
            case BCLOSEPARENTHSIS:
                if(num_op <= num_cp) return;
                if(*input_str.rbegin() == '(') return;
                input_str.push_back(')');
                ++num_cp;
                break;
    
            case BEXP:
                input_str = input_str + "exp(";
                ++num_op;
                break;
        }
    
        //update screen
        RECT rect;
        GetClientRect(hWnd, &rect);
        rect.bottom /= 3;
        InvalidateRect(hWnd, &rect, TRUE); 
    };
    
    void PutGridLayout(HWND hWnd, RECT& rect, int nrow, int ncol, std::vector<HWND>& v) {
        int width = static_cast<int>((rect.right - rect.left)/static_cast<float>(ncol) + 0.5);
        int height = static_cast<int>((rect.bottom - rect.top)/static_cast<float>(nrow) + 0.5);
    
        for(int i=0; i < nrow; ++i) {
            for(int j=0; j < ncol; ++j) {
                int index = i*ncol+j;
                if(index >= v.size()) return;
                SetWindowPos(
                    v[index],
                    HWND_TOP,
                    rect.left + width*j,
                    rect.top + height*i,
                    width,
                    height,
                    SWP_ASYNCWINDOWPOS | SWP_SHOWWINDOW
                );
            }
        }
    }
    
    void CreateBtns(HWND hWnd, RECT& rect, int nrow, int ncol, std::vector<HWND>& buttons) {
        std::vector<std::pair<LPCWSTR, HMENU>> btns = {
            std::pair(L"(", (HMENU)BOPENPARENTHESIS),
            std::pair(L")", (HMENU)BCLOSEPARENTHSIS),
            std::pair(L"AC", (HMENU)BAC),
            std::pair(L"<-", (HMENU)BBACKSPACE),
    
            std::pair(L"x^y", (HMENU)BPOW),
            std::pair(L"x^2", (HMENU)BSQR),
            std::pair(L"\u221A", (HMENU)BSQRT),
            std::pair(L"exp", (HMENU)BEXP),
    
            std::pair(L"7", (HMENU)B7),
            std::pair(L"8", (HMENU)B8),
            std::pair(L"9", (HMENU)B9),
            std::pair(L"+", (HMENU)BADD),
    
            std::pair(L"4", (HMENU)B4),
            std::pair(L"5", (HMENU)B5),
            std::pair(L"6", (HMENU)B6),
            std::pair(L"-", (HMENU)BSUB),
    
            std::pair(L"1", (HMENU)B1),
            std::pair(L"2", (HMENU)B2),
            std::pair(L"3", (HMENU)B3),
            std::pair(L"*", (HMENU)BMUL),
    
            std::pair(L".", (HMENU)BDOT),
            std::pair(L"0", (HMENU)B0),
            std::pair(L"=", (HMENU)BCALC),
            std::pair(L"/", (HMENU)BDIV)
        };
    
        HWND hWnd_child;
        int btn_width = static_cast<int>((rect.right - rect.left)/static_cast<float>(ncol) + 0.5);
        int btn_height = static_cast<int>((rect.bottom - rect.top)/static_cast<float>(nrow) + 0.5);
    
        for(int i=0; i < nrow; ++i) {
            for(int j=0; j < ncol; ++j) {
                int index = i*ncol+j;
                if(index >= btns.size()) return;
                hWnd_child = CreateWindowW(
                    L"BUTTON",
                    btns[index].first,
                    WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
                    rect.left + btn_width*j,
                    rect.top + btn_height*i,
                    btn_width,
                    btn_height,
                    hWnd,
                    btns[index].second,
                    hInstance,
                    NULL
                );
                buttons.push_back(hWnd_child);
            }
        }
    
    }
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
        HDC hDC;
        PAINTSTRUCT ps;
        RECT rect1;
        RECT rect2;
        RECT btn_grid_rect;
    
        HFONT hFont;
        HFONT hFont_old;
    
        static std::vector<HWND> buttons;
    
        switch(message) {
            case WM_CREATE:
                GetClientRect(hWnd, &rect1);
                btn_grid_rect.left = rect1.left;
                btn_grid_rect.right = rect1.right;
                btn_grid_rect.top = rect1.bottom/3;
                btn_grid_rect.bottom = rect1.bottom;
                CreateBtns(hWnd, btn_grid_rect, 6, 4, buttons);
                return 0;
            
            case WM_DESTROY:
                PostQuitMessage(1);
                return 0;
            
            case WM_SIZE:
                GetClientRect(hWnd, &rect1);
                btn_grid_rect.left = rect1.left;
                btn_grid_rect.right = rect1.right;
                btn_grid_rect.top = rect1.bottom/3;
                btn_grid_rect.bottom = rect1.bottom;
                PutGridLayout(hWnd, btn_grid_rect, 6, 4, buttons);
            
            case WM_PAINT:
                GetClientRect(hWnd, &rect1);
                rect2.left = rect1.left + 20;
                rect2.right = rect1.right - 20;
                rect2.top = rect1.top + 40;
                rect2.bottom = rect1.bottom / 6;
    
                hDC = BeginPaint(hWnd, &ps);
                hFont = CreateFontW(
                    40,
                    0,
                    0,
                    0,
                    FW_DONTCARE,
                    FALSE,
                    FALSE,
                    FALSE,
                    DEFAULT_CHARSET,
                    OUT_DEFAULT_PRECIS,
                    CLIP_DEFAULT_PRECIS,
                    DEFAULT_QUALITY,
                    DEFAULT_PITCH | FF_SWISS,
                    L"Arial"
                );
                hFont_old = (HFONT)SelectObject(hDC, hFont);
    
                //draw input str
                //InvalidateRect(hWnd, &rect2, TRUE);
                DrawTextA(
                    hDC,
                    input_str.c_str(),
                    -1, //null 문자까지 읽기
                    &rect2,
                    DT_SINGLELINE | DT_RIGHT | DT_VCENTER
                );
    
                //draw result
                rect2.top = rect1.bottom / 6;
                rect2.bottom = rect1.bottom / 3;
                //InvalidateRect(hWnd, &rect2, TRUE);
                DrawTextA(
                    hDC,
                    result_str.c_str(),
                    -1, //null 문자까지 읽기
                    &rect2,
                    DT_SINGLELINE | DT_RIGHT | DT_VCENTER
                );
                SelectObject(hDC, hFont_old);
                DeleteObject(hFont);
                EndPaint(hWnd, &ps);
                return 0;
            
            case WM_COMMAND:
                btn_fn(hWnd, (Bottons)LOWORD(wParam), input_str, result_str);
                return 0;
        }
    
        return DefWindowProcW(hWnd, message, wParam, lParam);
    }

    댓글

Designed by Tistory.