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:
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 Region
s. 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);
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());
Region[] stringRegions;
stringRegions = g.MeasureCharacterRanges(cleanString, font,
layoutRect, stringFormat);
for (int i = 0; i < ranges; i++)
{
RectangleF measureRect = stringRegions[i].GetBounds(g);
g.FillRectangle(Brushes.Blue, Rectangle.Round(measureRect));
}
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.