我就不换行了.
[返回主页]
如果不堪忍受我的翻译,请阅读英文原文
我拿不准的地方用???标明了.
酒杯上的碟(The Dish on Kylix)(???)
CLX / Customs controls / Cross-platform
作者: Robert Kozak
翻译: cAkk
交叉平台控件(Cross-platform Controls)
从windows到Linux,或者相反
Borland处在一个令人兴奋的时期.并不是自从delphi--这个Borland的令人兴奋的产品的第一声.我正在谈论的当然是关于Kylix,这个把C++Builder和Delphi带到Linux操作系统的项目.Delphi版本将首先面世,所以在本文余下部分,Kylix指的是Delphi for Linux.
我们正在为Delphi开发崭新的能够在Windows和Linux下工作的VCL.这意味着你可以在windows下写一个应用程序,然后把源代码转移到Linux下面重新编译--反之亦然.这个新的VCL叫CLX,意即"交叉平台控件库(Component Library Cross-Platform)",CLX包含整个随Kylix发布的交叉平台库.在我写本文时它被分为下面四个子类:
BaseCLX就是RTL,包含并且升级了Classes.pas
VisualCLX包含了用户界面类,比如常用的控件
DataCLX包含交叉平台的数据库控件
NetCLX包含Internet部分,比如Apache等等..
在我写这篇文章的时候(2000年5月之前),Kylix的第一部分测试已经正在进行了.当你读到这篇文章的时候,我正在使用的Kylix和你将要看到的正式版本将会有很大不同.这为我的工作带来很多不便.是简单地谈一谈便罢?还是涉及一下底层的结构?我更倾向于详细的讨论,这样无论如何你能得到一点关于CLX控件构造的头绪.但是要牢记一点: 当你阅读此文的时候,很可能这篇文章中很多细节的讨论已经改变了.
没有更接近的了(No One else
Comes Close)
这篇文章是关于写定制VisualCLX控件的初级读本.从本质上说,VisualCLX就是你所知道并热爱的VCL.当你这样认为的时候,"可视构件库"(Visual Component Library)有一点用词不当:还有比"可视构件"更多的东西.但是在这篇文章里面,我只谈论"可视"控件.类似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就好像Windows API/通用控件和VCL的关系.对于Linux下的Delphi的定制构件开发者来说,Qt有一些明显的好处:
它是一个广泛使用的Linux下的widgets集,被流行的KDE桌面采用.
它的开发和Windows API风格非常相似
它的图形模块和VCL的图形模块相似
它的类看上去非常像VCL控件
它引入大量标准widgets,并且具有消息循环
这将引发两个疑问:是否这意味着Kylix只支持KDE,而不支持其他的桌面(desktop)?比如Gnome?并且,以Qt为基础的CLX会给我带来多大影响? 第一个问题的回答是:kylix应用程序将运行在所有Linux桌面下,特别是Gnome和KDE. 本文的余下部分将回答第二个问题.
不让你返回(????)(Don't Want You Back)
我们的目标是让开发者容易地将应用程序转移到linux下,并且困难要最小化.大部分(新旧控件)的名字都是一样的,大部分的属性也是一样的.尽管有一些控件的少数属性去掉了,增加了一些新的属性,但对于绝大部分来说,应该可以平稳的转移你的应用程序.
对控件作者来说有一些不同. 对于一个新手,现在没有Windows.pas了,也没有Windows API了.你可以对message标识和所有CN,CM通知(notifications)说再见了.这些都转换成了动态的(dynamics)(???).在第一版中也不再有dock,BiDi相关的方法/属性,输入法(IME),远东语言支持了.当然,更不会有ActiveX,COM或者OLE支持,Windows 3.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...等等都还在.
让我看看内涵(Show Me the Meaning)(???)
让我们进入到CLX的躯体,看看它是如何工作的.Qt是一个C++的工具集,所以所有的widgets都是C++对象.另一方面,CLX是用Object Pascal写的,并且Object Pascal不能直接和C++对象对话.越想简单就越难,Qt在几个地方使用了多继承,所以我们建立了一个接口层(interface layer)来获得所有Qt的类,并且把它们还原成一系列普通的C函数,然后把它们包装成Windows下的DLL或是Linux下的共享对象(shared object).
每个TWidgetControl都有CreateWidget, InitWidget, 和HookEvents虚方法,并且几乎总是被重载. CreateWidget创建Qt的widget,然后指派Handle到FHandle这个私有域变量.当widget被构造(constructed)后,InitWidget被调用,然后Handle有效.你的一些属性赋值将从Create这个构造函数转移到InitWidget.这将能够做到延迟构造(delayed construction)一个对象,直到真的需要它的时候.举个例子,你有一个属性叫Color,在SetColor里面,你可以通过HandleAllocated来检测是否你有一个Qt的Handle,如果handle已经分配(allocated),你就可以正确地调用Qt来设置颜色.如果没有分配,你可以把值保存在一个私有域变量中,然后在InitWidget中设置属性.
有两种类型的事件(events): Widget事件和系统事件.HookEvents是一个虚方法(virtual method),它钩住(hooks)CLX控件的事件方法到一个特殊的Hook对象,通过这个对象和Qt对象通讯.(至少这是我希望看到的)
这个hook对象其实是方法指针的集合.系统事件现在通过EventHandler,基本上是WndProc的替代品.
比生命还大(Larger Than Life)(????)
所有这些都只是后台信息(background information),因为你真的不必为了写交叉平台的定制控件而知道这些.在CLX的帮助下,写交叉平台控件只是小菜一碟(a snap).就像你不必理解Windows API的复杂性而去写VCL控件一样.CLX和Qt也是如此. 本文最后展示了一个用CLX写的定制控件代码
下面是一个工程文件CalcTest.dpr. 计算器控件运行在windows下(见附图4) 和Linux下(见附图5) 看上去多么像标准的Microsoft Windows 计算器!
program CalcTest;
uses
SysUtils, Classes, QControls, QForms, QStdCtrls, Qt,
QComCtrls, QCalc, Types;
type
TTestForm = class(TForm)
Calc: TCalculator;
public
constructor Create(AOwner: TComponent);
override;
end;
var
TestForm: TTestForm;
{ TTestForm }
constructor TTestForm.Create(AOwner: TComponent);
begin
inherited CreateNew(AOwner);
SetBounds(10,100,640,480);
Calc := TCalculator.Create(Self);
//do
n't forget: we have to set the parent.
Calc.Parent := Self;
Calc.Top := 100;
Calc.Left := 200;
// Uncomment these to try other Border effects:
// Calc.BorderStyle := bsEtched;
end;
begin
Application := TApplication.Create(nil);
Application.CreateForm(TTestForm, TestForm);
TestForm.Show;
Application.Run;
end.
附图 3: CLX计算器控件的工程文件
附图 4: 运行在Windows下的计算器控件.
附图 5: 运行在Red Hat Linux下的计算器控件.
就像你所看到的,TCalculator是TFrameControl的子类.TFrameControl继承自TWidgetControl的一个子类.它为你的控件提供了一个框架(frame),我们最感兴趣的属性是BorderStyle:
TBorderStyle = (bsNone, bsSingle, bsDouble, bsRaisedPanel,bsSunkenPanel,
bsRaised3d, bsSUnken3d, bs Etched, 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)
for i := Low(TButtonType) to High(TButtonType)do
with TButton.Create(Self)do
begin
Parent := Self;
SetBounds(Btns.Left, Btns.Top, Btns.Width,
Btns.Height);
Caption := Btns.Caption;
Color := Btns.Color;
OnClick := Calc;
Tag := Ord(i);
end;
附图 6: 在这种情况下,直接指派一个自定义事件是明智的
我有一个叫FStatus的TLabel控件. TLabel也是TFrameControl的后代,我想在计算器里面使用它,所以我让它具有"sunken box"的外观来显示计算器的存储记忆,就像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的优点,控件已经全部完工了.
所有不得不交待的(All I Have to Give)(????)
你已经看到,写一个交叉平台控件和写一个VCL控件并不是完全不同.如果你是一个控件开发的新手,学习起来不会很难.如果你是一个经验丰富的VCL控件作者,你的大部分知识都将平滑地转移到Kylix上去.
我前面说过,(交叉平台控件)会有一些不同,但是这只会影响那些依赖于Windows API写控件的开发者.如果你写的控件是从VCL继承的,是聚合(aggregate)了一些控件的(就像我在TCalculator里面做的),是一个非可视的不依赖于Windows API的,或者是一个TGraphic控件,那么转移到Linux下不会遇到什么麻烦.
这篇文章介绍的软件产品的功能正在开发中,并且会在没有通知的情况下有些变化.相应功能的描述是应时而变的,并且没有任何确定的服务承诺.
做为Delphi最初测试版的用户,Robert Kozak从1999年下半年加入Borland的Kylix R&D小组.从他加入Borland开始,他就和C++Builder 5和Kylix的开发密切相关.Robert还和TaDDA(多伦多区Delphi开发者协会)有关,这个协会后来和TDUG(多伦多区Delphi用户组)合并.Robert在这些用户社团一直很活跃,同时也经常出现在Borland的新闻组里面.
[列表1]
QCalc.pas
{ ***************************************************** }
{ }
{ Borland Delphi Visual Component Library }
{ Borland Delphi Component Library (X)Crossplatform }
{ }
{ Copyright (c) 2000 Inprise/Borland }
{ }
{ ***************************************************** }
unit QCalc;
// This is the very first Custom control written for CLX.
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] of TButtonRecord;
procedure BuildCalc;
procedure Calc(Sender: TObject);
function GetText : string;
override;
procedure SetText(const Value : string);
override;
public
constructor Create(AOwner: TComponent);
override;
property Value : Single read FCurrentValue;
published
property Text : string read GetText write SetText;
property BorderStyle;
property LineWidth;
property Margin;
property MidLineWidth;
property FrameRect;
end;
implementation
function ButtonRecord(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 }
constructor TCalculator.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
SetBounds(0,0,250,200);
FMemoryValue := 0;
FCurrentValue := 0;
FLastValue := 0;
FRepeatValue := 0;
BorderStyle := bsRaisedPanel;
BuildCalc;
end;
procedure TCalculator.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');
for i := Low(TButtonType) to High(TButtonType)do
with TButton.Create(Self)do
begin
Parent := Self;
SetBounds(Btns.Left, Btns.Top, Btns.Width,
Btns.Height);
Caption := Btns.Caption;
Color := Btns.Color;
OnClick := Calc;
Tag := Ord(i);
end;
FStatus := TLabel.Create(Self);
with FStatusdo
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;
procedure TCalculator.Calc(Sender: TObject);
const
MemoryStoreMap: array [Boolean] of string = (' M','');
var
ButtonType: TButtonType;
Temp: string;
TempValue: Single;
begin
ButtonType := TButtonType(TButton(Sender).Tag);
try
case ButtonType of
bt0..bt9:
begin
FBackSpaceValid := True;
if (FResultEdit.Text = '0') or (FCurrentValue = 0) then
FResultEdit.Text := '';
FResultEdit.Text :=
FResultEdit.Text + Btns[ButtonType].Caption;
FCurrentValue := StrToFloat(FResultEdit.Text);
FRepeatValue := 0;
end;
btDecimal:
if Pos('.', FResultEdit.Text) < 1 then
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:
if FBackSpaceValid then
begin
Temp := FResultEdit.Text;
Delete(Temp, Length(Temp),1);
if Temp = '' then
Temp := '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:
if FState <> csNone then
begin
FBackSpaceValid := False;
FCurrentValue := StrToFloat(FResultEdit.Text);
if FRepeatValue = 0 then
begin
FRepeatValue := FCurrentValue;
FCurrentValue := FLastValue;
end;
FLastValue := FRepeatValue;
case FState of
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;
// case ButtonType of...
except
on E: Exceptiondo
begin
FResultEdit.Text := E.Message;
FLastValue := 0;
FCurrentValue := 0;
FRepeatValue := 0;
FState := csNone;
end;
end;
FStatus.Caption := MemoryStoreMap[FMemoryValue = 0];
end;
function TCalculator.GetText: string;
begin
Result := FResultEdit.Text;
end;
procedure TCalculator.SetText(const Value: string);
begin
FResultEdit.Text := Value;
end;
end.
[返回主页]