Ir al contenido principal

TextBox con Icon/Imagen

Bien, continuando con este articulo: TextBox con borde personalizado, ahora le dare la funcionalidad de poder mostrar un icono o imagen dentro del Control TextBox.

Existen dos maneras de hacer esto:
  • Pintar el icono/imagen dentro del control o
  • Pintar el icono/imagen dentro del Non-Client Area del control.
Pintar el icono/imagen dentro del control.

Antes de escribir el código decidi googlear un poco, para ver si alguien más ya habia tenido la misma idea de usar el mensaje EM_SETMARGINS para dejar el espacio necesario para pintar el icono o imagen ya sea a la derecha o izquierda y me he encontrado con este articulo.


Pintar el icono/imagen dentro del Non-Client Area del control.
Usando el Non-Client Area no encontre resultados googleando, así que es la forma que usare para dibujar un icono o imagen dentro de un control TextBox.

En el control TextEditor que escribí, utilizo esta manera para pintar el icono o imagen dentro del TextBox, así que, si cambian el estilo del borde a "FixedSingle" vera un pequeño problema que no considere al escribir este control, pero!!, veremos la solución a este problema.   ;-)

Imagen del problema.



Si leyeron el articulo anterior a este (TextBox con borde personalizado), veran que menciono la solución al problema, pero no esta completa, no.... como en este articulo no modifique el Non-Client Area para pintar el borde de un color diferente, la solución no esta completa, pero la completaremos en este articulo.

Así que empecemos.

WM_NCCALCSIZE: Con este mensaje dejaremos el espacio a la izquierda o derecha para pintar el icono o imagen.

C#
private bool WmNCCalcSize(ref Message m)
{
    if (this.Image == null)
    {
        this.intClientArea = Rectangle.Empty;
        return true;
    }

    if (m.WParam == (IntPtr)1)
    {
        NativeMethods.NCCALCSIZE_PARAMS ncParams = (NativeMethods.NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NativeMethods.NCCALCSIZE_PARAMS));
        int top = 0, right = 0, bottom = 0, left = 0;
        if (this.BorderStyle == BorderStyle.Fixed3D)
        {
            top = right = bottom = left = 2;
        }

        if (this.ImageAlign == ImageAlign.Left)
        {
            left += this.Image.Width + 2;
            left += this.BorderStyle != BorderStyle.Fixed3D ? 2 : 0;
        }
        else
        {
            right += this.Image.Width + 2;
            right += this.BorderStyle != BorderStyle.Fixed3D ? 2 : 0;
        }

        ncParams.rgrc0.Top += top;
        ncParams.rgrc0.Left += left;
        ncParams.rgrc0.Right -= right;
        ncParams.rgrc0.Bottom -= bottom;

        this.intClientArea = new Rectangle(left, top, ncParams.rgrc0.Right - ncParams.rgrc0.Left, ncParams.rgrc0.Bottom - ncParams.rgrc0.Top);

        Marshal.StructureToPtr(ncParams, m.LParam, false);
        return false;
    }

    return true;
}

La idea de este método privado es, si hemos seleccionado una imagen para mostrar en el control, según donde queremos que la imagen aparezca,  con este mensaje modificare el Non-Client Area del control para pintar la imagen luego en el mensaje wm_ncpaint, ademas definido el valor para la variable "intClientArea" tipo Rectangle, este sera el rectángulo a excluir para evitar el parpadeo del texto.

WM_NCPAINT: En este mensaje pintaremos la imagen.

C#
private bool WmNCPaint(ref Message m)
{
    if (!this.IsHandleCreated || 
         this.IsDisposed || 
         (this.Image == null && this.BorderColor.IsEmpty))
        return true;

    Rectangle winRect = this.WindowRect;
    if (winRect.IsEmpty)
        return true;

    Rectangle bounds = new Rectangle(0, 0, winRect.Width, winRect.Height);
    if (this.BorderStyle == BorderStyle.Fixed3D && this.BorderColor.IsEmpty)
        bounds = new Rectangle(2, 2, winRect.Width - 4, winRect.Height - 4);

    IntPtr hDC = NativeMethods.GetDCEx(this.Handle, IntPtr.Zero, NativeMethods.DCXFlags.DCX_WINDOW | NativeMethods.DCXFlags.DCX_CACHE | NativeMethods.DCXFlags.DCX_CLIPSIBLINGS);
    if (hDC != IntPtr.Zero)
    {
        Rectangle excludeClip = new Rectangle(2, 2, bounds.Width - 4, bounds.Height - 4);
        if (!this.intClientArea.IsEmpty)
            excludeClip = new Rectangle(this.intClientArea.Left, this.intClientArea.Top, this.intClientArea.Right - this.intClientArea.Left, this.intClientArea.Bottom - this.intClientArea.Top);

        NativeMethods.ExcludeClipRect(hDC, excludeClip.Left, excludeClip.Top, excludeClip.Right, excludeClip.Bottom);
        using (BufferedGraphics bg = BufferedGraphicsManager.Current.Allocate(hDC, bounds))
        {
            using (SolidBrush brush = new SolidBrush(this.BackColor))
            {
                bg.Graphics.FillRectangle(brush, bounds);
            }
            this.OnNCPaint(bg.Graphics, bounds);
            bg.Render(hDC);
        }

        NativeMethods.ReleaseDC(this.Handle, hDC);
    }

    if (this.BorderColor.IsEmpty)
        base.DefWndProc(ref m);

    return false;
}


Al seleccionar el estilo de borde "FixedSingle", tenemos el problema antes mencionado en el control TextEditor, pero para resolverlo tendremos que hacerle más cambios al código en el método WM_PAINT, veamos.


C#
private bool WmPaint(ref Message m)
{
    if (this.BorderStyle != BorderStyle.FixedSingle || 
        (this.Image == null && this.BorderColor.IsEmpty))
        return true;

    NativeMethods.PAINTSTRUCT ps = default(NativeMethods.PAINTSTRUCT);

    bool flag = false;
    IntPtr hDC = m.WParam;
    if (m.WParam == IntPtr.Zero)
    {
        hDC = NativeMethods.IntBeginPaint(new HandleRef(this, this.Handle), ref ps);
        flag = true;
    }

    // Excluimos el area del texto para evitar parpadeo.
    NativeMethods.ExcludeClipRect(hDC, this.ClientRectangle.Left + 1, this.ClientRectangle.Top + 1, this.ClientRectangle.Right - 1, this.ClientRectangle.Bottom - 1);
    using (BufferedGraphics bg = BufferedGraphicsManager.Current.Allocate(hDC, this.ClientRectangle))
    {
        using (SolidBrush brush = new SolidBrush(this.BackColor))
        {
            bg.Graphics.FillRectangle(brush, this.ClientRectangle);
        }
        this.PaintBorder(bg.Graphics, this.ClientRectangle);
        bg.Render();
    }

    // excluimos toda el area del cliente.
    NativeMethods.ExcludeClipRect(hDC, this.ClientRectangle.Left, this.ClientRectangle.Top, this.ClientRectangle.Right, this.ClientRectangle.Bottom);
    // creamos una nueva area de cliente para excluir el border pintado por nosotros
    // y evitar que lo pinte el control.
    IntPtr hRgn = NativeMethods.CreateRectRgn(this.ClientRectangle.Left + 1, this.ClientRectangle.Top + 1, this.ClientRectangle.Right - 1, this.ClientRectangle.Bottom - 1);
    // aplicamos la nueva area para que el control pinte su contenido o texto.
    NativeMethods.ExtSelectClipRgn(hDC, hRgn, 2);

    m.WParam = hDC;
    base.DefWndProc(ref m);

    if (flag)
        NativeMethods.IntEndPaint(new HandleRef(this, this.Handle), ref ps);

    return false;
}

Si leemos la documentación de este mensaje podemos leer que el parámetro wParam puede recibir un puntero a un "Device Context" y este sera utilizado para pintar el control en este.


For some common controls, the default WM_PAINT message processing checks the wParam parameter. If wParam is non-NULL, the control assumes that the value is an HDC and paints using that device context.


Así que para evitar el problema antes mencionado lo que hago es excluir el área donde se pintara el borde para evitar este problema, para lo cual he acudido al uso de otras API's del windows, forzando previamente a utilizar un "Device Context" que puedo controlar.

CreateRectRgn
ExtSelectClipRgn
IntBeginPaint
IntEndPaint

Obteniendo como resultado.




y listo... eso es todo por hoy, ahora tenemos un control TextBox al cual le podemos cambiar el color del borde y también podemos mostrar un icono/Imagen en este, cuyo valor agregado es muy útil para los usuarios de nuestras aplicaciones.

así que espero sea de utilidad para ustedes, cualquier critica o comentario, sera bienvenido.

Salu2,

Descargas:
CSharp.Windows.Forms.ControlEx(2).rar


Articulos Relacionados:
Nullable DateTimePicker


Comentarios

  1. Hola Marvín: Este control textbox que mencionas (imagen y borde), lo ofreces para descargar? no veo el link..

    Saludos y felicidades por tu trabajo!

    Luis Escobar

    ResponderEliminar
    Respuestas
    1. mmm..... fijate que aún no tenia que publicarlo ya que aún no tengo el código en VB.NET, pero creo que en lugar de guardar el borrador lo publique....

      pero bien, el código que si tengo listo porque primero lo escribo en C# es el de C#, solo dejame subirlo para crear el Link y lo puedas descargar.

      Salu2,

      Eliminar
    2. Listo Luis, ya puedes descargar el código fuente en C#, pronto subiré el de VB.NET

      Salu2,

      Eliminar
  2. Sos toro man, yo aun vengo aprendiendo c#. Gracias, seguí así.

    ResponderEliminar
  3. como enlazo en el formulario el textbox a la clase

    ResponderEliminar
  4. Muchas gracias Marvin por tu trabajo.
    Una cuestión:
    ¿Sería posible capturar el evento click de la imagen?
    ¿Como se haría?
    Creo que sería un valor añadido al control.
    De nuevo muchas gracias por tu trabajo.

    ResponderEliminar
  5. Hola Marvin excelnte trabajo, gracias por compartir la fuente, pero como se puede capturar el click de la imagen, nos ayudaria bastante a tus seguidores.

    ResponderEliminar
  6. Como disminuyes o en donde disminuyes el margen para que la imagen entre en su totalidad

    ResponderEliminar

Publicar un comentario

Entradas populares de este blog

TextBox con Borde Personalizado

Bien, retomando nuevamente mi blog, luego de tanto tiempo ausente, veremos como personalizar el borde del control TextBox con un color diferente. hace poco vi en los foros de MSDN, en el foro de VB.Net esta pregunta, ¿ Cómo puedo cambiar el color del borde de un control TextBox ?, anteriormente también se hizo la misma pregunta en este mismo foro donde yo respondí como hacerlo VB2010 4.0 - Como crear un textbox personalizado . no hay manera fácil de personalizar un control, generalmente se tiene que sobre escribir el evento WndProc para escuchar los mensajes de window y reemplazar la funcionalidad de estos según sea la necesidad o el control. bien, para cambiar el color del borde del control TextBox sin mucha funcionalidad, se debe de escuchar y reemplazar el funcionamiento de los mensajes WM_PAINT y WM_NCPAINT . ¿ Porque WM_PAINT ?, porque cuando cambiamos la propiedad BorderStyle de este control a FixedSingle, quien pinta el borde es el mensaje WM_PAINT no asi el WM_NC

Personalizar DataGridView (II) - Bloquear columnas de solo lectura

Personalizar DataGridView - Actualizaciones Personalizar DataGridView (II.1) - Bloquear columnas de solo lectura. Personalizar DataGridView (III) - Cambiar Diseñador. Bien, continuando con el articulo " Personalizar DataGridView (I) - Pintar área vacía ", ahora lo que haré es darle al control la funcionalidad de bloquear las columnas cuya propiedad " ReadOnly " se establezca en " true ", entiéndase por "Bloquear" el evitar que las columnas cuya propiedad " ReadOnly=true " puedan recibir el foco, ya sea por el teclado o por el ratón ( mouse ). Para tal objetivo agregare una nueva propiedad al control la cual llamare " AllowFocusReadOnlyColumns " cuyo valor predeterminado sera " true ", en caso de ser " false " las columnas marcadas como solo lectura no recibirán el foco. También le daré la funcionalidad de poder avanzar a la siguiente columna al presionar la tecla " ENTER " agregando otr