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

一、 问题描述

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

二、 问题重现

1. 新建一个Winforms工程;

2. Form上添加一个Button,一个Label和一个Panel

3. 在把panel1Anchor属性设为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

5. 编译运行;

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

三、 原因分析

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

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

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

四、 解决办法

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

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

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

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

我们可以选择一个控件里面只有少数(最好只有一个)子控件,不设置它子空间的AnchorDock属性,而在该控件的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里删除对PanelDock设置。我们将在PanelLayout事件处理器里调整子控件的布局

4. button1_Click里添加代码:

panel.Layout += panel_Layout;

你可能感兴趣的:(windows)