酒杯上的碟酒杯上的碟酒杯上的碟交叉平台控件(CrossplatformControls) 从windows到Linux,或者相反 Borland处在一个令人兴奋的时期。并不是自从delphi这个Borland的令人兴奋的产品的第一声。我正在谈论的当然是关于Kylix,这个把CBuilder和Delphi带到Linux操作系统的项目。Delphi版本将首先面世,所以在本文余下部分,Kylix指的是DelphiforLinux。 我们正在为Delphi开发崭新的能够在Windows和Linux下工作的VCL。这意味着你可以在windows下写一个应用程序,然后把源代码转移到Linux下面重新编译反之亦然。这个新的VCL叫CLX,意即交叉平台控件库(ComponentLibraryCrossPlatform),CLX包含整个随Kylix发布的交叉平台库。在我写本文时它被分为下面四个子类: BaseCLX就是RTL,包含并且升级了Classes。pas VisualCLX包含了用户界面类,比如常用的控件 DataCLX包含交叉平台的数据库控件 NetCLX包含Internet部分,比如Apache等等。。 在我写这篇文章的时候(2000年5月之前),Kylix的第一部分测试已经正在进行了。当你读到这篇文章的时候,我正在使用的Kylix和你将要看到的正式版本将会有很大不同。这为我的工作带来很多不便。是简单地谈一谈便罢?还是涉及一下底层的结构?我更倾向于详细的讨论,这样无论如何你能得到一点关于CLX控件构造的头绪。但是要牢记一点:当你阅读此文的时候,很可能这篇文章中很多细节的讨论已经改变了。 没有更接近的了(NoOneElseComesClose) 这篇文章是关于写定制VisualCLX控件的初级读本。从本质上说,VisualCLX就是你所知道并热爱的VCL。当你这样认为的时候,可视构件库(VisualComponentLibrary)有一点用词不当:还有比可视构件更多的东西。但是在这篇文章里面,我只谈论可视控件。类似Button,Edit,ListBox,PageControl,StatusBar,ProgressBar等等的控件,都已经在交叉平台下重新实现。但是目前的VCL如此依赖Windows,我们是怎么做到这些的呢?简单地说,我们剥离了所有的Windows元素,然后把它们用别的工具包(toolkit)代替。 在Linux下,有大量的工具包包含标准windows控件(如Buttons)。它们被称做widgets。其中GTK和Qt(被发音成cute)就是两个非常流行的。Qt是一个工作在windows和Linux下的widgets,因为它非常接近我们的目标,所以Qt被选择作为CLX的基础。换句话说,Qt和CLX就好像WindowsAPI通用控件和VCL的关系。对于Linux下的Delphi的定制构件开发者来说,Qt有一些明显的好处: 它是一个广泛使用的Linux下的widgets集,被流行的KDE桌面采用。 它的开发和WindowsAPI风格非常相似 它的图形模块和VCL的图形模块相似 它的类看上去非常像VCL控件 它引入大量标准widgets,并且具有消息循环 这将引发两个疑问:是否这意味着Kylix只支持KDE,而不支持其他的桌面(desktop)?比如Gnome?并且,以Qt为基础的CLX会给我带来多大影响?第一个问题的回答是:kylix应用程序将运行在所有Linux桌面下,特别是Gnome和KDE。本文的余下部分将回答第二个问题。 不让你返回(????)(DontWantYouBack) 我们的目标是让开发者容易地将应用程序转移到linux下,并且困难要最小化。大部分(新旧控件)的名字都是一样的,大部分的属性也是一样的。尽管有一些控件的少数属性去掉了,增加了一些新的属性,但对于绝大部分来说,应该可以平稳的转移你的应用程序。 对控件作者来说有一些不同。对于一个新手,现在没有Windows。pas了,也没有WindowsAPI了。你可以对message标识和所有CN,CM通知(notifications)说再见了。这些都转换成了动态的(dynamics)(???)。在第一版中也不再有dock,BiDi相关的方法属性,输入法(IME),远东语言支持了。当然,更不会有ActiveX,COM或者OLE支持,Windows3。1控件也去掉了。 Methods CreateParams CreateSubClass CreateWindowHandle CreateWnd DestroyWindowHandle DestroyWnd DoAddDockClient DockOver DoDockOver DoRemoveDockClient DoUnDock GetDeviceContext MainWndProc ResetIme ResetImeComposition SetIme SetImeCompositionWindow WndProc Properties Ctl3D DefWndProc DockManager DockSite ImeMode ImeName ParentCtl3D UseDockManager WheelAccumulator 附图1:从TWidgetControl(和TWinControl相类似)里面去掉的Methods和properties。 此刻我打赌你正在想:还不坏。转移我的应用程序听上去不是很难,但是请等等还有更多的。在写此文的时候,CLX类的名字都被加上了一个Q的前缀,比如StdCtrls变成了QStdCtrls,有些类被稍微搅乱了一点,在类继承上面只有一些细微差别。(见附图 2) 附图2:在类继承上面的细微区别。 CLX的这个Q前缀不一定是最终版本的前缀。TWinControl现在变成了TWidgetControl,不过为了安抚痛苦,我们为TWidgetControl添加了一个TWinControl的别名。TWidgetControl和它的后代都有一个Handle属性,隐式地指向Qt对象,有一个Hooks属性指向一个hook对象,用来实现事件机制。(Hooks是一个复杂的话题,已经超出本文的讨论范围) OwnerDraw将被一种叫做Styles的方法替代。基本上Styles是widget或应用程序显示新面孔的一种机制,类似于windows下面的贴图(skins)。这部分正在开发当中,所以本文中我无法更进一步的介绍,我只能说:它非常酷! (新旧控件中)有没有什么是一样的?当然有,TCanvas(包括Pens,Brushes等)和你记得的一样。就像我说过的,类的继承基本上一样,还有事件,比如OnMouseDown,OnMouseMove,OnClick。。。等等都还在。 让我看看内涵(ShowMetheMeaning)(???) 让我们进入到CLX的躯体,看看它是如何工作的。Qt是一个C的工具集,所以所有的widgets都是C对象。另一方面,CLX是用ObjectPascal写的,并且ObjectPascal不能直接和C对象对话。越想简单就越难,Qt在几个地方使用了多继承,所以我们建立了一个接口层(interfacelayer)来获得所有Qt的类,并且把它们还原成一系列普通的C函数,然后把它们包装成Windows下的DLL或是Linux下的共享对象(sharedobject)。 每个TWidgetControl都有CreateWidget,InitWidget,和HookEvents虚方法,并且几乎总是被重载。CreateWidget创建Qt的widget,然后指派Handle到FHandle这个私有域变量。当widget被构造(constructed)后,InitWidget被调用,然后Handle有效。你的一些属性赋值将从Create这个构造函数转移到InitWidget。这将能够做到延迟构造(delayedconstruction)一个对象,直到真的需要它的时候。举个例子,你有一个属性叫Color,在SetColor里面,你可以通过HandleAllocated来检测是否你有一个Qt的Handle,如果handle已经分配(allocated),你就可以正确地调用Qt来设置颜色。如果没有分配,你可以把值保存在一个私有域变量中,然后在InitWidget中设置属性。 有两种类型的事件(events):Widget事件和系统事件。HookEvents是一个虚方法(virtualmethod),它钩住(hooks)CLX控件的事件方法到一个特殊的Hook对象,通过这个对象和Qt对象通讯。(至少这是我希望看到的) 这个hook对象其实是方法指针的集合。系统事件现在通过EventHandler,基本上是WndProc的替代品。 比生命还大(LargerThanLife)(????) 所有这些都只是后台信息(backgroundinformation),因为你真的不必为了写交叉平台的定制控件而知道这些。在CLX的帮助下,写交叉平台控件只是小菜一碟(asnap)。就像你不必理解WindowsAPI的复杂性而去写VCL控件一样。CLX和Qt也是如此。本文最后展示了一个用CLX写的定制控件代码 下面是一个工程文件CalcTest。dpr。计算器控件运行在windows下(见附图 4)和Linux下(见附图 5)看上去多么像标准的MicrosoftWindows计算器! programCalcTest; uses SysUtils,Classes,QControls,QForms,QStdCtrls,Qt, QComCtrls,QCalc,Types; type TTestFormclass(TForm) Calc:TCalculator; public constructorCreate(AOwner:TComponent);override; end; var TestForm:TTestForm; {TTestForm} constructorTTestForm。Create(AOwner:TComponent); begin inheritedCreateNew(AOwner); SetBounds(10,100,640,4 80); Calc:TCalculator。Create(Self); Dontforget:wehavetosettheparent。 Calc。Parent:Self; Calc。Top:100; Calc。Left:200; UncommentthesetotryotherBordereffects: Calc。BorderStyle:bsEtched; end; begin Application:TApplication。Create(nil); Application。CreateForm(TTestForm,TestForm); TestForm。Show; Application。Run; end。 附图3:CLX计算器控件的工程文件 附图4:运行在Windows下的计算器控件。 附图5:运行在RedHatLinux下的计算器控件。 就像你所看到的,TCalculator是TFrameControl的子类。TFrameControl继承自TWidgetControl的一个子类。它为你的控件提供了一个框架(frame),我们最感兴趣的属性是BorderStyle: TBorderStyle(bsNone,bsSingle,bsDouble,bsRaisedPanel,bsSunkenPanel, bsRaised3d,bsSUnken3d,bsEtched,bsEmbossed); 在这个控件(TCalculator)中有两个重要的方法。BuildCalc创建所有的按钮,并且把它们摆放到正确的位置。正如你所看到的,我使用了一个叫TButtonType的枚举类型来控制按钮的功能(function),还有少量的信息做为整型保存在Tag属性里面。我在后面的Calc方法里面会讲到它。所有的计算器按钮保存在一个叫做Btns的受保护的(protected)记录数组里面,类型是TButtonRecord。 TButtonRecordrecord Top:Integer; Left:Integer; Width:Integer; Height:Integer; Caption:string; Color:TColor; end; 这样做能够容易的在一个循环里面设置所有的按钮,而不用写一大串的TButton。Create调用。注意所有按钮的OnClick句柄都指派给了TCalculator的Calc方法。直接指派到一个自定义事件是不错的,因为所有按钮都在计算器的内部,并且这些事件都不用被published(见附图 6) fori:Low(TButtonType)toHigh(TButtonType)do withTButton。Create(Self)do begin Parent:Self; SetBounds(Btns〔i〕。Left,Btns〔i〕。Top,Btns〔i〕。Width, Btns〔i〕。Height); Caption:Btns〔i〕。Caption; Color:Btns〔i〕。Color; OnClick:Calc; Tag:Ord(i); end; 附图6:在这种情况下,直接指派一个自定义事件是明智的 我有一个叫FStatus的TLabel控件。TLabel也是TFrameControl的后代,我想在计算器里面使用它,所以我让它具有sunkenbox的外观来显示计算器的存储记忆,就像Windows里面的计算器一样。Qt标签的widget非常像VCL里面的TPanel控件。对于CLX里面的TLabel,我们没有发布(publish)它的框架(frame)属性,但是这并不妨碍你继承使用它。 在BuildCalc里面我做的最后一件事是创建一个edit控件来显示计算结果,正像你所看到的,计算器控件的Text属性和Edit控件的Text属性直接挂钩。 另一个重要的方法是Calc,它实质上是一个庞大的case语句,用来计算哪一个按钮被按下,并且决定该如何去做。我使用了私有域变量FCurrentValue,FLastValue和FRepeatValue来保存计算的值,所以我不必使用堆栈来实现。这个例子只是为了展示如何创建交叉平台控件,而不是如何写一个计算器。 很好,还记得吗,我在BuildCalc中使用了Tag属性来控制它的功能。在这个方法里面,我们将参数Sender强制转化成TButton,再将它的Tag强制转化成TButtonType类型,最后赋值给ButtonType。ButonType就是那个case语句里面的选择器表达式。 ButtonType:TButtonType(TButton(Sender)。Tag); 对于我们如何把这些转换成交叉平台控件,你感到惊奇吗?不?非常好!这说明你已经集中注意力了。这些代码可以同时在Windows和Linux下面编译,而不用改动任何地方。没有任何额外的步骤。正是仰仗于CLX的优点,控件已经全部完工了。 所有不得不交待的(AllIHavetoGive)(????) 你已经看到,写一个交叉平台控件和写一个VCL控件并不是完全不同。如果你是一个控件开发的新手,学习起来不会很难。如果你是一个经验丰富的VCL控件作者,你的大部分知识都将平滑地转移到Kylix上去。 我前面说过,(交叉平台控件)会有一些不同,但是这只会影响那些依赖于WindowsAPI写控件的开发者。如果你写的控件是从VCL继承的,是聚合(aggregate)了一些控件的(就像我在TCalculator里面做的),是一个非可视的不依赖于WindowsAPI的,或者是一个TGraphic控件,那么转移到Linux下不会遇到什么麻烦。 这篇文章介绍的软件产品的功能正在开发中,并且会在没有通知的情况下有些变化。相应功能的描述是应时而变的,并且没有任何确定的服务承诺。 做为Delphi最初测试版的用户,RobertKozak从1999年下半年加入Borland的KylixRamp;D小组。从他加入Borland开始,他就和CBuilder5和Kylix的开发密切相关。Robert还和TaDDA(多伦多区Delphi开发者协会)有关,这个协会后来和TDUG(多伦多区Delphi用户组)合并。Robert在这些用户社团一直很活跃,同时也经常出现在Borland的新闻组里面。 〔列表1〕 QCalc。pas {} {} {BorlandDelphiVisualComponentLibrary} {BorlandDelphiComponentLibrary(X)Crossplatform} {} {Copyright(c)2000InpriseBorland} {} {} unitQCalc; ThisistheveryfirstCustomcontrolwrittenforCLX。 interface uses Sysutils,Classes,QT,QControls,QStdCtrls,QComCtrls, QGraphics; typebt8,bt9,btDecimal,btPlusMinus,btMultiply,btDivide, btAdd,btSubtract,btSqrt,btPercent,btInverse, btEquals,btBackspace,btClear,btClearAll, btMemoryRecall,btMemoryStore,btMemoryClear, btMemoryAdd); TCalcState(csNone,csAdd,csSubtract,csMultiply,csDivide); TButtonRecordrecord Top:Integer; Left:Integer; Width:Integer; Height:Integer; Caption:string; Color:TColor; end; TCalculatorclass(TFrameControl) private FResultEdit:TEdit; FStatus:TLabel; FMemoryValue:Single; FCurrentValue:Single; FLastValue:Single; FRepeatValue:Single; FState:TCalcState; FBackSpaceValid:Boolean; protected Btns:array〔TButtonType〕ofTButtonRecord; procedureBuildCalc; procedureCalc(Sender:TObject); functionGetText:string;override; procedureSetText(constValue:string);override; public constructorCreate(AOwner:TComponent);override; propertyValue:SinglereadFCurrentValue; published propertyText:stringreadGetTextwriteSetText; propertyBorderStyle; propertyLineWidth; propertyMargin; propertyMidLineWidth; propertyFrameRect; end; implementation functionButtonRecord(aTop,aLeft,aWidth,aHeight:Integer;aCaption:string; aColor:TColorclBtnFace):