VBA in Excel/ Funktionen
Arten der Funktionen
[Bearbeiten]Bestandteil fast jeder – auch einfachsten – Programmierung sind Funktionen. Bei der Excel-/VBA-Programmierung hat man es mit 3 Gruppen von Funktionen zu tun:
- Excel-Funktionen
- VBA-Funktionen
- benutzerdefinierte Funktionen (UDF)
Einsatz von Excel-Funktionen
[Bearbeiten]Funktionen erwarten in der Regel Übergabewerte, auf deren Grundlage sie ihre Berechnungen durchführen und geben die Berechnungsergebnisse zurück. Sie können grundsätzlich sowohl innerhalb von VBA-Programmen verwendet wie auch in Tabellenblättern eingesetzt werden, wobei beim Einsatz von benutzerdefinierten Funktionen in Tabellenblättern Beschränkungen zu beachten sind.
Eine Reihe von Funktionen gibt es sowohl in Excel als auch in VBA. Bei der Wahl des Einsatzes der einen oder anderen muss beachtet werden, dass gleichlautende Excel/VBA-Funktionen zu durchaus unterschiedlichen Ergebnissen führen können. Hier sei exemplarisch auf die Trim-Funktion hingewiesen, die in VBA Leerzeichen am Anfang und Ende einer Zeichenfolge, bei Excel zusätzlich die überzähligen innerhalb eines Strings entfernt.
Grundsätzlich gilt für alle Funktionen, ob eingebaute, über VBA einzutragende oder benutzerdefinierte, dass sie keine Formatierungen transportieren können. Über Funktionen, die im Tabellenblatt aufgerufen werden, können Sie beispielsweise keine Hintergrundformate oder Schriftattribute festlegen, dazu benötigen Sie eine Sub. Jedoch können Funktionen, die über den VBA Editor ausgeführt werden, solche Änderungen vornehmen.
Verwendung innerhalb von VBA-Prozeduren
[Bearbeiten]Excel-Funktionen müssen in VBA als solche kenntlich gemacht werden, indem man ihnen entweder ein Application oder ein Worksheetfunction voranstellt. Soll die Arbeitsmappe abwärtskompatibel angelegt werden, ist Application zu verwenden, da die Vorgängerversionen (vor Excel 2000) kein Worksheetfunction kennen. Allgemein ist die Verwendung von Worksheetfunction zu empfehlen, da bei deren Einsatz zum einen die Elemente (Funktionen) automatisch aufgelistet werden und zum anderen als weitere Unterstützung die jeweilige Argumentenliste angezeigt wird.
Von diesem Prinzip sollte abgewichen werden, wenn beim Rückgabewert der Funktion Fehlerwerte zu erwarten sind. Worksheetfunction liefert statt des Fehlerwertes den beliebten, zum Programmabbruch führenden Laufzeitfehler 1004.
So funktioniert es nicht:
Function IsExistsA(strTxt As String) As Boolean
Dim var As Variant
var = WorksheetFunction.Match(strTxt, Columns(1), 0)
If Not IsError(var) Then IsExistsA = True
End Function
Die Notwendigkeit des Abfangens des Fehlers kann man sich ersparen, indem man statt Worksheetfunction jetzt Application verwendet:
Function IsExistsB(strTxt As String) As Boolean
Dim var As Variant
var = Application.Match(strTxt, Columns(1), 0)
If Not IsError(var) Then IsExistsB = True
End Function
Verwendung im Arbeitsblatt
[Bearbeiten]Sie haben die Möglichkeit, Excel-Funktionen oder deren Ergebnisse in einem Arbeitsblatt eintragen zu lassen. Sinnvollerweise werden die Funktionen (Formeln) dann eingetragen, wenn spätere Wertekorrekturen im zu berechnenden Bereich zu einer Neuberechnung in der Ergebniszelle führen sollen.
Hinweis: Wenn Range-Objekte (Range, Cells, Rows, Columns, Areas) nicht explizit auf ein Worksheet-Objekt angewendet werden (so z.B. als Worksheet("Tabelle 1").Range), bezieht Excel auf das aktive, sichtbare Arbeitsblatt (ActiveSheet), VBA liest also die Anweisung Cells(1,1) bzw. Range("A1") immer als ActiveSheet.Cells(1,1) bzw. ActiveSheet.Range("A1").
Nur mit einem Workbook-Objekt als Präfix kann auch auf inaktive, ausgeblendeten oder versteckten Tabellenblättern gearbeitet werden.
Der Eintrag eines absoluten Wertes (Summe des Wertebereiches in Spalte A):
Sub SumValue()
Dim intRow As Integer
intRow = Cells(Rows.Count, 1).End(xlUp).Row
Cells(intRow + 1, 1).Value = WorksheetFunction.Sum(Range("A1:A" & intRow))
End Sub
Der Eintrag einer Formel (Summe des Wertebereiches in Spalte A):
Sub SumFormula()
Dim intRow As Integer
intRow = Cells(Rows.Count, 1).End(xlUp).Row
Cells(intRow + 1, 1).Formula = "=Sum(A1:A" & intRow & ")"
End Sub
Für den Formeleintrag bieten sich folgende Möglichkeiten:
Formula
[Bearbeiten]Die Formel wird in englischer Schreibweise eingetragen und umfasst einen absoluten Bereich:
Sub AbsoluteFormel()
Range("B1").Formula = "=AVERAGE(A1:A20)"
End Sub
FormulaR1C1
[Bearbeiten]Die Formel wird in englischer Schreibweise eingetragen und umfasst einen relativen Bereich:
Sub RelativeFormelA()
Range("B2").Select
Range("B2").FormulaR1C1 = "=AVERAGE(R[-1]C[-1]:R[18]C[-1])"
End Sub
Sie kann auch einen teils absoluten und teils relativen Bereich umfassen:
Sub RelativeFormelB()
Range("C2").Select
Range("C2").FormulaR1C1 = "=AVERAGE(R1C[-1]:R20C[-1])"
End Sub
Lokale Formeln
[Bearbeiten]Bei FormulaLocal und FormulaR1C1Local wird die Formel wird entsprechend der Spracheinstellung des Betriebssystems eingetragen, in den folgenden Beispielen ist das die deutsche Sprache. Wird eine solche Arbeitsmappe auf einem Excel mit französischer Spracheinstellung ausgeführt, kann es zu Fehlern und unerwarteten Ergebnissen kommen.
Berücksichtigen Sie auch, dass sich die Präfixe für Zeilen/Spalten sowie den Austausch der eckigen gegen die runden Klammern der Sprachauswahl angepasst haben: R → Z, C → S, [] → ().
FormulaLocal
[Bearbeiten]Hier umfasst die Formel einen absoluten Bereich:
Sub AbsoluteFormelLocal()
Range("B1").FormulaLocal = "=MITTELWERT(A1:A20)"
End Sub
FormulaR1C1Local
[Bearbeiten]Die Formel umfasst einen relativen Bereich:
Sub RelativeFormelALocal()
Range("B2").Select
Range("B2").FormulaR1C1Local = "=MITTELWERT(Z(-1)S(-1):Z(18)S(-1))"
End Sub
Sie kann auch einen teils absoluten und teils relativen Bereich umfassen:
Sub RelativeFormelBLocal()
Range("C2").Select
Range("C2").FormulaR1C1Local = "=MITTELWERT(Z1S(-1):Z20S(-1))"
End Sub
FormulaArray
[Bearbeiten]Array-Formeln werden ohne die ihnen eigenen geschweiften Klammern eingegeben. Eine FormulaLocal-Entsprechung gibt es hier nicht.
Sub ArrayFormel()
Range("B3").FormulaArray = _
"=SUM((D16:D19=""Hosen"")*(E16:E19=""rot"")*F16:F19)"
End Sub
Dem FormulaArray-Befehl kommt einige Bedeutung zu, da Array-Berechnungen in VBA ihre Zeit benötigen und es sich in vielen Fällen empfiehlt, temporäre ArrayFormeln in Zellen eintragen zu lassen, um ihre Werte auszulesen.
Einsatz von VBA-Funktionen
[Bearbeiten]Verwendung innerhalb von VBA-Prozeduren
[Bearbeiten]Beim Einsatz von VBA-Funktionen ist bei geforderter Abwärtskompatibilität Vorsicht geboten. Während die Anzahl der Excel-Formeln seit Jahren im Wesentlichen konstant geblieben ist, trifft dies für VBA-Funktionen nicht zu. Im Interesse eines möglichst weitverbreiteten VBA-Einsatzes wird die Palette der VBA-Funktionen gelegentlich erweitert.
Der Aufruf einer VBA-Funktion ist einfachst; hier wird das aktuelle Verzeichnis geliefert:
Sub PathAct()
MsgBox CurDir
End Sub
Verlangt die Funktion Parameter, erfolgt der Aufruf mit der Parameterübergabe:
Sub TypeAct()
MsgBox TypeName(ActiveSheet)
End Sub
Verwendung im Arbeitsblatt
[Bearbeiten]Ergebnisse von VBA-Funktionen können über den Aufruf in benutzerdefinierten Funktionen auch direkt ins Tabellenblatt eingetragen werden:
Function UmgebungsVariable()
UmgebungsVariable = Environ("Path")
End Function
Einsatz von benutzerdefinierten Funktionen (UDF)
[Bearbeiten]Verwendung innerhalb von VBA-Prozeduren
[Bearbeiten]Benutzerdefinierte Funktionen werden in aller Regel dann eingesetzt, wenn mehrfach wiederkehrende Berechnungen durchgeführt werden sollen. Wenn es denn auch nicht verlangt wird, sollten sowohl die Funktionen selbst, deren Parameter sowie die in den Funktionen verwendeten Variablen sauber dimensioniert werden.
Im folgenden Beispiel wird aus einer Prozedur heraus mehrfach eine Funktion zum Gesperrtschreiben der Ortsnamen aufgerufen:
Sub PLZundOrt()
Dim intRow As Integer
intRow = 1
Do Until IsEmpty(Cells(intRow, 1))
Cells(intRow, 3) = Cells(intRow, 1) & " " & _
Gesperrt(Cells(intRow, 2))
intRow = intRow + 1
Loop
End Sub
Function Gesperrt(strOrt As String) As String
Dim intCounter As Integer
Do Until Len(strOrt) > 10
For intCounter = Len(strOrt) - 1 To 1 Step -1
If Mid(strOrt, intCounter, 1) <> " " Then
strOrt = Left(strOrt, intCounter) & " " & _
Right(strOrt, Len(strOrt) - intCounter)
End If
Next intCounter
Loop
Gesperrt = strOrt
End Function
Hier wird eine benutzerdefinierte Funktion zur Umrechnung von Uhrzeiten in Industriezeiten unter Berücksichtigung einer Pausenzeit eingesetzt:
Sub DateToNumber()
Dim intRow As Integer
intRow = 10
Do Until IsEmpty(Cells(intRow, 1))
Cells(intRow, 2) = IndustrieZeit(Cells(intRow, 1))
intRow = intRow + 1
Loop
End Sub
Function IndustrieZeit(dat As Date) As Double
Dim dblValue As Double
dblValue = dat * 24
IndustrieZeit = dblValue - 0.25
End Function
Verwendung im Arbeitsblatt
[Bearbeiten]Dimensionieren Sie die Funktions-Parameter entsprechend dem übergebenen Wert, nicht nach dem Range-Objekt, aus dem der Wert übergeben wird. Dies gilt unabhängig davon, ob die Range-Dimensionierung im aktuellen Fall ebenfalls richtige Ergebnisse zuläßt. Vorstehendes gilt selbstverständlich nicht für zu übergebende Matrizen (Arrays). Im Falle einer evtl. notwendigen Abwärtskompatibilität ist zu beachten, dass die Vorgängerversionen von Excel 8.0 (97) das Range-Objekt in der Parameter-Dimensionierung nicht akzeptieren; verwenden Sie hier das Object-Objekt.
Selbstverständlich lässt sich über Funktionen keine Cursor auf Reisen schicken, jegliches Selektieren entfällt. In Excel 5.0 und 7.0 ist es zudem auch nicht möglich, simulierte Richtungstastenbewegungen einzusetzen. Der nachfolgende Code führt dort zu einem Fehler:
Function GetLastCellValueA(intCol As Integer) As Double
Dim intRow As Integer
intRow = Cells(Rows.Count, intCol).End(xlUp).Row
GetLastCellValueA = Cells(intRow, intCol).Value
End Function
In diesen Versionen müssen die Zellen abgeprüft werden, wobei man von UsedRange als Ausgangsposition ausgehen kann:
Function GetLastCellValueB(intCol As Integer) As Double
Dim intRow As Integer, intRowL As Integer
intRowL = ActiveSheet.UsedRange.Rows.Count
For intRow = intRowL To 1 Step -1
If Not IsEmpty(Cells(intRow, intCol)) Then Exit For
Next intRow
GetLastCellValueB = Cells(intRow, intCol).Value
End Function
Der Versuch, einen gesuchten und gefundenen Zellwert an eine Funktion zu übergeben, führt bei Excel 8.0 und höher zu einem falschen Ergebnis (Leerstring) und bei den Vorgängerversionen zu einem Fehler:
Function GetFindCellValue(intCol As Integer, strTxt As String) As String
Dim rngFind As Range
Set rngFind = Columns(intCol).Find(strTxt, lookat:=xlWhole, LookIn:=xlValues)
If Not rngFind Is Nothing Then GetFindCellValue = rngFind.Value
End Function
Beachten Sie bitte, dass das in diesem Abschnitt geschriebene sich ausschließlich auf benutzerdefinierte Funktionen bezieht, die in ein Tabellenblatt eingetragen werden.
Unter Umständen muss die Adresse der aufrufenden Zelle den Ausgangspunkt für die in der benutzerdefinierten Funktion ablaufenden Berechnungen bilden. Nur beim Eingabezeitpunkt richtige Ergebnisse bringt hier die Festlegung mit ActiveCell, denn bei irgendeiner Eingabe in eine andere Zelle ist dies die aktive Zelle.
Falsche Verankerung:
Function MyValueA(intOffset As Integer) As Variant
Application.Volatile
MyValueA = ActiveCell.Offset(0, intOffset).Value
End Function
Richtige Verankerung:
Function MyValueB(intOffset As Integer) As Variant
Application.Volatile
MyValueB = Application.Caller.Offset(0, intOffset).Value
End Function
Die korrekte Zuweisung erfolgt über Application.Caller.
Benutzerdefinierte Funktionen berechnen sich auch bei eingeschaltete automatischer Berechnung nicht von selbst. Wünscht man eine Berechnung bei jeder Zelleingabe, ist den Funktionen ein Application.Volatile voranzustellen. Mit dieser Anweisung sollte vorsichtig umgegangen werden, denn sie kann Berechnungsabläufe extrem verzögern. In Arbeitsmappen, mit denen ständig abrufbare Funktionen bereitgestellt werden - bspw. in der Personl.xls - ist sie konsequent zu meiden.
Übergabe von Bereichen
[Bearbeiten]In benutzerdefinierten Funktionen können -neben Werten- auch ein oder mehrere Zellbereiche übergeben werden. So wie man z.B. der eingebauten Funktion =SUMME(D1:D33) mit D1:D33 einen Bereich übergibt, so kann auch einer Benutzerdefinierten Funktion ein Bereich übergeben werden.
Der Variable für einen Bereich (in den Beispielen wird die Variable oft Bereich genannt) kann nur als Typ Range festgelegt werden, es gibt zwar die Auflistungen Cells, Rows, Columns und Areas, aber die listen keine eigenen Objekte auf, sondern immer nur ein Range-Objekt.
Das folgende Beispiel zeigt eine Funktion, die einen Bereich als Argument entgegen nimmt und die Beträge des angegebenen Bereichs aufsummiert:
Public Function SummeBetrag(Bereich As Excel.Range) As Double
Dim Zelle As Excel.Range
For Each Zelle In Bereich.Cells
' Enthält die Zelle eine Zahl?
If IsNumeric(Zelle.Value) Then
' Nur bearbeiten, falls Zahl:
SummeBetrag = SummeBetrag + Abs(Zelle.Value)
End If
Next Zelle
End Function
Die For Each Schleife geht dabei den markierten Bereich von links nach rechts und dann von oben nach unten durch. Wäre der Bereich A1:B2 markiert worden, würde die Summe in der Reihenfolge A1 + B1 + A2 + B2 berechnet.
Manchmal möchte man einen Bereich spaltenweise durchlaufen. In diesem Beispiel bringt dies keinen Vorteil, aber man kann dazu die Spalteneigenschaft des Range-Objekts nutzen:
Public Function SummeBetrag(Bereich As Excel.Range) As Double
Dim Zelle As Excel.Range
Dim Spalte As Excel.Range
' Spalten von 1 bis zur letzten Spalte durchlaufen:
For Each Spalte In Bereich.Columns
' Oberste bis zur untersten Zelle durchlaufen:
For Each Zelle In Spalte.Cells
' Enthält die Zelle eine Zahl?
If IsNumeric(Zelle.Value) Then
' Betrag addieren:
SummeBetrag = SummeBetrag + Abs(Zelle.Value)
End If
Next Zelle
Next Spalte
End Function
Die verschachtelten For-Each Schleifen gehen dabei den markierten Bereich von oben nach unten und dann von rechts nach links durch. Wäre der Bereich A1:B2 markiert worden, würde die Summe in der Reihenfolge A1 + A2 + B1 + B2 berechnet.
Auch benutzerdefinierte Funktionen sollten fehlerhafte Bereichsauswahlen erkennen und darauf reagieren. Die drei folgenden Beispiele zeigen, wie man Bereiche überprüft:
Enthält der Bereich mehr als nur eine Zeile?
Public Function NurEineZeile(Bereich As Excel.Range) As Boolean
' Enthält der Bereich nur genau eine Zeile?
' Das Ergebnis wird als Rückgabewert der Funktion gespeichert
NurEineZeile = (Bereich.Rows.Count = 1)
If Not NurEineZeile Then
' Meldung an den Benutzer, falls mehr als eine Zeile markiert wurde
MsgBox "Nur eine Zeile erlaubt", vbExclamation
End If
End Function
Enthält der Bereich mehr als eine Spalte?
Public Function NurEineSpalte(Bereich As Excel.Range) As Boolean
NurEineSpalte = (Bereich.Columns.Count = 1)
If Not NurEineSpalte Then
MsgBox "Nur eine Spalte erlaubt"
End If
End Function
Ist der Bereich quadratisch?
Public Function NurQuadratischerBereich(Bereich As Excel.Range) As Boolean
NurQuadratischerBereich = (Bereich.Rows.Count = Bereich.Columns.Count)
If Not NurQuadratischerBereich Then
MsgBox "Quadratischer Bereich erwartet"
End If
End Function
Wenn eine benutzerdefinierte Funktion zwei Bereiche als Argumente erwartet, kann es erforderlich sein, dass sich diese Bereiche nicht überschneiden. Mit der Funktion Intersect wird die Schnittmenge aus beiden Bereichen bestimmt. Falls sich die Bereiche überschneiden, schreibt die Funktion den Fehler #BEZUG ins Arbeitsblatt, sonst die Anzahl der Zellen beider Bereiche:
Public Function GetrennteBereiche(Bereich1 As Excel.Range, _
Bereich2 As Excel.Range) As Variant
If Intersect(Bereich1, Bereich2) Is Nothing Then
GetrennteBereiche = Bereich1.Cells.Count + Bereich2.Cells.Count
Else
GetrennteBereiche = CVErr(xlErrRef)
End If
End Function
Damit die vorgesehene Fehlermeldung in der Zelle angezeigt werden kann, muss der Datentyp für den Rückgabewert der Funktion Variant sein, denn nur Der Datentyp Variant kann auch den speziellen Rückgabewert "Fehler" speichern.
Das folgende Beispiel erwartet je eine Spalte mit X- und zugehörigen Y-Werten. Die folgende Funktion prüft, ob die Anzahl der Zeilen gleich ist:
Public Function GleicheZeilenZahl(BereichX As Range, BereichY As Range) As Boolean
On Error Resume Next
GleicheZeilenZahl = (BereichX.Rows.Count = BereichY.Rows.Count)
End Function
Enthält eine der Variablen keinen Bereich, sondern den Wert Nothing, führt der fehler dazu, dass Zeile 2 nicht ausgeführt wird. Da der Default-Wert von GleicheZeilenZahl gleich False ist, ist auch im Fehlerfall der Rückgabewert korrekt.
Wenn der Anwender selbst Bereiche auswählen soll, kann der auch viel zu große Bereiche auswählen. Markiert der Anwender beispielsweise die Spalten A:C, obwohl nur der Bereich A1:C3 Daten enthält, dann bearbeitet die oben gezeigte Funktion SummeBetrag() in einer XLSX-Datei 3,2 Milliarden (3×230 Zellen, nämlich von A1:C10737741824. Um das zu verhindern, vergleicht man den ausgewählten Bereich mit dem benutzten Bereich (UsedRange) und bestimmt die Schnittmenge:
Public Sub BereichKorrigieren(Bereich As Excel.Range)
Set Bereich = Intersect(Bereich, Bereich.Parent.UsedRange)
End Sub
Ist der Bereich durch die Objektvariable MeinBereich festgelegt, so ruft man die Funktion so auf:
BereichKorrigieren MeinBereich
Nur wenn MeinBereich mehr Zeilen und/oder mehr Spalten als der benutzte Bereich enthält, reduziert ihn die Funktion BereichKorrigieren. Ist MeinBereich dagegen kleiner gewählt, behält er seine Größe. Die Funktion verändert beim Verkleinern auch die Objektvariable MeinBereich selbst, d.h. es ist danach nicht mehr möglich, die Größe des gesamten ursprünglichen Bereichs festzustellen.