简单的树形列表视图
一个简单的 .NET WinForms 树状列表视图控件
引言

有没有像这样的控件?是不是很酷?可惜,Windows Forms 控件集合中没有这样的控件。但你可以自己做一个;继续阅读。
我们可能已经见过这些类型的控件,它们被称为不同的名称。就本文而言(而且我认为在一般情况下),这类控件可以分为两类。这种分类主要基于所提供的功能,而不是视图本身。因此,这两类控件如下:
- 树状列表视图 (TLV) - 像传统的
ListView
(在其详细视图模式下)一样的控件,它提供了将项添加为控件中其他项的子项的功能,从而可以建立树状结构。项可以进行一些小的修饰,例如复选框或图像。这类控件不提供原地编辑功能。这意味着,如果控件处于编辑模式,它将不提供为每个(子)项弹出相应或关联控件来修改与(子)项关联的值的功能。 - 树状网格 - 我相信,到目前为止,您已经明白了这种控件能提供什么——树状列表视图所能提供和不能提供的一切。您可以将树状网格与传统的数据网格进行比较,在数据网格中可以添加元素来建立树状层次结构。
这就是树状列表视图控件。让我们看看如何构建一个。
实现计划
我们将从现有的 ListView
类派生出一个新类,命名为 TreeListView
。因此,我们的树状列表视图基本上是一个具有列表视图所有功能的控件,在其默认状态下完全相同。不仅如此,我们还需要捕获列表视图项之间的层次结构信息。为此,我们将从现有的 ListViewItem
类派生出一个新类,命名为 ListViewItem2
。假设 ListViewItem2
的任何实例都是(任何级别的)父项,我们应该能够添加子列表视图项。换句话说,ListViewItem2
的一个实例是其子项的容器,也是我们自定义渲染逻辑将其渲染为层次结构的提示。
这样就捕获了层次结构。剩下的就是渲染这个层次结构。
掌控渲染
是的,我们将不得不掌控此类控件的绘制逻辑。我们将设置 OwnerDraw
为 true
,并重写 DrawItem
和 DrawSubItem
来实现自定义逻辑,以便进行适当的渲染。
渲染逻辑包含许多部分。列表视图中的每个项都可以有一个复选框或图像。我们需要根据其父项是展开还是折叠来显示/隐藏项。此外,如果它有子项并且已展开,它将显示一个加号 (+) 图像;如果它有子项并且已折叠,它将显示一个减号 (-) 图像。有子项的项在单击折叠的 (+) 图像时应该展开,在单击展开的 (-) 图像时应该折叠。并且根据深度,每个列表视图项的第一个子项的文本必须相应地进行间隔/制表。当我们双击标题的接缝线时,我们应该处理标题项长度的自动调整。我们的自定义逻辑必须处理所有这些以进行渲染。
以下代码片段胜过千言万语的核心渲染逻辑。请参考附件源代码了解更多详情。
private void OnDrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
SuspendLayout();
var lvItem = e.Item as ListViewItem2;
if (lvItem == null || lvItem.IsEmpty)
{
return;
}
var txtMetrics = Helpers.GetTextMetrics(e.Graphics);
int yFactor = (e.Bounds.Height - txtMetrics.tmHeight) / 2;
bool hasChildren = lvItem.HasChildren;
int xBound = e.Bounds.X + 5;
if (e.SubItem == e.Item.SubItems[0])
{
int iLevel = lvItem.GetIndentLevel();
bool hasParent = lvItem.Parent == null ? false : true;
xBound += hasParent ? iLevel * 14 : 0;
if (hasChildren)
{
var imageLocation = new Point(xBound, e.Bounds.Y + yFactor + 1);
lvItem.PlusMinusLocation = imageLocation;
var image = lvItem.Expanded ? TreeListView.MinusImage : TreeListView.PlusImage
e.Graphics.DrawImage(image, imageLocation);
xBound += (TreeListView.PlusImage.Width + TreeListView.GeneralGapWidth);
}
if (this.CheckBoxes)
{
Size cbSize = CalculateCheckBoxSize(e.SubItem);
Rectangle cbBounds = new Rectangle(new Point(xBound, e.Bounds.Y), cbSize);
ControlPaint.DrawCheckBox(e.Graphics,
cbBounds,
(lvItem.Checked ? ButtonState.Checked : ButtonState.Normal) | ButtonState.Flat);
lvItem.CheckBoxBounds = cbBounds;
xBound += cbBounds.Width + TreeListView.GeneralGapWidth;
}
if (this.SmallImageList != null
&& e.Item.ImageIndex >= 0
&& e.Item.ImageIndex < this.SmallImageList.Images.Count)
{
Image img = e.Item.ImageList.Images[e.Item.ImageIndex];
int imageWidth = img.Width;
int imageHeight = img.Height - 2;
e.Graphics.DrawImage(img, new Rectangle(xBound, e.Bounds.Y + 1, imageWidth, imageHeight));
xBound += imageWidth + TreeListView.GeneralGapWidth;
}
}
PointF drawPoint = new PointF(xBound, e.Bounds.Y + yFactor);
SizeF drawBound = new SizeF(e.Bounds.X + e.Bounds.Width - xBound, e.Bounds.Height);
RectangleF drawRect = new RectangleF(drawPoint, drawBound);
StringFormat txtFormat = new StringFormat();
txtFormat.Trimming = StringTrimming.EllipsisCharacter;
txtFormat.LineAlignment = ToStringAlignment(e.Header.TextAlign);
e.Graphics.DrawString(e.SubItem.Text,
e.Item.Font,
new SolidBrush(e.Item.ForeColor),
drawRect,
txtFormat);
ResumeLayout(true);
}
就是这样。我们得到了可工作的控件。
兴趣点
- 此控件仅在详细视图模式且
OwnerDraw
设置为true
时才能发挥作用。否则,它只不过是一个普通的 ListView。因此,例如,您可以关闭OwnerDraw
并显示平铺的项;这在我当时的情况下是需要的。 - 截至撰写本文时,不支持列重新排序,但可以支持列重新排序。
- 截至撰写本文时,不支持列大小调整。列宽度被调整以适应最长的内容。可以通过修改
OnColumnWidthChanging
事件处理程序从代码中启用调整大小。然而,“适应内容大小”调整(双击列标题边框)无法实现,因为当通过拖动列标题进行调整大小时,控件会触发ColumnWidthChanging
事件,当双击列标题边框时也会触发。由于无法区分,因此无法以编程方式设置列宽度。
历史
- 2014年10月1日 - 初稿