Graphics: Texto alineado a la Derecha y Centrado

El método DrawText de la clase Graphics ofrece una vía simple de alinear texto a la izquierda a partir de las coordenadas X e Y proporcionadas, pudiendo proporcionar incluso un valor máximo de ajuste de ancho (wrap) para cada una de las líneas que componen el bloque de texto. ¿No sería genial poder hacer lo mismo alineando nuestros bloques de texto a la derecha o bien centrados? Continúa leyendo y te mostraré una técnica que puedes usar como punto de partida, de modo que puedas refinarlo para que se adapte mejor a tus propias necesidades.

1. Extensiones Gráficas

Texto alineado a derecha y centrado tanto en un Picture como en un PDF creado con PDFDocument, sobre Linux Desktop

Para empezar, y dado que queremos proporcionar esta capacidad a cualquier contexto gráfico, tiene sentido que incluyamos toda la lógica en un módulo que llamaremos, por ejemplo, GraphicExtextensions (como de costumbre, puedes utilizar cualquier otro nombre que prefieras). Así que inicia un nuevo proyecto Xojo y añádele un nuevo Módulo utilizando los siguientes valores en el Panel Inspector:

  • Name: GraphicsExtensions

A continuación, y con el ítem GraphicsExtensions seleccionado en el Navegador, añádele un nuevo método utilizando los siguientes valores en el Panel Inspector asociado:

  • Method Name: DrawText
  • Parameters: Extends g As Graphics, Value As String, x As Double, y As Double, Wrap As Double, alignment As TextAlignments = TextAlignments.Left
  • Scope: Global

Como puedes ver, prácticamente replica el método DrawText estándar de la clase Graphics. Algunos detalles a observar en este caso, estamos utilizando la palabra clave Extends en combinación con el primer parámetro para indicar al compilador que dicho método “ampliará” la funcionalidad existente de la clase Graphics, de modo que podrás invocarlo utilizando la notación por punto habitual en cada una de las instancias creadas a partir de la clase Graphics.

El segundo detalle se encuentra en el último de los parámetros: “alignment As TextAlignments”. TextAlignments es una enumeración global que utilizaremos para saber la alineación de texto que el usuario (o código) ha de aplicar sobre el bloque de texto.

Escribe a continuación el siguiente fragmento de código en el Editor de Código asociado con el método:

#Pragma DisableBackgroundTasks
#Pragma DisableBoundsChecking
#Pragma NilObjectChecking False

value = value.ReplaceLineEndings(EndOfLine)

Select Case Alignment
  
Case TextAlignments.Default, TextAlignments.Left
  
  g.DrawText value,x,y+g.TextHeight,Wrap-x
  
Case TextAlignments.Center, TextAlignments.Right
  
  Var tOutput() As String = PrepareOutput(g, value, wrap, x)
  
  Var tx, ty As Double
  ty = y+g.TextHeight
  
  Select Case Alignment
    
  Case TextAlignments.Center
    
    For Each s As String In tOutput
      
      tx = If(Wrap = 0,((g.Width-Wrap)/2 - g.TextWidth(s)/2) + (x/2), (Wrap/2 - g.TextWidth(s)/2)+(x/2))
      g.DrawText s,tx,ty
      ty = ty + g.TextHeight
      
      If ty > g.Height Then Exit
      
    Next
    
  Case TextAlignments.Right
    
    For Each s As String In tOutput
      
      tx = If(Wrap = 0,(g.Width-g.TextWidth(s)) + x/2, (Wrap-x) - g.TextWidth(s) + x)
      g.DrawText s, tx, ty
      ty = ty + g.TextHeight
      
      If ty > g.Height Then Exit
      
    Next
    
  End Select
  
End Select

Como puedes ver, el código es bastante simple. Observamos el valor del parámetro Alignment para ver cuál debe ser el camino a seguir. Así, si Alignment es igual a Default o Left, entonces simplemente nos limitamos a llamar al método DrawText estándar sobre el contexto gráfico recibido y representado por la variable “g”. Si no, tendremos que hacer algunas operaciones adicionales sobre el bloque de texto recibido para crear los fragmentos de línea que “quepan” en el ancho disponible y teniendo en cuenta también el valor de wrap o “vuelta de carro”. Esto es algo que haremos mediante el método PrepareOutput.

Una vez que tenemos todas las líneas con los anchos adecuados en el array Output, sólo necesitamos aplicar algunos cálculos sencillos para calcular la coordenada TX final para cada línea, e incrementar también la coordenada TY en cada una de las líneas a dibujar. Como puedes ver, y para acelerar un poco más las cosas, comprobamos si la coordenada TY excede el área visible del contexto gráfico y, si es así, salimos del método. Después de todo no tendría mucho sentido continuar dibujando texto que no se va a ver pero cuyo procesado sí consume tiempo.

Por supuesto, el cálculo de las coordenadas TX y TY dependen del valor del parámetro Alignment; esto es, si el bloque de texto se va a dibujar alineado a la derecha o centrado.

2. Calcular el ancho de cada línea

Añadamos ahora el método PrepareOutput al método GraphicsExtensions utilizando los siguientes valores en el Panel Inspector asociado:

  • Method Name: PrepareOutput
  • Parameters: g As Graphics, value As string, wrap As Double, x As Double
  • Return Type: String()
  • Scope: Private

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

#Pragma DisableBoundsChecking
#Pragma DisableBackgroundTasks
#Pragma NilObjectChecking False

Var totalWidth As Double = If(Wrap = 0, g.Width-x, Wrap-x)

Var Input() As String = value.ToArray(EndOfLine)
Var output() As String

For Each s As String In Input
  
  If g.textwidth(s) <= totalWidth Then
    output.add s
  Else
    AdjustWidth(g,s,totalWidth,output)
  End If
  
Next

Return output

Nuevamente, las primeras líneas son algunos pragmas para acelerar las cosas y, a continuación, la variable totalWidth almacenará el valor de ancho máximo requerido en el procesado de cada línea. El array Input contendrá cada párrafo del texto fuente, mientras que el array Output contendrá cada línea procesada y que, por tanto, será el devuelto al método que hubiese llamado a PrepareOutput.

A continuación sólo hemos de iterar por cada entrada en el array Input para comprobar si su ancho cumple con el requerimiento de totalWidth. Si es así, se añadirá la entrada en el array Output; si no, significa que el párrafo aun ha de dividirse en tantos fragmentos de texto como sea necesario hasta que cada uno de ellos cumpla el ancho máximo requerido. Esto es algo de lo que se ocupará el método AdjustWidth.

3. Dividiendo trozos de texto… de forma recursiva

Añade el tercer y último método requerido a nuestro módulo GraphicExtensions utilizando los siguientes valores en el Panel Inspector asociado:

  • Method Name: AdjustWidth
  • Parameters: g As Graphics, s As String, width As Double, byref output() As String
  • Scope: Private

A continuación, escribe las siguientes líneas de código en el Editor de Código asociado:

#Pragma DisableBoundsChecking
#Pragma DisableBackgroundTasks
#Pragma NilObjectChecking False

Var n As Integer

If g.TextWidth(s) <= width Then
  output.add s
Else
  
  // This can be improved pre-calculating the initial value for "n"… so it's
  // left as an exercise for the reader :-)
  
  While round(g.TextWidth(s.Left(s.Length-n))) > width
    n = n + 1
  Wend
  
  output.add s.Left(s.Length-n)
  
  AdjustWidth(g,s.Right(n),width,output)
  
End If

Como puedes ver, si el ancho de la cadena recibida es inferior o igual al ancho esperado, entonces se añade al array Output; de lo contrario, utilizamos aquí una técnica no muy óptima para calcular la longitud adecuada, guardando la cadena resultante en el array Output… y pasando el sobrante de la cadena de nuevo al método.

4. ¡Vamos a probarlo!

Ya tenemos todo lo necesario, así que vamos a probarlo. Comienza añadiendo una nueva constante a la ventana Window1 y nómbrala kSampleText. Usa el Panel Inspector asociado para asignar el bloque de texto que quieras usar para las pruebas (personalmente tiendo a utilizar el sitio web https://www.lipsum.com website para este propósito).

Si lo prefieres, puedes descargar el proyecto de ejemplo desde este enlace.

Añade a continuación el Manejador de Evento Opening a la ventana Window1 y escribe el siguiente fragmento de código en el Editor de Código asociado:

Var p As New Picture(612,792)
Var d As New PDFDocument

Var g As Graphics = p.Graphics
g.FontName = "Helvetica"
Var gPDF As Graphics = d.Graphics

g.DrawText(kSampleText,40,10,570,TextAlignments.Right)
gPDF.DrawText(kSampleText,40,10,570,TextAlignments.Right)

Var tH As Double = g.TextHeight(kSampletext,570) + g.TextHeight * 4
Var tPH As Double = gPDF.TextHeight(ksampletext,570) + gpdf.TextHeight * 4

g.DrawText(kSampleText,40,th,570,TextAlignments.center)
gPDF.DrawText(kSampleText,40,tPh,570,TextAlignments.center)

CustomDesktopCanvas1.Image = p
d.Save(SpecialFolder.Desktop.Child("PDFTextAlignment.pdf"))

Como puedes ver, en este caso he elegido el nombre de fuente “Helvetica”… pero puedes cambiarlo por cualquier otro que esté disponible en tu sistema operativo.

La clave aquí es que estamos pasando a nuestro método “extendido” el texto de muestra y la alineación que queremos utilizar en cada caso tanto para dibujar sobre el contexto gráfico de un Picture y también sobre el correspondiente a una instancia de PDFDocument.

También verás que se asigna la imagen a una subclase de Canvas (CustomDesktopCanvas1). Esta subclase se ha creado de forma que tenga una propiedad “Image” de tipo “Computed Property”, de modo que cuando se asigna un nuevo valor sobre ella se invoca al método Refresh para actualizar los contenidos de la instancia. Luego, en el evento Paint, lo único que se hace es comprobar que la propiedad Image no esté a Nil. Si es así, simplemente se dibuja su contenido centrado sobre la superficie del Canvas (puedes descargar el proyecto de ejemplo para ver como funciona con más detalle).

Ejecuta el proyecto de ejemplo y deberías de obtener un resultado similar al mostrado en las capturas de pantalla. ¡Eso es todo! Por supuesto aun quedan algunos detalles por pulir como, por ejemplo, cuando una letra de una palabra queda suelta en la línea siguiente, y ese tipo de cosas pero, como se ha indicado al inicio de este artículo, este es sólo un punto de partida que puedes adaptar y mejorar de forma que se adapte a tus propias necesidades.

¡Diviértete!

4 comentarios en “Graphics: Texto alineado a la Derecha y Centrado

  1. huitzi mora

    en el ejemplo da un error en la linea
    CustomDesktopCanvas1.Image = p del paso 4

    1. huitzi mora

      mainWindow.Opening, line 17
      This item does not exist
      CustomDesktopCanvas1.Image = p
      /////////
      mainWindow.Opening, line 17
      There is more than one method with this name but this does not match any of the available signatures.
      CustomDesktopCanvas1.Image = p

      1. huitzi mora

        he seguido los pasos al pie de la letra para poder ocupar esto en un proyecto, pero me he topado con esto, espero puedas orientarme en saber que he hecho mal. Gracias de antemano

      2. Javier Rodriguez

        Hola!

        He añadido la explicación sobre “CustomDesktopCanvas1” en el texto y también un enlace para que se pueda descargar el proyecto de ejemplo completo.

        Gracias!

Deja un comentario

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