Author: Yorai Aminov
Tips for Converting VCL Components to VCL.NET
Answer:
Delphi 8http://www.borland.com/delphi_net/ (actually, Borland® Delphi™ 8 for the
Microsoft® .NET Framework) introduces Delphi developers to the .NET world (and .NET
developers to Delphi). Delphi 8 allows developers to create native .NET
applications using any of the .NET framework's classes, including the standard
Windows Forms user interface controls and designers. It also provides a migration
path for existing applications and components using the new VCL.NET framework.
Using VCL.NET, many existing Delphi applications will port to .NET with little or
no modifications.
The story is a little different for components. Components tend to be "closer to
the metal", invoking system calls, managing memory and object lifetime, in
generally doing things we want to hide from normal application code. Component need
to be aware of the platform's architecture, and may require substantial work to
function properly or even compile in the new version.
In this article I'll show some of the conversion issues I've encountered while
working on a VCL.NET version of some of my components. Some may be obvious, while
other are more subtle and can be the source of hard-to-find bugs.
Objects and Pointers
Let's start with the big one: managed
codehttp://msdn.microsoft.com/library/en-us/netstart/html/cpglom.asp doesn't
support pointers. Pointers allow unsupervised memory access, and are therefore
considered unsafe. A lot of Delphi components rely heavily on pointers, and make
implicit or explicit assumptions about their content and structure. This has quite
a few implications for the conversion process.
Use TObject instead of Pointer
In Delphi for Win32 (and Kylix, for that matter), object references are simply
pointers. When declaring an object-type variable, you're actually declaring a
pointer variable. You can typecast your objects to pointers and vice-versa. Because
of this, pointer variables and properties are widely used to store object
references. Since unmanaged code no longer supports pointers, the first step in
converting a component to VCL.NET is to search your code for pointers and change
them to something else.
In most cases, you can safely replace pointers with object references. Remember
that in .NET, everything is an object. However, there may be cases where you may
need to refactor your code.
No more Integer typecasts
In 32-bit unmanaged code, a pointer is simply a 32-bit value. This means the
Integer, Pointer, and TObject types are all interchangeable. Components often use
this type compatibility, especially when using external code that expects a
specific type. For example, consider the Win32 EnumWindows function:
function EnumWindows(lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall;
The function expects two parameters: a callback function and a second parameter, of
type LPARAM, that will be passed back as a parameters to the callback function.
LPARAM is defined as:
type
LPARAM = Longint;
A common usage of the lParam parameter is to hold an object reference. The callback
function is written as a stub invoking a method of that object. Such code will have
to be rewritten or marked as unsafe in Delphi 8.
The case of TList
A special case for integer typecasting is the TList class, one of the most widely
used classes in the VCL. TList is a generic container. In Win32, TList holds
pointers, since they can be easily typecast to both integers and objects. In Delphi
8, TList holds TObject references.
PChars are Gone
Like all pointers, PChars are no longer supported. Code that used character
buffers, called Win32 API functions requiring strings, or passed character strings
to external DLLs will no longer work.
API functions the required PChars in Win32 now accept standard Delphi strings.
Functions that returned data into character buffers now accept StringBuilder
objects.
Memory Allocation
Since pointers are gone, code that allocates and deallocates memory for records and
buffers is no longer valid. Search for calls to GetMem, FreeMem, New, and Dispose -
they should all go away.
Message Handlers
One type of code that is heavily used in components but rarely in applications is
message handling. Visual controls handle Windows messages and internal VCL messages
using message functions. VCL.NET allows you to use your existing message handling
code, but introduces some quirks.
Message Types
Message handlers are invoked by TObject's Dispatch method. Visual controls call
Dispatch in their WndProc method, passing a TMessage record as Dispatch's only
parameters. The Dispatch method can accept any parameter type, but assumes the
first two bytes of the referenced parameter contain the message ID.
In unmanaged Delphi code, the type of the message parameter doesn't really matter.
Since WndProc calls Dispatch with a TMessage parameter, as long as you're using a
compatible record you'll be fine. Not so in Delphi 8. Consider the following code:
1 type
2 TForm1 = class(TForm)
3 private
4 procedure WMNCPaint(var message: TMessage); message WM_NCPAINT;
5 end;
6
7 {...}
8
9 procedure TForm1.WMNCPaint(var message: TMessage);
10 begin
11 inherited;
12 end;
This code compiles just fine on any version of Delphi. When compiled to a Win32
application, the code runs just fine. It doesn't really do anything with the
WM_NCPAINT message, so nothing should go wrong. In Delphi 8, however, running the
application produces a System.NullReferenceException exception. Since the exception
is raised deep in Delphi's RTL, it is almost impossible to debug. The solution is
deceptively simple, though:
13 type
14 TForm1 = class(TForm)
15 private
16 procedure WMNCPaint(var message: TWMNCPaint); message WM_NCPAINT;
17 end;
18
19 {...}
20
21 procedure TForm1.WMNCPaint(var message: TWMNCPaint);
22 begin
23 inherited;
24 end;
This code works in any version of Delphi, including Delphi 8. All we had to do is
change the message record type to TWMNCPaint. A little annoying, but fairly easy -
once you know about it.
A bit more annoying is the fact that this behavior doesn't affect every message,
just some of them. A message handle for WM_NCACTIVATE, for example, is perfectly
happy accepting a TMessage record, or in fact any other message record, such as
TWMNCPaint. Try it.
Once again, the answer is obvious once you already know it: WM_NCPAINT is already
handled in one of TForm's (or any other visual control's) ancestors - TWinControl,
where it expects a TWMNCPaint parameter. If your component handles a message that
is also handled by an ancestor, and calls the inherited handle, you must use the
same message record type.
Object References
Windows messages contain data as two integers, historically names wParam and
lParam. These are often used as pointers to more complex data structures. Since
normal .NET applications don't use pointers, VCL.NET has to perform some
behind-the-scenes magic to convert references to integers and manage their
lifetime. One side-effect of this magic is that the information passed by certain
messages requires additional handling. Let's take a look at the CM_HINTSHOW
message, send by the VCL before displaying a hint:
25 type
26 TForm1 = class(TForm)
27 private
28 procedure CMHintShow(var message: TCMHintShow); message CM_HINTSHOW;
29 end;
30
31 {...}
32
33 procedure TForm1.CMHintShow(var message: TCMHintShow);
34 begin
35 inherited
36 message.HintInfo.HintStr := 'This is my hint';
37 end;
This code, which works great in Win32, doesn't even compile in Delphi 8. In Delphi
8, TCMHintShow is an object, and HintInfo is a property of that object. The setter
method for the HintInfo property can only take an existing HintInfo reference, so
you can't simply assign values to HintInfo's members. You have to use a separate
reference variable:
38 procedure TForm1.CMHintShow(var message: TCMHintShow);
39 var
40 HintInfo: THintInfo;
41 begin
42 inherited;
43 HintInfo := message.HintInfo;
44 HintInfo.HintStr := 'This is my hint';
45 end;
This code compiles, but still doesn't work. A little more tweaking, and we get:
46 procedure TForm1.CMHintShow(var message: TCMHintShow);
47 var
48 HintInfo: THintInfo;
49 begin
50 inherited;
51 HintInfo := message.HintInfo;
52 HintInfo.HintStr := 'This is my hint';
53 message.HintInfo := HintInfo;
54 end;
We need to explicitly set the HintInfo property to copy the modified data to a
record VCL.NET can pass around using messages. This is what the last line does.
FCL Types
The .NET Framework Class Library (FCL) contains many types that are similar to
Delphi's standard types. In Delphi for .NET, standard types are mapped to their
.NET equivalents. For example, the string type is mapped to the FCL's System.String
class (although Delphi extends string handling to support the language syntax), and
the Integer type is mapped to System.Int32. Other types, such as TDateTime, have
been reimplemented for .NET.
TDateTime
In Delphi for Win32 (and Kylix), TDateTime is defined as a Double (64-bit
floating-point number). Date and time information is stored as the count of days
since midnight on 30-Dec-1899.
In Delphi 8, TDateTime is a record, declared in the Borland.Delphi.System unit.
TDateTime handles implicit conversions to and from Double values, so existing code
that assumes TDateTime is a Double value compiles without warnings. Implicit
conversions to System.DateTime and standard operators are also implemented, so
Delphi's TDateTime type is fully accessible from other .NET languages. Internally,
TDateTime stores its value using the System.DateTime type.
The problem is that System.DateTime was designed to hold dates and times, while
Double (the original TDateTime) was designed to hold floating-point numbers. And
sometimes, Delphi coders relied on this small, but critical, implementation detail.
Since TDateTime was a double, it was easy to perform arithmetic operations on it -
for example, subtracting one TDateTime from another to get the difference between
them in days. Occasionally, the difference was negative (the first date was later
then the second date), and if your code allows that, it will no longer work.
Instead, you'll get the absolute value of the difference.
To work around this, you'll have to use actual Double variables instead of
TDateTimes. You can use TDateTime's ToOADate method to get a value compatible with
previous versions of Delphi.
Conclusion
Delphi 8 for .NET is a great product, letting Delphi developers move into the .NET
world while using their existing skills and re-using their existing code. Still,
.NET and Win32 are two different platforms, and the transition requires extra
caution when trying to use skills and processes which were valid in the past.
Borland has made porting code to VCL.NET a fairly smooth process, but one cannot
expect 100% portability between the platforms. I have shown some of the issues I've
encountered. I'm sure there are more.
In addition to this article, be sure to read the Delphi 8 help topic, "Language
Issues in Porting VCL Applications to Delphi 8 for .NET". It's a little hard to
find (it doesn't seem to be in the contents or the index). If you have Delphi 8
installed, you can find it at
ms-help://borland.bds2/bds2guide/html/LanguageIssues.htm.
|