.net – WPF Listbox highlight part of ListBoxItem element

.nethighlightinglistboxwpfxaml

I have a TextBox and ListBox. User can search ListBox elements from TextBox.

ListBox is bound to CollectionViewSource.

CollectionViewSource has Filter event handler, that filters elements based on text that user enters into TextBox.

My requirement is to highlight user entered text within TextBlock of ListBoxItem elements.

I was thinking of breaking TextBlock into several Runs objects and modify Background property of Run objects that need to be highlighted.

I think it is not possible to do with DataTemplates.

Is there easy way to accomplish this?

Thank You!

Best Solution

UPDATE: I have elaborated significantly on this subject in this blog post.

I don't think there's any easy way to do this, but here is how I would tackle this problem:

  1. Define a view model for the items in the list. It should include properties to expose the text and define which part of the text should be highlighted (starting and ending indexes, basically).
  2. When the user enters text in the search box, look through the view models and check for matches in the text. If a match is found, set the indexes appropriately. If a match isn't found, set the indexes back to -1 or whatever signifies no match.
  3. In your view, set the Background of the TextBlocks to the indexes. Use a converter to convert the indexes into a GradientBrush that is bright yellow (or whatever) between the two indexes.

Here's how I think you can figure out the dimensions of the highlighted portions of the TextBlock:

  1. Get a TextPointer via the TextBlock.ContentStart property.
  2. Move to the start of the selection by calling TextPointer.GetPositionAtOffset(indexOfStart) using LogicalDirection.Forwards.
  3. Move to the end of the selection by calling TextPointer.GetPositionAtOffset(indexOfStart) using LogicalDirection.Backwards.
  4. Call TextPointer.GetCharacterRect to get the bounding Rectangle of the highlighted content.

To be honest, I'm not sure that last bit work. I'd have to try it for myself, and I may do that for a blog post.

EDIT: Just had time to try this for myself. It definitely works, although with slight changes to my logic above. Below is the code that demonstrates. Here is a screenshot:

Screenshot http://img219.imageshack.us/img219/2969/searchx.png

Window1.xaml:

<Window x:Class="TextSearch.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1">
    <StackPanel>
        <TextBox x:Name="_searchTextBox"/>
        <Grid>
            <Path Fill="Yellow" Stroke="Black" StrokeThickness="0">
                <Path.Data>
                    <RectangleGeometry x:Name="_rectangleGeometry"/>
                </Path.Data>
            </Path>
            <TextBlock x:Name="_textBlock">Some sample text that you can search through by typing in the above TextBox.</TextBlock>
        </Grid>
    </StackPanel>
</Window>

Window1.xaml.cs:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

namespace TextSearch
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            _searchTextBox.TextChanged += _searchTextBox_TextChanged;
        }

        void _searchTextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            var searchText = _searchTextBox.Text;
            var index = _textBlock.Text.IndexOf(searchText);

            if (index == -1)
            {
                _rectangleGeometry.Rect = Rect.Empty;
            }
            else
            {
                var textPointer = _textBlock.ContentStart;
                textPointer = textPointer.GetPositionAtOffset(index + 1, LogicalDirection.Forward);
                var leftRectangle = textPointer.GetCharacterRect(LogicalDirection.Forward);
                textPointer = textPointer.GetPositionAtOffset(searchText.Length, LogicalDirection.Backward);
                var rightRectangle = textPointer.GetCharacterRect(LogicalDirection.Forward);
                _rectangleGeometry.Rect = new Rect(leftRectangle.TopLeft, rightRectangle.BottomRight);
            }
        }
    }
}

I think the code is fairly self-explanatory. Obviously you will need to extend the concept to your particular scenario. You may prefer to leverage the Background property of the TextBlock combined with a DrawingBrush or GradientBrush instead of having the separate Path.