sábado, 4 de abril de 2015

Windows Forms. Control TextBox con Botón

Campos de búsquedasUn interfaz que nos encontramos con mucha frecuencia es el cuadro de texto con un botón asociado. Se utiliza principalmente en pantallas de búsquedas, pero también nos lo podemos encontrar para cubrir otras necesidades: acceso a interfaces de entrada de datos (calendario, calculadora, ...), a información de ayuda, etc.

Por desgracia el framework de .NET no incluye un control con esta funcionalidad. Podemos suplir esta carencia creándonos un control de usuario con un TextBox y un Button, pero vamos a ver cómo podemos modificar el TextBox para añadirle esta funcionalidad, creando un nuevo control que herede del control TextBox.



Definiendo la funcionalidad del control


Vamos a crear un nuevo control TextBoxButton que se visualizará como un control TextBox normal con un botón situado en la parte derecha del control.

El nuevo control tendrá todas las propiedades, métodos y eventos del control TextBox estándar y además:
  • Una propiedad ButtonImage que permita indicar la imagen a mostrar en el botón.
  • Un evento ButtonClick que se producirá cuando se realice click sobre el botón.

Creando el Proyecto para el Control


Para crear el control voy a crear un nuevo proyecto TextBoxButtonControl en Visual Studio utilizando la plantilla para bibliotecas de clases.

Crear proyecto de biblioteca de clases

Eliminamos la clase Class1 que genera el Visual Studio por defecto y creamos una nueva clase TextBoxButton.

Para que nuestro control tenga toda la funcionalidad de un TextBox clásico vamos a hacer que nuestra clase herede de la clase TextBox de System.Windows.Forms. Para ello necesitamos agregar la referencia a esta librería. En el menú Proyecto vamos a la opción Agregar referencia, desplegamos Ensamblados y vamos a la opción Framework. En la lista de ensamblados seleccionamos System.Windows.Forms y pulsamos Aceptar.

Agregar referencia a System.Windows.Forms

Hacemos que nuestra clase TextBoxButton herede de System.Windows.Forms.TextBox, y con esto ya tendríamos la base de nuestro propio control con toda la funcionalidad del TextBox clásico.

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

namespace TextBoxButtonControl
{
    public class TextBoxButton: TextBox
    {

    }
}
Imports System.Windows.Forms

Public Class TextBoxButton
    Inherits TextBox

End Class

Creando el proyecto de prueba


Para poder probar nuestro control vamos a agregar un nuevo proyecto PruebaControl a la solución utilizando la plantilla Aplicación de Windows Forms.

Crear proyecto de pruebas

Para poder utilizar el nuevo control desde el proyecto de pruebas tendremos que añadir una referencia al proyecto TextBoxButtonControl. En el menú Proyecto vamos a la opción Agregar referencia, desplegamos Solución y vamos a la opción Proyectos. Seleccionamos el proyecto TextBoxButtonControl y pulsamos Aceptar.

Si abrimos en modo diseño el formulario Form1 que Visual Studio ha creado automáticamente al crear el proyecto PruebaControl, veremos que en la ventana Cuadro de herramientas aparece nuestro nuevo control TextBoxButton.

TextBoxButton en Cuadro de herramientas

Si no puedes ver el control, compila la Solución (Menú Compilar > Compilar Solución) para que aparezca.

A continuación arrastramos el control al área de diseño del formulario y veremos que, como esperábamos, la apariencia es la misma que la de un control TextBox y, mirando la ventana Propiedades, que ha heredado todas las propiedades y eventos de éste.

Establecemos el proyecto PruebaControl como proyecto de inicio (Menú Proyecto >  Establecer como proyecto de inicio) y arrancamos la aplicación para comprobar que el funcionamiento de nuestro TextBoxButton es idéntico al de cualquier otro TextBox.

Añadiendo el botón al TextBoxButton


Para añadir el botón voy a crear un control Button en el constructor de la clase TextBoxButton y lo añadiré a la colección Controls de éste.

También voy crearé un método PosicionarBoton que se encargará de establecer el ancho y alto del botón al valor de la altura del TextBoxButton y de alinearlo a la derecha del TextBox. Llamaré a este método en la creación del control y cada vez que se genere el evento Resize indicando que el control ha cambiado de tamaño.

Para poder utilizar las clases Size y Point deberemos añadir una referencia al ensamblado System.Drawing de la misma forma que lo hicimos con System.Windows.Forms.

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

namespace TextBoxButtonControl
{
    public class TextBoxButton: TextBox
    {

        private readonly Button _button;

        public TextBoxButton()
        {
            _button = new Button {
                Cursor = Cursors.Default,
                TabStop = false
            };
            this.Controls.Add(_button);
            PosicionarBoton();
        }

        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            PosicionarBoton();
        }

        private void PosicionarBoton()
        {
            _button.Size = new Size(this.ClientSize.Height, this.ClientSize.Height);
            _button.Location = new Point(this.ClientSize.Width - _button.Width, 0);
        }

    }
}
Imports System.Windows.Forms
Imports System.Drawing

Public Class TextBoxButton
    Inherits TextBox

    Private ReadOnly _button As Button

    Public Sub New()
        _button = New Button() With { _
            .Cursor = Cursors.Default, _
            .TabStop = False}
        Me.Controls.Add(_button)
        PosicionarBoton()
    End Sub

    Protected Overrides Sub OnResize(e As EventArgs)
        MyBase.OnResize(e)
        PosicionarBoton()
    End Sub

    Private Sub PosicionarBoton()
        _button.Size = New Size(Me.ClientSize.Height, Me.ClientSize.Height)
        _button.Location = New Point(Me.ClientSize.Width - _button.Width, 0)
    End Sub

End Class

Si compilamos el proyecto y vamos a nuestro formulario de prueba veremos que ya aparece el botón a la derecha de nuestro control. Si modificamos el tamaño del control comprobaremos que el botón se redimensiona para ajustarse al tamaño del contenedor y, si arrancamos el proyecto PruebaControl veremos cómo se muestra correctamente en el formulario.



Sin embargo, si introducimos texto en el control, veremos que cuando el texto llega a la altura del botón el texto se sigue escribiendo debajo de éste.

El texto se escribe debajo del botón

Por desgracia el Framework de .NET no nos proporciona herramientas para evitar este problema así que tendremos que recurrir a la API de Windows. Para establecer los margenes de un control de edición deberemos enviar el mensaje EM_SETMARGINS.

        private void PosicionarBoton()
        {
            _button.Size = new Size(this.ClientSize.Height, this.ClientSize.Height);
            _button.Location = new Point(this.ClientSize.Width - _button.Width, 0);
            SendMessage(this.Handle, 0xd3, (IntPtr)2, (IntPtr)(_button.Width << 16));
        }

        [System.Runtime.InteropServices.DllImport("user32.dll")]
        private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
    Private Sub PosicionarBoton()
        _button.Size = New Size(Me.ClientSize.Height, Me.ClientSize.Height)
        _button.Location = New Point(Me.ClientSize.Width - _button.Width, 0)
        SendMessage(Me.Handle, &HD3, 2, _button.Width << 16)
    End Sub

    Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _
                ByVal hWnd As IntPtr, ByVal msg As IntPtr, ByVal wp As IntPtr, _
                ByVal lp As IntPtr) _
            As IntPtr

Si arrancamos de nuevo nuestro formulario de prueba e introducimos texto en el control comprobaremos que el espacio de edición finaliza justo antes de llegar al botón.

El texto se muestra respetando el espacio ocupado por el botón

Añadiendo funcionalidad al botón


Ya tenemos nuestro control de texto con botón incrustado, pero no tenemos forma de añadirle funcionalidad al botón ya que no podemos controlar cuando es pulsado por el usuario.

Para solucionarlo vamos a propagar el evento Click del botón a través de un evento ButtonClick en el nuevo control. Para ello creamos el evento ButtonClick y lo configuramos de forma que, cuando se añada un controlador al evento, se lo añadimos al evento Click del botón. Y cuando se elimine un controlador, lo eliminamos igualmente del evento Click del botón.

        [Category("Action")]
        public event EventHandler ButtonClick
        {
            add {  _button.Click += value; }
            remove { _button.Click -= value; }
        }
    <Category("Action")> _
    Public Custom Event ButtonClick As EventHandler
        AddHandler(value As EventHandler)
            AddHandler _button.Click, value
        End AddHandler

        RemoveHandler(value As EventHandler)
            RemoveHandler _button.Click, value
        End RemoveHandler

        RaiseEvent(sender As Object, e As EventArgs)
        End RaiseEvent
    End Event

He añadido un atributo Category al evento para que se muestre en la ventana de Propiedades del Visual Studio junto con el resto de eventos de la categoría Acción.


Si en el formulario de prueba añadimos un controlador para el evento podremos probar la nueva funcionalidad.

        private void textBoxButton1_ButtonClick(object sender, EventArgs e)
        {
            MessageBox.Show("Ha pulsado el botón del control");
        }
    Private Sub TextBoxButton1_ButtonClick(sender As Object, e As EventArgs) Handles TextBoxButton1.ButtonClick
        MessageBox.Show("Ha pulsado el botón del control")
    End Sub

Añadiendo una imagen al botón


Ya tenemos el control con la funcionalidad deseada. Únicamente nos faltaría mejorar la presentación. Para ello añadiré una propiedad que permita al desarrollador especificar una imagen a mostrar en el botón e incluiré una imagen en el proyecto que se mostrará por defecto en caso de que el desarrollador no indique ninguna.

Primero voy a añadir la imagen que mostraré por defecto en el proyecto. Para ello abrimos las propiedades del proyecto (Menú Proyecto > Propiedades de TextBoxButtonControl...) y seleccionamos la opción Recursos. Si nos aparece un mensaje indicando que no existe ningún archivo de recursos para el proyecto, hacemos click en el enlace para generar uno. Seleccionamos Imágenes como tipo de recurso y Agregar archivo existente... en Agregar recurso.

Esta es la imagen que voy a añadir.

Imagen puntos suspensivos


Esta imagen ha sido creada originalmente por Google y descargada de www.flaticon.com. La imagen se encuentra bajo licencia CC BY 3.0.

Una vez añadida la imagen establecemos el nombre del recurso como "ellipsis":



A continuación voy a modificar la inicialización del botón para establecer la imagen por defecto.

            _button = new Button
            {
                Cursor = Cursors.Default,
                TabStop = false,
                BackgroundImage = Properties.Resources.ellipsis,
                BackgroundImageLayout = ImageLayout.Zoom
            };
        _button = New Button() With { _
            .Cursor = Cursors.Default, _
            .TabStop = False, _
            .BackgroundImage = My.Resources.ellipsis, _
            .BackgroundImageLayout = ImageLayout.Zoom}

Si arrancamos la aplicación podremos ver el efecto del cambio:


Ya sólo falta darle al desarrollador la posibilidad de cambiar la imagen del botón. Así que voy a añadir al control una propiedad ButtonImage.

        private Image _buttonImage;

        [Category("Appearance"), Description("Imagen del botón")]
        public Image ButtonImage
        {
            get
            {
                return _buttonImage;
            }
            set
            {
                _buttonImage = value;
                if (_buttonImage == null)
                    _button.BackgroundImage = Properties.Resources.ellipsis;
                else
                    _button.BackgroundImage = _buttonImage;
            }
        }
    Private _buttonImage As Image

    <Category("Appearance"), Description("Imagen del botón")> _
    Public Property ButtonImage() As Image
        Get
            Return _buttonImage
        End Get
        Set(ByVal value As Image)
            _buttonImage = value
            If _buttonImage Is Nothing Then
                _button.BackgroundImage = My.Resources.ellipsis
            Else
                _button.BackgroundImage = _buttonImage
            End If
        End Set
    End Property

He creado una variable privada _buttonImage para almacenar la imagen elegida por el desarrollador. Si el desarrollador no establece ninguna imagen se utiliza la imagen por defecto.

Si compilo y vuelvo al formulario de prueba podemos ver la nueva propiedad del control.

Propiedad ButtonImage

Para probar la nueva propiedad voy a asignarle a la propiedad ButtonImage una imagen con una lupa:


Imagen creada por SimpleIcon y descargada de www.flaticon.com bajo licencia CC BY 3.0.

Establecer la lupa como imagen del control

Al arrancar la aplicación podemos ver el efecto del cambio.

Resultado final del formulario de prueba

El código completo del ejemplo, tanto en C# como en Visual Basic, está disponible en:

Códigos de muestra - Ejemplos MSDN


11 comentarios:

  1. maestro super bueno su control, pero lo utilice para incluirlo como una nueva columna en un datagridview y no se como llamar desde ahi al evento del click del boton ya que no me aparece en las opciones de eventos como cuando se usa como control directo, no se si me podrías ayudar con este tema, porque necesito generar una grilla con esta opción y no quiero recurrir a los paquetes de controles porque quedo atado a sus librerías.

    gracias

    ResponderEliminar
    Respuestas
    1. Sí, no eres el primero que me lo plantea. A ver si saco un poco de tiempo y preparo un ejemplo de cómo podría hacerse.

      Dependiendo de la funcionalidad que quieras implementar podría enfocarse de dos formas diferentes:
      - Crear un DataGridViewColumn que utilice el control como editor y propagar el evento ButtonClick del control a la columna. Sería la solución más flexible.
      - Encapsular el comportamiento que se desee que tenga el botón dentro del propio editor y modificar su comportamiento a través de propiedades de la columna

      Eliminar
    2. yo cree la nueva columna utilizando el TextBoxButton como base pero no se en que parte debería agregar el nuevo evento, no puedo copiar aca todo el código, pero hice la pregunta en msdn y nade me pudo responder, te dejo el link porque ahi esta todo el código
      Link

      Eliminar
    3. Hola, me puedes decir como hiciste para ponerlo en el datagridview?. Lo necesito.
      Gracias

      Eliminar
  2. Saludos Asier, que buen ejemplo, te felicito, tengo una pregunta.
    Como hago para limitar el espacio del boton hacia el lado izquierdo ya que con este codigo siguiente limitas el lado derecho
    //Usando un api para limitar el tamaño del texto
    Win32Api.SendMessage(this.Handle, 0xd3, (IntPtr)2, (IntPtr)(_button_Izquierda.Width << 16));
    Como seria para el lado izquierdo

    ResponderEliminar
    Respuestas
    1. Hola George,
      siento el retraso pero me acabo de tomar unos días de vacaciones.

      Si quieres establecer el botón a la izquierda no tienes más que establecer su posición como 0,0 y establecer el margen izquierdo en lugar del derecho.

      Para esto último debes establecer el tercer parámetro del método SendMessage con el valor 1 (EC_LEFTMARGIN) y establecer el valor del ancho en el low-word del double word que se pasa como cuarto parámetro, lo que en la práctica significa que no necesitas realizar el desplazamiento de 16 bits:

      _button.Location = new Point(0, 0);
      SendMessage(this.Handle, 0xd3, (IntPtr)1, (IntPtr)_button.Width);

      Puedes encontrar más información sobre el mensaje EM_SETMARGINS en:
      https://msdn.microsoft.com/es-es/library/bb761649(v=vs.85).aspx

      Un saludo

      Eliminar
  3. Hola buenas una consulta, tengo 2 formularios A y B, en el B tengo un formulario que genera números aleatorios al presionar un botón que le puse, pero yo necesito poner un botón en el formulario A para ejecutar desde ahí los números aleatorios del formulario B, como lo podría hacer..

    ResponderEliminar
  4. Asier buen día, muy buen aporte, me podrías orientar para establecer ambos margenes en el control (derecho e izquierdo)

    ResponderEliminar
  5. Hola Asier, soy Álvaro:
    Tu control es perfecto, va genial. Felicidades. Tengo una pregunta: ¿Es posible cargar tu control sin necesidad de añadir todo el proyecto a la solución? Estoy intentando cargar únicamente la DLL que genera tu proyecto pero no consigo visualizarlo en el cuadro de herramientas.
    Muchas gracias.

    ResponderEliminar
  6. Está muy bueno como siempre Asier.
    Dime una cosa, como le cambio el alto al control?
    Lo intento con MiminumSize y heigth pero no puedo ponerlo en un alto de 22
    Gracuas

    ResponderEliminar
  7. Asier no me reconoce el comando Properties...
    Esta sentencia:
    _button.BackgroundImage = Properties.Resources.ellipsis;
    Estoy en Net 4.5 con C# 2015
    Gracias

    ResponderEliminar