• Nieuwe artikelen

  • Introductie tekenen in Delphi

    Albert de Weerd

    Bijna elke applicatie gebruikt het beeldscherm om de gegevens die worden gemanipuleerd weer te geven. Dit artikel legt de beginselen van zelf tekenen in Delphi uit. Als basis voor de materie wordt eerst het hoe en waarom uitgelegd van schermopbouw en tekenen in het algemeen. Daarna volgt een beschrijving en uitleg van een eenvoudig programma waarin enkele veel voorkomende tekenopdrachten centraal staan.

    Inleiding

    Dit artikel is bedoeld voor iedereen die wil beginnen met tekenen in Delphi, maar ook voor iedereen die (meer) achtergrondinformatie over schermopbouw en tekenen in het algemeen wil opdoen.

    Vereist gereedschap

    In dit artikel wordt er vanuit gegaan dat je op de hoogte bent van standaard Delphi-handelingen zoals het plaatsen en gebruiken van standaard controls, het instellen van properties en het aanmaken van een standaard eventhandler. Ook de begrippen zoals method en private moeten bekend en vertrouwd zijn. Verder is het handig om te weten dat een programma of venster wordt aangestuurd door messages en dat deze messages door messagehandlers worden verwerkt. Aannemen dat dit zo werkt volstaat ook.

    Voor het kunnen namaken van het voorbeeldprogramma is elke standaard Delphi-installatie vanaf Delphi 5 voldoende.

    Inhoud

    Tekenen: definitie

    De term tekenen omvat in dit verband álle schermoperaties: tekst schrijven, inkleuren, lijnen trekken, cirkels tekenen, figuren weergeven, etc...

    Het beeldscherm waarop we tekenen is, zoals ongetwijfeld bekend, opgebouwd uit een aantal pixels welke gelijk is aan de schermresolutie, bijvoorbeeld 1024 pixels breed en 768 pixels hoog. Alles wat op je beeldscherm wordt weergegeven, wordt getekend door deze pixels in te kleuren. Dit lijkt misschien logisch of overbodig te vermelden, maar het is voor de begrijpelijkheid van de rest van dit artikel belangrijk om de betekenis hiervan helder te hebben. Je monitor heeft slechts één scherm waarop getekend kan worden. Dus als er getekend word, dan wordt er óver het vorige heengetekend en is dat wat er eerst stond gewoon verdwenen.

    De Windows en Delphi term voor tekenen is painting of drawing. Alles wat niet automatisch door je programma wordt getekend, maar waar je als programmeur zelf zorg voor draagt wordt custom painting of custom drawing genoemd. Vanwege gemak en het Nederlandstalig karakter vallen al deze begrippen in dit artikel onder de term tekenen.

    Het beeldscherm wordt aangestuurd door de videokaart. De videokaart wordt met commando's aangestuurd door het besturingssysteem1. Een citaat uit de Win32 Developer's Reference2:

    Microsoft ® Windows™ provides a rich environment for painting and drawing, but, because Windows is a multitasking operating system, applications must cooperate with one another when accessing the screen. To keep all applications functioning smoothly and cooperatively, Windows manages all output to the screen. Applications use windows as their primary output device rather than the screen itself.

    Het tekenen via Windows vraagt kennis van WinAPI3. Gelukkig neemt Delphi een groot deel van het programmeren via WinAPI voor onze rekening, zodat we op een makkelijke manier via het besturingssysteem iets op het scherm weer kunnen geven.

    1) De videokaart kan ook rechtstreeks door je applicatie aangestuurd worden, maar die mogelijkheid valt ver buiten de doelstelling van dit artikel.
    2) In Delphi bereikbaar via het Help-menu "Windows SDK" (Delphi 5)
    3) Windows Application Programming Interface. Veel van de WinAPI-functies zijn in Delphi toegankelijk gemaakt via de Windows-unit.

    Tekenen: de aansturing

    Hoe weet het besturingssysteem wat er getekend moet worden?

    Stel dat je een lijn op je Form wilt tekenen. Stel vervolgens dat je het Form minimaliseert, of dat je een ander Form over jouw Form plaatst. We weten inmiddels dat de lijn nu weg is, dus hoe weet het besturingssysteem dat de lijn opnieuw getekend moet worden als je Form weer de focus krijgt? We zouden de volgende twee mogelijkheden kunnen bedenken:

    1. het besturingssysteem onthoudt dat de lijn is getekend en tekent hem opnieuw vanuit het geheugen,
    2. het besturingssysteem onthoudt dit niet, maar berekent wat er getekend moet worden.

    De eerste mogelijkheid valt al snel af als we bedenken dat dat veel geheugen zou vragen. Ook zou deze mogelijkheid intensieve interactie met het geheugen vragen om de wijzigingen van het venster bij te houden (denk bijvoorbeeld aan bewegende beelden). 4

    Dus er wordt elke keer opnieuw berekend wat er getekend moet worden. Nu besteed Windows deze berekening op een handige manier uit. Windows laat aan het Form dat (opnieuw of deels) in beeld komt weten dat het zichzelf moet tekenen. Dit doet Windows met een bericht in de vorm van een WM_PAINT message:

    An application draws in a window at a variety of times: when first creating a window, when changing the size of the window, when moving the window from behind another window, when minimizing or maximizing the window, when displaying data from an opened file, and when scrolling, changing, or selecting a portion of the displayed data. Windows manages actions such as moving and sizing a window. If an action affects the content of the window, Windows marks the affected portion of the window as ready for updating and, at the next opportunity, sends a WM_PAINT message to the window.

    Het Form "vangt" dit bericht af met een WM_PAINT messagehandler en reageert navenant door zichzelf te tekenen. Hoe dit tekenen in zijn werk gaat komen we in het voorbeeldprogramma op terug. Voorlopig is het voldoende om te weten dat het Form zichzelf opnieuw tekent als het een WM_PAINT message van Windows krijgt.

    Wanneer moet het Form opnieuw getekend worden? De redenen op opnieuw te moeten tekenen, zoals hierboven genoemd in het voorbeeld met de lijn, zijn vanzelfsprekend: bij het verkrijgen van de focus als het daarvoor niet zichtbaar was (je activeert het Form bijvoorbeeld vanaf de taakbalk). Maar er moet vaker opnieuw getekend worden dan je denkt! Zelfs een actief Form wordt veelvuldig opnieuw getekend. Denk maar eens aan het scrollen van je Form: waar komt de nieuwe pagina vandaan? Of als je je Form iets vergroot: waar komt het nieuwe gedeelte van het Form vandaan? Of als je je Form iets verkleint: waar komt je bureaublad vandaan? Bij al deze acties wordt er steeds weer door Windows aan het Form of het achterliggende venster gevraagd zichzelf opnieuw te tekenen. Dit zijn dus nogal wat messages! Uiteraard is het van groot belang om je tekenopdrachten zo efficiënt mogelijk te houden, juist omdat die als gevolg van al die messages zo ongelofelijk vaak worden uitgevoerd.

    Eigen tekenopdrachten zullen we dus op één of andere manier moeten toevoegen aan de WM_PAINT messagehandler. Deze messagehandler zit diep verborgen in een private method van een voorouder van TForm:

      private
        procedure WMPaint(var Message: TWMPaint); message WM_PAINT;

    Omdat een dergelijke private method voor jou als gebruiker van het Form niet bereikbaar is, "verlengd" Delphi deze handler met behulp van een OnPaint event (zie het tabblad events in de Object Inspector). Indien je wilt dat er nog meer getekend wordt dan alleen de standaard ingekleurde achtergrond, zoals bijvoorbeeld de hierboven veronderstelde lijn, dan kan je deze lijn tekenen in je eigen OnPaint eventhandler. Elke keer als het Form nu een WM_PAINT message van Windows krijgt wordt eerst het Form zelf getekend in de messagehandler, wordt vervolgens het OnPaint event van het Form aangeroepen en worden alle door jou toegevoegde tekenopdrachten uitgevoerd.

    Beide handlers kunnen meerdere tekenopdrachten hebben, en een tekenopdracht, bijvoorbeeld: "teken lijn", of: "schrijf tekst", wordt diréct uitgevoerd. Je zou kunnen bedenken dat Windows eerst alle opdrachten in het geheugen verwerkt en het resultaat pas laat zien bij het verlaten van de WM_PAINT messagehandler, of bij het verlaten van de OnPaint eventhandler (mits toegewezen), maar dat is standaard niet het geval5. We kunnen dus stellen dat het scherm (heel snel) beetje bij beetje wordt opgebouwd.

    4) Windows gebruikt wel optimalisaties om vanuit het geheugen te tekenen (bijvoorbeeld bij het verplaatsen van de muis) of om te kunnen beslissen òf er wel opnieuw getekend moet worden. Deze optimalisaties zijn ook aan te sturen d.m.v. het gebruik van o.a. clipping.
    5) De techniek om tekenopdrachten eerst naar het geheugen te schrijven wordt Double Buffering genoemd. Vaak wordt dit gebruikt om bijvoorbeeld flikkering tegen te gaan.

    Tekenen: schermopbouw

    Omdat we eerder al hebben vastgesteld dat een pixel die opnieuw wordt ingekleurd, de oude ingekleurde pixel vervangt, bestaat dus de mogelijkheid dat een tekenopdracht een eerdere tekenopdracht deels of volledig teniet doet. De laatste tekenopdracht voor één bepaalde pixel is dus definitief. Deze werking kan soms lastig zijn, omdat je niet wilt dat vorig tekenwerk wordt overgetekend. Houdt daarom altijd rekening met de juiste tekenvolgorde: alleen het laatste is zichtbaar.

    Tot dusver hebben we het beeldscherm beschreven, als ware het een krijtbord: je kunt met meerdere kleurtjes krijten tekenen, maar altijd slechts op één plek tegelijk en elke nieuwe tekening vervangt de oude. Zou het niet handig zijn om een ingewikkelde tekening op bijvoorbeeld een stickit-plakkertje te tekenen en deze op het krijtbord te plakken? Voordeel daarvan zou zijn dat je hem niet steeds opnieuw hoeft te tekenen bij eventueel verplaatsen; je pakt het plakkertje gewoon op en plakt hem ergens anders neer. Wel, dergelijke stickit-plakkertjes bestaan in Windows en heten windowed controls6, waarmee het tijd is geworden voor de introductie en uitleg van 3 begrippen binnen Delphi:

    • WinControl
    • Canvas
    • Control

    Een WinControl (van het type TWinControl) is, om de vergelijking even te handhaven, het stickit-plakkertje, een windowed control. Voorbeelden van WinControls zijn: TForm, TEdit, TMemo, TButton. Deze controls staan altijd op de voorgrond. Uiteraard kunnen er meerdere WinControls tegelijk zijn, en zelfs ook op dezelfde plaats. In dat geval gaat weer hetzelfde op zoals hiervoor uitgelegd: de laatste WinControl die getekend wordt staat op de voorgrond. De volgorde waarin de diverse WinControls staan, wordt ook wel Z-order7 genoemd. Hoe hoger de Z-order, hoe later er wordt getekend. De eigenaar van de controls (bijvoorbeeld het Form waar de controls op staan) houdt deze Z-order bij, zodat alles in de juiste volgorde wordt getekend. Deze volgorde kun je als gebruiker wijzigen met de TWinControl methods BringToFront ("teken later") en SendToBack ("teken eerder").

    Voor de volledigheid is het goed te vermelden dat alleen WinControls de hiervoor besproken WM_PAINT messages kunnen ontvangen. Zoals we weten gaat de betreffende messagehandler de tekenopdrachten verzorgen, echter op een WinControl kan niet getekend worden. Hiermee stopt dan ook voor wat betreft het WinControl de vergelijking met het stickit-plakkertje. We hebben dus een ander hulpmiddel nodig....

    Het Canvas (van het type TCanvas) is het krijtbord waarop wel getekend kan worden. De WinAPI term voor Canvas is Device Context (DC). Een Canvas is onderdeel van een WinControl en vormt daarvan de achtergrond. Een Canvas is een in formaat onbeperkt oppervlak welke door het formaat van het WinControl wordt begrensd. Dit wetende is alles op het scherm uiteindelijk dus weer één groot Canvas, opgedeeld in meerdere kleinere canvassen, behorend bij de vele verschillende WinControls.

    Om te kunnen tekenen op een Canvas, biedt deze de volgende gereedschappen:

    • een Pen, waarmee lijnen, bogen en punten getekend kunnen worden,
    • een Brush, waarmee kan worden ingekleurd,
    • een Font, waarmee het lettertype voor het tekenen van tekst wordt bepaald.

    Elk gereedschap heeft zo weer zijn eigen eigenschappen en instellingen, welke we verderop gaan behandelen in het voorbeeldprogramma. Voorlopig is het voldoende om te weten dat een Canvas deze gereedschappen biedt en dat het altijd op de achtergrond ligt.

    Voor zover de vergelijking met het krijtbord tot nu toe stand hield, houdt deze vergelijking bij het Control (van het type TControl) zeker op. Het Control is geen WinControl en heeft ook geen Canvas. Een control tekent zichzelf op het Canvas van zijn eigenaar die wel een WinControl is. Voorbeelden van Controls: TLabel, TBevel en TImage. Een control heeft net als een WinControl ook een Z-order, maar verliest het daarbij altijd van een WinControl omdat het zichzelf op een Canvas tekent. Een Control kan wel boven een ander Control liggen. 8

    Tot zover de theoretische achtergrondinformatie bij het tekenen. Laten we hiervan nu maar eens wat in de praktijk ten uitvoer brengen.

    6a) Het kunnen oppakken en verplaatsen van een tekening is niet de hoofdreden van het bestaan van windowed controls.
    6b) In WinAPI wordt elk windowed control kortweg "window" genoemd.
    7) De Z in Z-order slaat op de virtuele Z-as van het coördinatenstelsel waarin getekend wordt, welke loodrecht door je beeldscherm wordt gedacht te lopen.
    8) Gemakshalve wordt in het algemeen gebruik onder de term "controls" zowel die van het type TControl als die van het type TWinControl verstaan. Binnen Delphi zijn beide ook hiërarchisch met elkaar verbonden: een TWinControl is een TControl, TControl is de voorouder van TWinControl.

    Toepassing in de praktijk

    Delphi biedt al een grote hoeveelheid componenten waarmee ingewikkelde tekenopdrachten kunnen worden uitgevoerd. Denk hierbij aan TImage voor het weergeven van een afbeelding, TProgressBar voor het visueel weergeven van voortgang, of TChart voor het visueel weergeven van ingewikkelde statistieken. Zoals aangekondigd houden we het bij een eenvoudig voorbeeldprogramma, maar het moet wel nuttig en zinvol zijn. Dus het voornemen is om iets te gaan tekenen dat niet met standaard Delphi-componenten mogelijk is...

    Onze opdrachtgever is een schilder die een computerprogramma wil laten maken waarmee hij zijn voorraad verf kan bijhouden. De schilder gebruikt drie kleuren verf: groen, crème en blauw, en de voorraad van elke kleur moet visueel worden weergegeven. Natuurlijk gaan we niet in het wilde weg tekenen, maar kiezen we eerst de juiste componenten waarop we gaan tekenen. Het lijkt de schilder leuk en overzichtelijk om zijn voorraad in tekstvelden in te voeren, en in gevulde verfblikken visueel weer te geven. Voor het tekstveld maken we gebruik van een TEdit. Voor het verfblik zijn we op zoek naar een component dat een Canvas heeft waarop we kunnen tekenen en welke een OnPaint event beschikbaar stelt zodat onze tekening zal worden ververst als dat nodig is. Het component dat we zoeken is een PaintBox, welke precies voor deze doeleinden is gemaakt.

    Begin een nieuwe applicatie en creëer een Form met 3 Edits, 6 Labels en 3 PaintBoxen totdat het er ongeveer zoals hieronder uitziet. Geef daarbij de PaintBoxen een Height van 137. Geef alle componenten een Width van 137 en zet voor alle componenten de Bottom-Anchor op True en de rest van de Anchors op False. Tip: gebruik hiervoor de multiselect optie van de IDE. Stel als laatste Form1.Constraints.MinWidth en MinHeight gelijk aan respectievelijk de Width en Height van het Form:

    unit Unit1;
    
    interface
    
    uses
      Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
      StdCtrls, ExtCtrls;
    
    type
      TForm1 = class(TForm)
        LabelGroen: TLabel;
        LabelCreme: TLabel;
        LabelBlauw: TLabel;
        PaintBoxGroen: TPaintBox;
        PaintBoxCreme: TPaintBox;
        PaintBoxBlauw: TPaintBox;
        EditGroen: TEdit;
        EditCreme: TEdit;
        EditBlauw: TEdit;
        Label4: TLabel;
        Label5: TLabel;
        Label6: TLabel;
      private
        { Private declarations }
      public
        { Public declarations }
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.DFM}
    
    end.

    Wel, tot zover redelijk basic. De PaintBoxen gaan we straks de verfblikken op tekenen, maar eerst gaan we nog wat aan het uiterlijk van onze applicatie sleutelen. Voor een duidelijk scheiding tussen de drie "kolommen" met controls gaan we een verticale lijn tekenen tussen elke kolom. Het component waar we die lijnen op gaan tekenen is al aanwezig: we gebruiken daar het Form zelf voor, want zoals eerder besproken beschikt het Form over een OnPaint event. Dus we maken een OnPaint eventhandler aan (door in de Object Inspector te dubbelklikken achter het OnPaint event van het Form). Vervolgens gaan we in deze method de twee lijnen tekenen. Hier volgt eerst de code, daarna de uitleg.

    procedure TForm1.FormPaint(Sender: TObject);
    begin
      Canvas.MoveTo(158, 10);
      Canvas.LineTo(158, 215);
      Canvas.MoveTo(318, 10);
      Canvas.LineTo(318, 215);
    end;

    De tekenopdrachten die we hier gebruiken zijn: Canvas.MoveTo en Canvas.LineTo. Canvas is in dit geval een al aanwezige property van het Form en naast de reeds bekende gereedschappen Pen, Brush en Font, biedt een TCanvas ook routines om met die gereedschappen wat te doen. De procedure MoveTo zet de Pen klaar op een punt X, Y (de argumenten van MoveTo). De procedure LineTo tekent een lijn vanaf het punt waar de Pen staat (Canvas.PenPos) naar een punt X, Y (de argumenten van LineTo). Hierbij wordt er met hetzelfde coördinatensysteem gewerkt als waarmee componenten op het Form worden geplaatst, dus de linkerbovenhoek is 0, 0 en de rechteronderhoek is ClientWidth, ClientHeight. Let op: deze zijn niet gelijk aan de properties Width en Height van het Form, welke het totale formaat van het Form inclusief border instellen. De clientspace waarop getekend kan worden is het oppervlak van het Form tussen de borders.

    Als het een beetje goed uitkomt, staan er na het starten van de applicatie nu twee zwarte lijnen tussen de drie kolommen, of pas de coördinaten in de code even aan zodat de lijnen wel tussen de controls worden getekend. Merk op dat bij het vergroten van het Form, de getekende lijnen op hun oude plek blijven staan, maar dat deze niet meer helemaal zichtbaar zijn als de Labels en de Edits vanwege de Anchor-instellingen worden verplaatst. We zien de hiervoor besproken schermopbouwvolgorde aan het werk: de edits (TWinControl's) staan altijd op de voorgrond en de labels (TControl's) worden ook over de lijnen heen getekend (dus de lijnen worden eerder getekend dan de labels). Je ziet, met behulp van de nodige theoretische achtergrondinformatie hebben we nu verklaard, en begrijpen we, wat er allemaal op het scherm gebeurd.

    Onze schilder wil dat de lijnen ook op de juiste plaats worden getekend als het Form van formaat veranderd. Hiervoor moeten we de code in de OnPaint eventhandler aanpassen zodat de plaats van de lijnen afhankelijk wordt van het formaat van het venster. Het is overigens een goede gewoonte om bij voorkeur de plaats van tekeningen te berekenen in tegenstelling tot deze te laten afhangen van vaste coördinaten. Eén en ander is daarbij natuurlijk geheel afhankelijk van het soort applicatie dat je schrijft. Om aan de wens van de schilder te voldoen, passen we de OnPaint eventhandler even aan:

    procedure TForm1.FormPaint(Sender: TObject);
    const
      Marge = 12;
    begin
      Canvas.MoveTo(PaintBoxCreme.Left - Marge, LabelCreme.Top);
      Canvas.LineTo(PaintBoxCreme.Left - Marge, ClientHeight - Marge);
      Canvas.MoveTo(PaintBoxBlauw.Left - Marge, LabelBlauw.Top);
      Canvas.LineTo(PaintBoxBlauw.Left - Marge, ClientHeight - Marge);
    end;
    

    De berekeningen in dit stukje code spreken waarschijnlijk voor zichzelf: er is een marge van 12 pixels gekozen welke de horizontale afstand van de lijn tot de volgende PaintBox en de vertikale afstand tot de onderkant van het Form bepalen.

    Als we het programma starten komen we tot de vervelende ontdekking dat dit helaas nog niet het gewenste resultaat geeft bij het wijzigen van het formaat van het Form. Wat kan er aan de hand zijn? Vol vertrouwen in onze eigen code kunnen we eigenlijk niets anders dan de conclusie trekken dat Windows niet in de gaten heeft dat het Form opnieuw getekend moet worden. Op zich is dit misschien ook wel logisch, want er komt bij het vergroten van het Form immers niets nieuws in het beeld. Dus hoe zou Windows moeten weten dat het Canvas van het Form moet worden ververst?

    Op één of andere manier moeten we het Form dus vertellen dat het zichzelf opnieuw moet tekenen bij het vergroten van het Form. Dit is het juiste moment om even de zojuist aangeleerde theorie terug naar boven te halen: zouden we zelf een WM_PAINT message naar het Form kunnen sturen? Wel, Delphi blijft ons verbazen en het blijkt dat TForm reeds een procedure ter beschikking stelt die precies datgene doet: Repaint. Met ons doel in gedachten voegen we de procedure Repaint toe bij het OnResize event:

    procedure TForm1.FormResize(Sender: TObject);
    begin
      Repaint;
    end;

    Start je programma maar weer eens op. Hoe groot je het Form nu ook maakt: de lijnen staan altijd goed, precies wat we wilden bereiken.

    Trots als we zijn laten we de eerste proefversie aan de schilder zien, welke tot onze teleurstelling vindt dat de zwarte lijnen te nadrukkelijk aanwezig zijn. Wel, het tekenen van de verfblikken komen we zo niet aan toe, maar vol goede moed storten we ons op dit nieuwe probleem. Zoals eerder opgemerkt hebben de gereedschappen van het Canvas diverse eigenschappen welke kunnen worden ingesteld. Omdat we de scheidingslijnen iets minder "hard" willen tekenen, kiezen we voor een wat zachtere kleur: clSilver, welke we toewijzen aan de Color property van de Pen:

      Canvas.Pen.Color := clSilver;

    Het is erg verleidelijk om deze éne regel code ook aan de OnPaint eventhandler toe te voegen. Maar dan zouden we direct al zondigen tegen het advies aan het begin van dit artikel: pas op met onnodige aanroepen in een OnPaint eventhandler! De penkleur hoeft wat ons betreft nooit meer veranderd te worden, dus is het beter om die code toe te voegen aan het OnCreate event van het Form, want dat event wordt eenmalig aangeroepen:

    procedure TForm1.FormCreate(Sender: TObject);
    begin
      Canvas.Pen.Color := clSilver;
    end;

    Merk op dat de standaard kleurinstelling van de Pen dus zwart is.

    Wel, het wordt nu tijd voor het echte tekenwerk: de blikken verf. Voeg een procedure PaintBoxPaint toe aan de declaratie van TForm1 van het type TNotifyEvent en gebruik class-completion (Ctrl-Shift-C) om de procedure te implementeren:

      TForm1 = class(TForm)
        ...
        procedure PaintBoxPaint(Sender: TObject);
      private
        { Private declarations }
      public
        { Public declarations }
      end;
    procedure TForm1.PaintBoxPaint(Sender: TObject);
    begin
    
    end;

    Wijs vervolgens deze procedure toe aan de OnPaint events van alle PaintBoxen. Stel ook de Color property in voor elke PaintBox: clLime, clInfoBk, clBlue.

    De zojuist aangemaakte procedure gaan we als OnPaint eventhandler gebruiken voor álle PaintBoxen, dus zullen we met de Sender parameter moeten gaan achterhalen welke PaintBox getekend wil worden. De Tag property van de PaintBox gebruiken we voor het aantal liters dat er in het verfblik zit. Hier volgt eerst weer de code:

    procedure TForm1.PaintBoxPaint(Sender: TObject);
    const
      CanWidth = 60;
      CanHeight = 75;
      Marge = 10;
      MaxLiters = 15;
      CanThickness = 6;
    var
      CanRect: TRect;
      Percentage: Single;
    begin
      with TPaintBox(Sender), Canvas do
      begin
        Brush.Color := clAqua;
        FillRect(ClientRect);
        if Tag > MaxLiters then
          Tag := MaxLiters;
        if Tag < 0 then
          Tag := 0;
        Percentage := 100 * (Tag / MaxLiters);
        Font.Size := 10;
        Font.Style := [fsBold];
        TextOut((Width div 2) - 15, 30, Format('%d%%', [Trunc(Percentage)]));
        Pen.Width := CanThickness;
        CanRect := Bounds((Width - CanWidth) div 2, Height - CanHeight - Marge,
          CanWidth, CanHeight);
        with CanRect do
        begin
          Polyline([TopLeft, Point(Left, Bottom), BottomRight, Point(Right, Top)]);
          Pen.Width := 2;
          Arc(Left - 7, Marge, Right + 7, (2 * Top) - Marge, Right, Top + 3,
            Left, Top + 3);
        end;
        InflateRect(CanRect, -CanThickness div 2, -CanThickness div 2);
        CanRect.Top := CanRect.Bottom -
          Trunc((CanHeight - CanThickness) * (Percentage / 100));
        Brush.Color := Color;
        FillRect(CanRect);
      end;
    end;
    

    Laat dit maar even voor wat het is, en voeg de volgende code toe aan de OnChange events van de drie Edits:

    procedure TForm1.EditGroenChange(Sender: TObject);
    begin
      try
        PaintBoxGroen.Tag := StrToInt(EditGroen.Text);
      except
      end;
      PaintBoxGroen.Repaint;
    end;
    
    procedure TForm1.EditCremeChange(Sender: TObject);
    begin
      try
        PaintBoxCreme.Tag := StrToInt(EditCreme.Text);
      except
      end;
      PaintBoxCreme.Repaint;
    end;
    
    procedure TForm1.EditBlauwChange(Sender: TObject);
    begin
      try
        PaintBoxBlauw.Tag := StrToInt(EditBlauw.Text);
      except
      end;
      PaintBoxBlauw.Repaint;
    end;
    

    Voordat we ons gaan verdiepen in de code, gaan we eens stiekem kijken naar wat we nu eigenlijk aan het doen zijn. Start het programma en voer een willekeurig aantal liters in de Edits in.

    Zoals je ziet is dit al wat ingewikkelder dan ons vorige tekenwerk, maar daar laten we ons niet door afschrikken, en bekijken eerst weer de tekenopdrachten:

    • de procedure FillRect(const Rect: TRect) kleurt een rechthoek in. De functie wordt twee keer gebruikt, allereerst om de achtergrond te kleuren en aan het einde van de procedure om de inhoud van het blik te kleuren.
    • de procedure TextOut(X, Y: Integer; const Text: string) schrijft het percentage dat het blik is gevuld vanuit locatie X, Y naar beneden en naar rechts. De tekst begint 15 pixels links van het midden van de PaintBox.
    • de procedure PolyLine(Points: array of TPoint) tekent lijnen aan elkaar van punt naar punt, naar punt, naar punt, etc... De zijkanten en de bodem van het blik hadden we ook met de routines MoveTo en LineTo kunnen tekenen, maar ja, die kenden we al...
    • de procedure Arc() met een verschrikkelijke hoeveelheid argumenten, tekent het hengsel van het verfblik. Zie de Help van Delphi voor gedetailleerde uitleg over deze routine.

    Wat is er ondertussen met de gereedschappen gebeurd?

    • de property Brush.Color bepaald de kleur van de "verfkwast" welke FillRect gebruikt voor het inkleuren. De achtergrond van de PaintBox wordt geheel met clCyan ingekleurd, en aan het eind wordt de kleur van de Brush ingesteld op die van de PaintBox, de kleur die we hebben ingesteld in de Object Inspector.
    • de property Size en Style van het Font bepalen het formaat en de stijl van het lettertype wat wordt gebruikt in procedure TextOut.
    • de Pen heeft ook een nieuwe eigenschap geïntroduceerd: de dikte van de te tekenen lijn. Hiermee kan je elke gewenste dikte tussen een FineLiner en een Edding instellen.

    De rest van de PaintBoxPaint procedure bestaat voornamelijk uit het valideren van de Tag property (er past maximaal 15 liter verf in het blik) en wat Rect-manipulatie om de juiste waarden voor de diverse parameters van de tekenopdrachten te verkrijgen. De drie OnChange eventhandlers stellen de Tag property van de juiste PaintBox in en vertellen de desbetreffende PaintBox dat deze zichzelf opnieuw moet tekenen door middel van de procedure Repaint. Mocht het je, na deze uitleg van de procedure PaintBoxPaint, nog duizelen als ware het abracadabra, vergelijk dan in de bijgesloten sourcecode de geheel uitgeschreven variant van de procedure.

    De sourcecode van dit voorbeeldprogramma is hier te downloaden: VerfVoorraad.zip.

    Het programma is klaar en de schilder is helemaal tevreden met het resultaat. Probeer hier en daar zelf nog eens wat te veranderen of toe te voegen aan de code of de opmaak, en bekijk of je wijzigingen het verwachtte resultaat geven. Ik wens je veel tekenplezier met Delphi!

    Tot slot

    Uiteraard zijn er nog veel meer methods en properties van TCanvas welke behandeld zouden kunnen worden. Helaas valt dat niet meer binnen deze "introductie tekenen in Delphi". Hierbij wil ik nog wel de onderwerpen benoemen die in dit artikel niet aan bod zijn gekomen, maar die nog meer tekenmogelijkheden bieden:

    • het gebruikmaken van WinAPI functies voor uitgebreidere tekenmogelijkheden,
    • custom drawing van bijvoorbeeld TMainMenu, TStringGrid, TTreeView, etc...
    • het bouwen en implementeren van een TCustomControl (TWinControl) of een TGraphicControl (TControl),
    • bewegende beelden en animaties.

    Conclusie

    We hebben een theoretische uiteenzetting gevolgd over het hoe en waarom van tekenen in Delphi. De oorsprong van al het tekenwerk ligt bij de opdrachten van Windows in de vorm van WM_PAINT messages naar de schermonderdelen. Binnen Delphi kunnen we met behulp van het OnPaint event ook reageren op deze messages en onze eigen tekenopdrachten uitvoeren. Binnen de definitie van tekenen hebben we vastgesteld dat elke tekening op het beeldscherm de vorige verdringt en dat daardoor alles in de juiste volgorde getekend moet worden. De volgorde van de schermopbouw wordt bepaald door de Z-Order van de controls die getekend worden, en of deze controls van het type TWinControl of TControl zijn. Verder hebben we geleerd dat we kunnen tekenen op een Canvas, wat we aan het eind van het artikel hebben beoefend in een voorbeeldprogramma.