martes, 27 de marzo de 2018

VBA: Crear un índice de hojas con autoformas

Veremos en el post de hoy como generar en Excel un índice de todas las hojas que existan en nuestro libro de trabajo, pero en este caso asociándolo a unas Autoformas...
Tal como se ve en la imagen siguiente:

VBA: Crear un índice de hojas con autoformas



Esta personalización no permitirá crear rápidamente un índice de hojas empleando nuestras autoformas favoritas, con las características de formato de forma que deseemos...
Importante!, el índice se genera en una hoja que he llamado 'Menu'.


En un módulo estándar del libro incluimos el siguiente procedimiento:

Sub CreaIndice_con_AutoForma()
Dim wks As Worksheet

alt = 0
For Each sh In Sheets
    If sh.Name <> "Menu" Then
        Set wks = Worksheets("Menu")
        Dim miForma As Shape
        'definimos la forma de Rectángulo redondeado
        ' .AddShape ( Tipo , izquierda , arriba , ancho , alto )
        Set miForma = wks.Shapes.AddShape(msoShapeRoundedRectangle, 5, 10 + alt, 75, 50)
        alt = alt + 60
        'añadimos como texto el nombre de la hoja destino/ en negrita
        With miForma.TextFrame.Characters
            .Text = "Ir a " & sh.Name
            .Font.Bold = True
        End With
        'alineamos texto centrado vertical/horizontal
         With miForma
            .TextFrame2.VerticalAnchor = msoAnchorMiddle
            .TextFrame2.TextRange.ParagraphFormat.Alignment = msoAlignCenter
        End With
        'carácterísticas 3D de la forma con Bisel circular
        miForma.ThreeD.BevelTopType = msoBevelCircle
        'y del color de fondo
        miForma.Fill.ForeColor.RGB = RGB(197, 90, 17)
        'y también efecto de luminosidad
        With miForma.Glow
            .Color.RGB = RGB(197, 90, 17)
            .Transparency = 0.6
            .Radius = 8
        End With
        
        ' y finalmente añadimos la funcionalidad de hipervínculo a la Autoforma
        wks.Hyperlinks.Add Anchor:=miForma, _
                address:="", _
                SubAddress:=sh.Name & "!" & Cells(1, 1).address
    End If
Next sh

End Sub



El resultado se visualiza rápidamente.

Se observa como el orden de las hojas es el mismo en el que aparecen dispuestas en el libro...

jueves, 22 de marzo de 2018

Construir un ordinal secuencial

Días atrás un usuario planteaba una cuestión interesante a través de un comentario.
Preguntaba por la forma de generar un ordinal secuencial tipo índice (apartados, subapartados, etc).
[...]Se trata de rellenar una serie en celdas vacias de la siguiente forma:
en celda A1 tengo 1.1 y en A2 rellene hacia abajo las celdas vacias con 1.1.1, 1.1.2, 1.1.3, etc; asi hasta que encuentre la proxima celda con 1.2 y rellene con 1.2.1, 1.2.2, 1.2.3, etc.
Es un archivo como con 4000 lineas que hay que rellenar de esa forma[...]


La idea que se me ocurrió fue hacerlo sin programación, buscando una fórmula que lograse este objetivo.. y aquí la muestro.

Sobre la siguiente distribución de datos en la columna A:

Construir un ordinal secuencial



Para agilizar el trabajo tras seleccionar el rango completo A1:A26 nos hemos ido a la opción Ir a Especial y luego seleccionar celdas en blanco.
En la celda activa A2 escribimos la fórmula buscada:
=IZQUIERDA(A1;SI(LARGO(A1)-LARGO(SUSTITUIR(A1;".";""))>1;ENCONTRAR(".";A1;ENCONTRAR(".";A1)+1)-1;LARGO(A1)))&"."&CONTAR.SI($A$1:A1;IZQUIERDA(A1;SI(LARGO(A1)-LARGO(SUSTITUIR(A1;".";""))>1;ENCONTRAR(".";A1;ENCONTRAR(".";A1)+1)-1;LARGO(A1)))&"*")


Donde obtenemos:

Construir un ordinal secuencial



La fórmula está dividida en dos partes.
Primera:
IZQUIERDA(A1;SI(LARGO(A1)-LARGO(SUSTITUIR(A1;".";""))>1;ENCONTRAR(".";A1;ENCONTRAR(".";A1)+1)-1;LARGO(A1)))

que nos devuelve, tras contar el número de puntos en el valor de celda anterior (en A1), los caracteres por la izquierda hasta ese punto.. de tal forma que mantenemos el cuerpo básico de cada elemento (1.100, 13.2, 1.13 ó 9876.345)

La segunda parte, bastante similar:
CONTAR.SI($A$1:A1;IZQUIERDA(A1;SI(LARGO(A1)-LARGO(SUSTITUIR(A1;".";""))>1;ENCONTRAR(".";A1;ENCONTRAR(".";A1)+1)-1;LARGO(A1)))&"*")

contabiliza las coincidencias previas con ese patrón básico (1.100, 13.2, 1.13 ó 9876.345), lo que nos retornará el creciente 1,2,3,4, etc. para cada elemento principal.


Tras ejecutar nuestra fórmula podemos comprobar el éxito de la operación.

martes, 20 de marzo de 2018

VBA: Verificar Referencias en un Proyecto VB

Cuántas veces hemos visto/sufrido que al compartir ficheros con macros con compañeros con la suite de Office de diferentes versiones y/o necesidades, libros de trabajo que ayer funcionaban perfectamente, hoy depuran errores simples donde antes no lo hacían... o sencillamente salta un error tipo: No se puede encontrar proyecto o librería.
Es el gran problemas de los fallos con las Referencias de los proyectos VB.

Normalmente, cuando la experiencia te lo indica, accedemos manualmente al proyecto de VB > menú Herramientas > Referencias y buscamos librerías con errores (FALTA-MISSING o cualquier otro mensaje extraño).
Manualmente desmarcamos esa librería y aceptamos.. para posteriormente marcar la que entendamos debía estar seleccionada...
Recuerda que habitualmente son temas de versión.


Hoy veremos una alternativa mediante programación para verificar que todo está correcto.
MUY importante que la librería:
Microsoft Visual Basic for Applications Extensibility 5.3 library
esté habilitada.


Igualmente importante, para este caso y siempre en general, es que esté habilitada la opción de Excel:
Confiar en el acceso al modelo de objetos de proyectos VB
que se encuentra en el centro de confianza.

VBA: Verificar Referencias en un Proyecto VB


Puedes activarlo rápidamente desde la ficha Programador > grupo Código > botón Seguridad Macros

En un módulo estándar del libro incluimos el siguiente procedimiento:

Sub CheckReferencias()
Dim vbProj As VBProject
Dim Refs As Reference
'TRabajamos sobre el Proyecto de VB de nestro libro de trabajo
Set vbProj = ThisWorkbook.VBProject
'recorremos las referencias/librerías existentes
For Each Refs In vbProj.References
   'Verificamos que no haya referencias en FALTA (o rotas)
   If Refs.IsBroken = False Then
      'llevamos la info a la ventana de inmediato
      Debug.Print Refs.Name & vbTab & Refs.Description
      'quita referencia si está rota
      'vbProj.References.Remove Refs
      'carga nueva referencia
      'vbProj.References.AddFromFile "E:\Excelforo\TestFiles\RefMe.dot"
   End If
Next
Debug.Print "Fin recorrido"
End Sub



El resultado en mi caso dispuesto en la ventana de inmediato:

VBA: Verificar Referencias en un Proyecto VB

jueves, 15 de marzo de 2018

Personalizar el rango de entrada en un control de formulario

Hoy aprenderemos un truco para personalizar el rango de entrada de un control de formulario en nuestras hoja de cálculo, sin programación, para que condicione qué rango de elementos se mostrará en el cuadro combinado.

La idea es que nuestro cuadro combinado (control de formulario) muestre una lista de elementos u otra según una condición de una tercera celda:

Personalizar el rango de entrada en un control de formulario



Por tanto, si en la celda F2 de nuestra hoja marcamos una X el desplegable mostrará las ciudades del Listado1, en cualquier otro caso las ciudades del Listado2.

La clave para el éxito de esta tarea es emplear Nombres definidos.
Estos son los nombres definidos a emplear:
CiudadES =Hoja1!$A$2:$A$5
CiudadFR =Hoja1!$C$2:$C$4
selecciona =SI(Hoja1!$F$2="x";CiudadES;CiudadFR)


Fundamental el tercer nombre, al que le hemos asociado una fórmula que emplea la función condicional SI que nos devolverá un rango operable según la condición de la celda F2.


El siguiente paso, y último, consiste en acceder sobre el control (clic derecho) y entrar en el Formato del control par modificar el Rango de entrada y añadirle el nombre definido:
=selecciona

Personalizar el rango de entrada en un control de formulario



Listo... podemos comprobar que al cambiar la cela F2 el desplegable ofrece una u otra lista de ciudades tal como queríamos.

martes, 13 de marzo de 2018

VBA: Abrir un fichero con nombre aproximado

Hace algunos días un usuario planteaba una cuestión interesante a través de un comentario.
Preguntaba por la manera de abrir un fichero con macros sabiendo únicamente el comienzo del nombre del fichero (y su ubicación).
[...]necesito que una macro me abra un archivo que empieza por un texto fijo pero el final va cambiando.
Por ejemplo, tengo un archivo que se llama artículos genéricos, pero luego puede llevar un número que va cambiando.
¿Hay alguna forma de decirle al programa que abra el documento que empieza por "artículos genéricos*"?[...]


La solución la encontramos en el uso de los comodines, el asterisco en este caso... que nos servirá para componer el nombre del fichero a buscar.

Así pues añadimos la siguientes macro dentro de la ventana de código de un módulo estándar en nuestro proyecto de VB:

Sub Abrir_Workboook_con_Nombre_Parecido()
Dim WB As Workbook
Dim miRuta As String, miFichero As String
miRuta = "E:\excelforo\"
'indicamos cuál es el nombre aproximado del fichero
'emplamos el comodín *
miFichero = Dir(miRuta & "*Scenarios.xlsm")

'lanzamos rutinas si hemos indicado algún fichero
Do While miFichero <> ""
    Application.ScreenUpdating = False
    'abrimos....
    Set WB = Workbooks.Open(miRuta & miFichero)
    nombre = WB.Name
    'y pedimos confirmación al usuario de que es el correcto...
    If MsgBox("Seguro que es este libro el que quieres abrir?" & vbTab & nombre, vbYesNo) = vbYes Then
        'si es correcto nos vamos a la etiqueta 'continua'
        'y realizamos las acciones que deseemos sobre el libro abierto
        GoTo continua
    Else
        'si no es el libro deseado lo cerramos
        WB.Close
    End If
    'Para obtener más nombres de archivo que coincidan con la Ruta,
    'volveremos a llamar a la función Dir sin argumentos
    miFichero = Dir()
Loop
Exit Sub

continua:
'aquí trabajaríamos con el libro abierto...

'y al terminar
'cerramos el libro
WB.Close
MsgBox "Fichero encontrado... y cerrado " & nombre

End Sub



Se observa el uso del comodín en la definición del nombre del fichero:
miFichero = Dir(miRuta & "*Scenarios.xlsm")

donde se indica que el fichero termina con la palabra 'Scenarios.xlsm'

Con lo que se soluciona la duda planteada.

jueves, 8 de marzo de 2018

Power Query: Ordenar en descendente en base a un segundo criterio

Recientemente trabajando con un cliente necesitábamos obtener una lista de países ordenados en base a unos importes, obtenidos a partir de un listado principal.
A partir de esa necesidad, que tuvimos que solventar con funciones por razones que ahora no vienen al caso, encontré la siguiente solución empleando Pwer Query / Obtener y transformar.


A partir de la siguiente tabla de países con diferentes importes facturaciones necesitamos recuperar un listado de países únicos pero ordenados en sentido descendente (de mayor a menos) de acuerdo a sus acumulados de facturación.

Nuestro listado:

Power Query: Ordenar en descendente en base a un segundo criterio



Nuestro primer paso es cargar al complemento nuestra tabla. Desde la ficha Datos > grupo Obtener y transformar > botón Desde una tabla.

Una vez cargada la tabla comenzamos a editarla...


En la ventana del Editor de Consultas iremos a la ficha Transformar > grupo Tabla > botón Agrupar por, lo que nos abrirá la siguiente ventana que nos permitirá agrupar por cada país, de manera única, acumulando/sumando sus importes correspondientes:

Power Query: Ordenar en descendente en base a un segundo criterio



Al indicar que queremos agrupar por 'País', sumando los importes del campo 'Facturación' obtenemos la siguiente tabla...

Power Query: Ordenar en descendente en base a un segundo criterio



El siguiente paso es simple, sobre el campo 'Acumulado' forzamos una regla de ordenación en descendente. Basta presionar el desplegable del campo en cuestión y marcar 'Ordenar descendente'

Power Query: Ordenar en descendente en base a un segundo criterio



Paso siguiente: Quitar la columna de acumulado.
Puesto que solo nos interesa recuperar el listado de países, nos situamos encima del campo a eliminar (en nuestro caso el campo 'Acumulado') y seleccionamos 'Quitar columna'

Power Query: Ordenar en descendente en base a un segundo criterio



Hemos acabado la configuración de la consulta... Solo nos queda Cerrar y cargar en... nuestra hoja de cálculo:

Power Query: Ordenar en descendente en base a un segundo criterio



Para vuestra verificación he añadido (solo a efectos de check!) una tabla dinámica ordenada por país de acuerdo a su importe...

Con lo que llegamos a nuestra meta. Obtener un listado de países ordenados en sentido descendente de acuerdo al acumulado de sus importes.

martes, 6 de marzo de 2018

Power Pivot: Relacionando diferentes tablas

Haremos uso de la herramienta de gestión de base de datos que tenemos a nuestra disposición dentro del entorno de Excel: Power Pivot.

Nuestro punto de partida será una tabla principal (llamada 'TblDatos') con información de Fechas - Producto - Unidades.
Por otro lado tenemos otras dos tablas auxiliares con información de Producto(código/Referencia) - Precio.
Las dos tablas se llaman: 'TblPzs_1' y 'TblPzs_2':

Power Pivot: Relacionando diferentes tablas



El objetivo será relacionar las diferentes tablas entre sí, para mostrar en un informe de tabla dinámica la información cruzada por producto y precio.

Nuestro trabajo consiste en cargar las tres tablas en el complemento Power Pivot, para ello accedemos a la ficha Power Pivot > grupo Tablas > botón Agregar al modelo de datos.
Repetimos el paso para las tres tablas.


En el siguiente paso entramos en el Administrador de Power Pivot: ficha Power Pivot > grupo Modelo de datos > botón Administrar

Power Pivot: Relacionando diferentes tablas



En la ventana del Administrador de Power Pivot iremos a la ficha Inicio > grupo Ver > botón Vista de diagrama.
Veremos nuestras tres tablas cargadas.
Ahora solo tenemos que arrastrar los campos asociados entre las tres tablas... esto es, el campo 'Producto' de la tabla 'TblDatos' hacia el campo 'Código' de la tabla 'TblPzs_1'; y de igual forma, el campo 'Producto' de la tabla 'TblDatos' hacia el campo 'Referencia' de la tabla 'TblPzs_2'.
Veremos nuestras relaciones entre tablas como sigue:

Power Pivot: Relacionando diferentes tablas



Ahora volveremos a la vista de datos (ficha Inicio > grupo Ver > botón Vista de datos).

Accedemos a la 'TblDatos' y agregaremos dos columnas calculadas.
Una primera que llamaré 'Precios' con la siguiente fórmula:
=RELATED(TblPzs_1[Precio])+RELATED(TblPzs_s[precio])

La función RELATED dentro de Power Pivot permite recupera información de campos de otras tablas con las que exista una relación previa.

y una segunda más sencilla que llamaré 'Total' como producto de unidades por precios, con fórmula:
=[Unidades]*[Precios]

Power Pivot: Relacionando diferentes tablas



Por último creamos la tabla dinámica en la hoja de cálculo.
Navegamos, en la venta del administrador de Power Pivot, en la ficha inicio > botón Tabla dinámica > desplegable Crear tabla dinámica

Power Pivot: Relacionando diferentes tablas



Solo nos queda configurar el diseño de la tabla dinámica mostrada en la hoja de cálculo.
Para ello arrastraremos los campos 'Producto' y 'Precios' al área de filas, y los campos 'Unidades' y 'Total' al área de valores resumidos por Suma.

Power Pivot: Relacionando diferentes tablas



Listos.... hemos conseguido nuestro objetivo. A partir de tres tablas tenemos una única tabla dinámica que nos muestra para cada producto el precio correspondiente y el sumatorio acumulado de su importe (unidades x precio).

jueves, 1 de marzo de 2018

VBA: El objeto .Dictionary

Existen en VBA para Excel varios y diferentes métodos para almacenar nuestra información.
Dos de los más frecuentes son:
1- las Array (las matrices) ya sean simples o multidimensionales
2- las Collection
3- otras (en ListBox, ComboBox, etc.)

Lo que veremos hoy es el objeto .Dictionary, posiblemente más potente que todas las demás... siempre dependiendo de qué necesitemos.

Una característica importante de un Diccionario es el uso de claves únicas (ideal para crear listas de elementos únicos), esto es, no es necesario buscar un elemento mediante un 'bucle' o mediante el uso de una función 'application.match'. Sin duda, es una gran ventaja de un diccionario en comparación con una matriz.

Por otra parte, la forma en que el Diccionario almacena elementos es comparable al objeto Colección.
El Diccionario tiene algunas propiedades-funciones-métodos como .keys, .items, .removeall o .count entre otras, que pueden ser ventajosas en comparación con el uso de una Colección.


Ya que hablamos de claves únicas es importante conocer la propiedad .CompareMode de los 'Diccionarios' que nos permiten discriminar mayúsculas de minúsculas:

MiDiccionario.CompareMode = vbBinaryCompare    'diferencia entre mayúsculas y minúsculas
MiDiccionario.CompareMode = vbTextCompare      'NO diferencia entre mayúsculas y minúsculas


el valor por defecto es 0 ó vbBinaryCompare, es decir sí diferencia...


El primer paso será crear el Dictionario. Mi forma preferida es:

Dim MiDiccionario As Object
Set MiDiccionario = CreateObject("Scripting.Dictionary")



Comenzaremos 'cargando' de datos nuestro Diccionario.
Podemos conseguirlo con dos métodos: .Add o .Item
Veamos el uso indistinto para cargar el mismo dato (ya con el objeto creado):

MiDiccionario.Add Key:="Uno" , Item:="Excel"

o también

MiDiccionario.Item("Uno") = "Excel"


Siempre que el elemento (Item) no existe se agregará al 'Diccionario'; sin embargo hay una diferencia entre los dos métodos.
El método .Add actúa igual que una Collection, por tanto si el elemento existe se generará un error.
Por ejemplo:

Sub Diccionarios_1()
Dim MiDiccionario As Object
Set MiDiccionario = CreateObject("Scripting.Dictionary")

MiDiccionario.Add Key:="Uno", Item:="Excel"
MiDiccionario.Add Key:="Uno", Item:="Excelforo"

End Sub


Al ejecutarla comprobamos nos devuelve el mensaje de error 457, donde nos indica que la 'clave' ya está asociada con un elemento de la colección.

Sin embargo al usar el segundo de las formas, con Item:

Sub Diccionarios_2()
Dim MiDiccionario As Object
Set MiDiccionario = CreateObject("Scripting.Dictionary")

MiDiccionario.Item("Uno") = "Excel"
MiDiccionario.Item("Uno") = "Excelforo"

MsgBox MiDiccionario.Item("Uno")
End Sub


Al ejecutar la macro vemos que se ha cargado sin error, manteniendo el último elemento cargado!!

Una alternativa, empleado igualmente el método .Item es cargar a una variable el elemento:

Sub Diccionarios_3()
Dim MiDiccionario As Object
Set MiDiccionario = CreateObject("Scripting.Dictionary")

y = MiDiccionario.Item("Uno")
y = MiDiccionario.Item("uno")

MsgBox Join(MiDiccionario.keys, vbLf)
End Sub



Un ejemplo sería obtener un listado de países únicos (independientemente de cómo aparezcan-en mayúscula o minúscula):

Sub Diccionarios_Unicos()
Dim MiDiccionario As Object
Set MiDiccionario = CreateObject("Scripting.Dictionary")

'por defecto distingue mayúsculas de minúsculas...
'per cambiando la propiedad .CompareMode conseguimos no discrimine
MiDiccionario.comparemode = vbTextCompare

For Each elto In Array("España", "ESPAÑA", "México", "MÉXICO", "méxico", "Colombia")
    y = MiDiccionario.Item(elto)
Next elto

'países únicos
With MiDiccionario
    MsgBox "Num eltos: " & .Count & vbCrLf & _
        Join(.Keys, vbLf)
End With
End Sub

Al ejecutar la macro vemos en nuestra ventana emergente el listado de tres países: 'España', 'México' y 'Colombia'
Esto es, de los primeros elementos cargados.


Otro método interesante de un 'Diccionario' es el método .Exists con el que verificamos la existencia de una clave en el diccionario creado...
Veamos un ejemplo:

Sub CheckPais()
Dim MiDiccionario As Object
Set MiDiccionario = CreateObject("Scripting.Dictionary")

With MiDiccionario
    'para que no discrimine mayusculas ni minúsculas
    .CompareMode = vbTextCompare
    
    ' añadimos tres países al diccionario
    .Add Key:="España", Item:=1000
    .Add Key:="México", Item:=2000
    .Add Key:="Colombia", Item:=3000
    
    Dim sPais As String
    'Preguntamsoo al usuario por un país
    sPais = InputBox("Introduce un país...")
    
    'empleamos el método .Exists
    'para determinar si existe o  no el pais introducido
    If .Exists(sPais) Then
        MsgBox sPais & " ya existe, y su valor es " & MiDiccionario(sPais)
    Else
        MsgBox sPais & " no existe."
    End If
End With
Set MiDiccionario = Nothing
End Sub



Dos último métodos que veremos son: .Remove y .RemoveAll
El primero .Remove elimina u elemento concreto que indiquemos, así como su clave.
El segundo limpia todo el 'Diccionario', pero dejando activo el objeto.

Sub EliminarElementos()

Dim MiDiccionario As Object
Set MiDiccionario = CreateObject("Scripting.Dictionary")

With MiDiccionario
    'para que no discrimine mayusculas ni minúsculas
    .CompareMode = vbTextCompare
    
    'añadimos tres países al diccionario
    .Add Key:="España", Item:=1000
    .Add Key:="México", Item:=2000
    .Add Key:="Colombia", Item:=3000
    MsgBox "Número elementos: " & .Count

    'eliminamos un elemento concreto
    .Remove "España"
    MsgBox "Número elementos: " & .Count
    
    'y el resto de elementos
    .RemoveAll
    MsgBox "Número elementos: " & .Count
End With
End Sub



Y hasta aquí la breve introducción a este objeto .Dictionary