Excel VBA Programmierung
Mit der Declare-Anweisung ist es möglich auf Modulebene Verweise auf Funktionen in einer DLL (Dynamic Link Library) zu deklarieren. Solche DLL können z.B. in C oder C++ geschrieben sein und sind immer dann sinnvoll einzusetzen, wenn zeitkritische Aufgaben bearbeitet werden sollen, der Umfang von Visual Basic nicht ausreicht oder, wie in diesem Fall die eigenen Kenntnisse in einer anderen Sprache die in VBA deutlich übersteigen. Dieser Artikel zeigt, wie man mit Hilfe des kostenlosen Microsoft Visual C++ 2008 Express [1] eine C++ DLL erstellt und diese in einer Excel Anwendung einsetzt.
Der Projektassistent von Visual C++ 2008 bietet ein Template für Win32 DLL Projekte,
erreichbar unter File->New->Project... Win32 Win32 Project.

Im folgenden Dialog setzt man Häkchen für "DLL" und "Export Symbols" und
beendet de Assistenten mit "Finish".

Der Assistent legt einige Dateien für uns an. Die dllmain.cpp definiert den Einstiegspunkt für die DLL. Um diese Datei müssen wir uns erstmal nicht weiter kümmern. In der ranges.cpp können wir Klassen und Funktionen definieren, die von der DLL exportiert werden sollen. Der Assitent hat hier schon einige Beispiele angelegt, die alle gelöscht werden können.
In der Datei ranges.h findet man folgenden Hinweis:
// The following ifdef block is the standard way of creating macros which make exporting // from a DLL simpler. All files within this DLL are compiled with the RANGES_EXPORTS // symbol defined on the command line. this symbol should not be defined on any project // that uses this DLL. This way any other project whose source files include this file see // RANGES_API functions as being imported from a DLL, whereas this DLL sees symbols // defined with this macro as being exported. #ifdef RANGES_EXPORTS #define RANGES_API __declspec(dllexport) #else #define RANGES_API __declspec(dllimport) #endif
Um Funktionen aus einer DLL zu exportieren bieten sich grundsätzlich zwei Methoden an [2].
Die eine Möglichkeit ist die vom Assistenten vorgesehene Verwendung des __declspec(dllexport)-Schlüsselwortes
in der Funktionsdefinition. Die andere Möglichkeit ist das Erzeugen einer
Moduldefinitionsdatei (.def) [3].
Bei Verwendung der Declare-Anweisung wird die aufzurufene Funktion über deren Namen identifiziert.
Funktionen in C- und C++-Programmen werden i.d.R. intern durch ergänzte Namen angesprochen.
Diese ergänzten Namen [4] unterscheiden sich jenachdem ob es sich um eine
C oder C++ Funktion handelt. Durch Verwendung von extern "C" in der Funktionsdeklaration
kann für C++ Funktionen die C-Bindung angegeben werden. Für diese Funktion wird dann die
C Namensergänzung verwendet.
Verwendet man eine Moduldefinitionsdatei um Funktionen zu exportieren, werden die in der .def Datei
angegebenen Funktionsnamen exportiert. Beim Exportieren von C++ Funktionen sollten in der .def Datei
die erweiterten Namen angegeben werden oder die Funktion mit extern "C" deklariert werden.
Um die exportierten Funktionen einer DLL und ihre Namen zu betrachten, kann das Programm Dependency Walker [5] eingesetzt werden. Die folgenden drei Funktionen werden durch drei unterschiedliche Methoden exportiert:
// ranges.h // Verwendet __declspec(dllexport): RANGES_API int __stdcall func1(); // Verwendet __declspec(dllexport) und C Bindung: extern "C" RANGES_API int __stdcall func2(); // Verwendet .def Datei: extern "C" int __stdcall func3(); //ranges.def LIBRARY RANGES EXPORTS func3 @1
Öffnet man die erstellte DLL mit Dependency Walker werden folgende Funktionen angezeigt:

Um in Excel in der Declare-Anweisung den einfachen Funktionsnamen benutzen zu können, empfiehlt sich
der Einsatz der .def Datei. Dazu wird dem Projekt eine .def Datei hinzugefügt, in die in der ersten Zeile
die LIBRARY Anweisung eingefügt wird. Auf die LIBRARY Anweisung folgt der Name der DLL und die
EXPORTS Anweisung in der alle exportierten Namen mit Ordinalzahl (optional).
Damit der Linker die .def Datei verwendet muss sie in den Linkereinstellungen angegeben werden (/DEF:"ranges.def" bzw. Linker->Input->Module Definition File).

Die voreingestellte Aufrufkonvention (calling convention) ist __cdecl (\Gd).
Wird die DLL mit dieser Einstellung erstellt und einer ihrer Funktion in Excel aufgerufen,
erhalten wir folgenden Fehler: Laufzeitfehler '49': Falsche Dll Aufrufkonvention

Hinweis: Die gleiche Fehlermeldung erhält man unter Umständen, bei Übergabe von falschen Argumenten!
Um eine Funktion aus Excel aufzurufen muss sie mit __stdcall Aufrufkonvention deklariert werden.
Dies kann entweder in den Projekteinstellungen für das gesamte Projekt oder für jede Funktion einzeln
vorgenommen werden.

In Visual Basic werden Argumente standardmäßig als Referenz übergeben. Wird der Funkion
extern "C" long __stdcall func1(long n);
ein Wert ByRef übergeben, so erhält n die Adresse dieses Wertes. Es sollte daher darauf geachtet werden, in der Declare-Anweisung das Argument als ByValue zu deklarieren oder die Funktion umzuschreiben, dass sie einen Zeiger auf long akzeptiert.
extern "C" long __stdcall func2(long * n); // ranges.xls Private Declare Function Func2 Lib "ranges.dll" Alias "func2" (ByRef n As Long) As Long Private Declare Function Func1 Lib "ranges.dll" Alias "func1" (ByVal n As Long) As Long
Die Declare-Anweisung deklariert auf Modulebene Verweise auf externe Prozeduren in einer DLL. Die Syntax ist:
[Public | Private] Declare Function Name Lib "BibName" [Alias "Aliasname"] [([ArgListe])] [As Typ]
Ist die Bibilothek nicht geladen, wird diese beim Aufruf der Funktione Name automatisch gesucht und
geladen. Wird die .dll Datei nicht gefunden erhält man die Fehlermeldung:
Laufzeitfehler '53': Datei nicht gefunden: ranges.dll
Wird "BibName" ohne vollständigen Pfad angegeben, wird die angegebene Datei an folgenden Orten gesucht:
Der letzte Punkt ist etwas verwirrend, denn das Verzeichnis der xls ist nicht immer als aktuelles Arbeitsverzeichnis gesetzt.
Wird die DLL im Verzeichnis der xls abgelegt und die xls in Excel über Datei->Öffnen... geöffnet, ist das Verzeichnis
korrekt gesetzt und die DLL wird gefunden. Wird die xls auf anderen Wegen geöffnet, z.B. über Doppelklick im Explorer wird die
.dll Datei i.d.R. nicht gefunden. Die .dll Datei sollte daher an einem der anderen genannten Orten ebgelegt werden
oder es sollte eine vollständig Pfadangabe in Declare benutzt werden.
Wenn diese Vorgehensweise nicht wirklich zufriedenstellend ist, z.B. wenn die xls zusammen mit der dll
an Kollegen weitergegeben werden soll, bietet sich die Möglichkeit die DLL manuell mit Hilfe von LoadLibrary aus der kernel32.dll zu laden. Dazu erstellen wir ein Modul und fügen die folgenden Declare-Anweisungen ein.
Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal lpLibFileName As String) As Long Declare Function FreeLibrary Lib "kernel32" (ByVal hLibModule As Long) As Long Declare Function Func1 Lib "ranges.dll" Alias "func1" (ByVal n As Long) As Long
Nun können wir an geeigneter Stelle (z.B. beim Öffnen der Arbeitsmappe) die Bibliothek laden, eine ihrer Funktionen aufrufen und sie nach Gebrauch entladen.
Dim hndModule As Long
Private Sub Workbook_Open()
hndModule = LoadLibrary(ThisWorkbook.Path & "\ranges.dll")
End Sub
Private Sub Workbook_BeforeClose(Cancel As Boolean)
FreeLibrary (hndModule)
End Sub
Private Sub CommandButton1_Click()
A = Func1(5)
End Sub
Um die DLL in der gewünschten Excel Anwendung eingesetzt zu debuggen,
können folgende Einstellungen vorgenommen werden.

Excel wird hierbei automatisch mit der angegebenen xls gestartet.
Range Objekte werden aus Excel als VARIANT übergeben. VARIANT Objekte speichern Werte von unterschiedlichen Typen. Welcher Typ in einem VARIANT gespeichert ist, lässt sich über den vt(VARTYPE) Wert abfragen (siehe VARENUM). Um den Wert aus einem VARIANT in einen Wert unserer Wahl zu kopieren definieren wir zunächst folgendes Funktionstemplate (wir beschränken uns dabei zunächst auf vier Typen):
template <typename Out, typename In> Out var_cast(const In & in) { Out out; wstringstream ws; if (ws << in){ if (ws >> out) return out; } ws.clear(); ws.str(L""); ws << 0; ws >> out; return out; } template <typename T> T get_variant_element(VARIANT & val) { T t; switch(val.vt){ case VT_I2: t = var_cast<T>(val.iVal); break; case VT_R8: t = var_cast<T>(val.dblVal); break; case VT_INT: t = var_cast<T>(val.intVal); break; case VT_BSTR: t = var_cast<T>(val.bstrVal); break; default: t = var_cast<T>(0); } return t; }
Als Cast ist hier ein Funktionstemplate var_cast definiert. var_cast verwendet Streams, so dass die Voraussetzung für den Typ des Eingabewertes ein definierter << Operator, der ein std::wostream auf der linken Seite akzeptiert, sowie für den Typ des Rückgabewertes ein definierter >> Operator, der ein std::wistream auf der linken Seite akzeptiert ist.
Beispiel:
struct Bogus { double d; }; std::wistream & operator >> (std::wistream & ws, Bogus & t) { ws >> t.d; return ws; }
Range Objekte werden als VARIANT vom Typ VT_DISPATCH übergeben. Um an die gespeicherten Werte zu kommen, benutzt die folgende get_array-Funktion die IDispatch::GetIDsOfNames und IDispatch::Invoke Methoden. Mehr Informationen zum IDispatch Interface findet man im MSDN [6].
void get_array(const VARIANT & range, VARIANT & arr) { EXCEPINFO excep; DISPPARAMS dispparams; unsigned int uiArgErr; DISPID dispidValue; LPOLESTR XName = L"Value"; range.pdispVal->GetIDsOfNames(IID_NULL, &XName, 1, LOCALE_SYSTEM_DEFAULT, &dispidValue); dispparams.cArgs = 0; dispparams.cNamedArgs = 0; // Invoke PROPERTYGET and store values in array range.pdispVal->Invoke(dispidValue, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYGET, &dispparams, &arr, &excep, &uiArgErr); range.pdispVal->Release(); }
Mit der get_array Funktion können wir nun ein Funktionstemplate definieren, das einen SAFEARRAY (vt & VT_ARRAY) mit den Werten der Range erzeugt und jeden Wert einem Funktionsobjekt übergibt. Da die Funktion mit Kopien der Originalwerten arbeitet wird sie with_each_in_range genannt.
template <typename T, typename Fun> void with_each_in_range(const VARIANT& range, Fun func) { if(range.vt != VT_DISPATCH) return; VARIANT arr; get_array(range, arr); if (arr.vt & VT_ARRAY){ //get row&column count long cols = (arr.parray)->rgsabound[0].cElements, rows = (arr.parray)->rgsabound[1].cElements; for(long c=1; c <= cols; ++c){ for(long i=1; i <= rows; ++i){ long indi[] = {i,c}; VARIANT val; SafeArrayGetElement(arr.parray, indi, &val); func(get_variant_element<T>(val)); } } } else func(get_variant_element<T>(arr)); VariantClear(&arr); }
Mit diesem Funktionstemplate lassen sich nun verschiedene Funktionen realisieren. Wenn man z.B. die Werte einer Range in einem vector speichern möchte, könnte man schreiben:
extern "C" void Range_To_Vector(VARIANT & values) { if(values.vt == VT_DISPATCH){ vector<wstring> Vws; with_each_in_range<wstring>(values, boost::bind(&vector<wstring>::push_back, &Vws, _1)); vector<double> Vd; with_each_in_range<double>(values, boost::bind(&vector<double>::push_back, &Vd, _1)); ... } }
In unserem Beispiel Projekt ist eine Funktion definiert, die die Summe der übergebenen Werte zurückgibt:
struct sum : public binary_function<double,double*, void> { void operator()(const double & lh, double * const rh)const{ *rh+=lh; } }; extern "C" double sum_range(VARIANT& values) { double res = 0; if(values.vt == VT_DISPATCH) with_each_in_range<double>(values, bind2nd(sum(), &res)); else MessageBox(NULL, L"Pass a range!", L"error", NULL); VariantClear(&values); return res; }
Das Beispiel Projekt kann hier heruntergeladen werden:
doxapp_ranges.7z 77 KB (Visual C++ 2008)
/* * * [V] - may appear in a VARIANT * * [T] - may appear in a TYPEDESC * * [P] - may appear in an OLE property set * * [S] - may appear in a Safe Array * * * VT_EMPTY [V] [P] nothing * VT_NULL [V] [P] SQL style Null * VT_I2 [V][T][P][S] 2 byte signed int * VT_I4 [V][T][P][S] 4 byte signed int * VT_R4 [V][T][P][S] 4 byte real * VT_R8 [V][T][P][S] 8 byte real * VT_CY [V][T][P][S] currency * VT_DATE [V][T][P][S] date * VT_BSTR [V][T][P][S] OLE Automation string * VT_DISPATCH [V][T] [S] IDispatch * * VT_ERROR [V][T][P][S] SCODE * VT_BOOL [V][T][P][S] True=-1, False=0 * VT_VARIANT [V][T][P][S] VARIANT * * VT_UNKNOWN [V][T] [S] IUnknown * * VT_DECIMAL [V][T] [S] 16 byte fixed point * VT_RECORD [V] [P][S] user defined type * VT_I1 [V][T][P][s] signed char * VT_UI1 [V][T][P][S] unsigned char * VT_UI2 [V][T][P][S] unsigned short * VT_UI4 [V][T][P][S] unsigned long * VT_I8 [T][P] signed 64-bit int * VT_UI8 [T][P] unsigned 64-bit int * VT_INT [V][T][P][S] signed machine int * VT_UINT [V][T] [S] unsigned machine int * VT_INT_PTR [T] signed machine register size width * VT_UINT_PTR [T] unsigned machine register size width * VT_VOID [T] C style void * VT_HRESULT [T] Standard return type * VT_PTR [T] pointer type * VT_SAFEARRAY [T] (use VT_ARRAY in VARIANT) * VT_CARRAY [T] C style array * VT_USERDEFINED [T] user defined type * VT_LPSTR [T][P] null terminated string * VT_LPWSTR [T][P] wide null terminated string * VT_FILETIME [P] FILETIME * VT_BLOB [P] Length prefixed bytes * VT_STREAM [P] Name of the stream follows * VT_STORAGE [P] Name of the storage follows * VT_STREAMED_OBJECT [P] Stream contains an object * VT_STORED_OBJECT [P] Storage contains an object * VT_VERSIONED_STREAM [P] Stream with a GUID version * VT_BLOB_OBJECT [P] Blob contains an object * VT_CF [P] Clipboard format * VT_CLSID [P] A Class ID * VT_VECTOR [P] simple counted array * VT_ARRAY [V] SAFEARRAY* * VT_BYREF [V] void* for local use * VT_BSTR_BLOB Reserved for system use */
Sende ein Kommentar, Frage, Korrekturen, Beschimpfungen...
02.10.2010 :
Erstmal volle Anerkennung: das ist das Beste, was ich zu diesem Thema im Netz gefunden habe. Was für mich "schwierig" war (mag an mir liegen):
Für Novizen hilfreich wäre ein einfaches Beispiel (z.B. Differenzbildung) auf Excel VBA und VS C++ Seite voll durchgearbeitet zu präsentieren. Danke, kuske
doxapp c++ Zur Übersicht home