lunes, 8 de abril de 2013

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