안녕하세요 ~ MFC 카테고리의 꽃집총각 입니다.

이번에는 그 동안 함께 멀티터치를 공부하면서 다뤄보았던 예제 프로그램들의 소스를 정리해 보았습니다.

아래 코드를 다운 받으세요 :)


Gesture : WM_GESTURE 메세지를 이용해 화면상의 사각형을 이동, 회전, 확대/축소 하는 예제.

Touch : WM_TOUCH 메세지를 이용해 클라이언트 영역에 터치 입력 궤적을 따라 선을 그려주는 예제.

Manipulation : IManipulationProcessor 인터페이스를 활용해 Microsoft Surface에서와 같은 Manipulation 움직임을 제어하는 예제.

Manipulation Inertia : IManipulationProcessor, IInertiaProcessor 인터페이스를 모두 활용, 조작(Manipulation) 및 관성(Inertia) 모두를 적용한 예제.

조작(Manipulation)과 관성(Inertia) 처리 방법은 아직 포스팅으로 설명 드리지 못한 부분입니다만 샘플 코드 정리하면서 함께 정리해 공개합니다.

모든 코드는 기본적으로 MS에서 제공하는 예제 코드를 기반으로 하고, 제가 포스팅 하면서 추가로 변경한 부분들의 코드도 일부 포함되어 있습니다. 참고 부탁 드립니다. 

신고
크리에이티브 커먼즈 라이선스
Creative Commons License

Intro

안녕하세요 ~ MFC 카테고리의 꽃집총각 입니다.

지난 포스팅에서는 드디어 WM_TOUCH를 이용한 멀티터치 UX의 구현방법에 대한 이야기가 시작되었습니다. channel9에 공개된 예제를 함께 작성해 보면서 WM_TOUCH 메세지를 다루는 방법을 알아보고 있는데요, 어떨 때 WM_TOUCH를 이용해야 하는지, WM_GESTURE가 아닌 WM_TOUCH를 받고 싶을 땐 어떻게 처리해 주어야 하는지, 그리고 메세지를 받을 때마다 호출되는 CWnd::OnTouchInput 등에 대한 설명이 있었습니다.

그리고 실제로 OnTouchInput() 함수에서 터치 메세지를 제어할 예제 프로그램인 ‘TouchPad’ 프로젝트를 생성하고 기본적인 전처리 단계를 알아보았지요. 이에 이어서 오늘은 OnTouchInput() 함수의 구현부터 본격적으로 알아보도록 하겠습니다 ^^

 

Task 3 : 드로잉 코드는 가져다 씁시다. 터치에 집중해야죠 ㅎㅎ

지금 함께 작성해 보려고 하는 TouchPad는 윈도우 그림판처럼 멀티 터치 입력에 대해 라인을 그려주는 예제입니다. 한 발 더 나가서, 서로 다른 입력으로 드로잉 하는 라인은 색상도 다르게 표시되도록 할거예요. 하지만 사용자로부터 입력된 터치 정보의 처리방법에 집중하기 위해, 드로잉 코드는 샘플에서 제공된 클래스를 사용하도록 하겠습니다. 먼저 예제 프로젝트에 아래의 파일을 추가해 주세요.

 이 클래스들을 이용해 우리가 구현할 코드에 대해 간략히 설명해 보기로 하지요. 우리는 각각의 터치 입력에 대해서 라인(stroke. 스트로크라고 부르겠습니다.)을 그려내는 기능을 추가할 것인데, 이를 이해서 두 개의 스트로크 모음을 유지할겁니다. 한 쪽에서는 이미 드로잉이 끝난 스트로크 데이터를 모아 유지하게 되고, 또 다른 나머지 한 쪽에서는 현재 터치입력으로 드로잉하고 있는 데이터를 유지하게 됩니다. 스크린을 터치하는 한 개 혹은 다수의 입력을 받아 m_StrkColDrawing 멤버변수가 유지하는 스트로크 정보에 CPoint 형식의 좌표 데이터를 추가합니다. 그리고 스크린에서 손가락을 떼면, 손가락을 따라 그려지고 있던 스트로크 정보를 m_StrkColDrawing 변수에서 m_StrkColFinished로 이동시킵니다. 이 때 동시에 다수의 터치 입력이 동시에 일어나면 이를 구분하기 위해 서로가 다른 색상을 가진 스트로크를 출력하게 할겁니다.

 그럼 소스코드 상에서의 수정사항을 알아보기로 하지요. 먼저 위에 첨부되어있는 파일들을 다운받아서 프로젝트에 넣은 후, StdAfx.h 헤더파일에 새로 추가된 헤더들을 인클루드 해주세요.

// stdafx.h 파일에 추가해 주세요.
#include "Stroke.h"  
#include "StrokeCollection.h" 

그리고 ChildView.h 파일로 가서 CChildView 클래스에 private 멤버 변수로 아래의 세 변수를 추가해 줍니다.

private:
    int m_iCurrColor;                    // The current stroke color
    CStrokeCollection m_StrkColFinished; // The user finished entering strokes
                                         // after user lifted his or her finger.
    CStrokeCollection m_StrkColDrawing;  // The Strokes collection the user is
                                         // currently drawing.

이 중에서 m_iCurrColor는 드로잉 되는 스트로크의 색상을 결정하기 위한 인덱스로 쓰일 겁니다. 생성자에서 0으로 초기화 해주세요.

CChildView::CChildView() : m_iCurrColor(0)
{
}

드로잉이 다 끝나서 m_StrkColFinished 변수에 담겨 있을 스트로크 정보들을 화면에 출력해 주기 위해 CChileView::OnPaint() 함수에 아래의 코드를 추가해 줍니다.

// 드로잉이 완료된 스트로크 정보를 출력한다.
m_StrkColFinished.Draw(&dc);

 

Task 4 : 터치 데이터를 다뤄보자 (드디어!)

이제 드로잉 코드의 기본적인 설정들은 끝을 냈고, 지금부터 우리는 WM_TOUCH 메세지에 대해 아래의 세 가지 경우에 따른 코딩을 추가해 넣을 것입니다.

  1. 사용자가 스크린에 터치를 시작한 시점의 처리. – Touch Input Down
  2. 터치한 손가락을 스크린 상에서 움직이는 동안의 처리. – Touch Input Move
  3. 스크린에서 이동하던 손가락을 떼어 터치 입력이 끝나는 시점의 처리. – Touch Input Up

각각의 경우를 처리하기 위한 코드를 클래스에 추가해 봅시다. ChildView.h 헤더파일에서 위의 세가지 경우에 호출할 CChildView 클래스의 멤버함수를 선언해 줍니다.

protected:
    // Handlers for different touch input events
    BOOL OnTouchInputDown(CPoint pt, PTOUCHINPUT pInput);
    BOOL OnTouchInputMove(CPoint pt, PTOUCHINPUT pInput);
    BOOL OnTouchInputUp(CPoint pt, PTOUCHINPUT pInput);

그리고 ChildView.cpp 파일에 아래의 구현을 넣어주세요.

// OnTouchInputDown - 멀티터치 입력이 시작될 때 처리를 구현하는 함수.
BOOL CChildView::OnTouchInputDown(CPoint pt, PTOUCHINPUT pInput)
{
    // 새로운 스트로크 객체를 생성하고 Point 정보를 추가합니다.
    COLORREF strokeColor = GetTouchColor((pInput->dwFlags & TOUCHEVENTF_PRIMARY) != 0);
    
    CStroke* pStrkNew = new CStroke(pInput->dwID, strokeColor);
    pStrkNew->Add(pt);
    
    // 새로 생성한 스트로크 객체를 현재 드로잉 중인 스트로크 정보를 보관하는 
    // m_StrkColDrawing 변수에 추가해 줍니다.
    m_StrkColDrawing.Add(pStrkNew);
 
    return TRUE;
}
 
// OnTouchInputMove - 멀티터치 입력이 지속적으로 들어오고 있을 때의 처리를 구현하는 함수.
BOOL CChildView::OnTouchInputMove(CPoint pt, PTOUCHINPUT pInput)
{
    // 한 번에 다수의 터치 입력이 들어올 수 있다.
    // TOUCHINPUT 구조체의 dwID 변수를 이용해 
    // 현재 드로잉 하고 있는 스트로크들의 데이터 집합을 찾아낸다.
    int strokeIndex = m_StrkColDrawing.FindStrokeById(pInput->dwID);
 
    if (strokeIndex >= 0) // 해당 ID의 데이터를 찾아낸 경우.
    {
        CStroke* pStrk =  m_StrkColDrawing[strokeIndex];
    
        // 터치 입력이 들어온 좌표값 Point 정보를 스트로크에 추가해 준다.
        pStrk->Add(pt);
 
        // 스트로크를 다시 그려준다. 
        pStrk->Draw(GetDC());
    }
    
    return TRUE;
}
 
// OnTouchInputUp - 멀티터치 입력이 끝날 때의 처리를 구현하는 함수.
BOOL CChildView::OnTouchInputUp(CPoint pt, PTOUCHINPUT pInput)
{
    // 한 번에 다수의 터치 입력이 들어올 수 있다.
    // TOUCHINPUT 구조체의 dwID 변수를 이용해 
    // 현재 드로잉 하고 있는 스트로크들의 데이터 집합을 찾아낸다.
    int strokeIndex = m_StrkColDrawing.FindStrokeById(pInput->dwID);
 
    if (strokeIndex >= 0) // 해당 ID의 데이터를 찾아낸 경우.
    {
        CStroke* pStrkCopy = m_StrkColDrawing[strokeIndex];
 
        // 찾아낸 스트로크 정보를 m_StrkColDrawing 변수에서 제거하고...
        m_StrkColDrawing.RemoveAt(strokeIndex);
        
        // 제거한 CStroke 클래스의 포인터를 m_StrkColFinished 변수에 추가해준다.
        m_StrkColFinished.Add(pStrkCopy);
    }
    
    return TRUE;
}
 

 각각의 함수 본문에는 터치가 발생한 시점에 걸맞는 드로잉 코드들이 구현되어 있습니다. 샘플 원본에는 영어로 달려있던 주석을 제가 한글로 바꾸고 추가 설명도 좀 덧붙여 두었습니다. 아직까지는 별다른 설명 없이 코드만 보더라도 어렵지 않게 이해하실 수 있을 거예요.

 이 함수들의 본문보다 더 중요한 부분은, 이 함수들을 '언제 호출해 주어야 하느냐' 하는 점입니다. 멀티 터치 데이터의 입력 시점에 따른 상태를 확인하기 위해 주의깊게 확인해야 할 부분은 지난 포스팅에서 선언만 해두고 빈 함수 몸통만 덩그라니 남겨두었던 CChildView::OnTouchInput(...) 함수입니다. 이곳에서 현재 멀티터치 데이터를 조회해 현재 입력 시점이 어떻게 되는지, 혹은 부가적인 다른 정보들을 어떻게 얻어내는지를 알아보는 것이 이번 포스팅의 중요한 목표라고 볼 수 있죠. 아래의 코드를 유심히 봐주세요.

BOOL CChildView::OnTouchInput(CPoint pt, int nInputNumber, int nInputsCount, PTOUCHINPUT pInput)
{     
    if ((pInput->dwFlags & TOUCHEVENTF_DOWN) == TOUCHEVENTF_DOWN)
    {
        return OnTouchInputDown(pt, pInput); // 터치 시작!
    }
    else if ((pInput->dwFlags & TOUCHEVENTF_MOVE) == TOUCHEVENTF_MOVE)
    {
        return OnTouchInputMove(pt, pInput); // 터치 입력 진행중.
    }
    else if ((pInput->dwFlags & TOUCHEVENTF_UP) == TOUCHEVENTF_UP)
    {
        return OnTouchInputUp(pt, pInput);    // 터치 입력 마무리.
    }
 
    return FALSE;
}

OnTouchInput 함수의 인자로 TOUCHINPUT 구조체의 포인터가 전달되는데, 이 구조체의 멤버 변수인 dwFlag에 담겨있는 flag의 상태를 조회함으로써 현재 터치 입력에 대한 여러 가지 정보들을 알아낼 수 있습니다. 위에 적힌 코드에서도 dwFlag를 이용해 터치 데이터의 추가 정보들을 알아내고 있습니다. 위의 함수 본문을 복사해 OnTouchInput 함수의 구현을 대체해 주세요.

이 중 일련의 멀티터치 입력이 처음 시작될 때의 처리를 위한 OnTouchInputDown(…) 함수를 한 번 보기로 하죠. 여기에서도 TOUCHINPUT 구조체의 dwFlag 를 조회하는 코드를 볼 수 있습니다. 바로 TOUCHEVENTF_PRIMARY 플래그가 켜져 있는지를 확인해 보는 코드네요. 입력중인 터치 정보가 여러 개인 경우, 이 중 가장 먼저 시작된 (PRIMARY) 터치 정보 인지를 확인하는 부분입니다. PRIMARY 터치는 스트로크의 색상을 검정으로 유지하고, 그렇지 않은 경우는 미리 정의된 몇 가지 색상 정보를 rotation해가며 결정해 주기 위해서 PRIMARY 데이터 여부를 확인하고 있습니다. 스트로크의 색상을 결정해주는 GetTouchColor() 함수의 선언 및 구현도 아래와 같이 추가해 주세요.

// in ChildView.h 
private:
    COLORREF GetTouchColor(bool bPrimaryContact);
COLORREF CChildView::GetTouchColor(bool bPrimaryContact)
{
    static COLORREF c_arrColor[] =  // 2번째 색상 정보를 미리 정의하는 static array.
    {
        RGB(255, 0, 0),             // Red
        RGB(0, 255, 0),             // Green
        RGB(0, 0, 255),             // Blue
        RGB(0, 255, 255),           // Cyan
        RGB(255, 0, 255),           // Magenta
        RGB(255, 255, 0)            // Yellow
    };
 
    COLORREF color;
    if (bPrimaryContact)
    {
        // primary contact 는 항상 black으로 표시한다.
        color = RGB(0,0,0);         // Black
    }
    else
    {
        // secondary 색상을 결정 한 후,
        color = c_arrColor[m_iCurrColor];
 
        // array 의 다음 색상으로 인덱스를 이동시켜 준다.
        m_iCurrColor = (m_iCurrColor + 1) % (sizeof(c_arrColor)/sizeof(c_arrColor[0]));
    }
 
    return color;
}

 이제 추가해야 할 코딩의 마지막 입니다. 드로잉을 하면서 추가해 주었던 스트로크 정보들의 해제 처리를 소멸자에 넣어주세요. 멀티터치 데이터의 조작 방식을 알아보기 위한 이번 예제는 동적 메모리 할당의 효율적인 처리 같은 관점에서는 그다지 효율적이지 못합니다. 그래서 실행해보면 다소 반응 속도가 느리다고 느껴지실 수도 있습니다만, 이번 예제가 어디까지나 MFC 프로젝트에서 WM_TOUCH 메세지의 조작 방식을 알아보기 위함임을 감안해 주세요 ㅎㅎㅎ 그래도 양심상 할당한 메모리의 해제는 해주어야겠지요 ^^;… CChildView의 소멸자에 아래 코드를 넣어주시면 이제 코드 추가는 모두 완료입니다!

CChildView::~CChildView()
{
    for (int i = 0; i < m_StrkColDrawing.GetCount(); ++i)
    {
        delete m_StrkColDrawing[i];
    }
 
    for (int i = 0; i < m_StrkColFinished.GetCount(); ++i)
    {
        delete m_StrkColFinished[i];
    }
}

 

Task 5 : 프로그램을 실행하고 직접 스크린을 ‘멀티’터치해 봅니다.

이제 빌드하고 프로그램을 실행해 봅니다. 스크린 터치를 통해서 view 영역에 동시에 서로 다른 라인이 그려지는 기능을 직접 확인해 보세요. 샘플 프로그램을 훌륭하게 서로 다른 panning 제스처를 인식해 각각의 입력에 따른 스트로크를 출력하게 됩니다.

 

  

Outro

이번 글에서는 지난번 포스팅에 이어서 WM_TOUCH 메세지를 사용해 멀티터치 UX를 구현하는 TouchPad 예제를 완성해 보았습니다. WM_TOUCH 메세지가 발생할 때마다 호출되는 CWnd::OnTouchInput() 함수에서

  1. Touch 입력의 시작, 진행 중, 종료 시점을 알아내는 방법
  2. DWORD 타입의 ID값을 이용해 서로 다른 터치 입력을 구분하는 방법
  3. 동시에 여러 개의 멀티터치가 발생할 때, 먼저 일어난(primary) 터치 데이터를 구별하는 방법

… 등을 알아보았습니다. 또한 이를 활용해 동시 다발적인 멀티터치 입력에 대해 각기 다른 panning 제스처로 인식해 궤적을 그려내는 방법을 예제 구현과 함께 알아보았습니다. 이를 통해 WM_TOUCH 메세지를 제어하는 방법을 보다 쉽게 이해할 수 있으셨을 겁니다.

Native 코드로 멀티터치 UX를 구현하는 방법 중에 아직 소개해 드리지 않은 영역이 있습니다. MFC로 제공되는 기능은 아니지만, Com-Interface 형태로 제공되는 Manipulation(MS Surface에서와 같은 위젯 처리 기법. 한번에 여러 개의 제스처를 동시 적용한다.), Inertia(관성)의 제어기법 등이 그것입니다. 다음 포스팅에서는 이러한 부분에 대해서 알아보도록 하겠습니다.

그럼 다음에도 재미있는 내용으로 다시 찾아 뵙도록 하겠습니다. 

감사합니다 ^^*

 

Reference

    신고
    크리에이티브 커먼즈 라이선스
    Creative Commons License

    Intro

    안녕하세요 ~ MFC 카테고리의 꽃집총각 입니다.

    지난번 포스팅까지는 멀티터치를 좀 더 손쉽게 구현할 수 있도록 해주는 제스처(GESTURE)를 이용한 방법을 알아보았습니다. 기본적으로는 OS 자체에서 이미 한 번 가공하고 난 데이터를 WM_GESTURE라는 메세지를 전달해 줍니다. 이런 방식을 통해 프로그래머는 가공된 데이터를 바로 사용하기만 하면 된다는 장점도 가지지만, 좀 더 커스텀한 나만의 터치입력 제어처리를 하기에는 다소 제한적이라는 단점도 동시에 얻게 됩니다. 오늘은 이런 경우를 위해, OS의 처리를 아무 것도 거치치 않은 순수 터치입력 데이터인 WM_TOUCH메세지를 이용해 멀티터치 UX를 구현하는 방법을 알아보도록 하겠습니다.

    이전과 동일하게 본문의 내용은 msdn의 channel9에 공개되어 있는 예제를 기본 내용으로 합니다. 원문의 출처는 ( http://channel9.msdn.com/learn/courses/Windows7/Multitouch/Win7MultitouchMFC/Exercise-1-Build-a-Multitouch-Application/ ) 입니다.

    어떨 때 WM_TOUCH를 이용해야 하나요?

    그럼 일단 예제를 살펴보기 전에, WM_GESTOURE로 구현할 수 없는 멀티터치 UX의 예제란 어떤 게 있을지 한 번 이야기 해 보겠습니다. 위에 있는 이미지는 Windows7에 기본으로 포함되어있는 그림판 입니다. 제 PC에서 마우스 두 개를 꽂고, 가상 멀티터치 드라이브를 통해 마우스의 입력을 터치 입력으로 인식하게 변경한 다음, 그림판의 view 영역에 동시에 두 개의 브러시를 통해 그림을 그리고 있는 모습입니다. 멀티터치… 멀티터치니까 당연히 두 개의 panning 제스처 인식이 동시에 처리되는 게 맞겠죠. 제가 마우스가 하나 더 있다면 세 개도 동시에 드로잉 할 수 있을 겁니다. (손은 두 개지만… ㅡ,.ㅡ;..)

    ‘좋아, 나는 팀 블로그에서 제스처를 이용한 멀티터치 프로그래밍 방법을 익혔으니까 이걸 내가 직접 한 번 짜봐야지!’ 하고 마음을 먹어봅니다. 하지만… 어떻게 구현해야 할 지 막상 감이 잡히질 않는군요. panning 제스처니까 CWnd::OnGesturePan(CPoint ptFrom, CPoint ptTo) 함수를 상속받아서 구현하면 될까요? 근데 입력이 동시에 두 개가 들어오면 함수가 어떤 식으로 호출될까요?

    제스처를 통한 구현방법을 사용하는 경우엔 먼저 입력된 한 개의 panning 제스처만 인식이 됩니다. 만약에 제스처를 통해 위에 올린 스크린샷과 같은 저런 터치를 입력하면, OS는 저 것을 두 개의 panning이 아닌 rotate 내지는 zoom 제스처로 번역하게 되겠지요. 그림판에 저렇게 두 개의 라인이 드로잉 되는 모습을 보고 있어서 그렇지, 실제로 저 입력 동작을 지난 번 예제인 사각박스 움직이기 샘플에 입력했다면 당연히 zoom 내지는 rotate의 효과를 기대하게 될 겁니다.

    바로 이런 경우, WM_GESTURE가 아닌 WM_TOUCH 메세지를 사용하면 되겠습니다. 동시에 입력되는 두 개의 터치 데이터를 그대로 전달받아서 각각의 panning 제스처로 인식하도록 직접 처리하면 되겠지요. 그렇게 하려면 어떻게 해야 하는지 지금부터 함께 예제를 작성하면서 알아 보도록 하겠습니다.

    Task 1 : MFC Application 프로젝트를 만들자.

    WM_GESTURE 활용 예제와 유사하게 이번에도 프로젝트 생성부터 단계적으로 알아보도록 하겠습니다. MFC 응용 프로그램 마법사에서 MFC 표준 스타일의 SDI 타입을 지정해 줍니다. wizard의 주요한 설정 화면을 이번에도 스크린샷으로 대신하겠습니다. 지난번과 달리 이번엔 한글판 스크린샷을 찍었네요 ㅎㅎ

     

     

     

     

    Task 2 : 하드웨어 상태 확인 및 Touch WIndow 설정

    하드웨어 상태 확인은 지난번에 [MFC/윈도우 7 멀티터치] #2 : 제스처(gesture)를 이용한 구현(上) 편에서 해주었던 부분과 동일합니다. 자세한 설명은 지난 번 글을 참고해 주시고요, 간단히 CTouchPadApp::InitInstance() 함수에 추가해줄 소스코드만 다시 한번 적어보기로 하지요.

    BYTE digitizerStatus = (BYTE) GetSystemMetrics(SM_DIGITIZER);   
    if ((digitizerStatus & (NID_READY | NID_MULTI_INPUT)) == 0)  
    {   
        AfxMessageBox(L"현재 터치 입력이 불가능한 상태입니다.");   
        return FALSE;   
    }   
        
    BYTE nInputs = (BYTE) GetSystemMetrics(SM_MAXIMUMTOUCHES);   
       
    CString str;  
    str.Format(L"현재 %d개의 터치를 동시 인식할 수 있습니다.", nInputs);  
    AfxMessageBox(str);

    위에 있는 코드를 CTouchPadApp::InitInstance() 에 넣고 프로그램을 실행시켰을 때, 아래와 같은 안내 메세지가 나오는걸 확인해 주세요.

    하드웨어가 터치를 인식할 수 있는 장치인지를 확인하고 난 다음엔 예제 프로그램의 view 영역이 WM_TOUCH 메세지를 받을 수 있게끔 등록해 주는 절차가 필요합니다. 그렇지 않으면 터치 입력이 들어왔을 때 예전과 똑같이 WM_GESTURE만 날아오게 될 테니까요. 터치 메시지를 받는 윈도우로 등록해 주기 위해서는 CWnd::RegisterTouchWIndow() 함수를 호출해 주면 됩니다. 이 처리를 CChildView의 OnCreate() 함수에서 해주기로 합시다.

    Ctrl + Shift + X 키를 눌러서 MFC Class Wizard 창을 띄워줍니다. WM_CREATE 메세지를 처리하는 핸들러를 추가해주세요. 그리고 핸들러에 아래의 간단한 코드를 추가하면 끝입니다 :)

        if (!RegisterTouchWindow())
        {
            ASSERT(FALSE);
        }

    CWnd::RegisterTouchWIndow() 함수를 호출해주면 윈도우를 터치 윈도우로 등록하게 됩니다. 함수의 인자가 default value 때문에 생략되었는데, 터치 윈도우 등록을 해제하고 싶은 경우도 RegisterTouchWIndow( FALSE ) 를 호출해서 처리합니다. 터치 윈도우로 등록되고 나면 더이상 WM_GESTURE 메세지는 발생되지 않으며, 아무 가공도 거치지 않은 저레벨의 순수 터치 입력 데이터를 전달해 주는 메세지인 WM_TOUCH가 발생하게 됩니다.

    헌데 우리는 지금 Win32 프로젝트가 아니라 MFC 프로젝트를 보고 있지요 ㅎㅎ WM_TOUCH를 메시지 프로시저 함수에서 바로 얻어오자는 게 아닌 이상, WM_TOUCH 발생시 호출되는 함수를 알아봐야겠습니다. 우리는 CWnd::OnTouchInput() 함수를 이용하면 되겠군요. 하지만 이 함수는 MFC 클래스 마법사에서는 표시되지 않으니, 직접 손으로 선언과 구현부를 적어주어야 합니다. ChildView.h 파일과 ChildView.cpp에 각각 아래의 코드를 넣어주세요.

    // ChildView.h 파일에 추가.
    // Overrides
    protected:
        virtual BOOL OnTouchInput(CPoint pt, int nInputNumber, int nInputsCount, PTOUCHINPUT pInput);

    // ChildView.cpp 파일에 추가. BOOL CChildView::OnTouchInput(CPoint pt, int nInputNumber, int nInputsCount, PTOUCHINPUT pInput) { // TODO: Handle Tocuh input messages here return FALSE; }

    자, 이제 low-level의 터치 데이터를 직접 받아 처리하기 위한 모든 준비가 끝이 났습니다. 이제 사용자가 프로그램의 view 영역에 터치 입력을 할 때마다 OnTouchInput(…) 함수가 호출될 겁니다. 본격적인 터치 데이터의 활용 방법은 다음 포스팅에서 알아보도록 하겠습니다 ㅎㅎ

    Outro

    이번 포스팅에서는 WM_TOUCH를 이용해 멀티터치 UX를 구현해야 하는 경우에 대한 설명, 윈도우를 터치 윈도우로 등록하는 방법, WM_TOUCH가 발생할 때마다 호출되는 CWnd의 멤버함수 등에 대해 알아보았습니다. WM_TOUCH의 프로그래밍 방법을 알아보기 위한 예제인 TouchPad라는 이름의 프로젝트를 생성부터 기본 설정까지 함께 알아보았고요. 다음 포스팅 에서는 예제를 완성시켜 나가면서, low-level 터치 데이터를 조작하는 코드를 함께 알아보도록 하겠습니다.

    무더운 여름날씨에 건강 조심하시고 다음 포스팅에서 뵙도록 하겠습니다.
    감사합니다 ^^*

    Reference

    신고
    크리에이티브 커먼즈 라이선스
    Creative Commons License

    Intro

    안녕하세요~ MFC 카테고리의 꽃집총각 입니다.

    이번 포스팅에서는 앞 글에서 이야기 중이었던 예제 프로그램의 나머지 부분을 마저 작성하고, MFC 기반의 프로젝트에서 제스처를 이용해 멀티터치 프로그래밍을 제어하는 방법에 대한 보충 설명을 정리해 보도록 하겠습니다. 앞에 적은 글과 이어지는 내용이니까, 앞부분을 보지 못하신 분들은 ( http://vsts2010.net/292 ) 이 글을 먼저 읽어주세요 ^^ 지난번 글에서는 멀티터치 동작을 확인하기 위한 Drawing 처리까지 함께 알아봤습니다. 앞부분에서 진행됐던 내용을 소제목만 간추려서 알아보면,

    * 제스처를 인식하는 첫 번째 예제 프로그램 *

    1. MFC Application 프로젝트를 만들자
    2. 하드웨어 상태를 알아봅시다
    3. View 영역에 사각형을 그리자!

    …와 같은 내용이었습니다. 두 번째 스텝에서 알아봤던 하드웨어의 멀티터치 인식 가능여부 확인방법을 제외하고는 멀티터치와 직접적으로 연관된 내용은 많이 없었군요. 이번 시간부터는 본격적으로 터치 입력을 제어하는 코드를 함께 알아보겠습니다.

     

    Task 4 : 터치 인식 코드를 넣을 차례!

    자, 이제 드디어 제스처를 제어하는 코드를 알아볼 시간입니다. 사용자 경험(UX;User Experience) 분야에서 가장 뜨거운 이슈인 멀티터치! 뭔가 색다르고 놀라운 구현방식이 있을 거 같지만 실망(?)스럽게도 구현방법은 기존의 MFC 프로그래밍과 별다른 바가 없습니다. 그저 적당한 함수를 오버라이딩 해서 필요한 코드를 넣어주면 그만이거든요. 바로 이 점이 새롭게 변화된 MFC 추가기능들이 마음에 드는 이유 중에 하나입니다. 바로 ‘친숙한 인터페이스’ 말입니다 :)

    MFC의 윈도우 랩핑 클래스인 CWnd에는 이번에 새롭게 추가된 제스처 관련 함수들이 있습니다. 이 중에서 아래의 다섯 가지 제스처 핸들링 함수를 오버라이딩 합니다. 각각의 함수들은 이름에 명시되어 있는 제스처가 입력됐을 때 호출됩니다. 아래의 코드를 ChildView.h 파일의 class CChildView 선언에 넣어줍니다.

    // Overrides
    protected:
        // Gesture handlers
        virtual BOOL OnGestureZoom(CPoint ptCenter, long lDelta);
        virtual BOOL OnGesturePan(CPoint ptFrom, CPoint ptTo);
        virtual BOOL OnGestureRotate(CPoint ptCenter, double dblAngle);
        virtual BOOL OnGesturePressAndTap(CPoint ptFirstFinger, long lDelta);
        virtual BOOL OnGestureTwoFingerTap(CPoint ptCenter);

    기본적으로 터치가 인식되면 애플리케이션으로 WM_GESTURE 메세지가 날아옵니다. Win32 API만을 이용해 제스처를 제어하려면 WndProc 프로시저 함수에서 WM_GESTURE를 잡아서 처리해야 하지만, MFC 애플리케이션의 경우 자체적으로 각각의 제스처에 대한 전용 핸들링 함수들이 나뉘어져 있기 때문에, 제어가 필요한 제스처에 해당하는 함수를 용도에 맞게 따로따로 오버라이딩 해서 사용하면 됩니다.

    Note : 그런데 CWnd의 선언을 살펴보면 (afxwin.h 파일에 있습니다.) 위에 나열된 함수 말고도 OnGesture 함수가 정의되어 있는 것을 확인하실 수 있습니다. 함수의 원형은 아래와 같습니다.

    afx_msg LRESULT OnGesture(WPARAM wParam, LPARAM lParam);

    오호라~ 이것은 WM_GESTURE 메세지가 넘어올 때마다 해당 메세지를 직접 제어할 수 있는 함수인가 보군요~ 함수의 인자로 wParam, lParam이 모두 날아오니 win32 프로젝트에서 하는 것처럼 코딩하고 싶을 땐 이걸 상속받아서 작업하면 되겠구나 ~ … 라고 생각하고, 실제로 테스트 코드도 만들어 봤었지만 안되더군요 @.@… 똑똑한 여러분들은 이유가 무엇인지 바로 찾으셨을 거라고 생각합니다만… 저 함수는 가상 함수가 아닙니다 ^^;… 그저 이름만 보고 상속받아 써야지 했는데, 나중에 보니 재정의 할 수 없는 일반 멤버함수더라고요 ㅎㅎ 아마도 CWnd가 자체적으로 처리하는 코드를 구현한 부분이 아닌가 생각됩니다. 일반적으로는 각각의 제스처마다 독립적으로 호출되는 위의 다섯 가지 함수를 이용하면 되고요, 경우에 따라 부득이하게 WM_GESTURE 메세지를 직접 제어하고 싶을 때엔… 이전에 그랬던 것처럼 WndProc을 직접 제어하도록 하고 그곳에서 WM_GESTURE를 받은 경우에 대한 switch-case 문을 넣어주면 되겠죠 ^^

    이제 위에 소개된 다섯 개의 함수들에서 view 영역에 그려지고 있는 사각형을 제어하는 코드들을 넣어줍니다. 아래에 함수들의 본문 코드가 있습니다.

    BOOL CChildView::OnGesturePan(CPoint ptFrom, CPoint ptTo)
    {
        int dx = ptTo.x - ptFrom.x;
        int dy = ptTo.y - ptFrom.y;
        
        if (dx != 0 || dy != 0)
        {
            m_drawingObject.Move(dx, dy);
            RedrawWindow();
        }
        return TRUE;
    }
     
    BOOL CChildView::OnGestureZoom(CPoint ptCenter, long lDelta)
    {
        if ((m_pCurrentGestureInfo->dwFlags & GF_BEGIN) == GF_BEGIN)
        {
            m_dblZoomRatioStart = m_dblZoomRatioTotal = lDelta;
        }
        else if (lDelta != 0)
        {
            m_dblZoomRatioTotal += lDelta;
            double zoomFactor = (double)m_dblZoomRatioTotal / m_dblZoomRatioStart;
            
            m_drawingObject.Zoom(zoomFactor, ptCenter.x, ptCenter.y);
            
            m_dblZoomRatioStart = m_dblZoomRatioTotal;
            RedrawWindow();
        }
        return TRUE;
    }
     
    BOOL CChildView::OnGestureRotate(CPoint ptCenter, double dblAngle)
    {
        if ((m_pCurrentGestureInfo->dwFlags & GF_BEGIN) == GF_BEGIN)
        {
            // Make the first center, the rotating one
            m_ptCenter = ptCenter;
        }
        else if (dblAngle != 0.)
        {
            m_drawingObject.Rotate(dblAngle * PI / 100.0, m_ptCenter.x, m_ptCenter.y);
            RedrawWindow();
        }
        
        return TRUE;
    }
     
    BOOL CChildView::OnGesturePressAndTap(CPoint ptFirstFinger, long lDelta)
    {
        if ((m_pCurrentGestureInfo->dwFlags & GF_BEGIN) != 0)
        {
            m_drawingObject.ShiftColor();
            RedrawWindow();
        }
        
        return TRUE;
    }
     
    BOOL CChildView::OnGestureTwoFingerTap(CPoint ptCenter)
    {
        m_drawingObject.TogleDrawDiagonals();
        RedrawWindow();
        
        return TRUE;
    }
     

    함수의 이름과 전달되는 인자들 모두 직관적입니다. 함수 본문의 예제 코드들도 그리 어렵지 않군요. 위의 코드들을 복사해 적어준 뒤 빌드하고 프로그램을 실행해 봅니다. 애플리케이션이 뜨면 사각형을 손으로 움직여 보세요. 터치 입력에 따라 사각형은 이동하고, 늘어나고, 회전할겁니다 :)

    네…? 근데 뭔가가 안 된다고요?

     

    Task 5 : 다 되는 거 같은데 회전만 안되네요 ㅡㅜ…

    여기까지 진행하고 테스트를 해보면 다른 제스처는 다 인식을 하는데, 회전(rotate)만 제대로 안 되는 현상을 겪으실 겁니다. 참고로 이번 예제에서 확인하실 수 있는 다섯 가지 제스처에 대한 동작을 간략히 설명 드리면 아래와 같습니다.

    panning 손가락을 스크린에 대고 이동한다.
    zoom 두 손가락을 스크린에 대고 벌리거나 모은다.
    rotate 두 손가락을 스크린에 대고 회전시킨다.
    press and tab 한 손가락을 스크린에 댄 상태에서 다른 손가락으로 스크린을 빠르게 터치한다.
    two finger tab 두 손가락을 동시에 스크린에 붙였다 뗀다.

    그런데 다른 건 다 잘되는데 아마 rotate 제스처만 동작하지 않을 거예요. 그리고 다른 제스처들은 다 인식할 테지만… 사각형 내부에서 일어난 제스처인지를 판단하는 처리가 없었기 때문에, 꼭 사각형 내부가 아니더라도 client 영역 내에서 발생한 유효한 제스처라면 모두 다 인식하는 걸 확인할 수 있으실 겁니다.

    그 이유는 코드가 잘못된 것이 아니라, 윈도우 자체에서 다른 제스처들은 모두 기본적으로 활성화 되어있는데, rotate만은 기본적으로 비활성화 되어있기 때문입니다. 활성화 하려면 다 해두든가 아님 말든가 할 것이지 왜 rotate만 천대(?)하는지 자세한 내막은 모르겠습니다만… 그것이 현실이군요. rotate를 활성화하기 위해, 제스처의 설정(config)를 제어하는 방법을 알아보도록 합시다.

    MFC에서는 제스처의 설정을 쉽게 컨트롤 할 수 있도록 CGestureConfig라는 클래스를 제공합니다. CChildView의 선언( in ChildView.h )에 CGestureConfig 타입의 멤버변수를 하나 추가해줍니다.

    class CChildView 
    {
        // ...
     
    // Fields
    protected:
        // Holds gesture configuration
        CGestureConfig m_gestureConfig;
        
        // ...
    }

    그리고 새롭게 부활한 반가운 인터페이스, 마법사 중의 마법사 MFC Class Wizard를 띄워서 OnCreate 함수를 오버라이딩 합니다. (단축키 Ctrl + Shift + X 입니다 ^^)

    그리고 아래의 코드를 넣어주세요.

     
    int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct)
    {
        if (CWnd::OnCreate(lpCreateStruct) == -1)
            return -1;
     
        // TODO: Add your specialized creation code here
        GetGestureConfig(&m_gestureConfig);
        
        // Only rotation is not enabled by default
        m_gestureConfig.EnableRotate();
        
        SetGestureConfig(&m_gestureConfig);
     
        return 0;
    }
     
     
     
    그리고 나서 다시 한번 빌드하고 실행해 봅니다. 이제 rotate 제스처도 제대로 인식하는 첫 번째 예제 프로그램이 완성 되었습니다 ^^*
     
    자 드디어 첫 번째 예제의 완성입니다~ !! 짝짝짝 ~
     

    Task 6 : panning이 왜 이렇게 뻑뻑하지? 아이폰은 안 그런뎅..

     
    task5 까지가 channel9에서 소개한 MFC gesture 기능의 첫 번째 예제입니다. 필요한 부분과 불필요한 부분을 적절하게 구분해 잘 설명해 두어서, 한번 설명을 따라 작성해 보시는 것만으로 제스처 프로그래밍에 대한 대략적인 흐름을 손쉽게 파악하셨을 거라고 생각합니다.
     
    이대로 끝내기엔 좀 아쉬운 감이 있어서… 예제 코드에 대한 추가 설명을 드리고자 합니다. 사실 글 중간중간에도 제가 추가로 적은 글이 제법 있었지만… 마땅히 말씀드릴 타이밍을 못 찾았던 요것 한 가지만 더 말씀 드리도록 할게요 ㅎㅎ
     
    일단 예제를 조작해 보면 제일 먼저 거슬리는 부분이 바로 panning 제스처에 대한 조작감(?) – 게임개발만 오래 하다 보니 이런 표현이…^^; – 인데요. 아이폰에서 느껴지는 부드러운 반응성에 비해 사각형 움직임이 무척이나 둔하고 불만족스럽다는 걸 느끼실 겁니다. 사각형이 한 번 움직이고 나면 괜찮은데, panning 제스처를 인식해 움직여지기 시작할 때 까지가 뭔가 걸리는 느낌입니다. x, y 한 방향으로만 움직이고, 다른 방향으로는 안 움직이는 경우도 아마 겪으셨을 거예요. 왜 이럴까요? 기기의 한계? 아님 OS 자체의 한계?
     
    이런 현상은 하드웨어의 문제나 OS 기능 자체의 문제가 아닙니다. 이것도 역시 panning 제스처의 옵션에서 GC_PAN_WITH_GUTTER 플래그가 기본적으로 켜져 있기 때문에 나타나는 현상입니다. 저도 처음에 제스처(gesture)를 이용한 멀티터치 예제들을 실행해 보다가 panning의 무딘 반응성이 너무 눈에 거슬려서, 부드러운 움직임을 보려면 결국 WM_GESTURE에서는 한계가 있고, WM_TOUCH를 써야 하는가 보다 했었는데, 간단히 플래그만 조절해주면 좀 더 부드러운 움직임을 느낄 수가 있게 됩니다.

    GC_PAN_WITH_GUTTER에서 gutter는 홈통이나 배수로 등의 뜻으로, panning 제스처가 발생했을 때 가장 메인이 되는 방향으로의 움직임 이외의 방향성에 대한 반응은 특정 임계값을 넘기지 않는 이상 무시하게 하는 효과를 줍니다. 이 때문에 손가락에서의 미세한 움직임들이 무시되고, 반응성이 안좋다는 느낌을 받게 되죠. 이를 해결하기 위해서는 rotate 제스처의 활성화를 위해 오버라이딩 했었던 OnCreate 함수에서 panning에 대한 아래의 설정도 함께 처리해 주면 됩니다.

     
    config.EnablePan( TRUE, 
        GC_PAN_WITH_SINGLE_FINGER_VERTICALLY | 
        GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY |
        GC_PAN_WITH_INERTIA );
     
    pan 제스처를 활성화 하되, GC_PAN_WITH_GUTTER 플래그만을 제외한 나머지 플래그들을 활성화 해주는 코드입니다. 위 코드를 넣고 나서 빌드하고 다시 실행해보면 이전보다 훨씬 더 부드럽게 사각형이 이동되는 것을 느낄 수 있으실 겁니다.

     
     

    Outro

    이것으로 MFC를 이용해서 제스처를 이용한 멀티터치 프로그래밍 방법의 첫 번째 예제를 소개와 설명을 모두 마쳤습니다. 제스처의 설정을 컨트롤 하는 방법, 각각의 제스처에 대한 핸들링 코드를 넣어주는 방법 등을 알아보았는데, 대부분 예전 MFC 프로그래밍의 마우스/키보드 이벤트 처리 등과 비슷한 방식이었기 때문에 그리 어렵지는 않은 난이도였습니다.

    다음 포스팅 에서는 이제 가장 자유롭고 확장성 있는 (… 하지만 대신 좀 더 까다로운 ) WM_TOUCH를 이용한 멀티터치 프로그래밍 방법을 알아 보도록 하겠습니다. 혹시 그 전에 질문 사항들이나, 제스처 프로그래밍 방법들에 대한 추가 학습사항이 있다면 다시 한 번 정리하는 기회를 갖도록 하겠습니다.

    그럼 다음 포스팅에서 다시 인사 드리도록 하겠습니다. 그 때까지 더운 날씨에 모두들 건강하시고 공부 열심히 하세요 ~ ^^*

    감사합니다 ~ 꾸벅 ~

    Reference

    신고
    크리에이티브 커먼즈 라이선스
    Creative Commons License


     

    티스토리 툴바