从DataTable中填充WinForms TreeView

15
我有一个WinForm TreeView Control,用于显示CaseNotes的父子关系(我知道这对大多数人来说没有意义,但它可以帮助我理解答案)。
我有一个需要显示的CaseNotes DataTable。 父/子关系定义如下:如果行具有ParentNoteID,则它是该笔记的ChildNode,否则它是rootNode。 如果另一行的ID作为其ParentNoteID,则它也可能是父笔记(但不是rootNode)。
为了使事情变得复杂(也许简单),我已经编写了以下工作代码(大部分)以交替着色节点。 我手动为treeview创建了静态集合,并且它们的颜色相当正确。 现在我需要从我的DataTable动态填充节点。
既然我已经逐个遍历treeview节点,那么我应该能够在此过程中将数据附加到其中,对吗? 也许我需要先构建节点,然后再单独着色作为例行程序,但递归方法仍然适用,对吗?
假设我想为每个节点显示CaseNoteID。 在DataTable中返回并且是唯一的。
foreach (TreeNode rootNode in tvwCaseNotes.Nodes)
        {
            ColorNodes(rootNode, Color.MediumVioletRed, Color.DodgerBlue);

        }
protected void ColorNodes(TreeNode root, Color firstColor, Color secondColor)
    {
        root.ForeColor = root.Index % 2 == 0 ? firstColor : secondColor;

        foreach (TreeNode childNode in root.Nodes)
        {
            Color nextColor = childNode.ForeColor = childNode.Index % 2 == 0 ? firstColor : secondColor;

            if (childNode.Nodes.Count > 0)
            {
                // alternate colors for the next node
                if (nextColor == firstColor)
                    ColorNodes(childNode, secondColor, firstColor);
                else
                    ColorNodes(childNode, firstColor, secondColor);
            }
        }
    }

编辑

我目前的想法/尝试:

        public void BuildSummaryView()
    {
        tvwCaseNotes.Nodes.Clear();

        DataTable cNotesForTree = CurrentCaseNote.GetAllCNotes(Program._CurrentPerson.PersonID);
        foreach (var cNote in cNotesForTree.Rows)
        {

            tvwCaseNotes.Nodes.Add(new TreeNode("ContactDate"));
        }
        FormPaint();
    }
显然这是有缺陷的。首先它只是一遍又一遍地显示 ContactDate。虽然它显示了正确次数,但我想要的是ContactDate的值(它是数据库中的列,并在 DataTable 中返回)。其次,我需要添加 ChildNode 逻辑。一个if (node.parentNode = node.CaseNoteID) blah... 编辑2:
所以我找到了这个链接,在这里,看起来像是我需要将我的 DataTable 转换成 ArrayList。这是正确的吗?
编辑3:
好的,多亏了 Cerebus,这基本上是有效的。我只有一个问题:我如何获取这个内容?
DataTable cNotesForTree = CurrentCaseNote.GetAllCNotes(Program._CurrentPerson.PersonID);

我该如何使用返回的 DataTable?只需要将其替换这里的内容吗?

    dt = new DataTable("CaseNotes");
dt.Columns.Add("NoteID", typeof(string));
dt.Columns.Add("NoteName", typeof(string));
DataColumn dc = new DataColumn("ParentNoteID", typeof(string));
dc.AllowDBNull = true;
dt.Columns.Add(dc);

// Add sample data.
dt.Rows.Add(new string[] { "1", "One", null });
dt.Rows.Add(new string[] { "2", "Two", "1" });
dt.Rows.Add(new string[] { "3", "Three", "2" });
dt.Rows.Add(new string[] { "4", "Four", null });
dt.Rows.Add(new string[] { "5", "Five", "4" });
dt.Rows.Add(new string[] { "6", "Six", null });
dt.Rows.Add(new string[] { "7", "Seven", null });
dt.Rows.Add(new string[] { "8", "Eight", "7" });
dt.Rows.Add(new string[] { "9", "Nine", "8" });

我感到困惑的是,我是否仍需要执行Column.Add和Row.Adds操作?此外,DataColumn应该如何转换为我的实际数据结构?抱歉提出这些非常幼稚的问题,好消息是我再也不必问第二次了。

编辑4

以下代码运行时会出现错误。

if (nodeList.Find(FindNode) == null)
  {
    DataRow[] childRows = dt.Select("ParentNoteID = " + dr["NoteID"]);
    if (childRows.Length > 0)
    {
      // Recursively call this function for all childRowsl
      TreeNode[] childNodes = RecurseRows(childRows);

      // Add all childnodes to this node.
      node.Nodes.AddRange(childNodes);
    }

    // Mark this noteID as dirty (already added).
    //doneNotes.Add(noteID);
    nodeList.Add(node);
  }

错误信息为 --> 找不到列[ea8428e4],这是正确的NoteID的前8位(我必须使用Guid)。它是否应该查找该名称的列?因为我正在使用Guid,所以我需要做些什么其他的事情吗?我已将我的代码和您的代码中的所有引用更改为Guid...


关于“编辑2”-不,这不是必要的,只要你能执行自己的逻辑来管理数据结构中的迭代。 - Cerebrus
关于“编辑3”-我代码的那部分是用来构建一个带有示例数据的样本表格,以确保它能正常工作。如果您的GetAllCNotes函数返回具有类似结构的DataTable,则可以直接删除整个CreateData函数。 - Cerebrus
谢谢,我正在把它们拼合在一起。万分感谢!不过我有个问题,就是在这段代码中 --> DataRow[] childRows = dt.Select("ParentNoteID = " + dr["NoteID"]); 它给了我一个运行时错误,说找不到该列的名称但却列出了一个值... 你有什么想法吗? - Refracted Paladin
把下列与编程有关的内容从英语翻译成中文。请仅返回翻译后的文本,不要进行任何解释。谢谢。 - Refracted Paladin
关于“编辑4” - 您可能需要进行简单的更改: dt.Select("ParentNoteID = '" + dr["NoteID"] + "'"); 这会在NoteID周围添加单引号,表示它是一个字符串(请参见SQL WHERE子句语法)。 - Cerebrus
有人知道一个相反的方法吗? - Lucifer
3个回答

12
为了解决这个问题,我创建了一个示例Windows窗体并编写了以下代码。我想象中的datatable设计如下:
 NoteID  NoteName  ParentNoteID
   "1"    "One"        null
   "2"    "Two"        "1"
   "3"    "Three"      "2"
   "4"    "Four"       null
...

这应该创建一棵树,如下所示(对不起,我不太擅长ASCII艺术!):
One
 |
 ——Two
 |
 ————Three
 |
Four

伪代码如下:
  1. 遍历数据表中的所有行。
  2. 对于每一行,创建一个TreeNode并设置其属性。递归地重复此过程,直到所有具有与此行ID匹配的ParentNodeID的行都被处理完毕。
  3. 每次完整迭代返回一个节点,该节点将包含所有匹配的子节点,可以无限嵌套。
  4. 将完成的节点列表添加到TreeView中。
在您的情况下出现问题是因为“外键”引用同一表中的列。这意味着当我们遍历行时,必须跟踪哪些行已经被解析。例如,在上面的表中,与第二行和第三行匹配的节点已经在第一次完整迭代中添加。因此,我们不能再次添加它们。有两种方法来跟踪这个:
  1. 维护一个已完成节点的ID列表(doneNotes)。在添加每个新节点之前,检查noteID是否存在于该列表中。这是更快的方法,通常应该优先考虑。(此方法在下面的代码中被注释掉了
  2. 对于每次迭代,使用谓词泛型委托(FindNode)搜索已添加节点的列表(考虑到嵌套节点),以查看要添加的节点是否存在于该列表中。这是较慢的解决方案,但我有点喜欢复杂的代码!:P
好的,这是经过验证的代码(C# 2.0):
public partial class TreeViewColor : Form
{
  private DataTable dt;
  // Alternate way of maintaining a list of nodes that have already been added.
  //private List<int> doneNotes;
  private static int noteID;

  public TreeViewColor()
  {
    InitializeComponent();
  }

  private void TreeViewColor_Load(object sender, EventArgs e)
  {
    CreateData();
    CreateNodes();

    foreach (TreeNode rootNode in treeView1.Nodes)
    {
      ColorNodes(rootNode, Color.MediumVioletRed, Color.DodgerBlue);
    }
  }

  private void CreateData()
  {
    dt = new DataTable("CaseNotes");
    dt.Columns.Add("NoteID", typeof(string));
    dt.Columns.Add("NoteName", typeof(string));
    DataColumn dc = new DataColumn("ParentNoteID", typeof(string));
    dc.AllowDBNull = true;
    dt.Columns.Add(dc);

    // Add sample data.
    dt.Rows.Add(new string[] { "1", "One", null });
    dt.Rows.Add(new string[] { "2", "Two", "1" });
    dt.Rows.Add(new string[] { "3", "Three", "2" });
    dt.Rows.Add(new string[] { "4", "Four", null });
    dt.Rows.Add(new string[] { "5", "Five", "4" });
    dt.Rows.Add(new string[] { "6", "Six", null });
    dt.Rows.Add(new string[] { "7", "Seven", null });
    dt.Rows.Add(new string[] { "8", "Eight", "7" });
    dt.Rows.Add(new string[] { "9", "Nine", "8" });
  }

  private void CreateNodes()
  {
    DataRow[] rows = new DataRow[dt.Rows.Count];
    dt.Rows.CopyTo(rows, 0);
    //doneNotes = new List<int>(9);

    // Get the TreeView ready for node creation.
    // This isn't really needed since we're using AddRange (but it's good practice).
    treeView1.BeginUpdate();
    treeView1.Nodes.Clear();

    TreeNode[] nodes = RecurseRows(rows);
    treeView1.Nodes.AddRange(nodes);

    // Notify the TreeView to resume painting.
    treeView1.EndUpdate();
  }

  private TreeNode[] RecurseRows(DataRow[] rows)
  {
    List<TreeNode> nodeList = new List<TreeNode>();
    TreeNode node = null;

    foreach (DataRow dr in rows)
    {
      node = new TreeNode(dr["NoteName"].ToString());
      noteID = Convert.ToInt32(dr["NoteID"]);

      node.Name = noteID.ToString();
      node.ToolTipText = noteID.ToString();

      // This method searches the "dirty node list" for already completed nodes.
      //if (!doneNotes.Contains(doneNoteID))

      // This alternate method using the Find method uses a Predicate generic delegate.
      if (nodeList.Find(FindNode) == null)
      {
        DataRow[] childRows = dt.Select("ParentNoteID = " + dr["NoteID"]);
        if (childRows.Length > 0)
        {
          // Recursively call this function for all childRowsl
          TreeNode[] childNodes = RecurseRows(childRows);

          // Add all childnodes to this node.
          node.Nodes.AddRange(childNodes);
        }

        // Mark this noteID as dirty (already added).
        //doneNotes.Add(noteID);
        nodeList.Add(node);
      }
    }

    // Convert this List<TreeNode> to an array so it can be added to the parent node/TreeView.
    TreeNode[] nodeArr = nodeList.ToArray();
    return nodeArr;
  }

  private static bool FindNode(TreeNode n)
  {
    if (n.Nodes.Count == 0)
      return n.Name == noteID.ToString();
    else
    {
      while (n.Nodes.Count > 0)
      {
        foreach (TreeNode tn in n.Nodes)
        {
          if (tn.Name == noteID.ToString())
            return true;
          else
            n = tn;
        }
      }
      return false;
    }
  }

  protected void ColorNodes(TreeNode root, Color firstColor, Color secondColor)
  {
    root.ForeColor = root.Index % 2 == 0 ? firstColor : secondColor;

    foreach (TreeNode childNode in root.Nodes)
    {
      Color nextColor = childNode.ForeColor = childNode.Index % 2 == 0 ? firstColor : secondColor;

      if (childNode.Nodes.Count > 0)
      {
        // alternate colors for the next node
        if (nextColor == firstColor)
          ColorNodes(childNode, secondColor, firstColor);
        else
          ColorNodes(childNode, firstColor, secondColor);
      }
    }
  }
}


请注意(双关语),我已保留您的ColorNodes函数不变。 - Cerebrus
这可能是个愚蠢的问题,但当创建和使用像这样的局部类时,您通常会将其放在哪里?是单独的类库吗?还是作为我的BLL的一部分?谢谢! - Refracted Paladin
不,这是一个纯粹的Windows表单。 - Cerebrus
是的,抱歉我发帖后不久就解决了,但忘记回来更新了。 - Refracted Paladin
谢谢Cerebrus。但是这段代码在某些数据格式上并不完美,请参考http://stackoverflow.com/questions/9044104/how-to-insert-datas-to-the-winform-treeviewc-in-effitive-coding。 - Sagotharan
顺便说一下,有人在1:1地复制你的答案:http://stackoverflow.com/a/9045072/435093 - slhck

1
我已经为TreeView创建了更简单的扩展方法,涉及使用新的简单扩展类,该类为TreeNode添加了两个有用的属性。
    internal class IdNode : TreeNode
    {
        public object Id { get; set; }
        public object ParentId { get; set; }
    }

    public static void PopulateNodes(this TreeView treeView1, DataTable dataTable, string name, string id, string parentId)
    {
        treeView1.BeginUpdate();
        foreach (DataRow row in dataTable.Rows)
        {
            treeView1.Nodes.Add(new IdNode() { Name = row[name].ToString(), Text = row[name].ToString(), Id = row[id], ParentId = row[parentId], Tag = row });
        }
        foreach (IdNode idnode in GetAllNodes(treeView1).OfType<IdNode>())
        {
            foreach (IdNode newparent in GetAllNodes(treeView1).OfType<IdNode>())
            {
                if (newparent.Id.Equals(idnode.ParentId))
                {
                    treeView1.Nodes.Remove(idnode);
                    newparent.Nodes.Add(idnode);
                    break;
                }
            }
        }
        treeView1.EndUpdate();
    }

    public static List<TreeNode> GetAllNodes(this TreeView tv)
    {
        List<TreeNode> result = new List<TreeNode>();
        foreach (TreeNode child in tv.Nodes)
        {
            result.AddRange(GetAllNodes(child));
        }
        return result;
    }
    public static List<TreeNode> GetAllNodes(this TreeNode tn)
    {
        List<TreeNode> result = new List<TreeNode>();
        result.Add(tn);
        foreach (TreeNode child in tn.Nodes)
        {
            result.AddRange(GetAllNodes(child));
        }
        return result;
    }

感谢 modiX 提供的 方法,可以获取所有(嵌套的)节点。

-1

请检查这个:

Public Sub BuildTree(ByVal dt As DataTable, ByVal trv As TreeView, ByVal expandAll As [Boolean])
    ' Clear the TreeView if there are another datas in this TreeView
    trv.Nodes.Clear()
    Dim node As TreeNode
    Dim subNode As TreeNode
    For Each row As DataRow In dt.Rows
        'search in the treeview if any country is already present
        node = Searchnode(row.Item(0).ToString(), trv)
        If node IsNot Nothing Then
           'Country is already present
            subNode = New TreeNode(row.Item(1).ToString())
            'Add cities to country
            node.Nodes.Add(subNode)
        Else
            node = New TreeNode(row.Item(0).ToString())
            subNode = New TreeNode(row.Item(1).ToString())
            'Add cities to country
            node.Nodes.Add(subNode)
            trv.Nodes.Add(node)
        End If
    Next
    If expandAll Then
        ' Expand the TreeView
        trv.ExpandAll()
    End If
End Sub

获取更多和完整源代码的方式:如何在vb.net中从datatable填充treeview


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接