使用 OwnerDrawText 模式定位和突出显示 TreeView 节点文本

Posted

技术标签:

【中文标题】使用 OwnerDrawText 模式定位和突出显示 TreeView 节点文本【英文标题】:Positioning and highlighting of TreeView node text with OwnerDrawText mode 【发布时间】:2020-11-17 04:12:28 【问题描述】:

我的问题

我正在尝试创建一个TreeView,它将与搜索词匹配的节点文本部分加粗。我的代码是从this question 采用的。无论是否使用ImageList,我都有相同/相似的问题,但我将在此处发布未使用的版本。当我渲染我的文本时,我会得到这样的东西,其中文本的最后一部分被截断,但 用于某些节点。即 版本 看起来不错,但其余的文本从边界截断的数量不同。

我认为我的TextFormatFlags 标志会影响这一点,但如果我在文本的测量/渲染期间没有传递这些标志,节点会在左侧切断。

如果我绘制粗体文本,还会引入 垂直 间距问题。你可以看到,当我使用 Plan 作为搜索词时,它比文本的其余部分高一点。

如果我选择一个节点,您会看到我再次遇到水平间距问题。

我的问题

    不加粗体的节点渲染的水平尺寸问题 用粗体部分渲染的节点的垂直尺寸问题 使用粗体 活动的节点呈现的水平尺寸问题。

更新代码

感谢@jimi,我能够巩固一些东西。我在他回答时接近了,但以下是我根据他的建议所做的更改。我确实做了一些与他不同的事情。

    ( formClosing || e.Bounds.X == -1 ) 为真时,我会立即退出tree_DrawNode,以避免出现一些图形故障。你可以在下面看到我的评论。

    我喜欢BuildDrawingString 清理代码的方式。我添加了一个计算的 Width 属性,另外我修复了一个关于返回匹配文本的错误。

    当一个节点有焦点时我绘制了高亮背景,而当它没有更好地模拟默认 TreeView 行为时,我绘制了窗口背景。尤其是当鼠标在一个节点上向下移动,而在其他地方鼠标向上移动时。

    我不是仅仅使用e.Bounds 来绘制背景矩形,而是基于 e.Node.Bounds x/y、渲染文本所需的宽度和一点填充创建了自己的矩形。

    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;
                
            
        
    
    

新的转折

我将此处创建的所有最终代码从 WinForms 应用程序移动到 VSTO Word AddIn 项目/表单中的表单,并且由于某种原因字体呈现不同。

    一般字体(普通字体)看起来更细更小。 粗体字体似乎比普通字体偏移了一点。

在下图中,最上面的表单是 Word 中的表单,第二个表单(Form1 的标题)是我的 WinForms 应用程序。作为 VSTO 插件运行时是否存在兼容性问题或其他问题?

原始码

    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;
        
    

【问题讨论】:

e.Node.Bounds 不能用作粗体项目的高亮矩形,因为e.Node 不知道它使用的是不同的字体。您必须使用 TextRendered.MeasureText 调整该矩形的大小 现在正在处理这个问题,但在渲染文本之前我不知道大小。但如果我 FillRectangle 那么它在文本之上。我唯一的选择是只循环一次测量,然后绘制背景,然后再次循环并测量/绘制文本吗? 先测量,然后绘图——就这么简单。我无法从您的 for-each 循环中得出正面或反面。您正在绘制一个节点,为什么会出现循环? 我在节点文本中循环“匹配”,所以我可以用粗体和其他部分常规绘制匹配。因此,如果您搜索“N”,PlanInfo 将看起来像 PlanInfo。 哦,原来是RenderNodeCharacter。在这种情况下,您最好编写一个小字母字典,其中包含粗体和简单的预渲染尺寸。由于TextFormatFlags.Top,垂直对齐已关闭。您可能应该改用VerticalCenter(我猜)。 【参考方案1】:

previous question 中找到的部分信息没有被删除。

TextFormatFlags 很重要:这些设置对文本的呈现方式有很大影响。此外,每个控件都有自己的特定要求,也许差异很小 - 就像在这种情况下一样 - 但我们无论如何都需要适应。 当文本左对齐并垂直居中时,TreeView 控件的效果更好。 TextRenderer 非常精确,但我们希望始终使用矩形作为参考容器来测量和绘制文本(如前所述)。可能,不要使用点,您会注意到,当使用这个简单的参考时,结果可能会在类似的情况下发生变化。在 Control 上绘图时,我们真的不希望这样。 您从原始代码中删除了TextFormatFlags.NoClipping,不好,这是一个守护者。除非你真的想剪辑文本,但是你需要指定如何剪辑它。可以为此组合其他标志。

具体到这个问题:

e.State == TreeNodeStates.Selected 还不够,我们还需要测试TreeNodeStates.Focused,否则我们在选中或聚焦某个Node 时Text 渲染会有一个奇怪的差异;这些是不同的状态:一个节点可以被选中,另一个节点聚焦,两者必须同等渲染。 DrawTreeNodeEventArgs 的 Graphics 边界与 Node 项的边界之间存在细微差别。绘制背景时,使用前者,定义节点文本的约束,使用后者。 对同一段Text使用不同权重的Font,我们必须以Node的bounds为起始位置,使用TextRenderer.MeasureText返回的measures,将这些measures相加并偏移文本位置手动(如前所述,依靠 MeasureText 的精度)。 Node 是否有图像并不重要,我们只需要考虑初始偏移量,等于 e.Node.Bounds.X。在代码中,它存储在 int drawingPosition = e.Node.Bounds.X;

视觉效果:


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);
                
            
        
    

【讨论】:

谢谢。我用我更新的代码和我对您的建议所做的更改更新了问题,以使其功能更好一些。你可以阅读上面的那些。走向三连冠。将发布有关 ListView 的问题,但恐怕会是最棘手的问题。 没有。不反对。以为那是给其他人的,我只是标记“已回答”。但我可以投票。 :) 顺便说一句,我会更新这段代码,它可以在这里和那里改进。当我这样做时,我会 ping 你。 太棒了。迫不及待地想看看。这是我的结局(或者更好地说,也许是你的结局);)***.com/questions/63146058/… 在黑暗中拍摄。这在 WinForms 应用程序中运行良好。我刚刚将代码/控件复制到 Word 的 VSTO Addin 内的表单中,字体渲染有点混乱。任何想法?我会用图片更新问题。

以上是关于使用 OwnerDrawText 模式定位和突出显示 TreeView 节点文本的主要内容,如果未能解决你的问题,请参考以下文章

加载后滚动到引导模式中的元素

对于给定单元格中的每个值,定位并突出显示数据中的相同值

DBGrid通过代码突出显示所定位的行吗?

CSS:将元素定位在悬停或焦点上,但不能同时定位

如何防止 UITableViewCell 重复和重用?

在记事本++中突出显示xml验证错误