0

GDI+ in C# is fun!

While on the topic of doing some printing I ran into the following situation:

  • Graphics.DrawString allows us to render a complete paragraph inside of an arbitrarily defined bounding box, complete with sensible linebreaks
  • Graphics.MeasureString even allows us to get the height needed for a text given a specified width (or vice versa) with the aforementioned perks of nice linebreaks
  • A paragraph of text can contain words that have a different FontStyle than the words surrounding it (think of making a word italic for emphasis)
  • There is no way to get proper layout information as in "the text you rendered did not fill the last line completely, here is a point where you can start rendering the next text that might have a different FontStyle and continue over the proper beginning of the next line in this given layout rectangle"

I am trying to render text (think multiple lines of the stuff) with interspersed FontStyle changes (not the font or size, just the style). I need a way to determine where each block of individually styled text starts and begins and handle graceful line transitions.

Graphics.MeasureCharacterRanges to the rescue? I fear not.

I found an example doing roughly what I want to do. They are trying to render text with each word a different color. And from the answer it seems they are succeeding. But.

I have adapted the code and came up with the following (I made this in LINQPad for ease of fiddeling)

void Main()
{
    string lorem = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.";

    Image img = new Bitmap(400, 220); //Resulting graphics

    Font font = new Font("Arial", 12); //Font to use
    Brush brush = Brushes.Black; //Default braush
    Pen pen = Pens.Black; //Default pen
    PointF point = new PointF(0, 0); //start point for area to render to
    RectangleF rectangle = new RectangleF(point, img.Size); //area to render to
    StringFormat stringFormat = StringFormat.GenericTypographic; 

    Brush[] RegionBrushes = { Brushes.LightGray, Brushes.LightBlue }; //for display purposes
    Pen[] RegionPens = { Pens.Red, Pens.Blue }; //for display purposes

    using(Graphics g = Graphics.FromImage(img))
    {
        g.Clear(Color.LightGreen); //give contrast against white background

        g.DrawString(lorem, font, Brushes.Gray, rectangle, stringFormat); //the whole string, with proper linebreaks for reference

        CharacterRange[] characterRanges = { new CharacterRange(42, 20), new CharacterRange(68, 38) }; //the desired portions
        stringFormat.SetMeasurableCharacterRanges(characterRanges); //set these portions as important
    
        Region[] stringRegions = g.MeasureCharacterRanges(lorem, font, rectangle, stringFormat); //get the regions where the desired portions are
    
        for(int i = 0; i < characterRanges.Length; i++) //for each desired portion
        {
            g.FillRegion(RegionBrushes[i], stringRegions[i]); //fill the corresponding region with an individual color (as defined above, to ilustrate that the measuring does in fact work)
            RectangleF bounds = stringRegions[i].GetBounds(g); //the bounds of the region, I probably go wrong here
            string word = lorem.Substring(characterRanges[i].First, characterRanges[i].Length); //the portion of the overall string to draw
    
            g.DrawRectangles(RegionPens[i],new RectangleF[]{ bounds}); //draw the outline of the bounds
            g.DrawString(word, font, brush, bounds, stringFormat); //draw the actual portion of the string, only one of the strings gets drawn and to the wrong place
        }

    }

    img.Dump(); //LINQPad-specific, shows the image in the result window
}

in principle the code works sort of, but not satisfactory.

  1. depending on the actual character ranges chosen, either the first or second range get drawn as text
  2. while g.FillRegion() correctly fills the region where the text is supposed to be, the text is nowhere near this region

I don't have any idea why the 1st is happening. The 2nd naturally comes from the fact, that DrawString is given a rectangle that is derived from the bounds of the region. While the bounds of the second range (with the current values from the example) are also the outline of the region, they are not for the first range of characters spanning multiple lines and ending before they begin.

Is there a way to get useful position information for use in DrawString?

lhiapgpeonk
  • 457
  • 1
  • 5
  • 18
  • You may want to see: [ComboBox OwnerDrawVariable Font format size problem](https://stackoverflow.com/a/63103375/7444103) (the method used there is meant to draw text with Fonts in different weights, calculate and render seamlessly sub-parts of the text) and [How to compute the correct width of a digit in pixels?](https://stackoverflow.com/a/54772134/7444103) -- You may also check [Properly draw text using GraphicsPath](https://stackoverflow.com/a/53074638/7444103), in case you decide to opt for a slightly different method (the information related to how a glyph is rendered is still relevant) – Jimi Aug 23 '23 at 14:25
  • If you instead mean to change the *weight* of the text by outlining it, see [Graphics DrawPath produces unexpected results when rendering text](https://stackoverflow.com/a/68877110/7444103) and (the quite related) [Get the position where drawn text is truncated by GraphicsPath.DrawString](https://stackoverflow.com/a/69036544/7444103) -- C# version of both at the bottom – Jimi Aug 23 '23 at 14:35
  • Thank you for the many links. Especially the last one has caught my eye for the moment. The code you give there (GetPathLastCharPosition) does sort of work, but if I include it in my example above, the returned position is offset a bit to the actual last character and will only return a value different from -1 when there is a whole line just fitting to the bottom of the rect. if there is too much space below (but less than a line) it yields -1 – lhiapgpeonk Aug 24 '23 at 07:01
  • The correct functionality of that code implies that the text is rendered using a GraphicsPath – Jimi Aug 24 '23 at 11:18
  • Yes, using GtaphicsPath I get the not so occasional -1. – lhiapgpeonk Aug 25 '23 at 06:42

0 Answers0