Thursday, June 11, 2009

Search and Highlight Text in an Arbitrary Visual Tree

image A while back I answered this question on stackoverflow and have been meaning to elaborate on my answer ever since with a more comprehensive blog post. The question is about how to highlight some text in the UI when the user enters some search text. I thought I’d extend the concept into a clean, generic solution as far as I could and share it here.

My requirements are pretty straightforward:

  1. Provide a search box in which the user can enter some text.
  2. Highlight all matching text in the window, regardless of where it appears in the visual tree.

imageI was able to pull this off to an extent I am satisfied with. See the screenshot to the left. The user can enter some text in the top-right search text box, and then hit enter (or click the magnifying glass). Then all matching text is highlighted anywhere in the visual tree.

After an initial approach that relied on reflection in order to highlight content elements (ugh!), Marlon Grech kindly passed my question about how to do this more cleanly onto Dr. WPF and Jamie Rodriguez, who pointed me in the right direction (thanks guys!).

The basic approach I ended up using is:

  1. Traverse the visual tree recursively, looking for IContentHosts and DocumentViewerBases. For DocumentViewerBases, extract the IContentHosts corresponding to each page.
  2. For each IContentHost, search through its HostedElements collection to find any Runs.
  3. For each Run, inspect its Text to determine whether the search term matches.
  4. If the search term matches, use some TextPointer trickery to determine the bounding rectangle of the text.
  5. Use a Canvas to lay out all semi-transparent rectangles on top of the application window, thus highlighting the matches.

There are a few problems that I’m aware of:

  1. If the matching text spans multiple lines, the bounding rectangle isn’t correctly calculated. You could use IContentHost.GetRectangles to calculate all bounding rectangles correctly, but I just haven’t bothered to do so for the purposes of this post.
  2. It doesn’t work for all content element hosts, such as FlowDocumentScrollViewer, because they don’t inherit from DocumentViewerBase. I suspect it’s probably not too hard to add support for them, but again I haven’t bothered to do so for this example.
  3. It’s quite slow when the search term is short and frequently matched (as an example, try searching for “a”).

So the approach isn’t perfect, but this was an experiment more than anything else. I think a real application would want search support more intrinsic rather than relying on a generic mechanism like this. That said, there are bound to be some practical applications of the general technique used here.

PS. You may have noticed a lack of TMNT-related material in my sample this post. Unfortunately, my Windows Home Server recently died, which has prevented me from continuing to watch the series and has let my obsession waver somewhat. A replacement server is due next week, so expect more green reptile-based content soon!


anvaka said...

Hi Kent!

Thank you for sharing your brilliant solution!

By the way, have you tried using TextEffects to highlight matches? All TextElements have Foreground property, and PositionStart + PositionCount. This could solve problems with changing application/text size and multiple lines...

anvaka said...

> [..] All TextElements have Foreground property.

Sorry, I'm little slow :). I meant all TextElements have TextEffects property, which in turn has Foreground and PositionStart + PositionCount properties.

Kent Boogaart said...

@Anvaka: thanks. Yeah, using TextEffects would have been ideal apart from two major problems:

1. it only supports changing foreground color (I wanted to highlight the background) and transforms (of which none are particularly amenable to highlighting search results)
2. it doesn't appear to work very well at all with Runs inside a content host, such as the document viewer in my sample. It just appears to get very confused and apply the effect to the wrong section of text, or not at all.

anvaka said...

> 2. it doesn't appear to work very well at all with Runs inside a content host ...

It's interesting. Looks like a bug. Or maybe they had a specific reason for this behaviour, who knows...

Anonymous said...

Why not use adorners?

Kent Boogaart said...

@Anonymous: more importantly: why use adorners? Why complicate a POC with something that adds no value?

Anonymous said...

There's an additional problem.

When resizing the window, text is wrapped to other lines / positions, while highlight rectangles don't.

In other words, rectangles become irrelevant.

This could probably be solved by re-searching when layout invalidates.

RredCat said...

Hello Kent.
I have solved issues of previous Anonymous. You can check it and add to your code.

Anonymous said...

Thank you, thank you...just what I needed.