Listbox "tipo web" al pasar el ratón sobre los ítems. Visual C#

Tamaño de letra:

Presentación visual del listbox v1.0.2

Listbox tipo WebHace unos días, programando una aplicación propia,  me encontré con un sencillo problema: tenía un cuadro de texto multilínea que el usuario podía modificar (imagina un blog de notas), sin embargo, necesitaba que el usuario tuviese unas opciones para no escribir siempre frases repetidas. Por ejemplo, el usuario utilizaba muy a menudo la frase "hoy es un buen día para ser feliz, te lo mereces" y en el cuadro de texto la tenía que escribir unas 10 veces. Para evitar escribir el texto tantas veces, se me ocurrieron varias cosas, pero finalmente decidí mostrarle un listbox a la derecha del cuadro de texto y cada vez que seleccionaba una opción, el texto se escribía automáticamente. Funciona perfecto.

Sin embargo, la presentación visual por defecto del listbox deja bastante que desear:

  • No se resaltan los ítems cuando pasas el ratón sobre ellos: esto es bastante importante para no equivocarte porque la selección la haces a ojo. Lo ideal, por defecto, sería que fuese como los ítems de un Combobox.
  • Si los ítems sobrepasan la altura del listbox, este muestra la barra de desplazamiento vertical.

El uso de un menú lo descarté porque, si tienes muchas opciones, hay que añadir un nuevo submenú y se pierde un tiempo importante en: pulsar botón derecho para el menú emergente » selección del submenú » selección del ítem correcto. Un listbox que muestre todas las opciones es muchísimo más rápido.

Mostrando el ejemplo de hoy

Para que veas cuál es el objetivo final de hoy, te voy a mostrar un corto vídeo que he realizado con el código de más abajo y algunas configuraciones para mostrar o no las líneas de separación de ítems. Como puedes ver, el resultado visual está bastante conseguido:

Vista previa:

Listbox tipo web

Explicación del código

Altura del listbox, borde.

Para que el listbox 1 no muestre la barra de desplazamiento vertical, hago que la altura se ajuste al número de ítems.

listBox1.ItemHeight = 20;
listBox1.Height = listBox1.Items.Count * listBox1.ItemHeight;

pero esto solo ocurre cuando el borde del listbox está puesto en: none:

listBox1.BorderStyle = BorderStyle.None;

por este motivo, se me ha ocurrido usar un control panel para dibujar el borde del listbox 2, al que ajusto su tamaño y posición:

panel1.Location = new Point (listBox1.Location.X - 1, listBox1.Location.Y - 1);
...
panel1.Size = new Size (listBox1.Size.Width + 2, listBox1.Size.Height + 2);

Selección del ítem, dibujar listbox

El método listBox1_DrawItem es el encargado de dibujar el listbox (y el ítem seleccionado 3) pero para que esto ocurra debemos dibujarlo "a mano" y que no sea de forma automática:

listBox1.DrawMode = DrawMode.OwnerDrawFixed;

Resaltar cuando el ratón pasa sobre un ítem

Todo el código lo hice pensando principalmente en esta opción 4. Muchos códigos que he visto en Internet hace uso de:

listBox1.Invalidate();

que hace repintar de nuevo todo el listbox. Esto tiene un gran problema y es que produce parpadeos y el resultado final deja mucho que desear. Como no vi ningún código interesante, me propuse hacer uno desde cero y para eso utilicé el método listBox1_MouseMove. Para evitar que el código se redibuje siempre que movemos el ratón y solo lo haga cuando cambiamos de ítem, he creado 2 variables Entero:

private int indexViejo = 0;
private int indexNuevo = -1;

que es el índice del ítem donde está actualmente el cursor (indexNuevo) y de donde viene (indexViejo).

El ítem en el que se encuentra el cursor se puede conocer de esta forma:

Point point = listBox1.PointToClient(Cursor.Position);
int index = listBox1.IndexFromPoint(point);

y el código restante es muy sencillo de entender. El resaltado del ítem 4, lo hago con un degradado de 2 colores amarillos:

Brush brush = new LinearGradientBrush(rectNuevo, Color.FromArgb(255, 231, 157), Color.FromArgb(255, 193, 6), 90f);

y el borde redondeado del ítem donde está el cursor, lo hago con un código que no es mío haciendo uso de la clase: ExtendedPrimitives. Dicho código lo ví en el siguiente enlace: Extended Graphics - An implementation of Rounded Rectangle in C#. Sin embargo, el código no es el que muestra el ejemplo (que es muy complejo), sino que está en la primera respuesta de los comentarios: gracias a kzerza.

Como se muestra en el vídeo, he dejado 2 DrawRectangle comentados que sirven para dibujar los rectángulos de cada ítem. Si los usas, deberías eliminar el panel para que el borde exterior no se duplique. También, cuando el listbox pierde el foco, repinto el control de este modo:

listBox1.Invalidate();

Código final

En Visual C#, crea un nuevo proyecto de Windows Form, añade un listbox: listBox1 y un panel: panel1. Pega el siguiente código:

 
// Versión 1.0.2
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
 
namespace listbox_web
{
    public partial class Form1 : Form
    {
        private int indexViejo = 0;
        private int indexNuevo = -1;
        private int contador = 0;
 
        public Form1()
        {
            InitializeComponent();
        }
 
        private void Form1_Load(object sender, EventArgs e)
        {
            // Listbox:
            panel1.Location = new Point (listBox1.Location.X - 1, listBox1.Location.Y - 1);
            listBox1.DrawMode = DrawMode.OwnerDrawFixed;
            listBox1.ItemHeight = 20;
            listBox1.BorderStyle = BorderStyle.None;
            listBox1.Cursor = Cursors.Hand;
 
 
            string[] texto = new string[8] {"Primera", "Segunda", "Tercera",
                                              "Cuarta", "Quinta", "Sexta",
                                              "Séptima", "Optava"};
 
            for (int i = 0; i < texto.Count(); i++)
            {
                texto[i] += " línea.";
            }
 
            listBox1.Items.AddRange(texto);
            listBox1.Items.Add("karmany.NET");
            listBox1.Items.Add("Última línea");
            listBox1.Height = listBox1.Items.Count * listBox1.ItemHeight;
            panel1.Size = new Size (listBox1.Size.Width + 2, listBox1.Size.Height + 2);
        }
 
        private void listBox1_DrawItem(object sender, DrawItemEventArgs e)
        {            
            if (contador != -1) contador++;
 
            if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
            {
                e.Graphics.FillRectangle(Brushes.CornflowerBlue, e.Bounds);
            }
            else
            {
                e.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(218, 224, 231)), e.Bounds);
            }
 
            //e.Graphics.DrawRectangle(Pens.Brown, new Rectangle(e.Bounds.X, e.Bounds.Y,
            //                                                   e.Bounds.Width - 1, e.Bounds.Height));
            e.Graphics.DrawString(listBox1.Items[e.Index].ToString(),
                                  this.Font, Brushes.Black, e.Bounds.X, e.Bounds.Y + 3);
 
 
            // Si el control tiene el foco, dibuja un rectángulo:
            e.DrawFocusRectangle();
 
            // Resalta el ítem si el listbox aparece sobre el cursor:
            if (contador >= listBox1.Items.Count)
            {
                contador = -1;
                System.Drawing.Graphics g = listBox1.CreateGraphics();
                Point point = listBox1.PointToClient(Cursor.Position);
                int index = listBox1.IndexFromPoint(point);
 
                if (listBox1.SelectedIndex != index)
                {
                Rectangle rectNuevo = new Rectangle(listBox1.GetItemRectangle(index).Location.X,
                                                        listBox1.GetItemRectangle(index).Location.Y,
                                                        listBox1.GetItemRectangle(index).Width,
                                                        listBox1.GetItemRectangle(index).Height);
                // Dibuja el rectángulo nuevo:
                Brush brush = new LinearGradientBrush(rectNuevo,
                                       Color.FromArgb(255, 231, 157),
                                       Color.FromArgb(255, 193, 6),
                                       90f);
 
                g.FillRectangle(brush, rectNuevo);
                g.DrawString(listBox1.Items[index].ToString(),
                             this.Font, Brushes.Black,
                             listBox1.GetItemRectangle(index).Location.X,
                             listBox1.GetItemRectangle(index).Location.Y + 3);
 
                // Dibuja el contorno redondeado
                GraphicsPath rrect = ExtendedPrimitives.RoundRect(rectNuevo.X,
                                                                  rectNuevo.Y,
                                                                  rectNuevo.Width,
                                                                  rectNuevo.Height,
                                                                  3, 1);
                g.DrawPath(new Pen(Color.Brown, 1), rrect);
                }
            }
 
        }
 
        private void listBox1_MouseMove(object sender, MouseEventArgs e)
        {
            System.Drawing.Graphics g = listBox1.CreateGraphics();
 
            Point point = listBox1.PointToClient(Cursor.Position);
            int index = listBox1.IndexFromPoint(point);
            if (index < 0) return;
 
            if (index != indexNuevo)
            {
                indexViejo = indexNuevo;
                indexNuevo = index;
                Rectangle rectNuevo = new Rectangle(listBox1.GetItemRectangle(indexNuevo).Location.X,
                                                    listBox1.GetItemRectangle(indexNuevo).Location.Y,
                                                    listBox1.GetItemRectangle(indexNuevo).Width,
                                                    listBox1.GetItemRectangle(indexNuevo).Height);
 
 
                if (indexViejo != -1 && listBox1.SelectedIndex != indexViejo)
                {
                    // Dibuja de nuevo el rectángulo viejo:
                    Rectangle rectViejo = new Rectangle(listBox1.GetItemRectangle(indexViejo).Location.X,
                                    listBox1.GetItemRectangle(indexViejo).Location.Y,
                                    listBox1.GetItemRectangle(indexViejo).Width,
                                    listBox1.GetItemRectangle(indexViejo).Height);
 
                    g.FillRectangle(new SolidBrush(Color.FromArgb(218, 224, 231)), rectViejo);
                    //g.DrawRectangle(Pens.Brown, new Rectangle(listBox1.GetItemRectangle(indexViejo).Location.X,
                    //                                          listBox1.GetItemRectangle(indexViejo).Location.Y,
                    //                                          listBox1.GetItemRectangle(indexViejo).Width-1,
                    //                                          listBox1.GetItemRectangle(indexViejo).Height));
                    g.DrawString(listBox1.Items[indexViejo].ToString(),
                                    this.Font, Brushes.Black,
                                    listBox1.GetItemRectangle(indexViejo).Location.X,
                                    listBox1.GetItemRectangle(indexViejo).Location.Y + 3);
                }
 
 
                if (listBox1.SelectedIndex != index)
                {
                    // Dibuja el rectángulo nuevo:
                    Brush brush = new LinearGradientBrush(rectNuevo,
                                           Color.FromArgb(255, 231, 157),
                                           Color.FromArgb(255, 193, 6),
                                           90f);
 
                    g.FillRectangle(brush, rectNuevo);
                    g.DrawString(listBox1.Items[index].ToString(),
                                 this.Font, Brushes.Black,
                                 listBox1.GetItemRectangle(index).Location.X,
                                 listBox1.GetItemRectangle(index).Location.Y + 3);
 
                    // Dibuja el contorno redondeado
                    GraphicsPath rrect = ExtendedPrimitives.RoundRect(rectNuevo.X,
                                                                      rectNuevo.Y,
                                                                      rectNuevo.Width,
                                                                      rectNuevo.Height,
                                                                      3, 1);
                    g.DrawPath(new Pen(Color.Brown, 1), rrect);
                }
 
            }
        }
 
        private void listBox1_MouseLeave(object sender, EventArgs e)
        {
            indexNuevo = -1;
            listBox1.Invalidate();
        }
    }
    // El autor del siguiente código es: kzerza (primer comentario)
    // http://www.codeproject.com/Articles/5649/Extended-Graphics-An-implementation-of-Rounded-Rec
    // Permite crear un rectángulo redondeado.
 
    public class ExtendedPrimitives
    {
 
        public static GraphicsPath RoundRect(int x, int y,
                                             int width, int height,
                                             int radius, int lw)
        // x,y - top left corner of rounded rectangle
        // width, height - width and height of round rect
        // radius - radius for corners
        // lw - line width (for Graphics.Pen)
        {
            GraphicsPath g = new GraphicsPath();
            int diameter = radius * 2;
            g.AddArc(x + lw, y, diameter, diameter, 180, 90);
            g.AddArc(x + (width - diameter - lw), y, diameter, diameter, 270, 90);
            g.AddArc(x + (width - diameter - lw), y + (height - diameter - lw),
            diameter, diameter, 360, 90);
            g.AddArc(x + lw, y + (height - diameter - lw), diameter, diameter, 90, 90);
            g.CloseFigure();
            return g;
 
        }
    }
}
 
 

Versiones

Versión 1.0.1 (01 de septiembre de 2013)

Primera versión del código

Versión 1.0.2 (02 de septiembre de 2013)

Cuando el listbox aparecía sobre el cursor del ratón (por ejemplo al hacerlo visible), no era resaltado ningún ítem. Este problema era producido porque no se ejecuta un evento mousemove después de dibujar los ítems (Drawtem). Para solucionar esto, he creado un contador y en DrawItem dibuja el resaltado tras el último ítem.

Última actualización: Martes, 24 Marzo 2015
Comentarios  
0 # Marenostrum 28-09-2021 19:19
Hola
Estaría interesado en este código, pero cuando sigo las instrucciones
“Código final
En Visual C#, crea un nuevo proyecto de Windows Form, añade un listbox: listBox1 y un panel: panel1. Pega el siguiente código:”

No hace nada y no muestra el listbox , uso Visual Studio 2017
Me podrías ayudar
Gracias
Responder | Responder con una citación | Citar
0 # Editor 28-09-2021 19:32
Hola Marenostrum...
Hace mucho tiempo que no uso ese código. Voy a hacer una cosa, a ver si puedo esta noche compilarlo y probarlo y te responderé.
Un saludo
Responder | Responder con una citación | Citar
Escribir un comentario
Antes de publicar un comentario, usted debe aceptar nuestras condiciones de uso: Condiciones de uso. Debido al spam, todos los comentarios serán moderados. Normalmente se responde en unos minutos, refresca los comentarios para comprobarlo.



 
Visitas: 8524261