1

I have a couple of methods that draw outlined text. The details of this are unimportant, but it serves to illustrate the problem:
(source code from Graphics DrawPath produces unexpected results when rendering text)

Private Sub FillTextSolid(g As Graphics, bounds As RectangleF, text As String, font As Font, fillColor As Color, sf As StringFormat)
    Using gp As GraphicsPath = New GraphicsPath(),
                                brush As New SolidBrush(fillColor)
        gp.AddString(text, font.FontFamily, font.Style, font.Size, bounds, sf)

        g.FillPath(brush, gp)
    End Using
End Sub

correctly converts a long string into one with an ellipsis inside the bounds. E.g.

Manic Miner is a platform video game originally written for the ZX Spectrum by Matthew Smith and released by Bug-Byte in 1983. It is the first game in the Miner Willy series and among the early titles in the platform game genre.

becomes:

Manic Miner is a platform video game originally written for the ZX Spectrum by Matthew Smith and released by Bug-Byte in 1983. It is the first game in the Miner...

All well and good. What I need is a way in code to see exactly what part of the full text has been displayed. This will then be used to cycle through the text in the same bounds (almost paging if you will) to display all the text.

I looked at MeasureString but this didn't seem to achieve this. Is there any way I can discern this? In pseudo code, something like:

Dim textShown as string =  gp.AddString(text, font.FontFamily, font.Style, font.Size, bounds, sf).TextShown

Thanks

Jimi
  • 29,621
  • 8
  • 43
  • 61
stigzler
  • 793
  • 2
  • 12
  • 29
  • [Graphics.MeasureCharacterRanges](https://learn.microsoft.com/en-us/dotnet/api/system.drawing.graphics.measurecharacterranges) -- You should link the post where that code comes from :) – Jimi Sep 02 '21 at 14:47
  • Heh. You're becoming my GDI saviour, Jimi! Fair point about the reference - added. Have looked into `MeasureCharacterRanges` and I'm struggling to see how this relates to the question. My reading of this is that if you pass in some CharacterRanges (e.g. `New CharacterRange(0,5)` would reference the chards "Manic" in my string) then `MeasureCharacterRanges` returns the bounds of those characters as drawn? That is, I'd get a `RectangleF` around the letters "Manic". What I'm looking for is what letters have been rendered within the ascribed `rectangle` in `bounds` – stigzler Sep 02 '21 at 15:43

1 Answers1

1

Given the FillTextSolid() method shown before in:
Graphics DrawPath produces unexpected results when rendering text

Private Sub FillTextSolid(g As Graphics, bounds As RectangleF, text As String, font As Font, fillColor As Color)
    Using gp As GraphicsPath = New GraphicsPath(),
        brush As New SolidBrush(fillColor),
        format = New StringFormat(StringFormat.GenericTypographic)
        format.Trimming = StringTrimming.EllipsisWord
        gp.AddString(text, font.FontFamily, font.Style, font.Size, bounds, StringFormat.GenericTypographic)
        g.FillPath(brush, gp)
        Dim lastCharPosition = GetPathLastCharPosition(g, format, gp, bounds, text, font)
    End Using
End Sub

you can use the current GraphicsPath, Rectangle bounds, Font size and style used for drawing the the text in a graphics context, to calculate the position of the last character drawn and, as a consequence, the last word, if necessary.

I've added in FillTextSolid() a call to the GetPathLastCharPosition() method, which is responsible for the calculation. Pass to the method the objects described, as they're currently configured (these settings can of course change at any time: see the animation at the bottom).

Dim [Last Char Position] = GetPathLastCharPosition(
    [Graphics], 
    [StringFormat], 
    [GraphicsPath], 
    [RectangleF], 
    [String], 
    [Font]
)

To determine the current last word printed using a GraphicsPath object, you cannot split the string in parts separated by, e.g., a white space, since each char is part of the rendering.

Also to note: for the measure to work as intended, you cannot set the drawing Font size in Points, the Font size must be expressed in pixels.
You could also use Point units, but the GraphicsPath class, when Points are specified, generates (correctly) a Font measure in EMs - considering the Font Cell Ascent and Descent - which is not the same as the Font.Height.
You can of course convert the measure from Ems to Pixels, but it just adds complexity for no good reason (in the context of this question, at least).

See a description of these details and how to calculate the GraphicsPath EMs in:
Properly draw text using GraphicsPath

GetPathLastCharPosition() uses Graphics.MeasureCharacterRanges to measure the bounding rectangle of each char in the Text string, in chunks of 32 chars per iteration. This is because StringFormat.SetMeasurableCharacterRanges only takes a maximum of 32 CharacterRange elements.

So, we take the Text in chunks of 32 chars, get the bounding Region of each and verify whether the Region contains the last Point in the GraphicsPath.
The last Point generated by a GraphicsPath is returned by the GraphicsPath.GetLastPoint().

  • This procedure only considers text drawn from top to bottom and left to right.
    It can be adapted to handle right to left languages.

Of course, you could also ignore the last point and just consider whether a Region bounds fall outside the bounding rectangle of the canvas.

Anyway, when the a Region that contains the last point is found, the method stops and returns the position of the last character in the string that is part of the drawing.

Private Function GetPathLastCharPosition(g As Graphics, format As StringFormat, path As GraphicsPath, bounds As RectangleF, text As String, font As Font) As Integer
    Dim textLength As Integer = text.Length
    Dim p = path.GetLastPoint()
    bounds.Height += font.Height

    For charPos As Integer = 0 To text.Length - 1 Step 32
        Dim count As Integer = Math.Min(textLength - charPos, 32)
        Dim charRanges = Enumerable.Range(charPos, count).Select(Function(c) New CharacterRange(c, 1)).ToArray()

        format.SetMeasurableCharacterRanges(charRanges)
        Dim regions As Region() = g.MeasureCharacterRanges(text, font, bounds, format)

        For r As Integer = 0 To regions.Length - 1
            If regions(r).IsVisible(p.X, p.Y) Then
                Return charRanges(r).First
            End If
        Next
    Next
    Return -1
End Function

This is how it works:

GraphicsPath last draw char

C# version of the method:

private int GetPathLastCharPosition(Graphics g, StringFormat format, GraphicsPath path, RectangleF bounds, string text, Font font)
{
    int textLength = text.Length;
    var p = path.GetLastPoint();
    bounds.Height += font.Height;

    for (int charPos = 0; charPos < text.Length; charPos += 32) {
        int count = Math.Min(textLength - charPos, 32);
        var charRanges = Enumerable.Range(charPos, count).Select(c => new CharacterRange(c, 1)).ToArray();

        format.SetMeasurableCharacterRanges(charRanges);
        Region[] regions = g.MeasureCharacterRanges(text, font, bounds, format);

        for (int r = 0; r < regions.Length; r++) {
            if (regions[r].IsVisible(p.X, p.Y)) {
                return charRanges[r].First;
            }
        }
    }
    return -1;
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • By great Zeus' beard, I feel blessed by the StackOverflow gods! Astounding answer, Jimi - thanks. I'm up to my nachos in other critical optimisations at the moment, but twill come back once I try this out. Now this is the standard to which all StackOverflow answers should be judged.. – stigzler Sep 03 '21 at 11:39
  • I can't get this to work in my code, but it's quite clearly the right answer, so have marked it as such. – stigzler Sep 06 '21 at 13:22
  • Well, maybe it's the *right answer*, but if you cannot get it to work it's not that useful. What is the problem? The procedure shown here uses a (very) standard text and draws it without any *special treatment* (using the code in your previous question). Do you know why you have troubles adapting it to your existing code? -- If you post that code somewhere, I'll give it a look (just the part that should work with the procedure described here). – Jimi Sep 06 '21 at 13:42
  • I appreciate the offer Jimi and will come back to this. Having a nightmare with SVN + a weird critical bug on my main code atm + trying to fix it. Failing that, it might be a full rewrite! Blurgh, some projects just turn into nightmares. Thanks again. – stigzler Sep 06 '21 at 14:36