lunes, 18 de mayo de 2015

Windows Forms. Personalizar la apariencia de los items de un ListBox

En este artículo voy a mostrar cómo se puede personalizar la apariencia individual de cada elemento de un control ListBox.

El objetivo final es conseguir un ListBox con esta apariencia.

Listbox con elementos personalizados




Para construir el ejemplo he creado un nuevo proyecto usando la plantilla de Aplicación de Windows Forms y he añadido un control ListBox al formulario Form1 que Visual Studio crea por defecto.

Para cargar los datos en el ListBox he creado una nueva clase Item que definirá cada uno de los elementos a añadir al ListBox con dos propiedades: Text que establecerá el texto a mostrar y Category que nos permitirá identificar el tipo de elemento y darle un formato diferente según su valor.

En el evento Load del formulario creo una lista de elementos Item y se lo asigno a la propiedad DataSource del ListBox.

        class Item
        {
            public Item(string itemText, string itemType)
            {
                Text = itemText;
                Category = itemType;
            }

            public string Text { get; set; }
            public string Category { get; set; }
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            List<Item> ListaItems = new List<Item>();
            ListaItems.Add(new Item("Píldoras .NET", "Título"));
            ListaItems.Add(new Item("Asier Villanueva", "Autor"));
            ListaItems.Add(new Item("ASP.NET", "Sección"));
            ListaItems.Add(new Item("MVC. Personalizar el Motor de Vistas de MVC", "Artículo"));
            ListaItems.Add(new Item("MVC. Obtener el código generado por una vista Razor cshtml/vbhtml", "Artículo"));
            ListaItems.Add(new Item("MVC. Plantilla de editor para fecha y hora", "Artículo"));
            ListaItems.Add(new Item("Localizando Data Annotations", "Artículo"));
            ListaItems.Add(new Item("Windows Forms", "Sección"));
            ListaItems.Add(new Item("DataGridView. Columna con editor de fechas.", "Artículo"));
            ListaItems.Add(new Item("DataGridView. Columna que acepta sólo números.", "Artículo"));
            ListaItems.Add(new Item("Control TextBox con Botón", "Artículo"));
            ListaItems.Add(new Item("MDI Child sin ControlBox", "Artículo"));

            ListBox1.DataSource = ListaItems;
            ListBox1.DisplayMember = "Text";
            ListBox1.ValueMember = "Text";
        }
    Class Item
        Public Sub New(itemText As String, itemType As String)
            Text = itemText
            Category = itemType
        End Sub

        Public Property Text As String
        Public Property Category As String
    End Class

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Dim ListaItems = New List(Of Item)
        ListaItems.Add(New Item("Píldoras .NET", "Título"))
        ListaItems.Add(New Item("Asier Villanueva", "Autor"))
        ListaItems.Add(New Item("ASP.NET", "Sección"))
        ListaItems.Add(New Item("MVC. Personalizar el Motor de Vistas de MVC", "Artículo"))
        ListaItems.Add(New Item("MVC. Obtener el código generado por una vista Razor cshtml/vbhtml", "Artículo"))
        ListaItems.Add(New Item("MVC. Plantilla de editor para fecha y hora", "Artículo"))
        ListaItems.Add(New Item("Localizando Data Annotations", "Artículo"))
        ListaItems.Add(New Item("Windows Forms", "Sección"))
        ListaItems.Add(New Item("DataGridView. Columna con editor de fechas.", "Artículo"))
        ListaItems.Add(New Item("DataGridView. Columna que acepta sólo números.", "Artículo"))
        ListaItems.Add(New Item("Control TextBox con Botón", "Artículo"))
        ListaItems.Add(New Item("MDI Child sin ControlBox", "Artículo"))

        ListBox1.DataSource = ListaItems
        ListBox1.DisplayMember = "Text"
        ListBox1.ValueMember = "Text"
    End Sub

Por supuesto que si arrancamos la aplicación en este punto se añadirán los elementos al ListBox pero la apariencia está muy lejos de la que deseamos:

Aspecto estándar del ListBox

Para poder personalizar la apariencia de cada elemento debemos cambiar el valor de la propiedad DrawMode del ListBox. Podemos asignar a la propiedad cualquiera de los valores de la enumeración DrawMode:

  • DrawMode.Normal: Todos los elementos se dibujan con la misma apariencia. Es el valor por defecto.
  • DrawMode.OwnerDrawFixed: Los elementos se dibujan manualmente, pudiendo personalizarse de manera independiente, pero todos tienen el mismo tamaño.
  • DrawMode.OwnerDrawVariable: Los elementos se dibujan manualmente y pueden ser de diferente tamaño.
He establecido el valor de la propiedad DrawMode como OwnerDrawVariable para mostrar cómo podemos personalizar no sólo la apariencia si no también el tamaño de cada elemento.

El dibujado de cada elemento lo realizaremos capturando el evento DrawItem del ListBox. Así que he creado un controlador para el evento DrawItem en el que dibujo el elemento con diferente estilo, tamaño y color dependiendo del valor de la propiedad Category del elemento a dibujar.

El evento DrawItem recibe un parámetro DrawItemEventArgs que tiene propiedades desde las que podemos obtener el formato de fuente y colores con los que se dibujaría el elemento por defecto.

Además proporciona otras dos propiedades que nos van a resultar útiles: la propiedad Index que devuelve el índice del elemento del ListBox a dibujar y la propiedad Graphics que proporciona un objeto Graphics que se utilizará para dibujar el elemento. Contiene además dos métodos que nos permiten dibujar el fondo del elemento (DrawBackground) y el rectángulo que indica que un elemento tiene el foco (DrawFocusRectangle).

        private void ListBox1_DrawItem(object sender, DrawItemEventArgs e)
        {
            Item currentItem = (Item)ListBox1.Items[e.Index];
            Font customFont = e.Font;
            Brush customBrush = new SolidBrush(e.ForeColor);
            string displayText = currentItem.Text;

            switch (currentItem.Category)
            {
                case "Título":
                    customFont = new Font("Arial", 14, FontStyle.Bold);
                    customBrush = Brushes.Orange;
                    break;
                case "Autor":
                    customFont = new Font("Arial", 7);
                    break;
                case "Sección":
                    customFont = new Font(e.Font.FontFamily, 10, FontStyle.Bold);
                    break;
                default:
                    displayText = "     " + displayText;
                    break;
            }

            e.DrawBackground();
            e.Graphics.DrawString(displayText,
                customFont, customBrush, e.Bounds, StringFormat.GenericDefault);
            e.DrawFocusRectangle();
        }
    Private Sub ListBox1_DrawItem(sender As Object, e As DrawItemEventArgs) Handles ListBox1.DrawItem

        Dim currentItem As Item = ListBox1.Items(e.Index)
        Dim customFont As Font = e.Font
        Dim customBrush As Brush = New SolidBrush(e.ForeColor)
        Dim displayText As String = currentItem.Text

        Select Case currentItem.Category
            Case "Título"
                customFont = New Font("Arial", 14, FontStyle.Bold)
                customBrush = Brushes.Orange
            Case "Autor"
                customFont = New Font("Arial", 7)
            Case "Sección"
                customFont = New Font(e.Font.FontFamily, 10, FontStyle.Bold)
            Case Else
                displayText = "     " + displayText
        End Select

        e.DrawBackground()
        e.Graphics.DrawString(displayText, _
            customFont, customBrush, e.Bounds, StringFormat.GenericDefault)
        e.DrawFocusRectangle()
    End Sub



Si ahora arrancamos la aplicación podremos ver el efecto conseguido:

ListBox con items con el texto formateado

Le hemos dado el formato al texto de los items, pero el tamaño de cada uno de ellos se mantiene con el tamaño estándar por lo que se nos montan unos textos encima de otros.

Para poder indicar el tamaño de cada elemento individual del ListBox deberemos crear un controlador para el evento MeasureItem del ListBox.

El controlador del evento MeasureItem recibe un parámetro del tipo MeasureItemEventArgs. Este objeto nos proporciona, además de las propiedades Index y Graphics ya mencionadas, dos propiedades para establecer el alto y el ancho del elemento: ItemHeight e ItemWidth.

        private void ListBox1_MeasureItem(object sender, MeasureItemEventArgs e)
        {
            Item currentItem = (Item)ListBox1.Items[e.Index];
            switch (currentItem.Category)
            {
                case "Título":
                    e.ItemHeight = 22;
                    break;
                case "Sección":
                    e.ItemHeight = 18;
                    break;
            }
        }
    Private Sub ListBox1_MeasureItem(sender As Object, e As MeasureItemEventArgs) Handles ListBox1.MeasureItem
        Dim currentItem As Item = ListBox1.Items(e.Index)
        Select Case currentItem.Category
            Case "Título"
                e.ItemHeight = 22
            Case "Sección"
                e.ItemHeight = 18
        End Select
    End Sub

Le he aumentado el tamaño a los elementos de las categorías "Título" y "Sección", manteniendo el resto con el tamaño estándar. Si arrancamos ahora la aplicación veremos que hemos conseguido el efecto pretendido:


Resultado final del ListBox personalizado

No hay comentarios:

Publicar un comentario