Word in Delphi toepassingen “embedden” |
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.
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:
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.
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.
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.