Bob Swart (aka Dr.Bob)
Word in Delphi toepassingen “embedden”

Soms heb je van die klanten die (denken dat ze) precies weten wat ze willen. Vanuit Delphi een Word document openen en bewerken bijvoorbeeld. Maar dan niet door Word op te starten en aan te sturen. Nee, Word mag vooral niet als “aparte” toepassing gezien worden, maar het document (en met name het edit-scherm) moet als onderdeel van de gehele toepassing gezien worden. Het embedden van Word in de Delphi toepassing zelf dus als het ware.

Servers componenten?
Voor mijn dagelijkse werk gebruik ik Delphi 2006, maar ook oudere versies van Delphi hebben een “Server” tab in het Component Palette of Tool Palette, met daarin de Office componenten die we in Delphi kunnen gebruiken. Zo hebben we de keuze uit TWordDocument en TWordApplication, en nog meer Word, Excel, PowerPoint, Outlook en Access specifieke componenten. Helaas bieden die wel de mogelijkheid om Word (net als Excel, PowerPoint, Outlook en Access) op afstand te “automaten”, maar niet te embedded. Ik kan bijvoorbeeld wel een TWordApplication en TWordDocument component op mijn Delphi VCL Form zetten, maar dat zijn dan non-visueel componenten. En bij het gebruik kan ik de volgende code schrijven:

  try
    Wordapplication.Connect;
  except
    MessageDlg('Word may not be installed', mtError, [mbOk], 0);
    Abort
  end;
(zoals ook terug te vinden is in de pWordComp demo in de Main.pas demo unit uit de BDS\4.0\Demos\DelphiWin32\ActiveX\OleAuto\SrvComp\Word directory), maar dit start of connect slechts aan een instantie van Word die “buiten” de Delphi toepassing zelf draait. Wel OLE Automation, maar niet embedded dus.
Nee, het ziet er naaruit dat we een stapje terug moeten. Of een flinke stap terug eigenlijk, naar een component dat al sinds Delphi 2 of 3 terug te vinden is in Delphi (geef jezelf een prijs als je uit je hoofd weet in welke versie de TOleContainer werd geďntroduceerd).

OLEContainer
De TOleContainer is een control waarin we een embedded OLE object kunnen serveren. In ons geval wil ik een Word document automatisch laten openen en vertonen in een Delphi Form. Van de TOleContainer moet ik dan de Align property op alClient zetten, en de AutoActivate op aaGetFocus (zodat het OLE object geactiveerd wordt zodra het Form de focus krijgt). De truc zit hem nu in het creeren (of activeren) van de OleContainer op de juiste manier. Daartoe kunnen we het beste de CreateObjectFromFile methode voor gebruiken, die als argument een filename meekrijgt, alsmede een Boolean die aangeeft of het een “iconic” instantie moet zijn of niet (niet dus). Een hardcoded aanroep om het bestand BobSwart.doc in de C:\usr directory te openen is als volgt:

  procedure TFormWord.FormCreate(Sender: TObject);
  begin
    OleContainer1.CreateObjectFromFile('c:\usr\BobSwart.doc', False);
  end;
Het gevolg is echter dat we een form krijgen met daarin een “grijze” achtergrond met daarin de inhoud van het Word document. Maar zonder mogelijkheden om te scrollen of te editen. Net alsof het toch nog niet actief is, zoals in figuur 1 te zien is:

Word Inactief in Delphi

Als we dan met de rechtermuisknop op het grijze gebied klikken, krijgen we een pop-up menu met de opties “Open” en “Edit”. De keuze maakt niet uit: in beide gevallen wordt het document geopend om te editen, zoals in figuur 2 te zien is.

Word Actief in Delphi

Dit ziet er wel (bijna) uit zoals we het willen, maar start nog niet helemaal goed.

OnActivate
Er is gelukkig ook een manier om het gebruik van het pop-up menu te voorkomen, en het document automatisch te openen, door de volgende regel code uit te voeren:

  OleContainer1.DoVerb(ovShow);
Dit kunnen we echter niet in de FormCreate doen, want dan krijg je de foutmelding dat een invisible window de focus niet kan krijgen. We kunnen dit pas in de OnActivate doen, maar dan is het effect ook precies wat we willen: het document wordt meteen geopend.

Extra Extra...
Alhoewel dit redelijk lijkt te werken, ontbreekt er natuurlijk nog wel het een-en-ander aan. Afsluiten van de toepassing heeft niet tot gevolg dat ook de wijzigingen in het word document worden opgeslagen. En alhoewel we dit ook via de OleContainer zouden kunnen doen, lijkt het meer voor de hand te liggen om hier dan Word zelf voor te gebruiken.

MainMenu
Allereerst maar eens wat ontbrekende zaken toevoegen, zoals de Word menus. Het is wat lastig dat die niet te zien zijn in het embedded document. De oplossing is erg eenvoudig: plaats een TMainMenu op het form, en bij het activeren van het Word Document zal vanzelf de juiste menu structuur worden toegevoegd aan het TMainMenu component. Zonder dat we daar zelf iets aan hoeven te doen!
In eerste instantie zal het menu nog niet te zien zijn, maar na de Open of Edit operatie, is het in vol ornaat aanwezig: een Edit, View, Insert, Format, Tools, Table en Help menu. Via View | Toolbars kun je ook toolbars aanzetten, die dan automatisch onder het menu komen te staan (zie figuur 3).

Het enige dat ontbreekt (vergeleken met een niet-embedded Word document) is het File en het Window menu. Dat laatste maakt me niet zoveel uit, maar het File menu is wel lastig, want dat is juist de plek om bestanden te bewaren, iets anders te openen, af te drukken, etc.
Gelukkig kunnen we ook zelf al een File menu toevoegen aan het TMainMenu. Wat we nodig hebben is in ieder geval een Open, een Save en een Save As, en ook nog een Print. De Close en Exit zijn makkelijker, en ook op andere manieren te realiseren (gewoon door de OleContainer of het Delphi Form te destroyen – alhoewel je dan wel moet kijken of er wijzigingen in het document zijn natuurlijk, anders heb je weer een vol bad weggegooid).

File | Open
De OnClick event handler van de File | Open zal een TOpenDialog moeten gebruiken om de gebruiker het nieuwe bestand te laten kiezen. Vervolgens kunnen we weer de CreateObjectFromFile aanroepen, gevolgd – deze keer wel direct – door een DoVerb van ovShow.

  procedure TFormWord.Open1Click(Sender: TObject);
  begin
    if OpenDialog1.Execute then
    begin
      OleContainer1.CreateObjectFromFile(OpenDialog1.FileName, False);
      OleContainer1.DoVerb(ovShow);
      OleContainer1.Modified := False
    end
  end;
De Modified property van de TOleContainer gebruik ik hier om aan te geven dat de inhoud van het nieuwe document niet is gewijzigd. Deze property wordt automatisch op True gezet als de gebruiker wijzigingen aanbrengt in de inhoud van de OleContainer (in dit geval het Word document). Erg handig, en kunnen we ook in de OnCloseQuery gebruiken bijvoorbeeld om te controleren of er nog wijzigingen zijn die niet zijn opgeslagen als (lees: voor) we het Form willen opruimen.

File | Save
Voor de OnClick event handler van de File | Save kunnen we de SaveAs methode aanroepen van het onderliggende OleObject (helaas is de “Save” niet beschikbaar). De aanroep van SaveAs verwacht een bestandsnaam, en optioneel het bestandsformaat als tweede argument. Voor een normale Save zal de bestandsnaam niet veranderd zijn, dus moeten we van het begin af aan de naam van het huidige document bijhouden (ook bij het openen van een nieuw document).
Uitgaande van een veld “FileName”, wordt de Save code dan als volgt:

  procedure TFormWord.Save1Click(Sender: TObject);
  begin
    OleContainer1.OleObject.SaveAs(FileName)
  end;
Met dit in het achterhoofd is de SaveAs niet veel moeilijker.

File | SaveAs
Voor de OnClick event handler van de File | SaveAs gebruiken we dezelfde SaveAs methode van het OleObject, maar nu ook eerst een SaveDialog om de nieuwe bestandsnaam te laten kiezen.

  procedure TFormWord.SaveAs1Click(Sender: TObject);
  begin
    if SaveDialog1.Execute then
    begin
      FileName := SaveDialog1.FileName;
      OleContainer1.OleObject.SaveAs(FileName);
      // now re-open in new location
      OleContainer1.CreateObjectFromFile(FileName, False);
      OleContainer1.DoVerb(ovShow);
      OleContainer1.Modified := False
    end
  end;
Let ook weer op het gebruik van de Modified property van de OleContainer, waarmee we in de gaten kunnen houden of de inhoud van de OleContainer is gewijzigd. Na het opslaan zal deze property op False gezet moeten worden, om aan te geven dat er geen “nog niet opgeslagen” wijzigingen meer zijn.
Bij het openen van een nieuw document (met File | Open) zouden we de Modified property van het huidige document moeten gebruiken om eventueel een Save / confirmatie dialoog te kunnen tonen voordat een nieuw document wordt geopend – dit laat ik echter over als oefening voor de lezer.

File | Print
Voor het printen tenslotte kunnen we de PrintOut routine gebruiken van het OleObject, en dat is dus weer een one-liner als volgt:

  procedure TFormWord.Print1Click(Sender: TObject);
  begin
    OleContainer1.OleObject.PrintOut(False)
  end;
Het argument hierbij geeft aan of we het printen op de achtergrond willen doen of niet.

Conclusie
Het uiteindelijke resultaat is een normale Win32 Delphi toepassing die echter op de plek van de OleContainer (die we overal kunnen plaatsen – ook op een Panel of een Tabblad bijvoorbeeld) een embedded Word document laat zien, inclusief de bijbehorende menu en toolbars, zodat het voor de gebruiker net is alsof de Word faciliteiten daadwerkelijk binnen de Delphi toepassing voorhanden zijn. Het lijkt zonder problemen te werken met verschillende versies van Word (o.a. Word97, Word 2000, Word XP en Word 2003 – ik heb Office 2007 nog niet geprobeerd).
Dit was precies wat m’n klant wou; klant blij, ik blij, en hopelijk kan ik er nog andere Delphi ontwikkelaars blij mee maken. Wie nog vragen of opmerkingen (of voorstelen tot verbeteringen) heeft, kan me op de gebruikelijke manier bereiken.


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