Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
Memory: from VCL via VCL for .NET to .NET
In this article, I'll show how to migrate a VCL application (the game of Memory) to VCL for .NET, and finally to WinForms on .NET, all using the Delphi for .NET Preview command-line compiler (Update 1 or later).
The application that we'll examine here is the simple game of memory (my kids love to play it). We start with the VCL edition that I built as an exercise in creating dynamic controls, while writing my monthly Under Construction column for issue #48 of The Delphi Magazine (published by iTec). I will first migrate the original VCL edition to .NET using VCL for .NET, showing the few minor changes that I had to made to the source code (most changes were due to the fact that we only have a Delphi for .NET preview command-line compiler, and not a full IDE with design-time environment, yet). The third and final version is also a .NET edition of Memory, written in Delphi for .NET but this time using WinForms instead of VCL for .NET.
VCL and VCL for .NET
In my view, the main purpose of VCL for .NET is to offer a quick-and-easy migration path for VCL applications.
So I was not surprised to learn that I only had to make a few modifications to my Delphi VCL application.
The steps to migrate a VCL application to use VCL for .NET are as follows:
Unit Namespaces
The first thing that I had to do, was make sure that the units in the uses clause are now using their fully qualified "namespace" name.
Originally, the uses clause of mainform.pas was as follows:
uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;In order to use namespaces, Windows becomes Borland.Win32.Windows, Messages becomes Borland.Win32.Messages, etc.. Since I wanted to end with an application that can be compiled with both Delphi 7 (VCL) and Delphi for .NET (VCL for .NET), I decided to use IFDEFs to distinguish between WIN32 and CLR. Ignoring the fact that we can now have three platforms: WIN32, CLR, and LINUX, I decided to use a simple {$IFDEF CLR} for "VCL for .NET" and the {$ELSE} part for the good-old Win32 "VCL".
uses {$IFDEF CLR}Borland.Win32.Windows{$ELSE}Windows{$ENDIF}, {$IFDEF CLR}Borland.Win32.Messages{$ELSE}Messages{$ENDIF}, {$IFDEF CLR}Borland.Delphi.SysUtils{$ELSE}SysUtils{$ENDIF}, {$IFDEF CLR}Borland.Delphi.Classes{$ELSE}Classes{$ENDIF}, {$IFDEF CLR}Borland.Vcl.Graphics{$ELSE}Graphics{$ENDIF}, {$IFDEF CLR}Borland.Vcl.Controls{$ELSE}Controls{$ENDIF}, {$IFDEF CLR}Borland.Vcl.Forms{$ELSE}Forms{$ENDIF}, {$IFDEF CLR}Borland.Vcl.Dialogs{$ELSE}Dialogs{$ENDIF}, {$IFDEF CLR}Borland.Vcl.StdCtrls{$ELSE}StdCtrls{$ENDIF};Note that a shorter way to write this could be the following (which unfortunately doesn't compile in the current preview version of the Delphi for .NET command-line compiler):
uses {$IFDEF CLR}Borland.Win32.{$ENDIF}Windows, {$IFDEF CLR}Borland.Win32.{$ENDIF}Messages, {$IFDEF CLR}Borland.Delphi.{$ENDIF}SysUtils, {$IFDEF CLR}Borland.Delphi.{$ENDIF}Classes, {$IFDEF CLR}Borland.Vcl.{$ENDIF}Graphics, {$IFDEF CLR}Borland.Vcl.{$ENDIF}Controls, {$IFDEF CLR}Borland.Vcl.{$ENDIF}Forms, {$IFDEF CLR}Borland.Vcl.{$ENDIF}Dialogs, {$IFDEF CLR}Borland.Vcl.{$ENDIF}StdCtrls;The error message that you get is "Error: Identifier expected but end of file found".
.dfm Information
The next step is something that is only necessary at this time, and probably no longer when the final version of Delphi for .NET (with VCL for .NET) ships.
I'm talking about the lack of .dfm streaming in the current version of the preview command-line compiler.
This means that the information from the .dfm file is not compiled/linked into our Delphi application.
For our example, the mainform.dfm file contained the following:
object Form1: TForm1 Left = 248 Top = 137 BorderStyle = bsDialog ClientHeight = 402 ClientWidth = 602 Color = clNavy Font.Charset = ANSI_CHARSET Font.Color = clMaroon Font.Height = -32 Font.Name = 'Comic Sans MS' Font.Style = [fsBold] OldCreateOrder = False OnCreate = FormCreate PixelsPerInch = 96 TextHeight = 45 endThere are only two properties that I won't be able to use in the Delphi for .NET language: OldCreateOrder and TextHeight. But all the others can be taken and placed inside a special routine called InitializeControls, which is called in the constructor and implemented as follows:
{$IFDEF CLR} constructor TForm1.Create(AOwner: TComponent); begin inherited; InitializeControls; FormCreate(Self) // explicit call... end; procedure TForm1.InitializeControls; begin Left := 248; Top := 137; BorderStyle := bsDialog; ClientHeight := 402; ClientWidth := 602; Color := clNavy; Font.Charset := ANSI_CHARSET; Font.Color := clMaroon; Font.Height := -32; Font.Name := 'Comic Sans MS'; Font.Style := [fsBold]; // OnCreate := FormCreate end; {$ENDIF}Note the explicit call to FormCreate that I had to make in the constructor (assigning FormCreate to the OnCreate event handler inside InitializeControls is not enough, since by that time the OnCreate event handler won't be called anymore).
implementation
{$IFNDEF CLR}
{$R *.dfm}
{$ENDIF}
Again, this won't be needed with the final version of Delphi for .NET, but if you want to convert VCL applications to VCL for .NET today, this is a technique that you can use.
Main Project
The third step consisted of working on the main project .dpr file.
The CreateForm method of Application is no longer available, but you have to create a form and assign it to the MainForm property instead.
program Memory; uses {$IFDEF CLR}Borland.VCL.{$ENDIF}Forms, MainForm in 'MainForm.pas' {Form1}; begin Application.Initialize; {$IFDEF CLR} Form1 := TForm1.Create(nil); Application.MainForm := Form1; {$ELSE} Application.CreateForm(TForm1, Form1); {$ENDIF} Application.Run; end.Also note the use of the IFDEF in the uses clause. This makes sure I can compile the application using the dcc32 command-line compiler (for Win32) as well as the dccil preview command-line compiler (for .NET). The downside of using an IFDEF in the uses clause of the main project file is that you can no longer open the project in the Delphi IDE itself - you'll get an error that an identifier is expected. And even if you modify the code so the project can be loaded, the IDE won't allow you to compile it (it still thinks the uses clause is invalid).
Miscellaneous
At this time, I could try to compile the mainform.pas unit, which resulted in a number of errors.
One of the errors confused me a bit, namely: "Error: Incompatible types: 'Object' and 'Integer'" (on a source line where I assigned something to the Tag property of a button).
As a result of reading this error, I prematurely concluded that for some reason the Tag property was perhaps no longer available in VCL for .NET (I was wrong, the Tag property has only changed type from integer to TObject).
Since my implementation of the Memory game uses the Tag property as integer value of each button, I really needed this property - and I need it to be an integer.
As quick fix, I decided to derive my own TTagButton from TButton, adding a Tag field of type integer again:
type TTagButton = class(TButton) {$IFDEF CLR} Tag: Integer; {$ENDIF} end;By making the Tag field only appear if CLR is defined, we can use TTagButton instead of TButton and still have a single source application that compiles with Delphi for Win32 as well as the Delphi for .NET preview command-line compiler.
Since I assumed that the Tag property was also missing from the Form, I added a new Tag field to the Form as well. And with the new constructor and InitializeControls (both only with CLR is defined), the new form definition looks as follows:
type TArrayArrayButton = Array of Array of TTagButton; TForm1 = class(TForm) procedure FormCreate(Sender: TObject); private { Private declarations } Turns: Integer; procedure Shuffle(Button: TArrayArrayButton); procedure FirstButtonClick(Sender: TObject); procedure SecondButtonClick(Sender: TObject); {$IFDEF CLR} procedure InitializeControls; {$ENDIF} public { Public declarations } Tag: Integer; procedure CreateBoard(X,Y: Integer); {$IFDEF CLR} constructor Create(AOwner: TComponent); override; {$ENDIF} end;A final addition to the FormCreate event handler made sure that the caption of the Form displays the version of the Delphi compiler that's being used (Delphi for Windows or Delphi for .NET), so we can see in the caption if we're working with a Win32 or a .NET application.
procedure TForm1.FormCreate(Sender: TObject); begin Randomize; {$IFDEF CLR} Caption := 'Memory compiled with Delphi for .NET'; {$ELSE} Caption := 'Memory compiled with Delphi for Win32'; {$ENDIF} Turns := 0; CreateBoard(6,4) end;
VCL Conclusion
The result of all this is a Memory.dpr file with mainform.pas and .dfm (only for Win32) that compiles to a 374,784 byte Win32 executable or a 665,088 .NET IL executable.
The latter loads a little bit slower than its Win32 counterpart, but works exactly the same.
When I close the VCL for .NET application, however, I noticed a small delay before the application is destroyed (garbage collection perhaps?).
I'm sure all this will be taken care of before the final version of Delphi for .NET (and VCL for .NET) is made available.
Apart from the main project source code and the fact that units in the uses clause must now be specified with their full namespace, there were actually very little changes that I had to make.
The .dfm streaming inside InitializeControls is only a temporary necessity, and the Tag property was there after all (I just had to box it).
Note: Update 3 of the Delphi for .NET preview command-line compiler contains a special utility called Dfm2Pas that will assist you in the process of coding the properties from the .DFM file in the .PAS file (generated the InitializeControls method for you automatically).
This is a great help in migrating existing VCL applications to VCL for .NET, so make sure to check it out!
Dfm2Pas The Delphi for .NET preview command-line compiler Update 3 (that's the full name, for those of you who didn't attend the March 18th meeting at POSK) enhanced support with regards to VCL for .NET by including a utility called Dfm2Pas. In this new section, I'll examine Dfm2Pas in some more detail, and see how it can be very useful when moving Delphi applications to the .NET world. The Dfm2Pas utility parses a Delphi .pas unit (with or without a .dfm file) or a Delphi .dpr main project file, and modifies these files in a number of ways.
Namespaces
No more .DFM files
Conditional Compilation
Supported VCL Units
Example unit MainForm; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls; type TForm1 = class(TForm) PageControl1: TPageControl; TabSheet1: TTabSheet; TabSheet2: TTabSheet; Edit1: TEdit; Memo1: TMemo; Button1: TButton; ListBox1: TListBox; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.Button1Click(Sender: TObject); begin ListBox1.Items.Add(Edit1.Text) end; end.The modified .pas file - after running Dfm2Pas on it, is shown below. Note the entire uses clause which is using the IFDEF structure to add the namespaces Borland.Win32, Borland.Delphi and Borland.Vcl to the units listed. Any unknown units will not be changed unless you add them to the Files section of the Dfm2Pas.ini file (which already contains the MyFile=MyCompany.MyProduct.MyFile example reference). // ************************************************************************ // // Dfm2Pas: WARNING! // ----------------- // Part of the code declared in this file was generated from data read from // a *.DFM file or a Delphi project source using Dfm2Pas 1.0. // For a list of known issues check the README file. // Send Feedback, bug reports, or feature requests to: // e-mail: fvicaria@borland.com or check our Community website. // ************************************************************************ // unit MainForm; interface uses {$IFDEF CLR}Borland.Win32.Windows{$ELSE}Windows{$ENDIF}, {$IFDEF CLR}Borland.Win32.Messages{$ELSE}Messages{$ENDIF}, {$IFDEF CLR}Borland.Delphi.SysUtils{$ELSE}SysUtils{$ENDIF}, {$IFDEF CLR}Borland.Delphi.Variants{$ELSE}Variants{$ENDIF}, {$IFDEF CLR}Borland.Delphi.Classes{$ELSE}Classes{$ENDIF}, {$IFDEF CLR}Borland.Vcl.Graphics{$ELSE}Graphics{$ENDIF}, {$IFDEF CLR}Borland.Vcl.Controls{$ELSE}Controls{$ENDIF}, {$IFDEF CLR}Borland.Vcl.Forms{$ELSE}Forms{$ENDIF}, {$IFDEF CLR}Borland.Vcl.Dialogs{$ELSE}Dialogs{$ENDIF}, {$IFDEF CLR}Borland.Vcl.StdCtrls{$ELSE}StdCtrls{$ENDIF}, {$IFDEF CLR}Borland.Vcl.ComCtrls{$ELSE}ComCtrls{$ENDIF}; type TForm1 = class(TForm) PageControl1: TPageControl; TabSheet1: TTabSheet; TabSheet2: TTabSheet; Edit1: TEdit; Memo1: TMemo; Button1: TButton; ListBox1: TListBox; procedure Button1Click(Sender: TObject); private { Private declarations } {$IFDEF CLR} procedure InitializeControls; {$ENDIF} public { Public declarations } {$IFDEF CLR} constructor Create(AOwner: TComponent); override; {$ENDIF} end; var Form1: TForm1; implementation {$IFNDEF CLR} {$R *.dfm} {$ENDIF} procedure TForm1.Button1Click(Sender: TObject); begin ListBox1.Items.Add(Edit1.Text) end; {$IFDEF CLR} constructor TForm1.Create(AOwner: TComponent); begin inherited; InitializeControls; end; {$ENDIF} {$IFDEF CLR} procedure TForm1.InitializeControls; begin // Initalizing all controls... PageControl1 := TPageControl.Create(Self); TabSheet1 := TTabSheet.Create(Self); Edit1 := TEdit.Create(Self); Memo1 := TMemo.Create(Self); Button1 := TButton.Create(Self); ListBox1 := TListBox.Create(Self); TabSheet2 := TTabSheet.Create(Self); with PageControl1 do begin Name := 'PageControl1'; Parent := Self; Left := 0; Top := 0; Width := 688; Height := 453; ActivePage := TabSheet1; Align := alClient; TabOrder := 0; end; with TabSheet1 do begin Name := 'TabSheet1'; if TabSheet1.ClassType.InheritsFrom(TControl) then TControl(TabSheet1).Parent := self; Caption := 'TabSheet1'; PageControl := PageControl1; end; with Edit1 do begin Name := 'Edit1'; Parent := TabSheet1; Left := 56; Top := 80; Width := 121; Height := 21; TabOrder := 0; Text := 'Edit1'; end; with Memo1 do begin Name := 'Memo1'; Parent := TabSheet1; Left := 248; Top := 104; Width := 185; Height := 89; Lines.Add('Memo1'); TabOrder := 1; end; with Button1 do begin Name := 'Button1'; Parent := TabSheet1; Left := 64; Top := 232; Width := 75; Height := 25; Caption := 'Button1'; TabOrder := 2; OnClick := Button1Click; end; with ListBox1 do begin Name := 'ListBox1'; Parent := TabSheet1; Left := 504; Top := 176; Width := 121; Height := 97; ItemHeight := 13; TabOrder := 3; end; with TabSheet2 do begin Name := 'TabSheet2'; if TabSheet2.ClassType.InheritsFrom(TControl) then TControl(TabSheet2).Parent := self; Caption := 'TabSheet2'; ImageIndex := 1; PageControl := PageControl1; end; // Form's PMEs' Left := 270; Top := 107; Width := 696; Height := 480; Caption := 'Form1'; Color := clBtnFace; Font.Charset := DEFAULT_CHARSET; Font.Color := clWindowText; Font.Height := -11; Font.Name := 'MS Sans Serif'; Font.Style := []; end; {$ENDIF} end.Note that sometimes the order of properties is important. The Dfm2Pas utility will list (and assign) the properties in alphabetical order. For some components, this may not work correctly (for example the ItemIndex property of a TListBox or TComboBox should be assigned after the Items property). You have to manually change the order of these statements in the generated unit if you encounter problems with this. A good thing is that the IFDEF CLR (and IFNDEF CLR) compiler directives make sure that the new form will compile with Delphi for .NET as well as Delphi 7 (as long as you keep the .DFM file with it as well). Obviously, any controls that will be add to the .DFM file will not be created in the InitializeControls, nor will any new properties values or event handlers be used, so you should only perform Dfm2Pas on the final version and not continue to work on the converted version of your project (unless you know what you're doing).
Dfm2Pas Summary |
And now it's time to cover native .NET WinForms.
WinForms
When I'm talking about WinForms, I mainly refer to the .NET controls in the System.Windows.Forms namespace.
Before the availability of VCL for .NET, I had already investigated the ways to convert the VCL application to .NET using the WinForms classes.
The result of these efforts was a single .dpr file (see next listing).
This time, I had to convert a lot more code, however:
program Memory42; uses System.Windows.Forms, System.ComponentModel, System.Drawing, Borland.Delphi.SysUtils; // Needed for Sleep() const Caption = 'Sharpen your mind: Dr.Bob''s Game of Memory for .NET (%d)'; const MaxX = 6; MaxY = 4; type TagButton = class(Button) Tag: Integer; // Not part of Windows.Forms.Button end; TArrayArrayButton = Array[1..MaxX] of Array[1..MaxY] of TagButton; TForm42 = class(Form) public { Public declarations } constructor Create; protected { Protected declarations } procedure ButtonClick(Sender: System.Object; eventAgrs: System.EventArgs); private { Private declarations } Tag: Integer; { previous button } Turns: Integer; First: Boolean; { first or second button? } Buttons: TArrayArrayButton; end; constructor TForm42.Create; const W = 600; H = 400; var i,j,Tag: Integer; X1,X2,Y1,Y2: Integer; begin inherited Create; Randomize; Turns := 0; Text := Format(Caption,[Turns]); First := True; Size := Size.Create(W + 12, H + 30); for i:=1 to MaxX do begin for j:=1 to MaxY do begin Buttons[i,j] := TagButton.Create; Buttons[i,j].Location := Point.Create(6 + (W div MaxX) * Pred(i), 6 + (H div MaxY) * Pred(j)); Buttons[i,j].Size := Size.Create((W div MaxX) - 8,(H div MaxY) - 8); Buttons[i,j].Tag := 1 + (Pred(j) * MaxX + Pred(i)) div 2; Buttons[i,j].Text := '?'; Buttons[i,j].Font := Font.Create('Comic Sans MS', 24); Buttons[i,j].add_Click(ButtonClick); Controls.Add(Buttons[i,j]) end end; for i:=1 to 42 do // shuffle begin X1 := 1+Random(MaxX); X2 := 1+Random(MaxX); Y1 := 1+Random(MaxY); Y2 := 1+Random(MaxY); Tag := Buttons[X1,Y1].Tag; Buttons[X1,Y1].Tag := Buttons[X2,Y2].Tag; Buttons[X2,Y2].Tag := Tag end end; procedure TForm42.ButtonClick(Sender: System.Object; eventAgrs: System.EventArgs); var i,j: Integer; begin if First then begin First := False; Inc(Turns); Text := Format(Caption,[Turns]); Tag := (Sender as TagButton).Tag; (Sender as TagButton).Text := IntToStr(Tag) end else // second button begin First := True; (Sender as TagButton).Text := IntToStr((Sender as TagButton).Tag); Update; if (Sender as TagButton).Tag = Tag then // the same begin for i:=1 to MaxX do for j:=1 to MaxY do if Buttons[i,j].Tag = Tag then begin Buttons[i,j].Enabled := False; BUttons[i,j].Text := Format('[%d]',[Tag]) end end else // not the same; hide again begin Sleep(1000); for i:=1 to MaxX do for j:=1 to MaxY do if Buttons[i,j].Enabled then Buttons[i,j].Text := '?' end end end; begin Application.Run(TForm42.Create); end.This results in a native .NET IL executable of only 16,896 bytes. Much smaller than the VCL for .NET IL executable, because it uses the WinForms assembly (and doesn't have to link in any of the VCL for .NET units). But it took me more time to produce this application from the original VCL project.
Conclusion
In this article, I've shown how to migrate a VCL application (the game of Memory) to VCL for .NET, as well as to WinForms on .NET, all using the Delphi for .NET Preview command-line compiler.
The steps to migrate a VCL application to VCL for .NET are less time consuming than migrating from VCL to WinForms.
The pure WinForms application is smaller, however, and runs a bit faster.
In my personal view, VCL for .NET is a good way to quickly migrate VCL applications to .NET, but for new (native) .NET development, I probably won't be using VCL for .NET.
I guess time will tell - so I can't wait until the next update of the Delphi for .NET command-line compiler is made available (or the final version of Delphi for .NET).