如何使用图形突出显示控件中的换行文本?

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

我需要使用填充矩形突出显示控件中的特定字符。 我可以通过使用如下所示的 Graphics.MeasureString() 方法获取文本未换行时的位置,

var size = g.MeasureString(tempSearchText, style.Font, 0, StringFormat.GenericTypographic);

如果文本被换行,我将无法找到字符的确切边界来突出显示文本。

我需要在换行的文本中获取给定字符的准确边界。请提供您的建议以实现此方案。

没有明确说明要针对哪些控件,因此我正在测试 3 个不同的控件:
TextBoxRichTextboxListBox

TextBox 和 RichTextbox 具有相同的行为并共享相同的工具,因此无需定义两种不同的方法来实现相同的结果。
当然 RichTextbox 提供了更多选项,包括 RTF.

此外,我正在测试 Graphics.DrawString() and TextRenderer.DrawText()

这是本次测试的结果,代码的作用就更清楚了。

警告:
对于这个例子,我使用 Control.CreateGraphics(),因为 TextBoxRichTextBox 控件不提供 Paint() 事件。对于真实世界的应用程序,您应该创建一个派生自 TextBoxRichTextBox 的自定义控件,覆盖 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 以评估 TextRendererGraphics 在这里的行为是否不同。

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() 实施:

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;
}