Creating Windows Touch Control Frameworks in C++ (PhotoStrip) の翻訳記事
マイクロソフトの田中達彦です。
Windowsタッチ関連の米国でのブログ記事を翻訳にかけましたので、紹介します。
今回紹介する記事は、「Creating Windows Touch Control Frameworks in C++ (PhotoStrip)」です。
オリジナルの記事はこちらです。
https://blogs.msdn.com/b/seealso/archive/2011/02/07/creating-windows-touch-control-frameworks-photostrip.aspx
=========================================================
C++ での Windows タッチコントロールフレームワークの作成 (PhotoStrip)
PDC 2009 で Reed Townsend が行ったプレゼンテーションでは、すばらしいマルチタッチのサンプルが紹介されていました。「Windows Touch Deep Dive」というタイトルです。次のビデオでこのプレゼンテーションをご覧いただけます。
(訳注:日本語字幕付きのビデオを下記サイトにて公開しています)
https://msdn.microsoft.com/ja-jp/events/hh237318.aspx
複数のウィンドウ オブジェクトを正しく扱う方法については、これまで多くの顧客から質問を受けてきました。このプレゼンテーションの前半では、そのベストプラクティスが説明されています。Reed がこのサンプルを紹介してから時間はたっていますが、そのソース コードを入手して、全体がどのように機能しているかについて説明を簡単にまとめました。Windows タッチ PhotoStrip のサンプルは私のサーバーからダウンロードできます。また、これ以降のメモで、さまざまな部分について説明しています。
概要
デモ用に作成されたこの PhotoStrip コントロールは、タッチ パックおよび Microsoft Surface で提供されているものと似ています。このデモでは、コントロールが作成されただけでなく、組み合わせてアプリケーションに配置できる複数の再利用可能なコントロールを使用するためのフレームワーク全体も提示されています。
デモの PhotoStrip コントロールのようなコントロールの一般的な目標として、次のようなことが考えられます。
• タッチ メッセージへの応答
• パンにおける制約付き操作の追加
• ドラッグによるコントロールからの出し入れのサポートの追加
• API サーフェスの追加
• オーバーレイとギャラリーの具体化
次の画像は、デモ アプリケーションの PhotoStrip コントロールを示しています。
画面の最上部と最下部に PhotoStrip コントロールがあります。コントロールに沿ってスライドすると、コントロールが動きます。最下部のコントロールから写真をドラッグして取り出すと、ギャラリーに表示されます。
デモの概要
PhotoStrip コントロールは、固有の HWND にカプセル化されています。これは、多数のコントロールを連携して動作させたい場合には問題になります。Windows タッチ メッセージは、最初にタッチ ダウン メッセージを受信した 1 つのウィンドウにしか送信されないためです。これを回避するため、オーバーレイ ウィンドウがすべての入力を受信し、ヒット検出に基づいて適切なコントロールに入力をマップします。次の図は、このウィンドウ階層の概要を示しています。
オーバーレイがあるウィンドウ階層を前提として、コントロール間のデータ フローを示したのが次の図です。
このメッセージ フローでは、WM_TOUCH メッセージが入力ハードウェアによって生成されます。このメッセージはオーバーレイ HWND によって受信されます。次に、オーバーレイ HWND がヒット検出を実行し、適切な子 HWND (たとえば PhotoStrip ウィンドウ) にメッセージを送信します。ユーザーが写真をギャラリーにドラッグすると、PhotoStrip はアプリケーションに対して、ユーザーが写真をギャラリーにドラッグしたことを伝えるメッセージを送信します。さらに、アプリケーションがギャラリーに対して、ギャラリー内に写真を表示するように伝えます。
次の画像では、実行中のデモのスクリーンショット上に、デモ アプリケーションのコントロールを図解しています。
アプリケーションの最上部と最下部に PhotoStrip コントロールがあり、Gallery コントロールはアプリケーションのレイアウトの中央にあります。
プログラミングの詳細
オーバーレイ コントロールに対するメッセージングの処理
アプリケーションは、最初にオーバーレイ ウィンドウを作成し、次に Gallery と PhotoStrip のコントロールを作成します。これらのコントロールは、オーバーレイに登録されます。
これを実行するのが次のコードです。
HRESULT InitWindow( HINSTANCE hInstance )
{
(…)
if( SUCCEEDED( hr ) )
{
// ウィンドウを作成する
hWnd = CreateWindowEx(0, WINDOW_CLASS_NAME, L"CollageSample",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, 0);
if (!(hWnd))
{
hr = E_FAIL;
}
}
if( SUCCEEDED( hr ) )
{
ShowWindow( hWnd, SW_SHOWMAXIMIZED );
DisableTabFeedback(hWnd);
}
if (SUCCEEDED( hr ))
{
GetClientRect((hWnd), &rClient);
iWidth = rClient.right - rClient.left;
iHeight = rClient.bottom - rClient.top;
iTopHeight = (INT)(iHeight * TOP_HEIGHT_PERCENTAGE);
iBottomHeight = (INT)(iHeight * BOTTOM_HEIGHT_PERCENTAGE);
hr = Overlay::CreateOverlay(hInstance, hWnd, 0, 0,
iWidth, iHeight, &g_pOverlay);
}
if (SUCCEEDED( hr ))
{
hr = Photostrip::CreatePhotostrip(hInstance, (hWnd),
0, 0,
iWidth, iTopHeight,
TOP_PHOTOSTRIP_DIRECTORY,
Photostrip::Top,
&g_pStripTop);
}
if (SUCCEEDED( hr ))
{
hr = Photostrip::CreatePhotostrip(hInstance, (hWnd),
0, rClient.bottom - iBottomHeight,
iWidth, iBottomHeight,
BOTTOM_PHOTOSTRIP_DIRECTORY,
Photostrip::Bottom,
&g_pStripBottom);
}
if (SUCCEEDED( hr ))
{
hr = Gallery::CreateGallery(hInstance, hWnd, 0, iTopHeight,
iWidth, (iHeight - iTopHeight - iBottomHeight), &g_pGallery);
}
if (SUCCEEDED( hr ))
{
g_pOverlay->RegisterTarget((ITouchTarget*)g_pGallery);
g_pOverlay->RegisterTarget((ITouchTarget*)g_pStripBottom);
g_pOverlay->RegisterTarget((ITouchTarget*)g_pStripTop);
}
WM_TOUCH メッセージが生成されると、オーバーレイ ウィンドウがそれらのメッセージを WndProc メソッドで受信します。オーバーレイは、メッセージに含まれる座標を画面座標からクライアント座標に変換し、メッセージを追跡し続けます。オーバーレイは特定の入力を追跡し、特定のコントロールにキャプチャを渡します。
次のコードは、オーバーレイがどのようにして適切な子コントロールに入力をマップするかを示しています。
LRESULT CALLBACK Overlay::S_OverlayWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
// WM_TOUCH メッセージとマウス メッセージを転送
// ダウン イベントの場合、ヒット テストを再実行してキャプチャを設定
// そうでない場合、カーソルを現在キャプチャしている対象にイベントを送信
LRESULT result = 0;
INT iNumContacts;
PTOUCHINPUT pInputs;
HTOUCHINPUT hInput;
POINT ptInputs;
ITouchTarget *pTarget = NULL;
Overlay* pOverlay;
pOverlay = (Overlay*)GetWindowLongPtr(hWnd, 0);
if (pOverlay != NULL)
{
switch(msg)
{
case WM_TOUCH:
iNumContacts = LOWORD(wParam);
hInput = (HTOUCHINPUT)lParam;
pInputs = new (std::nothrow) TOUCHINPUT[iNumContacts];
if(pInputs != NULL)
{
if(GetTouchInputInfo(hInput, iNumContacts, pInputs, sizeof(TOUCHINPUT)))
{
for(int i = 0; i < iNumContacts; i++)
{
// 接触の数だけ追跡
if (pInputs[i].dwFlags & TOUCHEVENTF_DOWN)
{
pOverlay->m_ucContacts++;
}
else if (pInputs[i].dwFlags & TOUCHEVENTF_UP)
{
pOverlay->m_ucContacts--;
}
// タッチ入力情報をクライアント座標に変換
ptInputs.x = pInputs[i].x/100;
ptInputs.y = pInputs[i].y/100;
ScreenToClient(hWnd, &ptInputs);
pInputs[i].x = (LONG)(ptInputs.x);
pInputs[i].y = (LONG)(ptInputs.y);
// ダウン時にはヒット テストを実行し、キャプチャを設定
if (pInputs[i].dwFlags & TOUCHEVENTF_DOWN)
{
pOverlay-> m_pDispatch->CaptureCursor(pInputs[i].dwID,
(FLOAT)pInputs[i].x, (FLOAT)pInputs[i].y, &pTarget);
}
// このカーソルに対してキャプチャが設定されている対象があれば、イベントを送信
if (pOverlay->m_pDispatch->GetCapturingTarget(pInputs[i].dwID, &pTarget))
{
pTarget->ProcessTouchInput(&pInputs[i]);
}
}
}
delete [] pInputs;
}
CloseTouchInputHandle(hInput);
break;
case WM_LBUTTONUP:
ReleaseCapture();
pOverlay->m_fIsMouseDown = FALSE;
pOverlay->ProcessMouse(msg, wParam, lParam);
break;
case WM_MOUSEMOVE:
pOverlay->ProcessMouse(msg, wParam, lParam);
break;
case WM_LBUTTONDOWN:
SetCapture(hWnd);
pOverlay->m_fIsMouseDown = TRUE;
pOverlay->ProcessMouse(msg, wParam, lParam);
break;
case WM_DESTROY:
SetWindowLongPtr(hWnd, 0, 0);
PostQuitMessage(0);
delete pOverlay;
break;
default:
result = DefWindowProc(hWnd, msg, wParam, lParam);
}
}
else
{
result = DefWindowProc(hWnd, msg, wParam, lParam);
}
return result;
}
コントロールはそれぞれ ProcessTouchInput メソッド内で同様の方法でタッチ メッセージを処理し、その結果、コントロール上で _ManipulationEvents を発生させます。
PhotoStrip のタッチ入力処理
PhotoStrip そのものは、慣性機能を有効にするために InertiaObj ユーティリティ インターフェイスを実装します。次の図は、オーバーレイ ウィンドウから PhotoStrip コントロールに対してマップされた WM_TOUCH 入力と、その写真を示します。
コントロールにキャプチャが渡されると、WM_TOUCH メッセージは ManipulationEvents インターフェイスに送信され、その結果、このインターフェイス上で操作イベントを発生させます。コントロール内で横方向に移動を行うと、すべての写真が同時に操作されます。適切な写真に入力を送信するために PhotoStrip コントロール内でヒット テストが使用されます。これは、写真そのものはコントロール内で上下方向に操作される可能性があるためです。PhotoStrip コントロールは、入力の解釈が完了すると、コントロールのキャプチャを解放します。
次のコードは、ManipulationDelta ハンドラーで入力がどのように操作にマップされるかを示しています。
HRESULT STDMETHODCALLTYPE Photostrip::ManipulationDelta(
FLOAT /*x*/,
FLOAT /*y*/,
FLOAT translationDeltaX,
FLOAT /*translationDeltaY*/,
FLOAT /*scaleDelta*/,
FLOAT /*expansionDelta*/,
FLOAT /*rotationDelta*/,
FLOAT /*cumulativeTranslationX*/,
FLOAT /*cumulativeTranslationY*/,
FLOAT /*cumulativeScale*/,
FLOAT /*cumulativeExpansion*/,
FLOAT /*cumulativeRotation*/)
{
HRESULT hr = S_OK;
// 写真の周辺領域に移動の境界を設定する
FLOAT fpTotalWidth = -INTERNAL_MARGIN, fpNewXOffset;
std::list<Photo*>::iterator it;
for (it = m_lPhotos.begin(); it != m_lPhotos.end(); it++)
{
fpTotalWidth += (*it)->GetWidth() + INTERNAL_MARGIN;
}
fpTotalWidth = max(0, fpTotalWidth); // 写真がない場合の処理
fpNewXOffset = m_fpXOffset + translationDeltaX;
FLOAT fpXLowerBound = -fpTotalWidth + m_nWidth * (1-SIDE_MARGIN_PERCENTAGE);
FLOAT fpXUpperBound = m_nWidth * SIDE_MARGIN_PERCENTAGE;
fpNewXOffset = min(fpXUpperBound, fpNewXOffset);
fpNewXOffset = max(fpXLowerBound, fpNewXOffset);
translationDeltaX = fpNewXOffset - m_fpXOffset;
// 写真を移動
m_fpXOffset += translationDeltaX;
for (it = m_lPhotos.begin(); it != m_lPhotos.end(); it++)
{
(*it)->Translate(translationDeltaX, 0);
}
return hr;
}
次のコードは、ManipulationDelta イベント ハンドラーで、PhotoStrip がどのように操作を使用して写真をコントロールに沿って横方向に移動させるかを示しています。
// 写真を移動
m_fpXOffset += translationDeltaX;
for (it = m_lPhotos.begin(); it != m_lPhotos.end(); it++)
{
(*it)->Translate(translationDeltaX, 0);
}
次のコードは、PhotoStrip コントロール内の写真で操作がどのように処理されるか、および上下方向の操作についてどのような制約が付けられているかを示しています。
HRESULT STDMETHODCALLTYPE ConstrainedPhoto::ManipulationDelta(
FLOAT /*x*/,
FLOAT /*y*/,
FLOAT /*translationDeltaX*/,
FLOAT translationDeltaY,
FLOAT /*scaleDelta*/,
FLOAT /*expansionDelta*/,
FLOAT /*rotationDelta*/,
FLOAT /*cumulativeTranslationX*/,
FLOAT /*cumulativeTranslationY*/,
FLOAT /*cumulativeScale*/,
FLOAT /*cumulativeExpansion*/,
FLOAT /*cumulativeRotation*/)
{
HRESULT hr = S_OK;
Translate(0.0f, translationDeltaY);
return hr;
}
注: 写真オブジェクトからの境界チェックにより、写真が境界に到達したことがアプリケーションに通知されます。フェイクのタッチ アップ メッセージが送信され、PhotoStrip からキャプチャが解放されます。
次のコードは、写真がどのようにキャプチャを解放するかを示しています。
case PS_PHOTO_BOUNDARY:
if (pStrip != NULL)
{
Photo *pPhoto = (Photo*)lParam;
TOUCHINPUT tUp = pPhoto->GetLastTouchInput();
pStrip->m_pDispatch->ReleaseCapture(tUp.dwID);
// キャプチャを解放するとイベント ストリームから写真が除去されるため
// 写真はカーソルのアップを受信することはない
// イベント ストリームのビューの一貫性を保つためにフェイクのアップを写真に渡す
tUp.dwFlags &= ~TOUCHEVENTF_DOWN;
tUp.dwFlags &= ~TOUCHEVENTF_MOVE;
tUp.dwFlags |= TOUCHEVENTF_UP;
pPhoto->ProcessTouchInput(&tUp);
// ユーザーが写真をドラッグして出したことを親に伝える
SendMessage(GetParent(hWnd), PS_PHOTOSTRIP_BOUNDARY, (WPARAM)pStrip, lParam);
}
break;
親ウィンドウに対して生成されるメッセージには、Gallery コントロールへの写真の表示に使用できる写真オブジェクトへのポインターが格納されます。
Collage コントロール
写真が Collage ウィンドウに移動されると、Collage はメッセージを使用して、表示する画像を生成します。これを実行するのが次のコードです。
case PS_PHOTOSTRIP_BOUNDARY:
// Photostrip から写真がドラッグされて出された場合
// - 写真をドラッグしたカーソルのキャプチャを Photostrip から解放
// - そのカーソルのキャプチャを Gallery に設定し、直ちに写真を
// ドラッグ可能にする
// - Photostrip からドラッグされた写真を Gallery に読み込む
pPhoto = (Photo*)lParam;
pStrip = (Photostrip*)wParam;
tInput = pPhoto->GetLastTouchInput();
g_pOverlay->ReleaseCapturedCursor(tInput.dwID);
// キャプチャを解放したため、ストリップはこのイベント ストリームのアップを受信しない
// 一貫性を保つため、フェイクのアップを対象に渡す
tUp = tInput;
tUp.dwFlags &= ~TOUCHEVENTF_DOWN;
tUp.dwFlags &= ~TOUCHEVENTF_MOVE;
tUp.dwFlags |= TOUCHEVENTF_UP;
pStrip->ProcessTouchInput(&tUp);
// 親ウィンドウの座標に変換
GetWindowRect(pPhoto->GetHWnd(), &stripWnd);
GetWindowRect(g_pGallery->GetHWnd(), &galleryWnd);
tInput.x += stripWnd.left;
tInput.y += stripWnd.top;
// 写真のサイズと位置を計算
fpPhotoHeight = (FLOAT)(stripWnd.bottom - stripWnd.top)
* (1 - INTERNAL_Y_MARGIN_PERCENTAGE)
* PHOTO_SIZE_MULTIPLIER;
fpPhotoYPos = (FLOAT)(tInput.y - galleryWnd.top);
fpPhotoYPos -= ((stripWnd.bottom - stripWnd.top) * INTERNAL_Y_MARGIN_PERCENTAGE);
// 写真を Gallery に読み込む
g_pGallery->LoadPhoto(pPhoto->GetPhotoURI(),
(FLOAT)tInput.x, fpPhotoYPos, fpPhotoHeight);
// 入力を新しい写真に送信
g_pOverlay->SetCursorCapture(g_pGallery, tInput);
break;
写真が Collage ウィンドウの境界に到達すると、Collage は追跡対象リストからその写真を除外し、タッチ入力のキャプチャを解放します。次のコードは、写真が境界に到達した場合の処理を示しています。
case PS_PHOTO_BOUNDARY_INERTIA:
case PS_PHOTO_BOUNDARY:
if (pCollage != NULL)
{
// 削除対象の写真にマークを付ける。次にタイマーが作動したときに削除される
pPhoto = (Photo*)lParam;
if (pPhoto != NULL)
{
pCollage->m_lPhotosToDelete.remove(pPhoto); // 2 回追加しないよう注意
pCollage->m_lPhotosToDelete.push_front(pPhoto);
}
}
break;
次のコードは、慣性の処理中に境界を越えた場合の処理を示しています。
case WM_TIMER:
if (pCollage != NULL)
{
// 慣性の処理
for (it = pCollage->m_lPhotos.begin(); it != pCollage->m_lPhotos.end(); it++)
{
(*it)->ProcessTimer();
}
// タイマー処理は境界イベントにより発生する可能性がある
// その場合、境界を越えた写真のクリーンアップが必要
for (it = pCollage->m_lPhotosToDelete.begin();
it != pCollage->m_lPhotosToDelete.end(); it++)
{
// 写真が境界を越えたままの場合のみ写真を削除。キャプチャを維持しているため、
// ユーザーはウィンドウから写真をドラッグして出したり戻したりできる
if ((*it)->IsPastBounds())
{
// その時点でマップ処理が行われていたら解放
DWORD dwID = 0;
while(pCollage->m_pDispatch->GetCursor(*it, &dwID))
{
pCollage->m_pDispatch->ReleaseCapture(dwID);
}
pCollage->m_lPhotos.remove((Photo*)(*it));
pCollage->m_pDispatch->UnregisterTouchTarget((*it));
(*it)->CleanUp();
}
}
pCollage->m_lPhotosToDelete.clear();
pCollage->Render();
}
break;
結論
再利用可能なコンポーネントを提供するための最善の方法は、複雑な Windows タッチ アプリケーション用のコントロール フレームワークを作成することです。このドキュメントの更新版は、2 月末にオンラインで公開予定です (https://code.msdn.microsoft.com/WinTouchPhotostrip ※訳注:公開済み)。更新情報は、Twitter アカウント @WinDevs でご覧いただけます。C++ コードに色が付いていないことをお詫びします。現在、このブログプラットフォームで機能するソリューションの準備を進めています。
関連情報
- Windows タッチ メッセージ - はじめに (MSDN)
- Windows タッチ アプリケーションのモバイル ユーザー向け強化 (MSDN マガジン)
- Windows タッチ ガイダンス ホワイトペーパー ( 英語)
- WACOM Bamboo での Windows タッチのシミュレーション ( 英語) (ブログも参照)
- マルチタッチパート 1: Windows 7 におけるマルチタッチ - はじめに (Jennifer Marsman ブログ) ( 英語)
- Windows 7 について: タッチ機能
=========================================================