Input pengguna: contoh yang diperluas

Mari kita gabungkan semua yang telah kita pelajari tentang input pengguna untuk membuat program menggambar sederhana. Berikut adalah cuplikan layar program:

cuplikan layar program gambar

Pengguna dapat menggambar elipsis dalam beberapa warna yang berbeda, dan memilih, memindahkan, atau menghapus elipsis. Agar antarmuka pengguna tetap sederhana, program ini tidak membiarkan pengguna memilih warna elips. Sebagai gantinya, program secara otomatis menelusuri daftar warna yang telah ditentukan sebelumnya. Program ini tak mendukung bentuk apapun selain elipsis. Jelas, program ini tidak akan memenangkan penghargaan 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 isian 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 Menjelajahi Fitur C++ dan MFC Baru di Visual Studio 2010 di Majalah MSDN. (Sumber daya ini mungkin tidak tersedia di beberapa bahasa dan negara.)

 

Program ini memiliki tiga mode:

  • Gambar mode. 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 seperti 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 gambar 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, ubah ukuran elips. Tidak ada tindakan. Pindahkan elips yang dipilih.
Tombol kiri ke atas Berhenti menggambar elips. Tidak ada tindakan. Beralih ke mode pemilihan.

 

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 jendela 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 Device-Independent Pixels.

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.