사용자 입력: 확장 예제

사용자 입력에 대해 배운 모든 것을 결합하여 간단한 그리기 프로그램을 만들어 보겠습니다. 프로그램의 스크린샷은 다음과 같습니다.

그리기 프로그램의 스크린샷

사용자는 여러 가지 색상으로 타원을 그리고 타원을 선택, 이동 또는 삭제할 수 있습니다. UI를 단순하게 유지하기 위해 프로그램은 사용자가 타원 색상을 선택할 수 없도록 합니다. 대신 프로그램은 미리 정의된 색상 목록을 자동으로 순환합니다. 프로그램은 타원 이외의 도형은 지원하지 않습니다. 분명히 이 프로그램은 어떤 그래픽 소프트웨어 상도 받지 못할 것입니다. 그러나 여전히 배울 만한 유용한 예제입니다. 간단한 그리기 샘플에서 전체 소스 코드를 다운로드할 수 있습니다. 이 섹션에서는 몇 가지 주요 항목만 다룹니다.

프로그램에서 타원은 타원 데이터(D2D1_ELLIPSE) 및 색상(D2D1_COLOR_F)을 포함하는 구조체로 표시됩니다. 또한 구조체는 두 가지 방법을 정의합니다. 타원을 그리는 방법과 적중 테스트를 수행하는 방법입니다.

struct MyEllipse
{
    D2D1_ELLIPSE    ellipse;
    D2D1_COLOR_F    color;

    void Draw(ID2D1RenderTarget *pRT, ID2D1SolidColorBrush *pBrush)
    {
        pBrush->SetColor(color);
        pRT->FillEllipse(ellipse, pBrush);
        pBrush->SetColor(D2D1::ColorF(D2D1::ColorF::Black));
        pRT->DrawEllipse(ellipse, pBrush, 1.0f);
    }

    BOOL HitTest(float x, float y)
    {
        const float a = ellipse.radiusX;
        const float b = ellipse.radiusY;
        const float x1 = x - ellipse.point.x;
        const float y1 = y - ellipse.point.y;
        const float d = ((x1 * x1) / (a * a)) + ((y1 * y1) / (b * b));
        return d <= 1.0f;
    }
};

프로그램은 동일한 단색 브러시를 사용하여 모든 타원을 채우고 윤곽선을 그리고 필요에 따라 색상을 변경합니다. Direct2D에서는 단색 브러시의 색을 변경하는 것이 효율적인 작업입니다. 따라서 단색 브러시 개체는 SetColor 메서드를 지원합니다.

타원은 STL 목록 컨테이너에 저장됩니다.

    list<shared_ptr<MyEllipse>>             ellipses;

참고

shared_ptr은 TR1에서 C++에 추가되고 C++0x로 공식화된 스마트 포인터 클래스입니다. Visual Studio 2010은 shared_ptr과 기타 C++0x 기능에 대한 지원을 추가합니다. 자세한 내용은 MSDN MagazineVisual Studio 2010에서 새 C++ 및 MFC 기능 탐색을 참조하세요. (일부 언어 및 국가에서는 이 리소스를 사용할 수 없습니다.)

 

프로그램에는 세 가지 모드가 있습니다.

  • 그리기 모드. 사용자는 새 타원을 그릴 수 있습니다.
  • 선택 모드. 사용자는 타원을 선택할 수 있습니다.
  • 끌기 모드. 사용자는 선택한 타원을 끌 수 있습니다.

사용자는 액셀러레이터 테이블에 설명된 것과 동일한 바로 가기 키를 사용하여 그리기 모드와 선택 모드 간에 전환할 수 있습니다. 선택 모드에서 사용자가 타원을 클릭하면 프로그램이 끌기 모드로 전환됩니다. 사용자가 마우스 단추를 놓으면 다시 선택 모드로 전환됩니다. 현재 선택은 타원 목록에 반복기로 저장됩니다. 도우미 메서드 MainWindow::Selection는 선택한 타원에 대한 포인터를 반환하거나 선택 항목이 없는 경우 nullptr 값을 반환합니다.

    list<shared_ptr<MyEllipse>>::iterator   selection;
     
    shared_ptr<MyEllipse> Selection() 
    { 
        if (selection == ellipses.end()) 
        { 
            return nullptr;
        }
        else
        {
            return (*selection);
        }
    }

    void    ClearSelection() { selection = ellipses.end(); }

다음 테이블에서는 각 세 가지 모드에서 마우스 입력의 효과를 요약합니다.

마우스 입력 그리기 모드 선택 모드 끌기 모드
왼쪽 단추 아래로 마우스 캡처를 설정하고 새 타원을 그리기 시작합니다. 현재 선택을 해제하고 적중 테스트를 수행합니다. 타원이 적중하면 커서를 캡처하고 타원을 선택한 다음 끌기 모드로 전환합니다. 작업이 필요 없습니다.
마우스 이동 왼쪽 단추가 아래쪽이면 타원의 크기를 조정합니다. 작업이 필요 없습니다. 선택한 타원을 이동합니다.
왼쪽 단추 위로 타원 그리기를 중지합니다. 작업이 필요 없습니다. 선택 모드로 전환합니다.

 

MainWindow 클래스의 다음 메서드는 WM_LBUTTONDOWN 메시지를 처리합니다.

void MainWindow::OnLButtonDown(int pixelX, int pixelY, DWORD flags)
{
    const float dipX = DPIScale::PixelsToDipsX(pixelX);
    const float dipY = DPIScale::PixelsToDipsY(pixelY);

    if (mode == DrawMode)
    {
        POINT pt = { pixelX, pixelY };

        if (DragDetect(m_hwnd, pt))
        {
            SetCapture(m_hwnd);
        
            // Start a new ellipse.
            InsertEllipse(dipX, dipY);
        }
    }
    else
    {
        ClearSelection();

        if (HitTest(dipX, dipY))
        {
            SetCapture(m_hwnd);

            ptMouse = Selection()->ellipse.point;
            ptMouse.x -= dipX;
            ptMouse.y -= dipY;

            SetMode(DragMode);
        }
    }
    InvalidateRect(m_hwnd, NULL, FALSE);
}

마우스 좌표는 이 메서드에 픽셀 단위로 전달된 다음 DIP로 변환됩니다. 이 두 단위를 혼동하지 않는 것이 중요합니다. 예를 들어 DragDetect 함수는 픽셀을 사용하지만 그리기와 적중 테스트는 DIP를 사용합니다. 일반적인 규칙은 Windows 또는 마우스 입력과 관련된 기능은 픽셀을 사용하는 반면 Direct2D와 DirectWrite는 DIP를 사용한다는 것입니다. 항상 높은 DPI 설정에서 프로그램을 테스트하고 프로그램을 DPI 인식으로 표시해야 합니다. 자세한 내용은 DPI 및 장치 독립적 픽셀을 참조하세요.

다음은 WM_MOUSEMOVE 메시지를 처리하는 코드입니다.

void MainWindow::OnMouseMove(int pixelX, int pixelY, DWORD flags)
{
    const float dipX = DPIScale::PixelsToDipsX(pixelX);
    const float dipY = DPIScale::PixelsToDipsY(pixelY);

    if ((flags & MK_LBUTTON) && Selection())
    { 
        if (mode == DrawMode)
        {
            // Resize the ellipse.
            const float width = (dipX - ptMouse.x) / 2;
            const float height = (dipY - ptMouse.y) / 2;
            const float x1 = ptMouse.x + width;
            const float y1 = ptMouse.y + height;

            Selection()->ellipse = D2D1::Ellipse(D2D1::Point2F(x1, y1), width, height);
        }
        else if (mode == DragMode)
        {
            // Move the ellipse.
            Selection()->ellipse.point.x = dipX + ptMouse.x;
            Selection()->ellipse.point.y = dipY + ptMouse.y;
        }
        InvalidateRect(m_hwnd, NULL, FALSE);
    }
}

타원 크기를 조정하는 논리는 이전에 예제: 원 그리기 섹션에서 설명했습니다. 또한 InvalidateRect 호출에 유의하세요. 이렇게 하면 창이 다시 그려집니다. 다음 코드는 WM_LBUTTONUP 메시지를 처리합니다.

void MainWindow::OnLButtonUp()
{
    if ((mode == DrawMode) && Selection())
    {
        ClearSelection();
        InvalidateRect(m_hwnd, NULL, FALSE);
    }
    else if (mode == DragMode)
    {
        SetMode(SelectMode);
    }
    ReleaseCapture(); 
}

보다시피 마우스 입력에 대한 메시지 처리기에는 모두 현재 모드에 따라 분기 코드가 있습니다. 이는 매우 간단한 이 프로그램에 적합한 설계입니다. 그러나 새 모드를 추가하면 너무 복잡해질 수 있습니다. 더 큰 프로그램의 경우 MVC(Model-View-Controller) 아키텍처가 더 나은 설계일 수 있습니다. 이러한 종류의 아키텍처에서 사용자 입력을 처리하는 컨트롤러는 애플리케이션 데이터를 관리하는 모델과 분리됩니다.

프로그램이 모드를 전환하면 커서가 변경되어 사용자에게 피드백을 제공합니다.

void MainWindow::SetMode(Mode m)
{
    mode = m;

    // Update the cursor
    LPWSTR cursor;
    switch (mode)
    {
    case DrawMode:
        cursor = IDC_CROSS;
        break;

    case SelectMode:
        cursor = IDC_HAND;
        break;

    case DragMode:
        cursor = IDC_SIZEALL;
        break;
    }

    hCursor = LoadCursor(NULL, cursor);
    SetCursor(hCursor);
}

마지막으로 창에서 WM_SETCURSOR 메시지를 수신하면 커서를 설정해야 합니다.

    case WM_SETCURSOR:
        if (LOWORD(lParam) == HTCLIENT)
        {
            SetCursor(hCursor);
            return TRUE;
        }
        break;

요약

이 모듈에서는 마우스와 키보드 입력을 처리하는 방법, 바로 가기 키를 정의하는 방법, 프로그램의 현재 상태를 반영하도록 커서 이미지를 업데이트하는 방법을 알아보았습니다.