by Nikolai Sklobovsky,
Senior System Analyst,
Retail Technologies International, Inc.
In this article I tried to summarize my first impressions about fascinating new Borland product, Kylix. My primary target of investigation was to understand what makes Kylix tick. I was not going to re-explore many wonderful Delphi features such as component dropping, two-way programming and other amenities assuming that a reader have sufficient knowledge of Delphi IDE already. My interest was lying deeper - under the hood.
Of course it was not possible to cover all the new features of this extensive product. I barely managed to scratch the top of the iceberg. Hopefully, it was a scratch deep enough to make it interesting to look at.
Caveat: all information is based on pre-release version of Kylix and therefore is subject to change in the final version. Also, please bear in mind that at the time I wrote this article there was no "What's New" section whatsoever, so many of the features described here might be outlined in the online help and easily accessible for every Kylix user, which, unfortunately, was not my case.
back to the topThis is how this new language feature is called in the online help. Readers who are familiar with C++ find nothing new in it, but for the rest of us - now you have a convenience of both const and enum in one bottle. It means that in Kylix (and, hopefully, Delphi 6) you can explicitly assign values to the members of enumerated type. Simple example of this feature would be:
type
TNewEnum = (
neOne = 3, // 3
neTwo = 10, // 10
neThree = neOne + neTwo, // 10 + 3 = 13
neFour = neOne + 2, // 3 + 2 = 5
neFive, // 6, as a succ(neFour)
neSix = 33, // 33
neSeven = neOne - 1 // 3 - 1 = 2
neEight = neSeven // 2
); // TNewEnum
const // these const only used for convenience
MinNewEnum = low(TNewEnum); // 2
MaxNewEnum = high(TNewEnum); // 33
|
Basically, in this case we deal with enumerated type of 2..33, some members of which have additional mnemonical identifiers. It's very close (but not identical) to the following conventional Delphi declaration:
const MinNewEnum = 2; MaxNewEnum = 33; type TNewEnum = MinNewEnum .. MaxNewEnum; const neOne = 3; neTwo = 10; neThree = neOne + neTwo; neFour = neOne + 2; neFive = succ(neFour); neSix = 33; neSeven = neOne - 1; neEight = neSeven; (* or, if you need more rigid typization (D1..D5 do not allow usage of typed const on the right-hand part in this case): neOne: TNewEnum = 3; neTwo: TNewEnum = 10; neThree: TNewEnum = 3 + 10; neFour: TNewEnum = 3 + 2; neFive: TNewEnum = succ(5); neSix: TNewEnum = 33; neSeven: TNewEnum = 3 - 1; neEight: TNewEnum = 2; *) |
Listing 2. Delphi analogue for new enumerated type
As you can see, now you can have best of both worlds an a simple, elegant and efficient manner, but this comes at a price. The consequences of this fact are:
The last statement is more than explainable - sometimes you may simply not have a name for a value or you may have more than one.
There is a new suite of compile-time goodies associated with the new IF directive, its generic syntax being:
{$IF expression}
// ...
{$ELSEIF expression}
// ...
{$ELSEIF expression}
//...
... {$ELSE}
// ..
{$IFEND}
|
Listing 3. Example of new IF compiler directive
Note, that this new IF directive should be also closed with new {$IFEND} clause rather than with existing {$ENDIF} one, which can be used only in conjunction with obsolete {$IFDEF}/{$IFNDEF} syntax. I said "obsolete" and I mean it: along with new directive come two new functions and new expression syntax. The functions are Defined() and Declared() and can be used only within {$IF}.. {$IFEND} block. According to Kylix help, Defined() returns TRUE if its argument is a defined conditional symbol, whose existence you can control via {$DEFINE}/{$UNDEF} statements. Declared() returns TRUE if its argument is a valid declared Pascal identifier visible within the current scope.
Expression syntax complies with Delphi standard constant boolean expression which can be resolved at compile time. You can't use typed constants here, but usage of logical operations and whole bunch of functions is now possible. The complete list of functions is defined in your Delphi/Kylix online help under constant expressions topic and consists of
Abs, Chr, Hi, High, Length, Lo, Low, Odd, Ord, Pred, Round, SizeOf, Succ, Swap, Trunc |
Listing 4. Compile-time functions.
Next thing which came to my mind after I had compiled few simple test applications was whether it would be possible to "port" my ActivityLog code (if you missed two original papers, "Waking from Threadmare" and "Childern of Threadmare", you can always pick them up at Delphizine site or from my Community page). Briefly, this component takes care of queuing user "messages" coming from different threads and providing convenient way of processing them in the main thread without having serious problems with Synchronize, OnTerminate, etc. It also provides support for non-Delphi threads, i.e. native Win32 threads that are not based on TThread class.
The project went surprisingly well. The only essential problem I stumbled on was the absence of Win32 WaitForSingleObject function, which I used in a couple of places to detect or wait for thread termination. I called WaitForSingleObject with two different values of its Timeout parameter, namely INIFINITE (-1) and 0. In first occasion I needed the thread to be terminated before I could proceed, while in the second one I only needed to check whether or not it's done - without necessarily waiting for it. While neither Kylix, Qt, nor even Linux itself seem to provide a functionality similar to WaitForSingleObject in one method, the workaround turned out to be pretty simple.
In Kylix' Libc.pas interface unit you can find the whole bunch of routines named pthread_xxx, some comments from source files giving an idea that "P" stands for "POSIX" (another possibility could be "portable") To wait for the thread termination you can simply use pthread_join. The other functionality, i.e. checking on thread, didn't seem to have a direct analogue in Linux. So I came to the idea of using pthread_kill with 0 as signal identifier. This scary-named function normally used to send a thread some "signal" information. But in case of zero it only checks thread's "validity" - which was exactly my point!
As a free bonus for getting rid of WaitOfSingleObject I could also remove all the code related to obtaining, storing and releasing additional thread's handle, which I kept in FThreadHandle field. It was only necessary to have one to work with this function, so now I simply didn't need it.
Amazingly, the whole thing started working the next moment I get it compiled.
Few more issues I discovered while digging sources and looking for Linux thread-related information:
Other than that, threads were looking great, and ActivityLog was providing the same excellent service it did in Win32 world.
Having had some relatively easy fun with multithreading, I decided to check how Kylix supports what in the ancient times was called "subclassing". In Windows you had to intercept application message loop and eventually replace particular window's window procedure with your own. Delphi also provided you with its own WndProc wrapper for all TWinControl-based components, as well as with OnMessage event and message keyword, which provided you with an easy access to individual message handlers. As far as Kylix works in completely different environment, you'd be right to suspect that something has to be changed.
My first target was to see what happened to application's message loop and ubiquitous calls to SendMessage/PostMessage. I was rather pleased to find out that underlying Qt library provides a lot of methods named QApplication_xxx, some of them doing exactly what I needed. The primary difference with Windows world lies in the fact that instead of messages Qt (and therefore Kylix) operates with events. From Qt standpoint each event is represented by merely a handle. Kylix wraps these handles into class hierarchy to provide run-time type detection and benefit from its OOP capabilities. Using some hints from the Kylix source code, soon I was able to create two methods I needed. Actually, I created even two couples, one working with Qt-native QObjectH and another working with Kylix-friendly TWidgetControl, some analogue of former TWinControl:
interface procedure PostEvent(TargetH: QObjectH; EventType: QEventType; Sender: TObject = nil); overload; procedure PostEvent(Ctrl: TWidgetControl; EventType: QEventType; Sender: TObject = nil); overload; function SendEvent(TargetH: QObjectH; EventType: QEventType; Sender: TObject = nil): boolean; overload; function SendEvent(Ctrl: TWidgetControl; EventType: QEventType; Sender: TObject = nil): boolean; overload; implementation procedure PostEvent(TargetH: QObjectH; EventType: QEventType; Sender: TObject); register; begin QApplication_postEvent(TargetH, QCustomEvent_create(EventType, Sender)); end; // PostEvent procedure PostEvent(Ctrl: TWidgetControl; EventType: QEventType; Sender: TObject); register; begin QApplication_postEvent(Ctrl.Handle, QCustomEvent_create(EventType, Sender)); end; // PostEvent function SendEvent(TargetH: QObjectH; EventType: QEventType; Sender: TObject): boolean; register; begin Result := QApplication_sendEvent(TargetH, QCustomEvent_create(EventType, Sender)); end; // SendEvent function SendEvent(Ctrl: TWidgetControl; EventType: QEventType; Sender: TObject): boolean; register; begin Result := QApplication_sendEvent(Ctrl.Handle, QCustomEvent_create(EventType, Sender)); end; // SendEvent |
Listing 5. SendEvent and PostEvent methods.
Next question here was to create my own type of event. Again, with a little help of Kylix source code, this task was successfully solved: all you need to do is to define a proper constant like this:
const QEventType_MyFirstEvent = QEventType(integer(QEventType_ClxUser) + 1); QEventType_MyAnotherEvent = QEventType(integer(QEventType_ClxUser) + 2); // etc. |
Listing 6. Example of custom event declaration
Actually, you don't even have to use QEventType - from Qt stand point, all events ids are simply integers. As in Windows, some lower values of this type are reserved, so if you don't mean or want any trouble it's recommended to start your own events with QEventType_ClxUser. Complete list of reserved event types is surprisingly short and so forth can be easily quoted for illustration purposes:
type
QEventType = (
QEventType_None = 0 { $0 },
QEventType_Timer = 1 { $1 },
QEventType_MouseButtonPress = 2 { $2 },
QEventType_MouseButtonRelease = 3 { $3 },
QEventType_MouseButtonDblClick = 4 { $4 },
QEventType_MouseMove = 5 { $5 },
QEventType_KeyPress = 6 { $6 },
QEventType_KeyRelease = 7 { $7 },
QEventType_FocusIn = 8 { $8 },
QEventType_FocusOut = 9 { $9 },
QEventType_Enter = 10 { $a },
QEventType_Leave = 11 { $b },
QEventType_Paint = 12 { $c },
QEventType_Move = 13 { $d },
QEventType_Resize = 14 { $e },
QEventType_Create = 15 { $f },
QEventType_Destroy = 16 { $10 },
QEventType_Show = 17 { $11 },
QEventType_Hide = 18 { $12 },
QEventType_Close = 19 { $13 },
QEventType_Quit = 20 { $14 },
QEventType_Reparent = 21 { $15 },
QEventType_ShowMinimized = 22 { $16 },
QEventType_ShowNormal = 23 { $17 },
QEventType_WindowActivate = 24 { $18 },
QEventType_WindowDeactivate = 25 { $19 },
QEventType_ShowToParent = 26 { $1a },
QEventType_HideToParent = 27 { $1b },
QEventType_ShowMaximized = 28 { $1c },
QEventType_Accel = 30 { $1e },
QEventType_Wheel = 31 { $1f },
QEventType_AccelAvailable = 32 { $20 },
QEventType_CaptionChange = 33 { $21 },
QEventType_IconChange = 34 { $22 },
QEventType_ParentFontChange = 35 { $23 },
QEventType_ApplicationFontChange = 36 { $24 },
QEventType_ParentPaletteChange = 37 { $25 },
QEventType_ApplicationPaletteChange = 38 { $26 },
QEventType_Clipboard = 40 { $28 },
QEventType_Speech = 42 { $2a },
QEventType_SockAct = 50 { $32 },
QEventType_AccelOverride = 51 { $33 },
QEventType_DragEnter = 60 { $3c },
QEventType_DragMove = 61 { $3d },
QEventType_DragLeave = 62 { $3e },
QEventType_Drop = 63 { $3f },
QEventType_DragResponse = 64 { $40 },
QEventType_ChildInserted = 70 { $46 },
QEventType_ChildRemoved = 71 { $47 },
QEventType_LayoutHint = 72 { $48 },
QEventType_ShowWindowRequest = 73 { $49 },
QEventType_ActivateControl = 80 { $50 },
QEventType_DeactivateControl = 81 { $51 },
QEventType_User = 1000 { $3e8 });
const QEventType_ClxBase = QEventType_User; QEventType_ClxUser = QEventType(Integer(QEventType_ClxBase) + $250); |
Listing 7. QEventType from Qt.pas
Curiously enough, the QEventType is declared as a new, explicitly assigned enumeration type. This allows you to successfully create and typecast your own new values of this type, which under normal Delphi circumstances would cause range check error. But, as we know now, such types do not carry RTTI, so this would work fine, bugging you sometimes with compiler warnings (which you can shut off with {$WARNING OFF} directive).
To accomplish the task of event handling I had to learn how to intercept a certain event. As in Delphi/Windows, this task had to be solved differently for Kylix-based visual components and bare-iron Qt widgets.
In Kylix, as always with Delphi, the task is simple. All you have to do is to override form's protected EventFilter virtual function. Example could be as simple as follows:
... protected
function EventFilter(Sender: QObjectH; Event: QEventH): boolean; override;
... function TForm1.EventFilter(Sender: QObjectH; Event: QEventH): boolean; begin Result := TRUE; // means 'event processed' case QEvent_type(Event) of // this function converts Event to "integer" QEventType_MyFirstEvent: do something; QEventType_MyAnotherEvent: do something else; else Result := inherited EventFilter(Sender, Event); end; // case end; |
Listing 8. Overriding form's EventFilter.
This function returns TRUE if event is processed, otherwise event would flow till the end of the hooks chain (c.f. below). To obtain event ID from the event you need to use QEvent_type function.
Obviously, you have to utilize the same technique if you want to inherit some TWidgetControl-based component and would like to have a priority of event handling.
Everything was really nice and smooth so far. The question remained, however, how could we intercept an event sent or posted to the object whose EventFilter method is unavailable or merely does not exist (e.g. this is not a Kylix object). In Delphi/Windows, we'd ask for Windows procedure. In Kylix/Qt, we set the hook.
Hooks reside in the very foundation of the Qt programming system. You can create and show a widget, but if you want its response you have to establish the hook. The difference between Windows and Qt, however, is that you can establish multiple hooks for the same object. They are internally called in LIFO (stack) order and unless any of their event filters returns TRUE are enumerated from last to first, giving every hook a chance to respond to the event.
To establish the hook, you have to create a hook object first and then assign this hook along with its event filter to the object whose events you'd like to intercept.
Let's consider following simple example of hooking TMemo widget:
...
private
{ Private declarations }
FMemoHook: THandle;
procedure HookMemo;
procedure UnhookMemo;
function MemoEventFilterHook(Sender: QObjectH; Event: QEventH): Boolean; cdecl;
... function SetHook(Ctrl: TWidgetControl; EventFilter: TEventFilterMethod): THandle; procedure ClearHook(var Hook: THandle); ... function SetHook(Ctrl: TWidgetControl; EventFilter: TEventFilterMethod): THandle; var Method: TMethod; begin Result := 0; if not Assigned(Ctrl) or not Assigned(EventFilter) then EXIT; TEventFilterMethod(Method) := EventFilter; QWidget_hookH(Result) := QWidget_hook_create(Ctrl.Handle); Qt_hook_hook_events(QWidget_hookH(Result), Method); end; // SetHook procedure ClearHook(var Hook: THandle); begin if Hook = 0 then EXIT; Qt_hook_hook_destroy(QWidget_hookH(Hook)); Hook := 0; end; // ClearHook ... procedure TForm1.HookMemo;
begin
if FMemoHook <> 0 then EXIT;
FMemoHook := SetHook(Memo1, MemoEventFilterHook);
end;
procedure TForm1.UnhookMemo;
begin
ClearHook(FMemoHook);
end;
function TForm1.MemoEventFilterHook(Sender: QObjectH; Event: QEventH): Boolean;
begin
Result := chkEventHookResult.Checked;
if chkLogEvents.Checked then
Memo2.Lines.Add(Format('Event ID #%d', [integer(QEvent_type(Event))]));
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
UnhookMemo;
end;
|
Listing 9. Hooking example.
Here we can call HookMemo first. It checks that hook does not exist yet and then calls our generic SetHook function, which creates hook object by calling QWidget_hook_create. There are different hook types, we use this one simply as the most appropriate. Then we set Method variable to our event filter MemoEventFilterHook and incorporate it into existing widget hook chain by calling Qt_hook_hook_events.
Our hook event filter looks surprisingly similar to previously discussed EventFilter method (guess why:-). The only notable and important difference is cdecl keyword in its declaration: as far as we're going to work directly with Qt, we have to follow its C-based rules. Other that that, we have same boolean Result, which tells Qt event handling mechanism whether to continue processing this event by other hooks or not. In our case, we introduced two check boxes, whose state allow us to control hook behaviour.
When we don't need our hook, we can destroy it by calling appropriate xxx_hook_destroy method, QWidget_hook_destroy in our case. Simply to be on a safe side, we also call UnhookMemo in form's OnDestroy method. As a convenience feature, we provide ClearHook method that operates with generic THandle rather than with Qt low-level QWidget_hookH.
We know now that Qt (and hence Kylix) does not operate with Windows-style messages, replacing them with event-based technology. So why Kylix keeps the word message highlighted as if it is still a keyword? What does it do now, in the message-less environment?
It turned out that this technique is still alive. I don't know why Kylix team decided to keep it - probably, because it was a convenient, compiler-supported way to react on individual messages without getting into the mess with hooks as well as providing a backward compatibility for the thousands and thousands lines of original Delphi legacy controls. But the point is - this message processing system is still at large. The only difference is that messages are not generated "automatically" by the underlying system. Instead, a programmer can generate them at will, and Kylix compiler would provide a familiar (and the only) way to intercept them. Let's recall, that in Delphi/Windows messages were either generated by the operating system (e.g. mouse/keyboard messages) or generated by the application. Respectively, you could:
In Kylix we only have the last option. At this point I don't have enough information to explain what exactly is compiler doing in this case, it only seems that it generates a dynamic virtual method with a specific index. The only way for a programmer to send a message is to declare a variable of arbitrary record type whose first field is an integer (used to store message ID), set this field to a desired Message ID and then call TObject.Dispatch method, which now becomes a replacement for Delphi's Perform method. There is another method, Broadcast, declared at TWidgetControl level, which simply activates every control's (its from Controls collection) Dispatch method assuming that message record second field is a boolean Handled. Unfortunately, at this moment Kylix does not provide a method which provides both Dispatch (ask control itself) and Broadcast (ask its "children") functionality, so I decided to write it myself:
interface const
PM_BASE = CM_BASE - 1; // ids below CM_BASE are OK to use
type
TPrivateMessage = record
ID: cardinal;
Handled: boolean;
pData: pointer;
end; // TPrivateMessage
function SendBroadcastMessage(Target: TObject; MsgID: cardinal; pData: pointer = nil): boolean;
implementation
function SendBroadcastMessage(Target: TObject; MsgID: cardinal; pData: pointer = nil): boolean;
var
Msg: TPrivateMessage;
begin
Result := FALSE;
if Target = nil then EXIT;
Msg.ID := MsgID;
Msg.Handled := FALSE;
Msg.pData := pData;
Target.Dispatch(Msg);
if not Msg.Handled then // control didn't process it itself
if Target is TWidgetControl then
TWidgetControl(Target).Broadcast(Msg);
Result := Msg.Handled;
end; // SendBroadcastMessage
|
Listing 10. SendBroadcastMessage function.
The interesting point here is that unlike Windows message IDs, which were usually safe to use starting with WM_USER and up, in Kylix you can only use message IDs below $C000. Any attempt to use any bigger value results in compiler's "Illegal message method index" error. Taking into account that Kylix uses few entries for itself, it is only safe to use message IDs which are LESS than a certain value, namely CM_BASE = $B000. So instead of staring from WM_USER and going up, in Kulix we start from CM_BASE and go down.
Below is an example of using this technique:
interface
const
PM_TEST = PM_BASE - 1;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
procedure PmTest(var Msg: TPrivateMessage); message PM_TEST;
public
{ Public declarations }
end;
implementation
procedure TForm1.PmTest(var Msg: TPrivateMessage);
begin
Beep;
Msg.Handled := TRUE;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
SendBroadcastMessage(self, PM_TEST);
end;
|
Listing 11. Example of message usage.
As you can see, this technique does not require more complicated hooking or event-based programming and works well - as long as you don't need true asynchronous reaction (which can be still simulated with timer:-) or intercepting the actual events - let's recall that now this is only more or less convenient programming way to inform a bunch of controls about something, not a reaction to actual action, like painting or key pressing. If your original code relied on interception of your own messages the only thing you'd have to do is to change their IDs to match Kylix limitations. However, if you used this technique to react on WM_PAINT, WM_SYSCOMMAND and other Windows generated messages you'd have to rewrite this portion your code and move it to Event Filter or set your own hook.
In this article we took a first look into Kylix. We discussed some new language features, multithreading support and new event-based way of programming. Hopefully it makes your first steps in this new wild world easier.
Happy Kylixing!
Nikolai Sklobovsky,
kylixfirstlook@sklobovsky.com
|
You are Visitor No:
|