Cómo iluminar menús en .NET

Imágenes, colores y tipos de letra para convertir un menú en una obra de arte.

Fecha: 29 Julio 2003 (06/Ago/03)
Cipriano Valdezate Sayalero
 
.

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:

No podemos personalizar:

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:

    Public Sub New()
        MyBase.New()
        MyBase.OwnerDraw = True 
    End Sub
    Protected Overrides Sub OnDrawItem(ByVal e As DrawItemEventArgs)
    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 Function

La 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 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étera

Y 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 Sub

TypeDescriptor.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) + 35

el 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:

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 Sub

En los ejemplos adjuntos tenéis la clase entera en acción en VBNET, C#, J# y C++NET

Que seais dichosos
Cipri

Pulsa 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++.NET

Nota del Guille: La versión de Visual Studio .NET usada es la 2003


ir al índice