Programming Kylix with the CLXDisplay API
CLX, VisualCLX, CLXDisplay and Qt explained.
By Bruno Sonnino
When Delphi was
released, it came with a set of components named the Visual Component Library.
The VCL is based on the Windows API, which it uses extensively. In order to
provide unlimited extensibility, the VCL's architects left a back door open.
Developers could use the Windows API or any C API directly. This widened the application range
and allowed the development of all kinds of Windows applications. As a Delphi
programmer, you take this extensibility for granted.
Now there is Kylix. One of the first obstacles that the Kylix development
team must have faced is that the VCL is based on Win32, so unless you use an emulation library like
Wine you cannot use Windows
API functions. The Kylix team had to rewrite the VCL completely in order to remove all Win32 dependencies.
Thus CLX -- the Component Library for Cross-platform-- was born. The visual
portion of the new VCL was named VisualCLX; it uses the Qt runtime library as its graphics and GUI engine.
The Qt runtime is based on a C++ class library developed by a
Norwegian company named Trolltech.
Qt is available on Windows, Linux, Solaris, and many other platforms. There
is even a light version for Palm computers.
This library has a
special kind of design. It is a very thin layer over the environment. Instead
of using the operating system's native API, it uses only some primitives and draws
everything by itself. That is, it does not use the Button provided by Windows,
for instance -- it draws its own button. This kind of design has several advantages:
- It is very
portable. To convert to another platform, you must rewrite only these primitive drawing functions. If
Qt used a design like the VCL's, on the other hand, all the basic stuff
would have to be rewritten to port to another platform.
- It can be very
fast. The drawing routines can be optimized to get the fastest speed.
- You can have the
appearance you want. Because the controls are drawn by the library, they are not
tied to the operating system. You can have a Linux application with a
Windows appearance if that's what you want. Or vice-versa.
- It is resistant to operating system bugs. The drawing routines are completely rewritten,
so they are not vulnerable to bugs in the OS code. (Of course, the library
can have bugs of its own!)
The Qt approach has some
disadvantages also:
- It does not take
advantage of all the operating system can offer.
- Because Qt controls are created instead of called, some controls do not
work exactly the same way native controls do.
- When a new
control is created or a new version of the GUI is built, the library must be
rewritten to emulate the new visual interface.
Qt and Kylix
When the Kylix team chose Qt as one of the foundations of CLX, it
gained a significant
advantage: portability. The Qt runtime is portable across many platforms, and
this should make it easy for the Kylix team to make VisualCLX portable across
the same platforms. When VisualCLX is available to Windows programmers in Delphi 6, all GUI applications
written in Kylix can be ported easily to Windows. And if Delphi for Solaris, Delphi for Mac, and Delphi for Palm are launched,
your applications will port to these platforms as well.
Like the VCL, CLX has a back door to give programmers extensibility. Kylix allows direct access to CLXDisplay, a C interface to the Qt runtime, and the ability to directly access
any Linux API. CLXDisplay will be portable to other environments.
The relation between
VisualCLX and the Qt runtime is very similar to the relationship between the
VCL and Windows GUI APIs. A lower-level API beneath VisualCLX -- called
CLXDisplay --
was developed to provide a fully programmable interface layer between VisualCLX
and Qt.
Some VisualCLX components are simply very light weight abstractions of Qt
widgets. For example, the CLXDisplay API provides a class named QButton that directly represents a Qt button. VisualCLX encapsulates this
class, creating a component named TButton, and adds the properties, events, and other methods of the component.
Other VisualCLX components, such as the Tab control, are custom classes written in VisualCLX
and CLXDisplay. The Kylix team was very careful
with VisualCLX to emulate VCL behavior, so a Windows developer can easily
port Windows applications to Kylix without traversing a major learning curve.
This was accomplished by adding properties, methods, and events to Qt widgets and custom VisualCLX components to
make them work
just like their VCL counterparts. This component programming was done with
combinations of VisualCLX programming and the CLXDisplay API.
The
Qt library is a set of C++ classes, and it's not entirely compatible with Kylix. To
allow programmers to use
Qt functionality, Borland engineers created CLXDisplay by "flattening" the classes,
transforming them to cdecl functions. You will see that there is no CLXDisplay help file provided in
Kylix, so you must find references in other places. The best reference is Qt
Help itself. It is a set of html files, installed in the Doc directory of the Qt installation. If you
don't find the files on your hard drive, you can download them from TrollTech: http://www.troll.no.
When you are examining
the help files, you will find code like this:
QLabel(QWidget *parent=0, const char *nome=0, WFlags f=0)
This is one of the
constructors of QLabel, the class that represents
a label in a Form. To use it in Kylix, you must make some changes:
- All Qt components
have a handle, a pointer to an instance of the class created. This handle has
the name of the class plus an H. QLabelH, for instance, is a handle to a QLabel.
- All access to Qt
components must be made via this handle.
- The constructor
is translated to a function that returns the handle.
- When using a class
method, the first parameter must be the handle returned by the constructor.
- The method name
is constructed using the class name, followed by an underline character and the
method name. For example, the method setText of the QLabel class is
QLabel_setText.
CLXDisplay's translated
Qt classes and methods are contained in the Qt.pas unit, and this unit must be included in your Uses clause when you are calling into Qt through
CLXDisplay functions. The constructor shown earlier is translated in Qt.pas as:
function QLabel_create(parent: QWidgetH; name: PAnsiChar; f: WFlags): QLabelH; overload; cdecl;
This constructor
returns a QLabelH handle, which can be used with the other QLabel methods.
For example, to set the text of the label, you must use the setText method, declared in Qt
Help as:
virtual void setText(const QString &)
This is the equivalent
of a Pascal procedure that receives a string as a parameter. This method is
translated as:
procedure QLabel_setText(handle: QLabelH; p1: PWideString); cdecl;
As you can see, the
first parameter is the handle of the label and the second parameter is a pointer to a
string. This code changes the label text:.
TxtLbl := 'this is a test';
QLabel_setText(LabelHandle,@Txtlbl);
Qt and CLXDisplay
In the VCL, the
components that encapsulate Windows controls inherit from TWinControl. Each has a property named Handle, the window handle that allows
it to interact with Windows API functions. VisualCLX incorporates a similar concept:
Components that encapsulate Qt
controls inherit from TWidgetControl and have a property named Handle, the handle of the underlying control. TLabel,
for instance, inherits
from TCustomLabel, which is a descendant of TWidgetControl. It has a property Handle,
QLabelH.
If you wish to use the CLXDisplay API to set the text of a CLX
label (this is not a real sample; of course you would simply access
Label1.Text),
you could create some code like this:
procedure TForm1.FormCreate(Sender: TObject);
var
TxtLbl : WideString;
begin
TxtLbl := 'this is a test';
QLabel_setText(Label1.Handle,@Txtlbl);
end;
As you can see, you
can mix VisualCLX with CLXDisplay functions, extending your program in ways
CLX's architects did not anticipate.
Another concept
introduced by Qt and implemented in the CLXDisplay API is the notion of slots and signals.
They are the Qt equivalents of events and event handlers. When a button is clicked, it emits the clicked() signal. This is equivalent to the activation of the
OnClick event when a TButton instance is clicked.
A signal can be
connected to a special kind of method called a slot.
When a slot is connected to a signal, it is executed when the signal is
activated. For example, if we connect the clicked() signal of a QButton
to the quit() slot of QApplication, the application will terminate when the
button is clicked (assuming that's what the quit() slot does). A slot is like an event
handler: When you assign an event handler to an event, the
event handler is executed when the event is fired.
The Qt API includes many predefined slots
and signals. If you want to
create new slots and signals, you can only do so using TrollTechs Qt C++
Framework. This kind of functionality is not provided in Kylix at the
CLXDisplay level. Note that a separate Qt C++ license from TrollTech is required
for proprietary use of the native Qt C++ API.
Creating a custom application using CLXDisplay
We will create a
minimum application using the CLXDisplay API. The Kylix license agreement
requires that if you wish to use the CLXDisplay API in an application to access
custom Qt functionality, this application must contain at least a TApplication instance and a component derived from
TControl. To create an application that uses the
CLXDisplay API without these two components, you must have a developer license from Troll Tech or develop your application under GPL
license terms.
Ready? Let's go.
Start by creating a new console
application, using File | New and selecting Console
Application. This creates
a simple project file with no forms. We will not use visual design to develop this
small application.
We must remove the {$APPTYPE CONSOLE} directive, put the Qt and QForms units in the
Uses clause and include the following code:
Program Project1;
uses
Qt,
QForms,
SysUtils;
var
App : QApplicationH; // QApplication handle
MainWnd : QWidgetH; // Main window
Button : QWidgetH; // Button handle
MainForm : TForm; // Main application form
Caption : WideString;
begin
// initializes application and stores handle in App
Application.Initialize;
App := Application.Handle;
// creates main window
MainForm := TForm.Create(nil);
Caption := 'Minimum CLX Application';
MainWnd := MainForm.Handle;
// set window caption and size
QWidget_setCaption(MainWnd,@Caption);
QWidget_setGeometry(MainWnd, 100,100,200,100);
// creates the button
Caption := 'Close App';
Button := QPushButton_create(@Caption, MainWnd, PChar('Button'));
// sets button size
QWidget_setGeometry(Button,20,20,160,160);
// sets application main widget
QApplication_setMainWidget(App,MainWnd);
// shows widget and enters application loop
QWidget_show(MainWnd);
QApplication_exec(App);
end.
This isn't exactly what you do in Delphi -- nor in Kylix using CLX -- this code is very simple to
understand.
Any C++ Qt application
has a class named QApplication that manages the control flow. The application
must have a main widget that will contain all other widgets. We called this
widget MainWnd.
The first thing to do
is initialize the Application variable. It is declared in QForms.pas and is created
when the application begins to execute. We assign Application.Handle to the variable we have declared,
App, a QApplication handle.
The next step is
to create the main window. To be a VisualCLX application, we must have a component
inherited from TControl. This will be the main Form. We create it
using TForm.Create, passing nil as its owner. To use CLXDisplay functions, we
store the handle -- a QWidgetH -- in the MainWnd variable. To set widget properties, we use the
QWidget_set... methods, passing the desired position, size, and
caption. The window created here will be at position 100,100 and will have a
width of 200 pixels and a height of 100 pixels.
The next step is to
create a button with QPushButton_create. This time, we tell the application that the parent
of the button will be the main form.
Finally, we set the
main widget, show it, and enter the program loop. If you run the program, you
will see it is fully functional. I am sure you will not start building applications
like this one. I developed this one only for educational purposes, to show how
things work internally, when you create a visual application.
Connecting signals to slots
I call this program fully functional, but that may be an exaggeration. If you
run it, you will see that nothing happens when you click the Close App button. That's because we haven't
yet connected a slot
to the button's clicked() signal. To connect a slot to a signal, you use the function QObject_connect,
which is declared like this:
function QObject_connect(sender: QObjectH; signal: PAnsiChar; Receiver: QObjectH; member: PAnsiChar): Boolean; overload;
Sender is the component that emitted the signal and Receiver is the component
whose slot will be executed when the signal is
emitted. Take a look at the signal and member parameter types. You will find that there is something wrong
here. Both
are PAnsiChar parameters, but we want to pass class members.
The first time I saw this in Qt.pas, I thought the conversion was incorrect.
What gives?
If
you take a look at the connect function in Qt help, you will see it is declared like
this:
bool connect (const QObject *sender, const char *signal, const char *member)
A typical C++ call to this function is like
this:
connect(Button,SIGNAL(clicked()),App,SLOT(quit())
So we are
passing a method name to some strange function named SIGNAL, which converts
it into a PChar. This function may be a macro, converted by the C++ preprocessor. If we
take a look in qobjectdefs.h, we can see:
#define SLOT(a) "1"#a
#define SIGNAL(a) "2"#a
What the preprocessor
does here is add a number 1 or 2 to the method name and convert it to a string.
That way, SLOT(quit()) will be translated to 1quit() and SIGNAL(clicked()) will
be transformed into 2clicked().
Knowing this, we can do the same conversion by hand, connecting a signal to a slot. To make the button-click close the application, we
can use code like this:
QObject_connect(Button,PAnsiChar('2clicked()'),App, PAnsiChar('1quit()'));
If you insert this
line before QApplication_setMainWidget and run the program, you will see that
the application closes when you click the button. That is because we connected
the clicked() signal of the button to the quit() slot of QApplication. We can do the same to connect other slots and
signals.
Customized code in slots: hooks
The signal-slot connection does not allow for customized code. You connect
a slot to a signal and that's it.
Let's say we wanted the user to confirm the
exit when he clicks the button. That cannot be done by connecting a slot to a
signal. There must be some other mechanism to do it. If you were using C++, you
could create a customized slot, but we do not have this capability in Kylix.
In Kylix, we must use
the hooks technique. Take a look at this event in Qt.pas:
>QButton_clicked_Event = procedure() of object cdecl;
As our event handler
will handle the clicked() event, it must be a procedure like this. But
we aren't using any classes and this event is a procedure of object. So we must define a dummy
class to handle the event:
type
TDummyClass = class
procedure ButtonClicked; cdecl;
end;
The ButtonClicked method will be the handler that is executed when the user clicks
the button:
procedure TDummyClass.ButtonClicked;
var
Caption, Text, YesText, NoText : WideString;
begin
Caption := 'Close Application';
Text := 'Are you sure you want to quit?';
YesText := 'Yes';
NoText := 'No';
// create messagebox
if QMessageBox_query(@Caption, @Text, @YesText, @NoText, MainWnd,PChar('mb'))then
QApplication_quit(App);
end;
See how it works? This code simply displays a query
messagebox. If the user clicks the Yes button, it closes the application. If
the user clicks the No button, nothing happens.
Now that we have created the event handler, we must
link it to the clicked() event. The first thing to do is to create a
button hook with QButton_hook_create. We pass the button handle as a parameter to
this function, and it returns the button hook, like this:
ButtonHook := QButton_hook_create(Button);
Once we have created the hook
we must link it to the event handler. Because we want to hook the method to the
clicked event, we will use the QButton_hook_hook_clicked procedure. It hooks the clicked event to the
handler we created. The first parameter to this function is the hook we've just
created and the second is a QHookH.
If we take a look at Qt.pas, we'll see this:
QHookH = TMethod;
This means, simply enough, that a QHookH is a TMethod. So we must pass a TMethod
as the second parameter. To convert the handler to a TMethod,
we declare a variable of type TMethod and do this assignment:
QButton_clicked_Event(BtnClicked) := DummyClass.ButtonClicked;
Then we can pass BtnClicked as the second parameter of QButton_hook_hook_clicked.
Removing the line with the QObject_connect function completes the application.
Here's how it looks now:
program SimpleQt;
uses
Qt,
QForms,
SysUtils;
type
TDummyClass = class
procedure ButtonClicked; cdecl;
end;
var
App : QApplicationH; // QApplication handle
MainWnd : QWidgetH; // Main window handle
Button : QWidgetH; // Button handle
MainForm : TForm; // Main application form
Caption : WideString;
DummyClass : TDummyClass;
ButtonHook : QButton_hookH;
BtnClicked : TMethod;
procedure TDummyClass.ButtonClicked;
var
Caption, Text, YesText, NoText : AnsiString;
begin
Caption := 'Close Application';
Text := 'Are you sure you want to quit?';
YesText := 'Yes';
NoText := 'No';
// creates messagebox
if QMessageBox_query(@Caption, @Text, @YesText, @NoText, MainWnd, PChar('mb')) then
QApplication_quit(App);
end;
begin
DummyClass := TDummyClass.Create;
// initializes application and stores handle in App
Application.Initialize;
App := Application.Handle;
// creates main window
MainForm := TForm.Create(nil);
Caption := 'Minimum CLX Application';
MainWnd := MainForm.Handle;
// set window caption and size
QWidget_setCaption(MainWnd,@Caption);
QWidget_setGeometry(MainWnd,100,100,200,100);
// creates the button
Caption := 'Close App';
Button := QPushButton_create(@Caption, MainWnd, PChar('Button'));
// sets button size
QWidget_setGeometry(Button,20,20,160,160);
// creates the button hook
ButtonHook := QButton_hook_create(Button);
// convert the method to a TMethod
QButton_clicked_Event(BtnClicked) := DummyClass.ButtonClicked;
// link the method to the clicked event of the button
QButton_hook_hook_clicked(ButtonHook, BtnClicked);
// sets application main widget
QApplication_setMainWidget(App,MainWnd);
// shows widget and enters application loop
QWidget_show(MainWnd);
QApplication_exec(App);
DummyClass.Free;
end.
Run the application
and you will see that it performs as expected.
Wrapping up
This application was
created to show what is happening internally in a Kylix application. I have
shown how to create a minimal VisualCLX application and extend it using the low-level CLXDisplay API. There was no visual development
here -- all
controls were built at runtime, as in any traditional language.
As you can see, Kylix lets you access the full power of Linux and Qt so
you can build complete applications using the best IDE available for Linux. And when Delphi 6 is available, this code will be portable between Linux
and Windows.
I would like to thank Michael Swindell, Borland's director of product
management for RAD tools,
for suggestions and corrections he made during the preparation of this article.