65.9K
CodeProject 正在变化。 阅读更多。
Home

简单的树形列表视图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (6投票s)

2014年10月3日

CPOL

4分钟阅读

viewsIcon

31399

downloadIcon

407

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

引言

Tree List View

有没有像这样的控件?是不是很酷?可惜,Windows Forms 控件集合中没有这样的控件。但你可以自己做一个;继续阅读。

我们可能已经见过这些类型的控件,它们被称为不同的名称。就本文而言(而且我认为在一般情况下),这类控件可以分为两类。这种分类主要基于所提供的功能,而不是视图本身。因此,这两类控件如下:

  • 树状列表视图 (TLV) - 像传统的 ListView(在其详细视图模式下)一样的控件,它提供了将项添加为控件中其他项的子项的功能,从而可以建立树状结构。项可以进行一些小的修饰,例如复选框或图像。这类控件不提供原地编辑功能。这意味着,如果控件处于编辑模式,它将不提供为每个(子)项弹出相应或关联控件来修改与(子)项关联的值的功能。
  • 树状网格 - 我相信,到目前为止,您已经明白了这种控件能提供什么——树状列表视图所能提供和不能提供的一切。您可以将树状网格与传统的数据网格进行比较,在数据网格中可以添加元素来建立树状层次结构。

这就是树状列表视图控件。让我们看看如何构建一个。

实现计划

我们将从现有的 ListView 类派生出一个新类,命名为 TreeListView。因此,我们的树状列表视图基本上是一个具有列表视图所有功能的控件,在其默认状态下完全相同。不仅如此,我们还需要捕获列表视图项之间的层次结构信息。为此,我们将从现有的 ListViewItem 类派生出一个新类,命名为 ListViewItem2。假设 ListViewItem2 的任何实例都是(任何级别的)父项,我们应该能够添加子列表视图项。换句话说,ListViewItem2 的一个实例是其子项的容器,也是我们自定义渲染逻辑将其渲染为层次结构的提示。

这样就捕获了层次结构。剩下的就是渲染这个层次结构。

掌控渲染

是的,我们将不得不掌控此类控件的绘制逻辑。我们将设置 OwnerDrawtrue,并重写 DrawItemDrawSubItem 来实现自定义逻辑,以便进行适当的渲染。

渲染逻辑包含许多部分。列表视图中的每个项都可以有一个复选框或图像。我们需要根据其父项是展开还是折叠来显示/隐藏项。此外,如果它有子项并且已展开,它将显示一个加号 (+) 图像;如果它有子项并且已折叠,它将显示一个减号 (-) 图像。有子项的项在单击折叠的 (+) 图像时应该展开,在单击展开的 (-) 图像时应该折叠。并且根据深度,每个列表视图项的第一个子项的文本必须相应地进行间隔/制表。当我们双击标题的接缝线时,我们应该处理标题项长度的自动调整。我们的自定义逻辑必须处理所有这些以进行渲染。

以下代码片段胜过千言万语的核心渲染逻辑。请参考附件源代码了解更多详情。

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日 - 初稿
© . All rights reserved.