问题
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_DrawNode
when( formClosing || e.Bounds.X == -1 )
is true to avoid some graphical glitches. You can see my comment below.I liked how
BuildDrawingString
cleaned 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
TreeView
behavior. Especially when mousing down on a node, and mousing up somewhere else.Instead of just using
e.Bounds
to 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;
}
}
回答1:
Part of the information found in the previous question didn't make the cut.
- TextFormatFlags are important: these settings have a great impact on how the Text is rendered. Also, each Control has its own specific requirements, maybe the difference is small - as in this case - but we need to adapt anyway.
The TreeView Control is better served when the Text is align to left and centered vertically. - TextRenderer is very precise, but we want to give it hand (as described before) always using Rectangles as the reference container to both measure and draw the Text. Possibly, don't use a Point, you'll notice that the results can change in similar situation when this simple reference is used. When drawing on a Control, we really don't want that.
- You removed
TextFormatFlags.NoClipping
from the original code, not good, this is a keeper. Unless you actually want to clip the Text, but then you need to specify how to clip it. Other flags can be combined for that.
Specific to this question:
- e.State == TreeNodeStates.Selected is not enough, we also need to test TreeNodeStates.Focused, otherwise we have a weird difference in the Text rendering when a Node is selected or focused; these are different states: one Node can be selected and a different one focused, both must be rendered equally.
- There's a subtle difference between the Graphics bound of the DrawTreeNodeEventArgs and the bounds of the Node item. When drawing the background, the former is used, to define the constraint of the Node text, the latter is used instead.
- Using Font with different weights for the same section of Text, we must use the bounds of the Node as the starting position, use the measures returned by TextRenderer.MeasureText, sum these measures and offset the text position manually (as mentioned, counting on the precision of
MeasureText
). - Whether the Node has an image is not really important, we just need to account for the initial offset, equal to
e.Node.Bounds.X
. In code, it's stored inint drawingPosition = e.Node.Bounds.X;
.
Visual result:
TextFormatFlags twFormat = TextFormatFlags.Left | TextFormatFlags.VerticalCenter |
TextFormatFlags.NoClipping | TextFormatFlags.NoPadding;
private void tree_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
Color textColor = e.Node.ForeColor;
Color backColor = e.Node.BackColor == Color.Empty ? tree.BackColor : e.Node.BackColor;
if (e.State.HasFlag(TreeNodeStates.Selected) || e.State.HasFlag(TreeNodeStates.Focused)) {
textColor = SystemColors.HighlightText;
backColor = SystemColors.Highlight;
}
using (var brush = new SolidBrush(backColor)) {
e.Graphics.FillRectangle(brush, e.Bounds);
}
string searchText = fieldSearch.Text; // Search string from TextBox
int drawingPosition = e.Node.Bounds.X;
foreach (var part in BuildDrawingString(e.Node.Text, searchText)) {
var style = part.Selected ? FontStyle.Bold : FontStyle.Regular;
drawingPosition += RenderNodeText(part.Text, e, style, new Point(drawingPosition, e.Node.Bounds.Y), textColor).Width;
}
}
private Size RenderNodeText(string text, DrawTreeNodeEventArgs e, FontStyle altStyle, Point offset, Color foreColor)
{
using (var font = new Font(tree.Font, altStyle)) {
var size = e.Node.Bounds.Size;
var textWidth = TextRenderer.MeasureText(e.Graphics, text, font, size, twFormat);
var rect = new Rectangle(offset, size);
TextRenderer.DrawText(e.Graphics, text, font, rect, foreColor, e.Node.BackColor, twFormat);
return textWidth;
}
}
private IEnumerable<(string Text, bool Selected)> BuildDrawingString(string itemContent, string pattern)
{
if (pattern.Length == 0) {
yield return (itemContent, false);
}
else {
var matches = Regex.Split(itemContent, $"(?i){pattern}");
int pos = itemContent.IndexOf(pattern, StringComparison.CurrentCultureIgnoreCase);
for (int i = 0; i < matches.Length; i++) {
if (matches[i].Length == 0 && i < matches.Length - 1) {
yield return (itemContent.Substring(pos, pattern.Length), matches[i].Length > 0 ? false : true);
}
else {
yield return (matches[i], false);
if (i < matches.Length - 1) {
yield return (itemContent.Substring(pos, pattern.Length), true);
}
}
}
}
}
来源:https://stackoverflow.com/questions/63121949/positioning-and-highlighting-of-treeview-node-text-with-ownerdrawtext-mode