[MFC/윈도우 7 멀티터치] #5 : WM_TOUCH 메세지를 이용한 구현(下)

MFC 2010. 7. 13. 09:00 Posted by 알 수 없는 사용자

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