Cómo iluminar menús en .NETImágenes, colores y tipos de letra para convertir un menú en una obra de arte.Fecha: 29 Julio 2003 (06/Ago/03)
|
. |
Introducción
Los monjes medievales, que no vieron un teclado en su vida, convertían los libros que copiaban en increíbles obras de arte. Lo llamaban "iluminar manuscritos". Pues voy a contaros cómo colorear, cambiar el tipo de letra y añadir imágenes a los menús para que estén "iluminados", y encapsularlo todo en una clase para que todo sea teclear y cantar.
(nota a pie de página: si no sabes crear menús en tiempo de ejecución te recomiendo que leas este artículo del Guille)
Qué se puede iluminar y qué no
Podemos personalizar:
- La imagen
- El color y tipo de letra del texto seleccionado y del no seleccionado
- El número de colores del ítem seleccionado y del no seleccionado: uno o dos
- Si son dos, el tipo de gradación de uno a otro
No podemos personalizar:
- El color del ítem no seleccionado. Mediante código es posible, y el sistema operativo parece que lo intenta, pero... no recomiendo ni que lo intentéis, y si no me hacéis caso, guardad antes todo lo que estéis haciendo si no queréis perderlo, porque Windows se vengará.
Y cómo se hace
Se trata de crear una clase derivada de System.Windows.Forms.MenuItem. La clave está en los tres puntos siguientes:
- La propiedad OwnerDraw de la clase base. Debemos asignarle el valor True en el constructor:
Public Sub New() MyBase.New() MyBase.OwnerDraw = True End Sub
- El método protegido OnDrawItem. Lo reimplementaremos y pintaremos en él el ítem:
Protected Overrides Sub OnDrawItem(ByVal e As DrawItemEventArgs)
- El método protegido OnMeasureItem. En su reimplementación calcularemos el tamaño del ítem según la longitud del texto y nuestro gusto:
Protected Overrides Sub OnMeasureItem(ByVal e As MeasureItemEventArgs)El código restante contiene la interfaz mediante la cual el código cliente define su obra de arte y un método público que coloca al ítem, una vez definido, en su posición dentro del MainMenu:
Public Overloads Function Add() As IconMenuItem _TopItem.MenuItems.Add(Me) Return Me End Function Public Overloads Function Add(ByVal Indice As Integer) As IconMenuItem _TopItem.MenuItems.Add(Indice, Me) Return Me End FunctionLa variable _TopItem representa la cabeza de la columna donde se colocará el IconMenuItem recién creado. Si no se especifica índice, se coloca el último, y si se indica índice, en la posición por el índice indicada.
OnDrawItem
Cuatro son las tareas que realiza este método
- La primera línea invoca al método OnDrawItem de su clase base
- Después pasa una mano de pintura por el ítem
- Luego cuelga la imagen
- Y por último escribe el texto
La mano de pintura
Lo primero es hacernos con la brocha y elegir color o colores. Mi implementación crea un objecto Brush con dos colores. Si queremos sólo uno, asignamos a los dos el mismo valor. El ejemplo siguiente pinta un ítem con los colores agua y blanco en gradación vertical: (expondré los ejemplos con valores determinados en vez de con variables por mor de la claridad)
Dim pincel, boli As Brush Dim rec As Rectangle = e.Bounds 'Esta operación detecta si el ítem está siendo seleccionado o no. If Convert.ToBoolean(e.State And DrawItemState.Selected) = True Then 'Aquí creamos la brocha gorda pincel = New LinearGradientBrush(rec, _ Color.White, Color.Aqua, LinearGradientMode.Vertical) 'De paso creamos el Brush con el que escribiremos el texto boli = New SolidBrush(Color.Blue) Else 'Si el ítem no ha sido seleccionado, solo cabe un color: SystemBrushes.Menu pincel = SystemBrushes.Menu 'El texto, sin embargo, puede ser de cualquier color aun deseleccionado. 'SystemColors.MenuText es el predeterminado boli = New SolidBrush(SystemColors.MenuText) End If 'Y aquí damos el brochazo e.Graphics.FillRectangle(pincel, rec)Aquí está nuestra creación:
El cuadro en la pared
Cualquier imagen que quepa en un ImageList puede clavarse en un ítem. Ahora bien, sólo las que son propiamente iconos, cuya extensión es .ICO, conservan su fondo transparente. También las imágenes .GIF admiten transparencia. Si disponemos de la aplicación adecuada, podemos convertir un color determinado de una imagen .GIF en transparente, de modo que si esa misma aplicación es capaz de exportar otros formatos a .GIF, entonces ya sabemos cómo transparentar el fondo de (en principio) cualquier imagen.
Primero marcamos el punto en la pared, no en el centro sino en la esquina superior izquierda:
Dim PF As PointF = New PointF(e.Bounds.Left + 2, e.Bounds.Top + 2)e.bounds representa la superficie rectangular del item. Ya tenemos el punto de arranque, ya podemos darle al pincel.
Si hemos rescatado la imagen del control ImageList (aunque originariamente fuera un icono) o la hemos recogido del suelo de nuestro disco duro y su formato no es .ICO, invocamos DrawImage:
e.Graphics.DrawImage(ImageList1.Images(<Índice>), PF)e.Graphics.DrawImage(Image.FromFile(<Ruta del archivo>), PF)Si, en cambio, estamos capturando un archivo .ICO, entones recurrimos a DrawIcon. A diferencia de las imágenes, que se estrechan cualquiera que sea su tamaño hasta que encajan en su rectángulo, los iconos se pintan tal cual, así que, antes de clavar un icono, tenemos que asegurarnos de que cabe. La medida idónea es 16x16 pixels. Puesto que los archivos .ICO pueden contener dentro de sí la misma imagen en varios tamaños, debemos asegurarnos de que capturamos la versión 16x16.
Primero creamos un objeto Icon tomándolo de su archivo:
Dim Icono As Icon = New Icon(<Ruta del archivo .ICO>)y luego creamos otro que extrae la versión 16x16. Si no existe obtendremos no un error, sino la versión disponible más cercana:
Dim Icono16 As Icon = New Icon(Icono, New Size(16, 16))Y ya podemos clavarlo:
e.Graphics.DrawIcon(Icono16, PF.X, PF.Y)
La pared sin cuadro
Si no queremos imagen en el ítem entonces no queremos IconMenuItem, pensará mi paciente lector. También pensé yo lo mismo. Pero resulta que, como es lógico, el MenuItem no reserva espacio para imagen y comienza a escribir el texto donde el IconMenuItem ha pintado su imagen. Si juntamos un MenuItem con un IconMenuItem nos queda, por tanto, el texto sin alinear.
Solución salomónica: pintar una imagen transparente. Añadimos a nuestra clase (en realidad es un componente) un ImageList, le introducimos una imagen en blanco y cuando el código cliente construya el IconMenuItem sin referencia a imagen o icono algunos se la estampamos.
Chequéame esos ítems
Si aplicamos el valor True a la propiedad heredada Checked esperamos un pequeño check dibujado en en el ala oeste del rectángulo e.bounds. Pero como hemos cubierto todo el rectángulo de pintura, hemos tapado el check que traía de serie. Habrá que dibujar otro a mano. Junto con la imagen transparente meteremos en nuestro ImageList un icono con pinta de check, y cuando hayamos de pintar el ítem comprobaremos si el valor de la propiedad Checked es True. Y entonces asombraremos al mundo con nuestro check:
If Me.Checked = True Then e.Graphics.DrawImage(Me.ImageList1.Images(1), PF) Else etcéteraY al final era el verbo
O sea, el texto. Elegimos un tipo de letra (en el ejemplo aparece el predeterminado) y un pincel o Brush (en el ejemplo, el "boli" definido más arriba) a discreción, calculamos un punto inicial dentro de e.bounds a estribor de la imagen y escribimos con GDI+:
e.Graphics.DrawString("Abrir", Form.DefaultFont, boli, e.Bounds.Left + 25, e.Bounds.Top + 3)
Con una línea liquidado.
Si hemos definido un atajo o Shortcut mediante la propiedad Shortcut de la clase base MenuItem, entonces eso ya es otro teclear. Convertir un miembro de la enumeración Shortcut a una cadena de texto que lo represente no es trivial. Además, hay que detectar si la propiedad ShowShortcut vale True. Aquí está el código:
Private Sub GetItemText(ByRef Verbo) If MyBase.ShowShortcut And MyBase.Shortcut <> Shortcut.None Then Verbo &= " (" & _ TypeDescriptor.GetConverter(GetType(Keys)).ConvertToString(CType(MyBase.Shortcut, Keys)) _ & ")" End If End SubTypeDescriptor.GetConverter(GetType(Keys)) obtiene un Descriptor de tipo Keys. CType(MyBase.Shortcut, Keys) convierte el miembro de la enumeración Shortcut en miembro de la enumeración Keys. Esto es posible porque todas las enumeraciones son de tipo Integer (no me refiero a las que crea el programador, que admiten también Short, Byte, Long; en cualquier caso, tipos numéricos enteros). La versión de la misma línea en J# lo muestra claramente, porque en J#, al menos esa ha sido mi experiencia (toda mi experiencia en J# ha sido esta clase), hay que convertir (cast) explícitamente todo:
String s = TypeDescriptor.GetConverter(Int32.class).ConvertToString((Keys)((int)super.get_Shortcut()));En vez de GetType(Keys) Java va directamente al tipo Integer: (Int32.class), y para poder convertir un tipo Shortcut en Keys primero tiene que convertirlo a Integer (int).
En definitiva: el descriptor de tipo Keys convierte el Shortcut convertido a Keys en una cadena de texto mediante la función ConvertToString. Ya tenemos la representación textual del atajo, es cosa nuestra decidir dónde lo ponemos y/o qué hacemos con él. Lo más refinado es justificar el verbo del menú a la izquierda y el Shortcut a la derecha, como hace Microsoft. Yo he sido más bruto: mis Shortcuts van inmediatamente a continuación del verbo.
OnMeasureItem
Este método es invocado, como es lógico, antes que OnDrawItem. porque establece las medidas del rectángulo que ocupa el MenuItem. Su principal cometido es calcular la longitud del MenuItem según la longitud de su texto y el tipo de letra. Para eso está GDI+:
e.ItemWidth = _ CInt(e.Graphics.MeasureString(Verbo, <Fuente>, _ New SizeF(e.ItemWidth, e.ItemHeight)).Width) + 35el parámetro e del evento nos entrega las medidas actuales y recoge las nuevas. Y eso es todo.
El código
El servidor
Cada instancia de la clase que presento (en cuatro versiones: VBNET, C#, J# y C++NET) es un por mí llamado IconMenuItem, o sea, un item de menú "diseñado" por el usuario (owner drawn menu item). Espera del código cliente una referencia a un item que será su "cabecera", de modo que, cuando el código cliente haya terminado de definir sus valores, pueda ella solita irse a la posición del menú que le corresponda. En la introducción de este artículo tenéis el código que lleva al item a su sitio.
Y el cliente
Modo de empleo: Agítese bien el frasco antes de ... digo... arrástrese un MainMenu al formulario y déjese reposar. Almacénense imágenes en un ImageList y déjense reposar. Las cabeceras-cabeceras, o sea, Archivo, Edición, Herramientas, etc, que nunca aparecen con imágenes, y las barras separadoras son MenuItems, y como tales se añaden al menú. Los ítems con imagen y/o coloreados, que son IconMenuItems, han de seguir el proceso de creación del IconMenuItem, que consta de tres fases:
- Construcción
- Diseño = entrega de valores a las propiedades
- Colocación en el menú
He aquí un ejemplo de cada fase::
'Primero creamos la cabecerra Dim Archivo As MenuItem = MainMenu1.MenuItems.Add("Archivo") 'Y luego le añadimos ítems. 'Construcción Dim men As IconMenuItem = New IconMenuItem(Archivo, "Abrir", ImageList1.Images(0), AddressOf Abrir) 'Diseño men.Shortcut = Shortcut.Alt0 men.ColorGradiente1 = Color.Aqua men.ColorGradiente2 = Color.Snow men.ColorTextoSeleccionado = Color.Blue men.Add() 'Colocación 'Barra separadora Archivo.MenuItems.Add("-")El procedimiento de evento no necesita, obviamente, Handles, porque ya le hemos entregado al IconMenuItem un delegado cuando lo construimos: (AddressOf Abrir)
Private Sub Abrir(ByVal s As Object, ByVal e As EventArgs) Dim r As DialogResult = OpenFileDialog1.ShowDialog If r = DialogResult.OK Then MessageBox.Show(OpenFileDialog1.FileName) End SubEn los ejemplos adjuntos tenéis la clase entera en acción en VBNET, C#, J# y C++NET
Que seais dichosos
CipriPulsa aquí para descargarte el programa de ejemplo en Visual Basic .NET
Pulsa aquí para descargarte el programa de ejemplo en C#
Pulsa aquí para descargarte el programa de ejemplo en J#
Pulsa aquí para descargarte el programa de ejemplo en C++.NETNota del Guille: La versión de Visual Studio .NET usada es la 2003