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.
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()
andGraphics.DrawString()
do the exact same thing here.
SettingApplication.SetCompatibleTextRenderingDefault()
totrue
orfalse
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, withTextFormatFlags.NoPadding
renders 2 pixels to the
left (the opposite without the flag). Graphics renders 1 pixel to the
right.
Of course ifOwnerDrawVariable
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:
- Get all the positions in the
ListItem
text where the pattern (string HighLightString
) appears.- Define an array of
CharacterRange
structures with the position and length of the pattern.- Fill a
StringFormat
with all theCharacterRange
structs using.SetMeasurableCharacterRanges()
- Define an array of Regions using
Graphics.MeasureCharacterRanges()
passing the initializedStringFormat
.- Define an array of Rectangles sized using
Region.GetBounds()
- Fill all the Rectangles with the highlight color using
Graphics.FillRectangles()
- 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()` implementationprivate 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 theListBox.DrawMode
, it may become necessary to
subscribe theListBox.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.
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;
}
Related Topics
Is Idisposable.Dispose() Called Automatically
The Difference Between Virtual, Override, New and Sealed Override
Wcf - Design Parameter Decision
Make a Specific Column Only Accept Numeric Value in Datagridview in Keypress Event
How Better to Resolve Dependencies in Object Created by Factory
Cannot Convert Lambda Expression to Type 'String' Because It Is Not a Delegate Type
Why Are Tolookup and Groupby Different
How to Start Chromedriver in Headless Mode
Different Ways of Adding to Dictionary
Is Graphics.Drawimage Too Slow for Bigger Images
Get All Associate/Composite Objects Inside an Object (In Abstract Way)
Should the Repository Layer Return Data-Transfer-Objects (Dto)
In C#, Is "This" Keyword Required
Get the Object Is Null Using JSON in Wcf Service
Is There a Complete Iequatable Implementation Reference
Dispatcher Invoke(...) VS Begininvoke(...) Confusion
Httpclient Single Instance with Different Authentication Headers