Winforms: 复杂布局改变大小时绘制错误

一、        问题描述

当一个Form非常复杂,里面的控件嵌套层次很深时,我们发现在改变Form大小的时候,处于最内层的控件会绘制错误。当我们设置了相应Layout之后,通常内层的控件在外层控件的大小改变时应该也随之改变。当问题出现时,我们期待的内层控件没有变化。

二、        问题重现

  1. 新建一个Winforms工程;
  2. 在Form上添加一个Button,一个Label和一个Panel;
  3. 在把panel1的Anchor属性设为Top|Bottom|Left|Right;
  4. 在类Form1中添加如下代码:

        public Form1()

        {

            InitializeComponent();

            UpdateLevel();

        }

 

        int level = 1;

        private void UpdateLevel()

        {

            label1.Text = "Level: " + level.ToString();

        }

 

        Panel lastPanel;

        private void button1_Click(object sender, EventArgs e)

        {

            Panel panel = new Panel();

 

            Random random = new Random((int)DateTime.Now.ToBinary());

            panel.BackColor = Color.FromArgb(random.Next(256), random.Next(256), random.Next(256));

 

            panel.Padding = new Padding(5, 5, 5, 5);

            panel.Location = new Point(5, 5);

            panel.Size = new Size(Math.Max(lastPanel.Width - 10, 0), Math.Max(lastPanel.Height - 10, 0));

            panel.Dock = DockStyle.Fill;

 

            lastPanel.Controls.Add(panel);

 

            lastPanel = panel;

 

            level++;

            UpdateLevel();

        }

上述代码主要功能是嵌套添加Panel;

  1. 编译运行;
  2. 反复点击Button,同时改变Form的大小,观察绘制的结果。当嵌套的深度到达一定程度时(我的电脑是28),最内层的Panel绘制出现问题。

三、        原因分析

当我们改变Form的大小的时候,会调用Form的OnLayout方法,在Form的OnLayout的方法里,会调用最外层控件的OnLayout的方法,外层控件的OnLayout函数会调用内层OnLayout方法。因此,Control.OnLayout是一个递归调用的函数。当我们控件嵌套层次太深的时候,这个调用栈会很深。

在OnLayout里,我们会调用Windows一个API:SetWindowPos。通常情况下,我们调用SetWindowPos去改变一个Windows窗口的位置和大小的时候,Windows会给该窗口发送WM_WINDOWPOSCHANGED。Winforms在该消息的处理函数里,会去调整窗口的大小并重新绘制。

当调用栈超过一定限度的时候,我们发现Windows并没有向窗口发送消息WM_WINDOWPOSCHANGED,于是Winforms就不能在其处理函数里图调整窗口的大小并重新绘制。由于消息是由Windows发送出来的,我们站在Winforms的角度不能知道该消息没有发出来的真正原因。

四、        解决办法

由于问题的真正原因是Windows没有发出WM_WINDOWPOSCHANGED消息,因此我们不能从根本上解决这个问题。但我们可以想办法去绕过这个问题。

正如前面分析所提到的,这个问题的出现与调用栈太深有关。如果我们能缩短递归调用栈的深度,也就能绕过这个问题。

因此我们的第一个建议是减少Form上的控件嵌套层次。经过大量实验,我们发现出现这个问题时,在32位操作系统上嵌套层次超过25层,在64位机器上嵌套层次超过15层(不同机器数据略有不同)。当控件的嵌套层次超过15层时,这个Form会非常复杂。因此我们可以考虑简化Form的设计,减少控件的嵌套层次。

如果我们确实需要很深的嵌套层次,我们可以尝试另外一个办法:是用异步调用的办法来减少调用栈的深度。我们在一个函数里异步调用另一个函数时,原函数会马上继续,而不用等待被调用函数返回,因此也就减少了调用栈的深度。

我们可以选择一个控件里面只有少数(最好只有一个)子控件,不设置它子空间的Anchor和Dock属性,而在该控件的Layout事件处理器里自己处理子控件的布局,也就是显式调整子控件的位置和大小。由于我们需要用异步调用的方式去缩短栈的深度,因此我们可以用Control.BeginInvoke来设置Control.Size。下面是一段参考代码:

1. 添加如下代码:

        private delegate void SetControlSizeDelegate(Control control, Size size);

        private void SetControlSize(Control control, Size size)

        {

            control.Size = size;

        }

 

        private void panel_Layout(object sender, LayoutEventArgs e)

        {

            Panel panel = sender as Panel;

            if (panel != null && panel.IsHandleCreated && panel.Controls.Count == 1)

            {

                Control control = panel.Controls[0];

 

                Size size = new Size(Math.Max(panel.Width - control.Padding.Left - control.Padding.Right, 0),

                    Math.Max(panel.Height - control.Padding.Top - control.Padding.Bottom, 0));

 

                SetControlSizeDelegate myDelegate = new SetControlSizeDelegate(SetControlSize);

                this.BeginInvoke(myDelegate, new object[] { control, size });

            }

        }

这段代码的主要功能就是在Layout的事件处理器中用BeginInvoke异步修改子控件的大小。

2. 在Form1的构造函数里添加代码:

panel1.Layout += panel_Layout;

3. 在button1_Click里删除对Panel的Dock设置。我们将在Panel的Layout事件处理器里调整子控件的布局

4. 在button1_Click里添加代码:

panel.Layout += panel_Layout;

posted on 2010-06-19 12:31  rxie  阅读(314)  评论(0编辑  收藏  举报