元件除错环境实作范例
许多以Delphi为工具的程序开发者,时常需要开发自定的元件类别以符合所需。在元件的开发过程中,由於无法直接由Delphi的集成除错环境中对元件进行除错,必须辗转透过其它方式逐步除错,既麻烦又费时。在本篇中笔者提供一个简易的除错环境,直接利用Delphi的集成除错器为您的元件除错,加速开发元件的时间。
Delphi提供了一个强而有力的元件结构VCL,建立了一个FrameWork的标准。程序开发者可於此基础上自由的运用以及继承,藉此开发自定的元件类别。在元件开发的过程中,除错一直是元件开发者常遭遇的问题。完整元件的测试工作包括元件行为测试,属性编辑器(Proeprty Editor)测试以及元件编辑器(Component Editor)测试。有关元件行为测试的部份,虽然可以藉由动态建立对象的方式直接应用Delphi集成除错器来除错,但是对於属性编辑器及元件编辑器部份,便只能靠原始的ShowMessage来模拟Delphi集成除错器的BreakPoint了。
由於错误可能发生在程序中的任何地方,因此这样做不仅费时,亦不容易找出真正的问题点。难道就没有其它解决的方法了吗?事实上Delphi提供了一个Designer的机制,Delphi的集成开发环境便是依此而来。由於这个部份Borland公司并未提供充足的相关信息,因此一般人也较难以应用。利用这个机制,我们可以轻易的实作一个环境以供除错,但在实作之前,您有必要对此机制做一概括性的认识。本篇中笔者以一自定的MessageBox元件为例,介绍如何应用自定的除错环境来对元件除错。
认识Designer
从字面意义上来看,Designer似乎与设计有关,事实上Designer掌控了元件与Form的沟通管道。藉由Designer,Delphi得以区分Design-Time与Run-Time的不同。Designer中有一个Form的属性,用来记录目前设计中的Form。除此之外,在设计的Form中也有一个Designer属性,记录了这个Form的Designer。当进入设计模式时,Designer会拦截所有来自鼠标以及键盘的信息,并将此信息传给设计的Form,并由Form来处理这些信息,忽略元件对信息的处理(如图一)。利用此机制,我们可以防止元件於设计时期中发生执行时期的行为,并在设计时期定义相关的元件处理机制,诸如处理对元件的拖放(Drag and Drop)、改变尺寸(Resize)、移动(Move)、复制(Copy)、删除(Cut)等等行为,以及一些如对齐(Aligment)、群组选取(Group Select)等附属的功能。Delphi并没有公开这些实作内容的原始程序码,但提供了Designer类别的介面宣告。你可以在Forms及DsgnIntf这两个单元中,找到Tdesigner及TFormDesigner两个Designer的抽象类别(见表一),虽然这些类别中有许多的方法介面,但我们并不需要实作所有的方法。仅针对需要实作的部份加以实作即可。
设计模式的进入点
在VCL的结构中,凡是由Tcomponent衍生的元件类别都有一个SetDesigning方法,用以设定元件的状态,不过此方法宣告在Protected区段,您无法直接使用,因此必须另外定义一个继承自Tcomponent的类别,并将此方法开放出来。透过SetDesigning方法,可以设定元件为设计模式(True)或是执行模式(False),要分辨某一元件所处的状态,你可藉由ComponentState属性来得知,此属性为一集合(Set)型态属性,因此你可以以(csDesigning in ComponentState)来判别元件所处的状态。
事实上在执行时期设定元件型态为csDesigning时,你并不会发现有任何的改变,主要是因为没有可以处理此状态的Designer,因此若要能反应设计时期的特徵,你必须先建立一个Designer,并将此Designer指定给设计中的Form。虽然Delphi提供了完整的机制让程序开发者能建立一个完整的Designer,但其实所谓的设计时期,仍端赖个人对设计模式的定义,倒不见得一定要和Delphi的设计模式一样,提供许多的功能。本篇主要的目的乃在建立一个能够使用Delphi集成除错器的设计环境,并不须要过多的复杂功能,因此就Designer的实作层面而言,我们所要做的不过是拦截信息而已。
当Form连结至Designer後,即表示此Form已进入设计模式,此时你必须对来自外界的信息做出相对的反应。在TFormDesigner中有一IsDesignMsg的方法介面,利用此方法可以接收来自外界对Form及其所有的元件所引发的信息。在此时机点,所要做的便是过滤出我们所要的信息,并将信息处理後传给设计中的Form以便做进一步的处理,表(三)中我们实作了IsDesignMsg方法,并针对所有来自鼠标的信息加以处理,将之转换为Form的信息,忽略元件本身对信息的的反应。在Form接受到信息後,便依原先预定的信息处理程序,做出适切的回应。
取得元件类别
就如同Delphi的设计环境一样,您必须向Delphi注册才能在Form中加入您的元件。Delphi提供了一支常式RegisterComponents让元件向系统注册,如此Delphi的元件库才能取得元件类别的信息,并依此建立元件。此常式定义在Classes单元中。观其始末,在此常式的实作区段中,会去调用一支名为RegisterComponentsProc的CallBack Function(见表四),在元件注册时,Delphi会主动指定此Fucntion,以便进行元件注册的动作。在我们的设计的环境中,亦可利用此方式让元件向我们的系统注册,只是RegisterComponentsProc必须由我们来指定。
本篇中笔者定义一支与RegisterComponentsProc同型态的函数,并将此函数指定给RegisterComponentsProc,函数中以一个Tstrings对象来存放所注册的元件名称与元件类别信息(见表五)。在调用此函数的过程中,会传入一个元件类别阵列以及元件所处的类别页名称。本例中并没有使用类别页的名称,因此在注册元件时,此参数可以空字串代替。利用所获得的Tstrings对象中的元件信息,我们可以调用TComponentClass类别中的Create方法建立此类别的元件,并将元件的Parent设为Form,使建立的元件能正确的显示於Form上。
非视觉化元件的处理
在VCL的结构中,并不是所有的元件都具有可视的外观型态,而是只有由TWinControl或是
TGraphicControl继承下来的元件才具有外观。因此在Delphi的设计环境中,对於这类不具外观的元件(如Ttable),必须特别处理。一般做法上是建立一个替代的元件,用以代表实际的元件。而在元件建立之时,藉由判断此元件类别的祖先类别,即可决定是否以替代类别取代之。因此你可以发现在Delphi的设计环境中,所有不具外观的元件都被一个小图示按钮所取代。这些替代的元件中都包含的一个对应实际元件的属性,以表明身份。
当在Form中点选某一个元件时,透过Designer对信息的拦截,Designer会将所点选的元件信息传给Form来处理,当Form接收到元件信息时可依照元件的类别来行决定所要反应的行为,如调用该元件的元件编辑器等等。由於点选的可能是不可视元件,因此Form必须检查点选的元件是否为替代元件类别,若是则以实际元件为主。为了达到也能处理不可视元件的能力,笔者定义了一个替代元件类别 TRTContainer来代表这些不可视的元件(见表六)。类别中的Component属性对应到实际的元件类别,由於此替代类别必须是可视的,因此我们继承TCustomControl类别来实作,利用此类别提供的Canvas属性,及Paint方法实作出元件的外观。本篇中的元件范例TMessageBox类别便是一个不可视的元件,你可以利用自定的设计环境建立并为此元件进行除错。
元件编辑器的处理
在Delphi的设计环境中,元件编辑器扮演的一个重要的角色。一般情形下,以鼠标左键双击元件时,Designer会为此元件建立一支对应的事件处理方法(Event Handler),例如在Form中DubleClickButton1元件後,会在Editor视窗中建立一支Button1Click事件处理的方法。基本上这便是元件编辑器的功能。Delphi定义了TComponentEditor的编辑器类别,亦实作了一个TDefaultEditor的预设类别,若您没有为您的元件注册任何的元件编辑器,则Delphi会以TDefaultEditor为此元预设的元件编辑器。此类别会以元件中的找到的第一个事件为建立方法的依据,你可以自行指定来源事件,建立此事件的事件处理方法。然而许多情形下,对话盒式的元件编辑器仍较受用户的喜爱,有些功能较强的元件,如Tchart元件,其本身的元件编辑器便十分的复杂,有如一个小型的应用程序,也因为如此,用户在设定元件时可以不必全部透过撰写程序法码来完成,元件编辑器已经为您处理了大部份的设定功能。
有的元件也支援由单击鼠标右键来调用本身的元件编辑器,此时会出现一个弹出式功能选单,让用户选择并调用。大部份有此功能的元件都提供不只一种元件编辑功能,例如Tquery元件便提供了四种编辑元件的功能供用户选择。在Delphi的设计环境中,是将Form的编辑功能与元件的元件编辑功能结合起来,因此在Click鼠标右键後可叫出一个标准的功能选单,如点选的元件有元件编辑器,则此编辑功能选项会显示在选单最上方,此为Delphi的处理机制。Delphi的Designer类别并没有提供这方面的支援,因此若要支援能元件编辑器的功能,我们必须自己实作。本篇中笔者使用的方式是於Form中建立一个全域性的弹出式选单,并利用TCompoentEditor类别中的GetVerbCount方法取得元件编辑器中的可选功能数量,利用GetVerb方法来取得元件编辑器的所有选项名称,并将之逐一加入此弹出式选单中,并於加入完成前,加入一个删除元件的选项,以便用户能删除建立的元件。
当用户调用某元件的元件编辑器时,透过Delphi提供的GetComponentEditor方法以取得此元件的元件编辑器,并在设定功能选单後,将选单显示於点选位置。待用户选定後,便透过TCompoentEditor类别中的ExecuteVerb方法调用相对应的元件编辑功能(见表八)。本篇中的元件范例TMessageBox类别便定义了一个元件编辑器,此编辑器亦为一对话视窗,在使用上您必须将此元件编辑器如同注册元件般,透过Delphi所提供的RegisterComponentEditor将此编辑器注册,如此才能正确的调用。
元件拖拉技巧的实作
在Delphi的设计时期的Designer提供不少的元件的编辑功能,举凡元件搬移,尺寸调整,复制,删除,等等,种种的功能营造出一个使用亲合且开发快速的集成环境,这些功能的背後的实作,有的牵涉技术十分复杂,有的却也简单明了。以元件搬移功能来说,乃是设计环境中不可或缺的功能之一。从观察Delphi中的元件移动行为来看,搬移的动作是以一个矩形方框来代替元件移动的过程,直到放开鼠标左键後,才实际将元件移至指定位置。这样的做法不仅可以减少系统的负荷,也容易对照位置,可说一举两得。
笔者在本例中提供了一个可将元件任意移动以及删除元件的设计环境,也许距离Delphi的设计环境仍嫌太远,但对於元件的除错上,这些基本的功能实已足够。至於详细的设计方法与程序,碍於篇幅限制,在此不便刊出,仅针对设计过程中笔者认为须注意的要点与技巧加以说明,其余的部份读者可参阅范例程序RTBForm.PAS,里面有完整的说明。
在设计过程中主要有以下几点是必须加以考量的:
(1)绘制移动时的矩形方框时,技巧上可将Canvas中Pen的Mode的属性值设定为pmXor(见表九),如此在移动的过程中只需将此方框於原位置重画一次即可将背景还原,之後再於新的位置画上新的方框便可,既省时又简单。
(2)由於绘制的矩形方框并没有Handle,因此在移动过程中经过其它元件上方的部份时会被元件所覆盖(见图五),为解决此问题,我们可以透过调用GetWindowLong及SetWindowLong此二支WindowsAPI来将矩形方框提至最上方,以解决覆盖的问题。
(3)为了避免在元件移动过程中,将元件移出视窗外不可见的区域,而导致元件消失的情形,我们可以调用ClipCursor这支Windows API,将滑属游标限制在某范围内,待放开鼠标左键後再取消限制。
(4)为能模拟调用元件编辑器的功能,必须动态建立一个Popup的选单元件,将取得的ComponentEditor选项信息加入Popup选单中。另由於OnDblClick事件会同时伴随触发OnMouseDown及OnClick事件,因此须直接栏截Windows的WM_LButtonDblClk信息,以确保伴随事件不会发生。
(5)由於元件的每一次移动重绘,都可能破坏其它元件的外观,因此为避免Form上的元件因彼此覆盖而导致画面的破坏,故於元件移动完成後将必须将所有Form上可见的元件重画一次。
为您自定的元件除错 TMessageBox
在基本的设计环境完成後,便可以加入元件开始为元件除错了。您可以透过RegisterComponents及RegisterComponentEditor两支函数将元件加入设计环境中,当然这一切得在Delphi的设计环境中完成。本例中笔者加入自行设计的TMessageBox元件,以测试元件的正确性。待加入系统编译执行後,您可以在主画面视窗右方看到一个TMessageBox的类别字串,试著按下[Create Component]按钮看看,你可以发现在Form中多了一个元件,由於此元件为不可视元件,因此由一TRTContainer元件所取代,以鼠标点选此元件,可於状态列中找到元件的名称及所属类别。先不要结束程序,回到Delphi的设计环境中,叫出此元件的原始程序码(MsgBox。PAS),於此元件的ComponentEditor实作区段中的ExecuteVerb方法中设定中断点,并回到执行中的程序,於元件上双击鼠标左键,或单击鼠标右键,调用此元件编辑器,此时你可以发现程序会暂停於设定的中断点,此时你可以一如一般应用程序般的利用Delphi的集成除错器为元件进行除错,而不用担心因害怕元件的Bug而导致整个元件库毁损而必须事先备份元件库了。这样的作法是不是既安全且方便呢?
结语
元件的设计除了本身及其元件编辑器外,另外一个重点便是属性编辑器(Property Editor),在Delphi的设计环境中,用来调用属性编辑器的是对象检视器(Object Inspector)。元件中的每个属性都有自己的属性编辑器,因此测试的功作更形重要。欲实作一个对象检视器也有许多相关课题需要注意的,事实上有关对象检视器中的许多信息来源,绝大部份仍需要仰赖Designer的的提供,因此一个完整的集成设计环境,各部份彼此都是环环相扣,缺一不可的。本篇中实作了元件编辑部份的除错功能,你可以利用此模式,为自己设计的专属的属性编辑器除错环境,如同对象检视器一般的方便实用。
在笔者目前所参与的RT-Builder计划中,便利用上述的机制实作了一个完整的开发设计环境,此环境除了可提供完整的元件除错能力外,亦支援Delphi设计环境中大部份的功能,如同另一个Delphi般,更由於此产品是以元件方式呈现,只须在编译时期於您的应用程序中加入此元件,便可使您的应用程序拥有Delphi般的设计环境,这对系统维护上来说,亦不失为一项有利的选择,若读者有兴趣可自行前往笔者公司网站(http://www.infolight.com)中下载试用版本,相信会让您耳目一新。/
一个功能完善,品质良好的元件是需要经过严格的考验的,而复杂功能的背後往往隐藏的是长篇深奥难解的程序码,因此在开发元件的过程中,一个功能齐全且信息完整的除错环境是必须的。良好的除错环境不仅能确保设计品质的可靠度,亦可缩短整个开发的时间,著实对程序开发者有著不可忽视的助益,您说是吗?