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.
Índice
1. Extensiones Gráficas
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
- 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
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 SelectComo 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étodoPrepareOutput
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
#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 outputNuevamente, 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
#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 IfComo 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 ventanaWindow1
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!