今天收到一名名为“三十而悟”的中专学校的计算机教师同行的邮件:
=====================================================
我最近正在把我掌握的有关Visual Basic的一些教学和开发经验写成一本书,目前写了20多万字了。在写的过程中,发现存在一个很有意思的问题,一直百思不解。特向您请教一下。
问题描述:在Visual Basic 2005的IDE环境下,我使用的IDE是Visual Studio 2005 Team Suite,环境设置选的是“Visual Basic开发设置”。在窗体的两个事件Resize和Load出现的先后顺序中出现的问题。按理说,这两个事件,Load一定是发生于Resize事件之前的。但是,我在编写代码过程中,有一次偶然的机会,发现Resize事件是发生在Load之前,我百思不得其解。在花了一天的时间找原因时,才重现了这种Resize发生于Load之前的情境。我发现,如果在窗体设计阶段,重新调整一下窗体的大小不按默认的300*300的大小设置,则Resize事件必发生在Load事件之前。我虽然重现了这种情况,但我找不出这种情况出现的原因。
当然如果一定要在Resize事件和Load事件中写代码且要区分他们的先后,用一个开关量是可以解决的。只是问题放在心里特不好受,网上也找了很多资料,就是没办法解决。只能向您发送一个请教了,您毕竟是同行中专家。打扰之处,请原谅!
来自一个中专学校的计算机教师同行
敬上,谢谢!
=================================================
我打开Visual Studio,检测了一下,真的如他所说。这也引发了我的兴趣,产生这种现象的原因何在?
查询MSDN未获直接答案,我觉得必须到.NET源代码中去找谜底。
以下是我的追寻之路:
首先,我发现Form类定义了一个DefaultSize属性:
protected override Size DefaultSize
{
get
{
return new Size(300, 300);
}
}
可以看到,它设定的默认属性就是300*300.
然后,我在Form类的基类Control的构造函数中找到以下代码:
internal Control(bool autoInstallSyncContext)
{
……
Size defaultSize = this.DefaultSize;
this.width = defaultSize.Width;
this.height = defaultSize.Height;
……
}
可以看到,在构造函数中窗体的高度和宽度被定义为默认值。
打开Visual studio自动生成的的Form1.Designer.vb,可以看到以下代码:
Private Sub InitializeComponent()
Me.SuspendLayout()
'
'Form1
'
Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 12.0!)
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
Me.ClientSize = New System.Drawing.Size(562, 415)
Me.Name = "Form1"
Me.Text = "Form1"
Me.ResumeLayout(False)
End Sub
这说明窗体的ClientSize属性已经被改为默认值(300,300)之外的数值 。
ClientSize是一个属性,给它赋值时会调用SetClientSizeCore方法。
public Size ClientSize
{
get
{
return new Size(this.clientWidth, this.clientHeight);
}
set
{
this.SetClientSizeCore(value.Width, value.Height);
}
}
SetClientSizeCore方法内部又调用SizeFromClientSize方法设置窗体的Size属性:
protected virtual void SetClientSizeCore(int x, int y)
{
this.Size = this.SizeFromClientSize(x, y);
this.clientWidth = x;
this.clientHeight = y;
this.OnClientSizeChanged(EventArgs.Empty);
}
Size属性内部调用SetBounds方法
public Size Size
{
get
{
return new Size(this.width, this.height);
}
set
{
this.SetBounds(this.x, this.y, value.Width, value.Height, BoundsSpecified.Size);
}
}
SetBounds方法又经过几个方法调用(不再赘述),最终调用UpdateBounds方法。
在Form类的UpdateBounds方法中,可以看到以下代码:
protected void UpdateBounds(int x, int y, int width, int height, int clientWidth, int clientHeight)
{
。。。
bool flag2 = (((this.Width != width) || (this.Height != height)) || (this.clientWidth != clientWidth)) || (this.clientHeight != clientHeight);
this.x = x; this.y = y; this.width = width; this.height = height;
this.clientWidth = clientWidth; this.clientHeight = clientHeight;
。。。
if (flag2) {
this.OnSizeChanged(EventArgs.Empty);
。。。
}
。。。
可以看到,只要尺寸不等于300*300,就会调用OnSizeChanged方法,而此方法负责激发Resize事件。
那么,UpdateBounds方法在什么情况被调用?
使用Reflector继续跟踪下去,可以看到,位于最顶层的并且间接调用它的语句在WmCreate()方法中,而此方法又位于窗体过程WndProc内。
protected virtual void WndProc(ref Message m)
{
if ((this.controlStyle & ControlStyles.EnableNotifyMessage) == ControlStyles.EnableNotifyMessage)
{
this.OnNotifyMessage(m);
}
switch (m.Msg)
{
case 1:
this.WmCreate(ref m);
return;
case 2:
this.WmDestroy(ref m);
return;
。。。
}
。。。
}
在窗体过程WndProc中,响应窗体创建消息(WM_CREATE)时,就会调用WmCreate()方法。WM_CREATE消息在窗体创建时由Windows负责发送给进程。
所以,经过刨根问底的一番寻根之旅,我们弄明白了产生这一现象的根本原因。
感想:
微软平台的技术大都有这样的一个特点:抽象层次较高,使用简易。但封装层次很多,表面的“简单”是由背后的“复杂”所支撑。很多使用微软技术的程序员习惯了“只管用,不管为什么”,其实这种工作与学习方式并不利于技术的进一步提升。
就本文所讨论的问题而言,虽然Resize事件激发顺序问题在实际开发中可能并不十分重要与关键,但这种技术探索精神很可贵。如果学技术、教技术、用技术的人都有这种探索劲头,杜绝浮燥的急功近利的风气,相信国内软件开发者的总体水平绝不会差。