Hace algunos meses publicamos una técnica para utilizar los símbolos de la fuente SF en macOS tal y como es posible cuando se utiliza el método Picture.SystemImage
en las apps de iOS. Sin embargo dicha técnica presentaba algunas desventajas: los glifos de los símbolos estaban definidos directamente en código, lo que significa que no es posible acceder a los nuevos símbolos añadidos de vez en cuando por Apple a la fuente SF. Además, tampoco era posible definir algunas propiedades como por ejemplo el peso o la escala del glifo. Continúa leyendo y te mostraré una forma más flexible de utilizar estos símbolos en macOS 11+.
De hecho, la nueva técnica propuesta es más parecida al funcionamiento del actual método Picture.SystemImage
que puedes usar en tus apps iOS de Xojo. Así, podrás definir los siguientes atributos:
- Nombre del Glifo. Puedes usar la utilidad Symbols SF de Apple para ver todos los símbolos disponibles así como el nombre asociado con cada uno de ellos.
- Tamaño (en puntos)
- Peso. El peso a utilizar en la fuente SF, comprendiendo desde Ultra-Light a Black. Este es un tipo de dato Enumerador.
- Escala. La escala del símbolo, comprendiendo de Small (pequeño) a Large (grande). Este es un valor de tipo Enumerador.
- TemplateColor. Si quieres tintar (colorear) el glifo resultante, entonces puedes proporcionar un parámetro ColorGroup (en las apps Desktop) o bien un valor de tipo de dato Color (apps Desktop y Consola).
- FallbackTemplateImage. Puedes proporcionar una imagen en escala de grises o blanco y negro a usar como imagen de respaldo en el caso de que no se pueda generar un Picture a partir del nombre de glifo proporcionado.
Tal y como ocurre en el actual método de iOS, recibirás una instancia de Picture. Por ejemplo:
Var c As New ColorGroup(Color.red, Color.White) Var p As picture = SystemImage("highlighter",120.0, SystemImageWeights.UltraLight, symbolscale.small,c, fallback) IVSFGlyph.Image = p
Siendo en este caso “fallback” una imagen que se ha añadido previamente al proyecto Desktop. Este fragmento de código resultará en el siguiente glifo asignado como imagen al control ImageView
con el nombre IVSFGlyph
:
Para añadir este tipo de funcionalidad a tus proyectos Xojo Desktop y de Consola en macOS, añade un nuevo Módulo al Navegador (por ejemplo, usando el nombre macOSLib). A continuación, y con el ítem macOSLib seleccionado en el Navegador, añade dos enumeradores con los siguientes valores:
- Name: SymbolScale
- Type: Integer
- Scope: Global
- Values:
Small = 1
Medium = 2
Large = 3
- Name: SystemImageWeights
- Type: Integer
- Values:
UltraLight = 0
Thin = 1
Light = 2
Regular = 3
Medium = 4
Semibold = 5
Bold = 6
Heavy = 7
Black = 8
Añadamos ahora al módulo macOSLib
el método que funcionará tanto en aplicaciones Desktop como de Consola en macOS:
- Method Name: SystemImage
- Parameters: name As String, size As Double, weight As SystemImageWeights = SystemImageWeights.Regular, scale As symbolScale = symbolScale.Medium, templateColor As color, fallbackTemplateImage As Picture = Nil
- Return Type: Picture
- Scope: Global
Haz clic en el icono de la rueda dentada en el Inspector para cambiar a la sección de atributos para dicho método, y asegúrate de seleccionar sólo las opciones mostradas en la siguiente imagen:
Escribe ahora el siguiente código en el Editor de Código asociado con el método recién creado:
#If TargetMacOS If System.Version >= "11.0" Then If name = "" Then Return Nil Declare Function Alloc Lib "Foundation" Selector "alloc" (classRef As ptr) As ptr Declare Sub AutoRelease Lib "Foundation" Selector "autorelease" (classInstance As ptr) Declare Function NSClassFromString Lib "Foundation" (clsName As CFStringRef) As ptr Declare Function ImageWithSystemSymbolName Lib "AppKit" Selector "imageWithSystemSymbolName:accessibilityDescription:" (imgClass As ptr, symbolName As CFStringRef, accesibility As CFStringRef) As ptr Declare Function ConfigurationWithPointSize Lib "AppKit" Selector "configurationWithPointSize:weight:scale:" (symbolConfClass As ptr, size As CGFloat, weight As CGFloat, tscale As SymbolScale) As ptr Declare Function ImageWithSymbolConfiguration Lib "AppKit" Selector "imageWithSymbolConfiguration:" (imgClass As ptr, config As ptr) As ptr Declare Function ColorWithRGBA Lib "Foundation" Selector "colorWithRed:green:blue:alpha:" (nscolor As ptr, red As CGFloat, green As CGFloat, blue As CGFloat, alpha As CGFloat) As ptr Declare Sub SetTemplate Lib "AppKit" Selector "setTemplate:" (imageObj As ptr, value As Boolean) Declare Sub LockFocus Lib "AppKit" Selector "lockFocus" (imageObj As ptr) Declare Sub UnlockFocus Lib "AppKit" Selector "unlockFocus" (imageObj As ptr) Declare Sub Set Lib "Foundation" Selector "set" (colorObj As ptr) Declare Sub NSRectFillUsingOperation Lib "AppKit" (rect As NSRect, option As UInteger) Declare Function RepresentationUsingType Lib "AppKit" Selector "representationUsingType:properties:" (imageRep As ptr, type As UInteger, properties As ptr) As ptr Declare Function InitWithFocusedView Lib "AppKit" Selector "initWithFocusedViewRect:" (imageObj As ptr, rect As NSRect) As ptr Var nsimage As ptr = NSClassFromString("NSImage") Var orImage As ptr = ImageWithSystemSymbolName(nsimage, name,"") Var symbolConfClass As ptr = NSClassFromString("NSImageSymbolConfiguration") // Obtenemos el peso de la fuente como valor float, tal y como requiere SystemImageWeight Var tWeight As CGFloat = SystemImageWeight(weight) // Creamos un objeto de configuración para el glifo Var symbolConf As ptr = ConfigurationWithPointSize(symbolConfClass, size,tWeight,scale) // Obtenemos la NSImagen final para la combinación del glifo con la configuración dada (aún en forma vectorial) Var finalImage As ptr = ImageWithSymbolConfiguration(orImage, symbolConf) // No es posible crear una imagen para el nombre de glifo recibido, de modo que se devuelve Nil en el caso de que no se haya proporcionad una imagen de respaldo. // O bien se colorea la imagen de respaldo en el caso de que se haya proporcionado una. If finalImage = Nil Then If fallbackTemplateImage = Nil Then Return Nil Var fallbackData As MemoryBlock = fallbackTemplateImage.ToData(Picture.Formats.PNG) Var fallbackDataPtr As ptr = fallbackData Declare Function DataWithBytesLength Lib "Foundation" Selector "dataWithBytes:length:" (dataClass As ptr, data As ptr, length As UInteger) As ptr If fallbackData <> Nil And fallbackData.Size > 0 Then Var NSDataClass As ptr = NSClassFromString("NSData") Var NSDataObj As ptr = DataWithBytesLength(NSDataclass, fallbackDataPtr, fallbackData.Size) If NSDataObj <> Nil Then Declare Function InitWithData Lib "AppKit" Selector "initWithData:" (imageInstance As ptr, data As ptr) As ptr Var NSImageClass As ptr = NSClassFromString("NSImage") finalImage = Alloc(NSImageClass) finalImage = InitWithData(finalImage, NSDataObj) AutoRelease(NSDataObj) End If End If End If If finalImage = Nil Then Return Nil Var c As Color Var nscolor As ptr LockFocus(finalImage) // Aplicamos el coloreado a la imagen en el caso de que hayamos recibido un objeto ColorGroup válido. c = templateColor nscolor = NSClassFromString("NSColor") Var tColor As ptr = ColorWithRGBA(nscolor, c.Red/255.0, c.Green/255.0, c.Blue/255.0, 1.0-c.Alpha/255.0) // Hemos de definir la propiedad Template de la NSImage a False para poder colorearla. SetTemplate(finalImage, False) Declare Function ImageSize Lib "AppKit" Selector "size" (imageObjt As ptr) As NSSize Var tRect As NSRect tRect.Origin.x = 0 tRect.Origin.y = 0 tRect.RectSize = ImageSize(finalImage) Set(tColor) NSRectFillUsingOperation(tRect,3) // Obtenemos la representación como imagen de mapa de bits para extraer los datos en formato PNG. Var NSBitmapImageRepClass As ptr = NSClassFromString("NSBitmapImageRep") Var NSBitmapImageRepInstance As ptr = Alloc(NSBitmapImageRepClass) Var newRep As ptr = InitWithFocusedView(NSBitmapImageRepInstance, tRect) UnlockFocus(finalImage) Var data As ptr = RepresentationUsingType(newRep,4, Nil) // 4 = PNG AutoRelease(newRep) AutoRelease(nscolor) // Obtenemos los datos de la imagen para generar el objeto Picture en Xojo. 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) // Necesitamos obtener la cantidad de datos disponibles… Var dlen As Integer = DataLength(data) // …para crear un MemoryBlock del tamaño correcto. Var mb As New MemoryBlock(dlen) Var mbPtr As ptr = mb // Y ahora podemos volcar los datos PNG del objeto NSData al MemoryBlock. GetDataBytes(data,mbPtr,dlen) // De modo que obtengamos un Picture a partir de dicho MemoryBlock. Return Picture.FromData(mb) End If #EndIf
Como puedes ver, este método llama al método SystemImageWeight
, y también hace uso de algunas Estructuras
. Añadamos en primer lugar al Módulo el método encargado de convertir el valor de enumeración recibido al valor CGFloat
requerido posteriormente por el Declare correspondiente:
- Method Name: SystemImageWeight
- Parameters: weight As SystemImageWeights
- Returned Type: CGFloat
- Scope: Global
Y escribe el siguiente código en el Editor de Código asociado:
Var tWeight As CGFloat = 0 Select Case weight Case SystemImageWeights.UltraLight tWeight = -1.0 Case SystemImageWeights.Thin tWeight = -0.75 Case SystemImageWeights.Light tWeight = -0.5 Case SystemImageWeights.Regular tWeight = -0.25 Case SystemImageWeights.Medium tWeight = 0 Case SystemImageWeights.Semibold tWeight = 0.25 Case SystemImageWeights.Bold tWeight = 0.5 Case SystemImageWeights.Heavy tWeight = 0.75 Case SystemImageWeights.Black tWeight = 1 End Select Return tWeight
Y añadamos ahora tres nuevas Estructuras al módulo macOSLib utilizando los siguientes valores:
- Structure Name: NSOrigin
- Scope: Global
X as CGFloat
Y as CGFloat
- Structure Name: NSSize
- Scope: Global
Height as CGFloat
Width as CGFloat
- Structure Name: NSRect
- Scope: Global
Origin as NSOrigin
RectSize as NSSize
Añadamos a continuación un método sobrecargado dirigido únicamente a las apps macOS Desktop. La principal diferencia es que este recibirá un parámetro ColorGroup
en vez de un tipo de dato Color
object:
- Method Name: SystemImage
- Parameters: name As String, size As Double, weight As SystemImageWeights = SystemImageWeights.Regular, scale As symbolScale = symbolScale.medium, templateColor As ColorGroup = Nil, fallbackTemplateImage As Picture = Nil
- Returned Type: Picture
- Scope: Global
Y escribe ahora el siguiente código en el Editor de Código asociado al método:
Var c As Color If templateColor <> Nil Then c = templateColor End If Return SystemImage(name,size,weight,scale,c,fallbackTemplateImage)
Haz clic en el icono de la rueda dentada en el Panel Inspector para el método, definiendo los atributos tal y como se muestra en la siguiente imagen:
Asignar los Glifos SF directamente a las Vistas
Añadamos ahora un tercer método al módulo cuya principal diferencia es que no devolverá un Picture. En vez de ello, asignará el glifo SF indicado como imagen sobre el handler del control recibido. Por ejemplo, esto facilita las cosas cuando se quiera asignar un símbolo SF a un botón en la interfaz de usuario (entre otros controles).
Esta es la firma del nuevo método:
- Name: SystemImage
- Parameters: name As String, size As Double, weight As SystemImageWeights = SystemImageWeights.Regular, scale As SymbolScale = SymbolScale.small, controlHandler as integer
- Scope: Global
Y el código que debe escribirse en el Editor de Código asociado:
#If TargetMacOS If System.Version >= "11.0" Then If name = "" Then Exit Declare Function Alloc Lib "Foundation" Selector "alloc" (classRef As ptr) As ptr Declare Sub AutoRelease Lib "Foundation" Selector "autorelease" (classInstance As ptr) Declare Function NSClassFromString Lib "Foundation" (clsName As CFStringRef) As ptr Declare Function ImageWithSystemSymbolName Lib "AppKit" Selector "imageWithSystemSymbolName:accessibilityDescription:" (imgClass As ptr, symbolName As CFStringRef, accesibility As CFStringRef) As ptr Declare Function ConfigurationWithPointSize Lib "AppKit" Selector "configurationWithPointSize:weight:scale:" (symbolConfClass As ptr, size As CGFloat, weight As CGFloat, scale as SymbolScale) As ptr Declare Function ImageWithSymbolConfiguration Lib "AppKit" Selector "imageWithSymbolConfiguration:" (imgClass As ptr, config As ptr) As ptr Var nsimage As ptr = NSClassFromString("NSImage") Var orImage As ptr = ImageWithSystemSymbolName(nsimage, name,"") Var symbolConfClass As ptr = NSClassFromString("NSImageSymbolConfiguration") // Obtenemos el peso de la fuente como un valor float requerido por SystemImageWeight. Var tWeight As CGFloat = SystemImageWeight(weight) // Creamos un objeto de configuración para el glifo. Var symbolConf As ptr = ConfigurationWithPointSize(symbolConfClass, size,tWeight,scale) // Obtenemos la instancia NSImage final para la combinación de glifo y objeto de configuración (aun en forma vectorial). Var finalImage As ptr = ImageWithSymbolConfiguration(orImage, symbolConf) // Necesitamos saber si el Handler recibido se corresponde con un objeto que pueda responder al mensaje setImage. Declare Function RespondsToSelector Lib "/usr/lib/libobjc.A.dylib" Selector "respondsToSelector:" (obj As Integer, sel As ptr) As Boolean Declare Function NSSelectorFromString Lib "Foundation" (sel As CFStringRef) As ptr Var sel As ptr = NSSelectorFromString("setImage:") If controlHandler <> 0 And finalImage <> Nil And RespondsToSelector(controlHandler, sel) Then Declare Sub Set Lib "AppKit" Selector "setImage:" (control As Integer, Image As ptr) // Y asignamos el objeto NSImage al control recibido. Set(controlHandler,finalImage) End If End If #EndIf
De modo que, por ejemplo, puedes asignar el símbolo Gear (rueda dentada) como la imagen de un control PushButton
añadido a la interfaz de usuario de la app usando el siguiente código en el Evento Open
de la instancia PushButton
:
SystemImage("gearshape.2",14.0, SystemImageWeights.Regular,SymbolScale.Small,Me.Handle)
Resumen
¡Y eso es todo! Como puedes ver, no se incluyen los valores de los glifos directamente en el código, además de que podrás crear un Picture a partir de cualquier símbolo SF con el tamaño, peso y escala deseados. Además, podrás colorearlo y recibir la imagen de respaldo si algo va mal en el proceso.
Puedes descargar el proyecto de ejemplo Xojo con el módulo ya creado y listo para usar desde este enlace.