Bagikan melalui


"Tutorial: Membuat panel kustom

Pelajari cara menulis kode untuk kelas Panel kustom, menerapkan metode ArrangeOverride dan MeasureOverride , dan menggunakan properti Anak .

API Penting: Panel, ArrangeOverride, MeasureOverride

Contoh kode menunjukkan implementasi panel kustom, tetapi kami tidak mencurahkan banyak waktu untuk membahas konsep tata letak yang memengaruhi bagaimana Anda bisa menyesuaikan panel untuk skenario tata letak yang berbeda. Jika Anda ingin informasi selengkapnya tentang konsep tata letak ini dan bagaimana konsep tersebut mungkin berlaku untuk skenario tata letak tertentu Anda, lihat Gambaran umum panel kustom XAML.

Panel adalah objek yang menyediakan perilaku tata letak untuk elemen anak yang dikandungnya, saat sistem tata letak XAML berjalan dan UI aplikasi Anda dirender. Anda dapat menentukan panel kustom untuk tata letak XAML dengan membuat kelas kustom yang diturunkan dari kelas Panel. Anda memberikan perilaku untuk panel Anda dengan mengambil alih metode ArrangeOverride dan MeasureOverride , menyediakan logika yang mengukur dan mengatur elemen anak. Contoh ini berasal dari Panel. Saat Anda memulai dari Panel, metode ArrangeOverride dan MeasureOverride tidak memiliki perilaku awal. Kode Anda menyediakan cara di mana elemen turunan diketahui oleh sistem tata letak XAML dan ditampilkan di UI. Jadi, sangat penting bahwa kode Anda memperhitungkan semua elemen anak dan mengikuti pola yang diharapkan sistem tata letak.

Skenario tata letak Anda

Saat Anda menentukan panel kustom, Anda menentukan skenario tata letak.

Skenario tata letak dinyatakan melalui:

  • Apa yang akan dilakukan panel ketika memiliki elemen turunan
  • Ketika panel memiliki batasan pada ruangnya sendiri
  • Bagaimana logika panel menentukan semua pengukuran, penempatan, posisi, dan ukuran yang akhirnya menghasilkan tata letak UI yang dirender untuk elemen anak

Dengan mengingat hal itu, yang BoxPanel ditunjukkan di sini adalah untuk skenario tertentu. Demi menjaga kode tetap terdepan dalam contoh ini, kami belum akan menjelaskan skenario secara rinci, dan sebaliknya berkonsentrasi pada langkah-langkah yang diperlukan dan pola pengodean. Jika Anda ingin tahu lebih banyak tentang skenario terlebih dahulu, lewati ke "Skenario untuk BoxPanel", lalu kembali ke kode.

Mulailah dengan turunan dari Panel

Mulailah dengan mendapatkan kelas kustom dari Panel. Cara termudah untuk melakukan ini adalah dengan mendefinisikan file kode terpisah untuk kelas ini, menggunakan opsi menu konteks Add | Item Baru | Class untuk proyek dari Solution Explorer di Microsoft Visual Studio. Beri nama kelas (dan file) BoxPanel.

File templat untuk kelas tidak dimulai dengan banyak menggunakan pernyataan karena tidak khusus untuk aplikasi Windows. Jadi pertama-tama, tambahkan menggunakan pernyataan. File template juga dimulai dengan beberapa using statement yang mungkin tidak Anda butuhkan, dan dapat dihapus. Berikut adalah daftar yang disarankan untuk menggunakan pernyataan yang dapat mengatasi jenis yang Anda perlukan untuk kode panel kustom yang khas:

using System;
using System.Collections.Generic; // if you need to cast IEnumerable for iteration, or define your own collection properties
using Windows.Foundation; // Point, Size, and Rect
using Windows.UI.Xaml; // DependencyObject, UIElement, and FrameworkElement
using Windows.UI.Xaml.Controls; // Panel
using Windows.UI.Xaml.Media; // if you need Brushes or other utilities

Sekarang setelah Anda berhasil menyelesaikan Panel, jadikan itu sebagai kelas dasar dari BoxPanel. Selain itu, buat BoxPanel publik:

public class BoxPanel : Panel
{
}

Pada tingkat kelas, tentukan beberapa nilai int dan ganda yang akan dibagikan oleh beberapa fungsi logika Anda, tetapi yang tidak perlu diekspos sebagai API publik. Dalam contoh, ini diberi nama: maxrc, , rowcount, colcount, cellwidthcellheight, , maxcellheightaspectratio.

Setelah Anda melakukan ini, file kode lengkap terlihat seperti ini (menghapus komentar saat menggunakan, sekarang Anda tahu mengapa kami memilikinya):

using System;
using System.Collections.Generic;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

public class BoxPanel : Panel 
{
    int maxrc, rowcount, colcount;
    double cellwidth, cellheight, maxcellheight, aspectratio;
}

Mulai dari sini, kami akan menunjukkan satu definisi anggota pada satu waktu, baik itu override metode atau sesuatu yang mendukung seperti properti dependensi. Anda dapat menambahkannya ke kerangka di atas dalam urutan apa pun.

MeasureOverride

protected override Size MeasureOverride(Size availableSize)
{
    // Determine the square that can contain this number of items.
    maxrc = (int)Math.Ceiling(Math.Sqrt(Children.Count));
    // Get an aspect ratio from availableSize, decides whether to trim row or column.
    aspectratio = availableSize.Width / availableSize.Height;

    // Now trim this square down to a rect, many times an entire row or column can be omitted.
    if (aspectratio > 1)
    {
        rowcount = maxrc;
        colcount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc;
    } 
    else 
    {
        rowcount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc;
        colcount = maxrc;
    }

    // Now that we have a column count, divide available horizontal, that's our cell width.
    cellwidth = (int)Math.Floor(availableSize.Width / colcount);
    // Next get a cell height, same logic of dividing available vertical by rowcount.
    cellheight = Double.IsInfinity(availableSize.Height) ? Double.PositiveInfinity : availableSize.Height / rowcount;
           
    foreach (UIElement child in Children)
    {
        child.Measure(new Size(cellwidth, cellheight));
        maxcellheight = (child.DesiredSize.Height > maxcellheight) ? child.DesiredSize.Height : maxcellheight;
    }
    return LimitUnboundedSize(availableSize);
}

Pola yang diperlukan dari implementasi MeasureOverride adalah loop melalui setiap elemen di Panel.Children. Selalu panggil metode Pengukuran pada setiap elemen ini. Ukuran memiliki parameter jenis Ukuran. Apa yang Anda sampaikan di sini merupakan ukuran yang disediakan oleh panel Anda untuk elemen turunan tertentu. Jadi, sebelum Anda dapat melakukan loop dan mulai memanggil Measure, Anda perlu tahu berapa banyak ruang yang dapat disediakan setiap sel. Dari metode MeasureOverride itu sendiri, Anda memiliki nilai availableSize . Itulah ukuran yang digunakan induk panel ketika memanggil Measure, yang merupakan pemicu untuk MeasureOverride ini dipanggil pada awalnya. Jadi logika yang umum adalah merancang skema di mana setiap elemen turunan membagi ruang dari keseluruhan availableSize panel. Anda kemudian meneruskan setiap pembagian ukuran ke Ukuran setiap elemen anak.

Cara BoxPanel membagi ukuran cukup sederhana: membagi ruangnya menjadi sejumlah kotak yang sebagian besar dikontrol oleh jumlah item. Kotak berukuran berdasarkan jumlah kolom dan baris serta ukuran yang tersedia. Terkadang satu baris atau kolom dari persegi tidak diperlukan, sehingga dihilangkan dan panel menjadi persegi panjang daripada persegi dalam hal barisnya : rasio kolom. Untuk informasi selengkapnya tentang bagaimana logika ini tiba, lewati ke "Skenario untuk BoxPanel".

Jadi apa yang dilakukan oleh pengesahan ketetapan itu? Ini menetapkan nilai untuk properti DesiredSize yang bersifat baca-saja pada setiap elemen di mana Measure dipanggil. Memiliki nilai DesiredSize kemungkinan penting setelah Anda mencapai tahap pengaturan tata letak, karena DesiredSize menunjukkan ukuran yang bisa atau seharusnya saat diatur dan dalam proses perenderan akhir. Bahkan jika Anda tidak menggunakan DesiredSize dalam logika Anda sendiri, sistem masih membutuhkannya.

Dimungkinkan bagi panel ini untuk digunakan ketika komponen tinggi dari availableSize tidak terbatas. Jika itu benar, panel tidak memiliki tinggi yang diketahui untuk melakukan pembagian. Dalam hal ini, logika untuk langkah pengukuran memberi tahu setiap anak bahwa ia belum memiliki tinggi yang terbatas. Ini melakukannya dengan meneruskan Ukuran ke panggilan Pengukuran untuk anak-anak di mana Size.Height tidak terbatas. Itu legal. Ketika Pengukuran dipanggil, logikanya adalah bahwa DesiredSize ditetapkan sebagai minimum dari ini: apa yang diteruskan ke Pengukuran, atau ukuran alami elemen tersebut dari faktor-faktor seperti Tinggi dan Lebar yang diatur secara eksplisit.

Nota

Logika internal StackPanel juga memiliki perilaku ini: StackPanel meneruskan nilai dimensi tak terbatas ke Measure pada elemen anak, menunjukkan bahwa tidak ada batasan pada elemen anak dalam dimensi orientasi. StackPanel biasanya mengukur dirinya secara dinamis, untuk mengakomodasi semua anak dalam tumpukan yang tumbuh dalam dimensi tersebut.

Namun, panel itu sendiri tidak dapat mengembalikan Ukuran dengan nilai tak terbatas dari MeasureOverride; yang akan menyebabkan pengecualian saat tata letak berlangsung. Jadi, bagian dari logika adalah mencari tahu tinggi maksimum yang diminta anak mana pun, dan menggunakan tinggi tersebut sebagai tinggi sel jika tidak berasal dari batasan ukuran panel sendiri. Berikut adalah fungsi LimitUnboundedSize bantu yang dirujuk dalam kode sebelumnya, yang kemudian mengambil tinggi sel maksimum tersebut dan menggunakannya untuk menetapkan panel dengan tinggi yang terbatas, serta memastikan bahwa cellheight adalah angka yang terbatas sebelum proses pengaturan dimulai.

// This method limits the panel height when no limit is imposed by the panel's parent.
// That can happen to height if the panel is close to the root of main app window.
// In this case, base the height of a cell on the max height from desired size
// and base the height of the panel on that number times the #rows.
Size LimitUnboundedSize(Size input)
{
    if (Double.IsInfinity(input.Height))
    {
        input.Height = maxcellheight * colcount;
        cellheight = maxcellheight;
    }
    return input;
}

Tata Atur Sesuaikan

protected override Size ArrangeOverride(Size finalSize)
{
     int count = 1;
     double x, y;
     foreach (UIElement child in Children)
     {
          x = (count - 1) % colcount * cellwidth;
          y = ((int)(count - 1) / colcount) * cellheight;
          Point anchorPoint = new Point(x, y);
          child.Arrange(new Rect(anchorPoint, child.DesiredSize));
          count++;
     }
     return finalSize;
}

Pola yang diperlukan dari implementasi ArrangeOverride adalah loop melalui setiap elemen dalam Panel.Children. Selalu panggil metode Atur pada setiap elemen ini.

Perhatikan bagaimana tidak ada perhitungan sebanyak dalam MeasureOverride; itu khas. Ukuran anak sudah diketahui dari logika MeasureOverride panel itu sendiri, atau dari DesiredSize masing-masing anak yang ditentukan selama tahap pengukuran. Namun, kita masih perlu memutuskan lokasi dalam panel tempat setiap anak akan muncul. Dalam panel yang biasa, setiap anak harus ditampilkan pada posisi yang berbeda. Panel yang membuat elemen tumpang tindih tidak diinginkan dalam skenario umum (meskipun tidak mustahil untuk membuat panel dengan sengaja tumpang tindih, jika memang itu skenario yang Anda maksudkan).

Panel ini diatur berdasarkan konsep baris dan kolom. Jumlah baris dan kolom sudah dihitung (perlu untuk pengukuran). Jadi sekarang bentuk baris dan kolom ditambah ukuran yang diketahui dari setiap sel berkontribusi pada logika menentukan posisi penyajian ( anchorPoint) untuk setiap elemen yang dikandung panel ini. Titik Itu, bersama dengan Ukuran yang sudah diketahui dari ukuran, digunakan sebagai dua komponen yang membuat Rect. Rect adalah jenis input untuk Arrange.

Panel terkadang perlu mengklip kontennya. Jika ya, ukuran yang diklip adalah ukuran yang ada di DesiredSize, karena logika Pengukuran menetapkannya sebagai minimum dari apa yang diteruskan ke Ukuran, atau faktor ukuran alami lainnya. Jadi Anda biasanya tidak perlu secara khusus memeriksa pemotongan selama Arrange; pemotongan hanya terjadi berdasarkan pengoperan DesiredSize ke setiap panggilan Arrange.

Anda tidak selalu memerlukan hitungan saat melalui loop jika semua info yang Anda butuhkan untuk menentukan posisi penyajian diketahui dengan cara lain. Misalnya, dalam logika tata letak Kanvas , posisi dalam koleksi Anak tidak masalah. Semua info yang diperlukan untuk memposisikan setiap elemen dalam Kanvas dapat diketahui dengan membaca nilai Canvas.Left dan Canvas.Top dari anak-anak sebagai bagian dari logika penataan. Logika BoxPanel kebetulan membutuhkan hitungan untuk dibandingkan dengan colcount sehingga diketahui kapan harus memulai baris baru dan mengimbangi nilai y .

Biasanya input finalSize dan Ukuran yang Anda kembalikan dari implementasi ArrangeOverride sama. Untuk informasi selengkapnya tentang alasannya, lihat bagian "ArrangeOverride" dari gambaran umum panel kustom XAML.

Penyempurnaan: mengontrol jumlah baris vs. kolom

Anda dapat mengkompilasi dan menggunakan panel ini seperti sekarang. Namun, kita akan menambahkan satu penyempurnaan lagi. Dalam kode yang baru saja ditampilkan, logika menempatkan baris atau kolom tambahan di sisi yang terpanjang dalam rasio aspek. Tetapi untuk kontrol lebih banyak atas bentuk sel, mungkin diinginkan untuk memilih satu set sel 4x3 alih-alih 3x4 walaupun rasio aspek panel sendiri adalah "potret." Jadi kita akan menambah properti dependensi opsional yang dapat diatur oleh pengguna panel untuk mengontrol perilaku tersebut. Berikut adalah definisi properti dependensi, yang sangat mendasar:

// Property
public Orientation Orientation
{
    get { return (Orientation)GetValue(OrientationProperty); }
    set { SetValue(OrientationProperty, value); }
}

// Dependency Property Registration
public static readonly DependencyProperty OrientationProperty =
        DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(BoxPanel), new PropertyMetadata(null, OnOrientationChanged));

// Changed callback so we invalidate our layout when the property changes.
private static void OnOrientationChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
    if (dependencyObject is BoxPanel panel)
    {
        panel.InvalidateMeasure();
    }
}

Dan di bawah ini adalah bagaimana penggunaan Orientation mempengaruhi logika pengukuran di MeasureOverride. Pada dasarnya yang dilakukan adalah mengubah cara rowcount dan colcount diturunkan dari maxrc dan rasio aspek sesungguhnya, sehingga terdapat perbedaan ukuran untuk setiap sel. Ketika OrientationVertikal (default), nilai rasio aspek sebenarnya dibalik sebelum menggunakannya untuk menghitung jumlah baris dan kolom dalam tata letak persegi panjang "potret" kami.

// Get an aspect ratio from availableSize, decides whether to trim row or column.
aspectratio = availableSize.Width / availableSize.Height;

// Transpose aspect ratio based on Orientation property.
if (Orientation == Orientation.Vertical) { aspectratio = 1 / aspectratio; }

Skenario untuk BoxPanel

Skenario khusus untuk BoxPanel adalah bahwa itu adalah panel di mana salah satu penentu utama tentang cara membagi ruang adalah dengan mengetahui jumlah item anak, dan membagi ruang yang diketahui tersedia untuk panel. Panel secara alami berbentuk persegi panjang. Banyak panel beroperasi dengan membagi ruang persegi panjang itu menjadi persegi panjang lebih lanjut; Itulah yang dilakukan Grid untuk sel-selnya. Dalam kasus Grid, ukuran sel diatur oleh nilai ColumnDefinition dan RowDefinition, dan elemen menetapkan sel yang tepat yang mereka masuki dengan properti terlampir Grid.Row dan Grid.Column. Mendapatkan tata letak yang baik dari Grid biasanya memerlukan mengetahui jumlah elemen anak sebelumnya, sehingga tersedia cukup sel dan setiap elemen anak menyesuaikan properti terlampirnya sehingga cocok ke dalam selnya masing-masing.

Tetapi bagaimana jika jumlah anak bersifat dinamis? Itu tentu saja mungkin; kode aplikasi Anda dapat menambahkan item ke koleksi, sebagai respons terhadap kondisi run-time dinamis apa pun yang Anda anggap cukup penting untuk layak memperbarui UI Anda. Jika Anda menggunakan pengikatan data ke koleksi pendukung/objek bisnis, mendapatkan pembaruan tersebut dan memperbarui UI ditangani secara otomatis, sehingga seringkali merupakan teknik yang lebih disukai (lihat Pengikatan data secara mendalam).

Namun, tidak semua skenario aplikasi cocok untuk pengikatan data. Terkadang, Anda perlu membuat elemen UI baru saat runtime dan membuatnya terlihat. BoxPanel adalah untuk skenario ini. Perubahan jumlah item anak tidak menjadi masalah BoxPanel karena menggunakan jumlah anak dalam perhitungan, dan menyesuaikan elemen anak yang ada dan baru ke dalam tata letak baru sehingga semuanya pas.

Skenario lanjutan untuk memperluas BoxPanel lebih jauh (tidak ditampilkan di sini) dapat mengakomodasi anak-anak dinamis dan menggunakan DesiredSize anak sebagai faktor yang lebih kuat untuk ukuran sel individu. Skenario ini mungkin menggunakan berbagai ukuran baris atau kolom atau bentuk non-kisi sehingga ada lebih sedikit ruang "terbuang". Ini membutuhkan strategi tentang bagaimana beberapa persegi panjang dari berbagai ukuran dan rasio aspek semuanya dapat masuk ke dalam persegi panjang yang menampung, baik untuk estetika maupun ukuran yang paling kecil. BoxPanel tidak melakukan itu; menggunakan teknik yang lebih sederhana untuk membagi ruang. BoxPanelTekniknya adalah menentukan jumlah kuadrat paling sedikit yang lebih besar dari jumlah anak. Misalnya, 9 item akan pas dalam persegi 3x3. 10 barang membutuhkan sebuah persegi 4x4. Namun, Anda sering dapat mencocokkan item sambil tetap menghapus satu baris atau kolom persegi awal, untuk menghemat ruang. Dalam contoh count=10, yang sesuai dengan persegi panjang berukuran 4x3 atau 3x4.

Anda mungkin bertanya-tanya mengapa panel tidak memilih 5x2 untuk 10 item, karena itu cocok dengan jumlah item dengan rapi. Namun, dalam praktiknya, panel biasanya berukuran persegi panjang yang jarang memiliki rasio aspek yang amat jelas terarah. Metode kuadrat terkecil adalah cara untuk mengadaptasi logika pengukuran agar berfungsi dengan baik dengan bentuk tata letak yang biasa dan tidak mendorong ukuran yang menimbulkan rasio aspek yang tidak biasa.

Referensi

Konsep