【问题标题】:Avoiding Infinite Loops While Traversing Children Controls在遍历子控件时避免无限循环
【发布时间】:2010-07-09 17:38:58
【问题描述】:

我正在编写一个简单的扩展方法来对控件及其所有子控件执行操作,我想知道我是否必须担心两次遇到同一个控件。

安全:

public static void Traverse(this Control control, Action<Control> action)
{
    Traverse(control, action, new HashSet<control>());
}

private static void Traverse(this Control control, Action<Control> action, HashSet<Control> handled)
{
    handled.Add(control);
    foreach (Control child in control.Controls)
        if (!handled.Contains(child))
            Traverse(child, action, handled);
    action.Invoke(control);
}

可能不安全:

public static void Traverse(this Control control, Action<Control> action)
{
    foreach (Control child in control.Controls)
        Traverse(child, action, handled);
    action.Invoke(control);
}

散列集是保证这段代码安全所必需的吗?每个控件只需要调用一次动作,不能进入死循环。是不是父子控件的结构这样我就不用担心了?

用法:

this.Traverse(o => o.SuspendLayout());

// Do lots of UI changes

this.Traverse(o => o.ResumeLayout());

执行此操作的(可能)综合方法:

public static class ControlExtensions
{
    public static void Traverse(this Control control, Action<Control> action)
    {
        Traverse(control, action, TraversalMethod.DepthFirst);
    }

    public static void Traverse(this Control control, Action<Control> action, TraversalMethod method)
    {
        switch (method)
        {
            case TraversalMethod.DepthFirst:
                TraverseDepth(control, action);
                break;
            case TraversalMethod.BreadthFirst:
                TraverseBreadth(control, action);
                break;
            case TraversalMethod.ReversedDepthFirst:
                TraverseDepthReversed(control, action);
                break;
            case TraversalMethod.ReversedBreadthFirst:
                TraverseBreadthReversed(control, action);
                break;
        }
    }

    private static void TraverseDepth(Control control, Action<Control> action)
    {
        Stack<Control> controls = new Stack<Control>();
        Queue<Control> queue = new Queue<Control>();

        controls.Push(control);
        while (controls.Count != 0)
        {
            control = controls.Pop();
            foreach (Control child in control.Controls)
                controls.Push(child);
            queue.Enqueue(control);
        }
        while (queue.Count != 0)
            action.Invoke(queue.Dequeue());
    }

    private static void TraverseBreadth(Control control, Action<Control> action)
    {
        Queue<Control> controls = new Queue<Control>();
        Queue<Control> queue = new Queue<Control>();

        controls.Enqueue(control);
        while (controls.Count != 0)
        {
            control = controls.Dequeue();
            foreach (Control child in control.Controls)
                controls.Enqueue(child);
            queue.Enqueue(control);
        }
        while (queue.Count != 0)
            action.Invoke(queue.Dequeue());
    }

    private static void TraverseDepthReversed(Control control, Action<Control> action)
    {
        Stack<Control> controls = new Stack<Control>();
        Stack<Control> stack = new Stack<Control>();

        controls.Push(control);
        while (controls.Count != 0)
        {
            control = controls.Pop();
            foreach (Control child in control.Controls)
                controls.Push(child);
            stack.Push(control);
        }
        while (stack.Count != 0)
            action.Invoke(stack.Pop());
    }

    private static void TraverseBreadthReversed(Control control, Action<Control> action)
    {
        Queue<Control> controls = new Queue<Control>();
        Stack<Control> stack = new Stack<Control>();

        controls.Enqueue(control);
        while (controls.Count != 0)
        {
            control = controls.Dequeue();
            foreach (Control child in control.Controls)
                controls.Enqueue(child);
            stack.Push(control);
        }
        while (stack.Count != 0)
            action.Invoke(stack.Pop());
    }
}

【问题讨论】:

  • 如果一个孩子有多个父母,那么 Child.Parent 会返回什么?哪个父母会包含孩子?事件/击键会在哪里冒泡?
  • @Rob:所有优点。可以说,可能存在不同类型的遏制,例如一棵树用于事件,另一棵树用于显示(z 顺序等)。在这种情况下,某些控件可能会出现在一个集合中,但不会出现在另一个集合中,而其余控件则同时出现在两个集合中。
  • WPF 实际上在某种程度上做到了这一点。 “控件”的逻辑树和可视树不一定在所有情况下都相同...请参阅:msdn.microsoft.com/en-us/library/ms753391.aspx
  • @Reed:很有趣。如果我理解正确,它看起来像是有效地保留了一棵树,但提供了经过过滤的“逻辑”视图以及完整的“视觉”视图。
  • @Steven:“逻辑”树基本上就是您在代码中设置的内容。但是,由于所有内容都在 WPF 中进行了模板化,因此“可视化树”或实际呈现的内容可能完全不同。树的层之间可以存在更多的“项目”。

标签: c# .net winforms controls


【解决方案1】:

每个孩子都有一个父母,因此无需担心。

【讨论】:

  • 啊,但是如果启动控件没有父控件怎么办?我已经看到这种情况发生了,顺便说一句
  • 我们正在递归地查看给定控件的子控件。这意味着我们永远不会查看父代,而是使用堆栈来跟踪该沿袭。因此,当第一个控件是无父控件时,它会正常工作。
【解决方案2】:

控件实际上只能有一个父级。实际上没有理由跟踪“已处理”,因为您只会在控件上执行您的方法一次。

现在,如果您使用的框架允许控件有多个父级(我不知道有任何 .NET 框架允许这样做),那么这可能是必需的。但是,如果您使用的是 Windows 窗体(看起来就是这样)或 WPF,您可以将其简化为:

private static void Traverse(this Control control, Action<Control> action)
{
    foreach (Control child in control.Controls)
        Traverse(child, action);
    action(control);
}

【讨论】:

  • 在这种情况下,您将遇到其他问题。很难想象一种安全的方法会在旨在“对控件及其所有子项执行操作”的方法中重新设置控件的父级
  • @Steven:另外,如果你要改变父母身份,你会在 foreach 中遇到问题,即使“处理”到位......
  • 好点。改变父母会有效地改变每个集合的内容,这是一个 foreach 禁忌。我想唯一的选择是通常的解决方法:迭代一次以构建待办事项列表,然后遍历并实施该列表。
【解决方案3】:

你需要使用递归

public sub DoStuffToControlAndChildren(TargetControl as Control)

    'Insert code to do stuff to TargetControl here

     if TargetControl.Controls.count = 0 then
         return
     end if 

     For each ChildControl in TargetControl.Controls
        DoStuffToControlAndChildren(ChildControl)
     next

end sub

【讨论】:

  • 我通过保留堆栈/队列来选择不使用递归。
  • @Daniel:一棵树到底有多深?如果答案是“不太”,那么递归就可以了。
  • 答案“不是很”,但我喜欢堆栈和队列的优雅 (IMO),这既是一种遍历控件的方式,也是一种一般在遍历中进行实验,所以我想保持算法尽可能广泛。有任何理由使用堆栈和队列吗?当您只遍历嵌套数个深度的一百个控件时,开销是否不值得?
  • @Daniel:坦率地说,我认为无论哪种方式都可以。甚至有一些反对在 .NET 中使用递归的普遍论点,基于延迟本地人的收集。在这种情况下,我怀疑递归的性能至少与显式分配堆栈一样好,而且我觉得它更优雅。显然,你不同意后者。 :-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-08-02
  • 1970-01-01
  • 2013-03-19
  • 2021-03-07
  • 1970-01-01
  • 2019-05-01
  • 2022-01-15
相关资源
最近更新 更多