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

三态 TreeView - 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (14投票s)

2009 年 4 月 1 日

CPOL

2分钟阅读

viewsIcon

117856

downloadIcon

3322

支持 3 态逻辑的带复选框的 TreeView

引言

我寻找带有三态复选框的Treeview,然后找到了这篇文章。它正确地解释了逻辑,并承诺了第 2 部分,其中应该解决关于所有者绘制复选框的问题。

但是,由于某种原因,第 2 部分从未被编写。但是作者允许观众自由编写第 2 部分。所以我感到很自由。 ;)

重复逻辑

用户只能选中或取消选中Treenodes - 不能设置为不确定状态。 选中/取消选中一个节点会将所有子节点设置为该新状态。 如果ParentNode包含不同状态的节点,那么它将显示不确定状态。

代码

原理是使用带有 3 个图像的StateImageList属性:UncheckedCheckedIndeterminate。 双重逻辑由Treeview很好地完成。 Treeview正确使用前两个图像来显示Checked/Unchecked。 尽管如此,我还是仔细设置了正确的StateImageIndices,尽管这不是必需的(对于双重逻辑)。
但是需要它来保持 3 种状态。 当需要绘制时,我只需要绘制不确定的Checkbox

一个问题是,我需要使用TreeViewDrawMode.OwnerDrawAll来确定要绘制的节点。 但我不想完全绘制节点,因为这非常困难(Checkbox,可选图标,SelectedIcon,Text,SelectedText,Focus)。 我只想在必要时添加我不确定的Checkbox
不幸的是,DrawMode.OwnerDrawAll禁用了_Paint事件,并且没有“AfterDrawNode”事件。 所以我不得不对窗口消息进行子类化,观察WM_PAINT窗口消息何时传递。 在那一刻,我可以绘制我不确定的Checkbox,它们不会被Treeview覆盖绘制。

所以在这里你可以看到ThreeStateTreeview最重要的部分,我希望它被注释得足够好,以至于更多的解释是多余的。

protected override void OnAfterCheck(TreeViewEventArgs e) {
   /* Logic: All children of an (un)checked Node inherit its Checkstate
    * Parents recompute their state: if all children of a parent have same state, 
    * that one will be taken over as parents state - otherwise take Indeterminate 
    */
   if(_skipCheckEvents) return;/* changing any Treenodes .Checked-Property will raise 
                                     another Before- and After-Check. Skip'em */
   _skipCheckEvents = true;
   try {
      TreeNode nd = e.Node;
      /* uninitialized Nodes have StateImageIndex -1, 
       * so I associate StateImageIndex as follows:
       * -1: Unchecked
       *  0: Checked
       *  1: Indeterminate
       *  That corresponds to the System.Windows.Forms.Checkstate - enumeration, 
       *  but 1 less.
       *  Furthermore I ordered the images in that manner
       */
      int state = nd.StateImageIndex == 0 ? -1 : 0;      /* this state is already toggled.
          Note: -1 (Unchecked) and 1 (Indeterminate) both toggle to 0, 
                        that means: Checked */
      if((state == 0) != nd.Checked) return;       //suppress redundant AfterCheck-event
      InheritCheckstate(nd, state);         // inherit Checkstate to children
      // Parents recompute their state
      nd = nd.Parent;
      while(nd != null) {
         // At Indeterminate (==1) skip the children-query - 
         // every parent becomes Indeterminate
         if(state != 1) {
            foreach(TreeNode ndChild in nd.Nodes) {
               if(ndChild.StateImageIndex != state) {
                  state = 1;
                  break;
               }
            }
         }
         AssignState(nd, state);
         nd = nd.Parent;
      }
      base.OnAfterCheck(e);
   } finally { _skipCheckEvents = false; }
}

private void AssignState(TreeNode nd, int state) {
   bool ck = state == 0;
   bool stateInvalid = nd.StateImageIndex != state;
   if(stateInvalid) nd.StateImageIndex = state;
   if(nd.Checked != ck) {
      nd.Checked = ck;                // changing .Checked-Property raises 
                // Invalidating internally
   } else if(stateInvalid) {
      // in general: the less and small the invalidated area, the less flickering
      // so avoid calling Invalidate() if possible, and only call, if really needed.
      this.Invalidate(GetCheckRect(nd));
   }
}

private void InheritCheckstate(TreeNode nd, int state) {
   AssignState(nd, state);
   foreach(TreeNode ndChild in nd.Nodes) {
      InheritCheckstate(ndChild, state);
   }
}

public System.Windows.Forms.CheckState GetState(TreeNode nd) {
   // compute the System.Windows.Forms.CheckState from a StateImageIndex 
   // is not that complicated
   return (CheckState)nd.StateImageIndex + 1;
}

protected override void OnDrawNode(DrawTreeNodeEventArgs e) {
   // here nothing is drawn. Only collect Indeterminated Nodes, 
   // to draw them later (in WndProc())
   // because drawing Treenodes properly (Text, Icon(s) Focus, Selection...) 
   // is very complicated
   if(e.Node.StateImageIndex == 1) _indeterminateds.Add(e.Node);
   e.DrawDefault = true;
   base.OnDrawNode(e);
}

protected override void WndProc(ref Message m) {
   const int WM_Paint = 15;
   base.WndProc(ref m);
   if(m.Msg == WM_Paint) {
      // at that point built-in drawing is completed - 
      // and I paint over the Indeterminate-Checkboxes
      foreach(TreeNode nd in _indeterminateds) {
         _graphics.DrawImage(_imgIndeterminate, GetCheckRect(nd).Location);
      }
      _indeterminateds.Clear();
   }
}

致谢

  • 三态树形控件 - 第 1 部分 - 虽然我没有使用该代码中的任何一行,但它给了我一个关于如何将Checked属性与StateImageIndex的 3 个选项同步的想法,以及如何在更新Treenode状态时避免多次 Before-/After-Check-Events。

历史

  • 2009 年 4 月 1 日:首次发布
  • 2010 年 5 月 18 日:Bug 修复:Christo667 报告了一个隐藏很好的 bug,当以编程方式将节点的Checked属性设置为与其之前相同的值时(请参见消息板)。 该错误的根本原因是,在这种情况下,常见的TreeView会引发冗余的 Before-/After-Checked-Events,而ThreeStateTreeview会切换节点外观,即使它不应该这样做。
    现在ThreeStateTreeview抑制了这些冗余事件。 这也可能是对常见TreeView的 bug 解决方法。
    谢谢你,Christo!
  • 修复了双击节点复选框时 Standard-Treeview 的错误。
© . All rights reserved.