lunes, 29 de marzo de 2010

Tutorial: Cómo crear enlaces directos en Windows con Python

Enlaces directos en Windows

En esta ocasión trataré de resumir varios artículos escritos por Mike Driscoll y Tim Golden. Todos estos tutoriales tratan de explicar diferentes vías para crear desde Python enlaces directos (a.k.a shortcuts) para acceder a aplicaciones de Windows. Mi intención es, primeramente, acordarme. En segundo lugar quisiera facilitar documentar en un solo lugar los enfoques disponibles, y tratar de ilustrar las diferencias más ventajas o desventajas. Finalmente el texto estará en español ;o). Antes que todo, si todavía no se ha dado cuenta le digo que solo hablaré de Microsoft Windows. El artículo puede servir también como un pequeño mini-tutorial acerca del uso de los módulos winshell, win32com, pythoncom y habrá hasta un poquito de corrutinas ;o). Si desea estar al tanto de próximos artículos igualmente útiles, le invito a suscribirse por medio de RSS.

Preparación

Python Programming on Win32

Lo primero que hay que hacer es instalar las extensiones de Python para Win32 que se encuentran en el paquete de Mark Hammond llamado PyWin32. Igualmente útil puede resultar tener listo el módulo winshell de Tim Golden. Este último permite ubicar fácilmente las carpetas especiales (semi-mágicas) del sistema.

Creando un enlace a una URL en el Escritorio

Quizás la tarea más sencilla es crear un enlace (a.k.a link) en el Escritorio para acceder fácilmente a un sitio conocido. En caso que sea suficiente utilizar el navegador de Internet predeterminado, solo tendríamos que hacer esto:

import os.path, winshell

desktop = winshell.desktop()
path = os.path.join(desktop, "myNeatWebsite.url")
target = "http://www.google.com/"
 
shortcut = file(path, 'w')
shortcut.write('[InternetShortcut]\n')
shortcut.write('URL=%s' % target)
shortcut.close()

Brevemente podemos apreciar cómo se utiliza el módulo winshell para obtener el camino al Escritorio del usuario y se crea un fichero con extensión .url dentro de esta carpeta. El contenido de este fichero es el requerido por el sistema operativo y, finalmente debe lucir más o menos así

[InternetShortcut]
URL=http://www.google.com/

¡Eso es todo! :o)

La solución a más bajo nível con pythoncom

Quizás la versión más complicada, y por tanto más explícita, es la que se obtiene al utilizar la API Win32 desde Python. Para ello se emplea el módulo pythoncom, que permite cargar cualquier objeto COM e interactuar casi directamente con las interfaces y otros elementos. Veamos el ejemplo.

import os, sys
import pythoncom
from win32com.shell import shell, shellcon
 
shortcut = pythoncom.CoCreateInstance (
  shell.CLSID_ShellLink,
  None,
  pythoncom.CLSCTX_INPROC_SERVER,
  shell.IID_IShellLink
)
program_location = r'C:\Program Files\SomeCoolProgram\prog.exe'
shortcut.SetPath (program_location)
shortcut.SetDescription ("My Program Does Something Really Cool!")
shortcut.SetIconLocation (program_location, 0)
 
desktop_path = shell.SHGetFolderPath (0, shellcon.CSIDL_DESKTOP, 0, 0)
persist_file = shortcut.QueryInterface (pythoncom.IID_IPersistFile)
persist_file.Save (os.path.join (desktop_path, "My Shortcut.lnk"), 0)

Como se puede apreciar esto no es muy pitónico que digamos, sobre todo debido a que la API Win32 está diseñada para ser utilizada desde lenguajes como C. Más adelante se verán otras formas más sencillas de hacerlo. Sin embargo, creo que es un ejemplo muy interesante porque revela que los conocedores de la API Win32 siempre podrán utilizarla desde Python de una forma muy similar a la que ya están acostumbrados. Veamos qué está pasando.

Después de importar los módulos necesarios, se carga una instancia de la clase ShellLink y se indica que se utilizarán los métodos de la interfaz IShellLink que ella implementa. Por suerte los GUID de ambos ya están definidos (i.e. win32com.shell.shell.CLSID_ShellLink, win32com.shell.shell.IID_IShellLink). La función pythoncom.CoCreateInstance no es más que un simple wrapper alrededor de la archi-conocida función con un nombre similar presente en la API Win32. A continuación se especifican los datos necesarios para crear el enlace (i.e. programa a ejecutar, descripción y el ícono). En este momento ya todo está casi listo.

Lo único que falta por hacer es crear el fichero con la información que hemos especificado anteriormente. El último bloque de instrucciones se encarga de esto. La función win32com.shell.shell.SHGetFolderPath es similar a la presente en la API Win32. En este caso se utiliza la constante win32com.shell.shellcon.CSIDL_DESKTOP para obtener el camino a la carpeta del escritorio del usuario. Allí se creará el fichero My Shortcut.lnk, que es el que deseamos. Para hacerlo, hay que utilizar el objeto COM anterior pero necesitamos el método Save definido en la interfaz IPersistFile (su GUID es pythoncom.IID_IPersistFile). El método QueryInterface hace todo el trabajo de preparación.

A continuación les presento un código que utiliza la misma filosofía para actualizar el enlace creado en la sección anterior y hacer que apunte a otra URL.

import os, sys
import pythoncom
from win32com.shell import shell, shellcon

shortcut = pythoncom.CoCreateInstance (
  shell.CLSID_InternetShortcut,
  None,
  pythoncom.CLSCTX_INPROC_SERVER,
  shell.IID_IUniformResourceLocator
)

desktop_path = shell.SHGetFolderPath (0, shellcon.CSIDL_DESKTOP, 0, 0)
shortcut_path = os.path.join (desktop_path, "myNeatWebsite.url")
persist_file = shortcut.QueryInterface (pythoncom.IID_IPersistFile)
persist_file.Load (shortcut_path)

shortcut.SetURL ("http://python.org")
persist_file.Save (os.path.join (shortcut_path, "myNeatWebsite.url"), 0)

Creando un enlace al Media Player Classic con win32com

Python

Este ejemplo será un tin más complejo. Utilizaremos el módulo win32com para crear un enlace. Este módulo brinda acceso desde Python a ciertos componentes implementados sobre la tecnología COM, así que se trata de una operación digamos que a bajo nivel. Sin embargo, La ventaja en este caso consiste en que se facilitan varios pasos que sí son necesarios si se utiliza la API Win32 directamente con otros lenguajes (e.g. C, Object Pascal), o cómo se mostró en el ejemplo anterior. Veamos el código.

import os, winshell
from win32com.client import Dispatch
 
desktop = winshell.desktop()
path = os.path.join(desktop, "Media Player Classic.lnk")
target = r"P:\Media\Media Player Classic\mplayerc.exe"
wDir = r"P:\Media\Media Player Classic"
icon = r"P:\Media\Media Player Classic\mplayerc.exe"
 
shell = Dispatch('WScript.Shell')
shortcut = shell.CreateShortCut(path)
shortcut.Targetpath = target
shortcut.WorkingDirectory = wDir
shortcut.IconLocation = icon
shortcut.save()

Como se puede apreciar, en este caso se utiliza el shell de Windows mediante la interfaz IDispatch (más bien su equivalente en Python win32com.client.Dispatch ;o). Se utiliza el identificador WScript.Shell para cargar el objeto COM que deseamos y luego se utilizan sus métodos para asociarle el camino donde se guardará el enlace, el del ejecutable, el directorio de trabajo inicial, y el ícono que se mostrará. Este último puede ser el camino a un fichero .ico o a un ejecutable o dll'. La última llamada al método Save es muy importante, pues es la que hace que el enlace se haga persistente (i.e. se escriba en el disco)

A continuación les muestro una función que puede ser útil para manejar ambos casos.

from win32com.client import Dispatch
import os.path
 
def createShortcut(path, target='', wDir=None, icon=''):
    if wDir is None :
        wDir = os.path.dirname(os.path.abspath(target))
    ext = path[-3:]
    if ext == 'url':
        shortcut = file(path, 'w')
        shortcut.write('[InternetShortcut]\n')
        shortcut.write('URL=%s' % target)
        shortcut.close()
    else:
        shell = Dispatch('WScript.Shell')
        shortcut = shell.CreateShortCut(path)
        shortcut.Targetpath = target
        shortcut.WorkingDirectory = wDir
        if icon <> '':
            shortcut.IconLocation = icon
        shortcut.save()

Creando el enlace en Archivos de programas utilizando winshell

Ya hemos visto varias variantes pero sin dudas esta resultará la más económica en cuanto a líneas de código.

import os, winshell
 
program_files = winshell.programs()
winshell.CreateShortcut (
               Path=os.path.join (program_files, 'My Shortcut.lnk'),
               Target=r'C:\Program Files\SomeCoolProgram\prog.exe')

En este caso se guardará el enlace My Shortcut.lnk en la carpeta Archivos de programas del menú de inicio del usuario actual. El camino lo devuelve winshell.programs(). La función winshell.CreateShortcut hace exactamente lo que necesitamos. Además de los argumentos mostrados en el ejemplo, también es posible utilizar los siguientes:

  • Path : el camino dónde se creará el enlace
  • Target : el fichero que se ejecutará al activar el enlace
  • Arguments : Los argumentos (i.e. línea de comandos) que recibirá la aplicación
  • StartIn? : directorio de trabajo inicial
  • Icon : ícono que se mostrará en el Explorador de Windows
  • Description : Descripción del enlace

Resolviendo los enlaces

En este caso trataremos de implementar para Windows una funcionalidad que se obtiene casi sin esfuerzo al utilizar los enlaces simbólicos de sistemas Unix. A diferencia de este último, en Windows los enlaces directos son ficheros comunes y corrientes para el sistema de archivos, pero como ya vimos para el shell representan objetos de tipo IShellLink. Esta diferencia nos obliga a poner el parche porque ya salió el hueco ;o). A continuación leeremos una lista de ficheros y, si encontramos un enlace directo, lo seguiremos para retornar el fichero de destino.

import os, sys
import glob
import pythoncom
from win32com.shell import shell, shellcon

def shortcut_target (filename):
  link = pythoncom.CoCreateInstance (
    shell.CLSID_ShellLink,    
    None,
    pythoncom.CLSCTX_INPROC_SERVER,    
    shell.IID_IShellLink
  )
  link.QueryInterface (pythoncom.IID_IPersistFile).Load (filename)
  #
  # GetPath devuelve el camino y una estructura de tipo `WIN32_FIND_DATA`
  # que se ignorará. El parámetro indica si se desea obtener el 
  # nombre corto, el UNC o el camino (a.k.a. "raw path").
  # Por alguna extraña razón la documentación indica que las 
  # constantes correspondientes se pueden combinar :-$ .
  #
  name, _ = link.GetPath (shell.SLGP_UNCPRIORITY)
  return name

def shell_glob (pattern):
  for filename in glob.glob (pattern):
    if filename.endswith (".lnk"):
      yield (filename, shortcut_target (filename))
    else:
      yield (filename, filename)

desktop = shell.SHGetSpecialFolderPath (None, shellcon.CSIDL_DESKTOP)
for filename in shell_glob (os.path.join (desktop, "*")):
  print filename

¿Se podría mejorar este ejemplo utilizando winshell?

Conclusiones

Hay múltiples variantes para utilizar Python con el fin de crear enlaces directos en Windows. La más sencilla parece ser la que utiliza la función winshell.CreateShortcut. Sin embargo, hay tela por donde cortar y níveles de complejidad para todos los gustos.

Espero que le haya sido útil este corto tutorial. Si es así, le invito a estar al tanto de los próximos artículos. Trataré de abordar temas tan útiles y recurrentes como este. Me sentiría satisfecho si logro sacarle de un apuro o darle una idea para mejorar sus scripts de Python.

1 comentario: