Bob Swart (aka Dr.Bob)
Internet Plaatjes vanuit Delphi

Het internet bestaat niet alleen maar uit HTML bestanden, maar ook uit plaatjes (.gif, .jpg of tegenwoordig zelfs .png bestanden), plug-in bestanden (audio, video, pdf, etc). In mijn WebBroker en InternetExpress artikelen van de afgelopen tijd heb ik echter met name aandacht besteed aan het produceren (genereren) van dynamische HTML pagina's. Het is dan ook de hoogste tijd om te zien hoe we andere output kunnen genereren, en wel in het bijzonder plaatjes uit tabellen, gebruikmakend van zowel WebBroker als (lastiger) InternetExpress.

De Tabel
Voordat we plaatjes kunnen genereren uit een tabel, moeten we ze eerst in een tabel stoppen natuurlijk. Voor dit artikel is de BIOLIFE tabel niet bruikbaar, omdat de IMAGE velden uit deze tabel niet in GIF of JPEG formaat zijn opgeslagen (maar in WMF formaat). Dus begin ik met het specificeren van een nieuwe tabel, genaamd developer.db, met records die bestaan uit vier velden: Name, Email, Website en Photo. Photo is hierbij van type G (= Graphic). We moeten zelf zorgen dat de inhoud van dit veld in GIF of JPEG formaat is. Voor het vullen van de tabel, en dan met name het Photo veld, kunnen we gebruik maken van de LoadFromFile method van de TGraphicField, om externe .jpg bestanden in het veld te stoppen.

WebBroker
Als deze tabel klaar staat, kunnen we Delphi starten om een WebBroker toepassing te bouwen die de JPEG plaatjes kan genereren in een web pagina. Doe File | New en kies de Web Server Application icon uit de Object Repository. Nu krijg je de New Web Server Application wizard, en het is belangrijk om (voorlopig) voor een CGI toepassing te kiezen, en het project een zinvolle naam te geven (zoals webimage.dpr) omdat we deze informatie straks nog moeten her-gebruiken als we onszelf gaan aanroepen. Na OK krijgen we een lege Web Module, waarbinnen we twee WebItemActions nodig hebben: een om de webpagina te genereren, en een met PathInfo /image om de individuele plaatjes te genereren (voor op de webpagina). De eerste is tevens de default WebActionItem, trouwens:

Voor de eerste WebActionItem gaan we een TDataSetPageProducer gebruiken - verbonden met de developer.db tabel - om de HTML te genereren die de velden en hun waarden van de tabel laat zien. De HTMLDoc property van de DataSetPageProducer zal vier speciale HTML #-tags bevatten om de velden uit de developer.db tabel op te halen:

  <BR>Name:    <#Name>
  <BR>Email:   <#Email>
  <BR>Website: <#Website>
  <BR>Photo:   <#Photo>
The vier #-tags zullen automatisch vervangen worden door de "Values" van de vier velden met vergelijkbare waarden uit de developer.db tabel. Om dat te demonstreren moeten we de Producer property van de eerste WebActionItem laten wijzen naar de DataSetPageProducer, en de webimage.exe uitvoeren in een browser: We zien dan dat de waarde van Photo niet de foto is, maar de string (GRAPHIC).

De HTML Code
Om een daadwerkelijke foto te kunnen zien, moeten we eerst de Fields Editor te gebruiken om TField componenten te maken voor elk van de vier velden van onze developer.db tabel, en vervolgens het OnGetText event van het Photo veld te gebruiken om iets anders te produceren dan de (GRAPHIC) DisplayText die we zien voor een een plaatje. Helaas kunnen we niet zomaar een binair plaatje in het HTML document embedden; we zullen gebruik moeten maken van een HTML <img src=...> tag, waarbij de SRC verwijst naar een (recursieve) aanroep van onszelf, namelijk de webimage.exe met dan als PathInfo de /image en als meegegeven key de waarde van het Email veld (ik ga er vanuit dat niemand hetzelfde e-mail adres heeft):

  procedure TWebModule1.TableDeveloperPhotoGetText(Sender: TField;
    var Text: String; DisplayText: Boolean);
  begin
    Text := '<img src="/cgi-bin/webimage.exe/image?Email=' +
      Sender.DataSet.FieldByName('Email').AsString + '">'
  end;
Het is overigens ook mogelijk om een geheel nieuw (calculated) veld aan de tabel toe te voegen (maar dan niet-persistent, dus zonder opslag in de tabel), waarbij we de waarde van het calculated veld gelijk laten zijn aan de hierboven gegenereerde HTML code. Op die manier kun je eenvoudig de waarvan van het nieuwe veld gebruiken, en heb je alleen maar een OnCalcFields event handler nodig.

Alternatieve HTML Code
Een alternatief is om helemaal niet vanuit het Photo veld zelf te redeneren, maar om vanuit de OnHTMLTag event handler te kijken of de ReplaceText op (GRAPHIC) staat. Zo ja, dan moeten we dat vervangen door de HTML code die het dynamische plaatje laat zien. Merk hierbij op dat we soms ook (Graphic) als veldwaarde kunnen krijgen. Echter, die wijst op een lege inhoud van het veld, dus dat kunnen we rustig negeren.

  procedure TWebModule1.DataSetPageProducer1HTMLTag(Sender: TObject; Tag: TTag;
    const TagString: String; TagParams: TStrings; var ReplaceText: String);
  begin
    if ReplaceText = '(GRAPHIC)' then
      ReplaceText :=
        '<img src="/cgi-bin/webimage.exe/image?Name=' +
          (Sender AS TDataSetPageProducer).
           DataSet.FieldByName('Name').AsString + '">'
  end;

Het Plaatje
Na de HTML bestaat de tweede stap uit het genereren van het plaatje in de ActionItem behorende bij de /image PathInfo. Omdat we hierbij een binair plaatje teruggeven, moeten we zelf de ContentType op image/jpeg zetten. Vervolgens moeten we de tabel op de juiste plek positioneren (door te zoeken naar het juiste records behorende bij het meegegeven Email adres), het plaatje uit de tabel halen, en via een ImageStream opsturen:

  procedure TWebModule1.WebModule1WebActionItem2Action(Sender: TObject;
    Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
  var
    Email: String;
    ImageStream: TmemoryStream;
  begin
    Email := Request.QueryFields.Values['Email'];
    TableDeveloper.Open;
    TableDeveloper.FindKey([Email]);
    ImageStream := TMemoryStream.Create;
    TableDeveloperPhoto.SaveToStream(ImageStream);
    ImageStream.Position := 0; // reset ImageStream
    Response.ContentType := 'image/jpg';
    Response.ContentStream := ImageStream;
    Response.SendResponse
  end;
Merk op dat we de ImageStream niet zelf moeten weggooien, omdat het eigendomsrecht daarvan overgaat naar het Response object (dat de ImageStream zal weggooien zodra alles verstuurd is). Het resultaat ziet er als volgt uit:

InternetExpress
Laten we hetzelfde probleem eens proberen op te lossen met InternetExpress, de grote broer van WebBroker waarbij gebruik wordt gemaakt van een MIDAS connectie voor bi-directioneel dataverkeer. Hierbij wordt de inhoud van een tabel in XML formaat over de lijn gestuurd. Helaas zullen we merken dat bepaalde BLOB velden (zoals plaatjes) niet in XML worden gecodeerd, en ook niet over de lijn worden gestuurd. Oftewel, de client zal wel de velden en inhoud voor de Name, Email en Website velden bevatten, maar niet voor het Photo veld. Maar laten we eerst een begin maken met het bouwen van de InternetExpress toepassing, dan zien we het vanzelf misgaan (en ook hoe we dit kunnen omzeilen). Voor we beginnen is het belangrijk om uit de DEMOS/MIDAS/InternetExpress/INetXCustom de design-time package dclinetxcustom te compileren en installeren. Hierna zijn niet alleen extra InternetExpress componenten beschikbaar voor gebruik, maar deze directory bevat ook enkele voorbeelden die de moeite van het bekijken waar zijn. De volledige source code van InternetExpress zelf is te vinden in de Source/Internet directory (en daarvan is met name MidItems.pas interessant om te bekijken).

Onder de TableDeveloper zetten we een DataSetProvider (van de MIDAS tab), een XMLBroker en een MidasPageProducer component (ook van de InternetExpress tab). Verbindt de DataSet property van de DataSetProvider met de TableDeveloper, en de ProviderName property van de XMLBroker met de DataSetProvider. Klik met de rechtermuisknop op de MidasPageProducer om de Web Page Editor te starten. Druk vervolgens op Insert om een DataForm toe te voegen. Zet daar een DataNavigator en FieldGroup op. We krijgen dan twee warnings (die vertoond worden binnen de Web Page Editor), maar eenvoudig op te lossen zijn. Verbindt de XMLComponent property van de DataNavigator met de FieldGroup, en de XMLBroker property van de FieldGroup component met XMLBroker1. Dat zorgt ervoor dat beide warnings weg zijn. Helaas zien we ook dat er geen inhoud voor het Photo veld te zien is. Als de met de rechtermuisknop op de FieldGroup klikken kunnen we met "Add All Fields" nog expliciet alle velden toevoegen, waarna we zien dat voor het Photo veld slechts een ruimte van 10 tekens is gereserveerd. Met wat dan? De (GRAPHIC) tekst soms? Er is geen ruimte voor onze HTML <img src=...> string, dus ik ben benieuwd. Voordat we dit kunnen testen moeten we echter eerst de Web Page Editor afsluiten, en de IncludePathURL van de MidasPageProducer invullen met het pad waar de JavaScript files (uit Delphi5/Source/WebMidas) door de web server te vinden zijn. Daarna moeten we de MidasPageProducer verbinden met een WebActionItem (bijvoorbeeld de eerste default ActionItem), en kunnen dan de web server toepassing compileren en uitvoeren.

Het veld Photo is helemaal leeg. Als je de XML data binnen deze HTML pagina nader onderzoekt blijkt er helemaal geen waarde voor een ImageField doorgegeven te worden; zelfs niet de gegenereerde HTML die we voor de WebBroker oplossing konden gebruiken. Waarschijnlijk om zo min mogelijk XML te hoeven genereren (en omdat het überhaubt was lastiger is om binaire data naar XML om te zetten in de ClientDataSet benadering). In ieder geval wordt aan de MIDAS kant al automatisch besloten om plaatjes niet door te geven.

Een Oplossing?
Wat te doen? Laten we eerst teruggaan naar Delphi, naar de Web Page Editor en met de rechtermuisknop op de FieldGroup klikken. Kies dan voor de keuze "Add All Fields" om individuele componenten voor Name, Email, Website, Photo en FieldStatus1 te krijgen. De laatste is een speciaal veld dat aangeeft of het huidige record gewijzigd is ("M"), nieuw ingevoerd is ("I") of verwijderd is ("D"). Behalve "Add All Fields", kunnen we ook nieuwe velden van een bepaald type toevoegen met Insert (terwijl we op de FieldGroup staan). Er is van alles, maar geen FieldImage. En dat is niet zo gek natuurlijk, als je bedenkt dat de (binaire) field data ook helemaal niet wordt doorgegeven. Net als met de WebBroker oplossing, zullen we een recursieve aanroep naar onszelf (in dit geval de WebBroker editie) nodig hebben om het plaatje tevoorschijn te toveren. De vraag hierbij is of we het plaatje iedere keer automatisch willen ophalen (en dus bij iedere navigatie door de table voor een expliciete aanroep naar de webbroker toepassing te maken om het plaatje op te halen), of dat we kiezen voor een expliciete aanroep door bijvoorbeeld op een knop "Vertoon Foto" of "Show Image" te drukken. Ik kies in dit geval voor het laatste omdat we anders het zogenaamde MIDAS "BriefCase" model geweld aandoen, door niet langer stand-alone te kunnen werken (maar bij iedere navigatie een request doen voor een bijbehorend plaatje). We zullen dus een speciale Button component voor InternetExpress gaan maken die desgevraagd de juiste foto kan laten zien - en die dus in feite de aanroep webimage.exe/image?Email=b.swart@chello.nl genereert.

TPopupButton
InternetExpress component zijn niet zomaar Delphi componenten (en niet iedere Delphi component kan in een InternetExpress toepassing gebruikt worden). Dat werd eigenlijk al duidelijk door de speciale Web Page Editor die via een dialoog steeds alleen die componenten laten zien die ook echt relevant zijn op een bepaalde plek (in feite worden alleen maar de sub-componenten van een geselecteerd component vertoond, zoals een DataNavigator en FieldGroup voor de (vader) component DataForm). Dit gedrag is geïmplementeerd door gebruik te maken van het IScriptComponent interface (gedefinieerd in WebComp.pas). Waar wij echter in geïntereseerd zijn zijn de eindbladeren, zoals de FieldGroup of DataNavigator, maar dan net iets anders, namelijk een speciale button die doet wat wij willen. Voorbeelden van dit soort componenten kunnen we terugvinden in MidItems.pas, en betreffen onder andere de FieldText, FieldTextArea en FieldRadioGroup. Een gemeenschappelijke voorvader van deze drie voorbeelden is de TWebDataInput, een component dat op zijn beurt afgeleid is van TWebDataDisplay en drie interfaces implementeert: de IRestoreDefault, IDataSetField en IValidateDateField. De TWebDataDisplay component introduceert een zeer belangrijke methode, namelijk de ControlContent (alhoewel deze op dit niveau nog abstract is). Aan de hand van deze methode wordt de HTML gegenereert om het specieke component invulling te geven. Voor een TFieldText bestaat dat uit <input type=text>, terwijl de TFieldTextArea uiteraard <textarea>...</textarea> genereert en de TFieldRadioGroup juist een <input type=radio>. Voor onze speciale button moeten we een <input type=button> genereren in de ControlContent. De vraag is of de TPopupButton moeten afleiden van TWebDataInput of TWebDataDisplay? Het blijkt in praktijk niet zoveel uit te maken, alhoewel TWebDataDisplay als vader wat zuiverder aanvoelt (we geven niet echt input, maar drukken slechts op de knop om een foto uit de database op te halen), dus leiden we de TPopupButton af van de TWebDataDisplay. De methode ControlContents laten we <input type=button> teruggeven, met als extra argument een "onclick" event die een stukje JavaScript code uitvoert. De JavaScript code zelf kunnen we tijdens design-time in the property JavaScript zetten.

  unit DrBob42X;
  interface
  uses
    Classes, HTTPApp, WebComp, MidItems;

  type
    TPopupButton = class(TWebDataDisplay)
    private
      FJavaScript: string;
    protected
      function ControlContent(Options: TWebContentOptions): string; override;
    published
      property Caption;
      property CaptionAttributes;
      property CaptionPosition;
      property TabIndex;
      property Style;
      property Custom;
      property StyleRule;
    published
      property JavaScript: string read FJavaScript write FJavaScript;
    end;

  procedure Register;

  implementation
  uses
    SysUtils;

  { TPopupButton }

  function TPopupButton.ControlContent(Options: TWebContentOptions): string;
  begin
    Result := Format('<input value="Show Image" type=button onclick="%s">',
                      [FJavaScript]);
  end;

  { Register procedure }

  procedure Register;
  begin
    RegisterWebComponents([TPopupButton]);
  end;

  end.
Door dit TPopupButton te registreren kunnen we nu als nieuw sub-component van de FieldGroup voor de PopupButton kiezen, die we dan een stukje JavaScript kunnen meegeven. Als JavaScript wil ik graag een pop-up window vertonen met daarin de inhoud van het gewenste plaatje. Dat kan met de JavaScript method window.open, die ik in een soort speciale funktie "ShowImage" heb gestopt. De JavaScript code voor ShowImage is als volgt:
  <script language=JavaScript>
  function ShowImage(url)
  {
    var w = window.open("","","resizable,width=320,height=240");
    w.title = "Photo";
    w.document.open();
    w.document.write('<html><body>
      <img src="http://192.168.92.201/cgi-bin/webimage.exe/image?
       Email='+url+'"></body></html>');
    w.document.close();
  }
  </script>
Merk op dat de JavaScript funktie ShowImage al volledig is voorbereid op de WebBroker toepassing die we eerder in dit artikel schreven, en dat we als argument (de url) alleen nog maar de waarde van het Email veld hoeven mee te geven. En dat stukje JavaScript is dan ook wat we tijdens design-time nog in de JavaScript property moeten schrijven:
  ShowImage(document.forms['DataForm1'].Email.value)
We roepen dus de ShowImage aan, met als argument de waarde van het Email veld. Dat veld is terug te vinden op ons DataForm (genaamd DataForm1), en veld genaamd Email. Let op dat je hierbij de juiste namen gebruikt, anders krijg je JavaScript errors. Het laatste wat we moeten doen is de implementatie van de JavaScript funktie ShowImage nog ergens laten. De makkelijkste manier is om die ergens in de HTMLDoc property van de MidasPageProducer neer te zetten (maar dan wel voor de #-tags). Een alternatief is om de AddElements methode te implementen, behorend bij de IScriptComponent interface. Via AddElements kun je aangeven welke JavaScript file(s) nodig zijn om het huidige component te ondersteunen. Bijvoorbeeld xmlshow.js voor de TCustomShowXMLButton. Het resultaat is een knop met de tekst "Show Image" erop, die een pop-up Window laat zien met als inhoud de aanroep van webimage.exe met als PathInfo /image en als Email de juiste waarde (van het huidige record).

We kunnen nu rustig door alle records heen navigeren zonder dat de aanroep naar de webbroker toepassing nodig is (oftewel: het briefCase model blijft geldig), terwijl het plaatje van de foto toch maar een druk op de button verwijderd is.

TWebCheckBox
Als afsluiting nog een voorbeeld van een andere InternetExpress component die erg handig is (en om de een of andere reden nog niet aanwezig is in de verzameling): een HTML CheckBox component. Omdat dit wel een echt input control is, heb ik als parent control de TWebTextInput genomen (ook de vader van TFieldText bijvoorbeeld). Het verschil met de TFieldText is wederom de ControlContent methode, die nu <input type=checkbox teruggeeft. Behalve de TWebCheckbox, die te gebruiken is als sub-component van een DataForms, moeten we ook een speciale editie maken die te gebruiken is als sub-component van een QueryForm. In het laatste geval moet namelijk ook nog de IQueryField interface geïmplementeerd worden. Dit interface bestaat o.a. uit een class funcyion IsQueryField dat True moet teruggeven om aan te geven dat het om een QueryForm component gaat.

  unit DrBob42X;
  interface
  uses
    Classes, HTTPApp, WebComp, MidItems;

  type
    TWebCheckbox = class(TWebTextInput)
    protected
      function ControlContent(Options: TWebContentOptions): string; override;
    published
      property DisplayWidth;
      property ReadOnly;
      property Caption;
      property CaptionAttributes;
      property CaptionPosition;
      property TabIndex;
      property Style;
      property Custom;
      property StyleRule;
    end;

    TQueryCheckbox = class(TWebCheckbox, IQueryField)
    private
      FText: string;
    protected
      function GetText: string;
      procedure SetText(const Value: string);
    public
      class function IsQueryField: Boolean; override;
    end;

  procedure Register;

  implementation
  uses
    SysUtils;

  { TWebCheckbox }

  function TWebCheckbox.ControlContent(Options: TWebContentOptions): string;
  var
    Attrs: string;
  begin
    AddAttributes(Attrs);
    Result := Format('<input type=checkbox %0:s>', [Attrs]);
  end;

  { TQueryCheckbox }

  class function TQueryCheckbox.IsQueryField: Boolean;
  begin
    Result := True;
  end;

  function TQueryCheckbox.GetText: string;
  begin
    Result := FText;
  end;

  procedure TQueryCheckbox.SetText(const Value: string);
  begin
    FText := Value;
  end;

  { Register procedure }

  procedure Register;
  begin
    RegisterWebComponents([TWebCheckBox,TQueryCheckbox]);
  end;

  end.

Toekomst van InternetExpress
Het ligt overigens in de lijn der verwachtingen dat een volgende versie van Delphi de nodige uitbreidingen op InternetExpress zal bevatten (genaamd SiteExpress of WebSnap). Zo verwacht ik dan een native CheckBox (misschien wel op basis van mijn voorstel hier), en wellicht ondersteuning voor SOAP (Simple Object Access Protocol) om data uit te wisselen tussen de ene tier en de andere. Maar dat zal de tijd leren. In de eerste release van Kylix zal overigens nog geen InternetExpress zitten, omdat Kylix release #1 te vergelijken zal zijn met Delphi Professional, en InternetExpress zit in de Enterprise editie (vanwege de MIDAS provider componenten onder andere). Wel zal de WebBroker Technologie in Kylix zitten, en de eerdergenoemde oplossing in dit artikel om een dynamich plaatje te produceren zal dan ook volgend verwachting zonder problemen compileren met Kylix en werken met een web server zoals Apache onder Linux (temminste... als je inderdaad voor een CGI executable had gekozen aan het begin van dit artikel)...
Mocht iemand nog vragen, opmerkingen of suggesties hebben, dan hoor ik die het liefst via .


Dit artikel is eerder verschenen in SDGN Magazine #63 - 2000

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