Vertically centering text in a rectangle

This is a black, transparent rectangle drawn on a canvas in a Desktop app. The contents of the text and the position, size, text color, background color and transparency all live in a class object. There can be multiple objects like this on the canvas.

What I can’t seem to figure out is how to get the bounding box to be exactly at the top and bottom of the text, including descenders - there’s always padding at the top, and the bottom of the rectangle is in line with the text baseline, not the bottom of the lowest descender. What am I doing wrong here?

(mRects is an array of BITCObjects, which contain the text attributes)

//Add the on-screen objects
For Each r As BITCObject In mRects
  //Set the font size in pixels
  g.FontUnit = FontUnits.Pixel
  g.FontName = "Courier"
  
  //The size of the font is stored in a BITCObject as a percentage of the height
  //of the canvas (r.Scale), not as a point or pixel size
  //Calculate the pixel size from that percentage
  g.FontSize = LayoutCanvas.Height * (r.Scale / 100)
  
  //change the width and height of the bounding box to match the text
  r.Width = g.TextWidth(r.Text)
  r.Height = g.FontAscent
  //using fontAscent for height because g.fontSize results in a 
  //rectangle that's about 2x taller than needed
  
  
  //draw outer rectangle
  g.Transparency = 0
  g.PenSize = 1
  
  if r.isSelected = true then 
    g.DrawingColor = Color.Red
    r.isSelected = false
  else
    g.DrawingColor = Color.Gray
  end if
  
  
  //Fill the rectangle
  g.DrawingColor = r.bgColor
  g.Transparency = r.bgTransparency
  g.FillRectangle(r.x, r.y, r.Width, r.Height)
  g.DrawRectangle(r.X, r.Y, r.Width,  r.Height )
  
  
  //Draw the text to screen
  g.DrawingColor = r.fgColor
  g.Transparency = r.fgTransparency
  g.DrawText(r.Text, r.x, r.y + g.FontAscent)
  
  
  //Draw the Text Align Box
  g.DrawingColor = color.Gray
  g.FillRectangle(r.x, r.y, r.DragBoxWidth, r.DragBoxHeight)
  
  //format the label text (All of this is outside the filled rect, and not really relevant)
  g.FontUnit = FontUnits.Point
  g.FontSize = r.LabelFontHeight
  g.DrawingColor = color.gray
  //Get the plain english name for the field
  var lbl as string = GetKeyFromValue(textTypes,r.Type)
  //Add the text
  if r.y < (LayoutCanvas.Height * 0.1) then
    //too close to the top, move text below box)
    g.DrawText(lbl, r.x, r.y + r.Height + 5 + g.FontSize)
  else
    g.DrawText(lbl, r.x, r.y - 5)
  end if 
  
  
Next

Any idea why the code above isn’t formatted (color, etc) and is just showing as preformatted text? I selected it and hit the code wraper icon in the toolbar. It works in this comment, but not in the parent post:


var x as integer = 1

That’s just the way fonts work. They typically have padding built-in to make sure the glyphs render correctly - for example, they leave room for  even though you aren’t using that character in your test. That means that a capital A will have some space above it for that diacritical mark. The same is true for descenders.

If you want to work around this, you could try using an Object2D TextShape object, but I don’t know if it will provide any better results.

1 Like

Here you would also need the FontDescent, but the Graphics Object is missing such a Property.

2 Likes

You can get the descent via Graphics.FontHeight - Graphics.FontAscent.

2 Likes

Aha. I’ll give that a try!

Would this type of approach help? Send it a TextShape and it returns a Dictionary with the line height, text width, and text height. I’m not sure what kind of precision it has.

Public Function textWH(txshape As TextShape) As Dictionary
  
  Var p As New Picture(100, 100, 32) ' any size will do
  
  p.Graphics.FontSize=txshape.FontSize
  p.Graphics.Bold=txshape.Bold
  p.Graphics.FontName=txshape.FontName
  p.Graphics.Italic=txshape.Italic
  p.Graphics.Rotate(txshape.Rotation)
  p.Graphics.Scale(txshape.Scale,txshape.Scale)
  p.Graphics.Underline=txshape.Underline
  
  Var rdict As Dictionary = New Dictionary
  rdict.Value("lh")=p.Graphics.TextHeight("line",1000)  ' height of one line
  rdict.Value("tw")= p.Graphics.TextWidth(txshape.Text) ' text width
  rdict.Value("th")= p.Graphics.TextHeight(txshape.Text,rdict.Value("tw")) ' text height
  Return rdict
  
End Function

I have made use of GMImage from the Monkeybread plugins to get the exact bounds based on where the pixels ended up, rather than where the font suggests they will.

I create a picture, draw the text, then get the bounding values

Took the following from my code, see if it helps


dim gm as new GMImageMBS(thepicture)
dim k as string = gm.boundingBox.StringValue  //can be examined in debugger

pseudowidth = gm.boundingBox.width - gm.boundingBox.xOff
pseudoheight = gm.boundingBox.yOff *2 + gm.boundingBox.height

You can also try to offset using the fontsize indtead of textheught.

So I think you mean Graphics.TextHeight. Turns out there’s no need to offset if all I do is:

r.Height = g.TextHeight

The end result looks like this:

That’s centered enough for my purposes.

1 Like

Oh gosh - sure enough, you were using FontAscent in your original code. I should have picked that up earlier. Glad it worked out in the end!