Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
123 views
in Technique[技术] by (71.8m points)

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

I need to highlight the particular character in a control using the fill rect. I can get the location of the text when it's not wrapped by using the Graphics.MeasureString() method like below,

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

enter image description here

If the text is wrapped then I'm not able to find the exact bounds of the character to highlight the text.

enter image description here

I need to get the exact bounds of the given character in the text which is wrapped. Provide your suggestion to achieve this scenario.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.

enter image description here

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 = li

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...