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
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!
en el ejemplo da un error en la linea
CustomDesktopCanvas1.Image = p del paso 4
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
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
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!