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.