WindowsAPI - 실습 - 그림판 구현 5 (기능 구현 / 더블 버퍼링)

컴퓨터/Win32-API

728x90
반응형

서론

지난 포스트에 이어서 이제 그림판에 기능을 부여하고 동작시켜보도록 합시다. 

이때 더블 버퍼링을 적용하여 시각적으로 불편한 부분이 없는 그림판 동작을 해보도록 하겠습니다.

 

 

 

WindowsAPI - 실습 - 그림판 구현하기 4 (버튼 제어 추가 & 오류 수정)

서론 팬과 지우기 버튼에 토글 기능을 구현하고, 색상판에 스크롤바를 제어할 수 있는 기능을 추가하도록 하겠습니다. 그리고 몇가지 오류 수정을 하도록 합시다. WindowsAPI - 실습 - 그림판 구현

blog-of-gon.tistory.com

 

메모리 DC 및 비트맵 전역 변수 추가

이제 메모리 공간에서 그림을 그리고 고속 복사를 통해 연속적으로 넘겨줍니다. 따라서 제어할 전역 변수를 선언해줍시다.

그리고 시작 점을 기억하기 위한 POINT값 하나를 선언하겠습니다.

HDC memDC;
HBITMAP memBitmap;
POINT stPos;

 

전역변수에 핸들 값 할당 하기

이제 선언된 메모리 DC와 비트맵에 초기 핸들 값을 할당하는 함수를 선언합니다.

이때 DC값은 2차 포인터로 생성하여 주어야합니다.

//메모리 DC 및 비트맵에 메모리 할당 하기
void CreateBackPage(HWND hWnd, HINSTANCE hInst, HDC* memDC , HBITMAP* memBitmap)
{
    HDC hdc = GetDC(hWnd);
    *memDC = CreateCompatibleDC(hdc);
    RECT rect;
    GetClientRect(hWnd, &rect);
    *memBitmap = CreateCompatibleBitmap(hdc, rect.right, rect.bottom);
    (HBITMAP)SelectObject(*memDC, *memBitmap);
    FillRect(*memDC, &rect, (HBRUSH)GetStockObject(WHITE_BRUSH));
    ReleaseDC(hWnd, hdc);
}

이후 WM_CREATER에서 호출해주도록 합시다.

        //윈도우 생성 메세지
    case WM_CREATE:
    {
        //버튼 생성
        CreateButton(L"Pen", 20, 20, 50, 50, (HMENU)1, hWnd, hInst);
        CreateButton(L"Erase", 20, 75, 50, 50, (HMENU)2, hWnd, hInst);
        //버튼 생성 및 이미지 씌우기

        //색상 테이블 생성
        CreateRGBTable(L"RGBtable", 80, 30, 150, 120, (HMENU)10, hWnd, hInst);

        //더블 버퍼링을 위한 메모리 셋팅
        CreateBackPage(hWnd, hInst, &memDC, &memBitmap);
        break;
    }

이제부터는 선언된 비트맵에 메모리 DC를 이용해서 그린 후 고속 복사를 통해 복사를 합니다.

 

따라서 아래의 몇 가지 부분을 수정해 주도록 합시다.

  • 스크롤 동작시 재갱신 부분
    • InvalidateRect 배경 (TRUE -> FALSE)
    case WM_HSCROLL:
    {
        SetScrollFunction(wParam, lParam);
        InvalidateRect(hWnd, NULL, FALSE);
        UpdateWindow(hWnd);
        break;
    }
  • WM_PAINT 부분
    • SetColor 호출 시 memDC로
    • 이후 BitBlt를 통한 고속 복사
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);
        SetColor(hWnd,memDC);
        RECT rect;
        GetClientRect(hWnd, &rect);
        BitBlt(hdc, 0, 0, rect.right, rect.bottom, memDC, 0, 0, SRCCOPY);

        EndPaint(hWnd, &ps);
        break;
    }
  • 프로그램 종료 시 할당된 메모리 제거
    case WM_DESTROY:
        DeleteObject(memBitmap);
        DeleteDC(memDC);
        PostQuitMessage(0);
        break;

 

그리기 기능 구현 

이제 마우스 좌클릭을 통한 그리기 기능 구현을 하도록 합시다. 

우선 호출되는 구조를 보면 아래와 같습니다.

        /*각종 마우스 동작에 따른 컨트롤*/
    case WM_MOUSEMOVE:
    {
        if (wParam == MK_LBUTTON) {
            //그리기 모드
            if (GetFunction(wParam, lParam, hWnd) == 1)
            {
                stPos = Draw(hWnd, memDC, wParam, lParam, stPos);
                HDC hdc = GetDC(hWnd);
                RECT rect;
                GetClientRect(hWnd, &rect);
                BitBlt(hdc, 0, 0, rect.right, rect.bottom, memDC, 0, 0, SRCCOPY);
                ReleaseDC(hWnd, hdc);
            }
            //지우기 모드
            else if (GetFunction(wParam, lParam, hWnd) == 2)
            {
                stPos = Eraser(hWnd, memDC, wParam, lParam, stPos);
                HDC hdc = GetDC(hWnd);
                RECT rect;
                GetClientRect(hWnd, &rect);
                BitBlt(hdc, 0, 0, rect.right, rect.bottom, memDC, 0, 0, SRCCOPY);
                ReleaseDC(hWnd, hdc);
            }

        }
        break;
    }
    case WM_LBUTTONDOWN:
    {
        //좌 클릭시 초기 좌표 값 저장
        stPos.x = GET_X_LPARAM(lParam);
        stPos.y = GET_Y_LPARAM(lParam);
        break;
    }
    case WM_LBUTTONUP:
    {
        break;
    }
    case WM_RBUTTONDOWN:
    {
        break;
    }
    case WM_RBUTTONUP:
    {
        break;
    }

 

그리기와 지우기 함수는 아래와 같습니다.

//그리기
POINT Draw(HWND hWnd, HDC hdc, WPARAM wParam, LPARAM lParam,POINT stPos) 
{

    //스크롤 Bar의 값 받아오기
    int R = GetScrollPos(FindWindowExW(hWnd, NULL, L"scrollbar", L"R"), SB_CTL);
    int G = GetScrollPos(FindWindowExW(hWnd, NULL, L"scrollbar", L"G"), SB_CTL);
    int B = GetScrollPos(FindWindowExW(hWnd, NULL, L"scrollbar", L"B"), SB_CTL);

    HPEN newPen = CreatePen(PS_SOLID, 10, RGB(R, G, B));
    HPEN oldPen = (HPEN)SelectObject(hdc, newPen);

    POINT pos;
    pos.x = GET_X_LPARAM(lParam);
    pos.y = GET_Y_LPARAM(lParam);

    MoveToEx(hdc, stPos.x, stPos.y, NULL);
    LineTo(hdc, pos.x, pos.y);
    SelectObject(hdc, oldPen);
    DeleteObject(newPen);

    stPos.x = pos.x;
    stPos.y = pos.y;
    return stPos;
}
//지우기
POINT Eraser(HWND hWnd, HDC hdc, WPARAM wParam, LPARAM lParam, POINT stPos)
{
    HPEN newPen = CreatePen(PS_SOLID, 10, RGB(255, 255, 255));
    HPEN oldPen = (HPEN)SelectObject(hdc, newPen);

    POINT pos;
    pos.x = GET_X_LPARAM(lParam);
    pos.y = GET_Y_LPARAM(lParam);

    MoveToEx(hdc, stPos.x, stPos.y, NULL);
    LineTo(hdc, pos.x, pos.y);
    SelectObject(hdc, oldPen);
    DeleteObject(newPen);

    stPos.x = pos.x;
    stPos.y = pos.y;
    return stPos;
}

전체 소스코드 및 구현 화면

  • Paint.cpp
더보기
// Paint.cpp : Defines the entry point for the application.
//

#include "framework.h"
#include "Paint.h"

//함수 원형 선언 부분
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

// 전역변수
HINSTANCE hInst;                               

HDC memDC;
HBITMAP memBitmap;
POINT stPos;


// WinMain함수 부분
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    hInst = hInstance; // 인스턴스 핸들을 전역 변수에 저장합니다.

    /*윈도우 설정 및 메인 윈도우를 생성합니다.*/
    HWND hWnd = InitMainWindowSet(hInstance, &WndProc, L"Paint");
    //정상적으로 생성되었다면 윈도우를 갱신합니다.
    if (!hWnd)
    {
        return FALSE;
    }
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);
    MSG msg;
    // 기본 메시지 루프입니다:
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}

// WndProc 함수부분
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
        //윈도우 생성 메세지
    case WM_CREATE:
    {
        //버튼 생성
        CreateButton(L"Pen", 20, 20, 50, 50, (HMENU)1, hWnd, hInst);
        CreateButton(L"Erase", 20, 75, 50, 50, (HMENU)2, hWnd, hInst);
        //버튼 생성 및 이미지 씌우기

        //색상 테이블 생성
        CreateRGBTable(L"RGBtable", 80, 30, 150, 120, (HMENU)10, hWnd, hInst);

        //더블 버퍼링을 위한 메모리 셋팅
        CreateBackPage(hWnd, hInst, &memDC, &memBitmap);
        break;
    }

    //메인 기능 그리기 Or 지우기
    case WM_COMMAND:
    {
        //토글 버튼 처리하기
        SetFunction(wParam, lParam, hWnd);
        //각종 버튼에 따른 동작 
        break;
    }
    //스크롤 기능
    case WM_HSCROLL:
    {
        SetScrollFunction(wParam, lParam);
        InvalidateRect(hWnd, NULL, FALSE);
        UpdateWindow(hWnd);
        break;
    }


        /*각종 마우스 동작에 따른 컨트롤*/
    case WM_MOUSEMOVE:
    {
        if (wParam == MK_LBUTTON) {
            //그리기 모드
            if (GetFunction(wParam, lParam, hWnd) == 1)
            {
                stPos = Draw(hWnd, memDC, wParam, lParam, stPos);
                HDC hdc = GetDC(hWnd);
                RECT rect;
                GetClientRect(hWnd, &rect);
                BitBlt(hdc, 0, 0, rect.right, rect.bottom, memDC, 0, 0, SRCCOPY);
                ReleaseDC(hWnd, hdc);
            }
            //지우기 모드
            else if (GetFunction(wParam, lParam, hWnd) == 2)
            {
                stPos = Eraser(hWnd, memDC, wParam, lParam, stPos);
                HDC hdc = GetDC(hWnd);
                RECT rect;
                GetClientRect(hWnd, &rect);
                BitBlt(hdc, 0, 0, rect.right, rect.bottom, memDC, 0, 0, SRCCOPY);
                ReleaseDC(hWnd, hdc);
            }

        }
        break;
    }
    case WM_LBUTTONDOWN:
    {
        //좌 클릭시 초기 좌표 값 저장
        stPos.x = GET_X_LPARAM(lParam);
        stPos.y = GET_Y_LPARAM(lParam);
        break;
    }
    case WM_LBUTTONUP:
    {
        break;
    }
    case WM_RBUTTONDOWN:
    {
        break;
    }
    case WM_RBUTTONUP:
    {
        break;
    }
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);
        SetColor(hWnd,memDC,memBitmap);
        RECT rect;
        GetClientRect(hWnd, &rect);
        BitBlt(hdc, 0, 0, rect.right, rect.bottom, memDC, 0, 0, SRCCOPY);

        EndPaint(hWnd, &ps);
        break;
    }
    case WM_DESTROY:
        DeleteObject(memBitmap);
        DeleteDC(memDC);
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}
  • MySource.cpp
더보기
#include "MySource.h"

//윈도우 창 설정
HWND InitMainWindowSet(HINSTANCE hInstance, WNDPROC WndProc,const WCHAR* name)
{
    //윈도우 창 구조체 설정 및 적용하기
    WNDCLASSEXW wcex;
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = NULL;
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = NULL;
    wcex.lpszClassName = name;
    wcex.hIconSm = NULL;
    RegisterClassExW(&wcex);

    //해당 윈도우 창을 가지고와서 윈도우 창 생성하기
    return CreateWindowW(name, name, WS_MAXIMIZE| WS_SYSMENU | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
        100, 100, 750, 750, nullptr, nullptr, hInstance, nullptr);
}

//버튼생성하기 
void CreateButton(const WCHAR* name,LONG x,LONG y,LONG width ,LONG height, HMENU id, HWND hWnd, HINSTANCE hInst)
{
    CreateWindowW(L"button", name, WS_CHILD | WS_VISIBLE | BS_CHECKBOX, x,y,width,height, hWnd, id, hInst,NULL);
}
//색상 선택 도구 만들기
void CreateRGBTable(const WCHAR* name, LONG left, LONG top, LONG right, LONG bottom, HMENU id, HWND hWnd, HINSTANCE hInst)
{
    
    //RGB의 스크롤 바 생성 및 범위 설정
    CreateWindowW(L"scrollbar", L"R", WS_CHILD | WS_VISIBLE | SBS_HORZ, right+20, top+15, right + 30, 15, hWnd, id, hInst, NULL);
    SetScrollRange(FindWindowExW(hWnd, NULL, L"scrollbar", L"R"), SB_CTL, 0, 255, TRUE);
    CreateWindowW(L"scrollbar", L"G", WS_CHILD | WS_VISIBLE | SBS_HORZ, right+20, top+40, right + 30, 15, hWnd, id+1, hInst, NULL);
    SetScrollRange(FindWindowExW(hWnd, NULL, L"scrollbar", L"G"), SB_CTL, 0, 255, TRUE);
    CreateWindowW(L"scrollbar", L"B", WS_CHILD | WS_VISIBLE | SBS_HORZ, right+20, top+65, right + 30, 15, hWnd, id+2, hInst, NULL);
    SetScrollRange(FindWindowExW(hWnd, NULL, L"scrollbar", L"B"), SB_CTL, 0, 255, TRUE);
    //SetColor(hWnd);
}

//메모리 DC 및 비트맵에 메모리 할당 하기
void CreateBackPage(HWND hWnd, HINSTANCE hInst, HDC* memDC , HBITMAP* memBitmap)
{
    HDC hdc = GetDC(hWnd);
    *memDC = CreateCompatibleDC(hdc);
    RECT rect;
    GetClientRect(hWnd, &rect);
    *memBitmap = CreateCompatibleBitmap(hdc, rect.right, rect.bottom);
    (HBITMAP)SelectObject(*memDC, *memBitmap);
    FillRect(*memDC, &rect, (HBRUSH)GetStockObject(WHITE_BRUSH));
    ReleaseDC(hWnd, hdc);
}

//색상 선택
void SetColor(HWND hWnd,HDC memdc,HBITMAP memBitmap)
{
    //스크롤 Bar의 값 받아오기
    int R = GetScrollPos(FindWindowExW(hWnd, NULL, L"scrollbar", L"R"), SB_CTL);
    int G = GetScrollPos(FindWindowExW(hWnd, NULL, L"scrollbar", L"G"), SB_CTL);
    int B = GetScrollPos(FindWindowExW(hWnd, NULL, L"scrollbar", L"B"), SB_CTL);

    HBRUSH newBrush = CreateSolidBrush(RGB(R, G, B));
    HBRUSH OldBrush = (HBRUSH)SelectObject(memdc, newBrush);

    //선택된 색상 보여주기
    Rectangle(memdc, 80, 30, 150, 120);


    //스크롤 Bar의 값 표시해주기
    WCHAR text[20];
    wsprintf(text, L"R : %d    ", R );
    TextOutW(memdc, 360, 40, text, lstrlenW(text));
    wsprintf(text, L"G : %d     ", G );
    TextOutW(memdc, 360, 70, text, lstrlenW(text));
    wsprintf(text, L"B : %d     ", B );
    TextOutW(memdc, 360, 100, text, lstrlenW(text));
    SelectObject(memdc, OldBrush);
    DeleteObject(newBrush);

}
//그리기
POINT Draw(HWND hWnd, HDC hdc, WPARAM wParam, LPARAM lParam,POINT stPos) 
{

    //스크롤 Bar의 값 받아오기
    int R = GetScrollPos(FindWindowExW(hWnd, NULL, L"scrollbar", L"R"), SB_CTL);
    int G = GetScrollPos(FindWindowExW(hWnd, NULL, L"scrollbar", L"G"), SB_CTL);
    int B = GetScrollPos(FindWindowExW(hWnd, NULL, L"scrollbar", L"B"), SB_CTL);

    HPEN newPen = CreatePen(PS_SOLID, 10, RGB(R, G, B));
    HPEN oldPen = (HPEN)SelectObject(hdc, newPen);

    POINT pos;
    pos.x = GET_X_LPARAM(lParam);
    pos.y = GET_Y_LPARAM(lParam);

    MoveToEx(hdc, stPos.x, stPos.y, NULL);
    LineTo(hdc, pos.x, pos.y);
    SelectObject(hdc, oldPen);
    DeleteObject(newPen);

    stPos.x = pos.x;
    stPos.y = pos.y;
    return stPos;
}
//지우기
POINT Eraser(HWND hWnd, HDC hdc, WPARAM wParam, LPARAM lParam, POINT stPos)
{
    HPEN newPen = CreatePen(PS_SOLID, 10, RGB(255, 255, 255));
    HPEN oldPen = (HPEN)SelectObject(hdc, newPen);

    POINT pos;
    pos.x = GET_X_LPARAM(lParam);
    pos.y = GET_Y_LPARAM(lParam);

    MoveToEx(hdc, stPos.x, stPos.y, NULL);
    LineTo(hdc, pos.x, pos.y);
    SelectObject(hdc, oldPen);
    DeleteObject(newPen);

    stPos.x = pos.x;
    stPos.y = pos.y;
    return stPos;
}

//이미지 씌우기
//더블버퍼링
//색상선택
//버튼 제어

//토글 값 셋팅
void SetFunction(WPARAM wParam, LPARAM lParam,HWND hWnd)
{
    if (HIWORD(wParam) == BN_CLICKED)
    {
        switch (LOWORD(wParam))
        {
        case 1:
        {
            if (SendMessageW((HWND)lParam, BM_GETCHECK, 0, 0) == BST_UNCHECKED) {
                SendMessageW((HWND)lParam, BM_SETCHECK, BST_CHECKED, 0);
                SendMessageW(FindWindowExW(hWnd, NULL, L"button", L"Erase"), BM_SETCHECK, BST_UNCHECKED, 0);
                return ;
            }
            else {
                SendMessageW((HWND)lParam, BM_SETCHECK, BST_UNCHECKED, 0);

            }
        }
        case 2:
        {
            if (SendMessageW((HWND)lParam, BM_GETCHECK, 0, 0) == BST_UNCHECKED) {
                SendMessageW((HWND)lParam, BM_SETCHECK, BST_CHECKED, 0);
                SendMessageW(FindWindowExW(hWnd, NULL, L"button", L"Pen"), BM_SETCHECK, BST_UNCHECKED, 0);
                return ;
            }
            else {
                SendMessageW((HWND)lParam, BM_SETCHECK, BST_UNCHECKED, 0);
            }
        }

        } // 스위치문 종료

    }
    return ;
}

//토글 값 받아오기
int GetFunction(WPARAM wParam, LPARAM lParam, HWND hWnd)
{
    // 1 == 그리기 2 == 지우기 없을 시 0
    if (SendMessageW(FindWindowExW(hWnd, NULL, L"button", L"Pen"), BM_GETCHECK, BST_CHECKED, 0) == BST_CHECKED) return 1;
    if (SendMessageW(FindWindowExW(hWnd, NULL, L"button", L"Erase"), BM_GETCHECK, BST_CHECKED, 0) == BST_CHECKED) return 2;
    else return 0;

}

//스크롤 동작
void SetScrollFunction(WPARAM wParam, LPARAM lParam)
{

    switch (LOWORD(wParam))
    {
    case SB_LINELEFT:
        SetScrollPos((HWND)lParam, SB_CTL, max(0,GetScrollPos((HWND)lParam,SB_CTL)-1), TRUE);
        break;
    case SB_PAGELEFT:
        SetScrollPos((HWND)lParam, SB_CTL, max(0, GetScrollPos((HWND)lParam, SB_CTL) - 5), TRUE);
        break;
    case SB_LINERIGHT:
        SetScrollPos((HWND)lParam, SB_CTL, min(255, GetScrollPos((HWND)lParam, SB_CTL) + 1), TRUE);
        break;
    case SB_PAGERIGHT:
        SetScrollPos((HWND)lParam, SB_CTL, min(255, GetScrollPos((HWND)lParam, SB_CTL) + 5), TRUE);
        break;
    case SB_THUMBTRACK:
        SetScrollPos((HWND)lParam, SB_CTL, HIWORD(wParam), TRUE);
        break;
    }
}
//등등 기능을 쪼개서 하나씩 추가할 예정

추가 개선사항

컨트롤인 자식 윈도의 경우에 계속해서 그려지면서 깜빡이는 현상이 발생했습니다.

이를 개선하기 위해 메인 윈도우 생성 시 스타일 옵션을 추가했습니다.

//윈도우 창 설정
HWND InitMainWindowSet(HINSTANCE hInstance, WNDPROC WndProc,const WCHAR* name)
{
    //윈도우 창 구조체 설정 및 적용하기
    WNDCLASSEXW wcex;
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = NULL;
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = NULL;
    wcex.lpszClassName = name;
    wcex.hIconSm = NULL;
    RegisterClassExW(&wcex);

    //해당 윈도우 창을 가지고와서 윈도우 창 생성하기
    return CreateWindowW(name, name, WS_MAXIMIZE| WS_SYSMENU | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
        100, 100, 750, 750, nullptr, nullptr, hInstance, nullptr);
}

 

다음 단계

어느 정도 원하는 기능은 다 구현했습니다. 다만, 선택하는 공간에 그려지는 부분은 상당히 불편합니다.

다음 포스트에서 이런 애러 사항을 개선하도록 합시다.

 

WindowsAPI - 실습 - 그림판 구현 6 (버튼 이미지 삽입 / 그리기 영역 )

서론 이제 처음에 목표치에 거의 도달한 것 같습니다. 우선 버튼에 이미지를 삽입하고, 그리기 영역을 제한하도록 합시다. WindowsAPI - 실습 - 그림판 구현 5 (기능 구현 / 더블 버퍼링) 서론 지난 포

blog-of-gon.tistory.com

 

728x90
반응형

Commnet

G91개발일지

Gon91(지구일)

91년생 공학엔지니어의 개발일지

TODAY :

YESTER DAY :

TOTAL :