Kylix预览
Cross-platform Controls
From Windows to Linux, and Back
These are exciting times for Borland. Not since the first whisper of Delphi has there been this much excitement about a Borland product. I'm talking, of course, about Kylix, the project to bring C Builder and Delphi to the Linux operating system. The Delphi version will be available first, so for the rest of this article, Kylix refers to Delphi for Linux.
We're developing a new VCL that will work with the Windows and Linux versions of Delphi. This means you can write an application in Windows, then move the source to a Linux box and recompile it - or vice versa. This new VCL is named CLX, for Component Library Cross-Platform. CLX encompasses the entire cross-platform library distributed with Kylix. There are a few sub-categories, which, as of this writing, break down as follows:
- BaseCLX is the RTL, up to, and including, Classes.pas.
- VisualCLX includes the user interface classes, i.e. the usual controls.
- DataCLX comprises the cross-platform database components.
- NetCLX includes the Internet stuff, e.g. Apache, etc.
At the time of this writing [early May 2000], the first Field Test for Kylix is just beginning. By the time you read this, there will be a big difference between the Kylix I'm using and working width=330>
Figure 2: The differences in the class hierarchy are subtle.
The CLX prefix of "Q" may or may not end up as the permanent prefix in the final release. TWinControl is now a TWidgetControl, but to ease the pain we added a TWinControl alias to TWidgetControl. TWidgetControl and descendants all have a Handle property that is an opaque reference to the Qt object; and a Hooks property, which is a reference to the hook objects that handle the event mechanism. (Hooks are part of a complex topic that is outside the scope of this article.)
OwnerDraw will be replaced with a new idea called Styles. Styles are basically a mechanism whereby a widget or application can take>OnMouseDown, OnMouseMove, OnClick, etc., are still there.
Show Me the Meaning ...
Let's move>TWidgetControl has CreateWidget, InitWidget, and HookEvents virtual methods that almost always have to be overridden. CreateWidget creates the Qt widget, and assigns the Handle to the FHandle private field variable. InitWidget gets called after the widget is constructed, and the Handle is valid. Some of your property assignments will move from the Create constructor to InitWidget. This will allow delayed construction of the Qt object until it's really needed. For example, say you have a property named Color. In SetColor, you can check with HandleAllocated to see if you have a Qt handle. If the Handle is allocated, you can make the proper call to Qt to set the color. If not, you can store the value in a private field variable, and, in InitWidget, you set the property.
There are two types of events: Widget and System. HookEvents is a virtual method that hooks the CLX controls event methods to a special hook object that communicates with the Qt object. (At least that's how I like to look at it.) The hook object is really just a set of method pointers. System events now go through EventHandler, which is basically a replacement for WndProc.
Larger Than Life
All of this is just background information, because you really don't need to know it in order to write cross-platform custom controls. It helps, but with CLX, writing cross-platform controls is a snap. Just as you didn't have to understand the complexities of the Windows API to write a VCL control, the same goes for CLX and Qt. Listing>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);
// Don'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.
Figure 3: The project file for the CLX calculator control.
Figure 4: The calculator in Windows at run time.
Figure 5: The control at run time, as it appears>TFrameControl is a new control introduced to the hierarchy under TWidgetControl that provides a frame for your controls. The property we're most interested in is BorderStyle:
TBorderStyle = (bsNone, bsSingle, bsDouble, bsRaisedPanel,bsSunkenPanel,
bsRaised3d, bsSUnken3d, bs Etched, bsEmbossed);
There are two important methods in this control. BuildCalc creates all of the buttons, and places them in their proper locations. As you can see, I used an enumerator named TButtonType to hold the "function" of the button, and this tidbit of information is stored as an integer in the Tag property. I refer to this later in the Calc method. All of the calculator buttons are stored in a protected array of TButtonRecord records named Btns:
TButtonRecord = record
Top: Integer;
Left: Integer;
Width: Integer;
Height: Integer;
Caption: string;
Color: TColor;
end;
This makes it easy to set up all of the buttons in a loop, rather than using an ugly bunch of TButton.Create calls. Notice that the buttons' OnClick handlers get assigned to the TCalculator's Calc method. It's alright to do a direct assignment to what is typically a user event, because all of these buttons are internal to the calculator, and these events won't be published (see Figure 6).
for i := Low(TButtonType) to High(TButtonType) do
with TButton.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;
>Edit control.
The other main method is Calc, which is essentially a huge case statement that evaluates which button was pushed, and decides what to do about it. I use the private field variables FCurrentValue, FLastValue, and FRepeatValue to handle the value of the calculations, so I don't have to implement a stack. The idea was to show how to create a cross-platform control, not how to write a calculator.
Oh yeah! Remember, that I used the Tag property in BuildCalc to hold its function? That's retrieved in this method by casting the Sender to a TButton, and casting the Tag back to a TButtonType. ButtonType is the selector expression of the case statement:
ButtonType := TButtonType(TButton(Sender).Tag);
Are you wondering how we convert this to a cross-platform control? No? Good! That means you've been paying attention. This code will compile in Windows and Linux with absolutely no changes. There are no extra steps involved. Just by the virtue of using CLX, this control is ready to go.
All I Have to Give
As you can see, writing a cross-platform control isn't all that different from writing a VCL component. If you're a new component developer, it won't be difficult to learn. If you're an experienced VCL component builder, most of your knowledge will transfer to Kylix nicely.
As I said earlier, there are a lot of differences, but that should>TGraphic control, then you shouldn't have much trouble porting it to Linux.
This article describes features of software products that are in development and subject to change without notice. Description of such features here is speculative and does not constitute a binding contract or commitment of service.
The files referenced in this article are available for download.
Involved as a user of Delphi since the initial beta, Robert Kozak is a member of the Kylix R&D team and has been with Borland since the later half of 1999. Since he joined Borland, he has been involved in the development of C Builder 5 and Kylix. Robert was involved with the start of TaDDA! (Toronto Area Delphi Developers Association), which later merged with TDUG (Toronto Delphi Users Group). Robert continues to stay active in the user community, and is active>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[i].Left, Btns[i].Top, Btns[i].Width,
Btns[i].Height);
Caption := Btns[i].Caption;
Color := Btns[i].Color;
>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
>string);
begin
FResultEdit.Text := Value;
end;
end.
- 最新文章
- DELPHI中回调函数的使用[01-04]
- Delphi点滴[01-04]
- 简单的在线升级的实现方法[01-04]
- Delphi使用VB编写的ActiveX控件全攻略[01-04]
- Delphi开发经验四则[01-04]
- 使用Delphi获取系列信息[01-04]
- 相关文章
