(C) 2009 - 2021 by Mourad Louha · Alle Rechte vorbehalten

Excel VBA Application Objekte per Windows API erzeugen (Teil 1)

Im Microsoft Office 2010 Answers Forum stellte jemand die Frage, wie auf eine Excel Instanz von einer weiteren zugegriffen werden könnte und aus dieser heraus, Code in der ersten Instanz ausgeführt werden könnte. Dies brachte mich auf die Idee, alle Excel Instanzen per Windows API zu ermitteln und zu versuchen, auf diese per VBA zuzugreifen. Dieser Artikel beschreibt die Vorgehensweise und stellt Teile des Codes vor, wobei Kenntnisse in der VBA Programmierung vorausgesetzt werden, so auch das Verwenden von TreeViews, ListViews und Einbinden von Windows API Funktionen. Eine Beispielanwendung kann am Ende des Artikels heruntergeladen werden und ist ungeschützt, so dass der Code eingesehen werden kann.

VBA Window Handles

Eine relativ bekannte Möglichkeit, an ein Application Objekt einer anderen Excel Instanz heranzukommen, ist die Methode GetObject(). Wird diese beispielweise über Set xlApp = GetObject(, „Excel.Application“) aufgerufen, liefert die Methode eine Referenz zur zuerst gestarteten Excel Instanz. Sind jedoch weitere Excel Instanzen offen, ist es nicht möglich, auf die zweite Instanz mit Hilfe dieser Methode zuzugreifen, sofern man nicht den Namen einer geöffneten Arbeitsmappe in der anderen Instanz kennt.

In Windows werden Fenster über sogenannte „Handles“ verwaltet. Diese sind in einer Hierarchie angeordnet, wobei der Desktop die oberste Ebene bildet. Die Fensterhandles werden dynamisch zugewiesen und, vereinfacht gesagt, als HWND Datentyp geführt, wobei jedes generierte Fensterhandle immer eindeutig ist. Interagieren können Anwendungen über ein Nachrichtensystem; beispielsweise kann eine Anwendung eine Nachricht an ein Dokumentfenster zur Neupositionierung des Fensters senden oder ein Unterfenster einen Mausklick an das Hauptfenster der Anwendung weiterleiten.

Ich bin glücklicher Besitzer von Visual Studio 2010, welches ein Tool enthält, um alle Fensterhandles zu ermitteln und anzuzeigen. Folgende Abbildung zeigt einen Teil der Fensterhierarchie zum Zeitpunkt, wo ich gerade diesen Artikel schreibe und mehrere Excel Instanzen inklusive einer Excel 2010 Instanz geöffnet habe.

VBA Window Handles

Das Hauptfenster von Excel 2010 hat hier das hexadezimal dargestellte Handle „0015058A“ und die Arbeitsmappe „Dummy.Excel.2010.xlsx“ das Handle „00020642“. Das Fenster der Arbeitsmappe ist logischerweise hierarchisch unterhalb des Hauptfensters angesiedelt, da es ja von letzterem abhängt. Zudem sind rechts neben den Titeln der Fenster die Bezeichner „XLMAIN“ und „EXCEL7“ zu sehen, die jeweils eine interne Klassenbezeichnung darstellen. Alle Excel Instanzen haben dieselbe Klassenbezeichnung „XLMAIN“, unabhängig von der Excel Version. Diese Bezeichnung werde ich später mir zu Nutze machen, um die Fensterhandles zu filtern. Die Beispielanwendung enthält eine UserForm, in welcher folgende Steuerelemente abgelegt sind:

  • Eine Combobox dient zur Auswahl der Office Anwendung, wobei in dieser Beispielanwendung nur Microsoft Excel vorgesehen ist.
  • Ein TreeView soll, wie das Visual Studio Tool, die Fenster hierarchisch darstellen.
  • Ein paar Steuerelemente zeigen Informationen zum dem im TreeView markierten Eintrag an.
  • Ein Button referenziert, wenn möglich, ein VBA Objekt zum Excel Application Objekt der Excel Instanz.
  • Eine ListView enthält, je nach markiertem Eintrag im TreeView, die über VBA abrufbaren Excel Instanzen.
  • Einige Felder liefern weitere Infos zur geöffneten Excel Instanz.

Die Beispielanwendung enthält die drei Module „MLP_Api“, „MLP_Controls“ und „MLP_Run“. Ersteres enthält die Importe der Windows API Funktionen sowie einige Hilfsfunktionen. Im Modul „MLP_Controls“ finden Sie eine wesentliche Funktionalität der Anwendung: das Füllen des TreeViews und Ermitteln der Fenster. MLP_Run enthält nur Code zum Starten der UserForm und Beenden der Anwendung. Im Codemodul zur UserForm sind, wie üblich, die Ereignishandler zu dessen Steuerelementen abgelegt und ein paar Hilfsfunktionen implementiert.

Fensterhandles ermitteln und im TreeView ablegen

Wie kommen wir aber nun an die Handles der Fenster heran? Zunächst müssen wir die passenden Windows API Funktionen deklarieren und importieren. Da wir wissen, dass der Desktop das Wurzelelement in der Fensterhierarchie ist, binden wir die API Funktion „GetDesktopWindow()“ ein. Um den Titel des Fensters, den Klassennamen und das übergeordnete Fenster zu ermitteln, binden wir noch respektive „GetWindowTextA()“, „GetClassNameA()“ und „GetParent()“ein. Zudem möchten wir ja auch die Kindfenster ermitteln, somit ist noch die entsprechende API Funktion „EnumChildWindows()“ einzubinden. Zum Suchen eines speziellen Fensters kann die API Funktion „FindWindowExA()“ verwendet werden. Tipp: viele Windows API Funktionen sind so benannt, dass wenn nach den englischen Begriffen zu ihrem Zweck, diese verhältnismäßig einfach bei MSDN gefunden werden können. In MLP_Api steht nun in einem ersten Schritt:

Anschließend habe ich der UserForm die Steuerelemente und wichtigsten Ereignishandler hinzugefügt. Um die Elemente (Nodes) im TreeView anzulegen, habe ich die Funktion „mlfpControlsTreeviewWindows()“ implementiert. Diese habe ich jedoch in das Modul „MLP_Controls“ ausgelagert, um z.B. ein einfacheres wiederverwenden in anderen Projekten zu ermöglichen. Als Argumente erwartet diese Funktion eine Referenz auf die geladene UserForm, den Namen des TreeViews, den Klassennamen des Hauptfensters der gesuchten Anwendung und zwei Zusatzparameter, die festlegen, ob die Kindfenster ermittelt werden sollen oder es sich um Excel handelt.

Die Funktion sieht auf einen ersten Blick etwas komplex aus, ist jedoch relativ einfach aufgebaut. Zunächst wird das Handle zum Desktop über die Windows API Funktion ermittelt und ein Eintrag dem TreeView hinzugefügt. Jedes TreeView Element erhält übrigens als Schlüssel den String „K & Fensterhandle“. Dies ermöglicht später auf sehr einfache Weise, die Fensterhierarchie aufzubauen. In der Eigenschaft „Tag“ jedes TreeView Elementes sind zusätzliche Informationen zu den gefundenen Fenstern abgelegt, wie der Titel und der Klassenname. Hierfür wird, der Übersichtlichkeit halber, die Funktion „mlfhTag()“ aufgerufen.

Anschließend wird geprüft, ob Excel Instanzen abgerufen werden sollen. Wenn ja, wird das Handle der Instanz, in welcher der Code läuft, als erster Untereintrag im TreeView hinzugefügt. Um alle weiteren Instanzen von Excel zu ermitteln, durchläuft der Code danach eine Do While Schleife. In dieser werden die Handles der dem Desktop untergeordneten Fenster abgerufen, die als Klassenname den Wert der Variable „Code“ enthalten; in unserem Fall ist das dann „XLMAIN“ für Excel. Für jedes gefundene Handle ruft der Code die Funktion „mlfhChildEnumerator()“ auf, die wiederum alle Kindfenster ermittelt. Diese Funktion enthält letztlich nur den API Aufruf:

An dieser Stelle wird es interessant, denn die API Funktion erwartet als Parameter einen Zeiger auf eine sogenannte Callback-Funktion. Heißt, diese Funktion wird von der API Funktion so oft aufgerufen, wie Fensterhandles ermittelt werden können und ermöglicht somit bei jedem Aufruf die übergebenen Argumente auszuwerten. In der Beispielanwendung habe ich als Callback-Funktion „mlfhChild()“ verwendet. Die Argumente der Callback-Funktion müssen übrigens vorgegebenen Regeln gehorchen, es kann also nicht irgendeine Funktion verwendet werden.

Wie zu sehen, wird über den Aufruf von „mlfpApiGetParent(Handle)“ das Handle zum übergeordneten Fenster ermittelt, dem TreeView als Untereintrag hinzugefügt und die „Tag“ Eigenschaft des eingefügten Elementes gesetzt. Und da sich die Schlüssel im Treeview aus „K & Fensterhandle“ zusammensetzen, sortieren sich die neuen Elemente von selbst ein. Im Wesentlichen war’s das dann auch schon, um die Fenster zu ermitteln.

Application Objekt aus einem Fensterhandle ermitteln

Wie bekommen wir aber nun ein Fensterhandle in ein Application Objekt „umgewandelt“, so dass dann „ganz normal“ über VBA auf diese Excel Instanz zugegriffen werden kann?

Andrew Whitechapel hat dazu einen sehr interessanten Artikel geschrieben, der sich zwar auf .NET bezieht, aber prinzipiell in VBA umsetzbar ist. Da die Office Anwendungen eine Automatisierung per COM ermöglichen, müsste man nur ein solches Element in der Hierarchie der Handles finden, um anschließend daraus ein in VBA nutzbares Objekt generieren zu können. Ist dieses Objekt einmal erstellt und würde es sich auf ein Unterobjekt von Application Objekt der Hauptanwendung beziehen, wäre das denn kein Problem mehr, denn über die „Application“ Eigenschaft der Unterobjekte gelangen wir dann wiederum an das Application Objekt selbst.

Um zu prüfen, ob ein gegebenes Fensterhandle eine entsprechend verwertbare Schnittstelle enthält, kann die Windows API Funktion „AccessibleObjectFromWindow()“ verwendet werden. Trick hierbei ist, einerseits eine in eine Struktur transferierte GUID und andererseits ein VBA Objekt an die Funktion zu übergeben. Falls der Aufruf der API Funktion erfolgreich war, kann aus der Rückgabe ein VBA Application Objekt initialisiert werden, welches dann genau dem Application Objekt der gefundenen Excel Instanz entspräche. Ok, hört sich kompliziert an, ist aber relativ einfach:

Hierbei muss man wissen, dass COM Komponenten über eine GUID (Global Unique Identifier) identifiziert werden können und die verfügbaren Funktionen über Interfaces zur Verfügung stellen, die auch wiederum über GUID’s unterschieden werden können. Eine solche verwendbare ID ist für Excel 2007 der Wert „{00020400-0000-0000-C000-000000000046}“.

In der Beispielmappe sind im Codemodul zur UserForm die Funktionen „mlfhTreeviewDecodeTag()“ und „mlfhTreeviewIterate()“ abgelegt. Erste Funktion zerlegt den Tag des gewählten Eintrags im TreeView und zeigt die Eigenschaften des Fensters an. Zudem wird ermittelt, ob für das Fensterhandle zum ausgewählten Eintrag sowie für alle untergeordneten Einträge im TreeView eine Objekterstellung zur Excel Instanz möglich ist. Das Durchlaufen der Untereinträge übernimmt dann die Funktion „mlfhTreeviewIterate()“. Jedes gefundene Fenster, welches eine Objekterstellung erlaubt, wird der ListView „LSV_0001“ hinzugefügt. Folgend der Code zu „mlfhTreeviewIterate()“.

Wenn Sie beispielsweise mehrere Excel Instanzen öffnen und anschließend die Beispielanwendung sowie die UserForm aufrufen, werden Sie feststellen, dass bei einem Klick auf den Eintrag „Desktop“ alle geöffneten Arbeitsmappen der verschiedenen Excel Instanzen aufgelistet werden. Ist jedoch in einer der Instanzen keine Mappe geöffnet, wird das Fenster zwar aufgeführt, es kann jedoch kein Objekt auf diese Instanz erstellt werden. Somit ist ein Kriterium für die erfolgreiche Objekterstellung, dass mindestens eine Mappe in der Instanz geöffnet ist.

Momentan bin ich noch auf der Suche nach einer Lösung zu diesem Problem; ob die Objekterstellung im Fall geöffneter Instanzen ohne Arbeitsmappe möglich sein wird, kann ich noch nicht sagen. In einem weiteren Artikel werde ich dann über meine Erkenntnisse berichten. Ausserdem wird die Beispielanwendung um weitere Office Anwendungen erweitert werden. Und ich plane zusätzliche Testfunktionen zur „Fernsteuerung“ der Anwendungen.

Abschließend der Hinweis, dass es sich bei dieser Anwendung um ein Experiment handelt; es kann keine Gewähr oder Garantie übernommen werden, dass die Anwendung auf Ihrem System funktionsfähig ist oder nicht unerwartet reagiert. Ich empfehle während der Ausführung der Beispielanwendung keine wichtigen Dokumente geöffnet zu haben. Getestet wurde die Beispielanwendung unter Windows 7 mit parallel – wichtig – installierten Office 2003, 2007 und 2010. Die Anwendung ist für 32 Bit ausgelegt, d.h. unter Windows 7 64 Bit funktioniert die Anwendung nicht, da sich dort die Einbindung der API Funktionen unterscheidet. Folgend der Downloadlink zur Beispielanwendung sowie im Anschluß Links mit weiterführenden Artikeln zum Thema.

Nachtrag vom 18.08.2010: den Titel dieses Artikels musste ich nachträglich ändern, da er sonst zu lang geworden wäre. Zudem ist nun ein Folgeartikel vorhanden, wo auch eine neue Version zum Tool zu finden ist. Excel VBA Application Objekte per Windows API erzeugen (Teil 2)