C & C++/Windows API 예제

7. 첫 번째 실습 – 도형 이동 시키기 [Windows API]

언휴 2024. 1. 26. 10:46

안녕하세요. 언제나휴일입니다.

이번에는 현재까지 다룬 내용(윈도우 클래스 등록 및 개체 생성, 기본 그리기, 키보드 입력, 마우스 입력, 타이머 입력)을 정리하는 실습을 할게요.

실습할 시나리오는 다음과 같습니다.

주어진 공간 안에서 도형을 이동시키는 프로그램

방향 키를 누르면 도형의 방향이 바뀐다.

space 키를 누르면 도형은 멈춘다.

현재 방향과 도형의 좌표(논리 좌표)를 출력하시오.

마우스 왼쪽 버튼을 클릭하면 도형이 바뀐다.(사각형->원->사각형->원…)

도형이 움직일 수 있는 공간을 표시하고 논리 좌표에 맞게 모눈을 그리시오.

도형은 움직일 수 있는 공간 경계 밖으로 이동할 수 없어요.

진입점에서는 윈도우 클래스 등록, 개체 생성, 메시지 루프를 작성합니다.

#include <Windows.h>
#define MY_DEF_STYLE CS_HREDRAW | CS_VREDRAW|CS_DBLCLKS
LRESULT CALLBACK MyWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam);
void RegMyWndClass(LPCWSTR cname);
void MakeWindow(LPCWSTR cname);
void MsgLoop();
INT APIENTRY WinMain(HINSTANCE hIns, HINSTANCE hPrev, LPSTR cmd, INT nShow)
{
    RegMyWndClass(TEXT("MyWindow"));
    MakeWindow(TEXT("MyWindow"));
    MsgLoop();
}
void RegMyWndClass(LPCWSTR cname)
{
    //윈도우 클래스 속성 설정
    WNDCLASS wndclass = { 0 };
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.hInstance = GetModuleHandle(0);
    wndclass.hIcon = LoadIcon(0, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(0, IDC_ARROW);
    wndclass.lpfnWndProc = MyWndProc;
    wndclass.lpszMenuName = 0;
    wndclass.lpszClassName = cname;
    wndclass.style = MY_DEF_STYLE;
    //윈도우 클래스 등록
    RegisterClass(&wndclass);
}
void MakeWindow(LPCWSTR cname)
{
    //윈도우 개체 생성
    HWND hWnd = CreateWindow(cname,//클래스 이름
        TEXT("도형 이동시키기"),//타이틀 명
        WS_OVERLAPPEDWINDOW,//윈도우 스타일
        100, 30, 700, 600,//Left, Top, Width, Height
        0, //부모 윈도우 핸들
        0,//메뉴
        GetModuleHandle(0),//모듈 핸들
        0 //WM_CREATE에 전달할 인자       
    );
    //윈도우 개체 시각화
    ShowWindow(hWnd, SW_SHOW);
}
void MsgLoop()
{
    MSG Message;
    while (GetMessage(&Message, 0, 0, 0))//응용 큐에서 메시지를 꺼내오기
    {
        DispatchMessage(&Message);//메시지 수행(콜백 가동)
    }
}

윈도우 콜백 프로시저에는 WM_LBUTTONDOWN, WM_KEYDOWN, WM_PAINT, WM_DESTORY 메시지를 처리하는 함수를 호출하게 구현합니다.

LRESULT CALLBACK MyWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
    switch (iMessage)
    {        
    case WM_LBUTTONDOWN: OnLButtonDown(hWnd); return 0;    
    case WM_KEYDOWN: OnKeyDown(hWnd,wParam); return 0;
    case WM_PAINT: OnPaint(hWnd); return 0;
    case WM_DESTROY: OnDestroy(hWnd); return 0;
    }
    return DefWindowProc(hWnd, iMessage, wParam, lParam);
}

OnDestroy 함수에서는 창이 닫히면 메시지 루프를 탈출할 수 있게 PostQuitMessage 함수를 호출합니다.

void OnDestroy(HWND hWnd)
{
    PostQuitMessage(0);//WM_QUIT 메시지를 발급, 메시지 루프 종료 위함
}

프로그램에서 관리할 변수를 선언합시다.

방향을 기억하기 위한 변수, 도형이 사각형인지 판별하는 변수, 현재 도형이 있는 좌표를 기억할 변수를 선언합니다.

방향을 화면에 표시하기 위한 상수들도 선언합니다.

int dir = 0;
bool is_rect;
POINT now;
const WCHAR dirs[5][2] = { TEXT("◎"), TEXT("↑") ,TEXT("←") ,TEXT("→"), TEXT("↓") };
const int xs[5] = { 50, 50,10,90,50 };
const int ys[5] = { 40, 10,40,40,70 };

도형이 이동할 공간의 시작 좌표를 매크로 상수(SX, SY)로 정의합니다.

논리 좌표 폭과 높이를 매크로 상수(MY_WIDTH, MY_HEIGHT)로 정의합니다.

도형이 이동할 수 있는 공간의 폭과 높이를 매크로 상수(BOARD_WIDTH, BOARD_HEIGHT)로 정의합니다.

논리 좌표를 화면 좌표로 바꿔주는 매크로 함수(SCX, SCY)를 정의합니다.

#define SX  200
#define SY  10
#define MY_WIDTH 15
#define MY_HEIGHT 15
#define BOARD_WIDTH   20
#define BOARD_HEIGHT 20
#define SCX(x) (SX+(x)*MY_WIDTH)
#define SCY(y) (SY+(y)*MY_HEIGHT)

OnLButtonDown에서는 도형을 토글합니다. 자기 자신과 true를 ^연산하면 토글(true->false->true->false…)할 수 있어요.

출력하는 도형을 다시 그려주기 위해 무효화 영역을 발생시킵니다.(여기에서는 전체 영역을 무효화 영역으로 지정하였기 때문에 깜빡임이 있어요.)

void OnLButtonDown(HWND hWnd)
{
    is_rect ^= true;
    InvalidateRect(hWnd, 0, TRUE);
}

OnKeyDown에서는 누른 키에 따라 방향 값을 변경합니다.

방향 키가 아닐 때는 아무 일도 하지 않습니다.

바뀐 방향이 0일 때는 타이머를 해제합니다.

바뀐 방향이 0이 아닐 때는 타이머가 가동 중이지 아닐 때 타이머를 생성합니다.

void OnKeyDown(HWND hWnd, DWORD vkey)
{    
    switch (vkey)
    {
    case VK_LEFT: dir = 2; break;
    case VK_RIGHT: dir = 3; break;
    case VK_UP: dir = 1; break;
    case VK_DOWN:dir = 4; break;
    case VK_SPACE: dir = 0; break;
    default:return;
    }
    static bool is_alive = false;
    if (dir == 0)
    {
        KillTimer(hWnd, 0);
        is_alive = false;
    }
    else
    {
        if (is_alive == false)
        {
            SetTimer(hWnd, 0, 300, MoveProc);
            is_alive = true;
        }
    }
}

타이머 콜백 프로시저에서는 방향에 따라 논리 좌표를 변경하고 다시 그릴 수 있게 무효화 영역을 발생시킵니다.

VOID CALLBACK MoveProc(HWND hWnd, UINT, UINT_PTR, DWORD)
{

    switch (dir)
    {
    case 1: if (now.y > 0)now.y--; break;
    case 2: if (now.x > 0)now.x--; break;
    case 3: if (now.x < BOARD_WIDTH - 1)now.x++; break;
    case 4: if (now.y < BOARD_HEIGHT - 1)now.y++; break;
    }
    InvalidateRect(hWnd, 0, TRUE);
}

여기에서는 정보 출력, 보드 출력, 공 출력하는 부분을 나누어 구현할게요.

void OnDraw(HDC hdc)
{
    DrawInfo(hdc);    
    DrawBoard(hdc);
    DrawBall(hdc);
}
void OnPaint(HWND hWnd)
{
    PAINTSTRUCT ps;
    BeginPaint(hWnd, &ps);    
    OnDraw(ps.hdc);
    EndPaint(hWnd, &ps);
}

정보 출력에서 현재 방향을 출력합니다. 이 부분은 함수로 작성하여 호출할게요.

현재 방향은 다른 방향 모습과 다르게 특징 지어 그립니다.

void DrawDirection(HDC hdc)
{
    SetTextColor(hdc, RGB(0xFF,0,0));
    SetBkColor(hdc, RGB(0,0xFF, 0xFF));
    TextOut(hdc, xs[dir], ys[dir], dirs[dir],1);
}

나머지 방향들도 그려줍니다.

그리고 도형의 논리 좌표를 그려줍니다.

void DrawInfo(HDC hdc)
{   
    DrawDirection(hdc);
    SetTextColor(hdc, RGB(0, 0, 0));
    SetBkColor(hdc, RGB(0xFF, 0xFF, 0xFF));
    for (int i = 0; i < 5; i++)
    {
        if (i != dir)
        {
            TextOut(hdc, xs[i], ys[i], dirs[i], 1);
        }
    }    
    WCHAR buf[256];
    wsprintf(buf, TEXT("%3d,%3d"), now.x, now.y);
    TextOut(hdc, 35, 100, buf, lstrlen(buf));
}

보드를 그리는 함수에서는 보드 외곽을 먼저 그려준 후에 수직선과 수평선을 그려줍니다.

void DrawBoard(HDC hdc)
{
    HPEN hPen = CreatePen(PS_SOLID, 4, RGB(0, 0, 0));
    HGDIOBJ oPen = SelectObject(hdc, hPen);
    Rectangle(hdc, SX, SY, SCX(BOARD_WIDTH), SCY(BOARD_HEIGHT));
    hPen = CreatePen(PS_DOT, 1, RGB(0x7f, 0x7f, 0x7f));
    SelectObject(hdc, hPen);
    for (int i = 1; i < BOARD_WIDTH; i++)
    {
        MoveToEx(hdc, SCX(i), SCY(0), 0);
        LineTo(hdc, SCX(i), SCY(BOARD_HEIGHT));
    }
    
    for (int i = 1; i < BOARD_HEIGHT; i++)
    {
        MoveToEx(hdc, SCX(0), SCY(i), 0);
        LineTo(hdc, SCX(BOARD_WIDTH), SCY(i));
    }
    SelectObject(hdc, oPen);    
}

공을 그려주는 함수에서는 도형 모양에 따라 사각형 혹은 원을 그려줍니다.

void DrawBall(HDC hdc)
{
    HBRUSH hBrush = CreateSolidBrush(RGB(0xFF, 0x00, 0x00));
    HGDIOBJ oBrush = SelectObject(hdc, hBrush);
    if (is_rect)
    {        
        Rectangle(hdc, SCX(now.x), SCY(now.y), SCX(now.x+1)-1,SCY(now.y+1)-1);
    }
    else
    {
        Ellipse(hdc, SCX(now.x), SCY(now.y), SCX(now.x + 1) - 1, SCY(now.y + 1) - 1);
    }
    SelectObject(hdc, oBrush);
}

다음은 전체 소스 코드입니다.

//Windows API
//방향 키를 누르면 도형의 방향이 바뀐다.
//space 키를 누르면 도형은 멈춘다.
//현재 방향과 도형의 좌표(논리 좌표)를 출력하시오.
//마우스 왼쪽 버튼을 클릭하면 도형이 바뀐다.(사각형->원->사각형->원...)
//도형이 움직일 수 있는 공간을 표시하고 논리 좌표에 맞게 모눈을 그리시오.
//도형은 움직일 수 있는 공간 경계 밖으로 이동할 수 없어요.

#include <Windows.h>
#define MY_DEF_STYLE CS_HREDRAW | CS_VREDRAW|CS_DBLCLKS
LRESULT CALLBACK MyWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam);
void RegMyWndClass(LPCWSTR cname);
void MakeWindow(LPCWSTR cname);
void MsgLoop();
INT APIENTRY WinMain(HINSTANCE hIns, HINSTANCE hPrev, LPSTR cmd, INT nShow)
{
    RegMyWndClass(TEXT("MyWindow"));
    MakeWindow(TEXT("MyWindow"));
    MsgLoop();
}
void RegMyWndClass(LPCWSTR cname)
{
    //윈도우 클래스 속성 설정
    WNDCLASS wndclass = { 0 };
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.hInstance = GetModuleHandle(0);
    wndclass.hIcon = LoadIcon(0, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(0, IDC_ARROW);
    wndclass.lpfnWndProc = MyWndProc;
    wndclass.lpszMenuName = 0;
    wndclass.lpszClassName = cname;
    wndclass.style = MY_DEF_STYLE;
    //윈도우 클래스 등록
    RegisterClass(&wndclass);
}
void MakeWindow(LPCWSTR cname)
{
    //윈도우 개체 생성
    HWND hWnd = CreateWindow(cname,//클래스 이름
        TEXT("도형 이동시키기"),//타이틀 명
        WS_OVERLAPPEDWINDOW,//윈도우 스타일
        100, 30, 700, 600,//Left, Top, Width, Height
        0, //부모 윈도우 핸들
        0,//메뉴
        GetModuleHandle(0),//모듈 핸들
        0 //WM_CREATE에 전달할 인자       
    );
    //윈도우 개체 시각화
    ShowWindow(hWnd, SW_SHOW);
}
void MsgLoop()
{
    MSG Message;
    while (GetMessage(&Message, 0, 0, 0))//응용 큐에서 메시지를 꺼내오기
    {
        DispatchMessage(&Message);//메시지 수행(콜백 가동)
    }
}
void OnDestroy(HWND hWnd)
{
    PostQuitMessage(0);//WM_QUIT 메시지를 발급, 메시지 루프 종료 위함
}

int dir = 0;
bool is_rect;
POINT now;
const WCHAR dirs[5][2] = { TEXT("◎"), TEXT("↑") ,TEXT("←") ,TEXT("→"), TEXT("↓") };
const int xs[5] = { 50, 50,10,90,50 };
const int ys[5] = { 40, 10,40,40,70 };

#define SX  200
#define SY  10
#define MY_WIDTH 15
#define MY_HEIGHT 15
#define BOARD_WIDTH   20
#define BOARD_HEIGHT 20
#define SCX(x) (SX+(x)*MY_WIDTH)
#define SCY(y) (SY+(y)*MY_HEIGHT)

void OnLButtonDown(HWND hWnd)
{
    is_rect ^= true;
    InvalidateRect(hWnd, 0, TRUE);
}
VOID CALLBACK MoveProc(HWND hWnd, UINT, UINT_PTR, DWORD)
{

    switch (dir)
    {
    case 1: if (now.y > 0)now.y--; break;
    case 2: if (now.x > 0)now.x--; break;
    case 3: if (now.x < BOARD_WIDTH - 1)now.x++; break;
    case 4: if (now.y < BOARD_HEIGHT - 1)now.y++; break;
    }
    InvalidateRect(hWnd, 0, TRUE);
}

void OnKeyDown(HWND hWnd, DWORD vkey)
{    
    switch (vkey)
    {
    case VK_LEFT: dir = 2; break;
    case VK_RIGHT: dir = 3; break;
    case VK_UP: dir = 1; break;
    case VK_DOWN:dir = 4; break;
    case VK_SPACE: dir = 0; break;
    default:return;
    }
    static bool is_alive = false;
    if (dir == 0)
    {
        KillTimer(hWnd, 0);
        is_alive = false;
    }
    else
    {
        if (is_alive == false)
        {
            SetTimer(hWnd, 0, 300, MoveProc);
            is_alive = true;
        }
    }
}

void DrawDirection(HDC hdc)
{
    SetTextColor(hdc, RGB(0xFF,0,0));
    SetBkColor(hdc, RGB(0,0xFF, 0xFF));
    TextOut(hdc, xs[dir], ys[dir], dirs[dir],1);
}
void DrawInfo(HDC hdc)
{   
    DrawDirection(hdc);
    SetTextColor(hdc, RGB(0, 0, 0));
    SetBkColor(hdc, RGB(0xFF, 0xFF, 0xFF));
    for (int i = 0; i < 5; i++)
    {
        if (i != dir)
        {
            TextOut(hdc, xs[i], ys[i], dirs[i], 1);
        }
    }    
    WCHAR buf[256];
    wsprintf(buf, TEXT("%3d,%3d"), now.x, now.y);
    TextOut(hdc, 35, 100, buf, lstrlen(buf));
}

void DrawBoard(HDC hdc)
{
    HPEN hPen = CreatePen(PS_SOLID, 4, RGB(0, 0, 0));
    HGDIOBJ oPen = SelectObject(hdc, hPen);
    Rectangle(hdc, SX, SY, SCX(BOARD_WIDTH), SCY(BOARD_HEIGHT));
    hPen = CreatePen(PS_DOT, 1, RGB(0x7f, 0x7f, 0x7f));
    SelectObject(hdc, hPen);
    for (int i = 1; i < BOARD_WIDTH; i++)
    {
        MoveToEx(hdc, SCX(i), SCY(0), 0);
        LineTo(hdc, SCX(i), SCY(BOARD_HEIGHT));
    }
    
    for (int i = 1; i < BOARD_HEIGHT; i++)
    {
        MoveToEx(hdc, SCX(0), SCY(i), 0);
        LineTo(hdc, SCX(BOARD_WIDTH), SCY(i));
    }
    SelectObject(hdc, oPen);    
}
void DrawBall(HDC hdc)
{
    HBRUSH hBrush = CreateSolidBrush(RGB(0xFF, 0x00, 0x00));
    HGDIOBJ oBrush = SelectObject(hdc, hBrush);
    if (is_rect)
    {        
        Rectangle(hdc, SCX(now.x), SCY(now.y), SCX(now.x+1)-1,SCY(now.y+1)-1);
    }
    else
    {
        Ellipse(hdc, SCX(now.x), SCY(now.y), SCX(now.x + 1) - 1, SCY(now.y + 1) - 1);
    }
    SelectObject(hdc, oBrush);
}
void OnDraw(HDC hdc)
{
    DrawInfo(hdc);    
    DrawBoard(hdc);
    DrawBall(hdc);
}
void OnPaint(HWND hWnd)
{
    PAINTSTRUCT ps;
    BeginPaint(hWnd, &ps);    
    OnDraw(ps.hdc);
    EndPaint(hWnd, &ps);
}


LRESULT CALLBACK MyWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
    switch (iMessage)
    {        
    case WM_LBUTTONDOWN: OnLButtonDown(hWnd); return 0;    
    case WM_KEYDOWN: OnKeyDown(hWnd,wParam); return 0;
    case WM_PAINT: OnPaint(hWnd); return 0;
    case WM_DESTROY: OnDestroy(hWnd); return 0;
    }
    return DefWindowProc(hWnd, iMessage, wParam, lParam);
}