Bob Swart (aka Dr.Bob)
Chrome - Object Pascal compiler voor .NET

Chrome is een Object Pascal (command-line en/of VS.NET plugin) compiler voor .NET van RemObjects Software.

Een groot deel van de bezoekers van mijn website (misschien wel bijna iedereen) gebruikt Borland Delphi. En het zal vast niemand ontgaan zijn dat Microsoft de afgelopen paar jaar bezig is geweest de oude vertrouwde (maar niet-zo-veilige) Windows API te vervangen door het .NET Framework. Vooral voor software ontwikkelaars is dat een ontwikkeling die van betekenis is, en grote invloed heeft op onze manier van werken. Tot en met versie 7 kan Delphi alleen Win32 toepassingen produceren, terwijl Delphi 8 for .NET alleen .NET toepassingen kan opleveren. Het is Delphi 2005 die zowel Win32 als .NET aankan, wat Delphi 2005 de meest complete Delphi versie maakt tot nu toe (jammer alleen dat er niet ook een Kylix versie in de doos zit).
Waar ik me een beetje over verbaas is dat er wel een 30 dagen trial editie van de hoogste architect versie van Delphi 2005 beschikbaar is, maar niet een gratis uitgeklede editie die gewoon blijft werken. Delphi 7 Personal was beschikbaar voor iedereen, en was een prima middel om de taal en omgeving te leren kennen. Delphi 2005 Personal bestaat wel, maar wordt alleen gebruikt (uitgedeeld) bij "bijzondere" gelegenheden, zoals een speciaal "Delphi.NET" blad of seminar dat dan ook nog door een grote partij georiganiseerd moet worden voordat het de aandacht van Borland trekt. Je kan op zoek naar een speciaal nummer van PC Pro in Engeland, of een speciale uitgave van het Duitse Delphi.NET (door de uitgevers van Der Entwickler). Waar dan een DVD bij zit met Delphi 2005 Personal.
Dit is in mijn ogen een gemiste kans voor Borland, want juist nu is het belangrijk om de ontwikkelaars - alle ontwikkelaars, niet alleen de bestaande Delphi ontwikkelaars - te laten zien wat je met Delphi 2005 allemaal kunt doen. En hoe je het nieuwe .NET ontwikkelplatform kunt leren met behulp van een fijne taal en omgeving.

Dit artikel gaat dan ook niet over Delphi 2005 deze keer, maar over Chrome: een andere Pascal-achtige compiler waarmee we .NET toepassingen kunnen bouwen. Geen Win32, maar wel een uitstekende manier om met een fijne taal de nieuwe .NET omgeving te leren kennen. En zonder kosten, want de command-line compiler van Chrome is gratis te downloaden en te gebruiken. Chrome komt uit de stal van RemObjects Software, die meer hulpmiddelen hebben gemaakt waar ik veel goede ervaringen mee heb (zoals RemObjects SDK, Data Abstract en Hydra). Behalve de gratis command-line compiler, is Chrome ook te koop als plugin voor de Microsoft Visual Studio.NET IDE, maar die gebruiken we hier dus niet.

Installatie
Als voorbereiding hebben we een machine nodig met Windows erop plus het Microsoft .NET Framework versie 1.1. Nog niet de komende versie 2.0 (die op dit moment in beta is), alhoewel Chrome daar ook voor zal werken in de toekomst. Wie met Mono werkt (.NET bovenop Linux in plaats van Windows) kan overigens ook een versie van de RemObjects Chrome command-line compiler voor Mono downloaden.
Ga naar www.chromesville.com, en klik op Downloads om de gratis command-line versie van de Chrome compiler te kunnen downloaden (op dit moment is dit build .171 van half mei 2005, een download van ongeveer 8 MB). Je kunt daar ook wat informatie achterlaten (naam, e-mail, etc.) waarmee je geïnformeerd kan worden als er een nieuwe(re) versie verschijnt. Dit is niet verplicht - je kan de gratis command-line versie downloaden zonder ook maar iets van jezelf op te geven of achter te laten.

Na de RemObjects Chrome Licence Agreement, volgt dan een pagina waarin je je naam en eventueel organisatie kan invoeren, en uiteindelijk de pagina die je vertelt waar RemObjects Chrome geïnstalleerd zal worden:

Na de installatie wordt de README.html in de default browser geladen. Hier kun je de laatste bijzonderheden lezen, zoals het feit dat de gratis command-line compiler die ik in mei heb gedownload nog een preview versie is.
In de directory C:\Program Files\RemObjects\Chrome zijn na installatie al de helpfiles en enkele voorbeeldtoepassingen te vinden. Om de werking van de command-line compiler helemaal uit te leggen, bouw ik nu echter een geheel nieuwe voorbeeldtoepassing.

Voorbeeldtoepassing
De RemObjects command-line compiler is te starten door gewoon Chrome aan te roepen (op de command-line). Daarvoor moet dan wel de C:\Program Files\RemObjects\Chrome\bin directory in het pad staan, overigens.
De Chrome compiler kan .pas source files en .chrome project files bewerken. Een .chrome project file is een XML bestand, dat niet eenvoudig zelf te maken is. Maar een .pas source file is iets dat we natuurlijk wel zelf kunnen maken. Met notepad zelfs als het moet.
De structuur van een Chrome toepassing verschilt een beetje van die van Delphi. Een leeg skelet, dat je kan gebruiken als uitgangspunt voor nieuwe WinForms toepassingen, is als volgt:

  namespace Skelet;
  // Skelet application by Bob Swart (aka Dr.Bob - www.drbob42.com)
  interface
  uses
    System.Windows.Forms,
    System.Drawing;

  type
    SkeletForm = class(Form)
    public
      constructor;
      class method Main;
    end;

  implementation

  constructor SkeletForm;
  begin
    inherited Create;
    Text := 'WinForms Skelet';
  end;

  class method SkeletForm.Main;
  begin
    try
      Application.Run(SkeletForm.Create);
    except
      on E: Exception do
        MessageBox.Show(E.Message);
    end;
  end;

  end.
Er zijn een aantal verschillen ten opzichte van Delphi. Zo begint een Chrome source bestand met het keyword namespace, en niet met unit, library of program. Namespace lijkt nog het meest op unit, want ook hier hebben we een interface en een implementation sectie.
Een groter verschil is het feit dat een constructor in Chrome geen naam heeft (zie de class SkeletForm), en dat een methode geen procedure of function is maar een method. Dit is wel even wennen, maar daar ben je zo doorheen. Een class method is overigens hetzelfde als in Delphi: deze kan aangeroepen worden nog zonder een instantie van de class zelf te hebben. In Chrome geldt dat een class method genaams "Main" het hoofdprogramma is, dus de class method SkeletForm.Main is in feite ons hoofdprogramma. Hierin maak ik een instantie van het SkeletForm, en geef dat mee aan de Application.Run. Als er iets mis is gegaan en een exception is niet afgevangen, dan kan ik in het except-block van de Main de fout op het scherm laten zien.
De constructor (zonder naam) wordt gebruikt om het SkeletForm zelf te maken. Eerst de inherited constructor aanroepen, en daarna kunnen we zelf properties een waarde geven (en andere controls erop zetten, zoals we straks zullen doen).

Compileren
Het compileren van het source bestand met de Chrome compiler gaat door een hoop opties mee te geven op de command-line. Meestal maak ik daar dan ook een batch bestand van, zodat ik alle opties maar één keer hoef in te tikken (handig als er fouten in de source code zitten en/of je regelmatig wilt hercompileren). Voor het SkeletForm is de batch file ten behoeve van het compileren met Chrome als volgt:

  Chrome Skelet.pas /assemblyname:Skelet /type:winexe
    /ref:$(Framework)\System.Windows.Forms.dll /ref:$(Framework)\System.Drawing.dll
Als eerste argument geven we het source bestand mee (Skelet.pas), de /assemblyname: optie wordt gebruikt om de naam van de executable op te geven. De /type: optie kan de waarde exe, winexe of library krijgen. Een "normale" exe (de default keuze) opent ook een console window, en dat is minder handig bij een WinForms toepassing. Vandaar de /type:winexe optie. Tot slot moet ik een tweetal referenties opgeven naar de System.Windows.Forms en System.Drawing .NET assemblies. Iedere keer dat we een extra assembly nodig hebben, moeten we die via de /ref: optie toevoegen.

Behalve een batch file, kun je er ook voor kiezen om een .chrome bestand te maken. Zoals eerder gezegd, dat is een XML bestand dat met name voor de integratie met Visual Studio.NET wordt gebruikt. Er zijn echter een minimaal aantal knopen en attributen verplicht, zodat het niet echt moeilijk is er zelf eentje te maken. Zeker niet als ik al een voorbeeld voor de Skelet toepassing bij heb gevoegd. Die is als volgt:

  <ChromeProject>
    <ProjectOptions Name="Skelet">
      <OutputType>winexe</OutputType>
      <AssemblyName>Skelet</AssemblyName>
    </ProjectOptions>
    <References>
      <Reference Assembly="$(Framework)\System.Drawing.dll" />
      <Reference Assembly="$(Framework)\System.Windows.Forms.dll" />
    </References>
    <Files>
      <File Filename="Skelet.pas" />
    </Files>
  </ChromeProject>
Er zijn drie secties in het XML bestand. Binnen de root-knoop ChromeProject hebben we de ProjectOptions (daar hoef je hooguit de naam te wijzigen), de References (daar moet je eventueel extra assemblies toevoegen) en de Files (hier kun je extra source files toevoegen). Als er precies één .chrome bestand in een directory is, hoef je nu allen nog maar de "Chrome" compiler aan te roepen, zonder verdere argumenten, want dan zal hij automatisch de gegevens uit het ene .chrome bestand halen. Als er meer dan één .chrome bestand in een directory is moet je gewoon als argument het juiste .chrome bestand meegeven. Is een stuk korter dan de lange command-line die ik in de batch bestanden moet opbouwen, en werkt net zo goed.
In beide gevallen levert het compileren van het Skelet.pas source bestand een Skelet.exe op van 2560 bytes, en als we die uitvoeren dan zien we een leeg WinForm, als volgt:

Componenten, Properties en Events
De volgende stap bestaat uit het toevoegen van componenten, en het geven van waarden aan properties en events. Omdat we nog steeds alleen de command-line compiler gebruiken, is er geen designer of andere ontwerpomgeving waarin we de componenten op de WinForm kunnen plaatsen. We zullen dit allemaal met de hand moeten doen. Op zich niet zo'n probleem als het beperkt blijft, maar voor grotere toepassingen wordt dit al snel lastig zonder ook dit deel te automatiseren (dat zal ik in het laatste voorbeeld laten zien).
Als tweede toepassing zal ik de "beroemde" demo van de textbox, listbox en button nabouwen, waarbij de inhoud van de textbox naar de listbox wordt gekopieerd als we op de button klikken. Dit was de eerste publieke demo van Delphi 1, en ook Kylix werd hiermee voor het eerst gedemonstreerd. Eens kijken hoe het met Chrome gaat.
Als uitgangspunt neem ik het skelet project, en wijzig ik de naam SkeletForm in DemoForm, en voeg een drietal componenten toe aan de definitie van de DemoForm. Behalve deze drie componenten, zullen we ook een event moeten toevoegen: de Click van de button. Dit is de methode Button_Click, die onder .NET twee argumenten heeft: de sender van type Object, en een argument genaamt "e" van type EventArgs. De definitie van het DemoForm is daarmee als volgt geworden:

  namespace Demo;
  // Demo application by Bob Swart (aka Dr.Bob - www.drbob42.com)
  interface
  uses
    System.Windows.Forms,
    System.Drawing;

  type
    DemoForm = class(Form)
    public
      constructor;
      class method Main;
    protected
      method Button_Click(sender: System.Object; e: System.EventArgs);
    private
      Button1: Button;
      TextBox1: TextBox;
      ListBox1: ListBox;
    end;
De implementatie van de Button_Click zal niet zo'n verrassing zijn, en is als volgt:
  method DemoForm.Button_Click(sender: System.Object; e: System.EventArgs);
  begin
    ListBox1.Items.Add(TextBox1.Text);
  end;
Wat wel anders is dan we gewend zijn is de constructor. Delphi gebruikt al vanaf de allereerste versie naast de .pas bestanden ook speciale .dfm bestanden. Daarin zijn de properties en hun waarden opgenomen. Dit is echter niet de manier waarop Visual Studio.NET van Microsoft werkt (of Java), want daarin worden alle properties en instellingen in pure source code statements gedaan. Dus als je van een component de omvang of tekst wijzigt, dan wijzig je in feite de source code van het project, en niet het .dfm bestand. Overigens is de naam niet altijd hetzelfde: voor Delphi onder Windows is het .dfm (zowel 16- als 32-bit), voor het cross-platform CLX (Windows en Linux) is de uitgang .xfm, en onder .NET maken VCL for .NET toepassingen gebruik van .nfm bestanden.
Maar Chrome is een plugin voor de Visual Studio.NET IDE, en werkt dus niet met dergelijke bestanden. We zullen dus de lokatie en omvang van onze componenten in source code moeten aangeven. Dat heeft de volgende constructor code tot gevolg:
  constructor DemoForm;
  begin
    inherited Create;
    Button1 := Button.Create;
    Button1.Location := Point.Create(110, 10);
    Button1.Size := System.Drawing.Size.Create(80, 24);
    Button1.Text := 'Klik mij';
    Button1.Click += Button_Click;
    Controls.Add(Button1);
    TextBox1 := TextBox.Create;
    TextBox1.Location := Point.Create(10, 10);
    TextBox1.Size := System.Drawing.Size.Create(80, 24);
    Controls.Add(TextBox1);
    ListBox1 := ListBox.Create;
    ListBox1.Location := Point.Create(210, 10);
    ListBox1.Size := System.Drawing.Size.Create(80, 100);
    Controls.Add(ListBox1);
  end;
Op zich niet zo moeilijk, alhoewel de Controls.Add nieuw is. Die wordt gebruikt om de betreffende component toe te voegen aan de lijst met componenten van de "parent" (in dit geval het WinForm zelf). Zonder de Controls.Add zou je het betreffende component niet zien, en dat is niet de bedoeling natuurlijk.
Let ook even op de regel waarin we de Button_Click event handler toevoegen aan het Click event van Button1. Omdat er in .NET meerdere event handlers aan één event kunnen hangen, kan dit niet zomaar een assignment zijn met := maar moeten we de nieuwe event handler toevoegen aan de "lijst" met bestaande event handlers. Ook al was die nog leeg, en is het nu de enige. De += operator is specifiek voor Chrome, en bestaat niet in Delphi overigens. Maar het lijkt wel erg op wat er in C# ook moet gebeuren.
De DemoForm.Main methode is net als de SkeletForm.Main, met het verschil dat er deze keer een DemoForm wordt aangemaakt:
  class method DemoForm.Main;
  begin
    try
      Application.Run(DemoForm.Create);
    except
      on E: Exception do
        MessageBox.Show(E.Message);
    end;
  end;

Compileren
Er is nog wel iets belangrijks dat makkelijk over het hoofd te zien is bij het compileren. Teneinde het multi-cast event assignment te ondersteunen, hebben we nu ook een referentie naar de System.dll assembly nodig bij het compileren. Als we die assembly niet toevoegen (of met de /ref: optie op de command-line, of via een Reference Assembly knoop in het .chrome bestand), dan krijgen we een compiler foutmelding. In het .chrome bestand moeten we de volgende regel toevoegen:

  <Reference Assembly="$(Framework)\System.dll" />
Hierna compileert het source bestand zonder problemen, en levert de beroemde demo op:

Klikken op de Button voegt de tekst uit de TextBox toe aan de lijst in de ListBox. Net als we 10 jaar geleden met Delphi onder Windows konden. Tijd voor een laatste en wat complexere demo.

Memory Spel
Het memory spel is erg populair bij mijn kinderen, zeker nu ik ook een aantal implementaties voor de computer heb geschreven. Zo heb ik in 2002 voor Delphi o.a. een migratie van VCL naar VCL for .NET geschreven die op het Borland Developer Network te lezen is.
Voor de Chrome implementatie bestaat geen VCL for .NET, dus moeten we het doen met de native WinForms. De defintie van mijn MemoryForm is als volgt:

  namespace Memory42;
  // Memory game by Bob Swart (aka Dr.Bob - www.drbob42.com)
  interface
  uses
    System.Windows.Forms,
    System.Drawing;

  const
    MaxX = 6;
    MaxY = 4;

  type
    MemoryForm = class(System.Windows.Forms.Form)
    const
      Caption = 'Sharpen your mind: Dr.Bob''s Game of Memory for .NET (';
    public
      constructor;
      class method Main;
    protected
      method Buttons_Click(sender: System.Object; e: System.EventArgs);
    private
      Turns: Integer;
      First: Boolean; // eerste of tweede klik op een button?
      Buttons: array[1..MaxX,1..MaxY] of System.Windows.Forms.Button;
    end;
Merk op dat ik voor de Buttons een 2-dimensionaal array gebruik, waardoor we de hoogte en breedte van het spel desgewenst kunnen aanpassen. En aangezien we toch altijd alle componenten in de constructor moeten aanmaken, kunnen we net zo goed een array daarvoor gebruiken (we zien toch niks tijdens design-time).
De implementatie maakt gebruik van een nieuwe extra assembly: de System.Threading. Deze is nodig, omdat ik de Sleep methode moet aanroepen teneinde een of enkele seconden lang de text op de twee "omgedraaide" Buttons te laten zien, alvorens deze weer in ? te veranderen. Zonder de Thread.Sleep is dit niet goed mogelijk.
De constructor MemoryForm gebruikt ook de .NET Random generator om de teksten van de Buttons rond te schuiven. De .NET Random generator is in feite een Random class, die een methode Next heeft, waarbij de waarde van het argument aangeeft tussen welke waarde (en 0) het resultaat moet zijn. Een beetje anders dan de Random methode die in Delphi zit, maar werkt net zo goed.
  implementation
  uses
    System.Threading; // Sleep

  constructor MemoryForm;
  var
    i,j,Tag: Integer;
    X1,X2,Y1,Y2: Integer;
    Rand: Random;
  begin
    inherited Create;
    Rand := Random.Create;
    Turns := 0;
    Text := Caption + '-)';
    First := True;
    Size := System.Drawing.Size.Create(612, 436);
    for i:=1 to MaxX do
    begin
      for j:=1 to MaxY do
      begin
        Buttons[i,j] := Button.Create;
        Buttons[i,j].Location := Point.Create(6 + (600 div MaxX) * (i-1),
                                              6 + (400 div MaxY) * (j-1));
        Buttons[i,j].Size := System.Drawing.Size.Create((600 div MaxX) - 8,
                                                        (400 div MaxY) - 8);
        Buttons[i,j].Tag := System.Object(1 + (j-1) * MaxX + (i-1) div 2);
        Buttons[i,j].Text := '?';
        Buttons[i,j].Font := System.Drawing.Font.Create('Comic Sans MS', 24);
        Buttons[i,j].Click += Buttons_Click;
        Controls.Add(Buttons[i,j])
      end
    end;
    for i:=1 to 42 do
    begin
      X1 := 1+Rand.Next(MaxX);
      X2 := 1+Rand.Next(MaxX);
      Y1 := 1+Rand.Next(MaxY);
      Y2 := 1+Rand.Next(MaxY);
      Tag := Convert.ToInt16(Buttons[X1,Y1].Tag);
      Buttons[X1,Y1].Tag := Buttons[X2,Y2].Tag;
      Buttons[X2,Y2].Tag := System.Object(Tag)
    end
  end;
Merk op dat we nu ook de positie en omvang van de buttons goed moeten bepalen, en daarvoor gebruik ik de MaxX en MaxY om in een loopje de Location en Size properties van de buttons te zetten. Het toevoegen van de Buttons_Click event handler gaat weer voor iedere Button hetzelfde via de += operator.
De Buttons_Click methode kijkt of het de eerste keer is dat er op een Button is gedrukt (of dat er al een andere Button is omgedraaid), en vergelijkt anders de waarde van beide "omgedraaide" Buttons met elkaar. Als een knop eenmaal is omgedraaid, dan wordt de Enabled op False gezet, zodat je die niet nog een keer kunt gebruiken.
  method MemoryForm.Buttons_Click(sender: System.Object; e: System.EventArgs);
  var
    i,j: Integer;
  begin
    if First then
    begin
      First := False;
      Inc(Turns);
      Text := Caption + Turns.ToString + ')';
      Tag := (Sender as Button).Tag;
      (Sender as Button).Text := Tag.ToString
    end
    else // tweede button
    if (Sender as Button).Text = '?' then // niet eerder gebruikt
    begin
      First := True;
      (Sender as Button).Text := (Sender as Button).Tag.ToString;
      Update;
      if (Sender as Button).Tag.ToString = Tag.ToString then // hetzelfde!
      begin
        for i:=1 to MaxX do
          for j:=1 to MaxY do
            if Buttons[i,j].Tag.ToString = Tag.ToString then
            begin
              Buttons[i,j].Enabled := False;
              Buttons[i,j].Text := '['+Tag.ToString+']'
            end
      end
      else // niet hetzelfde: weer "omdraaien"
      begin
        Thread.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;
In plaats van getallen die in de Tag property staan, kun je iedere button ook uitrusten met een plaatje. Hiertoe kun je dan het getal van de Tag property gebruiken om een bepaald plaatje op te halen en te vertonen. Dat laat ik ook over als oefening voor de lezer.
De class method Main is zoals we inmiddels gewend zijn:
  class method MemoryForm.Main;
  begin
    try
      Application.Run(MemoryForm.Create);
    except
      on E: Exception do
        MessageBox.Show(E.Message);
    end;
  end;

  end.
Voor het compileren gebruik ik de volgende command-line:
  Chrome Memory42.pas /assemblyname:Memory42 /type:winexe
    /ref:$(Framework)\System.dll /ref:$(Framework)\System.Windows.Forms.dll
    /ref:$(Framework)\System.Drawing.dll
Of het volgende .chrome project bestand:
  <ChromeProject>
    <ProjectOptions Name="Memory42">
      <OutputType>winexe</OutputType>
      <AssemblyName>Memory42</AssemblyName>
    </ProjectOptions>
    <References>
      <Reference Assembly="$(Framework)\System.dll"/>
      <Reference Assembly="$(Framework)\System.Drawing.dll" />
      <Reference Assembly="$(Framework)\System.Windows.Forms.dll" />
    </References>
    <Files>
      <File Filename="Memory42.pas" />
    </Files>
  </ChromeProject>
Het resultaat is een memory spel van 4 rijen en 6 kolommen (deze waarden zijn desgewenst aan te passen - ik laat het aan de lezer over om ze bijvoorbeeld via een .ini of .config file dynamisch in te laten stellen).

Dit is een niet geheel triviale toepassing voor .NET, die ik met Chrome heb kunnen compileren tot een native WinForms .NET toepassing. Pascal en .NET samen, en dat met een gratis Chrome command-line compiler van RemObjects Software. Een ideale manier om het .NET Framework en WinForms te leren kennen (alhoewel het werken zonder designer erg lastig kan zijn voor grotere projecten).
De complete source code voor de projecten is te downloaden, en binnenkort volgen meerdere artikelen en source code projecten van Chrome.

Referenties
Introducing the Chrome Command Line Compiler
RemObjects Chrome
Dr.Bob's Chrome Shines

Mocht iemand nog vragen, opmerkingen of suggesties hebben, dan hoor ik die het liefst via .


This webpage © 2005-2006 by webmaster drs. Robert E. Swart (aka - www.drbob42.com). All Rights Reserved.