Delphi虽然为我们提供极其强大的调试功能,查找Bug仍然是一项艰巨的工作,通常我们写代码和调试代码的所消耗的时间是大致相同的,甚至有可能更多。为了减少无谓的时间和精力的浪费,有时我们还是需要专业调试工具的帮助来提高锁定Bug的效率。本文中我们将介绍著名的调试工具CodeSite Pro 2.0(它获得了2000年度Delphi Informant读者选择的最佳调试工具奖的第二名)。它的官方网址是www.raize.com。
CodeSite的主要功能是可以让开发者使用代码来发送运行时的详细信息到特殊的接收器,以便于进一步分析。更精确的说通过CodeSite实现的TCodeSite类,我们可以打包并发送运行时的信息给CodeSite Dispatcher(CodeSite的消息分发器),它可以路由这些消息到一个或多个接收器来察看。缺省的信息接收器是CodeSite Viewer(消息察看器)。
CodeSite的效率体现在它不同于简单的显示消息的对话框或设定断点来检查变量,它更类似于Delphi自带的事件日志功能(Event Log),当然毫无疑问它要比Event Log强大的多,它的消息是可持续的,也就是可以保存的,便于回溯分析。
在介绍CodeSite的具体使用前,我们先来看一下它的三个组成部分。
CodeSite对象
正如前面提到的,从运行的应用程序中向外发送CodeSite消息是通过使用TCodeSite类(定义在CSIntf单元中)的一个实例来完成的,我们只要简单的调用TCodeSite类的方法就可以 把消息发送给CodeSite Dispatcher。比如,可以使用对象的SendMsg方法来发送一个简单的字符串消息。TCodeSite 对象实现了大量的方法来支持各种类型的信息发送而无须任何数据转换,比如对象的SendObject方法有两个参数:一个是消息字符串,一个是对对象实例的引用,这个方法会获取对象所有published的属性,然后把这些属性的信息打包进CodeSite的消息中。
CodeSite Dispatcher
大多数情况下,CodeSite Dispatcher会安静的运行在系统的托盘区。它的唯一功能是路由从各个TCodeSite对象发来的CodeSite的消息到它们的目的地。缺省时,CodeSite消息都会发给CodeSite Viewer。我们甚至不需要启动CodeSite Dispatcher,因为它会被TCodeSite等对象自动启动。
TCodeSite 类定义了一个DestinationDetails属性,它允许开发者设定发送的CodeSite消息是如何被CodeSite Dispatcher路由到不同目的地,比如日志文件。但通常没有必要修改这个属性。
CodeSite Viewer
虽然CodeSite 支持发送消息到不同的目标,但决大多数情况下CodeSite Viewer是主要的发送目标。即使是发送到其他目标,比如日志文件或另外一台机器,CodeSite Viewer仍然是察看分析消息的主要工具。
CodeSite Viewer由下面四个面板构成:消息列表,消息察看器,调用堆栈和Scratch面板。CodeSite Viewer的主要工作区是Message列表,它用来显示发送给Viewer的全部消息或是从日志文件中加载的消息。
消息察看器用来察看同消息关联的额外信息。比如如果当前的消息是由SendObject方法发送的话,消息察看器就会显示对象全部的publised属性当前值。
调用堆栈面板会根据csmEnterMethod消息显示一个堆栈视图。
Scratch面板则是用来显示非可持续的信息的。当我们想跟踪某些信息,但又不想在消息日志中记录它们的时候,比如当我们想察看象鼠标当前位置这类大量的并重复的消息时,Scratch面板是非常有用的。这时我们可以可以使用TCodeSite对象的WritePoint方法,并指定Line ID参数以便指定用来容纳鼠标信息的scratch面板行数。
下面就让我们用一个简单的例子来演示一下如何从程序中发送消息给CodeSite Viewer:
(1)创建一个新的项目,然后切换组件面板到CodeSite页面(CodeSite安装后会在系统中安装两个组件TCSGlobalObject和TCSObject)。选择TCSGlobalObject组件然后放到窗体上。TCSGlobalObject组件提供了设计时对全局TCodeSite对象的交互(全局TCodeSite是在CSInft单元中被初始化的)。
(2)添加一个按钮,然后在它的OnClick事件中写下如下代码:
//CodeSite就是全局的TCodeSite对象
CodeSite.SendMsg(‘CodeSite的第一条消息’);
(3)编译并运行这个简单的程序。运行后点击按钮,CodeSite Dispatcher和CodeSite Viewer将会运行。同时在CodeSite Viewer的消息列表中将会看到程序发出的消息(注意:我们没有必要在程序运行前启动CodeSite Dispatcher和CodeSite Viewer,因为TCodeSite 对象在需要发送消息的时候会自动启动它们的)。运行结果如下图4.38所示:
图4.38 |
(4)接下来,停止程序,在OnClick事件处理过程中添加下面代码:
CodeSite.SendObject('Form1', Form1 );
(5)重新编译运行程序,再点按钮一次,这回你会在CodeSite Viewer中看到两条消息。其中Form1对应的消息包括Form1的对象信息。
(6)为了看到Form1的相关联的对象信息,选择CodeSite Viewer的菜单命令View|Inspector会在消息列表右侧显示一个新的面板,Form1的published属性都被显示在其中,如下图4.39所示:
图4.39 |
(7)再次停止程序,然后修改OnClick过程中代码如下:
CodeSite.EnterMethod('Button1Click');
CodeSite.SendMsg('CodeSite的第一条消息');
CodeSite.SendObject('Form1', Form1 );
CodeSite.ExitMethod('Button1Click' );
(8)这次我们再运行程序点击按钮后,就会看到“CodeSite的第一条消息”和“Form1”的消息被缩进在“Button1Click”消息之间,如下图4.40所示:
图4.40 |
通过添加EnterMethod和ExitMethod方法的调用,我们可以生成一个日志来记录方法何时被调用。
看过例子之后,我们就会发现CodeSite的功能是非常强大的,我们只要简单的在程序中添加几条语句就可以生成非常详细的信息,并通过CodeSite Viewer以生动的图表表现出来。接下来,我们再来谈谈CodeSite的高级应用技术。
发送消息到日志文件
每个程序或多或少都会有Bug,不在这时发生,也会在那时发生,短时间内不发生,很长时间就可能发作,有时反复出现,有时非常偶然的才能被发现。如果一个人告诉你他写的程序在任何时候都没有任何问题,他一定是在撒谎。正是由于Bug的偶然性和隐蔽性,就使得我们往往很难重复用户提交的Bug,这就给我们调试程序并找到问题的原因产生了极大的障碍,而CodeSite能够发送消息到日志文件的特性就使得用户报告Bug变得更容易,他们只要把运行时生成的信息文件提交就可以了。相应的我们调试程序的工作也会变得更轻松,我们可以使用CodeSite Viewer来直观的分析错误发生的原因和位置。
要想改变消息发送的目标,我们可以通过设定TCodeSite 对象的DestinationDetails属性来实现。这项功能要求客户的机器上必须安装了CodeSite Dispatcher,它属于CodeSite中可自由分发的部分。下面的要讲具体过程仍然是基于前面讲过的例子:
(1)在窗体的OnCreate事件中添加下面代码:
CodeSite.DestinationDetails := 'File[Path=C:/FirstLog.csl]';
(2)编译并余兴程序,这回我们在点击按钮后,消息就不再被发送给CodeSite Viewer而是发送到C盘的FirstLog.csl文件中。
(3)使用CodeSite Viewer加载FirstLog.csl文件,这回我们就象先前一样察看被保存的CodeSite消息了。
(4)如果我们想把消息同时发送到CodeSite Viewer和日志文件的话,只修改前面的代码为:
CodeSite.DestinationDetails := 'Viewer,File[Path=C:/FirstLog.csl]';
发送用户定制的数据
虽然TCodeSite 类提供了大量的处理不同数据类型的方法,但有时我们可能会需要发送某种自定义格式的数据信息。为此,TCodeSite 类定义了SendCustomData 方法,它支持发送任意的数据类型,并会根据一个自定义的格式器来格式化数据以便CodeSite Viewer可以正确的显示数据。
首先我们需要创建一个TCSFormatter 对象的子类,然后重载对象的FormatData,InspectorType和TypeName方法。然后调用CodeSite对象管理器对象CSObjectManager的来注册新的TCSFormatter子类。此外,我们还需要调用RegisterCustomFormat方法来注册一个新的消息类型。
下面是一个实际应用的例子,单元CSEmployee.pas中实现了一个TCSEmployeeRecord记录类型的定制格式器:
unit CSEmployee;
interface
uses
Windows, Graphics, CSIntf;
const
csmEmployeeSummary = csmUser + 1;
csmEmployeeDetails = csmUser + 2;
首先在Uses部分添加对CSIntf 单元的引用。第二步是为每一个格式器定义新的CodeSite消息类型常数,上面我们定义了两个常数,注意常数应该大于csmUser,但不能大过32,000。
type
TCSEmployee = record
LastName: string;
FirstName: string;
Address: string;
City: string;
State: string;
ZipCode: string;
PhoneNumber: string;
HireDate: TDateTime;
Salary: Currency;
VacationDays: Integer;
SickDays: Integer;
Manager: Boolean;
end;
上面的记录就是我们要发送的自定义的数据类型。
TCSEmployeeSummaryFormatter = class( TCSFormatter )
public
function InspectorType: TCSInspectorType; override;
procedure FormatData( var Data ); override;
function TypeName: string; override;
end;
TCSEmployeeDetailsFormatter = class( TCSFormatter )
public
function InspectorType: TCSInspectorType; override;
procedure FormatData( var Data ); override;
function TypeName: string; override;
end;
上面是两个定制的格式器类的定义。第一个格式器将把TCSEmployee 记录格式化为一个文本格式,第二个格式化器将把TCSEmployee 记录格式化为网格样式。
implementation
uses
SysUtils;
{=========================================}
{== TCSEmployeeSummaryFormatter Methods ==}
{=========================================}
function TCSEmployeeSummaryFormatter.InspectorType: TCSInspectorType;
begin
Result := itStockStringList;
end;
实现一个自定义的格式化器的第一步是确定哪种类型的内置察看器将被用来察看格式化后的数据,这里使用的是字符串列表察看器。察看器类型将被FormatData方法所使用。
procedure TCSEmployeeSummaryFormatter.FormatData( var Data );
var
EmpRec: TCSEmployee;
begin
EmpRec := TCSEmployee( Data );
AddLine( EmpRec.FirstName + ' ' + EmpRec.LastName );
AddLine( EmpRec.Address );
AddLine( EmpRec.City + ', ' + EmpRec.State + ' ' + EmpRec.ZipCode );
AddLine( '' );
AddLine( 'Phone: ' + EmpRec.PhoneNumber );
AddLine( 'Hire Date: ' + DateToStr( EmpRec.HireDate ) );
AddLine( 'Salary: ' + Format( '%m', [ EmpRec.Salary ] ) );
AddLine( '' );
AddLine( 'Vacation Days: ' + IntToStr( EmpRec.VacationDays ) );
AddLine( 'Sick Days: ' + IntToStr( EmpRec.SickDays ) );
if EmpRec.Manager then
AddLine( 'Manager: Yes' )
else
AddLine( 'Manager: No' );
end;
FormatData 方法是核心部分,注意传递给FormatData方法的Data参数是一个无类型的可变参数。这就意味着这个参数可以是任何数据类型的,通过格式注册过程,我们可以确保强制类型映射为自定义的数据记录,而不会发生转换错误。
转换数据类型后,我们就可以对数据进行格式化了,这里使用TCSFormatter 基类的 AddLine方法在字符串间添加分割线来进行格式化。
function TCSEmployeeSummaryFormatter.TypeName: string;
begin
Result := 'TCSEmployee';
end;
TypeName方法的重载是可任选的,但通常我们可以用它来返回显示在消息列表中的字符串。
{=========================================}
{== TCSEmployeeDetailsFormatter Methods ==}
{=========================================}
function TCSEmployeeDetailsFormatter.InspectorType: TCSInspectorType;
begin
Result := itStockGrid;
end;
对于employeedetails格式器来说,命名网格察看器将被用来察看数据信息:
procedure TCSEmployeeDetailsFormatter.FormatData( var Data );
var
EmpRec: TCSEmployee;
begin
EmpRec := TCSEmployee( Data );
AddNameValuePair( 'LastName', EmpRec.LastName );
AddNameValuePair( 'FirstName', EmpRec.FirstName );
AddNameValuePair( 'Address', EmpRec.Address );
AddNameValuePair( 'City', EmpRec.City );
AddNameValuePair( 'State', EmpRec.State );
AddNameValuePair( 'ZipCode', EmpRec.ZipCode );
AddNameValuePair( 'PhoneNumber', EmpRec.PhoneNumber );
AddNameValuePair( 'HireDate', EmpRec.HireDate );
AddNameValuePair( 'Salary', Format( '%m', [ EmpRec.Salary ] ) );
AddNameValuePair( 'VacationDays', EmpRec.VacationDays );
AddNameValuePair( 'SickDays', EmpRec.SickDays );
AddNameValuePair( 'Manager', EmpRec.Manager );
end;
这里为了在网格察看器中格式化数据,我们使用AddNameValuePair方法来实现。
function TCSEmployeeDetailsFormatter.TypeName: string;
begin
Result := 'TCSEmployee';
end;
下面两个过程是用来封装对SendCustomData方法的调用的,这里对全局的TCodeSite对象实例CodeSite进行了调用:
{=====================}
{== Support Methods ==}
{=====================}
procedure CSSendEmployeeSummary( const Msg: string; EmpRec: TCSEmployee );
begin
CodeSite.SendCustomData( csmEmployeeSummary, Msg, EmpRec );
end;
procedure CSSendEmployeeDetails( const Msg: string; EmpRec: TCSEmployee );
begin
CodeSite.SendCustomData( csmEmployeeDetails, Msg, EmpRec );
end;
最后,不要忘了调用CSObjectManager.RegisterCustomFormatter方法把格式器注册到CodeSite对象管理器中。
initialization
CSObjectManager.RegisterCustomFormatter( csmEmployeeSummary,
TCSEmployeeSummaryFormatter );
CSObjectManager.RegisterCustomFormatter( csmEmployeeDetails,
TCSEmployeeDetailsFormatter );
end.
除了上面谈到的特性外CodeSite还支持远程调试,也就是可以把消息路由到一台远程机器上的日志文件或Code Viewer上。由于通常时候我们很少会用到这一特性,这里就不进行详细的讨论了。