Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Drawing tagged strings on WinForm controls

0.00/5 (No votes)
24 Mar 2007 1  
This article explains how to draw strings with tagging information, such as different background color for a substring.

Introduction

While working as a GUI consultant for Perytons, a company that develops Protocol Analyzers, I was requested to write a GUI that will mark certain portions of a string with a different background color to indicate search results within this string. Since the string would have to be drawn on a custom control, I had to draw it myself.

I could have split the string into several substrings and draw each one separately with a different color, but this would have been slow and not accurate.

Fortunately, .NET provides a couple of sophisticated (though buggy) APIs: SetMeasurableCharacterRanges and MeasureCharacterRanges. This pair would provide me with the information I need to draw the background color exactly under the requested string portion without the need to do all the complex calculations.

Background

First, in a search function, I mark the highlighted portion by inserting start and end tags, like in HTML: <h> and </h> surrounding the substring I want to highlight. So, the string looks looks like this: "This is the <h>highlighted</h> portion <h>of</h> a string".

And the form will look like this:

Screenshot - sample.jpg

Now, in my drawing function, I first call the GetCharacterRanges method that creates a List of CharacterRange structures. This structure will hold the starting position and the length of each highlighted portion of a string. Then, I call the .NET API StringFormat.SetMeasurableCharacterRanges that accepts an array of these structures to be used in the next API call.

Calling Graphics.MeasureCharacterRanges now will return an array of Regions. Each Region holds the exact drawing information based on the CharacterRegion array passed earlier. Now all that is left is to draw FillRectangle for each such Region to create the background. And lastly, draw the string over it.

Using the code

private static string startTag = "<h>";
private static string endTag = "</h>";
 
private void Form1_Paint(object sender, PaintEventArgs e)
{
   RectangleF f = new RectangleF(0,0, 300,30);
   DrawTaggedString(e.Graphics, 
       "This is the <h>highlighted</h> portion <h>of</h> a string", 
       Font, Brushes.Magenta, f,new StringFormat() );
} 

private String GetCleanText(String text)
{
  if (text == null)
  {
    return null;
  }

  string cleanString = text.Replace(startTag, "");
  cleanString = cleanString.Replace(endTag, "");
  return cleanString;
}

private static List<CharacterRange> GetCharacterRanges(string text)
{
  int start = 0;
  List<CharacterRange> characterRanges = new List<CharacterRange>();
  int charsRemoved = 0;
  while (start < text.Length)
  {
    start = text.IndexOf(startTag, start);

    if (start >= 0)
    {
      int end = text.IndexOf(endTag, start);
      int ofs = start - charsRemoved;
      int length = end - charsRemoved - startTag.Length - ofs;
      characterRanges.Add(new CharacterRange(ofs, length));
      start = end + startTag.Length;
      charsRemoved += (startTag.Length + endTag.Length);
    }
    else
    {
       break;
    }   
  }
  return characterRanges;
}

public void DrawTaggedString(Graphics g, String text, Font font, 
       Brush brush, RectangleF layoutRect, StringFormat stringFormat)
{
   List<CharacterRange> characterRanges = GetCharacterRanges(text);
   // No the ugly part. Because of Microsoft's bug that limits
   // the array passed to SetMeasurableCharacterRanges
   // to 32 only. We loop on chunks of 32 elements each time.
   int offset = 0;
   int countLeft = characterRanges.Count;
   do
   {
      int count = Math.Min(countLeft, 32);
      List<CharacterRange> subRange = characterRanges.GetRange(offset, count);
      int ranges = subRange.Count;
      string cleanString = GetCleanText(text);
      stringFormat.SetMeasurableCharacterRanges(subRange.ToArray());
      // Measure all the regions - use the clean string now to get them
      Region[] stringRegions;
      stringRegions = g.MeasureCharacterRanges(cleanString, font, 
                                               layoutRect, stringFormat);
      // Loop on all the ranges and draw
      // a blue rectangle - this will mark the found string
      for (int i = 0; i < ranges; i++)
      {
         RectangleF measureRect = stringRegions[i].GetBounds(g);
         g.FillRectangle(Brushes.Blue, Rectangle.Round(measureRect));
      }
      // Draw string to screen.
      g.DrawString(cleanString, font, brush, layoutRect, stringFormat);
      offset += count;
      countLeft -= count;
   } while (countLeft > 0);
} 

Point of interest

There is a bug in SetMeasurableCharcterRanges that limits the size of the CharacterRange array passed to it to 32. Otherwise, an exception is thrown by this method. To overcome this bug, I'm doing all this painting in chunks of 32 max each time.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here