Input pengguna: contoh yang diperluas
Mari kita gabungkan semua yang telah kita pelajari tentang input pengguna untuk membuat program gambar sederhana. Berikut adalah cuplikan layar program:
Pengguna dapat menggambar elipsis dalam beberapa warna yang berbeda, dan memilih, memindahkan, atau menghapus elipsis. Agar antarmuka pengguna tetap sederhana, program tidak membiarkan pengguna memilih warna elips. Sebagai gantinya, program secara otomatis menelusuri daftar warna yang telah ditentukan sebelumnya. Program ini tidak mendukung bentuk apa pun selain elipsis. Jelas, program ini tidak akan memenangkan penghargaan apa pun untuk perangkat lunak grafis. Namun, ini masih merupakan contoh yang berguna untuk dipelajari. Anda dapat mengunduh kode sumber lengkap dari Sampel Gambar Sederhana. Bagian ini hanya akan mencakup beberapa sorotan.
Elipsis diwakili dalam program oleh struktur yang berisi data elips (D2D1_ELLIPSE) dan warna (D2D1_COLOR_F). Struktur ini juga mendefinisikan dua metode: metode untuk menggambar elips, dan metode untuk melakukan pengujian hit.
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;
}
};
Program ini menggunakan kuas warna solid yang sama untuk menggambar isi dan kerangka untuk setiap elips, mengubah warna sesuai kebutuhan. Di Direct2D, mengubah warna kuas warna solid adalah operasi yang efisien. Jadi, objek kuas warna solid mendukung metode SetColor.
Elipsis disimpan dalam kontainer daftar STL:
list<shared_ptr<MyEllipse>> ellipses;
Catatan
shared_ptr adalah kelas smart-pointer yang ditambahkan ke C++ di TR1 dan diformalkan dalam C++0x. Visual Studio 2010 menambahkan dukungan untuk shared_ptr dan fitur C++0x lainnya. Untuk informasi selengkapnya, lihat artikel Majalah MSDN Menjelajahi Fitur C++ dan MFC Baru di Visual Studio 2010.
Program ini memiliki tiga mode:
- Mode gambar. Pengguna dapat menggambar elipsis baru.
- Mode pemilihan. Pengguna dapat memilih elips.
- Mode seret. Pengguna dapat menyeret elips yang dipilih.
Pengguna dapat beralih antara mode gambar dan mode pilihan dengan menggunakan pintasan keyboard yang sama yang dijelaskan dalam Tabel Akselerator. Dari mode pilihan, program beralih ke mode seret jika pengguna mengklik elips. Ini beralih kembali ke mode pemilihan ketika pengguna melepaskan tombol mouse. Pilihan saat ini disimpan sebagai iterator ke dalam daftar elipsis. Metode pembantu MainWindow::Selection
mengembalikan penunjuk ke elips yang dipilih, atau nilai nullptr jika tidak ada pilihan.
list<shared_ptr<MyEllipse>>::iterator selection;
shared_ptr<MyEllipse> Selection()
{
if (selection == ellipses.end())
{
return nullptr;
}
else
{
return (*selection);
}
}
void ClearSelection() { selection = ellipses.end(); }
Tabel berikut ini meringkas efek input mouse di masing-masing dari tiga mode.
Mouse Input | Mode Gambar | Mode Pemilihan | Mode Seret |
---|---|---|---|
Tombol kiri ke bawah | Atur tangkapan mouse dan mulai menggambar elips baru. | Rilis pilihan saat ini dan lakukan tes hit. Jika elips tertembak, ambil kursor, pilih elips, dan beralih ke mode seret. | Tidak ada tindakan. |
Gerakan mouse | Jika tombol kiri tidak berfungsi, mengubah ukuran elips. | Tidak ada tindakan. | Pindahkan elips terpilih. |
Tombol kiri ke atas | Berhenti menggambar elips. | Tidak ada tindakan. | Beralih ke mode pilihan. |
Metode berikut di MainWindow
kelas menangani pesan 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);
}
Koordinat mouse diteruskan ke metode ini dalam piksel, lalu dikonversi ke DIP. Penting untuk tidak membingungkan kedua unit ini. Misalnya, fungsi DragDetect menggunakan piksel, tetapi menggambar dan pengujian klik menggunakan DIP. Aturan umumnya adalah bahwa fungsi yang terkait dengan input windows atau mouse menggunakan piksel, sedangkan Direct2D dan DirectWrite menggunakan DIP. Selalu uji program Anda pada pengaturan DPI tinggi, dan ingatlah untuk menandai program Anda sebagai sadar DPI. Untuk informasi selengkapnya, lihat DPI dan Piksel Independen Perangkat.
Berikut adalah kode yang menangani pesan 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);
}
}
Logika untuk mengubah ukuran elips dijelaskan sebelumnya, di bagian Contoh: Lingkaran Gambar. Perhatikan juga panggilan ke InvalidateRect. Ini memastikan bahwa jendela dicat ulang. Kode berikut menangani pesan WM_LBUTTONUP.
void MainWindow::OnLButtonUp()
{
if ((mode == DrawMode) && Selection())
{
ClearSelection();
InvalidateRect(m_hwnd, NULL, FALSE);
}
else if (mode == DragMode)
{
SetMode(SelectMode);
}
ReleaseCapture();
}
Seperti yang Anda lihat, penangan pesan untuk input mouse semuanya memiliki kode percabangan, tergantung pada mode saat ini. Itu adalah desain yang dapat diterima untuk program yang cukup sederhana ini. Namun, itu bisa dengan cepat menjadi terlalu kompleks jika mode baru ditambahkan. Untuk program yang lebih besar, arsitektur model-view-controller (MVC) mungkin merupakan desain yang lebih baik. Dalam arsitektur semacam ini, pengontrol, yang menangani input pengguna, dipisahkan dari model, yang mengelola data aplikasi.
Ketika program beralih mode, kursor berubah untuk memberikan umpan balik kepada pengguna.
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);
}
Dan akhirnya, ingatlah untuk mengatur kursor saat jendela menerima pesan WM_SETCURSOR:
case WM_SETCURSOR:
if (LOWORD(lParam) == HTCLIENT)
{
SetCursor(hCursor);
return TRUE;
}
break;
Ringkasan
Dalam modul ini, Anda belajar cara menangani input mouse dan keyboard; cara menentukan pintasan keyboard; dan cara memperbarui gambar kursor untuk mencerminkan status program saat ini.