How to Highlight Wrapped Text in a Control Using the Graphics

How to highlight wrapped text in a control using the graphics?

There is no clear specification of which controls to target, so I'm testing 3 different:

TextBox, RichTextbox and ListBox.

TextBox and RichTextbox have the same behavior and share the same tools, so there's no need to define two different methods to achieve the same result.

Of course RichTextbox offers many more options, including RTF.

Also, I'm testing both Graphics.DrawString() and TextRenderer.DrawText().

This is the result of this test, so it's more clear what the code does.

Sample Image

Warning:

For this example I'm using Control.CreateGraphics(), because TextBox and RichTextBox controls don't provide a Paint() event. For a real world application, you should create a Custom Control derived from TextBox or RichTextBox, override WndPrc and handle WM_PAINT.

1) Highlight all t in a multiline TextBox control.

TextRenderer->DrawText():

//Define some useful flags for TextRenderer
TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.Top |
TextFormatFlags.NoPadding | TextFormatFlags.WordBreak |
TextFormatFlags.TextBoxControl;
//The char to look for
char TheChar = 't';

//Find all 't' chars indexes in the text
List<int> TheIndexList = textBox1.Text.Select((chr, idx) => chr == TheChar ? idx : -1)
.Where(idx => idx != -1).ToList();

//Or with Regex - same thing, pick the one you prefer
List<int> TheIndexList = Regex.Matches(textBox1.Text, TheChar.ToString())
.Cast<Match>()
.Select(chr => chr.Index).ToList();

//Using .GetPositionFromCharIndex(), define the Point [p] where the highlighted text is drawn
if (TheIndexList.Count > 0)
{
foreach (int Position in TheIndexList)
{
Point p = textBox1.GetPositionFromCharIndex(Position);
using (Graphics g = textBox1.CreateGraphics())
TextRenderer.DrawText(g, TheChar.ToString(), textBox1.Font, p,
textBox1.ForeColor, Color.LightGreen, flags);
}
}

The same operation using Graphics.FillRectangle() and Graphics.DrawString():

if (TheIndexList.Count > 0)
{
using (Graphics g = textBox1.CreateGraphics())
{
foreach (int Position in TheIndexList)
{
PointF pF = textBox1.GetPositionFromCharIndex(Position);
SizeF sF = g.MeasureString(TheChar.ToString(), textBox1.Font, 0,
StringFormat.GenericTypographic);

g.FillRectangle(Brushes.LightGreen, new RectangleF(pF, sF));
using (SolidBrush brush = new SolidBrush(textBox1.ForeColor))
{
g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
g.DrawString(TheChar.ToString(), textBox1.Font, brush, pF, StringFormat.GenericTypographic);
}
}
}
}

There is no notable difference in behavior: TextRenderer.DrawText()
and Graphics.DrawString() do the exact same thing here.

Setting Application.SetCompatibleTextRenderingDefault() to true or
false does not seem to have any affect (in the current context, at least).

2) Highlight some string patterns ("Words") in a TextBox control and a multiline RichTextbox control.

Using TextRenderer only, since there's no difference in behavior.

I'm simply letting IndexOf() find the the first occurrence of the
strings, but the same search pattern used before can take it's place. Regex works better.

string[] TheStrings = {"for", "s"};
foreach (string pattern in TheStrings)
{
Point p = TextBox2.GetPositionFromCharIndex(TextBox2.Text.IndexOf(pattern));
using (var g = TextBox2.CreateGraphics()) {
TextRenderer.DrawText(g, pattern, TextBox2.Font, p,
TextBox2.ForeColor, Color.LightSkyBlue, flags);
}
}

TheStrings = new string []{"m", "more"};
foreach (string pattern in TheStrings)
{
Point p = richTextBox1.GetPositionFromCharIndex(richTextBox1.Text.IndexOf(pattern));
using (Graphics g = richTextBox1.CreateGraphics())
TextRenderer.DrawText(g, pattern, richTextBox1.Font, p,
richTextBox1.ForeColor, Color.LightSteelBlue, flags);
}

3) Highlight all s in all the ListItems of a ListBox control (of course it can be any other string :)

The ListBox.DrawMode is set to Normal and changed "on the fly" to OwnerDrawVariable to evaluate whether TextRenderer and Graphics behave differently here.

There is a small difference: a different offset, relative to the left
margin of the ListBox, compared to the standard implementation.
TextRenderer, with TextFormatFlags.NoPadding renders 2 pixels to the
left (the opposite without the flag). Graphics renders 1 pixel to the
right.
Of course if OwnerDrawVariable is set in design mode,
this will not be noticed.

string HighLightString = "s";
int GraphicsPaddingOffset = 1;
int TextRendererPaddingOffset = 2;

private void button1_Click(object sender, EventArgs e)
{
listBox1.DrawMode = DrawMode.OwnerDrawVariable;
}

How the following code works:

  1. Get all the positions in the ListItem text where the pattern (string HighLightString) appears.
  2. Define an array of CharacterRange structures with the position and length of the pattern.
  3. Fill a StringFormat with all the CharacterRange structs using .SetMeasurableCharacterRanges()
  4. Define an array of Regions using Graphics.MeasureCharacterRanges() passing the initialized StringFormat.
  5. Define an array of Rectangles sized using Region.GetBounds()
  6. Fill all the Rectangles with the highlight color using Graphics.FillRectangles()
  7. Draw the ListItem text.

TextRenderer.DrawText() implementation:

private void listBox1_DrawItem(object sender, DrawItemEventArgs e)
{
e.DrawBackground();

TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.Top | TextFormatFlags.NoPadding |
TextFormatFlags.WordBreak | TextFormatFlags.TextBoxControl;
Rectangle bounds = new Rectangle(e.Bounds.X + TextRendererPaddingOffset,
e.Bounds.Y, e.Bounds.Width, e.Bounds.Height);

string ItemString = listBox1.GetItemText(listBox1.Items[e.Index]);
List<int> TheIndexList = Regex.Matches(ItemString, HighLightString)
.Cast<Match>()
.Select(s => s.Index).ToList();

if (TheIndexList.Count > 0)
{
CharacterRange[] CharRanges = new CharacterRange[TheIndexList.Count];
for (int CharX = 0; CharX < TheIndexList.Count; CharX++)
CharRanges[CharX] = new CharacterRange(TheIndexList[CharX], HighLightString.Length);

StringFormat format = new StringFormat(StringFormat.GenericDefault);
format.SetMeasurableCharacterRanges(CharRanges);

Region[] regions = e.Graphics.MeasureCharacterRanges(ItemString, e.Font, e.Bounds, format);

RectangleF[] rectsF = new RectangleF[regions.Length];
for (int RFx = 0; RFx < regions.Length; RFx++)
rectsF[RFx] = regions[RFx].GetBounds(e.Graphics);

e.Graphics.FillRectangles(Brushes.LightGreen, rectsF);
}
TextRenderer.DrawText(e.Graphics, ItemString, e.Font, bounds, e.ForeColor, flags);
}
`Graphics.DrawString()` implementation
private void listBox1_DrawItem(object sender, DrawItemEventArgs e)
{
e.DrawBackground();
Rectangle bounds = new Rectangle(e.Bounds.X - GraphicsPaddingOffset,
e.Bounds.Y, e.Bounds.Width, e.Bounds.Height);

string ItemString = listBox1.GetItemText(listBox1.Items[e.Index]);
List<int> TheIndexList = Regex.Matches(ItemString, HighLightString)
.Cast<Match>()
.Select(s => s.Index).ToList();

StringFormat format = new StringFormat(StringFormat.GenericDefault);
if (TheIndexList.Count > 0)
{
CharacterRange[] CharRanges = new CharacterRange[TheIndexList.Count];
for (int CharX = 0; CharX < TheIndexList.Count; CharX++)
CharRanges[CharX] = new CharacterRange(TheIndexList[CharX], HighLightString.Length);

format.SetMeasurableCharacterRanges(CharRanges);
Region[] regions = e.Graphics.MeasureCharacterRanges(ItemString, e.Font, e.Bounds, format);

RectangleF[] rectsF = new RectangleF[regions.Length];
for (int RFx = 0; RFx < regions.Length; RFx++)
rectsF[RFx] = regions[RFx].GetBounds(e.Graphics);

e.Graphics.FillRectangles(Brushes.LightGreen, rectsF);
}
using (SolidBrush brush = new SolidBrush(e.ForeColor))
e.Graphics.DrawString(ItemString, e.Font, brush, bounds, format);
}

Note:
Depending on the ListBox.DrawMode, it may become necessary to
subscribe the ListBox.MeasureItem() event or set the .ItemHeight
property to the corrent value.

private void listBox1_MeasureItem(object sender, MeasureItemEventArgs e)
{
e.ItemHeight = listBox1.Font.Height;
}

Measure wrapped string

Can you can use the Graphics.MeasureString method to get the dimensions of the string and draw the next string accordingly?

SizeF size = g.MeasureString(someText, someFont, someRectangleF.Size.Width);
g.DrawString(someText, someFont, someBrush, new PointF(0, 0), someRectangleF);
g.DrawString(someMoreText, someFont, someBrush, new PointF(0, size.Height), someRectangleF);

How to highlight search Arabic text in DataGridView?

Problem


You need to take into account several factors for each cell:

  • The DataGridViewContentAlignment.
  • The exact size of the selected characters.
  • The zero-width characters (white spaces).
  • The size of the content.
  • The empty space.
  • The index of the first occurrence of the search string.

All the mentioned are necessary to calculate and adjust both the location and size of the highlight rectangle.

Here's an example:

RTL Languages - RTL Layout


private void dgv_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
if (txtSearch.TextLength < 1
|| e.RowIndex < 0
|| e.ColumnIndex < 1
|| e.Value == null)
return;

var zeroWidth = "|";
var v = e.Value.ToString().Replace(" ", zeroWidth);
var f = txtSearch.Text.Replace(" ", zeroWidth);
var i = v.IndexOf(f, StringComparison.InvariantCultureIgnoreCase);

if (i < 0) return;

e.Handled = true;
var g = e.Graphics;

using (var sf = ToStringFormat(e.CellStyle.Alignment))
{
var zw = g.MeasureString(zeroWidth, e.CellStyle.Font, e.CellBounds.Width, sf).Width;
var valWidth = g.MeasureString(v, e.CellStyle.Font, e.CellBounds.Width, sf).Width;
var w = g.MeasureString(f, e.CellStyle.Font, e.CellBounds.Width, sf).Width;
var x = e.CellBounds.Right - ((e.CellBounds.Width - valWidth) / 2);

x -= g.MeasureString(v.Substring(0, i), e.CellStyle.Font,
e.CellBounds.Width, sf).Width;
x -= w;

switch (e.CellStyle.Alignment)
{
case DataGridViewContentAlignment.BottomLeft:
case DataGridViewContentAlignment.MiddleLeft:
case DataGridViewContentAlignment.TopLeft:
x += ((e.CellBounds.Width - valWidth) / 2) - zw;
break;
case DataGridViewContentAlignment.MiddleRight:
case DataGridViewContentAlignment.BottomRight:
case DataGridViewContentAlignment.TopRight:
x -= ((e.CellBounds.Width - valWidth) / 2) - zw;
break;
default:
break;
}

var r = new RectangleF(
x,
e.CellBounds.Y + 3,
w,
e.CellBounds.Height - 7);

e.PaintBackground(e.CellBounds, true);
g.FillRectangle(Brushes.Yellow, r);
e.PaintContent(e.CellBounds);
}
}

private StringFormat ToStringFormat(DataGridViewContentAlignment ca)
{
var sf = StringFormat.GenericTypographic;

switch (ca)
{
case DataGridViewContentAlignment.MiddleCenter:
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Center;
break;
case DataGridViewContentAlignment.MiddleLeft:
sf.Alignment = StringAlignment.Near;
sf.LineAlignment = StringAlignment.Center;
break;
case DataGridViewContentAlignment.MiddleRight:
sf.Alignment = StringAlignment.Far;
sf.LineAlignment = StringAlignment.Center;
break;
case DataGridViewContentAlignment.BottomCenter:
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Far;
break;
case DataGridViewContentAlignment.BottomLeft:
sf.Alignment = StringAlignment.Near;
sf.LineAlignment = StringAlignment.Far;
break;
case DataGridViewContentAlignment.BottomRight:
sf.Alignment = StringAlignment.Far;
sf.LineAlignment = StringAlignment.Far;
break;
case DataGridViewContentAlignment.TopLeft:
sf.Alignment = StringAlignment.Near;
sf.LineAlignment = StringAlignment.Near;
break;
case DataGridViewContentAlignment.TopRight:
sf.Alignment = StringAlignment.Far;
sf.LineAlignment = StringAlignment.Near;
break;
case DataGridViewContentAlignment.TopCenter:
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Near;
break;
}

sf.FormatFlags |= StringFormatFlags.DirectionRightToLeft;

return sf;
}

Here's a demo.

SOQ60983817A

Note: Only the DGV is set to RTL layout in the demo.

LTR Languages - LTR Layout


Maybe out of scope, however might be useful for someone.

private void dgv_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
if (txtSearch.TextLength < 1
|| e.RowIndex < 0
|| e.ColumnIndex < 1
|| e.Value == null)
return;

var zeroWidth = "|";
var v = e.Value.ToString().Replace(" ", zeroWidth);
var f = txtSearch.Text.Replace(" ", zeroWidth);
var i = v.IndexOf(f, StringComparison.InvariantCultureIgnoreCase);

if (i < 0) return;

e.Handled = true;

var g = e.Graphics;

using (var sf = ToStringFormat(e.CellStyle.Alignment))
{
var zs = g.MeasureString(zeroWidth, e.CellStyle.Font,
e.CellBounds.Width, sf).Width;
var valWidth = g.MeasureString(v, e.CellStyle.Font,
e.CellBounds.Width, sf).Width;
var x = g.MeasureString(v.Substring(0, i), e.CellStyle.Font,
e.CellBounds.Width, sf).Width;
var w = g.MeasureString(v.Substring(i, f.Length), e.CellStyle.Font,
e.CellBounds.Width, sf).Width;

switch (e.CellStyle.Alignment)
{
case DataGridViewContentAlignment.MiddleCenter:
case DataGridViewContentAlignment.BottomCenter:
case DataGridViewContentAlignment.TopCenter:
x += (e.CellBounds.Width - valWidth) / 2;
x -= zs / 2;
break;
case DataGridViewContentAlignment.MiddleRight:
case DataGridViewContentAlignment.BottomRight:
case DataGridViewContentAlignment.TopRight:
x += (e.CellBounds.Width - valWidth);
x -= zs * 1.5f;
break;
default:
x += zs / 2;
break;
}

var r = new RectangleF(
e.CellBounds.X + x,
e.CellBounds.Y + 3,
w,
e.CellBounds.Height - 7);

e.PaintBackground(e.CellBounds, true);
g.FillRectangle(Brushes.Yellow, r);
e.PaintContent(e.CellBounds);
}
}

private StringFormat ToStringFormat(DataGridViewContentAlignment ca)
{
var sf = StringFormat.GenericTypographic;

switch (ca)
{
case DataGridViewContentAlignment.MiddleCenter:
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Center;
break;
case DataGridViewContentAlignment.MiddleLeft:
sf.Alignment = StringAlignment.Near;
sf.LineAlignment = StringAlignment.Center;
break;
case DataGridViewContentAlignment.MiddleRight:
sf.Alignment = StringAlignment.Far;
sf.LineAlignment = StringAlignment.Center;
break;
case DataGridViewContentAlignment.BottomCenter:
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Far;
break;
case DataGridViewContentAlignment.BottomLeft:
sf.Alignment = StringAlignment.Near;
sf.LineAlignment = StringAlignment.Far;
break;
case DataGridViewContentAlignment.BottomRight:
sf.Alignment = StringAlignment.Far;
sf.LineAlignment = StringAlignment.Far;
break;
case DataGridViewContentAlignment.TopLeft:
sf.Alignment = StringAlignment.Near;
sf.LineAlignment = StringAlignment.Near;
break;
case DataGridViewContentAlignment.TopRight:
sf.Alignment = StringAlignment.Far;
sf.LineAlignment = StringAlignment.Near;
break;
case DataGridViewContentAlignment.TopCenter:
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Near;
break;
}

return sf;
}

SOQ60983817B



Related Topics



Leave a reply



Submit