My Problem
I'm trying to create a TreeView that will bold portions of node text that match a search term. My code was adopted from this question. I have the same/similar problem with or without an ImageList used, but I'll post the unused version here. When I render my text, I get something like this where the last part of the text is cut off, but only for some of the nodes. i.e. Version looks ok, but the rest have differing amounts of text cut off from the bounds.
I think my TextFormatFlags flags are affecting this, but if I don't pass those in during Measure/Render of the text, the node cuts off on the left.
If I draw my bold text, I additionally have vertical spacing issues introduced. You can see when I use Plan as the search term, it is a bit higher than the rest of the text.
If I select a node, you can see I then get horizonal spacing issues again.
My Questions
- Horizontal sizing issues of nodes rendering without bold
- Vertical sizing issues of nodes rendering with bold portions
- Horizontal sizing issues of nodes rendering with bold and active.
Updated Code
Thanks to @jimi, I was able to firm up some stuff. I got close while he was answering, but below are my changes due to his suggestions. I did do a few things differently than he did.
I immediately exit
tree_DrawNodewhen( formClosing || e.Bounds.X == -1 )is true to avoid some graphical glitches. You can see my comment below.I liked how
BuildDrawingStringcleaned up the code. I added a Width property that is calculated and additionally I fixed a bug about the return of matched text.I drew the highlight background when a node had focus and window background when it doesn't to better emulate default
TreeViewbehavior. Especially when mousing down on a node, and mousing up somewhere else.Instead of just using
e.Boundsto draw background rectangle I created my own rectangle based on e.Node.Bounds x/y, the required width for rendered text, and a little padding.private void tree_DrawNode( object sender, DrawTreeNodeEventArgs e ) { var textPadding = 2; // formClosing - don't need to redraw when shutting down, avoids seeing a little glitch with text offset // e.Bounds.X == -1 - when form loads, all *non-top level* nodes seem to draw on top of each other on first line // causing a big 'black blur' to happen when form loads b/c text is mashed together if ( formClosing || e.Bounds.X == -1 ) { return; } using ( var boldFont = new Font( tree.Font, FontStyle.Bold ) ) { var stringParts = BuildDrawingString( e, fieldSearch.Text, boldFont ).ToArray(); // To better emulate default behavior, draw the 'selected' look only when focused, so if // you click down on item, originally selected item draws 'normal' and item clicking on is 'selected' // and if you let up on mouse outside of node, it reverts back to how it was. var isSelected = e.State.HasFlag( TreeNodeStates.Focused ); var color = isSelected ? Color.White : tree.ForeColor; // Use e.NodeBounds X,Y and width of measured text with just a little bit of // padding on left and right, e.Bounds was too wide. var nodeRectangle = new Rectangle( e.Node.Bounds.X, e.Node.Bounds.Y, stringParts.Sum( p => p.Width ) + textPadding * 2, e.Node.Bounds.Height ); e.Graphics.FillRectangle( isSelected ? SystemBrushes.Highlight : SystemBrushes.Window, nodeRectangle ); if ( isSelected ) { using ( var focusPen = new Pen( Color.Black ) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot } ) { nodeRectangle.Size = new Size( nodeRectangle.Width - 1, nodeRectangle.Height - 1 ); e.Graphics.DrawRectangle( focusPen, nodeRectangle ); } } var point = new Point( e.Node.Bounds.X + textPadding, e.Node.Bounds.Y ); foreach ( var part in stringParts ) { var font = part.Selected ? boldFont : tree.Font; RenderNodeText( part.Text, e, font, point, color ); point.Offset( part.Width, 0 ); } } } private void RenderNodeText( string text, DrawTreeNodeEventArgs e, Font font, Point offset, Color color ) { var size = e.Node.Bounds.Size; var rect = new Rectangle( offset, size ); TextRenderer.DrawText( e.Graphics, text, font, rect, color, e.Node.BackColor, treeFlags ); } private IEnumerable<(string Text, bool Selected, int Width)> BuildDrawingString( DrawTreeNodeEventArgs e, string pattern, Font boldFont ) { var itemContent = e.Node.Text; int measureText( string t, bool s ) => TextRenderer.MeasureText( e.Graphics, t, s ? boldFont : tree.Font, e.Bounds.Size, treeFlags ).Width; if ( pattern.Length == 0 ) { yield return (itemContent, false, measureText( itemContent, false )); } else { var matches = Regex.Split( itemContent, $"(?i){pattern}" ); var currentCharacter = 0; var patternLength = pattern.Length; for ( int i = 0; i < matches.Length; i++ ) { if ( matches[ i ].Length >= 0 ) { yield return ( matches[ i ], false, measureText( matches[ i ], false ) ); currentCharacter += matches[ i ].Length; } if ( i < matches.Length - 1 ) { var matchText = itemContent.Substring( currentCharacter, patternLength ); yield return ( matchText, true, measureText( matchText, true ) ); currentCharacter += patternLength; } } } }
New Twist
I moved all the final code created here from a WinForms Application to a Form inside a VSTO Word AddIn project/form and the font rendering is different for some reason.
- The font in general (normal font) looks thinner and smaller.
- The bold font seems offset a bit higher than the normal font.
In the image below, the top form is the one from Word and the second one (caption of Form1) is my WinForms application. Is there some compatability issue or something when running as a VSTO add-in?
Original Code
private void Form1_Load( object sender, EventArgs e )
{
tree.DrawMode = TreeViewDrawMode.OwnerDrawText;
tree.DrawNode += tree_DrawNode;
tree.Font = new Font( "Microsoft YaHei UI", 10F, FontStyle.Regular, GraphicsUnit.Point, 0 );
// tree.ImageList = imageList;
tree.Nodes.Add( "PlanInfo" );
tree.Nodes[ 0 ].Nodes.Add( "Version" );
tree.Nodes[ 0 ].Nodes.Add( "Plan Name" );
tree.Nodes[ 0 ].Nodes.Add( "Plan Sponsor" );
}
TextFormatFlags treeFlags = TextFormatFlags.Top | TextFormatFlags.Left | TextFormatFlags.NoPadding;
private void tree_DrawNode( object sender, DrawTreeNodeEventArgs e )
{
var currentX = 0;
var searchText = e.Node.Text;
var searchTerm = fieldSearch.Text;
var matches = Regex.Split( searchText, "(?i)" + searchTerm );
var point = new Point( e.Node.Bounds.X + currentX, e.Node.Bounds.Y );
var isSelected = ( e.State & TreeNodeStates.Selected ) != 0;
var color = isSelected ? Color.White : tree.ForeColor;
if ( isSelected )
{
e.Graphics.FillRectangle( SystemBrushes.Highlight, e.Node.Bounds );
}
if ( !string.IsNullOrEmpty( searchTerm ) && matches != null )
{
var currentCharacter = 0;
var currentMatch = 0;
var keyLength = searchTerm.Length;
foreach ( var m in matches )
{
if ( !string.IsNullOrEmpty( m ) )
{
point.Offset(
RenderNodeText( m, e, FontStyle.Regular, point, color ).Width,
0
);
currentCharacter += m.Length;
}
currentMatch++;
if ( currentMatch < matches.Length || ( string.IsNullOrEmpty( m ) && currentMatch == 1 ) )
{
var boldText = searchText.Substring( currentCharacter, keyLength );
point.Offset(
RenderNodeText( boldText, e, FontStyle.Bold, point, color ).Width,
0
);
currentCharacter += keyLength;
}
}
}
else
{
RenderNodeText( e.Node.Text, e, FontStyle.Regular, point, color );
}
}
private Size RenderNodeText( string text, DrawTreeNodeEventArgs e, FontStyle altStyle, Point offset, Color color )
{
using ( var font = new Font( tree.Font, altStyle ) )
{
var size = e.Node.Bounds.Size;
var textWidth = TextRenderer.MeasureText( e.Graphics, text, font, size, treeFlags );
var rect = new Rectangle( offset, size );
TextRenderer.DrawText( e.Graphics, text, font, rect, color, Color.Transparent, treeFlags );
return textWidth;
}
}




