Programmeren in Delphi zonder de VCL

Waarom? Bestandsgrootte.

Een klein project in Delphi XE. Een knop en een Edit-veld, een beetje code, en compileren. Is het al opgevallen hoe groot het aangemaakte uitvoerbare bestand is? Delphi maakt een mooie standalone exe, maar zelfs zonder code is die 780 kilobytes groot. In Delphi XE2 zelfs 1165 kilobytes.

Voor grotere projecten is dat niet erg. Bij intypen van code zal de bestandsgrootte overigens maar een klein beetje toenemen. Maar wat met een kleiner project? In een vast bestandje lijnen doorzoeken naar een substring bijvoorbeeld. Erg toch als dergelijke eenvoudige code 780 kilobytes inneemt?

Er worden, na een zoektocht op internet, enkele oplossingen aangeboden, waarmee in Delphi zelf 10% kan bespaard worden, en met externe compressors (upx bijvoorbeeld) 76%. Maar sommige virusscanners doen moeilijk over bestanden die met upx gecomprimeerd werden. En zelfs met 10% er af blijft dat een grote turf. Spreekwoordelijk wordt een vrachtwagen gebruikt om een zakje chips te vervoeren. In andere programmeertalen is de bestandsgrootte van de exe niet beter, dit is geen probleem van Delphi alleen. Al de drag & drop 'knoppen op venster zetten' grafische toestanden voegen heel wat ballast toe aan de code. Een klein deel van die ballast wordt gebruikt, het grootste deel niet.

VCL?

De "VCL" staat voor de "Visual Component Library". Dat komt neer op het zichtbare formulier en de knoppen of velden die er op neergezet worden (design modus van Delphi). Dit is erg gemakkelijk om snel te programmeren, doch zorgt voor grote uitvoerbare programma's. Maar het kan ook zonder die VCL. Als er geen "uses Forms" en geen .pas bestanden gebruikt worden. Al de code komt in het dpr-bestand. Een icoon en de Windows-styles komen uit een klein .res-bestandje.

Een klein project in Delphi, met een klein icoon (in bestandsgrootte dan). Het icoon toevoegen aan het project, en niet aan het formulier koppelen, zoals dat steeds moet bij een project met slechts 1 formulier. Compileren en opslaan. In de folder in kwestie worden alle bestanden geschrapt, behalve het dpr en het res-bestand.

folder

Dat dpr-bestand kan bewerkt worden, met Delphi (in verkenner er op dubbelklikken). De meldingen van Delphi (oudere versie enz.) bij het opstarten mogen genegeerd worden. Als voorbeeld: een minimaal Windowsprogramma (windowsvenster) zonder knoppen er op.

Let op: dit is de "oude" manier om Windowsprogramma's aan te maken. Het resultaat is een zeer klein en zeer snel bestand. Maar dit zijn geen RAD "Rapid Application Development"-toestanden. Dit gaat trager (in het begin, de eerste keren toch). Heel gemakkelijk of snel om aan te maken is het niet. En toch, met kopiëren en plakken, eigenlijk gaat het wel. Maar er moet dus geprogrammeerd worden zonder een grafisch formulier op je scherm, of knoppen die er op kunnen neergezet worden. Nu, zo moeilijk is het niet om ze in code (tekst) te zetten. En het stand-alone uitvoerbaar bestand is 23 keer kleiner dan bij "gewoon" programmeren. Bij deze manier van werken komt enige kennis van de Windows API van pas.


Een leeg "Windows venster"

Inhoud van een bestandje "test.dpr". Voorlopig zonder veel commentaarlijnen.


program test;    // Bestand moet dus test.dpr heten, anders werkt het niet

uses
  Windows, Messages;

var
  WindowClass: TWndClass;
  Msg        : TMsg;

function WindowMainProc(hWnd,Msg,wParam,lParam:Integer):Integer; stdcall;
begin
  if Msg = WM_DESTROY then PostQuitMessage(0);
  Result := DefWindowProc(hWnd, Msg, wParam, lParam);
end;

begin
  WindowClass.lpszClassName:= 'KiesMaar';
  WindowClass.hCursor := LoadCursor (0, IDC_ARROW);
  WindowClass.lpfnWndProc  :=  @WindowMainProc;
  RegisterClass(WindowClass);
  CreateWindow(WindowClass.lpszClassName,
               'Tekst in Titelbalk',
               WS_TILEDWINDOW or WS_VISIBLE,
               20, 40, 410, 246, 0, 0, hInstance, nil);

  while GetMessage(Msg, 0, 0, 0) do
    DispatchMessage(Msg);
end.


Wat die reeks cijfers enz. betekent (20, 40, 410, 246, 0, 0, hInstance, nil) staat onderaan deze pagina in de volledig gedocumenteerde broncode.

Als dit in Delphi opgestart wordt, toont dit zowaar een Windowsprogramma met een venster:

Leeg Venster

De exe is 27 kilobytes groot in Delphi XE. 16 kilobytes in Delphi 3. Uiteraard doet die code niks. Afgezien van het aanmaken van het venster, een berichtendispatching
WindowMainProc
en een steeds zichzelf doorlopende lus die het venster openhoudt
while GetMessage(Msg, 0, 0, 0) do
  DispatchMessage(Msg);
staat er niets in de code. In Delphi XE kan de exe (tenminste een exe met een venster) niet kleiner gemaakt worden. En het toevoegen van code maakt het niet direct veel groter. Maar nuttig is het voorlopig niet. Het kan worden opgestart, geresized, verplaatst, gemaximaliseerd of geminimaliseerd en afgesloten, en dat is het zowat.

Enkel een popup?

Test van een programma zonder een echt venster, maar toch een standaard Windows-bericht:
Bestand test.dpr


program test;

uses
  Windows;

begin
  MessageBox(0,
             'Hallo Wereld!',
             'Hallo',
             MB_ICONINFORMATION or MB_OK)
end.

Leeg Venster

Blijft 27 kilobytes in Delphi XE (in Delphi 3 is dit 15 i.p.v. 16 kilobytes). De clausule "uses Windows" neemt nogal wat ballast mee aan boord.

Hetzelfde kan met inline assembler:
Bestand test.dpr

program test;

uses
  Windows;

var
  titel, tekst:  string;

begin
  titel := 'Hallo';
  tekst := 'Hallo Wereld!';
  asm                 // Begin assembler code
    push 64           // uType '64' = MB_ICONINFORMATION (or MB_OK)
    push titel        // tekst in titelbalk
    push tekst        // tekst van bericht
    push 0            // parent handle 0 = HWND_DESKTOP
    call MessageBox   // toon de messagebox met de 4 eerder 'gepushte' elementen
  end;                // Einde assembler code
end.


Dit is zeker geen perfecte, zelfs geen erg nuttige asm-code. Enkel een voorbeeld dat dit mogelijk is, moest het nodig zijn.

API...

Bij het programmeren op deze manier kan niet met objecten gewerkt worden. Edit1.Text kan niet benaderd worden - dat moet met een API-call:
GetWindowText(handleEdit1, MijnCharArray, MAX_PATH); // tekst naar variabele "MijnCharArray" kopiëren


Windows API?
Staat voor "application programming interfaces". Is volledig gedocumenteerd op de MSDN-site. Het gaat om een reeks low-level commando's van het besturingssysteem. De compiler zal de code van de programmeertaal omzetten in API calls. De Delphi-code:
Edit1.text := 'Niet gevonden'; zou compiler omzetten in SetWindowText(hEdit1, 'Niet gevonden');
(waarbij hEdit1 de handle van het tekstveld is). En de code
Edit1.SelectAll; Wordt vertaald naar SendMessage(hEdit1,EM_SETSEL,0,-1);
Als de programmeur zelf rechtstreeks naar de API schrijft i.p.v. de compiler dat te laten doen, is het programma uiteraard sneller en kleiner.



Windows beep API call

Kan er niet beter iets nuttig gedaan worden? Uiteraard, maar eerst nog even spelen: een geluidje bij het opstarten van het programma.
Bestand test.dpr



program test;

uses
  Windows, Messages;

var
  WindowClass: TWndClass;
  Msg        : TMsg;

function WindowMainProc(hWnd, Msg, wParam, lParam: Integer): Integer; stdcall;
begin
  if Msg = WM_DESTROY then PostQuitMessage(0);
  Result := DefWindowProc(hWnd, Msg, wParam, lParam);
end;

begin
  WindowClass.lpszClassName:= 'KiesMaar';
  WindowClass.lpfnWndProc  :=  @WindowMainProc;
  RegisterClass(WindowClass);

  CreateWindow(WindowClass.lpszClassName,
             'Tekst in Titelbalk',
             WS_TILEDWINDOW or WS_VISIBLE,
             20, 40, 410, 246, 0, 0,
             hInstance, nil);

  Beep(330,460); Beep(392,340); Beep(329,230); Beep(329,110); Beep(440,230); 
  Beep(329,230); Beep(293,230); Beep(329,460); Beep(494,340); Beep(329,230); 
  Beep(329,110); Beep(523,230); Beep(494,230); Beep(392,230); Beep(329,230); 
  Beep(494,230); Beep(659,230); Beep(329,110); Beep(293,230); Beep(293,110); 
  Beep(247,230); Beep(380,230); Beep(329,720);

  while GetMessage(Msg, 0, 0, 0) do
    DispatchMessage(Msg);
end.


Dit is een leuk muziekje maar geen mooie code, met die repetitieve blokken 'Beep' (PC speaker sound) er in. In Delphi met VCL zou het commando 'Beep' overigens niet werken met de parameters (frequentie, milliseconden) er achter. De 'beep' hier is een rechtstreekse API-call, in Delphi met VCL moet Windows.beep(freq,millisec) ingetikt worden voor hetzelfde resultaat.

Deze code is gewoon om te illustreren dat het aanroepen van API-calls zeer eenvoudig is bij deze manier van programmeren. Overigens, moest dit 'leeg kaderprogramma' in een andere taal gemaakt worden dan Delphi, bv. In C, dan zou de code er niet veel anders uitzien. De Beep(321,321); (inclusief de puntkomma) zou hetzelfde blijven, begin..end zou {..} worden en nil wordt NULL. Een tekenreeks staat dan tussen dubbele quotes i.p.v. enkele quotes, en de uses windows wordt een #include <windows.h>- maar verder ziet alles er omzeggens hetzelfde uit.

Een knop op een venster

Nu wordt er bij het programma een knop toegevoegd (code in rode kleur):
Bestand test.dpr


program test;

uses
  Windows, Messages;

var
  wClass           : TWndClass;
  Msg              : TMsg;
  hZoek, hAppHandle: THandle;    // handle hZoek toegevoegd

function WindowMainProc(hWnd, Msg, wParam, lParam: Integer): Integer; stdcall;
begin
  if Msg = WM_DESTROY then PostQuitMessage(0);
  Result := DefWindowProc(hWnd, Msg, wParam, lParam);
end;

begin
  wClass.hInstance := hInstance;
  with wClass do begin
    style := CS_DBLCLKS;
    hIcon := LoadIcon(hInstance,'MAINICON');
    lpfnWndProc :=  @WindowMainProc;
    hbrBackground:= (COLOR_BTNFACE+1);
    lpszClassName:= 'Om Het Even';
    hCursor :=  LoadCursor(0,IDC_ARROW);
    lpszMenuName := '';
    cbClsExtra := 0;
    cbWndExtra := 0;
  end;
  RegisterClass(wClass);

  hAppHandle := CreateWindow(
    wClass.lpszClassName,
    'Tekst in Titelbalk',
    WS_TILEDWINDOW or WS_VISIBLE,
    20,
    40,
    310,
    146,
    0,
    0,
    hInstance,
    nil);

  hZoek := CreateWindow(
    'Button',
    'Tekst Op Knop',
    WS_VISIBLE or WS_CHILD or BS_PUSHBUTTON or BS_TEXT,
    6,
    6,
    110,
    20,
    hAppHandle,
    0,
    hInstance,
    nil);

  while GetMessage(Msg, 0, 0, 0) do
    DispatchMessage(Msg);
end.

Het resultaat is nu volgend venster:

Venster met knop

Knop met andere font

De tekst op de knop is een 'ouderwetse Windows 16-bit' font. Om dit te wijzigen, moet net achter de opmaak van de knop, (en in dit geval net voor de berichtenlus) de volgende code:


SendMessage(hZoek,WM_SETFONT,GetStockObject(ANSI_VAR_FONT),0);

Er kan ook een specifieke font en grootte enz. bepaald worden, maar om de code niet onnodig lang te maken wordt dat er hier uitgelaten. Deze code (ANSI_VAR_FONT) komt op deze plaats (rood)

hZoek := CreateWindow(
    'Button',
    'Tekst Op Knop',
    WS_VISIBLE or WS_CHILD or BS_PUSHBUTTON or BS_TEXT,
    6,
    6,
    110,
    20,
    hAppHandle,
    0,
    hInstance,
    nil);

SendMessage(hZoek,WM_SETFONT,GetStockObject(ANSI_VAR_FONT),0);


while GetMessage(Msg, 0, 0, 0) do
  DispatchMessage(Msg);

En het venster ziet er nu zo uit:

Venster met knop/font

Knop onzichtbaar in XP of Vista?

In Windows XP en Vista is het mogelijk dat de knop niet getoond wordt, en dat enkel een leeg venster getoond wordt. In XP zijn andere velden (Edit of Memo) soms ook onzichtbaar. Vanaf Windows 7 bestaat dat probleem niet meer.

Die zichtbaarheid van de knoppen in Windows XP / Vista kan opgelost worden met volgende code:
(Bijgevoegde lijnen in het rood)
bestand test.dpr


program Test;

uses
  Windows, Messages;

var
  wClass           : TWndClass;
  Msg              : TMsg;
  hZoek, hAppHandle: THandle;

procedure InitCommonControls; stdcall; external 'Comctl32.dll';  

function WindowMainProc(hWnd, Msg, wParam, lParam: Integer): Integer; stdcall;
begin
  if Msg = WM_DESTROY then PostQuitMessage(0);
  Result := DefWindowProc(hWnd, Msg, wParam, lParam);
end;

begin
  InitCommonControls;
  wClass.hInstance := hInstance;
  with wClass do begin
    style := CS_DBLCLKS;
    hIcon := LoadIcon(hInstance,'MAINICON');
    lpfnWndProc :=  @WindowMainProc;
    hbrBackground:= (COLOR_BTNFACE+1);
    lpszClassName:= 'Om Het Even';
    hCursor :=  LoadCursor(0,IDC_ARROW);
    lpszMenuName := '';
    cbClsExtra := 0;
    cbWndExtra := 0;
  end;
  RegisterClass(wClass);

  hAppHandle := CreateWindow(
    wClass.lpszClassName,
    'Tekst in Titelbalk',
    WS_TILEDWINDOW or WS_VISIBLE,
    20,
    40,
    310,
    146,
    0,
    0,
    hInstance,
    nil);

  hZoek := CreateWindow(
    'Button',
    'Tekst Op Knop',
    WS_VISIBLE or WS_CHILD or BS_PUSHBUTTON or BS_TEXT,
    6,
    6,
    110,
    20,
    hAppHandle,
    0,
    hInstance,
    nil);

  SendMessage(hZoek,WM_SETFONT,GetStockObject(ANSI_VAR_FONT),0);

  while GetMessage(Msg, 0, 0, 0) do
    DispatchMessage(Msg);
end.

Het 'programma' is nu volledig zichtbaar op elke Windows-PC.


Hier gebeurt iets wat in Delphi zelden gedaan wordt: een functie aanroepen in een bij naam genoemde dll - hier Comctl32.dll. Kost enkele bytes meer in grootte van het uitvoerbare bestand. Deze lijn kan ook weggelaten worden, en in de uses-clausule 'CommCtrl' mee opnemen om het commando "InitCommonControls" te laten werken. Maar dat voegt meer dan 20 KBytes toe aan het uitvoerbare bestand. Vandaar dat gekozen wordt voor een zo klein mogelijk uitvoerbaar programma. Op de site van MSDN (API Windows) staat wel dat InitCommonControls verouderd is, maar uiteindelijk is XP of Vista (yuk!) dat ook, en enkel daarvoor wordt dit stukje code gebruikt. Momenteel zou InitCommonControlsEx moeten gebruikt worden, en die vraagt een hoop parameters. Staat voor "initialization common controls extended".

Als er een functie zou moeten lopen na de klik op de 'knopklik', dan moet die functie of procedure eerst aangemaakt, en vervolgens opgeroepen in de WindowMainProc:

case Msg of
    WM_COMMAND: begin
      if LParam = Integer(hZoek) then Zoeken
      else if LParam = Integer(hHandleAndereKnop) then FunctieAndereKnop;
    end;
    WM_DESTROY: PostQuitMessage(0);

Er was vroeger een oud gezegde dat er maar 1 echt Windowsprogramma was, al de anderen waren er maar variaties van, met verschillende "switch(msg)" statements (dat was in C, in Delphi zou dat het identieke "case Msg of" statement zijn) in de berichtendispatching. Zo erg leken de broncodes van die eerste programma's op elkaar.

Windows styles + icoon

Tot hier toe werd geen naam.res bestand gebruikt in de voorbeelden hierboven.

Als de lijn {$R *.RES} toegevoegd wordt in de code (achter de 'uses' clausule) dan kan aan het programma een icoon toegevoegd worden, en de knop een moderner uitzicht krijgen. Die gebruikt dan de windows styles of "runtime themes" (in gebruik sedert Windows XP).

Venster met knop + style Venster met knop

Nu wordt een test.res bestand aangemaakt met Delphi, gewoon door bv. een klein icoon aan het project toe te voegen, en 'enable runtime themes' aan te vinken.

Styles + icoon

Dit kan niet met Delphi in een toepassing zonder VCL, maar er kan wel een nieuw project met VCL aangemaakt worden, en in Project -> options -> Application, runtime themes aangevinkt en een (in bestandsgrootte zo klein mogelijk) icoon gekozen. De broncode even opstarten en het nieuwe project opslaan. Vervolgens wordt het .res-bestand gekopiëerd naar de dpr (die zonder VCL uiteraard) met dezelfde naam. Bijvoorbeeld test.dpr en test.res bij elkaar in een folder. Hier is gekozen voor een icoon van 1950 bytes, weer eens, om de bestandsgrootte te beperken.

De lijn
{$R *.RES} doet de compiler zoeken naar een resource-bestand. Indien er geen staat, moet die lijn uitgecomment met '//', indien er bvb. een klein icoon moet toegevoegd, wordt een res-bestand van een andere applicatie gekozen, door Delphi aangemaakt met het gewenste icoon in, en bij dit bestand gezet. Moet dezelfde naam hebben als de dpr, want anders zal Delphi zijn standaard-icoon in een res-bestand aanmaken en mee compileren (90 kilobytes extra bij de exe). Als er een kleiner icoon gezet wordt van bvb. 8 of 2 kilobytes, blijft de exe ook zoveel kleiner.


Alleen hier

Informatie die niet elders op het internet staat: zichtbaarheid knoppen hierboven, bijna niets van te vinden, het paragraafje hieronder: helemaal niets van te vinden.
Delphi 3 kwam uit in 1997 en Delphi 5 in 1999. Toen was er nog geen sprake van Windows styles of runtime themes. Windows XP werd immers pas in 2001 uitgebracht. Programma's in Delphi 5 hebben dus hoekige knoppen. Moet er dan echt een nieuwe compiler (XE3 bvb.) gekocht worden om die styles te activeren? Neen dus. Als iemand een .res bestand heeft met - naast een icoon - ook de code voor de styles er in, dan zal ook een oude compiler een modern ogende toepassing afleveren.
Een .res-bestand van Delphi XE zetten bij een .dpr-bestand van Delphi 3 levert dus een modern uitzicht op, met afgeronde knoppen, zelfs indien gecompileerd met Delphi 3!
Moet wel dezelfde naam hebben: test.dpr vraagt test.res in dezelfde folder. En het icoon in het res-bestand moet beperkt worden tot 32 * 32 pixels. Zoniet blijven de knoppen ouderwets en hoekig.
Meer algemeen geweten kennis: dit kan zelfs zonder hercompilatie, door een bestand bij de oude exe te zetten. Bij een bestand "test.exe" wordt een bestand bijgezet "test.exe.manifest" in dezelfde folder. Gewoon dezelfde naam met een extra extentie "manifest". Inhoud van dit bestand:


<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity
    type="win32"
    name="CodeGear RAD Studio"
    version="15.0.3953.35171" 
    processorArchitecture="*"/>
  <dependency>
    <dependentAssembly>
      <assemblyIdentity
        type="win32"
        name="Microsoft.Windows.Common-Controls"
        version="6.0.0.0"
        publicKeyToken="6595b64144ccf1df"
        language="*"
        processorArchitecture="*"/>
    </dependentAssembly>
  </dependency>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel
          level="asInvoker"
          uiAccess="false"/>
        </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>
Dat is alles dus, ook het oude programma met hoekige knoppen - niet nodig om te hercompileren - krijgt nu een moderner uitzicht. Met de inhoud van de xml in dit .manifest bestand mag niet te veel gespeeld worden, want sommige delen zijn kritisch. Overigens mogen meerdere exe's een manifest-bestand hebben met dezelfde inhoud. Soms is een nieuwe sessie in Windows nodig (gebruiker afmelden en opnieuw aanmelden) eer Windows het manifest herkent en toepast.

Een knop met meerdere lijnen?

Die Windows API-calls zijn kort en krachtig. Hier een klein voorbeeldje van een knop met meerdere lijnen. Kost slechts 1 vermelding meer in de broncode (rode tekst)
Code van de knop in het bestand test.dpr



  hZoek := CreateWindow(
    'Button',
    'Tekst op knop die veel te lang is om op een lijn te passen',
    WS_VISIBLE or WS_CHILD or BS_PUSHBUTTON or BS_TEXT or BS_MULTILINE,
    6,
    6,
    110,
    50,
    hAppHandle,
    0,
    hInstance,
    nil);

Meer lijnen op 1 knop

Een csv-bestand doorzoeken

Maar genoeg gespeeld, een ernstig programma nu. Stel, er is een gegevensbestand met daarin woonplaats, naam, voornaam, telefoonnummer. Kan makkelijk aangemaakt met notepad, of in excel (csv-bestand, met komma gescheiden), of met een VBScript uit een database gehaald en naar een csv geschreven.

Er wordt een toepassing gemaakt om dit bestand snel kunnen doorzoeken, veel sneller dan met het openen van Excel en dan zoeken (met Ctrl + F). Het programma kan op vier duizendsten van een seconde 1000 lijnen doorzoeken en de resultaten weergeven. Getest op een HP Pavillion DV7 2.4 GHz - 6 gig - Windows 7 home premium 64-bits. Als 'Vandevenne' ingetikt wordt moeten alle lijnen getoond worden (inclusief de telefoonnummers) waar Vandevenne in voorkomt.
Idem voor 'Leuven' of 'Utrecht'. Ditmaal wordt een ernstig programma aangemaakt, met knoppen, velden, invoerbestand, uitvoerbestand, zoekfunctie op substring en schermoutput.
Indien bij de exe (slechts 40 kilobytes in Delphi XE) in dezelfde folder een bestandje gezet wordt "PersoonTelefoon.csv" met daarin een aantal lijnen (10 lijnen of 1.000.000, om het even)

LEUVEN,VANDEVELDE,PIETER,016454645
BRUSSEL,DUCHAMPS,BENJAMIN,023435656
ANTWERPEN,VERSTREKEN,ARMAND,032893427

Niet echt van belang, de lijnen mogen een andere inhoud hebben. Het programma doorzoekt dan de lijnen op het voorkomen van wat er ingetypt wordt. Om het eenvoudig te houden staan de gegevens in hoofdletters in het gegevensbestand zodat alles gevonden wordt. Dit kan gewijzigd worden - zie de lijn UpCase in de code hieronder, die lijn(en) worden dan best aangepast. Ofwel kan zowel de zoekstring als de lijnen omgezet worden naar hoofdletters vooraleer er gezocht wordt (dat vertraagt de werking maar het zoeken verloopt dan altijd correct).

Ergonomie: altijd het programma afwerken!


Ergonomie: er is niets erger dan een luie programmeur, die een quick & dirty programma aflevert, waarna de gebruikers duizenden keren onnodig van het toetsenbord naar de muis moeten overschakelen om het te gebruiken in de "standaard handeling". Eén keer iets meer moeite doen als programmeur betekent later tienduizenden overbodige bewegingen uitgespaard bij de gebruiker en dus een betere ergonomie. De zoekterm typen en dan de [Enter]-toets moet ook het zoeken starten. Dus niet de zoekterm typen en dan alleen op een knop kunnen klikken, dat is "onafgewerkt programmeren". Zoekterm typen + [ Enter] is echt wel verplicht om in te bouwen
De code hieronder lijkt lang, maar de Tab-toets moet werken, en Alt + O (knop "Opzoeken") ook. Als het gegevensbestand ontbreekt, moet de gebruiker een waarschuwing krijgen. En als er meerdere resultaten gevonden worden krijgt de gebruiker een melding om op de functietoets F2 te drukken, dus moet die code om op F2 te reageren ook voorzien worden. Omwille van egonomie moet de Enter-toets 'opzoeken', de F1-toets moet 'help' weergeven en 'Escape' moet het invoerveld leegmaken. Dus wordt een lokale keyboardhook toegevoegd, en wordt nagekeken of het gegevensbestand bij de exe in dezelfde folder staat. En ook een mutex om een 'single instance application' exe te maken.

De commentaar in deze broncode heeft geen enkele invloed op de bestandsgrootte van de exe. De compiler negeert dit volledig (de compiler negeert 'meerdere spaties', tabs en [Enters] overigens ook). Hier is de volledige broncode:
bestand test.dpr


program test;                       // en niet unit. Alleen program: werkt op zichzelf
                                    // verwacht dus test.dpr als eigen bestandsnaam
uses
  Windows, Messages, Shellapi;      // niets meer bijvoegen van SysUtils of ClipBrd om het programma
                                    // klein te houden. Zeker geen Forms toevoegen!
{$R *.RES}                          // verwacht test.res in dezelfde folder als test.dpr
{$O+}                               // compileren met optimalisatie aan
var                                 // globale variabelen - klassen, messages, hooks en handles
  wClass: TWndClass;
  Mutex, handleEerste, hAppHandle, hEdit, hLabel, hZoek, hClear: THandle;
  MainMsg: TMSG;
  FHook: HHook = 0;
  editfocus: boolean = False;

procedure InitCommonControls; stdcall; external 'Comctl32.dll';

procedure Zoek;                            // procedure om het csv-bestand te doorzoeken
var
  InputFile, OutputFile                    : TextFile;
  buffer, test                             : Array[0..MAX_PATH] of Char;
  i, j, atlres                             : integer;
begin
  GetWindowText(hEdit, test, MAX_PATH);    // zoekveld naar variabele 'test' halen
  i := Length(test);                       // omzetten naar hoofdletters. De functie Uppercase gebruiken 
  for j := 0 to i - 1 do begin             // gaat niet want sysutils is niet in de 'uses' opgenomen. 
    test[j] := UpCase(test[j]);            // Dus moet de de array van karakters 
  end;                                     // teken per teken naar hoofdletter omgezet worden.
  atlres := 0;                             // reset aantal resultaten
  AssignFile(InputFile, 'PersoonTelefoon.csv') ;// gegevensbestand klaarzetten
  Reset(InputFile);                        // om te lezen
  AssignFile(OutputFile, 'Results.txt');   // resultatenbestand klaarzetten en
  ReWrite(OutputFile);                     // leegmaken
  while not EOF(InputFile) do              // zo lang er lijnen zijn om te lezen
  begin
    ReadLn(InputFile, buffer);             // lees de lijn
    if Pos(test,buffer) > 0 then           // vergelijk tekst met zoekstring en indien gevonden
    begin
      inc(atlres);                         // aantal resultaten bijhouden (teller ophogen)
      SetWindowText(hEdit, buffer);        // zoekveld als voorlopige schermoutput gebruiken
      WriteLn(OutputFile, buffer);         // lijn naar outputbestand schrijven
    end;
  end;                                     // indien heel het bestand gelezen is
  CloseFile(InputFile);                    // bestanden uiteraard sluiten
  CloseFile(OutputFile);
  if atlres = 1 then begin                 // indien slechts 1 resultaat
    GetWindowText(hEdit, test, MAX_PATH);
    i := Length(test);
      for j := 0 to i - 1 do begin         // met spatie gescheiden in Edit zetten
        if test[j] = ',' then test[j] := ' '; // een deel selecteren kan dan met dubbelklik
      end;
    SetWindowText(hEdit, test);            // resultaat terug naar Editveld schrijven
    SendMessage(hEdit,EM_SETSEL,0,-1);     // selecteer de tekst in de "edit"     
  end
  else if atlres = 0 then begin            // tonen indien geen resultaten
    SetWindowText(hEdit, 'Niet gevonden in bestand');
    SendMessage(hEdit,EM_SETSEL,0,-1);
  end
  else if atlres > 1 then begin            // indien meerdere resultaten
    SetWindowText(hEdit, 'Meerdere resultaten - DRUK OP F2 ( toont in kladblok )');
    SendMessage(hEdit,EM_SETSEL,0,-1);
  end;
end;

procedure Clearen;                         // invoerveld leegmaken - Esc-toets of knop
begin
  SetWindowText(hEdit, '');
  SetFocus(hEdit);
end;

procedure Zoeken;                          // getriggered bij klikken op knop of Enter-toets
var
  nr: Array[0..MAX_PATH] of Char;
begin
  nr := '';
  GetWindowText(hEdit, nr, MAX_PATH);
  if (nr = '') then MessageBox(hAppHandle,
                               'Geen waarde ingevuld in het zoekveld!',
                               'Waarschuwing',
                               MB_ICONINFORMATION or MB_OK)
  else if not (GetFileAttributes('PersoonTelefoon.csv') = $ffffffff) then Zoek							   
  else MessageBox(hAppHandle,  'Gegevensbestand niet gevonden!',
                               'Waarschuwing',
                               MB_ICONINFORMATION or MB_OK)
end;

// toetsaanslagen onderscheppen. 
function KeyBoardHook(code: integer; wParam: word; lParam: cardinal): cardinal; stdcall;
begin
  Result := 0;
  if code < 0 then begin
    KeyBoardHook:=CallNextHookEx(FHook,code,wParam,lparam);
    Exit;
  end;
  if (lParam and $80000000) <> $00000000 then begin   // alleen op OnKeyUp en niet op OnKeyDown reageren
    case wParam of                                    // Welke toets is ingedrukt...
      VK_ESCAPE : Clearen;                            // Volgende lijnen: PChar('notepad.exe') is overbodig 
      VK_RETURN : if editfocus then Zoeken;           // bij ShellExecute(A) volstaat de string 'notepad.exe'.
      VK_F1     : ShellExecute(0,'open','notepad.exe', 'Leesmij.txt', nil, SW_SHOW);
      VK_F2     : ShellExecute(0,'open','notepad.exe', 'Results.txt', nil, SW_SHOW);
    end;
  end;
  CallNextHookEx(FHook,code,wParam,lparam);
  Exit;
end;

// functie voor berichtendispatching
function WndMessageProc(hWnd: HWND; Msg: UINT; WParam: WPARAM; LParam: LPARAM):
         UINT; stdcall;
begin
  case Msg of
    WM_COMMAND: begin   // Knopklikken verwerken
      if LParam = Integer(hZoek) then Zoeken
      else if LParam = Integer(hClear) then Clearen
      else if LParam = Trunc(hEdit) then if HIWORD(wParam) = EN_SETFOCUS
                                            then editfocus := True
                                         else if HIWORD(wParam) = EN_KILLFOCUS
                                            then editfocus := False;
    end;
    WM_CREATE: begin  // Invoerveld selecteren als het venster de focus krijgt
      SetFocus(hEdit);
      SendMessage(hEdit,EM_SETSEL,0,-1);
    end;
    WM_DESTROY: begin   // indien programma sluiten met windows 'X' of alt + F4
      if Mutex <> 0 then CloseHandle(Mutex);
      if FHook > 0 then UnHookWindowsHookEx(FHook);
      PostQuitMessage(0);
    end;
    WM_QUIT: begin      // indien softwarematig sluiten vanuit ander programma
      if Mutex <> 0 then CloseHandle(Mutex);
      if FHook > 0 then UnHookWindowsHookEx(FHook);
      PostQuitMessage(0);
    end;
  end;
  Result := DefWindowProc(hWnd,Msg,wParam,lParam);
  {call DefWindowProc moet voor normaal windowsgedrag}
end;


begin  // hoofdprogramma - formulier maken en berichtenlus
InitCommonControls();
// klasse voor hoofdformulier aanmaken en registreren
wClass.hInstance := hInstance;
with wClass do begin
  style := CS_DBLCLKS;                       // Venster dat ook dubbelklikken opvangt
  hIcon := LoadIcon(hInstance,'MAINICON');   // Het icoon uit het .res - bestand inladen en gebruiken
  lpfnWndProc :=  @WndMessageProc;           // Verwijzing naar de functie berichtendispatching
  hbrBackground:= (COLOR_BTNFACE + 1);       // Kleur van het venster
  lpszClassName:= 'Om Het Even';             // Naam van klasse niet belangrijk
  hCursor :=  LoadCursor(0, IDC_ARROW);      // Standaard "pijltje" cursor
  lpszMenuName := '';                        // Geen Menubalk in deze toepassing
  cbClsExtra := 0;                           // Geen extra bytes na de window class
  cbWndExtra := 0;                           // Extra voor structuur of windows instantie
end;
RegisterClass(wClass);

// hoofdformulier aanmaken
hAppHandle := CreateWindow(
    wClass.lpszClassName,	// PChar voor geregistreerde class name
    'Esc=Reset  F2=Results  F1=Help',
    WS_OVERLAPPED or WS_CAPTION or WS_SYSMENU or WS_MINIMIZEBOX,
    100,  // afstand van linkerrand scherm
    50,   // afstand van bovenrand schem
    318,  // breedte
    74,   // hoogte
    0,    // handle naar parent = geen want is zelf eerste instantie
    0,    {een ev. handle van hoofdmenu komt hier}
    hInstance,// handle van instantie van applicatie (en niet formulier)
    nil 	// pointer naar data voor venster creatie
    );
//hAppHandle = Parent. De knoppen enz. hieronder zijn de "children"

// Label aanmaken. een label wijzigt in principe niet
hLabel := CreateWindow('Static',
    '(Deel) naam of woonplaats',
    WS_VISIBLE or WS_CHILD or SS_LEFT,
    0,
    0,
    195,
    21,
    hAppHandle,
    0,
    hInstance,
    nil);
SendMessage(hLabel,WM_SETFONT,GetStockObject(ANSI_VAR_FONT),0);
// edit aanmaken
hEdit := CreateWindowEx(WS_EX_CLIENTEDGE,
    'Edit',
    'Typ hier een (gedeeltelijke) naam of woonplaats',
    WS_VISIBLE or WS_CHILD or WS_TABSTOP or ES_LEFT or ES_AUTOHSCROLL,
    2,
    21,
    306,
    18,
    hAppHandle,
    0,
    hInstance,
    nil);
SendMessage(hEdit,WM_SETFONT,GetStockObject(ANSI_VAR_FONT),0);
// twee knoppen
hZoek := CreateWindow(
    'Button',
    '&Opzoeken',           //die "&" onderlijnt de "O" en zorgt er voor dat Alt + O ook opzoekt
    WS_VISIBLE or WS_CHILD or BS_PUSHBUTTON or BS_TEXT or WS_TABSTOP,
    240,
    0,
    70,
    20,
    hAppHandle,
    0,
    hInstance,
    nil);
SendMessage(hZoek,WM_SETFONT,GetStockObject(ANSI_VAR_FONT),0);

hClear := CreateWindow(
    'Button',
    '&Leegmaken',
    WS_VISIBLE or WS_CHILD or BS_PUSHBUTTON or BS_TEXT or WS_TABSTOP,
    170,
    0,
    70,
    20,
    hAppHandle,
    0,
    hInstance,
    nil);
SendMessage(hClear,WM_SETFONT,GetStockObject(ANSI_VAR_FONT),0);

// venster klaarzetten ( + mutex + keyboardhook) en tonen en stay on top
Mutex := CreateMutex(nil, True, 'DeUniekeNaamVoorZoekProg');
if (Mutex = 0) or (GetLastError = ERROR_ALREADY_EXISTS) then     // Het programma is dus nog actief op de PC
  begin
    handleEerste := FindWindow(nil, 'Esc=Reset  F2=Results  F1=Help');    // Vorige instantie opzoeken
    MessageBox(hAppHandle, 'Dit programma is nog actief!!!'               // Bericht aan de gebruiker
                            + #13#10
                            + 'Wordt in beeld gezet zodra u dit venster sluit',
                            'Stop',
                            MB_ICONINFORMATION or MB_OK);
    SetForegroundWindow(handleEerste);                                    // Vorige instantie op voorgrond zetten
    ShowWindow(handleEerste, SW_SHOWDEFAULT);                             // En tonen
    SendMessage(hAppHandle,WM_DESTROY,0,0);                               // En deze instantie niet starten
  end
else begin                                                       // Het programma is nog niet actief op deze PC
  FHook := SetWindowsHookEx(WH_KEYBOARD, @KeyboardHook, 0, GetCurrentThreadID);//keyboardhook opstarten
  ShowWindow(hAppHandle, SW_SHOWDEFAULT);                                 // Programma tonen
  SetFocus(hEdit);                                                        // De tekst in Edit de focus geven
  SendMessage(hEdit,EM_SETSEL,0,-1);                                      // en selecteren
  SetWindowPos(hAppHandle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOACTIVATE       // Applicatie "on top" zetten t.o.v. andere vensters
    or SWP_NOMOVE or SWP_NOSIZE);
  UpdateWindow(hAppHandle);
end;

PostMessage(hAppHandle, WM_SYSKEYDOWN, 0, $20000000);    // Een Alt toetsaanslag simuleren om Alt + O en 
PostMessage(hAppHandle, WM_SYSKEYUP, 0, $20000000);      // Alt + L te activeren op de knoppen 

//  standaard windows programmalus
while GetMessage(MainMsg,0,0,0) do
If not IsDialogMessage(hAppHandle,MainMsg) then          // tabs wel verwerken
begin
  TranslateMessage(MainMsg);
  DispatchMessage(MainMsg);
end;

end.


In de functie WndMessageProc hierboven staat
else if LParam = Trunc(hEdit) then if HIWORD(wParam) = EN_SETFOCUS
                                     then editfocus := True
                                   else if HIWORD(wParam) = EN_KILLFOCUS
                                     then editfocus := False;

En in de code van de functie KeyBoardHook

VK_RETURN : if editfocus then Zoeken; 
Deze combinatie komt eigenlijk overeen met (keyboard event trapping) een procedure Edit1.OnKeyUp (met daarin: if key = VK_RETURN...), wat niet kan in een toepassing zonder VCL (of bijvoorbeeld in ANSI C) met een "Edit1.OnKeyUp". Daarom programmeren we het op deze manier: in de WndMessageProc (sectie WM_COMMAND) afvangen of de bedoelde controle de focus heeft, en vervolgens in de KeyBoardHook voorwaardelijk reageren op het hebben van de focus of niet.
Drie A4'tjes code, waarvan de helft formulieropmaak is. Het programma is nu af. Neen dus, software is nooit af. Er is bv. geen code ingebouwd om zeer lange lijnen op te vangen. Ook is de hoofdlettergevoeligheid van het zoeken is niet bepaald diep uitgewerkt. En het commando "ShellExecute" kan beter vervangen worden door een "CreateProcess" (mits ook een kleine structuur op te zetten), voor een lager geheugengebruik van het programma. Maar zelf had ik dat niet nodig, vandaar dat ik dit zo laat als voorbeeldje broncode.
Ons programma ziet er zo uit:

Zoekprogramma

Als een schrijfbeveiligd 'Leesmij.txt' bestandje toegevoegd wordt in dezelfde folder, dient dit als een eenvoudig helpbestandje. De functietoetsen werken nu, de toets 'enter' doet de zoekactie starten - de gebruiker kan dus 'Peeters [Enter]' intikken, zonder van het toetsenbord naar de muis te gaan, lekker ergonomisch. Uitvoerbaar bestand (Delphi XE): 39 kilobytes groot. Dezelfde code in Delphi XE2 is 44 kilobytes, in Delphi 3 is dat 23 kilobytes.

Een onoplettende gebruiker die het programma een tweede keer opstart, krijgt een melding (en de tweede start wordt niet uitgevoerd, en de eerste instantie wordt in het zicht gezet). De clausule 'uses ShellApi' voegt niets toe aan de bestandsgrootte van ons uitvoerbaar bestand. Iemand zou kunnen proberen om 'Clipbrd' toe te voegen aan de uses-clausule: de exe wordt dan direct 770 kilobytes groter! Dit alleen maar omwille van het klembord dat dan programmatorisch zou kunnen verwerkt worden. Gewoon selecteren, kopiëren en plakken in het Edit-veld kan je ook zonder 'uses Clipbrd'. Wordt dus best niet toegevoegd na vergelijking van de potentiële meerwaarde en de bestandsgrootte.


In Delphi (zonder VCL) kan de functie ShellExecute of ShellExecuteW (unicode, alle versies vanaf Delphi 9) en ShellExecuteA (ANSI, alle versies voor Delphi 9) 'voer een ander programma uit' extern (in Shell32.dll) gedeclareerd en dan aangeroepen worden. Maar beter is om ShellApi bij te voegen in de 'uses' clausule: dat heeft geen invloed op de bestandsgrootte van de exe. Daarna werkt ShellExecute, ShellExecuteW of ShellExecuteA volkomen normaal. Het kan het extern gedeclareerd, en de 'uses ShellApi' weglaten (in VB-stijl dus - maar het heeft weinig zin om dit te doen):

procedure ShellExecuteA(hwnd: THandle; lpOperation, lpFile, lpParameters,
  lpDirectory: string; nShowCmd: integer); stdcall; external 'Shell32.dll';


Compilers en bestandgrootte vergelijken

Het hierboven getoonde programma in Delphi XE2 of XE3 - met de VCL - geschreven, maakt een exe van ongeveer 1 megabyte groot.

Uiteindelijk heb ik het hele zootje nog eens herschreven (nogmaals: software is nooit af) in een kleine en snelle compiler in C: TCC (Tiny C compiler) - met exact dezelfde werking. Resultaat is ook een 32 bits Windowsprogramma van 6.656 bytes, dat op exact dezelfde manier werkt, en dezelfde functionaliteit biedt. In assembler (FASM - gratis compiler) schreef ik hetzelfde programma voor 32 bits Windows: 4.096 bytes groot. De bestandsgrootte is dus nog veel kleiner dan in Delphi.

In assembler heb ik alles nog eens herschreven in 64-bits code. Het hele programma is slechts 236 lijnen code. Inclusief een icoon, versie-info, windows styles en code om vensterafmetingen automatisch aan te passen is dat een exe van 9.728 bytes.

En nu ik toch bezig ben om hetzelfde programma op verschillende manieren aan te maken, een korte vergelijking van de grootte van het uitvoerbare bestand - dezelfde werking en functies, met verschillende compilers aangemaakt (de totaal gratis freeware compilers zijn aangeduid):



COMPILER              -    GROOTTE EXE    -  OPMERKINGEN

Lazarus               - 12.942.151 bytes  -  standaard instellingen [met debug-info in exe] (gratis)
Lazarus               -  1.634.304 bytes  -  built release [zonder debug-info in de exe] (gratis)
Delphi XE met VCL     -    881.664 bytes  -  of 800.768 bytes mits optimalisatie naar grootte
Delphi 3  met VCL     -    216.342 bytes  -  standaard instellingen
MinGw (command line)  -     63.099 bytes  -  standaard instellingen (gratis)
Delphi XE2 zonder VCL -     45.056 bytes  -  standaard, inclusief icoon van 1950 bytes
Delphi XE zonder VCL  -     39.936 bytes  -  standaard, inclusief icoon van 1950 bytes
Creative C++ / MinGw  -     34.571 bytes  -  standaard instellingen (gratis)
Delphi 3 zonder VCL   -     23.552 bytes  -  standaard, inclusief icoon van 1950 bytes
MinGw (command line)  -     13.312 bytes  -  mits optimalisatie naar grootte
TCC Tiny C Compiler   -      6.656 bytes  -  standaard (gratis)
Fasm Flat Assembler   -      4.096 bytes  -  (gratis) 

Kleinste programma = 1/3000ste van het grootste. 

Toen Delphi 2 ~ 3 uitkwam, moest het concurreren met Microsoft Visual Basic. Moeilijk was dat niet: Delphi was - net zoals VB - eenvoudig in gebruik, en genereerde een echte, standalone executable van pakweg 185 kilobytes. VB genereerde een kleiner pseudo-codebestand, dat feitelijk niet uitvoerbaar was. Je moest een in VB gemaakt programma installeren samen met de VBRUNxx.DLL pakketten, samen goed voor meer dan een megabyte. Die VBRUN-toestanden waren erg gevoelig aan de gebruikte versie, en moesten regelmatig een bijkomende update krijgen. Daarenboven was (en is) code in Delphi vele keren sneller in uitvoering. Het is dus begrijpelijk dat er toch een markt was - en nog steeds is - voor Delphi.

Maar hoe snel loopt dat nu? Snelheidstest

Een gegevensbestand van 35 Megabytes laten doorlopen, 1.032.000 lijnen. CSV-bestand met daarin telkens een rij bv. [Stad,Voornaam,Naam,telefoonnummer]. Enkel op de laatste lijn staat wat gezocht wordt, maar de programma's moeten alle lijnen doorlopen. Getest op een HP Pavillion DV7 2.4 GHz - 6 gig - Windows 7 home premium 64-bits.
Let wel, deze "snelheidstest" is zeer eenvoudig, een meer processorintensieve test zou ook kunnen (waar meer functies worden aangeroepen met diepere nesting en zware berekeningen of encryptie en veelvuldig overschrijven van bronbestanden of die een gedetailleerde fractal naar het scherm tekenen), waar vermoedelijk MinGw een betere uitvoeringssnelheid haalt dan TCC. Maar deze kleine test toont duidelijk aan dat een kleiner programma niet noodzakelijk sneller loopt. En al zeker niet evenredig. Het zou overigens gemakkelijk zijn om slechte code te schrijven in C of zelfs in assembler, die trager loopt dan Visual Basic of VB-script.

 
MinGw (C compiler)    -     63.099 bytes - 2.5 seconden  (13.312 bytes-versie is even snel) 
Delphi XE zonder VCL  -     39.936 bytes - 3.4 seconden
Delphi 3  zonder VCL  -     23.552 bytes - 1.6 seconde
TCC Tiny C Compiler   -      6.656 bytes - 1.8 seconde
(ter vergelijking)
VBScriptbestand.vbs   -        402 bytes - 4.8 seconden

Voor een popup-venster wordt in Delphi meestal het commando 'ShowMessage' gebruikt. Handig en snel om berichten in een kleine popup naar de gebruiker weer te geven. Toch is er een nadeel. Een 'stay on top' (TOPMOST) toepassing die een 'ShowMessage' weergeeft, verliest tijdelijk de 'on top' eigenschap, en de Showmessage-popup die de focus krijgt heeft die eigenschap evenmin. Als de gebruiker even naast de popup klikt (op een andere toepassing op de achtergrond) zitten zowel de toepassing als de popup uit het zicht achter die toepassing. Om dit euvel te vermijden is het beter om zelf een 'MessageBox' te laten weergeven via een Windows API call. Omdat we de MessageBox de handle van de applicatie meegeven, erft deze de 'TOPMOST' eigenschap van de toepassing die de box genereert. Beiden blijven dus 'on top', ook al klikt de gebruiker naast de Messagebox.

MessageBox(handle, 
           'Te veel resultaten',
           'Waarschuwing',
           MB_OK or MB_ICONINFORMATION);


Intussen heb ik voorbeelden aangemaakt van Delphiprogramma's (zonder VCL uiteraard) met "File Open/Save" - dialoogboxen, zonder "uses CommDlg" te gebruiken. Weer eens 88 Kilobytes uitgespaard: rechtstreeks de functies aanroepen in Comctl32.dll, en CommDlg niet gebruiken in de "uses"-clausule. Ook toepassingen met comboboxen die hun lijnen vanuit een bestand laden - eigenlijk is alles mogelijk op deze manier. Er is geen beperking, en de uitvoerbare bestanden zijn veel kleiner dan "gewone Delphi", alleen moet je soms zoeken hoe je iets moet doen.



Snellere functies in Delphi
De structuur van Windows-programma's in enkele programmeertalen
Een Windows-programma in FASM
Numlock problemen Windows 8 en 10