jueves, 30 de octubre de 2014

Artículo: Activar y Desactivar Botones en Tiempo de Ejecución de la Cinta de Opciones de Excel. 2ª Parte.

Al situarnos en la plantilla se desactiva el botón.
Al situarnos en la plantilla se desactiva el botón.
Seguimos con la segunda parte del artículo dedicado a explicar como activar y desactivar botones en la Cinta de Opciones de Excel. En el artículo anterior explique el uso de la propiedad enabled y empecé a explicaros el uso de la función "getEnabled" para activar y desactivar los botones en tiempo de ejecución. Con "getEnabled" conseguimos que al abrir el documento el botón se activará o desactivará dependiendo de en que hoja estábamos situados en el momento de abrirlo. En este artículo vamos a seguir con "getEnabled" y continuaremos donde lo dejamos hasta conseguir que el botón para eliminar facturas se active o se desactive dependiendo de en que hoja nos situemos. Así ya tendremos controlado ese botón en toda las situaciones posibles que se pueden dar: Al abrir el documento y mientras estamos trabajando con él.

Parte del trabajo ya lo tenemos hecho del artículo anterior, pero nos faltan algunos "detalles". Si alguien empieza por esta parte en lugar de por el artículo anterior, le recomiendo que lo deje y que primero realice los pasos anteriores. No voy a repetir todo lo que ya hemos visto y quizás os sintáis un poco perdidos. Para seguir este artículo podéis utilizar el documento finalizado del artículo anterior o bien descargarlo en el siguiente enlace.


Entonces, nuestro objetivo es hacer algo para que cuando se cambie de una hoja a otra del documento, el botón se active o se desactive según le toque. Antes de ponernos con el trabajo os voy a intentar explicar a grandes rasgos que vamos a tener que hacer para conseguir lo que quiero, así no vais tan a ciegas con lo que os voy explicando. Como ya os comenté el control de la Ribbon desde VBA es un poco complicado y lo que vamos a ver hoy lo confirma mucho más. Ya os dije que no hay una orden tipo "botonEliminar.Enabled = False", de nuevo vamos a tener que usar la función "getEnabled" y algún que otro "truco" más. Abrimos el documento en el Custom UI Editor y os sigo comentado.

Código XML artículo anterior.
Código XML artículo anterior.

<button id="botonEliminarFactura" label="Eliminar" 
getEnabled="ModuloRibbon.ActivarDesactivarBotonEliminar" 
onAction="ModuloFacturas.EliminarFactura"/>

En el código del artículo anterior, en "getEnabled", indicamos que Excel debía llamar a un procedimiento de VBA para decidir que estado debía tener el botón, activado o desactivado. El procedimiento estaba situado en un módulo llamado "ModuloRibbon" y se llamaba "ActivarDesactivarBotonEliminar". Este procedimiento es llamado cuando se crea el botón en la Ribbon, al cargar la personalización que estamos creando, Excel se da cuenta que para mostrar este botón debe llamar a ese procedimiento y así se decidirá si está activado o no. En el artículo anterior nos aprovechamos de esto para que al abrir el documento en Excel el botón de eliminar facturas esté en el estado correcto. Como al abrir el documento se carga la personalización, Excel llama a ese procedimiento para ver como tiene que quedar el botón.

Procedimiento VBA para activar y desactivar el botón.
Procedimiento VBA para activar y desactivar el botón.

¿Que es lo que vamos hacer ahora? Bueno pues básicamente, vamos a obligar a cargar de nuevo el botón cuando cambiemos de una hoja a otra y al hacer esto, se volverá a llamar a ese procedimiento para decidir que estado debe tener el botón. Recordad que en el procedimiento "ActivarDesactivarBotonEliminar" se controlaba en que hoja del documento estábamos y en función de eso se decidía si se activaba o no el botón. Bueno pues al volver a cargar de nuevo el botón en la Ribbon, esa comprobación se volverá a realizar y se activará o desactivará el botón según la hoja en la que estemos en ese momento. Vamos a conseguir que el botón se cree de nuevo en la Ribbon.

En realidad es mucho más retorcido lo que vamos hacer, porque seguro que más de uno estará pensando... ¿Y como le vamos a decir que cargue de nuevo el botón? Bueno pues es que en realidad no le vamos a decir que cargue el botón, le vamos a decir que lo invalidamos... Y al hacerlo, Excel lo vuelve a cargar porque ya no es válido. Es decir yo no puedo cargar de nuevo un control, pero si puedo indicar que ya no es válido para que Excel se encargue de cargarlo de nuevo y así provocaremos que se llame otra vez al procedimiento que hemos indicado en "getEnabled". Este es el proceso que hemos seguido hasta ahora y el que vamos a seguir:

  • Abrimos el documento en Excel y se carga la Ribbon personalizada.
  • Al cargar la Ribbon personalizada debe crear el botón para eliminar facturas y ahí se da cuenta que para establecer el estado del botón, se llama desde "getEnabled" a una macro que hemos creado con VBA.
  • Se ejecuta esa macro y se activa o se desactiva el botón dependiendo de la hoja en la que aparecemos situados al abrir el documento.
  • A partir de aquí empieza lo nuevo. Cuando cambiemos de una hoja a otra mientras trabajamos en el documento, usaremos el evento SheetActivate para invalidar el botón de eliminar facturas. Ese evento salta cuando se activa una hoja, al cambiar de una hoja a otra se activa en la que nos situamos.
  • Al invalidar el botón, Excel lo intentará crear otra vez y de nuevo llegará a la misma conclusión. Para establecer el estado del botón hay que llamar a la macro que apunta "getEnabled" y así en función de la hoja en la que estamos en ese momento se activará o se desactivará el botón.

Esta es la idea, ya os había dicho que no es fácil y que el acceso a la Ribbon no es directo, hay que dar unas "pocas vueltas". Bueno, vamos a empezar con el trabajo de codificación. En este paso vais a tener que hacer un pequeño acto de fe, ya que el código que vamos a escribir no va tener una representación visual en la Ribbon. Tenemos que hacer una modificación a la primera línea, la línea en la que indicábamos el espacio de nombres que queríamos usar en este documento. Al final de la línea debemos añadir lo siguiente:

<customUI
xmlns="http://schemas.microsoft.com/office/2009/07/customui" 
onLoad="ModuloRibbon.AlCargarRibbon">

La parte nueva en la línea anterior es en la que usamos la función "onLoad". La idea de esta función es muy similar a lo que os he explicado sobre "getEnabled".  En ella he indicado un procedimiento de VBA, una macro, que será ejecutada cuando la personalización que estamos creando se cargue. Es decir, ahí le he indicado que cuando se cargue la personalización que está definida en el customUI que estamos creando, debe ejecutar una macro llamada “AlCargarRibbon”, que está guardada en un módulo llamado “ModuloRibbon”.
¿Por qué hacemos esto? Bueno pues porque en el interior de ese procedimiento o macro, que se ejecuta justo en el momento en el que se cargue la personalización, vamos a tener acceso a la Ribbon, a la Cinta de Opciones…  Y eso nos va permitir hacer algo que después nos facilitará el acceso continuo a la Ribbon, para poder hacer con ella lo que necesitemos en cualquier momento. En este caso concreto activar o desactivar el botón a nuestro gusto. Ya se que suena muy raro pero tenéis que hacer unos pocos actos de fe en este artículo.

Ya está, esto es todo lo que teníamos que hacer en el Custom UI Editor, como muy bien os imaginaréis, ahora el siguiente paso es ir a Excel y desarrollar la macro que acabamos de indicar en “onLoad”.



Todo el trabajo que ves aquí te lo doy de forma gratuita. Si te gusta mucho y quieres que continúe publicando más material, puedes apoyarme con una donación para que cada vez pueda dedicar más tiempo a este proyecto. Cualquier cantidad desde 1€/1$ es bienvenida. Gracias!!!!!!!!!!!!!!


Cerramos el Custom UI Editor guardando los cambios y abrimos el documento en Excel. Si se os abre el documento en la vista protegida y con las macros deshabilitadas. Habilitáis las dos cosas, la edición y las macros. Cuando abráis el documento os aparecerá el siguiente aviso:

No encuentra la macro que hemos indicado en "onLoad".
No encuentra la macro que hemos indicado en "onLoad".

En él nos dice que no encuentra la macro “AlCargarRibbon” o que puede que estén deshabilitadas las macros. En nuestro caso el problema es evidente, todavía no hemos creado la macro, ahora íbamos a realizar ese paso. Fijaros que ya podemos ver que todo funciona como os he dicho, al abrir el documento, intenta cargar la personalización y ejecutar la macro que hemos indicado en “onLoad”. Pero bueno, como no la tenemos, aceptamos y vamos a crearla. Accedemos al editor de VB pulsando en el botón “Visual Basic” de la ficha “Desarrollador” o la combinación de teclas “Alt + F11”. Y ahí, entre otras cosas ya podemos ver el módulo en el que vamos a tener que crear la macro.

Botón para acceder al Editor de Visual Basic.
Botón para acceder al Editor de Visual Basic.

Accedemos al Editor de Visual Basic.
Accedemos al Editor de Visual Basic.

Dentro del módulo tenemos definido el procedimiento al que se llamaba desde la función “getEnabled”, el que realmente activa o desactivaba el botón. Como el módulo ya lo tenemos creado del artículo anterior sólo nos queda crear el procedimiento o macro para “onLoad” y definir una variable global que ahora os voy a explicar para que sirve. Para empezar, justo antes del código que ya tenemos escrito, escribimos la siguiente línea para crear una variable global:

Public MiRibbon As IRibbonUI


Declaramos una variable global para acceder a la Ribbon.
Declaramos una variable global para acceder a la Ribbon.

En esta línea estamos definiendo una variable global, una variable que puede ser accedida desde cualquier parte del proyecto. Con Public la hemos declarado pública y al estar declarada en un módulo es accesible para todo el proyecto. Esta variable es de tipo IRibbonUI, es decir del tipo Ribbon, del tipo Cinta de Opciones.

¿Para qué vamos a utilizar esta variable? Bueno pues nos va servir para poder acceder en todo momento, desde cualquier sitio a la Ribbon. Si os acordáis, hace un momento, os he dicho que el “truco” para conseguir crear otra vez el botón iba ser invalidarlo para que Excel lo cree de nuevo. Bueno pues eso, lo vamos a poder hacer a través de esta variable. Lo que pasa, que tal y como la tenemos ahora, esta variable no sirve para nada. Es una variable de tipo Ribbon pero ahora mismo no apunta a la Ribbon de nuestro documento, ahora mismo es igual a nada. Vamos a solucionarlo, justos después de la línea que acabamos de escribir, definimos otro procedimiento, escribimos lo siguiente:

Sub AlCargarRibbon(Ribbon As IRibbonUI)
    Set MiRibbon = Ribbon
End Sub

Código de la Macro que se llama en onLoad.
Código de la Macro que se llama en onLoad.

¿Qué tenemos aquí? Bueno pues tenemos la macro que hemos indicado en el código XML, la macro que debe ejecutarse al cargar la personalización de la interface. Por supuesto tiene el mismo nombre que hemos indicado en el código XML y entre los paréntesis recibe un argumento del tipo IRibbonUI, del tipo Ribbon o Cinta de Opciones. Ese argumento es obligatorio, las macros que se deben ejecutar desde el “onLoad” de un “customUI” deben tener esta estructura, deben recibir ese argumento.

¿Y que es lo que hacemos en este procedimiento? Bueno, pues es muy parecido a lo que pasaba con “getEnabled”. Este procedimiento recibe un argumento llamado Ribbon del tipo IRibbonUI. Con él , se nos ofrece la posibilidad de acceder a la “Ribbon” del documento en el momento de cargar la personalización, se trata de una puerta de acceso que nos permitirá manejar la Ribbon desde VBA. En realidad es una puerta muy limitada que nos va permitir hacer tres o cuatro operaciones con la Ribbon, pero es una especie de puente hacia la Ribbon. ¿Y que hacemos con ese puente dentro de este procedimiento?

Bueno pues esta muy bien tenerlo, pero el problema es que ese puente sólo se puede utilizar dentro de esta macro, dentro de el procedimiento “AlCargarRibbon”. Las variables o argumentos solo son usables en el ámbito en el que han sido creadas y ese argumento sólo se puede usar dentro de esa macro. Nosotros necesitamos un puente que podamos usar cuando se cambie de una hoja a otra, un puente que podamos usar en cualquier momento. Bueno pues para eso hemos creado la variable global MiRibbon. Dentro de esta macro, usando la instrucción Set, asignamos a MiRibbon el argumento Ribbon. ¿Y que es lo que conseguimos con esto? Pues conseguimos que la variable MiRibbon se convierta en otra puerta de acceso a la Cinta de Opciones, pero en este caso una puerta que podemos usar desde cualquier parte del proyecto y en cualquier momento porque ese puente se ha creado en una variable global a todo el proyecto.

Este tema ya lo comenté en el artículo anterior está muy relacionado con los punteros. Un puntero es una variable que en su interior no tiene un valor, por ejemplo no tiene un número para sumar. Tiene una direción de memoria, tiene identificado el sitio de la memoria del equipo en el que hay guardado algo que nos interesa. Bueno, pues aquí es más o menos lo mismo. El argumento Ribbon de la macro “AlCargarRibbon” me indica donde está la Cinta de Opciones del documento, me indica una puerta o dirección de acceso. Asignando esa puerta o dirección a la variable MiRibbon creamos una nueva puerta de acceso pero que en este caso va ser accesible desde cualquier parte del documento, del proyecto. Ya se que esto es un poco raro, pero cuando lo tengamos finalizado va quedar mucho más claro. Bueno, de momento con esta macro hemos acabado.

Recapitulemos. Ya tenemos una macro que decide si el botón se activa o se desactiva, ya tenemos una puerta de acceso a la Ribbon para indicar que queremos desactivar un botón de su interior, ¿Qué nos falta? Bueno pues nos falta indicarle a través de esa puerta que el botón debe volver a cargarse porque quizás halla que cambiar su estado, activarlo o desactivarlo. ¿Cómo hacemos esto?

Tenemos que pensar en que momento debería cambiar el estado del botón. Os he dicho que si estamos en la hoja de la plantilla se debe desactivar y cuando estamos en una hoja de una factura guardada se de activar… Por lo tanto cuando cambiemos de una hoja a otra debemos indicarle que compruebe si hay que activar o desactivar el botón. ¿Qué vamos a utilizar para esto? Bueno pues los documentos de Excel tienen un evento perfecto para esto, tienen el evento “SheetActivate” que se produce al activar una hoja. Es perfecto porque al cambiar a una hoja, activamos esa hoja y se produce el evento. Para localizar ese evento debemos acceder al objeto “ThisWorkbook” en el “Editor de Visual Basic”, tenemos que hacer un doble clic sobre él para conseguirlo.

Accedemos a ThisWorkbook.
Accedemos a ThisWorkbook.

La parte derecha del editor se quedará en blanco ya que ese objeto todavía no tiene código asociado. Ahora debemos mostrar el evento "SheetActivate" para escribir código en su interior. Para hacerlo primero le indicamos que queremos acceder a los eventos del objeto “Workbook” que contiene “ThisWorkbook”. Esto provocará que se cree el evento “Open”, este evento es interesante cuando queremos que se ejecute código al abrir un documento de Excel, pero no es nuestro caso. En la lista que tenemos situada a la derecha de la que acabamos de usar, tenemos disponibles todos los eventos que tiene “Worbook”, ahí debemos buscar el evento “SheetChange”.

Mostramos el evento SheetActivate.
Mostramos el evento SheetActivate.


Al seleccionarlo aparecer la estructura del evento y en su interior es donde debemos escribir algo. El evento “Open” lo podéis dejar o borrar, da igual, no lo vamos a usar. Escribimos lo siguiente dentro de “SheetChange”.

MiRibbon.InvalidateControl ("botonEliminarFactura")

Código para invalidar el botón de la Ribbon.
Código para invalidar el botón de la Ribbon.

Esta línea es la clave de todo el artículo, en ella invalidamos el botón para eliminar facturas. A través de la variable que nos sirve de puente hacia la Ribbon usamos el método "InvalidateControl" para invalidar el control que nos interesa. Fijaros que como argumento de ese método pasamos el "ID" del control que queremos invalidar, el "ID" que hemos definido en el código XML. Unos artículos atrás os comenté que ese "ID" nos sería útil, bueno pues ahí tenemos el primer ejemplo. Cada vez que se active una hoja, con esta línea provocamos la invalidez del botón y con eso provocamos que se vuelva a llamar a la macro de "getEnabled" para establecer el estado del botón, cuando se vuelva a crear. Dependiendo de que hoja se active, el botón se activará o se desactivará. Bueno, pues ya está solo queda ver si funciona, cerramos el documento y lo abrimos de nuevo.

Comprobamos como se desactiva y activa el botón.
Comprobamos como se desactiva y activa el botón.

Bueno, pues como podéis comprobar funciona. Dependiendo de la hoja en la que nos situamos el botón se activa o se desactiva. A costado pero ya lo tenemos, antes de finalizar os voy a dar dos detalles más. Para empezar os cuento que si lo necesitáis se puede invalidar la Ribbon completa en lugar de un control determinado. Colocando esta línea en lugar de la anterior conseguiríamos el mismo resultado, pero es que en este caso no hace falta invalidar toda la Ribbon.

MiRibbon.Invalidate

"Invalidate" es un método de los objets IRibbonUI que permite invalidar toda la Ribbon y así provocar que se crea entera de nuevo. El otro detalle también tiene que ver con el código que estamos metiendo dentro del evento "SheetActivate". Las variables globales como la que estamos usando aquí tienen un problema, si se produce un error en tiempo de ejecución pierden su valor, es decir perderíamos el puente de acceso que teníamos a la Ribbon. Si nos recuperamos de un error así lo mejor sería cerrar el documento y abrirlo de nuevo. Si no lo hacéis y vais cambiando de una hoja a otra provocaréis nuevos errores en tiempo de ejecución porque ya no tenemos abierto el "puente" a la Ribbon, la variable "MiRibbon" ha perdido su valor.

Error en tiempo de ejecución porque la variable MiRibbon a perdido el enlace con la Cinta de Opciones.
Error en tiempo de ejecución porque la variable MiRibbon a perdido el enlace con la Cinta de Opciones.


Para evitarlo deberíamos controlar si la variable global sigue siendo válida, si esa variable tiene como valor "Nothing" significaría que ya no es válida para acceder a la Ribbon. Añadiendo estas líneas tendríamos controlado ese detalle.

Private Sub Workbook_SheetActivate(ByVal Sh As Object)
    If Not MiRibbon Is Nothing Then
        MiRibbon.InvalidateControl ("botonEliminarFactura")
    End If
End Sub

Con el condicional le indicamos que controle si la variable MiRibbon no es igual a "Nothing". Si eso se cumple significa que la variable tiene en su interior la puerta de acceso a la Ribbon y debe invalidar el botón. Si fuera igual a "Nothing" significaría que la variable esta vacía, no hay nada y no nos serviría para invalidar el botón.

Bueno, pues ahora sí, ya está. Seguro que quedan cientos de cosas para controlar pero eso ya es trabajo para cada uno de vosotros. Mi objetivo era enseñaros el sistema que tenemos para acceder a la Ribbon desde VBA y en este caso aprender como activar o desactivar botones. El "truco" para poder conseguir ese resultado ha sido la posibilidad de invalidar controles o incluso la Ribbon entera y de esa manera provocar otra vez la creación de controles. Este sistema, como veremos más adelante, es el que usaremos para modificar otras características de los controles en tiempo de ejecución. Pero de eso ya hablaremos...

*Siguiente Artículo:*:

3 comentarios:

  1. Saludos, Excelente de nuevo.

    Quisiera saber si hay algun manual que me permita conocer y explorar más a fondo el XML para modificar la UI de software como Excel y/o el uso conjunto con VBA.

    Muchas gracias. Eres excelente

    ResponderEliminar
    Respuestas
    1. Pues la verdad es que no hay mucho, estos son los dos que yo conozco, no se si hay más. Están en Inglés:

      RibbonX for DUMmIES
      RibbonX Customizing the Office 2007 Ribbon

      Saludos

      Eliminar
    2. Muchas gracias, empezaré a explorar.

      Saludos.

      Eliminar