martes, 28 de enero de 2014

VBA: ShellExecute o cómo abrir desde Excel otros archivos.

En un post anterior del blog expuse el empleo de una función API para reproducir un sonido (ver).
En esta ocasión comentaré una nueva función API que nos permitirá abrir cualquier tipo de archivo tan sólo informando de dónde se encuentra (cuál es su ruta), sin saber el nombre de la aplicación asociada; en respuesta a la pregunta planteada:

...una macro que logre hipervincular los nombres de varias imágenes (p.e. a.jpg, b.jpg, c.jpg, etc) que están en una columna, con las imágenes, propiamente dichas, que están en la siguiente carpeta: "D:\BancoFotos\". Para logra que con un click en el nombre de la imagen abra dicha imagen...


Antes de 'meternos en faena' me gustaría comentar, de manera muy superficial, qué es una API y cuán peligroso puede llegar a ser, si no se emplea adecuadamente... comenzar diciendo que antes de llamar a funciones de Microsoft Windows (API), debemos entender cómo las DLL de API de Windows tratan los argumentos y tipos de datos, ya que las llamadas incorrectas a funciones de Windows pueden causar errores de páginas no válidas u otro comportamiento imprevisto.
También podemos leer una introducción en Wikipedia


En nuestro ejercicio de hoy no existe peligro alguno, ya que sólo abriremos un fichero desde nuestra hoja de cálculo... para este trabajo emplearemos la función API ShellExecute.
Esta función API 'ShellExecute' dispone de seis parámetros, muchos de ellos opcionales:

1- hWnd: Identifica la ventana principal. Esta ventana recibe las cajas de mensajes que una aplicación produce (por ejemplo, para el informe de errores).
2- lpOperation: Especificamos la acción que queremos realizar al ejecutar nuestra función. Podrían ser entre otras las siguientes acciones:
  1. "Open" = nuestra función abrirá el archivo especificado por el parámetro siguiente 'lpFile'. El archivo podrá ser indistintamente un ejecutable (.exe) o cualquier otro tipo de documento (.jpg, .pdf, etc., etc.), incluso podría ser sencillamente una carpeta o directorio.
  2. "Print" = La función imprimiría el archivo indicado por 'lpFile', lógicamente el archivo deberá ser algún tipo de documento no ejecutable, ya que en caso de serlo, simplemente lo abriría... (si así estuviera indicado).
  3. "Explore" = La función 'exploraría' la carpeta/directorio indicada por 'lpFile'.
  4. "Play" = Para aquellos métodos que soportan una función Play, como archivos de sonido...
  5. "Properties" = Detallaría las propiedades del archivo.
  6. 0& = el argumento 'lpOperation' puede ser también NULL (vbNullString si declaramos la variable como String, o bien 0& si queda declarada de cualquier otra forma). En estos casos la función toma la acción por defecto, que es OPEN.
3- lpFile: Este argumento queda definido como una cadena (String) que especifica la ruta del archivo que queremos abrir, imprimir, explorar, etc. Recuerda, lo hemos visto antes, que la función puede abrir indistintamoente ejecutables o documentos de cualquier tipo, pero sólo puede imprimir archivos tipo documento.
4- lpParameters: Si hemos informado en el anterior 'lpFile' un archivo ejecutable (.exe), entonces 'lpParameters' será una cadena(String) que detalle los parámetros para que opere dicha aplicación. Si 'lpFile' fuera un archivo tipo documento, 'lpParameters' debería ser NULL (0& or vbNullString).
5- lpDirectory: Cadena que identifica la carpeta/directorio de trabajo por defecto.
6- nShowCmd : Cómo queremos ver la applicación cuando se ejecute y abra:
    Valor
  • 0 = oculta la ventana de activación y pasa a otra ventana.
  • 1 = Activa y muestra una ventana. Si la ventana está Minimizada o Maximizada , Windows la restaura a su tamaño y posición original (igual que 9 ).
  • 2 = Activa una ventana y la muestra en forma de icono .
  • 3 = Activa una ventana y lo muestra como una ventana Maximizada.
  • 4 = muestra una ventana en su tamaño más reciente y posición. La ventana que está actualmente permanece activa.
  • 5 = Activa una ventana y lo muestra en su actual tamaño y la posición.
  • 6 = Minimiza la ventana especificada y activa la ventana de nivel superior en la lista del sistema.
  • 7 = Muestra una ventana como un icono. La ventana que está actualmente activa permanece activa.
  • 8 = Muestra una ventana en su estado actual. La ventana que está actualmente activa permanece activa.
  • 9 = Activa y muestra una ventana. Si la ventana está Minimizada o Maximizada, Windows restaura a su tamaño y posición original (lo mismo que 1).


Añadiremos nuestro código en la Hoja de trabajo, asociándolo a un evento _BeforeDoubleClick:

Private Declare Function ShellExecute _
    Lib "shell32.dll" _
    Alias "ShellExecuteA" ( _
    ByVal hwnd As Long, _
    ByVal lpOperation As String, _
    ByVal lpFile As String, _
    ByVal lpParameters As String, _
    ByVal lpDirectory As String, _
    ByVal nShowCmd As Long) _
    As Long

Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
Dim strFile As String
Dim strAction As String
Dim lngErr As Long

'ruta del archivo.
strFile = "E:\excelforo\Fotos\" & Target.Value
'las acciones pueden ser OPEN, NEW u otras; todo depende de qué necesitemos
strAction = "OPEN"
'llamamos a la funicón API
lngErr = ShellExecute(0, strAction, strFile, "", "", 0)

End Sub



Estamos listos para trabajar, bastará hacer doble clic sobre una celda cualquiera de la hoja para que la función busque y abra el fichero (sea cual sea) en la ruta especificada cuyo nombre corresponda con el valor de la celda 'clicada'...

30 comentarios:

  1. Hola muchas gracias por tu función, pero presento el siguiente problema:
    cuando cambio: strAction = "OPEN" por strAction = "PRINT" en este último no me imprime el archivo, pero con strAction = "OPEN" si lo abre. puedes ayudarme?. Gracias.

    ResponderEliminar
    Respuestas
    1. Hola!
      supongo que el fichero que quieres imprimir NO es ejecutable o algún otro tipo de fichero NO imprimible (comprimidos, etc)???
      Slds

      Eliminar
    2. Hola Ismael, es cierto, no es ejecutable pero si es imprimible pues tiene extensión htm. Gracias.

      Eliminar
    3. Hola,
      tengo mis dudas que esta API detecte los html como 'imprimibles', en su documentación exige que sean documentos de texto (y al fin y al cabo, un html es un fichero de código con protocolos UTF, etc. etc...).

      hace tiempo funcionaba otra instrucción.. no sé si aún siga activa, que específicamente sí permitía imprimir html:
      rundll32.exe mshtml.dll,PrintHTML nombrearchivo

      Espero te resulte.
      Slds

      Eliminar
    4. Hola Ismael, y específicamente como seria el código de la función para esa instrucción (rundll32.exe mshtml.dll,PrintHTML nombrearchivo). Gracias.

      Eliminar
    5. Pues no recuerdo muy bien..
      es un código, que como te indicaba, es algo viejo.. y me parece era específico para máquinas de 32 bits.
      Pero en principio, es tal cual, cambiando 'nombrearchivo' por el nombre de tu fichero.

      Saludos

      Eliminar
    6. Hola Ismael, lo máximo tu código, me sirvió muy bien. Muchas gracias por compartir tu conocimiento y experiencia de manera gratuita.

      Eliminar
  2. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  3. Estimado, me has salvado la mitad de un quebradero de cabeza que tengo hace meses, he aprendido excel sobre la marcha y muchos temas no manejo muy bien.

    tengo un informe con una macro que dispara a través de hipervinculos unos archivos .REP, estos a su vez exportan datos a unos TXT que es desde donde luego excel obtiene la información, el problema es que debo a cada instante aceptar la alerta de ejecución de hipervinculo, pero con el código que dejaste los archivos se ejecutan sin mas, la pregunta es como puedo agregar esto a mi macro?

    ResponderEliminar
    Respuestas
    1. Hola Kazuno,
      No sé exactamente a qué te refieres con tu pregunta ??
      pero entiendo tendrás tu macro en un módulo estándar.. asi que incorpora al inicio del módulo la declaración de la función:
      Private Declare Function ShellExecute ....

      y el cuerpo del procedimiento Sub que aparece en lugar de tu instrucción para abrir los ficheros.

      Un saludo

      Eliminar
    2. Gracias por la respuesta, disculpa si no me exprese bien, a ver si me aclaras un poco mas la idea (quede plop con lo de la declaración).

      por ejemplo, una de las macro que uso es así (comento todas las lineas en mis macro, mañas mías)

      Sub opa()

      'no queremos que muestre la operacion
      Application.ScreenUpdating = False
      'nos dirigimos a los links, como la pestaña esta oculta, la mostramos
      'seleccionamos los links y trabajamos en ellos, uno por vez
      Range("X2").Select
      Selection.Hyperlinks(1).Follow NewWindow:=False, AddHistory:=True
      'tiempo de espera mientras se abre el reporte CMS
      Application.Wait (Now + TimeValue("00:00:5"))
      'selecciona y trabaja en la pestaña oculta
      Sheets("CMS").Visible = True
      Sheets("CMS").Select
      Range("A1:AI25").Select
      'limpiamos lo que ya este, en caso de error
      Selection.ClearContents
      ....

      en que parte debería agregar lo que mencionas? suponiendo que el LINK de la celda X2 es "C:/texto.txt"

      Eliminar
    3. Por lo que entiendo quieres abrir el fichero que se encuentra en la ubicación del hipervínculo...
      podrías tomar la dirección de hipervínculo como variable, e incluirlo directamente en el procedimiento descrito en el post.

      También podrías usar directamente el método .Open del objeto Workbook.

      Espero haberte orientado esta vez.
      Saludos

      Eliminar
  4. Buenos días.
    Tengo una macro que imprime con esta API, los archivos PDF´s, pero dado que la impresora predeterminada está configurada para impresión a una cara no puedo hacer que me imprima uno o varios de los archivos a doble cara. Es decir, necesito que el primer archivo lo imprima a una cara y el segundo a doble cara. Gracias

    ResponderEliminar
    Respuestas
    1. Buenos días Oscar,
      esta API controla la impresora y no la configuración, date cuenta que el control sobre otras aplicaciones (Adobe en este caso) desde Excel es limitado...
      No creo posible tal cosa... salvo mejor opinion

      Lo siento
      un saludo

      Eliminar
    2. Buenas tardes, Ismael, podrías apoyarme para abrir un archivo en formato pdf, la situación es esta: capturó un número en un textbox y ese numero es el nombre del archivo en pdf, obviamente pues va a estar cambiando el nombre del archivo, soy principiante en esto y los archivos pdf están en una carpeta en C:\BD

      Eliminar
    3. Hola Daniel,
      a priori te serviría la macro expuesta en el post.. en todo caso cambiando la línea 18:
      strFile = "E:\excelforo\Fotos\" & Target.Value
      por tu particularidad:
      strFile = "C:\BD\" & TextBox1.Value

      OJO asegúrate cómo se llama tu control
      Saludos

      Eliminar
  5. Hola buenas, gracias por la explicación, amigo una pregunta, es posible también cerrar una aplicación desde el excel, o sea, que así mismo como uno la puede abrir, la pueda cerrar, muchas gracias

    ResponderEliminar
    Respuestas
    1. Hola Alexander,
      sí, claro es posible.. depende de la aplicación que quieras cerrar se suele emplear una u otra manera, por ejemplo para paquete Office se emplea el método:
      .Quit
      en otros casos .Terminate...

      Espero te oriente
      Saludos

      Eliminar
  6. Tengo el problema que cuando se abre el fichero .jpg con el visualizador de imagenes de windows en sistemas de 32bit lo hace bien pero en sistemas de 64bit puros como es el Windows Server 2013, no me funciona dándome el error en la linea Lib "shell32.dll".
    Por favor me pueden ayudar.

    ResponderEliminar
    Respuestas
    1. Hola Jorge,
      es correcto, ya que en el ejemplo estamos utilizando la librería correspondiente a 32 bits (justo en la línea que indicas).. aunque en teoría la de 32bits debería funcionar también en sistemas de 64 bits.
      Prueba de todas formas comenzando por:
      Private Declare PtrSafe Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" (....
      es decir, añadiendo PtrSafe antes de la palabra Function

      con esto debería asegurarse el correcto funcionamiento

      Saludos

      Eliminar
  7. Gracias amigo no me ha funcionado con .jpg y los he modificado en .pdf y de esta forma podemos funcionar con servidores windows server 2013.
    Eres un tio genial por contestarme tan rapido.

    ResponderEliminar
  8. Es grato saber que existe gente con un conocimiento tan basto como tu, dispuesto a compartirlo, mis más sinceras gracias.

    ResponderEliminar
    Respuestas
    1. ;-)
      muchas gracias...
      Al compartir TODOS aprendemos
      Un saludo

      Eliminar
  9. Hola Ismael

    Viendo el codigo de esta funcion API veo que la apertura del fichero correspondiente se apertura con el programa que por defecto abre la extension de dicho fichero (que aparece en la opcion Propiedades del menu contextual de dicho fichero)

    Podrias indicar una funcion API, para que antes de abrir un fichero en cuestion, estableciera como programa de apertura por defecto aquel que le pasemos como ruta a un argumento de la funcion.

    Estoy intentando abrir ficheros .pdf con el lector de pdfs SumatraPdf ejecutando el siguiente codigo:

    Dim AppPdf As Object
    Set AppPdf = CreateObject("Shell.Aplication")
    AppPdf.Open(Ruta)

    * Ruta es la ruta del fichero pdf a abrir, incluyendo su extension

    Es el unico codigo que funciona correctamente pero antes he debido establecer manualmente el programa por defecto que abre los pdf en la opcion Propiedades del menu contextual. Lo que intento es que esta accion manual sea ejecutada tambien por codigo y solo es posible con una funcion API. Seria posible?

    ResponderEliminar
    Respuestas
    1. Hola,
      creo recordar no es configurable la aplicación por defecto

      Revisaré documentación y si encuentro solución lo comentaré

      Slds

      Eliminar
  10. 2 cuestiones mas...

    1- Sabrias cual es la ruta que emplea Microsoft Edge?
    2- Al ejecutar el codigo indicado en el anterior comentario, a traves de los metodos Hoja1.Activate o Hoja1.cells(1, 1).Select intento que el foco se situe en Excel despues de abrir el pdf pero no lo consigo con dichas lineas de codigo. Que linea de codigo necesitaria para que el foco se situara de nuevo en Excel tras la apertura del fichero pdf?

    Gracias

    ResponderEliminar
    Respuestas
    1. Hola,
      suponiendo la macro está en el Excel al que quieres volver
      Thisworkbook.Hoja1.Activate

      ¿La ruta que emplea para Edge?, a qué te refieres?

      Slds

      Eliminar
  11. Hola de nuevo Ismael

    - El problema no es la referencia Hoja1 porque lo tengo establecida como:

    Set Hoja1 = ThisWorkbook.Worksheets(NOMBREHOJA)

    De hecho, he probado a ejecutar codigo de prueba posterior a la apertura del Objeto Pdf tal como...

    Dim AppPdf As Object
    Set AppPdf = CreateObject("Shell.Aplication")
    AppPdf.Open(Ruta)
    Hoja1.Cells(1, 1).Select
    Hoja1.Cells(1, 1).Value = 1

    * Ruta es la ruta del fichero pdf a abrir, incluyendo su extension


    La ejecucion de este codigo es correcta, abriendo el fichero pdf correctamente y posteriormente escribiendo el valor 1 en Excel en la celda A1, pero quedando finalmente el foco en el objeto Pdf (SumatraPdf, MicrosoftEdge o el que sea) sin mediar codigo por medio y no en Excel como seria lo logico. Un poco extraño, la verdad.

    2 - Respecto a saber la ruta de MicrosoftEdge lo digo para poder indicar la ruta de dicho objeto con la sentencia .Open para hacer una prueba, pero la ruta no es tan evidente como puede ser la del Objeto InternetExplorer o la de SumatraPdf, tal como:

    /.../InternetExplorer.exe o
    /.../SumatraPdf.exe

    No consigo averiguar dicha ruta.

    3 - Respecto a la funcion API que me cambie el programa predeterminado por defecto para abrir los ficheros pdf sigo sin averiguar nada.

    Gracias

    ResponderEliminar

Nota: solo los miembros de este blog pueden publicar comentarios.