敬业的IT人 >> 编程开发 >> C++Builder >> 窗体事件驱动链引发程序错误分析(一)

窗体事件驱动链引发程序错误分析(一)

敬业的IT人 互联网 佚名 2008-1-3 21:46:44

  BCB中采用的类库是VCL,其编程框架是事件驱动的,类似于VB。我在开发过程中发现,如果不对BCB的事件驱动链进行分析,写的程序会带有很多的错误,健壮性很成问题,而且调试很麻烦。

  我发现程序中的很多错误都来源于C++的指针操作。在程序中用new的方法创建了一个对象,然后delete这个对象之后,如果此时还有其它指针指向这个对象,访问此对象信息的代码必定会引发异常。这在C++中是常识性的问题。但这个问题在BCB这类事件驱动的开发环境中就复杂化了。由于事件模型其实是对Windows消息循环机制的一个封装,而Windows中一个消息可能会引发一连串的其他消息,所以,事件之间也是相互引发的,形成一个事件驱动的链条。 在BCB这类RAD的开发环境中,窗体(Form)是最核心的组件,窗体的事件模型也是程序中最需要分析的。

  我在BCB5中设计了几种典型情况,针对BCB提供的事件模型,绘出了BCB中的事件驱动链。由于WM_PAINT消息的反复激发,所以我有意识地屏蔽掉了对这一消息的响应事件OnPaint。我发现大多数错误就发生成窗体的生成与销毁过程中,其主要的原因就是程序员选错了事件,将代码放错了地方。所以,我重点分析了工程中窗体的创建和销毁过程中的事件驱动链。

  先对BCB的工程文件(BPR)进行分析:

WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TForm1), &Form1);
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}


  WinMain是BCB工程的入口点,Initialize()一句完成程序的初始化工作,CreateForm()一句完成窗体的创建工作(在此引发一系列的事件),Run()一句则进入了消息循环,此时事件主要是用户操作引发的(当然也有定时器等操作系统引发的事件)。

  Catch()一句保证应用程序不会引起操作系统的崩溃。但这也是无数的BCB和Delphi程序会引发“XXX内存读写错误”信息的根源所在。正是为了提高BCB程序的健壮性,我才进行了这个实验,其中比较枯燥,不想看记录的人可以直接去看结论部分。

  实验记录:

  一、由BCB自动创建的Form的事件驱动链

  实验一. 工程中只有一个主窗体Form1

  当窗体中只有一个主窗体(Form1)时,程序运行事件的发生次序如下:

  创建
  Form1构造函数àOnCreate() //到此窗体创建完毕,以下进入窗体的显示过程

  àOnShow()àOnActivate()àOnCanResize()àOnConstrainedResize()àOnResize()

  窗体显示完毕,以下进入消息循环,等待用户输入.

  关闭
  关闭主窗体Form1时:

  OnCloseQuery()àOnClose() //到此退出消息循环

  àOnHide()àOnDestory()à~Form1()析构函数

  显然,一个窗体要先隐藏,再销毁,最后再调用析构函数

  另外:OnShow()à……àOnResize()这一事件链在各种情况中始终不变,下面的记录中我就以……代表这些不变的事件链。

  实验二:工程中有两个窗体,Form1为主窗体,Form2为普通窗体
  当工程中有两个窗体,Form1为主窗体,Form2为普通窗体,事件链如下:

  创建
  Form1构造函数à OnCreate() //到此窗体Form1创建完毕,开始创建Form2

  à Form2构造函数

  //以下进入Form1的显示过程

  àOnShow()à……àOnResize()

  除非在代码中指定,缺省情况下Form2只处于加载状态,不会显示,Form2中的OnCreate()等事件代码不会运行。

  关闭
  关闭时,Form2处于隐藏或显示状态均一样:

  Form1的OnCloseQuery()àOnClose() //到此退出工程消息循环,以下销毁Form2

  àForm2析构函数àForm1的 OnHide()àForm1的OnDestory()à~Form1()析构函数

  值得注意的是,这时,Form2的OnDestory()没有被执行,对应地,当它创建时,Form2的OnCreate()也没有执行!

  实验三:工程中有两个窗体,Form1为主窗体,Form2继承自Form1
  在BCB中,可以很方便地继承某个窗体,这是BCB的一个非常好用的功能。

  创建
  Form1构造函数à OnCreate() //到此窗体Form1创建完毕,开始创建Form2

  àForm2的OnCreate()àForm1的构造函数àForm2的构造函数 //以下进入Form1的显示过程,同单窗体工程一样àOnShow()à……àOnResize()

  值得注意的是:Form2的OnCreate()函数居然先于Form2的构造函数运行!

  关闭
  1.Form2为隐藏状态,

  Form1的OnCloseQuery()àOnClose() //到此退出工程消息循环,以下销毁Form2

  àForm2析构函数àForm1的析构函数àForm2的OnDestory()àForm1的 OnHide()àForm1的OnDestory()àForm1的析构函数

  注意:Form1的析构函数被执行了两次!

  2.Form2为显示状态

  Form1的OnCloseQuery()àOnClose() //到此退出工程消息循环,以下销毁Form2

  àForm2的OnHide()àForm2的析构函数à Form1的析构函数à Form2的OnDestory()àForm1的 OnHide()àForm1的OnDestory()àForm1的析构函数

  注意:Form1的析构函数被执行了两次!

  两种情况对比,可以发现只多了一个Form2的隐藏过程.

    ·中国第一芯破灭 方舟事件调查
    ·Java Swing中的键盘事件处理
    ·c#委托与事件心得
    ·偷窥门事件
    ·解析VB的事件驱动编程
    ·浮动菜单是如何作出来的mouse事件
    ·用Java事件处理机制实现录制回放功能
    ·Java Swing中使用双击事件
    ·历史十大黑客事件:不堪一击的系统
    ·C#事件及响应方法

  二、由程序员用new的方法创建的窗体事件驱动链

  在程序中无数次地见到过以下代码:



 

TfrmConstant *pForm=new TfrmConstant(this); //创建一个窗体

pForm->Show();

……

delete pForm;
  我们经常需要动态创建一个Form,再显示它,然后在合适的地方销毁它。

  请注意上面代码中的this参数,它被传送给TfrmConstant窗体的构造函数,这一构造函数有一个唯一的形参——Owner。这个参数决定了由谁负责销毁它。

  在BCB的帮助文件中对Owner参数的描述如下:

  Owner描述了谁负责销毁这个组件。

  使用Owner参数来存取component所有者的接口。当一个组件拥有另一个组件时,当父组件内存被释放时,子组件所占的内存资源也被释放。这就是说,当一个窗体被销毁时,窗体上的所有组件也会被删除。

  缺省情况下,一个窗体拥有放在其上的所有组件,同样地,Application对象则拥有所有的Form对象。这样,当程序结束时,Application对象会释放所有Form占用的资源。

  下面是我对Owner参数的实验记录

  实验一:主窗体(Form1)new一个普通窗体(Form2),Owner=this
  Form1中的代码示例:

  p2=new TForm2(this);

  p2->Show();


  创建:
  先创建完主窗体后,只调用普通窗体Form2的构造函数,不管显不显示,事件驱动链与以前的实验一样。

  关闭:
  主窗体:OnCloseQueryàOnCloseàOnHide()àOnDestory()à主窗体析构函数àForm2析构函数

  关闭次序是先主窗体再普通窗体,如果创建了多个普通窗体,根据创建的先后顺序按后创建先销毁的原则逐个销毁窗体:

  普通窗体是显示还是隐藏对事件驱动链无影响

  实验二:主窗体(Form1) new一个普通窗体Form2,Owner=Application
  创建:

  同实验一

  关闭:
  首先是Form1:OnCloseQuery()àOnClose()àForm2析构函数(如果有多个,按照后进先出的次序调用) àForm1:OnHide()àOnDestory()àForm1析构函数.

  实验三:主窗体(Form1) new一个继承窗体(Form2继承自Form1),Owner=this
  创建:
  创建主窗体Form1,事件链同单窗体的创建;

  接着在代码中开始new Form2:

  Form2 OnCreate()àForm1的构造函数àForm2 的构造函数à创建结束

  我们看到要先调用父类的构造函数

  关闭:
  1.Form2未显示:

  Form1: OnCloseQuery()àOnClose()àOnHid()eàOnDestory()àForm1的析构函数

  àForm2的析构函数à Form1的析构函数à Form2 OnDestory()

  注意:并无Form1的OnDestory,同样地:Form1的析构函数被执行了两次

  2.Form2显示情况下::

  Form1: OnCloseQuery()àOnClose()àOnHide()àOnDestory()àForm1的析构函数

  Form2 OnHide()à Form2的析构函数à Form1的析构函数à Form2 OnDestory

  仅多了一个Form2 OnHide()过程

  实验四:主窗体new一个继承窗体(Form2继承自Form1),Owner=Application:
  创建(Form2不显示情况):
  Form2 OnCreate()àForm1的构造函数à Form2的构造函数

  关闭(Form2不显示情况)
  Form1 OnCloseQuery()àOnClose()àForm2的析构函数à Form1的析构函数à Form2 OnDestory()àMainForm HideàMainForm的析构函数à MainForm 的析构函数

  创建(Form2显示情况):
  Form2 OnCreate()àForm1的构造函数à Form2的构造函数à Form2 OnShow()à ……à Form2:OnResize()

  多了一个显示的事件

  关闭(Form2显示情况)
  Form1: OnCloseQuery()àOnClose()à Form2 :OnHide()àForm2的析构函数à Form1的析构函数à Form2 OnDestory()

  àForm1 OnHide()àForm1 OnDestoryà MainForm1析构函数

  三、MDI型应用程序事件发生次序

  主窗体为MainForm,子窗体为MDIChild

  创建
  MainForm构造函数àOnCreate()à……à OnResize()

  新建一个MDI子窗体时,Owner=Application或MainForm

  MDIChild 构造函数àOnCreateàshowàActivate

  关闭
  Owner=Application时:
  MDIChild OnCloseQuery()(有几个子窗体就执行几次)àMainForm OnCloseQuery()àMainForm OnClose()à MDIChild OnDestory()à MDIChild 析构函数àMainForm OnDestory()àMainForm析构函数

  Owner=MainForm时:
  MDIChild CloseQuery()(有几个子窗体就执行几次)àMainForm OnCloseQuery()àMainForm OnClose()àMainForm OnDestory()à MainForm析构函数à MDIChild OnDestory()à MDIChild 析构函数

  显然,Owner参数决定了主窗体与MDIChild的析构次序。
  有用的结论

  从实验中我们可以得出一些有用的结论:

  关于OnCreate和OnDestory事件:

  1. Form的OnCreate事件是不可靠的,并不像名字所说的当窗体一创建时就执行,而是当窗体是工程的主窗体或是一个继承其它窗体的子窗体时才执行.所以,在OnCreate()中初始化变量和创建对象是不合适的;

  2. 在有继承情况下,OnCreate事件先于构造函数发生,对应地OnDestroy事件后于析构函数发生。

  关于窗体的构造函数和析构函数

  1. 对于任何窗体,构造函数和析构函数是可靠的,因为它每一次创建和销毁窗体时都会执行。所以我们应在构造函数中进行程序的初始化操作,在析构函数中清理资源;而不要在OnCreate和OnDestory事件中进行处理;

  2. 在继承情况下BCB子窗体对象的构造过程是先构造父窗体,再构造子窗体,这点与C++完全一样,销毁过程则反之;

  3. 在有继承的情况下,父类窗体的构造函数和析构函数则会被执行多次,所以,在这两个地方不要new和delete对象,而应将这类代码放到具体的子窗体类中。如果不这样做,可能会由于多次删除同一对象而引发错误,除非将父类窗体的对象指针声明为static的,保证所有子类共享此new的唯一对象!

  4.析构函数总是最后调用的,而构造函数总是第一个调用。

  关于Owner参数:

  当Owner=Application时,先销毁new出来的窗体,再销毁主窗体;

  而当Owner=this(指主窗体)时,先销毁主窗体,再销毁子窗体;

  这个原则对于在窗体内部动态地生成VCL控件时也适用。特别地,当程序中用到了数据模块,或需要动态地生成DataSet等对象时更要注意。因为一个DataSet可能会向多个窗体提供数据(应该避免这样做,合适的方法是new TDataSet时Owner参数不设为Application)。

  其它:

  1.一个Visible=true的窗体一定要先Hide,才能被销毁。

  2.程序事件链的源头为工程中的主窗体的构造函数

  3.在有继承情况下,子类窗体实例一定要先Hide才可以被销毁。

  4.多窗体的管理BCB采用了栈的方法,按创建的次序,先创建的后销毁,千万要避免先创建的窗体含有对后创建的窗体(或其上的控件)的指针。

  5.关闭时一定会发生OnCloseQuery事件,如果是MDI,此事件还会在MDIChild窗体中传播,所以,在这个地方提示用户改变是合适的。

  6.有些事件一发生,则定然会跟着一系列的事件,这些事件发生的次序是不变的,比如,只要OnShow事件发生,则一定会接着发生以下事件:

àOnActivate()àOnCanResize()àOnConstrainedResize()àOnResize()
OnCloseQuery之后一定是OnClose()
粤ICP备06119539号
Copyright CiscoSky.Org,Some Rights Reserved.
Email:me1228#tom.com