Delphi中异常的截获及其个性化处理

一、 异常及异常处理机制 
  异常可以理解为一种特殊的事件,当这种特殊的事件发生时,程序正常的执行流程 将被打断。异常处理机制能够确保在发生异常的情况下,应用程序不会中止运行,并且能以一致的风格进行处理。许多刚刚涉及Delphi的程序员感觉异常处理机制高深莫测, 是高级程序员的“专利”,总有尽可能绕开它的想法,因而不得不小心翼翼地检查每一次函数调用的返回值,或者通过额外的代码捕获可能的错误,即使这样,也只能保证程序代码不出错,而无法保证包括操作系统、设备驱动程序、数据库驱动程序、Delphi自身的元件库和运行时间库在内的软硬件设备不出错。问题就在于,通过检查函数的返回值或设置错误陷阱只能捕获可预见的错误,如果出现没有预见到的错误,或者函数调用本身就已失败,程序正常的流程就会被打乱。由此可见,异常几乎是不可避免的。其实,Object Pascal提供了多种异常处理方法,使异常的处理变得轻而易举,可以将你从为你的应用程序执行的每个任务编写单独的错误处理程序的繁琐工作中解放出来。 
二、 处理异常的方法 
  1. Try...Except 结构 
  Try...Except 结构是这样工作的:Try后面到Except之前的语句通常是希望正常执行的代码,在执行过程中如果触发了异常,程序就跳入Except部分。在Except部分做出的处理可以有以下三种类型: 
  (a) 不需要确切知道异常的类型,只需要做笼统的处理时,可以弹出一个警告信息框,或者释放已分配的资源,或者退出程序。程序示例如下: 
  例1 Procedure TForm1.Button1Click(Sender :TObject) 
  Var 
   Num: Integer; 
  Begin 
   Try 
      Num:=StrToInt(Edit1.Text); 
     Edit2.Text:=IntToStr(Num*Num); 
   Except 
     ShowMessage(Edit1.Text+'无法转成整数!'); 
    End; 
  End; 
  该例中,点击Button1后,程序试图将编辑框Edit1中的内容转换为整型数,整数平方之后再转换成字符串类型数据,在编辑框Edit2中显示。如果Edit1.Text是7.89,传递给StrToInt将产生EConvertError异常,因为7.89不是一个有效整数。异常产生后,将显示一条警告信息,并退出该过程。 
  (b) 利用异常处理句柄处理某个特定的异常 
  例如,在例1中可以针对EConvertError异常做出如下的处理: 
  例2 Procedure TForm1.Button1Click(Sender :TObject) 
  Var 
     Num: Integer; 
  Begin 
     Try 
         Num:=StrToInt(Edit1.Text); 
         Edit2.Text:=IntToStr(Num*Num); 
     Except 
         On EConvertError Do 
               ShowMessage(Edit1.Text+'无法转成整数!'); 
     End; 
  End; 
  (c) 利用异常处理句柄处理多种异常 
  通常,执行的代码不只产生一种异常,这就需要在Except部分进行更具体的测试,根据产生的异常类型分别做出处理。现将例2稍加改动如下: 
  例3 Procedure TForm1.Button1Click(Sender :TObject) 
  Var 
     Num: Integer; 
  Begin 
     Try 
         Num:=StrToInt(Edit1.Text); 
         Edit2.Text:=IntToStr(30 div Num); 
     Except 
         On EConvertError Do 
               ShowMessage(Edit1.Text+'无法转成整数!'); 
         On EDivByZero Do 
         Begin 
              ShowMessage('除数不能为零!'); 
              Edit2.Text:='0'; 
         End; 
     End; 
  End; 
  在例3中,Try部分的第一条语句可能产生EConvertError 异常;当 Num为零时,Try 部分的第二条语句还会产生被零除的异常(即产生EDivByZero 异常)。由于产生异常原因不同,需要分别进行不同的处理。例3中的Except 部分的代码也可以写成以下的形式,这两种异常处理的结果是一样的。 
  Except 
   On E:Exception Do 
     Begin 
         If E is EconvertErro Then 
               ShowMessage(Edit1.Text+'无法转成整数!'); 
         If E is EdivByZero Then 
         Begin 
             ShowMessage('除数不能为零!'); 
             Edit2.Text:='0'; 
         End; 
  End; 
  2. Try...Finally 结构 
  Try...Finally 结构最大的用处是在异常发生的情况下,确保释放应用程序已经分配的资源,或者完成一些必须的操作,比如:剪贴板Clipboard 在打开之后必须调用Close 方法将剪贴板关闭;数据感知组件更新禁止之后必须调用EnableControls方法才能使更新显示有效等。Try...Finally 结构之所以能做到这一点,是因为不管异常是否发生,程序都要执行Finally 部分。请看下面的例子: 
  例4 Procedure TForm1.Button1Click(Sender :TObject) 
  Var 
     Icon : TIcon; 
  Begin 
     Try 
         Icon:=TIcon.Create; 
         Icon.LoadFromFile('Spin.ico'); 
         ImageList1.ReplaceIcon(0,Icon); 
     Finally 
         Icon.Free; 
     End; 
  End; 
  在例4中,Try 部分代码是从磁盘获取一个图标,用来代替ImageList1 中的0号图标。如果图标文件Spin.ico 不存在或者被损坏,将会产生EFOpenError 异常,中断 Try部分代码的执行,但是程序会执行Finally 部分的代码,使得Icon 占用的资源得以释放。当然,如果没有发生任何异常,Finally 部分也将被执行。可以说,Finally 部分是Try...Finally 结构的“必经之路”。 
  3. Try...Except 与Try...Finally 的嵌套结构 
  Try...Finally 结构确实有它独到的功能,但是Try...Finally结构中没有 Try...Except 结构中的异常处理句柄,也就无法知道当前确切的异常类型,并且不论异常是否发生,程序都执行 Finally 部分,这就决定了 Try...Finally 结构无法对发生的异常进行特别的处理。如果在应用程序中,既需要完成一些必要的操作,又要对发生的异常进行处理,那么最好使用这两种结构的嵌套。请看下例: 
  例5 Procedure TForm1.Button1Click(Sender :TObject) 
  var 
     Icon : TIcon; 
  Begin 
     Try 
         Try 
              Icon:=TIcon.Create; 
              Icon.LoadFromFile('Spin.ico'); 
              ImageList1.ReplaceIcon(0,Icon); 
          Finally 
              Icon.Free; 
          End; 
     Except 
         On EFOpenError Do 
               ShowMessage('无法打开图标文件Spin.ico'); 
     End; 
  End; 
  在例5中,无论异常发生与否,都将执行Finally 部分释放图标占用的资源。一旦异常发生,则产生警告信息。上例是在 Try...Except 结构的 Try 部分嵌套 Try...Finally 结构,也可以在 Try...Finally 结构的 Try 部分嵌套 Try...Except 结构,前者是先执行必须的操作,再进行异常的处理,而后者是先进行异常的处理,再执行必须的操作。在多数情况下,两者的区别并不大。除此之外,Try...Except 结构和Try...Finally 结构还可以各自嵌套。在复杂的应用程序中,Except 部分或Finally部分的异常处理代码本身又有可能触发异常,这就需要采用嵌套结构编写更深一层的异常处理代码,这里就不再详细举例了。 
  4. 定义自己的异常类 
  我们的探讨将进入更深的一层——定义自己的异常类。虽然在简单的应用程序中,程序员并无太大必要定义自己的异常类,但是它对编写个性化的异常处理程序仍然是有帮助的。例7中的过程是对数据集 Table1 中的字段Name进行合法性检查,如果Name 没有值或为空字符串,则产生异常,这其中就定义了一个 Exception 的派生类EMyException。 
  例6 
  Type EMyException=Class(Exception); {定义自己的异常类EMyException} 
  Procedure TForm1.Table1NameValidate(Sender : TField); 
  Begin 
     If ( Sender.IsNull ) or ( Sender.AsString='') Then 
       Raise EMyException.Create('必须填写名称'); 
  End; 
  三、个性化的异常处理句柄 
  Delphi会自动处理大部分的异常,但全英文信息提示会让应用程序的最终用户感觉并不友好。那么如何编写自己的异常处理代码呢? 在Forms单元中声明的TApplication类中的OnException 事件为编写自己的异常处理代码提供了可能。事实上,用于OnException 事件的事件句柄就是应用程序默认的异常处理句柄。请看下面的例子。 
  在 Project Resource 中,首先创建一个新类 TMyClass, TMyClass 中有一个MyExceptionHandler 方法,可以根据应用程序的需要来编写,这里体现了个性化的异常处理。由MyExceptionHandler事件来响应Application 的 OnException事件,这样在程序发生异常时,首先调用的就是自己编写的异常处理事件MyExceptionHandler事件。工程文件如下: 
  例7 
  program ExceptPrj; 
  uses Forms, Windows, SysUtils, Classes, Dialogs, DB, DBTables, 
      Example in 'Example.pas' {Form1}; 
  {创建一个TObject的派生类TMyClass} 
  Type 
   TMyClass=Class(TObject) 
   Public 
      Procedure MyExceptionHandler(Sender: TObject; EInstance: Exception); 
   end; 
  {编写自己的异常处理句柄} 
  procedure TMyClass.MyExceptionHandler(Sender: TObject; EInstance: Exception); 
  var 
      ErrorFile:TextFile; 
      FileName,Content:string; 
        FindFlag:Boolean; 
  Begin 
     {截获出现的新的异常,并存入文件ErrorInfo.txt.} 
      FileName:=ApplicationPath+'ErrorInfo.txt'; { ApplicationPath 是在主Form中定义的全局变量,记录应用程序所在的目录} 
     {打开文件} 
      AssignFile(ErrorFile,FileName); 
      if not FileExists(FileName) then 
          ReWrite(ErrorFile) 
      else 
          ReSet(ErrorFile); 
         {检查出现的异常是否曾经记录在文件ErrorInfo.txt中} 
      FindFlag:=False; 
      While not SeekEof(ErrorFile) do 
      begin 
          Readln(ErrorFile,Content); 
          if Pos(EInstance.ClassName+':'+EInstance.Message,Content)<>0 then 
          begin 
              FindFlag:=True; 
              break; 
          end; 
      end; 
      {如果是一个未被记录过的异常,则将它记录在文件中} 
      if not FindFlag then 
      begin 
          ShowMessage('出现新的错误!'); 
          Append(ErrorFile); 
          Writeln(ErrorFile,EInstance.ClassName+':'+EInstance.Message); 
      end; 
      {关闭文件} 
      CloseFile(ErrorFile); 
     {对出现的异常显示中文提示} 
      If EInstance is EDivByZero then 
          Application.MessageBox('除数不能为零!','错误',MB_OK+MB_ICONSTOP) 
      else if EInstance is EAccessViolation then 
          Application.MessageBox('访问了无效的内存区域!','错误',MB_OK+MB_ICONSTOP) 
      else if (EInstance is EDatabaseError) then 
          Application.MessageBox('数据库操作出现错误!','错误',MB_OK+MB_ICONSTOP) 
      else if (EInstance is EFOpenError) then 
          Application.MessageBox('文件不能打开!','错误',MB_OK+MB_ICONSTOP) 
      else if (EInstance is EConvertError) then 
          Application.MessageBox('非法的类型转换!','错误',MB_OK+MB_ICONSTOP) 
      else 
          MessageDlg(EInstance.ClassName+':'+EInstance.Message,mtInformation,[mbOK],0) 
  end; 
  {$R *.RES} 
  var 
      MyObject: TMyClass; {声明TMyClass类的一个变量} 
  begin 
      Application.Initialize; 
      Application.CreateForm(TForm1, Form1); 
      ApplicationPath:=ExtractFilePath(Application.ExeName); 
      MyObject:=TMyClass.Create; {创建TMyClass类的一个实例} 
      Application.OnException:=MyObject.MyExceptionHandler; {响应OnException事件} 
      Application.Run; 
  end. 
  例7中的MyExceptionHandler 事件是我们在开发铁路车站行车工作细则管理系统时编制的,由于篇幅所限,在此仅将这一事件的整体框架呈现给大家,希望通过这个例子来说明开发异常处理句柄的方法。