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!