Centred multiline text with drawstring

General background:

I have an object-based canvas where I draw circles, boxes, etc etc
One of the objects is text.
Right now, I have a rectangle area in which to render the text.
I find a suitable font size that fits within the area , and use .drawstring (API1) to draw the text within the rectangle, multiline if need be.

g.drawString(theText, left, top + g.textHeight, width)

Customers are asking for more flexibility - can I center the lines for example?
When using drawstring, I get left justified only.

(RTF would probably be best but too complex for now)

Does anyone have a method that would take long text and render into a rect as centred?
Or suggestions?

[quote=“Jeff Tullin, post:1, topic:85809, username:Jeff_Tullin”]
I use this:

Var pict = New Picture (  Me.Width   , Me.Height   , 32 )  

Var r_texte As New TextShape  
r_texte.FontName = "Helvetica"
r_texte.FontSize = 18
r_texte.FontSize = Me.FontSize
r_texte.Value = "bonjour"
r_texte.VerticalAlignment = TextShape.Alignment.Center
r_texte.HorizontalAlignment = TextShape.Alignment.Center
r_texte.FillOpacity = 100 // The opacity of the interior, from 0 (completely transparent) to 100 (opaque).
r_texte.FillColor = Me.TextColor
r_texte.BorderWidth = 0

pict.Graphics.DrawObject ( r_texte , Me.Width / 2 , Me.Height / 2 )
1 Like

Drawing a title centered at the top of a PDF I use:

Var pagewidth As Double = pdf.PageWidth
Var textwidth As Double = pdf.Graphics.TextWidth("System Profile")
g.DrawText("System Profile",(pagewidth - textwidth)/2,30 + ascent)

Not multi-line, though.

I solved that in a very old project by parsing the input string and separating each word.
Do a Preprocess to figure out the width of each word and know when to go to the next line. Also calculating the X position of each line.

Then draw each word at the correct X position to allow the line to be centred.

You’ll need to draw the text line by line, each line offset by half its own length from the desired center.

You’ll need to draw the text line by line, each line offset by half its own length from the desired center.

Thanks. I knew this, was looking for a method that already had the code , as I had tried and failed.
I do now have a couple of good suggestions, and will be exprimenting with these shortly.
Thanks to all who reached out.

this works fine for me.

Public Sub DrawCenteredString(extends g as Graphics, theString as string, x as Integer, y as Integer, rectWidth as Integer,rectHeight as Integer)
  If theString <> "" And rectWidth>0 Then
    theString = theString.NormalizeEndOfLines
    
    Dim txw As Integer = g.TextWidth( theString)
    Dim txh As Integer = g.TextHeight( theString, rectWidth)
    Dim displayWidth As Integer = rectWidth
    If txh>rectHeight Then
      txw = rectWidth-4
      displayWidth = txw
      txh = rectHeight-4
    End If
    Dim calcx As Integer = Round(x+((rectWidth-txw)/2))
    Dim calcy As Integer = y+g.TextHeight
    calcy = Floor( calcy)
    
    Dim lineCount As Integer = theString.CountLines
    If lineCount = 1 Then
      ' vérifie que la chaine tient sur une seule ligne
      If txw > rectWidth Then
        lineCount = txw \ rectWidth
        If lineCount>1 Then
          ' insertion de EndOfLine aux endroits requis
          Dim largChar As Integer = txw/Len(theString)
          Dim nbCharParLigne As Integer = rectWidth / largChar
          Dim lines() As String
          Dim ch As String = theString
          Do
            lines.Append Left( ch, nbCharParLigne)
            ch = Right( ch, Len(ch)-nbCharParLigne)
          Loop Until Len(ch)=0
          theString = Join( lines, EndOfLine)
          
        End If
      End If
    End If
    
    if lineCount=1 then
      g.DrawText( theString, calcx,calcy, displayWidth, True)
    else
      dim cy as Integer = y+((rectHeight-txh)/2)
      dim chh as Integer = g.TextHeight( theString, displayWidth)/lineCount
      for i as Integer=1 to lineCount
        dim ch as String = NthField( theString, theString.DetectEndOfLine, i)
        Dim chw As Integer = g.TextWidth( ch)
        calcx = Round(x+((rectWidth-chw)/2))
        g.DrawText( ch, calcx,cy+(i*chh)-g.TextDescent, displayWidth, True)
      next
    end if
  end if
  
End Sub

and

Public Function CountLines(extends s as String) As Integer
  If s="" Then Return 0
  
  Dim m As Integer = Max( CountFields( s, EndOfLine.Macintosh), CountFields( s, EndOfLine.UNIX), CountFields( s, EndOfLine.Windows), CountFields( s, EndOfLine.OSX))
  Return m
  
End Function

1 Like

oups also need this !

Public Function NormalizeEndOfLines(extends s as String) As String
  Dim unusedChar As String
  Dim foundChar As Boolean = False
  
  For i As Integer = 1 To 27
    unusedChar = Chr(i)
    If s.InStrB( unusedChar)=0 Then
      foundChar = True
      Exit For
    End If
  Next
  
  If foundChar Then
    s = ReplaceAllB( s, EndOfLine.Macintosh, unusedChar)
    s = ReplaceAllB( s, EndOfLine.UNIX, unusedChar)
    s = ReplaceAllB( s, EndOfLine.Windows, unusedChar)
    s = ReplaceAllB( s, EndOfLine.OSX, unusedChar)
    
    s = ReplaceAllB( s, unusedChar, EndOfLine.Macintosh)
  End If
  
  ' check for EndOfLine with nothing after
  
  While s.EndsWithVNS( EndOfLine.Macintosh)
    s = Left( s, Len( s)-1)
  Wend
  
  Return s
  
End Function

same method polished by claude …

Public Sub DrawCenteredString(extends graphicsContext as Graphics, textToDisplay as string, rectangleX as Integer, rectangleY as Integer, rectangleWidth as Integer, rectangleHeight as Integer)
  // Draws text centered within a specified rectangle, with automatic line wrapping if needed
  //
  // Parameters:
  // - graphicsContext: The Graphics object to draw on (extended by this method)
  // - textToDisplay: The string to be drawn (empty strings are ignored)
  // - rectangleX: Left edge X coordinate of the containing rectangle
  // - rectangleY: Top edge Y coordinate of the containing rectangle  
  // - rectangleWidth: Width of the containing rectangle (must be > 0)
  // - rectangleHeight: Height of the containing rectangle
  //
  // Behavior:
  // - Text is horizontally and vertically centered within the specified rectangle
  // - If text is too wide, it will be automatically wrapped to multiple lines
  // - If text is too tall for the rectangle, display area is reduced by 4 pixels padding
  // - Single lines are drawn using standard centering
  // - Multiple lines are drawn with each line individually centered
  
  // Only proceed if we have valid text and a positive width
  If textToDisplay <> "" And rectangleWidth > 0 Then
    // Normalize line endings to ensure consistent behavior across platforms
    textToDisplay = textToDisplay.NormalizeEndOfLines
    
    // Calculate the initial text dimensions
    Dim textWidth As Integer = graphicsContext.TextWidth(textToDisplay)
    Dim textHeight As Integer = graphicsContext.TextHeight(textToDisplay, rectangleWidth)
    Dim availableDisplayWidth As Integer = rectangleWidth
    
    // If text is too tall for the rectangle, reduce display area by 4 pixels padding
    If textHeight > rectangleHeight Then
      textWidth = rectangleWidth - 4
      availableDisplayWidth = textWidth
      textHeight = rectangleHeight - 4
    End If
    
    // Calculate horizontal center position for the text
    Dim centeredX As Integer = Round(rectangleX + ((rectangleWidth - textWidth) / 2))
    
    // Calculate vertical position (using TextHeight for baseline positioning)
    Dim centeredY As Integer = rectangleY + graphicsContext.TextHeight
    centeredY = Floor(centeredY)
    
    // Determine how many lines the text currently has
    Dim numberOfLines As Integer = textToDisplay.CountLines
    
    // Handle single-line text that might be too wide
    If numberOfLines = 1 Then
      // Check if the text is too wide for the rectangle
      If textWidth > rectangleWidth Then
        // Calculate how many lines we'll need
        numberOfLines = textWidth \ rectangleWidth
        
        If numberOfLines > 1 Then
          // Break the text into multiple lines by character count
          // Calculate average character width and characters per line
          Dim averageCharacterWidth As Integer = textWidth / Len(textToDisplay)
          Dim charactersPerLine As Integer = rectangleWidth / averageCharacterWidth
          Dim textLines() As String
          Dim remainingText As String = textToDisplay
          
          // Split text into chunks of calculated character length
          Do
            textLines.Append Left(remainingText, charactersPerLine)
            remainingText = Right(remainingText, Len(remainingText) - charactersPerLine)
          Loop Until Len(remainingText) = 0
          
          // Rejoin the lines with proper line endings
          textToDisplay = Join(textLines, EndOfLine)
        End If
      End If
    End If
    
    // Draw the text based on whether it's single or multi-line
    If numberOfLines = 1 Then
      // Single line: draw directly at calculated center position
      graphicsContext.DrawText(textToDisplay, centeredX, centeredY, availableDisplayWidth, True)
    Else
      // Multi-line: calculate starting Y position and draw each line separately
      Dim multiLineStartY As Integer = rectangleY + ((rectangleHeight - textHeight) / 2)
      Dim lineHeight As Integer = graphicsContext.TextHeight(textToDisplay, availableDisplayWidth) / numberOfLines
      
      // Draw each line individually, centering each line horizontally
      For lineIndex As Integer = 1 To numberOfLines
        Dim currentLine As String = NthField(textToDisplay, textToDisplay.DetectEndOfLine, lineIndex)
        Dim currentLineWidth As Integer = graphicsContext.TextWidth(currentLine)
        
        // Calculate horizontal center position for this specific line
        centeredX = Round(rectangleX + ((rectangleWidth - currentLineWidth) / 2))
        
        // Draw the current line at the calculated position
        graphicsContext.DrawText(currentLine, centeredX, multiLineStartY + (lineIndex * lineHeight) - graphicsContext.TextDescent, availableDisplayWidth, True)
      Next
    End If
  End If
End Sub

Ever heard of ReplaceLineEndings?

I made this method a looooooong time ago… and surely before xojo 2019r2…

ReplaceLineEndings has existed much longer than that. I’m sure it was in RealBasic.

I logged a request for this a few years ago:
#64833

Unfortunately, to split a paragraph into lines with word wrapping that works across multiple scripts / languages is complicated. We have probably spent over 40 days of time getting this working.
On macOS we use NSTextContainerMBS / NSLayoutManagerMBS / NSTextStorageMBS
On MS-Windows with the Gdi Xojo framework we use the Gdi and Uniscribe APIs via Declares
On MS-Windows with the Direct2D Xojo framework we use the Gdi and DirectWrite APIs via Declares