Utilizar un subinforme como sustituto de un control TreeView en Access 2010
Con los problemas y vulnerabilidades cada vez mas frecuentes de los Activex, aunque existan parches para ir tirando, muchos programadores en Access han decidido finiquitar el único control del vetusto Common Controls que seguía mereciendo la pena y que carecía de una alternativa nativa: el TreeView Control. El que mas y el que menos se ha elaborado sus propias soluciones recurriendo a lo que tenían a mano en Access, en general jugando con cuadros de lista o subformularios y el resultado, variable, a veces sorprende por lo sencillo y lo eficiente. Sin embargo, no conozco a nadie que haya optado por utilizar un informe.
En Access una estructura en árbol se muestra fácilmente, de toda la vida, con un informe agrupado. Puede ser como una fotografía, mas rica, de un Control TreeView, pero es difícil imaginarse un informe que no sea completamente estático, sin capacidad de contraer o expandir las ramas. Sin embargo las versiones 2007 y 2010 de Access aportaron nuevas características, la Vista Informe en 2007 y la posibilidad de incrustar un subinforme en un formulario en 2010 que, bien utilizadas, pueden convertirse en una alternativa, mas sencilla de usar, mas potente y mas bonita que el control original.
El subinforme como alternativa al TreeView Control
Desde Access 2007, existe una nueva vista para los informes, la llamada Vista Informe que, al contrario que los informes tradicionales, no está pensada para imprimir, sino para ver en pantalla; por eso no reparte el informe en páginas, por lo que tampoco se producen los eventos Format o Print, sino que lo muestra todo seguido, y, también por eso, admite búsquedas y filtros directamente o eventos mas propios de los formularios, como Click, DblClick, keyDown, etc.
Desde Access 2010, los subInformes, en Vista Informe, se pueden incluir en un formulario exactamente igual que si fueran subformularios. Pueden ser independientes, o vinculados al formulario principal a través de las propiedades LinkMasterFields y LinkChildFields.
Con todos estos ingredientes es fácil imaginar que un informe podría sustituir con ventaja a un control TreeView. Los informes podemos vincularlos a un origen de datos de forma muy sencilla y sin escribir una sola línea de código, y podemos agrupar datos con facilidad, mostrando una estructura en ramas. Podemos usar distintos formatos de texto, colores, fondos, formatos condicionales, añadir los campos que queramos, resúmenes, totales… solo con unos clics de ratón.
Parece que en todo, en facilidad y posibilidades, superan al control TreeView y que solo les falta una cosa: La facilidad de los controles TreeView para contraer y expandir ramas. Pues bien, esto también es posible con un poco de programación, mas bien muy poco, un poco de SQL y cuidando algunos detalles del diseño, nada que no esté al alcance la la inmensa mayoría de los desarrolladores en Access.
Aquí tenemos una imagen de un ejemplo de un subinforme ejerciendo de TreeView. Puede descargase la aplicación de ejemplo en http://www.bengoechea.net/utilidades-1/reporttreeview
¿Funciona? Sí, y solo un parpadeo casi imperceptible a la hora de contraer o expandir una rama hace que no sea cien veces mejor que un control TreeView.
Aunque hay que cuidar algunos detalles en el diseño convencional del informe para que el resultado sea el que buscamos, esta parte no se aleja de la manera de diseñar un informe con grupos a la que estamos acostumbrados. Lo que tiene su pequeño truco, que necesita mas explicación, es la manera de hacer que las ramas se expandan o contraigan.
La consulta origen del informe
Un informe con grupos se basa en una única consulta en la que están contenidas todas las tablas relacionadas entre sí por campos en común. Si a estos campos en común añadiéramos uno mas de cada lado, cambiando su valor podríamos hacer que la consulta no devolviera los registros correspondientes de esa rama, si hacemos, a continuación de cambiar el valor de ese campo, un requery del informe basado en esa consulta, el efecto será el de contraer o expandir la rama.
A ver si me explico un poco mejor. Tenemos dos tablas, Género y Especie, con un campo en común idGenero; con una consulta que vincule ambas tablas mediante ese campo el resultado será una relación de todos los géneros y las especies de cada género. Pero podemos incluir un campo mas en cada tabla, bExpandir y bExpandiendo, por ejemplo, y bExpandiendo tendría siempre valor True. Si relacionamos también ambas tablas por esos campos, cambiando el campo bExpandir de la tabla Género coincidirá o no con el de la tabla Especie y, en consecuencia, se mostrarán o no las especies de ese género.
Ese planteamiento inicial obligaría a a cambiar el diseño de las tablas implicadas y a andar cambiando datos en ellas de manera poco justificada, por lo que, partiendo de esa idea, lo que hacemos es utilizar una tabla auxiliar, con el campo en común y el campo bExpandir, que interponemos entre las dos tablas relacionadas. No necesitamos modificar ni andar tocando las tablas originales, sino que escribimos en la tabla auxiliar y, en vez utilizar una tabla auxiliar para cada par de tablas relacionadas, utilizamos una sola, con distintas consultas filtradas por el nombre de la tabla del lado uno, que son las que interponemos.
Así sería la consulta original del informe que mostramos arriba si fuera estático:
O, si lo prefiere, así sería el texto SQL:
SELECT Títulos.*, Grupos.*, cuentas.* FROM (Títulos LEFT JOIN Grupos ON Títulos.ID_TITULO = Grupos.ID_TITULO) LEFT JOIN cuentas ON Grupos.ID_GRUPO = cuentas.ID_GRUPO;
Sin embargo, para poder ocultar o mostrar las ramas a voluntad, la consulta qArbolCuentas ha quedado como ésta:
SELECT Títulos.*, qExpandirTitulo.NombreTablaPadre, qExpandirTitulo.Expandir AS ExpandirTitulo, Grupos.*, qExpandirGrupo.NombreTablaPadre, qExpandirGrupo.Expandir AS ExpandirGrupo, cuentas.* FROM (((Títulos LEFT JOIN qExpandirTitulo ON Títulos.ID_TITULO = qExpandirTitulo.idPadre) LEFT JOIN Grupos ON qExpandirTitulo.idPadre = Grupos.ID_TITULO) LEFT JOIN qExpandirGrupo ON Grupos.ID_GRUPO = qExpandirGrupo.idPadre) LEFT JOIN cuentas ON qExpandirGrupo.idPadre = cuentas.ID_GRUPO;
Es decir, entre Títulos y Grupos hemos interpuesto la consulta qExpandirTitulo, y, entre Grupos y Cuentas, qExpandirGrupo. También hemos añadido los respectivos campos Expandir, que cambiamos de nombre.
Las consultas qExpandirTitulo y aExpandirGrupo, se basan ambas en una única tabla filtrada porque Expandir sea Verdadero y por un nombre, que podría se aleatorio, y que servirá luego para filtrar en la tabla que registros modificar.
SELECT tblExpandir.Id, tblExpandir.idPadre, tblExpandir.NombreTablaPadre, tblExpandir.Expandir FROM tblExpandir WHERE (((tblExpandir.NombreTablaPadre)="Grupos") AND ((tblExpandir.Expandir)=True));
SELECT tblExpandir.Id, tblExpandir.idPadre, tblExpandir.NombreTablaPadre, tblExpandir.Expandir FROM tblExpandir WHERE (((tblExpandir.NombreTablaPadre)="Títulos") AND ((tblExpandir.Expandir)=True));
Como la tabla tblExpandir está de momento vacía, ahora la consulta qArbolCuentas solo mostraría los Títulos, pues no existen los campo idPadre que se interponen entre las distintas tablas. Si le añadimos el ID de un título, lo identificamos con el nombre “Titulos” y le damos al campo Expandir valor Verdadero, se mostrarán todos los Grupos de ese Título, y si cambiamos Expandir a Falso, no se mostrarán, pues qExpandirTitulo solo devuelve los registros en que Expandir es verdadero.
Con las consultas creadas, solo nos falta crear el informe y añadirle un par de procedimientos para que busque y modifique el campo Expandir correspondiente y lo añada si no existe.
El diseño del informe
El diseño del informe es bien sencillo, aunque hay algunos detalles que no se deben dejar escapar.
Tiene su origen en la consulta qArbolCuentas, está agrupado por Titulo y por Grupo y muestra los campos Titulo, Grupo y Cuenta y, a la izquierda de los dos primeros, el valor correspondiente de Expandir y, a la derecha de cada uno de ellos, oculto, el ID correspondiente.
Debemos cuidar que las propiedad Autocomprimible, tanto de los controles que se expanden como de las secciones que los contienen, sea Sí y, además, la propiedad Alto Automático de las distintas secciones (Encabezado de Grupo y Detalle) sea también Sí.
Si los controles no se comprimen, o si no se comprime la sección que los contiene, aunque no haya contenido para ese grupo, se muestra en blanco el espacio correspondiente a ese control o a esa sección y, aparte del derroche de espacio innecesario, el resultado es menos estético.
Lo controles solo se comprimen cuando no tienen nada que mostrar y las secciones cuando todos los controles están comprimidos, así que no podemos añadir a las secciones prácticamente nada que no sean campos de texto que puedan estar vacíos; no podemos, por ejemplo, añadir casillas de verificación o botones. Tampoco podemos ocultarlos o cambiar de tamaño sobre la marcha, pues no tenemos los eventos Format y Prine
La pega que tenemos es que ExpandirGrupo y ExpandiTitulo, devuelven valores 0 y –1, aunque no haya registros que mostrar y que, además, no queremos mostrarlos así, sino como + y –.
En el campo ExpandirTitulo es mas sencillo, pues, como, al ser el tronco, siempre se mostrará, y no importa que no se comprima. Lo que hacemos con el campo ExpandirGrupo es cambiarlo por un campo calculado que devuelve un nulo si el Grupo es nulo.
=SiInm(EsNulo([Grupo]);Nulo;[ExpandirGrupo])
Y, en ambos campos, para que se muestren como + y –, lo que utilizamos es la propiedad Formato, que tiene distintas secciones, separadas por punto y coma, para valores positivos, negativos, ceros y nulos. En ambos casos, la propiedad formato es:
+;-;+
En el caso de la cuenta, no hay un campo Expandir, por lo que utilizamos un campo calculado. txtPunto, cuyo origen es:
=SiInm(EsNulo([Cuenta]);Nulo;0)
Y, como lo que queremos es que se muestre una viñeta, la propiedad Formato es:
●;●;●
Volviendo a ExpandirTitulo, y puesto que siempre muestra algún registro, no nos ha importado adornarlo un poco poniendo un botón de forma redonda y fondo transparente sobre él, para darle un resultado mas aparente
El código VBA
Lo que básicamente hace el código es que, cuando pulsamos sobre el campo ExpandirTitulo o ExpandirGrupo, que en el informe se muestran con + o –, se cambia por el contrario el valor correspondiente en la tabla tblExpandir para la tabla Titulos o Grupos y, si no existía el registro en la tabla tblExpandir, se añade. A continuación, hacemos un Requery del informe, que será lo que produzca el efecto de contraer o expandir la rama.
Private Sub ExpandirTitulo_Click() fFindEditTblexpandir Nz(Me.Títulos_ID_TITULO), "Títulos", Not Nz(Me.ExpandirTitulo) Echo False Me.Requery Echo True End Sub
Private Sub TxtExpandirGrupo_Click() fFindEditTblexpandir Nz(Me.Grupos_ID_GRUPO), "Grupos", Not Nz(Me.TxtExpandirGrupo) Echo False Me.Requery Echo True End Sub
Estos eventos llaman a un procedimiento en un módulo general que busca en la tabla tblExpandir el ID y el texto identificativo de la tabla (“Títulos” o “Grupos”) que pasamos como argumento y cambia el valor de el campo Expandir por el que también le pasamos. Si no encuentra el registro, llama a otro procedimiento que lo añade con los valores de los argumentos que hemos pasado al primero. Ninguna cosa del otro mundo, pero, para que quede mas claro, pego el código.
'--------------------------------------------------------------------------- ' Procedure : fFindEditTblexpandir ' DateTime : 11/02/13 20:33 ' Author : Chea ' Purpose : Abrir un recordset de DAO , buscar por un campo y asignar valor '--------------------------------------------------------------------------- Public Function fFindEditTblexpandir(BuscarlidPadre As Long, sNombreTablaPadre As String, bExpandir As Boolean) As Long Dim miBD As DAO.Database Dim rstblExpandir As DAO.Recordset On Error GoTo fFindEditTblexpandir_Error Set miBD = CurrentDb() Set rstblExpandir = miBD.OpenRecordset("tblExpandir", dbOpenDynaset) With rstblExpandir If Not (.EOF And .BOF) Then .FindFirst "(idPadre = " & BuscarlidPadre & ") AND ( NombreTablaPadre = '" & sNombreTablaPadre & "')" If Not .NoMatch Then .Edit !Expandir = bExpandir .Update .Bookmark = .LastModified fFindEditTblexpandir = !Id Else fAddTblexpandir BuscarlidPadre, sNombreTablaPadre, bExpandir End If Else fAddTblexpandir BuscarlidPadre, sNombreTablaPadre, bExpandir End If .Close End With Set rstblExpandir = Nothing Set miBD = Nothing fFindEditTblexpandir_Salida: On Error GoTo 0 Exit Function fFindEditTblexpandir_Error: MsgBox "Error " & Err.Number & " (" & Err.Description & ") in procedure fFindEditTblexpandir " GoTo fFindEditTblexpandir_Salida: End Function '--------------------------------------------------------------------------- ' Procedure : fAddTblexpandir ' DateTime : 11/02/13 21:11 ' Author : Chea ' Purpose : Abrir un recordset de DAO , añadir un nuevo registro y asignar valor ' : a los campos de ese registro con los valores que se pasen como parámetros. '--------------------------------------------------------------------------- Public Function fAddTblexpandir(lidPadre As Long, sNombreTablaPadre As String, bExpandir As Boolean) As Long Dim miBD As DAO.Database Dim rstblExpandir As DAO.Recordset On Error GoTo fAddTblexpandir_Error Set miBD = CurrentDb() Set rstblExpandir = miBD.OpenRecordset("tblExpandir", dbOpenDynaset) With rstblExpandir .AddNew !idPadre = lidPadre !NombreTablaPadre = sNombreTablaPadre !Expandir = bExpandir .Update .Bookmark = .LastModified fAddTblexpandir = !Id .Close End With Set rstblExpandir = Nothing Set miBD = Nothing fAddTblexpandir_Salida: On Error GoTo 0 Exit Function fAddTblexpandir_Error: MsgBox "Error " & Err.Number & " (" & Err.Description & ") in procedure fAddTblexpandir " GoTo fAddTblexpandir_Salida: End Function
Hay algo mas de código, por ejemplo, para llamar al evento ExpandirTitulo_Click, cuando pulsamos sobre el botón que cubre el campo, o para detectar pulsaciones de teclas y pasarlas al evento Click correspondiente. No lo ponemos para que quede mas claro el funcionamiento básico.
Conclusión
Los informes, usados generalmente como subinformes, son una alternativa al Control Treeview, sencilla, completamente funcional y con mas posibilidades. Estamos utilizando elementos nativos de Access y algo tan sumamente familiar como son los informes. No vamos a tener que instalar o registrar nada ajeno a Acces, ni vamos a tener nunca problemas por cambio de versiones o características obsoletas. Se necesita menos código que con un TreeView Control y el diseño gráfico de los informe facilita muchísimo la tarea.
Se pueden añadir todos los campos que se quiera, del tipo que se quiera y en la posición que se quiera. No tenemos por qué limitarnos al clásico TreeView.
Seguramente es mas lento que un TreeView y produce un ligero parpadeo cada vez que hacemos Requery (contraemos o expandimos) Este parpadeo es mas notorio cuanto mas cosas le pidamos al informe, por ejemplo, formatos condicionales, totales y en general todo lo que signifique cálculos en el informe, pues los campos afectados tardan una pizca mas el mostrarse. Pero algún precio habría que pagar.