Truco: Obtener Iconos de archivos en macOS

De vez en cuando surge en el foro de Xojo alguna consulta acerca de cómo se pueden recuperar los iconos de archivo para presentarlos al usuario en macOS. Continúa leyendo y te mostraré cómo, tanto utilizando la vista previa del documento mediante la tecnología QuickLook de Apple como empleando el icono genérico asociado con el tipo de archivo.

QuickLook es la tecnología de Apple que se encarga de generar iconos de archivo basados en el contenido del mismo. De este modo se facilita aún más que el usuario pueda conocer de un simple vistazo si un archivo determinado es aquél en el que está interesado sin necesidad de abrirlo.

La buena noticia es que en Xojo no resulta muy complejo acceder a este tipo de iconos de archivo utilizando, como ya hemos hecho en otras ocasiones, los Declare. Ya sabes, se trata de la facultad de “declarar” desde Xojo las llamadas a funciones o métodos proporcionados por APIs o librerías externas, como son los propios frameworks ofrecidos, en este caso, por el propio sistema operativo.

Puedes descargar el proyecto Xojo de ejemplo desde este enlace.

De este modo, una vez que hemos “declarado” dichas funciones en nuestro código Xojo, ya podremos utilizarlas sin más; lo cuál amplía enormemente el uso de funciones o capacidades nativas del sistema operativo.

Para el caso que nos ocupa, comenzaremos añadiendo un nuevo método a un módulo en un nuevo proyecto Xojo de Escritorio. Para ello, utilizaremos la siguiente signatura:

Nombre de Método: IconForFile
Parámetros: Extends f As FolderItem, tSize As Size, Optional QuickLookIcon As Boolean = True
Tipo Devuelto: Picture
Ámbito: Global

Si estás comenzando a programar con Xojo, entonces conviene aclarar que la palabra clave Extends en la signatura del método significa que dicho método “extiende” o amplía los métodos disponibles para el tipo de dato que acompaña al parámetro; en este caso FolderItem.

La conveniencia de hacerlo así, es que posteriormente resultará mucho más legible saber sobre qué tipo de objeto estamos invocando el método al utilizar la notación por punto. Es decir, en vez de utilizar una sintaxis como la siguiente:

Var f As FolderItem = FolderItem.ShowOpenFileDialog("")
Dim tSize As New Size
tSize.Height = 128
tSize.Width = 128
var p As Picture = IconForFile(f,tSize)

Podremos invocar al método de la siguiente manera:

Var f As FolderItem = FolderItem.ShowOpenFileDialog("")
Dim tSize As New Size
tSize.Height = 128
tSize.Width = 128
var p As Picture = f.IconForFile(tSize)

Observa cómo en este segundo paso se invoca al método utilizando la notación por punto sobre la propia instancia de FolderItem referenciada por la variable f; y ya no es necesario pasar dicha variable como uno de los parámetros.

El segundo aspecto que encontrarás interesante en la definición del método es el uso de la palabra clave Optional. Cuando se utiliza, estamos indicando que el paso del parámetro al que acompaña dicha palabra clave es… bien… opcional. De hecho, en los dos ejemplos anteriores de código observarás que se ha omitido el paso de dicho parámetro.

Además, en el caso de los parámetros declarados como Opcionales podemos asignar el valor que tomarán por omisión en el caso de que no se pase dicho valor como parte de la llamada del método. En la definición de nuestro método verás que se asigna por omisión el valor Booleano True.

Obtener Icono QuickLook y de Tipo de Archivo

Una vez que hemos añadido nuestro nuevo método, tendremos que teclear el siguiente código en el Editor de Código asociado. Como verás, se hace un uso bastante amplio de Declares sobre diferentes frameworks de macOS.

Junto a cada uno de ellos se ha incluido un breve comentario sobre su funcionalidad así como el enlace a la Documentación disponible en el área de desarrolladores de Apple, de modo que puedas ampliar la información en el caso de que estés interesado en conocer más sobre su funcionamiento… y como se trasladan dichos métodos desde Objective-C a Xojo (especialmente en lo referente a los tipos de datos):

#If TargetMacOS Then

  #Pragma DisableBackgroundTasks
  #Pragma DisableBoundsChecking
  #Pragma NilObjectChecking False

  If f = Nil Or Not f.Exists Then Return Nil

  Var path As String = f.NativePath

  // Declare para obtener un objeto en Objective-C (reserva de memoria para el objeto, aún no inicializado)
  // https://developer.apple.com/documentation/objectivec/nsobject/1571958-alloc/
  Declare Function Alloc Lib "Foundation" Selector "alloc" (classRef As ptr) As ptr

  // Declare para liberar automáticamente un objeto en Objective-C (liberar la memoria utilizada por dicho objeto)
  // https://developer.apple.com/documentation/foundation/nsautoreleasepool/1807021-autorelease/
  Declare Sub AutoRelease Lib "Foundation" Selector "autorelease" (classInstance As ptr)

  // Declare para obtener una referencia a una clase de Objective-C a partir de la cadena de texto recibida
  // https://developer.apple.com/documentation/foundation/1395135-nsclassfromstring?language=objc
  Declare Function NSClassFromString Lib "Foundation" (className As CFStringRef) As ptr

  // Declare para obtener una referencia al Workspace compartido por un proceso en macOS.
  // https://developer.apple.com/documentation/appkit/nsworkspace/1530344-sharedworkspace/
  Declare Function sharedWorkSpace Lib "AppKit" Selector "sharedWorkspace" (classObject As ptr) As ptr

  // Declare para obtener el objecto de Icono a partir de la ruta de archivo recibida
  // https://developer.apple.com/documentation/appkit/nsworkspace/1528158-iconforfile/
  Declare Function iconForFile Lib "AppKit" Selector "iconForFile:" (instanceObject As ptr, path As CFStringRef) As ptr

  // Declare para ajustar el tamaño de un objeto NSImage o CGImageRef
  Declare Function setSize Lib "AppKit" Selector "setSize:" (instanceObject As ptr, size As NSSize) As ptr

  // Declare para Bloquear y Desbloquear el contexto gráfico de una instancia NSImage en Objective-C
  // https://developer.apple.com/documentation/appkit/nsimage/1519891-lockfocus?language=objc
  // https://developer.apple.com/documentation/appkit/nsimage/1519853-unlockfocus?language=objc
  Declare Sub LockFocus Lib "AppKit" Selector "lockFocus" (imageObj As ptr)
  Declare Sub UnlockFocus Lib "AppKit" Selector "unlockFocus" (imageObj As ptr)

  // Declare para obtener una nueva Representación en Mapa de bits basándose en la vista bloqueada
  Declare Function InitWithFocusedView Lib "AppKit" Selector "initWithFocusedViewRect:" (imageObj As ptr, rect As NSRect) As ptr

  // Declare para obtener un objeto NSData (similar a un MemoryBlock de Xojo) a partir del formato de imagen especificado desde una presentación de mapa de bits
  // https://developer.apple.com/documentation/appkit/nsbitmapimagerep/1395458-representationusingtype/
  Declare Function RepresentationUsingType Lib "AppKit" Selector "representationUsingType:properties:" (imageRep As ptr, type As UInteger, properties As ptr) As ptr

  Var targetSize As NSSize
  targetSize.Width = tsize.Width
  targetSize.Height = tsize.Height

  Var tRect As NSRect
  tRect.Origin.x = 0
  tRect.Origin.y = 0
  tRect.RectSize = targetSize

  Var data As ptr

  If QuickLookIcon Then

    //=================
    // Intentamos recuperar el icono de QuickLook para el archivo

    Var dictClass As ptr = NSClassFromString("NSDictionary")
    Var numberClass As ptr = NSClassFromString("NSNumber")

    // Declare para obtener un objeto NSNumber a partir de un valor Booleano
    // https://developer.apple.com/documentation/foundation/nsnumber/1551475-numberwithbool/

    Declare Function NSNumberWithBool Lib "Foundation" Selector "numberWithBool:" (numberClass As ptr, value As Boolean) As ptr
Var numberWithBool As ptr = NSNumberWithBool(numberClass, True)

    // Declare para obtener un objeto NSDictionary (similar al Dictionary de Xojo) utilizando la Clave y Valor recibidos como parámetros
    // https://developer.apple.com/documentation/foundation/nsdictionary/1414965-dictionarywithobject/

    Declare Function NSDictionaryWithObject Lib "Foundation" Selector "dictionaryWithObject:forKey:" (dictClass As ptr, value As ptr, key As CFStringRef) As ptr
    Var dictInstance As ptr = NSDictionaryWithObject(dictClass, numberWithBool,"IconMode")

    Var fileClass As ptr = NSClassFromString("NSURL")

    // Declare para obtener un objeto NSURL a partir de la ruta recibida como String
    // https://developer.apple.com/documentation/foundation/nsurl/1410828-fileurlwithpath/

    Declare Function NSFileURLWithPath Lib "Foundation" Selector "fileURLWithPath:" (fileClass As ptr, path As CFStringRef) As ptr
    Var fileInstance As ptr = NSFileURLWithPath(fileClass, f.NativePath)

    // Declare para obtener el previo de imagen QuickLook para el archivo indicado y con el tamaño de imagen especificado
    // https://developer.apple.com/documentation/quicklook/1402623-qlthumbnailimagecreate?language=objc

    Declare Function QLThumbnailImageCreate Lib "QuickLook" (allocator As Integer, file As ptr, size As NSSize, dictRef As ptr) As ptr
    Var imageRef As ptr = QLThumbnailImageCreate(0, fileInstance, targetSize, dictInstance)

    If imageref <> Nil Then

      Var BitmapImageRepClass As ptr = NSClassFromString("NSBitmapImageRep")
      Var BitmapImageRepInstance As ptr = Alloc(BitmapImageRepClass)

      // https://developer.apple.com/documentation/appkit/nsbitmapimagerep/1395423-initwithcgimage/

      Declare Function CGInitWithCGImage Lib "AppKit" Selector "initWithCGImage:" (bitmapInstance As ptr, CGImage As ptr) As ptr
      Var BitmapImageRep As ptr = CGInitWithCGImage(BitmapImageRepInstance, imageRef)

      data = RepresentationUsingType(bitmapImageRep,4, Nil) // 4 = PNG

      AutoRelease(BitmapImageRep)
      AutoRelease(imageref)

      // Obtenemos la instancia Picture de Xojo a partir del objeto NSData
      Var p As Picture = NSDataToPicture(data)

      data = Nil
      numberWithBool = Nil
      fileInstance = Nil
      dictInstance = Nil
      BitmapImageRepInstance = Nil
      Return p

    End If
  End If

  // Si llegamos a este punto significa que no hemos podido recuperar el
  // icono QuickLook para el archivo, que ha devuelto un objeto no válido,
  // o que se ha llamado al método con el parámetro QuickLookIcon definido a False
  // de modo que recuperaremos en estos casos el icono normal basado en el
  // tipo de archivo.

  Var WorkSpace As ptr = NSClassFromString("NSWorkspace")
  Var sharedSpace As ptr = sharedWorkSpace(WorkSpace)
  Var icon As ptr = iconForFile(sharedSpace, path) // Aquí obtendremos una NSImage

  Var resizedIcon As ptr = setSize(icon, targetSize)

  // Obtenemos la representación en mapa de bits de la imagen para extraerla como PNG.

  LockFocus(resizedIcon)

  Var NSBitmapImageRepClass As ptr = NSClassFromString("NSBitmapImageRep")
  Var NSBitmapImageRepInstance As ptr = Alloc(NSBitmapImageRepClass)
  Var newRep As ptr = InitWithFocusedView(NSBitmapImageRepInstance, tRect)

  UnlockFocus(resizedIcon)

  data = RepresentationUsingType(newRep,4, Nil) // 4 = PNG

  // Getting Xojo Picture instance from NSData object
  Var p As Picture = NSDataToPicture(data)

  data = Nil
  icon = Nil
  AutoRelease(newRep)

  Return p

#EndIfz

Como verás, todo el código está contenido en un condicional de compilación #If… #EndIf, de modo que este sólo se compilará y ejecutará cuando la plataforma de despliegue sea macOS; tal y como se indica en la comprobación del #If contra TargetMacOS.

A continuación, se utilizan una serie de directivas #Pragma encaminadas a acelerar un poco más las cosas deshabilitando en este caso las tareas en segundo plano, la comprobación de límites (en colecciones como por ejemplo los Array), y también las comprobaciones contra objetos a Nil.

El resto del código incluye algunas explicaciones que te permiten ver lo que va sucediendo en cada caso. Esto es, cómo obtenemos el icono QuickLook para el archivo o bien el icono asociado con el tipo de archivo.

Lo que observarás es que incluimos una llamada a un segundo método: NSDataToPicture. Este será el encargado de convertir un objeto NSDATA de Objective-C a una instancia Picture mediante una técnica que ya hemos utilizado en otras ocasiones. Sin embargo, y para evitar la duplicación de código en este caso, se ha extraído dicho fragmento de código llevándolo a su propio método.

Por tanto, definamos un segundo método dentro del mismo Módulo utilizando los siguientes valores:

Nombre de Método: NSDataToPicture
Parámetros: data As Ptr
Tipo devuelto: Picture
Ámbito: Protegido

Escribiendo el siguiente fragmento de código en el Editor de Código asociado:

Declare Function DataLength Lib "Foundation" Selector "length" (obj As ptr) As Integer
Declare Sub GetDataBytes Lib "Foundation" Selector "getBytes:length:" (obj As ptr, buff As ptr, Len As Integer)

Var dlen As Integer
Var mb As MemoryBlock
Var mbptr As ptr

// La variable data apunta a un objeto NSDATA que contiene la información de imagen en formato PNG; de modo que podamos crear un Picture en Xojo a partir de ella.
// Para ello necesitamos obtener la cantidad de datos en el objeto NSDATA…
dlen = DataLength(data)

// …para crear un MemoryBlock que tenga el mismo tamaño
mb = New MemoryBlock(dlen)
mbPtr = mb

// Y ahora podemos volcar simplemente los datos PNG desde el objeto NSDATA en nuestro objeto MemoryBlock…
GetDataBytes(data,mbPtr,dlen)

// …aprovechando el método compartido de Picture que nos permite crear un Picture a partir de los datos contenidos en un MemoryBlock, que pasamos como parámetro.
Return Picture.FromData(mb)

Estructuras Necesarias

Como ya hemos visto en otros tutoriales donde hacíamos uso de los Declare en macOS, aquí también necesitaremos añadir a nuestro Módulo una serie de estructuras requeridas para el paso de información a algunas de las funciones utilizadas en los frameworks nativos de macOS. Estas son las siguientes:

  • Structura NSOrigin
    X As CGFloat
    Y As CGFloat
  • Structura NSSize
    Height As CGFloat
    Width As CGFloat
  • Structura NSRect
    Origin As NSOrigin
    RectSize As NSSize

Interfaz de la aplicación de ejemplo

Ya contamos con todo el código necesario para obtener como una instancia Picture la imagen de icono de cualquiera de los archivos en macOS. Ahora sólo falta preparar una interfaz de usuario mínima para probar su funcionamiento.

Una vez finalizada, tendrá el siguiente aspecto:

Selecciona la ventana Window1 en el Navegador de proyecto. Con ella seleccionada, dirígete al Panel Inspector y cambia los siguientes valore:

  • Name: MainWindow
  • Title: Icon File Viewer

A continuación, con la ventana MainWindow aun seleccionada y visible en el Editor de Diseño, dirígete a la Librería y arrastra un control Button sobre la parte superior izquierda de la ventana.

Con el botón seleccionado, modifica los siguientes valores en el Panel Inspector asociado con el botón:

  • Name: SelectFileBt
  • Locking: Candados Superior e Izquierdo cerrados
  • Caption: Select File

Cambia de nuevo a la Librería para arrastrar ahora un control Label, situándolo justo bajo el botón en el Editor de Diseño. Utiliza el Panel Inspector asociado para modificar los siguientes valores:

  • Name: IconSizeLB
  • Locking: Candados Superior e Izquierdo cerrados
  • Text: Icon Size:

Nuevamente desde la librería, arrastra ahora un control Slider y sitúalo justo a la derecha de la etiqueta. Arrastra con el ratón desde la derecha del control slider para ampliar su longitud hasta el margen de la ventana indicado por la guía de alineación derecha que verás en el Editor de Diseño. Utiliza luego el Panel Inspector asociado para modificar los siguientes valores:

  • Name: IconSizeSL
  • Locking: Candados Superior, Izquierdo y Derecho cerrados
  • Line Step: 32
  • Allow Live Scrolling: Activado
  • Page Step: 32
  • Tick Mark Style: Bottom Right
  • Value: 128
  • Minimum Value: 32
  • Maximum Value: 1024

Nuevamente desde la Librería, arrastra un control CheckBox para situarlo justo bajo la etiqueta en el Editor de Diseño de la ventana. Utiliza el Panel Inspector asociado con el control para modificar los siguientes valores:

  • Name: QuickLookCB
  • Locking: Candados Superior e Izquierdo cerrados
  • Caption: QuickLook Preview
  • Visual State: Checked

El último control que añadiremos a nuestra interfaz de usuario será un ImageViewer. Sitúalo justo bajo la caja de verificación recién añadida. Utiliza los manejadores del ImageViewer recién añadido para modificar su ancho y altura de modo que se ajustes con las guías de alineación sobre los márgenes derecho e inferior de la ventana. A continuación, utiliza el Panel Inspector para modificar los siguientes valores:

  • Name: IconPreviewIV
  • Locking: Candados Superior, Izquierdo, Derecho e Inferior cerrados

Añadir funcionalidad a la Interfaz de Usuario

Por último añadiremos funcionalidad a algunos de los controles con los que interactuará los usuarios de nuestra aplicación.

Con el botón seleccionado en el Navegador o en el Editor de Diseño, utiliza el menú contextual para añadir el evento Pressed. Escribe el siguiente código en el Editor de Código asociado:

file = FolderItem.ShowOpenFileDialog("")
If file <> Nil Then
  FileNameLB.Text = file.Name
Else
  FileNameLB.Text = ""
End If
GetIconForFile

Con el control Slider seleccionado, utiliza el menú contextual para añadir el evento ValueChanged, e introduce el siguiente código en el Editor de Código asociado con el evento recién añadido:

If Self.file <> Nil Then
  getIconForFile
End If

Con el control CheckBox seleccionado, utiliza el menú contextual para añadir el evento ValueChanged. Escribe el siguiente código en el Editor de Código asociado con el evento recién añadido:

getIconForFile

Como verás, en varios de los eventos se invoca al método getIconForFile; de modo que lo añadiremos a la ventana utilizando la siguiente signatura:

  • Name: getIconForFile
  • Scope: Public

Y escribiendo a continuación el siguiente fragmento de código en el Editor de Código asociado:

If file <> Nil Then
  Var tSize As New Size
  tSize.Height = IconSizeSL.Value
  tSize.Width = IconSizeSL.Value

  IconPreviewIV.Image = file.IconForFile(tSize, QuickLookCB.Value)

End If

Por último tendremos que añadir la propiedad File a la ventana (definiendo su tipo como FolderItem), puesto que esta será la responsable de contener la referencia al archivo seleccionado por el usuario, y a la que hacemos referencia en varios de los fragmentos de código añadidos en los eventos de los controles.

Con esto ya habremos finalizado nuestra aplicación. Ejecuta la aplicación, pulsa sobre el botón Select File y prueba a modificar el tamaño de previsualización mediante el control Slider, y también a conmutar entre los modos de icono QuickLook (activo por omisión) o bien el uso del icono genérico para el tipo de archivo seleccionado.

Como verás, ¡la respuesta en la obtención del icono a diferentes tamaños es realmente rápida!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *