OwnerDrawText模式下TreeView节点文本的定位和高亮
Positioning and highlighting of TreeView node text with OwnerDrawText mode
我的问题
我正在尝试创建一个 TreeView
,它将与搜索词匹配的节点文本部分加粗。我的代码是从 采纳的。使用或不使用 ImageList
我都会遇到 same/similar 问题,但我将在此处 post 未使用的版本。当我呈现我的文本时,我得到这样的结果,其中文本的最后部分被截断,但某些节点仅 。即 Version 看起来不错,但其余部分的文本数量不同。
我认为我的 TextFormatFlags
标志对此有影响,但如果我在文本的 Measure/Render 期间不传递这些标志,节点将在左侧切断。
如果我绘制粗体文本,还会引入 垂直 间距问题。你可以看到当我使用Plan作为搜索词时,它比文本的其余部分高一点。
如果我 select 一个节点,你可以看到我又遇到了水平间距问题。
我的问题
- 没有加粗的节点渲染的水平大小问题
- 使用粗体部分呈现的节点的垂直大小问题
- 使用粗体 和 激活的节点呈现的水平大小问题。
更新代码
感谢@jimi,我能够巩固一些东西。我在他回答时接近了,但下面是我根据他的建议所做的更改。我确实做了一些与他不同的事情。
当 ( formClosing || e.Bounds.X == -1 )
为真时我立即退出 tree_DrawNode
以避免一些图形故障。你可以在下面看到我的评论。
我喜欢 BuildDrawingString
清理代码的方式。我添加了一个计算出的宽度 属性,另外我修复了一个关于匹配文本 return 的错误。
我在节点有焦点时绘制高亮背景,在没有焦点时绘制 window 背景以更好地模拟默认 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 加载项中的表单 project/form,并且由于某种原因字体呈现不同。
- 一般的字体(normal font)看起来更细更小
- 粗体字看起来比普通字体偏高了一点。
在下图中,顶部的表格是来自 Word 的表格,第二个表格(Form1 的标题)是我的 WinForms 应用程序。当 运行 作为 VSTO add-in 时是否存在一些兼容性问题或其他问题?
原码
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;
}
}
在 中找到的部分信息没有被删除。
- TextFormatFlags 很重要:这些设置对文本的呈现方式有很大影响。此外,每个控件都有自己的特定要求,也许差异很小 - 就像在这种情况下 - 但无论如何我们都需要适应。
当文本向左对齐并垂直居中时,TreeView 控件的效果会更好。
- TextRenderer 非常精确,但我们希望(如前所述)始终使用矩形作为参考容器来测量和绘制文本。可能不使用点,您会注意到使用这个简单的引用时,结果可能会在类似情况下发生变化。在控件上绘制时,我们真的不希望那样。
- 你从原来的代码中删除了
TextFormatFlags.NoClipping
,不好,这是一个守门员。除非你真的想剪辑文本,但你需要指定如何剪辑它。可以为此组合其他标志。
具体到这个问题:
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;这些是不同的状态:一个节点可以被选中,一个不同的节点被聚焦,两者必须被平等地渲染。
DrawTreeNodeEventArgs
的图形边界与节点项的边界之间存在细微差别。绘制背景时,使用前者,定义Node文字的约束,改用后者。
- 对同一段Text使用不同权重的Font,必须以Node的bounds为起始位置,使用
TextRenderer.MeasureText
返回的measures,将这些measures相加,偏移text位置手动(如前所述,依靠MeasureText
的精度)。
- 节点是否有图像并不重要,我们只需要考虑初始偏移量,等于
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);
}
}
}
}
}
我的问题
我正在尝试创建一个 TreeView
,它将与搜索词匹配的节点文本部分加粗。我的代码是从 ImageList
我都会遇到 same/similar 问题,但我将在此处 post 未使用的版本。当我呈现我的文本时,我得到这样的结果,其中文本的最后部分被截断,但某些节点仅 。即 Version 看起来不错,但其余部分的文本数量不同。
我认为我的 TextFormatFlags
标志对此有影响,但如果我在文本的 Measure/Render 期间不传递这些标志,节点将在左侧切断。
如果我绘制粗体文本,还会引入 垂直 间距问题。你可以看到当我使用Plan作为搜索词时,它比文本的其余部分高一点。
如果我 select 一个节点,你可以看到我又遇到了水平间距问题。
我的问题
- 没有加粗的节点渲染的水平大小问题
- 使用粗体部分呈现的节点的垂直大小问题
- 使用粗体 和 激活的节点呈现的水平大小问题。
更新代码
感谢@jimi,我能够巩固一些东西。我在他回答时接近了,但下面是我根据他的建议所做的更改。我确实做了一些与他不同的事情。
当
( formClosing || e.Bounds.X == -1 )
为真时我立即退出tree_DrawNode
以避免一些图形故障。你可以在下面看到我的评论。我喜欢
BuildDrawingString
清理代码的方式。我添加了一个计算出的宽度 属性,另外我修复了一个关于匹配文本 return 的错误。我在节点有焦点时绘制高亮背景,在没有焦点时绘制 window 背景以更好地模拟默认
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 加载项中的表单 project/form,并且由于某种原因字体呈现不同。
- 一般的字体(normal font)看起来更细更小
- 粗体字看起来比普通字体偏高了一点。
在下图中,顶部的表格是来自 Word 的表格,第二个表格(Form1 的标题)是我的 WinForms 应用程序。当 运行 作为 VSTO add-in 时是否存在一些兼容性问题或其他问题?
原码
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;
}
}
在
- TextFormatFlags 很重要:这些设置对文本的呈现方式有很大影响。此外,每个控件都有自己的特定要求,也许差异很小 - 就像在这种情况下 - 但无论如何我们都需要适应。
当文本向左对齐并垂直居中时,TreeView 控件的效果会更好。 - TextRenderer 非常精确,但我们希望(如前所述)始终使用矩形作为参考容器来测量和绘制文本。可能不使用点,您会注意到使用这个简单的引用时,结果可能会在类似情况下发生变化。在控件上绘制时,我们真的不希望那样。
- 你从原来的代码中删除了
TextFormatFlags.NoClipping
,不好,这是一个守门员。除非你真的想剪辑文本,但你需要指定如何剪辑它。可以为此组合其他标志。
具体到这个问题:
e.State == TreeNodeStates.Selected
is not enough, we also need to testTreeNodeStates.Focused
, otherwise we have a weird difference in the Text rendering when a Node is selected or focused;这些是不同的状态:一个节点可以被选中,一个不同的节点被聚焦,两者必须被平等地渲染。DrawTreeNodeEventArgs
的图形边界与节点项的边界之间存在细微差别。绘制背景时,使用前者,定义Node文字的约束,改用后者。- 对同一段Text使用不同权重的Font,必须以Node的bounds为起始位置,使用
TextRenderer.MeasureText
返回的measures,将这些measures相加,偏移text位置手动(如前所述,依靠MeasureText
的精度)。 - 节点是否有图像并不重要,我们只需要考虑初始偏移量,等于
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);
}
}
}
}
}