Startseite
Impressum
Skills
Referenzen
pfeil Produkte
Links
pfeil Tips
Benutzerverwaltung 1
Benutzerverwaltung 2
Das ideale Paßwort
Datum und SQL-DB
DSL-Probleme
Dr. Watson Teil 1
Dr. Watson Teil 2
DTS Automatisierung
Installation autom.
Kalender und Zeit
Mail Automatisierung
Office Automation
Stack Frame
SQL Server
Strings + Unicode
VB 6 Tips & Tricks
Windows-Uhrzeit

 

Drucker - freundliche Darstellung


  zalando.de - Schuhe und Fashion online

 

Letzte Aktualisierung: 04.01.2023



Office Automation

Die Programme der Microsoft Office - Reihe (zum Beispiel Word, Excel und Access) lassen sich ganz hervorragend durch selbstgeschriebene Programme steuern.
Ob man neue Dokumente erstellt und diese automatisch füllt, neue Tabellen und Diagramme anlegt, oder ein Arbeitsblatt aus einem Programm heraus ausdruckt, alles ist möglich.

Im Folgenden werde ich anhand von Beispielen in Visual Basic und Visual C++ erklären, wie man diese Aufgaben bewerkstelligen kann.

Das Office - Objektmodell und die Automation

Ein Beispiel für Visual Basic 6.0

Ein Beispiel für Visual C++ 6.0 und Outlook

Ein Beispiel für Visual C++ 6.0 und Word

Ein Beispiel für Visual C++ 6.0 und Excel

Hinweis zu Bookmarks

Probleme mit Excel-Formeln

Versionskonflikte

Weitere Tips

Eingebettetes

Buchtips

Das Office - Objektmodell und die Automation

Alle Programme der Microsoft Office - Reihe unterstützen eine Programmierschnittstelle, die man "Automation" nennt. Damit ist gemeint, daß man in einer beliebigen Sprache, die die sogenannte "OLE Automation" unterstützt, auf das Objektmodell der jeweiligen Programme zugreifen kann. Zu diesen Sprachen gehören eigentlich alle bekannten Sprachen, mit denen man in Windows Programmieren kann, seien es die Visual Studio - Sprachen, Windows - Scripting oder sogar Java.

Das Objektmodell ist dabei die Art und Weise, wie man mit dem jeweiligen Programm umgehen kann, und auf die einzelnen Objekte, die es dort gibt, zugreifen kann. Ein Beispiel: Man erzeugt sich ein Objekt vom Typ "Word.Application", und hat damit sozusagen einen Zeiger auf das bereits laufende Word - Programm. Dann kann man mit Befehlen wie "Documents.Add" einfach ein neues Dokument erzeugen, so als ob man in Word "Datei / Neu" aufrufen würde. Oder, um ein anderes Beispiel zu nennen, kann man mit "Documents(1).PrintOut" einfach das erste Dokument, welches in dieser Word - Instanz geöffnet ist, ausdrucken.

Selbstverständlich kann man auch alle Arten von Änderungen in den Dokumenten durchführen, oder komplette Dokumente erstellen (zum Beispiel aus Datenbank - Daten oder Benutzereingaben), und diese zum Beispiel irgendwo abspeichern.

Die folgenden Links verweisen zur offiziellen Dokumentation bei Microsoft, wo man sich die verschiedenen Objektmodelle ansehen kann:

Grundlagenartikel zur Automation von Office 97 und Office 2000
Grundlagenartikel zur Automation von Word aus VB heraus

Your Unofficial Guide to Using OLE Automation with Microsoft Office and Microsoft BackOffice
Automating Office Applications
"Super-Easy Guide to the Microsoft Office Excel 2003 Object Model"
Office 2003 Editions: VBA Language Reference for the Office Object Model (Helpfile Download)

Access - Objektmodell
Excel - Objektmodell
FrontPage - Objektmodell
Outlook - Objektmodell
Visio - Objektmodell
Word - Objektmodell

Microsoft Visual Studio 2005 Tools for Office Second Edition Runtime (build 8.0.50272.940) (x86) - Deutsch
Microsoft Visual Studio Tools for the Microsoft Office system (version 3.0 Runtime) (x86)
Bearbeiten von Excel 2007- und PowerPoint 2007-Dateien mit dem Open XML-Objektmodell (Teil 1 von 2)


Ein anderer interessanter Artikel, in Englisch: Word - Automation, unter Borland C++

Ein Beispiel für Visual Basic 6.0

Das Folgende Beispiel ist der komplette Code, der hinter zwei Knöpfen auf einer Form liegt, um damit eine Microsoft Word - Datei aus einer Vorlage zu Erstellen, zu Speichern und zu Drucken, und dasselbe mit einem Microsoft Excel - Arbeitsblatt.

Das erste, was man tun muß, wenn man ein frisches Visual Basic - Projekt erstellt hat, ist die Referenzierung der entsprechenden Objektbibliotheken unter "Projekt / Verweise". Wenn man also zum Beispiel das Word - Objektmodell verwenden will, dann muß man "Microsoft Word Object Library" anklicken:

wordreferenz

Ob dort als Versionsnummer 8.0 oder 9.0 oder 10.0 angezeigt wird, spielt dabei keine Rolle, weil das Beispiel so programmiert ist, daß die installierte Office - Version egal ist (naja, Office 95 oder später sollte es schon sein). Mit den anderen Office - Programmen geht es analog, also zum Beispiel für Excel:

excelreferenz

Und hier ist der vollständige Code:

Private Sub Command1_Click() Dim lobjWord As Word.Application Dim lwrdDoc As Word.Document Set lobjWord = CreateObject("Word.Application") Set lwrdDoc = lobjWord.Documents.Add("C:\Temp\Vorlage.dot") With lwrdDoc .Bookmarks("Name1").Range.Text = "Müller" .Bookmarks("Name2").Range.Text = "Alexander" .Bookmarks("Bearbeiterzeichen2").Range.Text = "http://www.a-m-i.de" .SaveAs "C:\Temp\Fertig.doc" .PrintOut .Close End With 'Word verlassen/ Prozess beenden lobjWord.Quit Set lwrdDoc = Nothing Set lobjWord = Nothing End Sub

Dieses Beispiel zeigt, wie auf Knopfdruck zuerst Word geöffnet wird, dann wird ein neues Dokument aus der Vorlage "C:\Temp\Vorlage.dot" erstellt, anschließend werden benamte Datenfelder gefüllt, und das neue Dokument wird abgespeichert unter "C:\Temp\Fertig.doc". Daraufhin wird das Dokument noch gedruckt und geschlossen, und die Word - Applikation wird beendet.

Dieser Code - Schnipsel ist ein brauchbares Gerüst, um darauf aufbauend eigene Erweiterungen einzubauen und zu Testen.

Das gilt auch für das folgende Beispiel, welches analog mit Microsoft Excel arbeitet:

Private Sub Command2_Click() Dim objExcelApp As Excel.Application Dim objWorkbooks As Excel.Workbooks Dim objSheet As Excel.Worksheet Dim nQuartal As Integer Set objExcelApp = CreateObject("Excel.Application") Set objWorkbooks = objExcelApp.Workbooks objWorkbooks.Open ("C:\Temp\Vorlage.xlt") Set objSheet = objWorkbooks.Item(1).Worksheets(1) objSheet.Range("Überschrift").Value = "Statistik" For nQuartal = 1 To 4 objSheet.Range("Vogel" & CStr(nQuartal)).Value = "Vogel 1" objSheet.Range("AnzKombi" & CStr(nQuartal)).Value = "Anzahl Kombi." Next nQuartal objSheet.Range("A1").Select objSheet.SaveAs "C:\Temp\Fertig.xls", xlExcel9795 objSheet.PrintOut objExcelApp.Visible = True End Sub

Auch hier wird aus einer Vorlage ein neues Arbeitsblatt erstellt, gefüllt und abgespeichert. Beim Füllen ist in Excel der "Range" - Befehl sehr wichtig, mit dem man eine Zelle oder auch mehrere auswählen kann, um darauf Operationen auszuführen. Dies kann z.B. über die Excel - üblichen Koordinaten ("A1, B2") oder über die Namen der Zellen erfolgen, beides ist im Beispiel angegeben.

Im obigen Beispiel wird das Arbeitsblatt auch noch gedruckt, und Excel bleibt offen, damit der angemeldete Benutzer "weitermachen" kann.

Beispiel für Visual C++ 6.0 und Outlook

Das folgende Beispiel legt in Outlook unterhalb des Ordners "Posteingang" einen neuen Ordner an, mit beliebigem Namen.

Das Beispiel nutzt stellenweise ATL - Makros, um kompakteren Code zu generieren.

#include "atlbase.h" #pragma warning ( disable : 4146 ) #import "C:\Programme\Gemeinsame Dateien\Microsoft Shared\Office10\MSO.DLL" #import "C:\Programme\Microsoft Office\Office10\msoutl.olb" (...) HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); _ASSERT(hr == S_OK); // Erzeugen einer Outlook - Instanz: { Outlook::_ApplicationPtr spOutlook; hr = spOutlook.CreateInstance(_uuidof(Outlook::Application)); _ASSERT(SUCCEEDED(hr)); if (SUCCEEDED(hr)) { try { CComBSTR bsMapi(L"MAPI"); Outlook::_NameSpace * pNameSpace = NULL; hr = spOutlook->raw_GetNamespace(bsMapi.m_str, &pNameSpace); _ASSERT(SUCCEEDED(hr)); if (pNameSpace != NULL) { CComBSTR bsFolderName(m_strFoldername); Outlook::MAPIFolderPtr spInbox = pNameSpace->GetDefaultFolder(Outlook::olFolderInbox); Outlook::_FoldersPtr spInboxFolders = spInbox->GetFolders(); spInboxFolders->Add(bsFolderName.m_str); pNameSpace->Release(); } } catch (_com_error & comError) { MessageBox(comError.ErrorMessage()); } catch (...) { // Fehlerbehandlung einbauen !!! } } spOutlook->Quit(); spOutlook = NULL; } CoUninitialize();

Beispiel für Visual C++ 6.0 und Word

Das folgende Beispiel zeigt exemplarisch verschiedene Aktionen, die man mit Word ferngesteuert machen kann:

  • Anlegen eines neuen Dokuments
  • Öffnen eines bestehenden Dokuments
  • Ändern des Textes einer Textmarke ("Bookmark")
  • Suchen und Ersetzen von Begriffen innerhalb eines Dokuments
  • Speichern unter einem anderen Namen, in einem beliebigen Format (z.B. reiner Text)
  • Einfügen von neuem Text
  • Drucken des aktuellen Dokuments auf dem Standard - Drucker

Die einzelnen Funktionen werden teilweise über eine if - Anweisung mit einer konstanten boolschen Bedingung ausgeklammert. Hier können die jeweils benötigten Funktionen entsprechend aktiviert und deaktiviert werden.

Es ist ein komplettes Programm, welches in C++ 6.0 sofort lauffähig ist. Daher wurde der Code leider sehr umfangreich, da praktisch alles umständlich über die Dispatch - Schnittstelle aufgerufen werden muß. Ich habe schon extra die ATL - Makros benutzt so gut es geht, um den Code kompakter zu bekommen.

Auch hier gilt wieder: Das Programm zeigt grundsätzlich als Basisgerüst, wie's geht, und kann als Grundlage für eigene Erweiterungen dienen.

#include "stdafx.h" #include "atlbase.h" CComModule _Module; #include "atlcom.h" #include "atlimpl.cpp" #include "atlctl.h" #include "atlctl.cpp" #include "comdef.h" // Wenn man die folgende Zeile nicht kommentiert und mitkompiliert, // erhält man zwar Fehlermeldungen, aber auch zwei Dateien, von denen // eine alle wichtigen Konstanten enthält, die Excel verwendet (msword.tlh): // #import "C:\Programme\Microsoft Office\Office10\msword.olb" int main(int argc, char* argv[]) { // COM initialisieren: CoInitialize(NULL); CLSID clsidWordApplication; HRESULT hr = CLSIDFromProgID(L"Word.Application", &clsidWordApplication); // erfolgreichen Aufruf prüfen: _ASSERT(SUCCEEDED(hr)); if (SUCCEEDED(hr)) { IDispatchPtr spIWord = NULL; // Word - Application erzeugen: hr = CoCreateInstance(clsidWordApplication, NULL, CLSCTX_SERVER, IID_IDispatch, (void **) (IDispatch *) &spIWord); // erfolgreichen Aufruf prüfen: _ASSERT(SUCCEEDED(hr)); if (SUCCEEDED(hr) && (spIWord != NULL)) { CComDispatchDriver spDispWord(spIWord); // Jetzt habe wir Zugriff auf die Dispatch - // Schnittstelle der Word - Application. // Damit können wir uns jetzt das "Documents" - Objekt beschaffen: VARIANT vResult; { DISPID dispidDocuments = 0; hr = spDispWord.GetIDOfName(L"Documents", &dispidDocuments); hr = spDispWord.GetProperty(dispidDocuments, &vResult); } CComDispatchDriver spDocuments(vResult.pdispVal); if (SUCCEEDED(hr) && (spDocuments != NULL)) { // Jetzt haben wir auch Zugriff auf die Documents - Collection. if (false) { // Durch den Aufruf der Add - Methode, // ohne Parameter, erzeugen wir ein neues Dokument: hr = spDocuments.Invoke0(L"Add", &vResult); } else { // Durch den Aufruf der Open - Methode // wird ein bestehendes Dokument geöffnet. // Hier rufen wir nur mit einem einzigen Parameter auf, dem Dateinamen. VARIANT vFilename; vFilename.vt = VT_BSTR; vFilename.bstrVal = SysAllocString(L"C:\\temp\\test.doc"); hr = spDocuments.Invoke1(L"Open", &vFilename, &vResult); SysFreeString(vFilename.bstrVal); } // Das aktuelle Dokument - Objekt beschaffen: IDispatchPtr spDokument = NULL; { VARIANT vDocNummer; vDocNummer.vt = VT_I4; vDocNummer.lVal = 1; hr = spDocuments.Invoke1(L"Item", &vDocNummer, &vResult); spDokument = vResult.pdispVal; } CComDispatchDriver spDispDokument(spDokument); if (true) { // In diesem Block bekommt eine Textmarke mit Namen "testbookmark" // einen neuen Wert zugewiesen (den Text "testinhalt"). // Beschaffen der Bookmarks - Collection des Dokuments: IDispatchPtr spBookmarks = NULL; spDispDokument.GetPropertyByName(L"Bookmarks", &vResult); spBookmarks = vResult.pdispVal; CComDispatchDriver spDispBookmarks(spBookmarks); // Beschaffen des gesuchten Bookmark - Objekts: IDispatchPtr spBookmark = NULL; { VARIANT vBkmName; vBkmName.vt = VT_BSTR; vBkmName.bstrVal = SysAllocString(L"testbookmark"); spDispBookmarks.Invoke1(L"Item", &vBkmName, &vResult); SysFreeString(vBkmName.bstrVal); spBookmark = vResult.pdispVal; } CComDispatchDriver spDispBookmark(spBookmark); // Range - Objekt des Bookmarks beschaffen: IDispatchPtr spBmRange = NULL; { DISPID dispidRange = 0; hr = spDispBookmark.GetIDOfName(L"Range", &dispidRange); hr = spDispBookmark.GetProperty(dispidRange, &vResult); spBmRange = vResult.pdispVal; } CComDispatchDriver spDispRange(spBmRange); // Bookmark mit neuem Inhalt befüllen: { VARIANT vNeuerInhalt; vNeuerInhalt.vt = VT_BSTR; vNeuerInhalt.bstrVal = SysAllocString(L"testinhalt"); DISPID dispidText = 0; hr = spDispRange.GetIDOfName(L"Text", &dispidText); hr = spDispRange.PutProperty(dispidText, &vNeuerInhalt); SysFreeString(vNeuerInhalt.bstrVal); } } if (true) { // In diesem Block wird exemplarisch "Suchen und Ersetzen" durchgeführt. // Zuerst beschaffen wir das Content - Objekt des aktuellen Dokuments: IDispatchPtr spContent = NULL; hr = spDispDokument.GetPropertyByName(L"Content", &vResult); spContent = vResult.pdispVal; CComDispatchDriver spDispContent(spContent); // Mit dem Content - Object holen wir uns das Find - Object: IDispatchPtr spFind = NULL; hr = spDispContent.GetPropertyByName(L"Find", &vResult); spFind = vResult.pdispVal; CComDispatchDriver spDispFind(spFind); hr = spDispFind.Invoke0(L"ClearFormatting", &vResult); VARIANT vArgsExecute[11]; memset(vArgsExecute, 0, sizeof(VARIANT) * 11); // Die Parameter müssen rückwärts angegeben werden ! // Parameter 1 = "FindText", der Text der gesucht wird. vArgsExecute[10].vt = VT_BSTR; vArgsExecute[10].bstrVal = SysAllocString(L"Datenbank"); // Parameter 2 = "MatchCase", eine Kennung, ob Groß-/Kleinschreibung vArgsExecute[9].vt = VT_BOOL; vArgsExecute[9].boolVal = TRUE; // Parameter 3 - 9 werden hier nicht benutzt // Parameter 10 = "ReplaceWith", der Text, mit dem ersetzt werden soll: vArgsExecute[1].vt = VT_BSTR; vArgsExecute[1].bstrVal = SysAllocString(L"TaunusBank"); // Parameter 11 = "Replace", die Angabe, // wie viele Ersetzungen durchgeführt werden. // "wdReplaceOne" = 1 ersetzt genau ein Vorkommen. // "wdReplaceAll" = 2 ersetzt alle Vorkommen. vArgsExecute[0].vt = VT_I2; vArgsExecute[0].lVal = 2; hr = spDispFind.InvokeN(L"Execute", vArgsExecute, 11, &vResult); SysFreeString(vArgsExecute[10].bstrVal); SysFreeString(vArgsExecute[1].bstrVal); } if (true) { // "Speichern unter" mit dem aktuellen // Dokument durchführen, Format = "Textfile" VARIANT vNeuerName, vSpeichernFormat; vNeuerName.vt = VT_BSTR; vNeuerName.bstrVal = SysAllocString(L"C:\\temp\\docneu.txt"); vSpeichernFormat.vt = VT_I4; vSpeichernFormat.lVal = 2; // wdFormatDocument = 0, // wdFormatText = 2, ... hr = spDispDokument.Invoke2(L"SaveAs", &vNeuerName, &vSpeichernFormat, &vResult); SysFreeString(vNeuerName.bstrVal); } if (false) { // In diesem Block wird eine neue Textzeile eingefügt. // Jetzt müssen wir das sogenannte "Selection" - Objekt beschaffen: { DISPID dispidSelection = 0; hr = spDispWord.GetIDOfName(L"Selection", &dispidSelection); hr = spDispWord.GetProperty(dispidSelection, &vResult); } CComDispatchDriver spSelection(vResult.pdispVal); // Aufrufen der "TypeText" - Methode, // um die aktuelle Selektion mit dem Text zu Ersetzen: { // Die TypeText - Methode benötigt // nur einen einzigen Parameter, einen String VARIANT vArgsTypeText; BSTR bstrTemp = SysAllocString(L"Eine Textzeile zum Testen !!"); vArgsTypeText.vt = VT_BSTR; vArgsTypeText.bstrVal = bstrTemp; hr = spSelection.Invoke1(L"TypeText", &vArgsTypeText, &vResult); SysFreeString(bstrTemp); } } // Dokument drucken: if (false) { hr = spDispWord.Invoke0(L"PrintOut", &vResult); } // Dokument ohne speichern wieder zumachen. // Dafür müssen wir einen Parameter mitgeben, // damit keine Frage kommt "Soll ich speichern?": { VARIANT vArgsClose; vArgsClose.vt = VT_BOOL; vArgsClose.boolVal = FALSE; hr = spDocuments.Invoke1(L"Close", &vArgsClose, &vResult); } // Word-Application wieder zumachen: hr = spDispWord.Invoke0(L"Quit", &vResult); } // Alle Interfaces wieder befreien: spIWord = NULL; } } else printf("Es ist kein Word installiert!\n"); CoUninitialize(); printf("Fertig!\n"); return 0; }

Beispiel für Visual C++ 6.0 und Excel

Im Folgenden ist eine C++ - Klasse wiedergegeben, die verschiedene Aktionen mit Excel-Dateien durchführen kann:

  • Öffnen und schließen eines Excel-Arbeitsblatts ("Open")
  • Speichern in einem Format, welches von Excel zum Export unterstützt wird, z.B. XLS, CSV, XML ("SaveAs")
  • Ausdrucken des Arbeitsblatts ("Print")
  • Ermitteln eines beliebigen Zelleninhalts ("GetCellValue")

Die Datei "ExcelDatei.h" ist zum Verständnis des Beispiels nicht unbedingt nötig, kann aber hier separat heruntergeladen werden, genauso wie ein komplettes lauffähiges Testprojekt für Visual C++ 6.0.

#include "atlbase.h" CComModule _Module; // Ist für ATL notwendig #include "atlcom.h" // Wenn man die folgende Zeile nicht kommentiert und mitkompiliert, // erhält man zwar Fehlermeldungen, aber auch zwei Dateien, von denen // eine alle wichtigen Konstanten enthält, die Excel verwendet (XL5EN32.tlh): // #import "C:\Programme\Microsoft Office\Office10\XL5EN32.OLB" #include "ExcelDatei.h" CExcelDatei::CExcelDatei() : m_spIExcelApp(NULL), m_spWorkbook(NULL) { } CExcelDatei::~CExcelDatei() { Close(); } void CExcelDatei::Close() { m_spWorkbook = NULL; if (m_spIExcelApp != NULL) { // Excel-Application wieder zumachen: VARIANT vResult; CComDispatchDriver spDispExcel(m_spIExcelApp); HRESULT hr = spDispExcel.Invoke0(L"Quit", &vResult); m_spIExcelApp = NULL; } } int CExcelDatei::Open(LPCTSTR lpszFilename) { // hat da jemand seine Excel-Datei noch nicht geschlossen ... ? _ASSERT(m_spWorkbook == NULL); _ASSERT(m_spIExcelApp == NULL); if ((m_spWorkbook != NULL) || (m_spIExcelApp != NULL)) Close(); // Unsinnige Eingaben ausschließen: if ((lpszFilename == NULL) || (_tcslen(lpszFilename) < 1)) return -1; // Dateiname für später merken: _tcscpy(m_lpszOriginalDateiname, lpszFilename); CLSID clsidExcelApplication; HRESULT hr = CLSIDFromProgID(L"Excel.Application", &clsidExcelApplication); // erfolgreichen Aufruf prüfen: _ASSERT(SUCCEEDED(hr)); if (FAILED(hr)) return -1; // Excel nicht installiert ! // Excel - Application erzeugen: hr = CoCreateInstance(clsidExcelApplication, NULL, CLSCTX_SERVER, IID_IDispatch, (void **) (IDispatch *) &m_spIExcelApp); // erfolgreichen Aufruf prüfen: _ASSERT(SUCCEEDED(hr)); _ASSERT(m_spIExcelApp != NULL); if (FAILED(hr) || (m_spIExcelApp == NULL)) return -1; // Excel nicht instanziierbar ! CComDispatchDriver spDispExcel(m_spIExcelApp); VARIANT vResult; // Beschaffen der Workbooks - Collection: { DISPID dispidWorkbooks = 0; hr = spDispExcel.GetIDOfName(L"Workbooks", &dispidWorkbooks); hr = spDispExcel.GetProperty(dispidWorkbooks, &vResult); } if (FAILED(hr) || (vResult.pdispVal == NULL)) return -1; // Workbooks nicht enthalten ! CComDispatchDriver spWorkbooks(vResult.pdispVal); { // Durch den Aufruf der Open - Methode wird ein bestehendes // Arbeitsblatt geöffnet. // Hier rufen wir nur mit einem einzigen Parameter auf, dem // Dateinamen: USES_CONVERSION; VARIANT vFilename; vFilename.vt = VT_BSTR; vFilename.bstrVal = SysAllocString(T2CW(lpszFilename)); hr = spWorkbooks.Invoke1(L"Open", &vFilename, &vResult); SysFreeString(vFilename.bstrVal); } if (FAILED(hr)) return -2; // Datei nicht gefunden ! // Das aktuelle Arbeitsblatt - Objekt beschaffen: { hr = spDispExcel.GetPropertyByName(L"ActiveWorkbook", &vResult); if (FAILED(hr)) return -1; // Workbook Nr. 1 nicht enthalten ! m_spWorkbook = vResult.pdispVal; } if (m_spWorkbook == NULL) return -1; // Workbook Nr. 1 nicht enthalten ! return 0; } int CExcelDatei::SaveAs(LPCTSTR lpszNewFilename, long lFileFormat, bool bDeleteOriginalFile) { // Sicherstellen, daß ein Workbook offen ist: _ASSERT(m_spIExcelApp != NULL); if (m_spIExcelApp == NULL) return -1; if (m_spWorkbook == NULL) return -1; CComDispatchDriver spDispWorkbook(m_spWorkbook); HRESULT hr; VARIANT vResult; { USES_CONVERSION; VARIANT vFilename, vFileFormat; vFilename.vt = VT_BSTR; vFilename.bstrVal = SysAllocString(T2CW(lpszNewFilename)); vFileFormat.vt = VT_I4; vFileFormat.lVal = lFileFormat; hr = spDispWorkbook.Invoke2(L"SaveAs", &vFilename, &vFileFormat, &vResult); SysFreeString(vFilename.bstrVal); } if (FAILED(hr)) return -2; // Speichern gescheitert ! // Workbook schließen, mit automatischer "Ja"-Beantwortung der Frage // ("Sollen die Änderungen ...") { VARIANT vAntwort; vAntwort.vt = VT_I4; vAntwort.lVal = 1; hr = spDispWorkbook.Invoke1(L"Close", &vAntwort, &vResult); } m_spWorkbook = NULL; if (bDeleteOriginalFile) { BOOL bSuccess = DeleteFile(m_lpszOriginalDateiname); if (!bSuccess) return GetLastError(); } return 0; } int CExcelDatei::Print() { // Sicherstellen, daß ein Workbook offen ist: _ASSERT(m_spIExcelApp != NULL); if (m_spIExcelApp == NULL) return -1; if (m_spWorkbook == NULL) return -1; CComDispatchDriver spDispWorkbook(m_spWorkbook); HRESULT hr; VARIANT vResult; hr = spDispWorkbook.Invoke0(L"PrintOut", &vResult); if (FAILED(hr)) return -1; // Drucken gescheitert ! return 0; } void CExcelDatei::GetCellValue(const BSTR bsRange, BSTR * pbsInhalt) { // bsRange ist die Adresse der Zelle, also z.B. "A1" *pbsInhalt = NULL; // Unsinnige Parameter abfangen: if (SysStringLen(bsRange) <= 0) return; // Sicherstellen, daß ein Workbook offen ist: _ASSERT(m_spIExcelApp != NULL); if (m_spIExcelApp == NULL) return; if (m_spWorkbook == NULL) return; CComDispatchDriver spDispWorkbook(m_spWorkbook); HRESULT hr; VARIANT vResult; hr = spDispWorkbook.GetPropertyByName(L"ActiveSheet", &vResult); _ASSERT(SUCCEEDED(hr)); CComDispatchDriver spSheet(vResult.pdispVal); hr = spSheet.GetPropertyByName(L"Cells", &vResult); _ASSERT(SUCCEEDED(hr)); CComDispatchDriver spCells(vResult.pdispVal); DISPID dwDispIDRange; { DISPPARAMS dispparms; VARIANT vArgs[1]; hr = spCells.GetIDOfName(L"Range", &dwDispIDRange); memset(vArgs, 0, sizeof(VARIANT) * 1); vArgs[0].vt = VT_BSTR; vArgs[0].bstrVal = SysAllocString(bsRange); dispparms.cArgs = 1; dispparms.rgvarg = vArgs; dispparms.cNamedArgs = 0; hr = spCells->Invoke(dwDispIDRange, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYGET, &dispparms, &vResult, NULL, NULL); _ASSERT(SUCCEEDED(hr)); if (FAILED(hr)) return; } CComDispatchDriver spRange(vResult.pdispVal); hr = spRange.GetPropertyByName(L"Value", &vResult); _ASSERT(SUCCEEDED(hr)); if (SUCCEEDED(hr)) { if ((vResult.vt == VT_BSTR) && (SysStringLen(vResult.bstrVal) > 0)) *pbsInhalt = SysAllocString(vResult.bstrVal); else if (vResult.vt == VT_I4) { wchar_t lpszBuffer[12]; swprintf(lpszBuffer, L"%ld", vResult.lVal); *pbsInhalt = SysAllocString(lpszBuffer); } else if (vResult.vt == VT_R8) { wchar_t lpszBuffer[64]; swprintf(lpszBuffer, L"%g", vResult.dblVal); *pbsInhalt = SysAllocString(lpszBuffer); } } }

Die Funktion SaveAs() kann zum Beispiel mit folgenden Parametern für das FileFormat aufgerufen werden:

  • xlNormal (= -4143, XLS-Datei)
  • xlCSV (= 6, CSV-Datei)
  • xlXMLSpreadsheet (= 46, XML-Datei)

Noch ein Hinweis bezüglich Bookmarks ...

Bookmarks eignen sich ganz hervorragend, um in Word oder Excel Vorlagen zu erstellen, die dann später von einer Applikation mit Daten gefüllt werden.

Dabei geht man beispielsweise in Word hin und definiert ein Bookmark, indem man eine Stelle im Dokument markiert und mit "Einfügen / Textmarke" einen Namen für dieses Bookmark festlegt. Aus der Applikation heraus kann man dann später diese Felder Füllen, mit Befehlen wie:

.Bookmarks("Name1").Range.Text = "Müller"

Einen kleinen Haken hat die Sache: Durch diesen Befehl geht das Bookmark verloren, kann also nicht mehr referenziert werden. Die ist aber manchmal nötig, z.B. wenn man in der Kopf- oder Fußzeile ein Datenfeld aufführen möchte, und ein zu füllendes Bookmark referenziert.

Mit folgenden Befehlen kann man ein Bookmark füllen, ohne daß die Bookmark-Definition verloren geht:

Dim objRange As Word.Range Set objRange = .Bookmarks("txtScheinNr").Range objRange.Text = CStr(strScheinNr) objRange.Bookmarks.Add "txtScheinNr", objRange

Probleme mit Excel-Formeln

Meistens kann man in Excel Formeln genauso in Zellen hineinbekommen, wie alle anderen Zellinhalte auch, also über den Befehl "Range(...).Value = ... "

Bei einigen Formeln klappt das jedoch nicht, und das liegt daran, daß bestimmte Formel-Befehle in andere Sprachen übersetzt worden sind, wie z.B. "WENN(;;)", welches im englischen Original als "IF(;;)" programmiert wird. Solche Formeln bekommt man nur mit dem Befehl "Range(...).FormulaLocal = ... " rein.

Versionskonflikte

Probleme treten auf, wenn man Office Automation in Visual Basic integriert, und eine Referenz auf ein relativ neues Office - Produkt einbaut; wenn man die damit erstellte EXE auf einem Rechner ausführt, der eine ältere Office - Version hat, knallt es bisweilen. Zum Beispiel der Excel - Befehl "SaveAs" kann gnadenlos abstürzen in einem solchen Fall.

Es gibt aber Abhilfe: Man compiliert das Projekt mit einer Referenz auf die ältere Office - Version; denn die neueren Office - Versionen sind in aller Regel Abwärtskompatibel zu alten Versionen.

Dies ist aber nicht so einfach und erfordert einen Trick: Wenn der Rechner, auf dem compiliert wird, nur die neue Office - Version kennt, und die alte Version dort nie installiert war, dann bietet er in der Liste der Referenzen auch nur die neue Version an. Auf dem Rechner, der die alte Office - Version hat (und es muß dort noch nicht einmal Visual Basic installiert sein), gibt es im Ordner "C:\Programme\Microsoft Office" verschiedene Dateien mit der Endung ".OLB", je nach Office - Version in bestimmten Unterordnern. Für jedes Office - Produkt (Excel, Word, ...) gibt es eine eigene OLB - Datei.

Diese Datei kann man kopieren auf den Rechner, auf dem compiliert wird (ich weiß aber nicht, ob dies lizenzrechtlich erlaubt ist, prüfen Sie dies unbedingt nach unter der Webadresse http://www.microsoft.com !). Dann setzt man im VB - Projekt unter Referenzen den Haken auf die neuere Office - Version weg (also z.B. "Microsoft Excel 11"). Nach Bestätigung mit OK kann man das Referenzen - Fenster wieder öffnen, und den Button "Durchsuchen" klicken; daraufhin wählt man die OLB - Datei aus, und kann anschließend die alte Office - Version in der Liste der Referenzen auswählen.

Weitere Tips

Zuschauer erwünscht ?

Man kann dem jeweiligen Office - Hostprodukt (also Word, Excel, Outlook etc) auch vorgeben, ob die Instanz, die wir hier starten, sichtbar sein soll oder nicht. Mit anderen Worten: man kann einstellen, ob die Operationen, die wir mit dem Office - Programm durchführen, live vom Betrachter verfolgt werden können, oder unsichtbar stattfinden.

lobjWord.Visible = True

oder in C++:

{ // Excel sichtbar machen: DISPID dispidVisible = 0; VARIANT vParam; vParam.vt = VT_BOOL; vParam.boolVal = VARIANT_TRUE; hr = spDispExcel.GetIDOfName(L"Visible", &dispidVisible); hr = spDispExcel.PutProperty(dispidVisible, &vParam); }

... machen beide dasselbe, nämlich das jeweilige Office - Produkt sichtbar. Mit "False" geht es entsprechend umgekehrt.

Wieviele Zeilen meines Excel - Arbeitsblattes sind gefüllt ?

Manchmal ist es sinnvoll, zu ermitteln, wieviele Zeilen eines Excel - Arbeitsblattes mit Werten gefüllt worden sind, um einen Ansatzpunkt zu haben, wo man mit automatischem Befüllen Fortsetzen kann. In Visual Basic findet man diese Information folgendermaßen heraus:

mobjWorkbook.Sheets(1).UsedRange.Rows.Count

Häufige Laufzeitfehler in VB im Zusammenhang mit Office Automation

Der Laufzeitfehler 462 taucht im Zusammenhang mit Office Automation sehr oft auf, insbesondere wenn man mehrere Dokumente gleichzeitig bearbeitet. Es gibt verschiedene Artikel von Microsoft zu diesem Problem, welches man meist durch "vollständige Qualifizierung" der Automatisierungsaufrufe lösen kann: Q189618 (PRB: Automation Error Calling Unqualified Method or Property), Q178510 und Q189949 beschreiben das Problem sowie seine Lösung.

Laufzeitfehler 5151 tritt dann auf, wenn man ein Dokument oder eine Vorlage öffnen will (z.B. mit "Documents.Add(Dateiname)"), welches aber nicht existiert. Hier sollte man als Workaround stets vor dem Befehl einen Existenztest mit den normalen VB - Bordmitteln machen, und damit eine richtige Fehlerbehandlung einbauen. Dabei sollte man auch gleich auf Lese- und Schreibberechtigung prüfen, je nachdem, was mit dem Dokument passieren soll.

Wenn man aus VB eine Word-Instanz automatisiert öffnen möchte, und auf demselben Rechner eine Word-Instanz bereits offen ist, dann erscheint aus Word heraus zunächst eine Messagebox mit dem Text "Word kann vorhandene ...", wobei das "..." variiert von konkreten Dateinamen bis zu graphischen Symbolen. Wenn man die Meldung bestätigt, bekommt man im Code in der Zeile, in der das Word.Application-Objekt instanziiert werden sollte, einen Laufzeitfehler 429 ("nicht instanziierbar").

Vermeiden von Meldungen aus einem Office - Programm

Bisweilen erscheinen bei der Automation Meldungen aus der Office - Anwendung, die unvorhergesehen daher kommen. Ein Beispiel ist die Meldung "Ich soll drucken, aber Ihr Drucker hat zu enge Ränder, da würde ich nicht alles reinquetschen können!", die man irgendwie immer mal auf einem System mit exotischem Drucker vorfindet (nur eben nicht auf den "normalen Systemen", auf denen man getestet hat :-)

Die folgenden Zeilen verhindern dies:

mobjApplication.DisplayAlerts = 0 mobjApplication.ActiveDocument.PrintOut False

Eigentlich reicht die erste Zeile schon aus (für die Masse der Fälle), aber für die heimtückische Meldung mit den Drucker - Rändern ist es laut Microsoft auch nötig, den Parameter "False" an die PrintOut - Routine mitzugeben (dies ist der erste Parameter von mehreren optionalen, er heißt eigentlich "Background", siehe MSDN).

Nutzung der Word - Rechtschreibprüfung für eigene Zwecke

Bei Codeproject gibt es einen schönen Artikel (allerdings in Englisch), der recht gut erläutert, wie man über die Fernsteuerung von Word die dort integrierte Rechtschreibprüfung für eigene Zwecke nutzen kann. Der Autor zeigt anhand eines Edit-Controls in seiner eigenen Applikation, bei der Word noch nichtmal sichtbar sein muß, wie er die Word - Rechtschreibprüfung fernsteuert. Verwendete Programmiersprache: C++.

Hier der Link: http://www.codeproject.com/KB/COM/AutoSpellCheck.aspx

Verfügbare Excel-Befehle herausfinden

Wenn man seine Anwendung schreibt, weiß man in der Regel nie im Voraus, welche Office - Version auf dem Ziel-Rechner installiert ist, und nutzt daher am besten stets das sogenannte "Late Binding". Wenn man nun Funktionen aus einer relativ neuen Office - Version verwendet, die nicht zwingend auf jedem Rechner verfügbar ist, sollte man im Code abfragen, ob diese Funktion überhaupt unterstützt wird. Wie das geht, beschreibt ein Artikel in codeproject.com (in relativ schlechtem Englisch).

Hier der Link: http://www.codeproject.com/KB/cs/How2LateBinding.aspx

Ganz simples Drucken in .NET 2008 mittels Word-Viewer

Eine sehr simple Lösung bietet sich in .NET, wenn man den kostenlosen Microsoft Word - Viewer benutzt, dann muß auf den Zielrechnern noch nicht einmal Microsoft Word oder Microsoft Office installiert sein. Der Trick besteht darin, sich eine XML-Datei als Vorlage (Template) abzulegen, und dynamische Felder zur Laufzeit einfach per Text - Ersetzung zu befüllen.

Hier der Link zu einem Artikel, der die Technik kurz beschreibt: http://www.codeproject.com/KB/vb/xmlwordprint.aspx

Eingebettetes

Die Königsdisziplin ist zweifellos das Arbeiten an eingebetteten Objekten, wenn also beispielsweise eine Word - Datei in sich ein Excel - Arbeitsblatt integriert hat; häufig besteht die Notwendigkeit, dessen Inhalt von außen zu manipulieren.

In Visual Basic geht dies folgendermaßen:

Dim objExcelSheet As Object, nObjektNr As Long, i As Integer With lobjWord.ActiveDocument For nObjektNr = 1 To .InlineShapes.Count If .InlineShapes(nObjektNr).Type = wdInlineShapeEmbeddedOLEObject Then If .InlineShapes(nObjektNr).OLEFormat.ProgID = "Excel.Sheet.10" Then .InlineShapes(nObjektNr).OLEFormat.DoVerb wdOLEVerbPrimary Set objExcelSheet = .InlineShapes(nObjektNr).OLEFormat.Object For i = 1 To 10 objExcelSheet.Sheets(1).Cells(i, 1).Value = i Next i MsgBox objExcelSheet.Sheets(1).Range("A1").Value Exit For End If End If Next nObjektNr End With

In diesem Beispiel wurde in einer Word - Datei nach allen eingebetteten Excel - Tabellen gesucht, und die ersten 10 Zellen der Zeile 1 bekommen jeweils die Spaltennummer als Zelleninhalt eingetragen.
Zum Schluß wird noch der Inhalt der Zelle A1 als MessageBox ausgegeben.

Um einiges schwieriger und komplizierter wird es in C++. Um es vorweg zu nehmen, ich habe meine Forschung noch nicht bis zum Ende getrieben. Ich bin in der Lage, das integrierte Excel - Sheet unter C++ anzusprechen, seinen Tabellennamen abzufragen, aber sobald versucht wird, auf eine einzelne Zelle zuzugreifen, crasht die Applikation (zumindest mit den Befehlen, die ich bislang getestet habe). Hier also der Code:

CComDispatchDriver spDispDokument(spDokument); // Selection-Objekt beschaffen: { DISPID dispidSelection = 0; hr = spDispWord.GetIDOfName(L"Selection", &dispidSelection); hr = spDispWord.GetProperty(dispidSelection, &vResult); } CComDispatchDriver spDispSelection(vResult.pdispVal); // Alles auswählen: hr = spDispSelection.Invoke0(L"WholeStory", &vResult); // InlineShapes - Collection beschaffen: { DISPID dispidInlineShapes = 0; hr = spDispSelection.GetIDOfName(L"InlineShapes", &dispidInlineShapes); hr = spDispSelection.GetProperty(dispidInlineShapes, &vResult); } CComDispatchDriver spDispInlineShapes(vResult.pdispVal); // Erstes Shape aus der Collection holen: { VARIANT vArgs; vArgs.vt = VT_I4; vArgs.lVal = 1; hr = spDispInlineShapes.Invoke1(L"Item", &vArgs, &vResult); } CComDispatchDriver spDispShape1(vResult.pdispVal); // OLEFormat - Objekt holen: hr = spDispShape1.GetPropertyByName(L"OLEFormat", &vResult); _ASSERT(SUCCEEDED(hr)); CComDispatchDriver spDispOLEFormat(vResult.pdispVal); // Excel-Objekt aktivieren: { VARIANT vArgs; vArgs.vt = VT_I4; //vArgs.lVal = -2; // wdOLEVerbOpen vArgs.lVal = 0; // wdOLEVerbPrimary hr = spDispOLEFormat.Invoke1(L"DoVerb", &vArgs, &vResult); _ASSERT(SUCCEEDED(hr)); } // zugehörige ProgID untersuchen: // hr = spDispOLEFormat.GetPropertyByName(L"ProgID", &vResult); // hier kann man mit dem Debugger sehen, daß es ein Excel.Sheet ist // _ASSERT(SUCCEEDED(hr)); hr = spDispOLEFormat.GetPropertyByName(L"Object", &vResult); _ASSERT(SUCCEEDED(hr)); Excel::WorkbookPtr spExcelWorkbook(vResult.pdispVal); _ASSERT(spExcelWorkbook != NULL); Excel::WorksheetPtr spWorksheet(spExcelWorkbook->GetActiveSheet()); _ASSERT(spWorksheet != NULL); // Name des integrierten Sheets checken: vResult = spWorksheet->GetName(); vResult = spWorksheet->Select(); { _variant_t vtRow(L"1"); _variant_t vtColumn(L"A"); --> Crashzeile: vResult = spWorksheet->Cells(vtRow, vtColumn); }

Ich habe bislang noch nicht herausgefunden, warum man den Namen des Worksheets abfragen kann, aber jeglicher Versuch, auf eine Zelle zuzugreifen, mit einem Crash endet. Wenn es ein kluger Leser herausfindet oder bereits weiß, bin ich für einen Tip natürlich dankbar. Ich tippe am ehesten auf ein Marshaling-Problem, möglicherweise wird unerlaubterweise über Apartment- Grenzen zugegriffen.

Buchtips

cover


cover


Die Reihe "Das Handbuch" von Microsoft Press ist eine sehr gelungene Serie zu den unterschiedlichen Office-Produkten. Für den Anwender, ob Anfänger oder Profi, bleibt keine Frage offen, wenngleich für den Programmierer die Themen VBA und Automation nicht detailliert genug behandelt werden. Trotzdem halte ich es für ein Muß für jeden, der professionell mit Office - Produkten arbeitet.

Informationen zum Excel-Handbuch
Informationen zum Word-Handbuch

cover


"Essential COM" von DonBox ist der Klassiker, um den Einstieg in das COM - Komponentenmodell zu bekommen. Für jemanden, der C (oder C++) kann, ist der Einstieg sogar noch etwas leichter. Für den Visual Basic - Entwickler lohnt es sich auf jeden Fall, zu Wissen, was "unter der Haube" passiert, wenn New oder CreateObject aufgerufen wird, und wie Events gefeuert werden.

Auch in .NET wird in Bezug auf die Komponentenarchitektur auf COM zurückgegriffen, so daß es keine vergeudete Investition ist.

Informationen zum Buch



cover


"Professional ATL COM" von Dr. Richard Grimes bietet einen guten Einstieg in die Programmierung mit ATL - Templates. Es wird auch sehr viel COM - Hintergrundwissen vertieft, wer also nur ein einziges Buch sucht, und die COM - Grundlagen kennt, sollte eher zu diesem Buch greifen. Die Active Template Library (ATL) ist eine Art Framework für die COM - Programmierung, welches dem Entwickler eine Menge an Arbeit abnimmt, und ihm trotzdem die volle Kontrolle läßt. Es ist aber nur etwas für C++ - Entwickler, in Visual Basic hat man keine Chance, damit zu arbeiten.

Informationen zum Buch



cover


cover


Support, Feedback, Anregungen

Alles verstanden ? Oder noch Fragen ? Wir freuen uns über Feedback zu den Tips, auch über Verbesserungsvorschläge. Schreiben Sie an support@a-m-i.de.