首页 > 免费论文 > 计算机论文 > 酒杯上的碟
 

酒杯上的碟

日期:2006-06-10 04:13:26  来源:
请您记住思索网的网址: http://www.4so.net  [加入收藏夹]

    交叉平台控件(Cross-platformControls)
从windows到Linux,或者相反


Borland处在一个令人兴奋的时期.并不是自从delphi--这个Borland的令人兴奋的产品的第一声.我正在谈论的当然是关于Kylix,这个把C++Builder和Delphi带到Linux操作系统的项目.Delphi版本将首先面世,所以在本文余下部分,Kylix指的是DelphiforLinux.

我们正在为Delphi开发崭新的能够在Windows和Linux下工作的VCL.这意味着你可以在windows下写一个应用程序,然后把源代码转移到Linux下面重新编译--反之亦然.这个新的VCL叫CLX,意即"交叉平台控件库(ComponentLibraryCross-Platform)",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.本文的余下部分将回答第二个问题.

不让你返回(????)(Don'tWantYouBack)

我们的目标是让开发者容易地将应用程序转移到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
TTestForm=class(TForm)
Calc:TCalculator;
public
constructorCreate(AOwner:TComponent);override;
end;

var
TestForm:TTestForm;
{TTestForm}
constructorTTestForm.Create(AOwner:TComponent);
begin
inheritedCreateNew(AOwner);
SetBounds(10,100,640,480);
Calc:=TCalculator.Create(Self);
//Don'tforget: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.

TButtonRecord=record
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的KylixR&D小组.从他加入Borland开始,他就和C++Builder5和Kylix的开发密切相关.Robert还和TaDDA(多伦多区Delphi开发者协会)有关,这个协会后来和TDUG(多伦多区Delphi用户组)合并.Robert在这些用户社团一直很活跃,同时也经常出现在Borland的新闻组里面.

[列表1]
QCalc.pas

{*****************************************************}
{}
{BorlandDelphiVisualComponentLibrary}
{BorlandDelphiComponentLibrary(X)Crossplatform}
{}
{Copyright(c)2000Inprise/Borland}
{}
{*****************************************************}

unitQCalc;

//ThisistheveryfirstCustomcontrolwrittenforCLX.

interface

uses
Sysutils,Classes,QT,QControls,QStdCtrls,QComCtrls,
QGraphics;

type
TButtonType=(bt0,bt1,bt2,bt3,bt4,bt5,bt6,bt7,
bt8,bt9,btDecimal,btPlusMinus,btMultiply,btDivide,
btAdd,btSubtract,btSqrt,btPercent,btInverse,
btEquals,btBackspace,btClear,btClearAll,
btMemoryRecall,btMemoryStore,btMemoryClear,
btMemoryAdd);

TCalcState=(csNone,csAdd,csSubtract,csMultiply,csDivide);

TButtonRecord=record
Top:Integer;
Left:Integer;
Width:Integer;
Height:Integer;
Caption:string;
Color:TColor;
end;

TCalculator=class(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:TColor=clBtnFace):TButtonRecord;
begin
Result.Top:=aTop;
Result.Left:=aLeft;
Result.Width:=aWidth;
Result.Height:=aHeight;
Result.Caption:=aCaption;
Result.Color:=aColor;
end;

{TCalculator}

constructorTCalculator.Create(AOwner:TComponent);
begin
inheritedCreate(AOwner);
SetBounds(0,0,250,200);
FMemoryValue:=0;
FCurrentValue:=0;
FLastValue:=0;
FRepeatValue:=0;
BorderStyle:=bsRaisedPanel;
BuildCalc;
end;

procedureTCalculator.BuildCalc;
var
i:TButtonType;
begin
Btns[bt7]:=ButtonRecord(70,48,36,29,'7');
Btns[bt4]:=ButtonRecord(102,48,36,29,'4');
Btns[bt1]:=ButtonRecord(134,48,36,29,'1');
Btns[bt0]:=ButtonRecord(166,48,36,29,'0');
Btns[bt8]:=ButtonRecord(70,88,36,29,'8');
Btns[bt5]:=ButtonRecord(102,88,36,29,'5');
Btns[bt2]:=ButtonRecord(134,88,36,29,'2');
Btns[btPlusMinus]:=
ButtonRecord(166,88,36,29,'+/-');
Btns[bt9]:=ButtonRecord(70,128,36,29,'9');
Btns[bt6]:=ButtonRecord(102,128,36,29,'6');
Btns[bt3]:=ButtonRecord(134,128,36,29,'3');
Btns[btDecimal]:=ButtonRecord(166,128,36,29,'.');
Btns[btDivide]:=ButtonRecord(70,168,36,29,'/');
Btns[btMultiply]:=ButtonRecord(102,168,36,29,'*');
Btns[btSubtract]:=ButtonRecord(134,168,36,29,'-');
Btns[btAdd]:=ButtonRecord(166,168,36,29,'+');
Btns[btBackspace]:=
ButtonRecord(37,49,63,25,'Backspace');
Btns[btClear]:=ButtonRecord(37,115,63,25,'CE');
Btns[btClearAll]:=ButtonRecord(37,181,63,25,'C');
Btns[btsqrt]:=ButtonRecord(70,208,36,29,'sqrt');
Btns[btPercent]:=ButtonRecord(102,208,36,29,'%');
Btns[btInverse]:=ButtonRecord(134,208,36,29,'1/x');
Btns[btEquals]:=ButtonRecord(166,208,36,29,'=');
Btns[btMemoryAdd]:=ButtonRecord(166,5,36,29,'M+');
Btns[btMemoryStore]:=
ButtonRecord(134,5,36,29,'MS');
Btns[btMemoryRecall]:=
ButtonRecord(102,5,36,29,'MR');
Btns[btMemoryClear]:=ButtonRecord(70,5,36,29,'MC');

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;

FStatus:=TLabel.Create(Self);
withFStatusdo
begin
Parent:=Self;
SetBounds(10,38,25,25);
BorderStyle:=bsRaisedPanel;
end;

FResultEdit:=TEdit.Create(Self);
FResultEdit.Parent:=Self;
FResultEdit.SetBounds(5,5,240,25);
FResultEdit.Alignment:=taRightJustify;
FResultEdit.Font.Height:=-13;
FResultEdit.Font.Name:='Arial';
FResultEdit.Text:='0';
end;

procedureTCalculator.Calc(Sender:TObject);
const
MemoryStoreMap:array[Boolean]ofstring=('M','');
var
ButtonType:TButtonType;
Temp:string;
TempValue:Single;
begin
ButtonType:=TButtonType(TButton(Sender).Tag);

try
caseButtonTypeof
bt0..bt9:
begin
FBackSpaceValid:=True;
if(FResultEdit.Text='0')or(FCurrentValue=0)thenFResultEdit.Text:='';
FResultEdit.Text:=
FResultEdit.Text+Btns[ButtonType].Caption;
FCurrentValue:=StrToFloat(FResultEdit.Text);
FRepeatValue:=0;
end;

btDecimal:
ifPos('.',FResultEdit.Text)<1then
begin
FCurrentValue:=StrToFloat(FResultEdit.Text);
FLastValue:=0;
FResultEdit.Text:=
FResultEdit.Text+Btns[ButtonType].Caption;
end;

btPlusMinus:
begin
FCurrentValue:=StrToFloat(FResultEdit.Text);
FCurrentValue:=FCurrentValue*-1;
FResultEdit.Text:=FloatToStr(FCurrentValue);
end;

btClearAll:
begin
FCurrentValue:=0;
FLastValue:=0;
FResultEdit.Text:='0';
FState:=csNone;
end;

btClear:
begin
FCurrentValue:=0;
FResultEdit.Text:='0';
end;

btAdd:
begin
FCurrentValue:=StrToFloat(FResultEdit.Text);
FState:=csAdd;
FLastValue:=FCurrentValue;
FCurrentValue:=0;
end;

btSubtract:
begin
FCurrentValue:=StrToFloat(FResultEdit.Text);
FState:=csSubtract;
FLastValue:=FCurrentValue;
FCurrentValue:=0;
end;

btDivide:
begin
FCurrentValue:=StrToFloat(FResultEdit.Text);
FState:=csDivide;
FLastValue:=FCurrentValue;
FCurrentValue:=0;
end;

btMultiply:
begin
FCurrentValue:=StrToFloat(FResultEdit.Text);
FState:=csMultiply;
FLastValue:=FCurrentValue;
FCurrentValue:=0;
end;

btBackSpace:
ifFBackSpaceValidthen
begin
Temp:=FResultEdit.Text;
Delete(Temp,Length(Temp),1);
ifTemp=''thenTemp:='0';
FCurrentValue:=StrToFloat(Temp);
FResultEdit.Text:=FloatToStr(FCurrentValue);
end;

btInverse:
begin
FCurrentValue:=StrToFloat(FResultEdit.Text);
FCurrentValue:=1/FCurrentValue;
FResultEdit.Text:=FloatToStr(FCurrentValue);
end;

btPercent:
begin
FCurrentValue:=StrToFloat(FResultEdit.Text);
FCurrentValue:=FCurrentValue/100;
FResultEdit.Text:=FloatToStr(FCurrentValue);
end;

btSqrt:
begin
FCurrentValue:=StrToFloat(FResultEdit.Text);
FCurrentValue:=Sqrt(FCurrentValue);
FResultEdit.Text:=FloatToStr(FCurrentValue);
end;

btMemoryStore:
begin
FMemoryValue:=StrToFloat(FResultEdit.Text);
FMemoryValue:=FMemoryValue*1;
FCurrentValue:=0;
end;

btMemoryAdd:
begin
TempValue:=FMemoryValue;
FMemoryValue:=StrToFloat(FResultEdit.Text);
FMemoryValue:=(FMemoryValue*1)+TempValue;
end;

btMemoryRecall:
begin
FResultEdit.Text:=FloatToStr(FMemoryValue);
FCurrentValue:=0;
end;

btMemoryClear:
begin
FMemoryValue:=0;
end;

btEquals:
ifFState<>csNonethen
begin
FBackSpaceValid:=False;
FCurrentValue:=StrToFloat(FResultEdit.Text);
ifFRepeatValue=0then
begin
FRepeatValue:=FCurrentValue;
FCurrentValue:=FLastValue;
end;
FLastValue:=FRepeatValue;
caseFStateof
csAdd:
FCurrentValue:=FCurrentValue+FLastValue;
csMultiply:
FCurrentValue:=FCurrentValue*FLastValue;
csSubtract:
FCurrentValue:=FCurrentValue-FLastValue;
csDivide:
FCurrentValue:=FCurrentValue/FLastValue;
end;
FLastValue:=FCurrentValue;
FResultEdit.Text:=FloatToStr(FCurrentValue);
FCurrentValue:=0;
end;
end;//caseButtonTypeof...

except
onE:Exceptiondo
begin
FResultEdit.Text:=E.Message;
FLastValue:=0;
FCurrentValue:=0;
FRepeatValue:=0;
FState:=csNone;
end;
end;
FStatus.Caption:=MemoryStoreMap[FMemoryValue=0];
end;

functionTCalculator.GetText:string;
begin
Result:=FResultEdit.Text;
end;

procedureTCalculator.SetText(constValue:string);
begin
FResultEdit.Text:=Value;
end;

end.


 
 
热门信息
 
相关文章