使用者輸入:擴充範例
讓我們結合我們所瞭解的使用者輸入,以建立簡單的繪圖程式。 以下是程式的螢幕快照:
用戶可以以數種不同的色彩繪製省略號,然後選取、移動或刪除省略號。 為了讓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 Magazine 文章 探索 Visual 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。 一般規則是與視窗或滑鼠輸入相關的函式使用圖元,而 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();
}
如您所見,滑鼠輸入的訊息處理程式全都有分支程式代碼,視目前模式而定。 這是這個相當簡單的程式可接受的設計。 不過,如果新增了新的模式,它可能會變得太複雜。 對於較大的程式,model-view-controller (MVC) 架構可能是更好的設計。 在這類架構中 ,處理使用者輸入的控制器會與 管理應用程式數據的模型分開。
當程式切換模式時,游標會變更以提供意見反應給使用者。
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;
摘要
在本課程模組中,您已瞭解如何處理滑鼠和鍵盤輸入;如何定義鍵盤快捷方式;以及如何更新數據指標影像,以反映程式的目前狀態。