如何使用图形突出显示控件中的换行文本?
How to highlight wrapped text in a control using the graphics?
我需要使用填充矩形突出显示控件中的特定字符。
我可以通过使用如下所示的 Graphics.MeasureString()
方法获取文本未换行时的位置,
var size = g.MeasureString(tempSearchText, style.Font, 0, StringFormat.GenericTypographic);
如果文本被换行,我将无法找到字符的确切边界来突出显示文本。
我需要在换行的文本中获取给定字符的准确边界。请提供您的建议以实现此方案。
没有明确说明要针对哪些控件,因此我正在测试 3 个不同的控件:
TextBox
、RichTextbox
和 ListBox
。
TextBox 和 RichTextbox 具有相同的行为并共享相同的工具,因此无需定义两种不同的方法来实现相同的结果。
当然 RichTextbox 提供了更多选项,包括 RTF.
此外,我正在测试 Graphics.DrawString()
and TextRenderer.DrawText()
。
这是本次测试的结果,代码的作用就更清楚了。
警告:
对于这个例子,我使用 Control.CreateGraphics()
,因为 TextBox
和 RichTextBox
控件不提供 Paint()
事件。对于真实世界的应用程序,您应该创建一个派生自 TextBox
或 RichTextBox
的自定义控件,覆盖 WndPrc
并处理 WM_PAINT
.
1) 在多行文本框控件中突出显示所有 t。
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);
}
}
使用Graphics.FillRectangle()
和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) 突出显示 TextBox 控件和多行 RichTextbox 控件中的一些字符串模式(“Words”)。
仅使用 TextRenderer
,因为行为没有差异。
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)在一个ListBox控件的所有ListItems
中高亮所有(当然也可以是其他任何字符串 :)
ListBox.DrawMode
设置为 Normal
并“即时”更改为 OwnerDrawVariable
以评估 TextRenderer
和 Graphics
在这里的行为是否不同。
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:
- 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 the CharacterRange
structs using .SetMeasurableCharacterRanges()
- Define an array of Regions using
Graphics.MeasureCharacterRanges()
passing the initialized StringFormat
.
- 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()
实施:
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()` 实现
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;
}
我需要使用填充矩形突出显示控件中的特定字符。
我可以通过使用如下所示的 Graphics.MeasureString()
方法获取文本未换行时的位置,
var size = g.MeasureString(tempSearchText, style.Font, 0, StringFormat.GenericTypographic);
如果文本被换行,我将无法找到字符的确切边界来突出显示文本。
我需要在换行的文本中获取给定字符的准确边界。请提供您的建议以实现此方案。
没有明确说明要针对哪些控件,因此我正在测试 3 个不同的控件:
TextBox
、RichTextbox
和 ListBox
。
TextBox 和 RichTextbox 具有相同的行为并共享相同的工具,因此无需定义两种不同的方法来实现相同的结果。
当然 RichTextbox 提供了更多选项,包括 RTF.
此外,我正在测试 Graphics.DrawString()
and TextRenderer.DrawText()
。
这是本次测试的结果,代码的作用就更清楚了。
警告:
对于这个例子,我使用 Control.CreateGraphics()
,因为 TextBox
和 RichTextBox
控件不提供 Paint()
事件。对于真实世界的应用程序,您应该创建一个派生自 TextBox
或 RichTextBox
的自定义控件,覆盖 WndPrc
并处理 WM_PAINT
.
1) 在多行文本框控件中突出显示所有 t。
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);
}
}
使用Graphics.FillRectangle()
和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) 突出显示 TextBox 控件和多行 RichTextbox 控件中的一些字符串模式(“Words”)。
仅使用 TextRenderer
,因为行为没有差异。
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)在一个ListBox控件的所有ListItems
中高亮所有(当然也可以是其他任何字符串 :)
ListBox.DrawMode
设置为 Normal
并“即时”更改为 OwnerDrawVariable
以评估 TextRenderer
和 Graphics
在这里的行为是否不同。
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 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()
实施:
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()` 实现
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 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;
}