Controles planos y sensibles al tacto en .NETEfecto bidimensional y reacción al contacto redibujando el control con GDI+.Publicado: 12/Ago/2003
|
. |
Índice
Introducción
VSNET permite eliminar el efecto tridimensional de los controles mediante las propidades FlatStyle y BorderStyle, sin embargo el borde es fijo. Los controles que aquí presento son sensibles al tacto, que es más que sensibilidad al foco: simplemente, alteran el color de los bordes no sólo al recibir el foco, sino también al pasar por encima el ratón. Ya sé que los hay en la web a patadas mucho más refinados que los míos, precisamente por eso oso publicarlos: porque son sencillísimos de implementar. No están hechos desde cero (me encanta esa expresión: from scratch), no llamo a ninguna API, no les he preparado skins ni estilo XP, sino que son totalmente artesanales, al alcance del principiante: un brochazo y cuatro rayas y a correr.
FlatTextBox y controles de un solo borde rectangular
Todo lo que tenemos que hacer es subclasificar el control, es decir, introducir en la corriente de los mensajes que lanza el sistema operativo a nuestro control un procedimiento escrito por nosotros justo antes del procedimiento propio del control (window procedure, abreviado, WndProc), y obligar a los mensajes a atravesar nuestro procedimiento antes de aterrizar en WndProc, de tal modo que podamos introducir código que tome decisiones basadas en los mensajes.
Ya estamos que si la abuela fuma. ¿No decías que iba a ser fácil? A mí eso me suena a puro C++ y a subclasificación en Visual Basic 6, en resumen, a vade retro.
Pues yo sigo en mis trece, vamos a hacer todo eso y ni nos vamos a enterar. Esta es otra de las maravillas de la plataforma .NET. Aquí lo tenéis:
Public Class FlatTextBox : Inherits TextBox ' Este es el mensaje que debemos discriminar Private Const WM_PAINT As Integer = &HF ' Este es el procedimiento que insertamos en la corriente de los mensajes ' Su parámetro es una encapsulación de la estructura Message ' que entra en el procedimiento Protected Overrides Sub WndProc(ByRef m As Message) ' Ya sabemos qué mensaje es, luego ya no lo necesitamos ' y lo despachamos al procedimiento del control MyBase.WndProc(m) ' Aqui ejecutamos código según qué mensaje nos ha visitado If m.Msg = WM_PAINT Then ' ' Aquí dibujamos ' End If End sub End ClassPrimero creamos una clase derivada del control que vamos a subclasificar y definimos la constante que representa el mensaje. Después reimplementamos el método WndProc del control, cuyo único argumento es una versión encapsulada de la estructura Message pasada por referencia. En realidad estamos interponiendo antes del procedimiento del control WndProc nuestro propio procedimiento, y estamos obligando a los mensajes a circular por él. Nada más entrar, sin embargo, les abrimos la puerta de salida y permitimos que se vayan al procedimiento propio del control invocando el método WndProc de la clase base. Y es que no necesitamos los mensajes: nos basta con actuar cuando detectamos que se trata de WM_PAINT.
WM_PAINT le obliga al control a dibujarse de nuevo. Y aquí viene la sensibilización del control. Sólo tenemos que definir las distintas variaciones del efecto bidimensional y decidir cuándo pintaremos cuál. Por ejemplo: queremos que cuando el ratón pase por encima, el botón se oscurezca. Pues en el momento en que acariciemos el botón con el ratón le enviaremos el mensaje WM_PAINT, y al atravesar nuestro procedimiento interpuesto WndProc lo detectaremos y pintaremos el botón de color oscuro.
Crearemos un efecto "resaltado" cuando se disparen los eventos OnMouseEnter, OnGotFocus y OnMouseHover, y volveremos a un aspecto "neutral" cuando ocurran OnMouseLeave y OnLostFocus. Declararemos una variable booleana a nivel de clase que indicará a WndProc si debe dibujar estilo resaltado o neutral. Así pues, cada evento dará un valor a esta variable e invocará el método Me.Invalidate(), que es quien envía el mensaje WM_PAINT al control:
Private IamON As Boolean Protected Overrides Sub OnMouseEnter(ByVal e As System.EventArgs) MyBase.OnMouseEnter(e) IamON = True Me.Invalidate() End Sub Protected Overrides Sub OnMouseLeave(ByVal e As System.EventArgs) MyBase.OnMouseLeave(e) IamON = False Me.Invalidate() End Sub Protected Overrides Sub OnLostFocus(ByVal e As System.EventArgs) MyBase.OnLostFocus(e) IamON = False Me.Invalidate() End Sub Protected Overrides Sub OnGotFocus(ByVal e As System.EventArgs) MyBase.OnGotFocus(e) IamON = True Me.Invalidate() End Sub Protected Overrides Sub OnMouseHover(ByVal e As System.EventArgs) MyBase.OnMouseHover(e) IamON = True Me.Invalidate() End SubYa sólo nos queda dibujar en el control. Necesitamos dos colores, así que los definiremos a nivel de clase y les daremos valores por defecto. Por supuesto que invitaremos al código cliente, ofreciéndole propiedades, a que los modifique. Y no hay que olvidar que la propiedad BorderStyle o FlatStyle del control debe definirse al construir la clase con un valor y no otro. Cuál de ellos depende del control, es cosa de probar. Aquí está la clase entera:
Imports System.Drawing.Drawing2D Namespace ControlesFlacos Public Class FlatTextBox : Inherits TextBox Private Const WM_PAINT As Integer = &HF Private _ActiveBorderColor As Color = SystemColors.ControlDarkDark Private _InactiveBorderColor As Color = SystemColors.ControlDark Private _BorderColor As Color Private IamON As Boolean Public Sub New() MyBase.New() Me.BorderStyle = BorderStyle.FixedSingle End Sub Public Property ActiveBorderColor() As Color Get Return _ActiveBorderColor End Get Set(ByVal Value As Color) _ActiveBorderColor = Value End Set End Property Public Property InactiveBorderColor() As Color Get Return _InactiveBorderColor End Get Set(ByVal Value As Color) _InactiveBorderColor = Value End Set End Property Protected Overrides Sub OnMouseEnter(ByVal e As System.EventArgs) MyBase.OnMouseEnter(e) IamON = True Me.Invalidate() End Sub Protected Overrides Sub OnMouseLeave(ByVal e As System.EventArgs) MyBase.OnMouseLeave(e) IamON = False Me.Invalidate() End Sub Protected Overrides Sub OnLostFocus(ByVal e As System.EventArgs) MyBase.OnLostFocus(e) IamON = False Me.Invalidate() End Sub Protected Overrides Sub OnGotFocus(ByVal e As System.EventArgs) MyBase.OnGotFocus(e) IamON = True Me.Invalidate() End Sub Protected Overrides Sub OnMouseHover(ByVal e As System.EventArgs) MyBase.OnMouseHover(e) IamON = True Me.Invalidate() End Sub Protected Overrides Sub WndProc(ByRef m As Message) MyBase.WndProc(m) If m.Msg <> WM_PAINT Then Exit Sub Dim g As Graphics = Me.CreateGraphics Dim cliente As Rectangle = Me.ClientRectangle If IamON Then _BorderColor = _ActiveBorderColor Else _BorderColor = _InactiveBorderColor End If g.DrawRectangle(New Pen(_BorderColor), 0, 0, cliente.Width - 1, cliente.Height - 1) g.Dispose() End Sub End Class End NamespaceComo véis, al final todo desemboca en el rectángulo que simula el borde del control. Sus coordenadas las obtenemos de la propiedad Me.ClientRectangle, de modo que sólo nos queda dibujarlo con GDI+.
(Nota: lo de ControlesFlacos va de guasa. Ya sé que flat no significa flaco Tampoco library significa librería y todos vamos por ahí con nuestras dynamic link bookshops y nos quedamos más anchos que largos. Así que yo también reclamo mi derecho a pertenecer al heap de los patanes e inauguro emocionado mi pésima traducción. (Nota a la nota: lo que pasa es que me acabo de leer La rebelión de las masas de Ortega y Gasset y todavía lo tengo fresco))
En los programas de ejemplo podéis ver que, salvo pequeños detalles, esta misma clase se aplica a los controles con un sólo borde rectangular, como ListView, TreeView, Listbox y Button. Al botón, además de pintarle el borde, le alteramos la propiedad BackColor de forma que, con el permiso del código cliente, al acariciarlo con el cursor se oscurece. Talmente como si se ruborizara.
FlatCheckBox y FlatRadioButton
CheckBox y RadioButton muestran dos peculiaridades:
- No es el rectángulo lo que debemos dibujar, sino el cuadradito del CheckBox y la circunferencia del Radiobutton que están dentro del rectángulo.
- Ese cuadradito y esa circunferencia pueden alinearse, ergo cambiar de posición, de seis formas diferentes.
Resolver la primera peculiaridad no tiene ningún misterio: sólo hay que calcular las coordenadas y el tamaño de la figura y dibujarla. La herramienta que he utilizado ha sido el ojímetro.
La segunda peculiaridad se resuelve con la misma herramienta que la primera. Introducimos un Select Case y calculamos las coordenadas para cada posición. Yo a tanto no he llegado. Mis FlatCheckBox y FlatRadioButton sólo admiten las dos alineaciones de toda la vida: a la izquierda y a la derecha. Copio el procedimiento WndProc del FlatRadioButton:
Protected Overrides Sub WndProc(ByRef m As Message) MyBase.WndProc(m) : If m.Msg <> WM_PAINT Then Exit Sub Const DIAM As Integer = 11 Dim Cliente As RectangleF Dim g As Graphics = Me.CreateGraphics Select Case Me.CheckAlign Case ContentAlignment.MiddleLeft Cliente = New RectangleF(Me.ClientRectangle.X, _ Me.ClientRectangle.Height / 2 - DIAM / 2, DIAM, DIAM) Exit Select Case ContentAlignment.MiddleRight Cliente = New RectangleF(Me.ClientRectangle.Width - DIAM, _ Me.ClientRectangle.Height / 2 - DIAM / 2, DIAM, DIAM) Exit Select Case Else Exit Sub End Select If IamON = True Then _BorderColor = _ActiveBorderColor Else _BorderColor = _InactiveBorderColor End If g.SmoothingMode = Drawing2D.SmoothingMode.HighQuality g.DrawEllipse(New Pen(_BorderColor), Cliente) End SubDignos de mención dos detalles: dada la necesaria pixelmétrica precisión, en vez de Rectangle se utiliza RectangleF, que se mide con decimales; y, para que no quede basto, suavizamos la línea con SmoothingMode.
FlatComboBox
Un control especial
El caso del ComboBox es especial porque se compone de tres controles interrelacionados: un TextBox, un Listbox y un botón. El ListBox ni lo tocamos porque es imposible que aparezca y el ComboBox no tenga el foco, así que redibujaremos la caja de texto, el botón y su flechita.
Tenemos que definir tres parejas de colores: una para la caja de texto, otra para el botón y otra para la flechita. Primero cubrimos el control entero de blanco y luego vamos perfilando cada elemento. Muestro el código de WndProc en C# porque el programa de ejemplo está en C#:
protected override void WndProc(ref Message m) { base.WndProc (ref m); if (m.Msg == WM_PAINT) { //Definimos el rectángulo que abarca //la caja de texto y el botón Graphics g = this.CreateGraphics(); Rectangle Cliente = this.ClientRectangle; //Establecemos los colores if (IAmOn == true) { _BorderColor = _ActiveBorderColor; _ArrowColor = _ActiveArrowColor; _ButtonColor = _ActiveButtonColor; } else { _BorderColor = _InactiveBorderColor; _ArrowColor = _InactiveArrowColor; _ButtonColor = _InactiveButtonColor; } //Pintamos el control entero de blanco //para eliminar todo rastro de tres dimensiones g.FillRectangle(new SolidBrush(SystemColors.Window), Cliente); //Dibujamos el borde de todo el control g.DrawRectangle(new Pen(_BorderColor),0,0, Cliente.Width, Cliente.Height-1); //Definimos la localización y el tamaño del botón Point Punto = new Point(Cliente.Width-18,0); Size Area = new Size(Cliente.Width-Punto.X, Cliente.Height-Punto.Y); Rectangle Boton = new Rectangle(Punto, Area); //y lo pintamos g.FillRectangle(new SolidBrush(_ButtonColor), Boton); //Movemos el eje de coordenadas a la esquina noroeste del botón //para dibujar más cómodamente g.TranslateTransform(Boton.X,Boton.Y); //Dibujamos el borde del botón g.DrawRectangle(new Pen(_BorderColor),0,0,Boton.Width-1,Boton.Height-1); //Definimos un GraphicsPath que contendrá el dibujo de la flecha GraphicsPath Flecha = new GraphicsPath(); PointF NO = new PointF(Boton.Width / 4, 9 * Boton.Height / 24); PointF NE = new PointF(3 * Boton.Width / 4, NO.Y); PointF SU = new PointF(Boton.Width / 2, 15 * Boton.Height / 24); Flecha.AddLine(NO, NE); Flecha.AddLine(NE, SU); //suavizamos los bordes en lo posible g.SmoothingMode = SmoothingMode.AntiAlias; //y dibujamos la flecha g.FillPath(new SolidBrush(_ArrowColor), Flecha); g.Dispose(); } }Hasta aquí va sobre ruedas. Pero resulta que si aplicamos al FlatComboBox el estilo DropDownList, que obliga al control a no admitir más ítems que los cargados en su lista de ítems, ¡se borra el contenido de la caja de texto al perder el control el foco! Y esto sí que no me lo ha dicho nadie en ninguna página web, que lo mío me costó descubrir porqué ocurría. Solución: implementar el estilo DropDownList "a mano". Y ya que no nos queda más remedio, vamos a hacerlo a nuestro gusto. Crearemos dos estilos: los dos autocompletarán la caja de texto con los ítems de la lista, pero uno de ellos permitirá introducir ítems nuevos y el otro no.
Autocompletado con admisión de ítems nuevos
En vez de crear una enumeración que contuviera los estilos que vamos a crear y una propiedad del tipo de la enumeración dentro de la clase FlatComboBox, he optado por crear tres controles diferentes, uno por cada estilo. A gusto del consumidor. Comento el código en el mismo código:
/// FlatComboBox que se autocompleta /// y admite entradas nuevas public class AutoComboBox : FlatComboBox { bool _AutoComplete = true; public AutoComboBox():base() { //El estilo debe ser DropDown //para evitar que nos ocurra lo que queremos evitar base.DropDownStyle = ComboBoxStyle.DropDown; } protected override void OnKeyDown(KeyEventArgs e) { //No aplicamos el autocompletado //si las teclas pulsadas son Suprimir o Borrar _AutoComplete = ((e.KeyCode != Keys.Delete) && (e.KeyCode != Keys.Back)); base.OnKeyDown (e); } protected override void OnTextChanged(EventArgs e) { if (_AutoComplete == true) { string Texto = this.Text; //FindString localiza el primer ítem de la lista //cuyas primeras letras coinciden con las de su argumento //Está claro que es una función pensada //para autocompletar el ComboBox. int Index = this.FindString(Texto); if (Index >= 0) { //Al cargar la caja de texto con el ítem localizado //se dispara otra vez el evento OnTextChanged //o sea, este procedimiento, //que volvería a cambiar el contenido de la caja de texto //aunque fuera con el mismo texto, //lo cual volvería a disparar el evento OnTextChanged //formándose un ciclo sin fin. //Por eso hay que crear una variable //de tipo booleano que haga de semáforo. _AutoComplete = false; this.SelectedIndex = Index; _AutoComplete = true; //Al ir autocompletando mantenemos seleccionado //el fragmento de texto que no se ha autocompletado. //Esto permite autocompletar el ítem entero //tecleando sólo las letras de la palabra. this.Select(Texto.Length, this.Text.Length); } } base.OnTextChanged (e); } }Autocompletado sin admisión de ítems nuevos
El evento dentro del cual podemos discriminar los caracteres que admitimos en la caja de texto es, como todo NETadicto sabe, OnKeyPress. Buscamos en la lista el texto de la caja de texto más el carácter nuevo, y si lo encontramos dejamos pasar al carácter, y si no, no. Este procedimiento no sustituye sino que se añade al anterior.
protected override void OnKeyPress(System.Windows.Forms.KeyPressEventArgs e) { if (base.AutoComplete == true) { e.Handled = true; //Añadimos al texto del control el carácter nuevo string Texto = this.Text.Substring(0, this.Text.Length - this.SelectedText.Length) + e.KeyChar; //Lo buscamos en la lista y si lo encontramos //permitimos que se una al texto if (this.FindString(Texto) >= 0) e.Handled = false; } base.OnKeyPress (e); }En Visual Basic están ejemplificados los controles Listview, TreeView, ListBx, TextBox, CheckBox, Radiobutton y Button. El ComboBox lo he reservado para C#. Y de regalo un formulario con quintuplas de los controles TextBox, Button, RadioButton y CheckBox en C++NET que disparan un evento cuando cambia el color del borde. Si supiera cómo se pasan argumentos por referencia en J# os habría ofrecido una versión en este idioma, pero no lo sé. Si alguien me lo explica se lo agradezco.
Que seáis felices
CipriPulsa aquí para descargarte el ejemplo en VBNET
Pulsa aquí para descargarte el ejemplo en C#
Pulsa aquí para descargarte el ejemplo en C++NET