jueves, 21 de mayo de 2015

Windows Forms. TextBox con sugerencias (I) - Creando el control

El control TextBox incluido con en el Framework de .NET incorpora la opción de mostrar sugerencias mientras se escribe estableciendo la propiedad AutoCompleteMode al valor Suggest. Sin embargo la posibilidad de controlar los textos sugeridos es muy limitada, se limita a mostrar los textos que comienzan con el texto introducido por el usuario en el TextBox a partir de una colección de cadenas.

Sin embargo, ¿qué pasa si el criterio que queremos utilizar para seleccionar los textos a sugerir es otro? Por ejemplo si queremos implementar un control con sugerencias al estilo de las búsquedas de Google.


En este artículo, y los siguientes, voy a mostrar cómo podemos crear un control TextBox que nos permita elegir el algoritmo a utilizar para mostrar los textos sugeridos.

En este artículo crearé un control que reproduzca el mecanismo de sugerencias de un control TextBox estándar, y en los siguientes mostraré cómo podemos dar al desarrollador la posibilidad de personalizar el algoritmo para seleccionar los textos sugeridos.

Para empezar voy a crear un nuevo proyecto del tipo Biblioteca de Clases llamado SuggestionTextBox.

Crear proyecto SuggestionTextBox

A continuación le he añadido referencias a los ensamblados System.Drawing y System.Windows.Forms que necesitaré en el proyecto.

Agregar referencias al proyecto

Para introducir el código del control he eliminado la clase Class1 que Visual Studio crea por defecto y he añadido una nueva clase TextSuggestion que hereda de TextBox.

Lo primero que haremos será crear dos propiedades para configurar las sugerencias:

  • MaxNumOfSuggestions: que determinará el número máximo de resultados a mostrar
  • SuggestDataSource: que establecerá la colección de cadenas que utilizará el control para las sugerencias

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace SuggestionTextBox
{
    public class TextSuggestion: TextBox
    {

        #region Propiedades públicas

        [DefaultValue(10)]
        public int MaxNumOfSuggestions { get; set; }

        public IEnumerable<string> SuggestDataSource { get; set; }

        #endregion
    }
}



Para mostrar los textos sugeridos voy a utilizar un control ListBox. Así que voy a crear dos variables privadas, una para contener el control ListBox y otra que indicará si el control ya ha sido inicializado.
Además crearé 3 métodos para controlar el funcionamiento del ListBox:

  • ShowListBox: que se encargará de inicializar el control ListBox si todavía no lo ha sido y lo muestra.
  • HideListBox: que se encargará de ocultar el control ListBox.
  • UpdateListBox: que actualizará el contenido del ListBox a partir del texto introducido en el TextBox, seleccionando las cadenas de SuggestDataSource que comiencen con el texto introducido.
Por último voy a crear un controlador para el evento Click del ListBox que se encargará de establecer el texto del TextBox con el contenido del elemento seleccionado y ocultará el control ListBox.

El constructor del control se encargará de inicializar la propiedad MaxNumOfSuggestions a su valor por defecto, iniciarlizar la variable _listBox y de asociar el controlador del evento Click al control ListBox.

        #region Variables privadas

        private ListBox _listBox;
        private bool _listBoxAddedToForm = false;

        #endregion

        #region Constructor

        public TextSuggestion(): base() 
        {
            MaxNumOfSuggestions = 10;
            _listBox = new ListBox();
            _listBox.Click += listBox_Click;
        }

        #endregion

        #region ListBox

        private void ShowListBox()
        {
            if (!_listBoxAddedToForm)
            {
                this.TopLevelControl.Controls.Add(_listBox);
                Point controlLocation = this.TopLevelControl.PointToClient(this.Parent.PointToScreen(this.Location));
                _listBox.Left = controlLocation.X;
                _listBox.Top = controlLocation.Y + this.Height;
                _listBox.Font = this.Font;
                _listBox.Width = this.Width;
                _listBox.MinimumSize = new Size(this.Width, _listBox.MinimumSize.Height);
                _listBox.Height = _listBox.ItemHeight * (MaxNumOfSuggestions + 1);
                _listBoxAddedToForm = true;
            }
            _listBox.Visible = true;
            _listBox.BringToFront();
        }

        private void HideListBox() { _listBox.Visible = false; }

        private void UpdateListBox() 
        {
            if (SuggestDataSource != null && !string.IsNullOrEmpty(this.Text))
            {
                IEnumerable<string> result = SuggestDataSource
                    .Where(s => s.StartsWith(this.Text, StringComparison.OrdinalIgnoreCase)
                        && !s.Equals(this.Text, StringComparison.OrdinalIgnoreCase))
                    .OrderBy(s => s)
                    .Take(MaxNumOfSuggestions);
                if (result.Count() > 0)
                {
                    _listBox.DataSource = result.ToList();
                    ShowListBox();
                }
                else
                    HideListBox();
            }
            else
                HideListBox();
        }

        private void listBox_Click(object sender, EventArgs e)
        {
            if (_listBox.SelectedIndex >= 0)
                Text = _listBox.SelectedItem.ToString();
            HideListBox();
        }

        #endregion

La parte más interesante de este código se encuentra en la inicialización del control ListBox. Lo que hago es añadir el ListBox al control especificado por la propiedad TopLevelControl (generalmente el formulario), de esta forma al mostrarse el ListBox no se verá limitado por el control contenedor del TextBox. A continuación establezco la posición del ListBox en la esquina inferior izquierda del TextBox y su ancho igual al ancho del TextBox. Cuando se muestra el ListBox llamo al método BringToFront para que se muestre por encima del resto de controles.

A continuación voy a añadir dos controladores para los eventos de teclado:

  • En el evento KeyDown controlo la pulsación de las teclas "Arriba" y "Abajo" para modificar el elemento seleccionado en el ListBox. También controla la pulsación de la tecla "Enter" para que, si existe un elemento del ListBox seleccionado lo establezca como texto del control.
  • En el evento KeyUp control si se ha modificado el texto y, si es así, actualizo los textos sugeridos a través del método UpdateListBox.

        #region Entrada de teclado

        private void this_KeyDown(object sender, KeyEventArgs e)
        {
            switch (e.KeyCode)
            {
                case Keys.Down:
                    {
                        if ((_listBox.Visible) && (_listBox.SelectedIndex < _listBox.Items.Count - 1))
                            _listBox.SelectedIndex++;
                        e.SuppressKeyPress = true;
                        break;
                    }
                case Keys.Up:
                    {
                        if (_listBox.Visible && _listBox.SelectedIndex >= 0)
                            _listBox.SelectedIndex--;
                        e.SuppressKeyPress = true;
                        break;
                    }
                case Keys.Enter:
                    {
                        if (_listBox.Visible)
                        {
                            if (_listBox.SelectedIndex >= 0)
                            {
                                Text = _listBox.SelectedItem.ToString();
                                SelectAll();
                            }
                            HideListBox();
                            e.SuppressKeyPress = true;
                        }
                        break;
                    }
            }
        }

        string _lastText;
        private void this_KeyUp(object sender, KeyEventArgs e)
        {
            if (this.Text != _lastText)
            {
                UpdateListBox();
                _lastText = this.Text;
            }
        }

        #endregion

Por último voy a controlar el evento LostFocus para que no se dispare cada vez que el foco pase al control ListBox, y para ocultar el ListBox cuando efectivamente se produzca. También modificaré el constructor para asociar los controladores de eventos recién creados.

        #region Constructor

        public TextSuggestion(): base() 
        {
            MaxNumOfSuggestions = 10;
            _listBox = new ListBox();
            KeyDown += this_KeyDown;
            KeyUp += this_KeyUp;
            LostFocus += this_LostFocus;
            _listBox.Click += listBox_Click;
        }

        #endregion
        
        #region LostFocus

        protected override void OnLostFocus(EventArgs e)
        {
            if (!_listBox.ContainsFocus)
                base.OnLostFocus(e);
        }

        private void this_LostFocus(object sender, EventArgs e)
        {
            HideListBox();
        }

        #endregion        

Ya tenemos la funcionalidad básica del control. En las próximas entradas crearemos un proyecto sobre el que poder probar el control y le añadiremos la posibilidad de poder personalizar el método de selección de sugerencias:

El código completo de este artículo y los dos siguientes, tanto en C# como en Visual Basic, está disponible en:

TextBox con sugerencias tipo Google. Ejemplos MSDN.

2 comentarios:

  1. Asier, muchas gracias por tu aporte.

    Tengo una pregunta: ¿cómo puedo extraer el id del registro seleccionado?

    Juan Carlos

    ResponderEliminar
    Respuestas
    1. Me imagino que ya paso mucho tiempo, pero en cuanto lo solucione yo, que tambien estoy trabajando en eso, publicare la respuesta

      Eliminar