Big Numbers

By , 30. Juni 2015

Hallo,
heute möchte ich mal über richtig große Zahlen berichten, denn VBA hat genau mit denen Probleme diese zu verarbeiten.
Gerade in der Finanzwelt z.B. bei der Berechnung von Prüfziffern werden diese benötigt. Es sind dann schon spezielle Fälle, aber man sollte wissen wie man damit umgehen kann.

Schauen wir uns mal die Datentypen an, die uns in VBA zur Verfügung stehen:
– der Long-Datentyp ist bereits mit etwas über 2,1 Milliarden überfordert.
– Double schaft es immerhin auf 15 Stellen, bevor in die gerundete Exponential-Schreibweise gewechselt wird.
– eine weitere Möglichkeit ist der wenig bekannte Dezimal-Datentyp. Dieser lässt sich mit der Funktion CDec() herstellen, und damit haben wir schon 29 Stellen mit denen wir genau rechnen können.

Aber darüber hinaus? Nichts, Nada, Niente.

Angenommen Sie möchten von einer 36-Stelligen Zahl den Modulo nehmen, denn so groß kann maximal der BBAN (Basic Bank Account Number) nämlich werden: 30 Stellen Kontoidentifikation + 4 Stellen konvertiertes Länderkürzel + „00“

Mit dividieren so wie in den vorherigen Beitrag zum Modulo kommt man da nicht weit.

Aber es gibt für solche Fälle spezielle Rechenvorschriften, und eine möchte ich euch vorstellen, die Neun-Stellen-Regel.
Diese Regel besagt folgendes:

1. Von einer großen Zahl werden von links weg 9 Ziffern genommen werden, davon der Modulo genommen.
2. Es wird eine neue Zahl gebildet die mit dem berechneten Modulo beginnt, 
   und mit Ziffern der großen Zahl bis auf 9 Stellen aufgefüllt wird.
3. Von dieser neuen Zahl wird wieder der Modulo genommen
4. Die Vorgänge 2. und 3. werden so lange wiederholt, bis alle Ziffern der ursprünglichen 
   großen Zahl verarbeitet wurden.
5. Der Modulo, der bei der letzten Berechnung herauskommt, ist das Endergebnis.

Machen wir mal ein Beispiel.
Unsere große Zahl sei diese: 661400580’9000014’5786179’5321700’131400
Der Übersichtlichkeit ist diese in die verwendeten Blöcke eingeteilt.
Davon wollen wir den Modulo 97 bestimmen:

661400580 Mod 97 = 66
669000014 Mod 97 = 35
355786179 Mod 97 = 73
735321700 Mod 97 =  8
8131400   Mod 97 = 84

Der Modulo 97 userer großen Zahl ist also 84.

Um nun nicht ständig von Hand zu rechnen, erstellen wir aus diesen Informationen eine Prozedur.
Da wir von unserer großen Zahl (Dividend) nur Ziffern abschneiden, und nicht mit ihr rechnen, können wir diese als String-Parameter übergeben.

Public Function Modulo(ByVal Dividend As String, ByVal Devisor As Long)
    Dim NeueZahl As Long
    Dim ZwischenModulo As Variant

    Do While Len(Dividend) > 0
        NeueZahl = CLng(ZwischenModulo & Left(Dividend, 9 - Len(CStr(ZwischenModulo))))
        Dividend = Mid(Dividend, 10 - Len(CStr(ZwischenModulo)))
        ZwischenModulo = NeueZahl Mod Devisor
    Loop
    Modulo = ZwischenModulo
End Function

Was euch sicherlich Kopfzerbrechen macht ist diese Zeile:
NeueZahl = CLng(ZwischenModulo & Left(Dividend, 9 – Len(CStr(ZwischenModulo))))

Im ersten Durchlauf der Schleife ist ZwischenModulo (Typ Variant) Leer bzw. Empty. Daher ist Len(CStr(Zwischenmodulo)) = 0. Es werden also genau 9 Stellen von der großen Zahl genommen.
Im zweiten und weiteren Durchläufen ist ZwischenModulo mit dem zuvor berechneten Modulo besetzt. Die NeueZahl bildet sich also aus ZwischenModulo & 9-Len(CStr(ZwischenModulo)) Stellen der großen Zahl, um wieder maximal 9 Stellen zu erhalten.

Die Zeile: Dividend = Mid(Dividend, 10 – Len(CStr(ZwischenModulo))) schneidet einfach nur die verwendeten Ziffern von der großen Zahl ab.
Da die Mid()-Funktion 1-Basiert ist, muss man Mid(Dividend,10) schreiben um 9 Stellen vom Dividend abzuschneiden, also alles richtig.

Vieleicht noch der Hinweis warum ZwischenModulo als Variant deklariert ist und nicht als Long: Wäre er als Long deklariert dann wäre im ersten Schleifendurchgang der Wert nicht Leer sondern 0, und die Länge von CStr(0) ist nunmal 1 und nicht 0.

OK, ich denke ich konnte euch die Problematiken von richtig großen Zahlen – den Big Numbers – ein wenig näher bringen. Und die neue Modulo-Funktion könnt ihr auch sofort in euren Projekten einsetzen und ihr läuft nicht Gefahr dass irgendwann der Wertebereich nicht ausreicht oder dass falsche Ergebnisse durch die gerundete Exponential-Darstellung von Double auftreten, denn diese können in der Finanzwelt schnell sehr teuer werden.

Bis dahin
© 2015 Andreas Vogt

Median ermitteln

By , 16. Juni 2015

Hallo,
heute möchte ich euch zeigen wie man aus Werten einer Tabelle oder Abfrage den Median bestimmt. Laut Wikipedia ist die Definition für den Median einer Auflistung der Wert, der an der mittleren Stelle steht, wenn man die Werte der Größe nach sortiert.
Beispiel: 9;14;20;30;50 Der mittlere Wert der sortierten Auflistung ist hier die 20, also ist 20 der Median. Hat man eine gerade Anzahl an Werten, so gibt es keinen mittleren Wert, also nimmt man die beiden mittleren Werte der sortierten Auflistung und bildet daraus den Mittelwert. Beispiel: 9;14;20;30 Der Median berechnet sich hier aus (14 + 20) / 2, also 17.
Bildet man diese Vorschriften auf VBA Code ab, so ist zu unterscheiden zwischer gerader Anzahl Datensätze und ungerader. Dies lässt sich feststellen indem man durch 2 teilt und den ganzzahligen Rest sich ansieht. Das macht dankenswerter Weise die interne Methode Mod (Mod für Modulo) für uns.
Beispiele:
0 Mod 2 = 0
1 Mod 2 = 1
40 Mod 2 = 0
99 Mod 2 = 1
… usw.
Also überall wo 0 heraus kommt haben wir eine gerade Anzahl von Datensätzen. Aufpassen müssen wir bei der 0, also keine Datensätze. Wenn wir dies nicht beachten laufen wir in eine Fehlermeldung hinein.
So genug der Theorie, kommen wir man zum Code.

Public Function Median(ByVal TableName As String, ByVal FieldName As String) As Double
    Dim numDS As Long
    Dim lowerValue As Double
    Dim upperValue As Double
    Dim rst As DAO.Recordset

    Set rst = CurrentDb.OpenRecordset("Select " & FieldName & " From " & TableName & " Order By " & FieldName, dbOpenSnapshot)
    If Not rst.EOF Then
        With rst
            .MoveLast
            numDS = .RecordCount
            .MoveFirst
            If numDS Mod 2 = 0 Then
                .Move Int(numDS / 2) - 1
                lowerValue = .Fields(FieldName)
                .MoveNext
                upperValue = .Fields(FieldName)
                Median = (lowerValue + upperValue) / 2
            Else
                .Move Int(numDS / 2)
                Median = .Fields(FieldName)
            End If
        End With
        
        rst.Close: Set rst = Nothing
    Else
        rst.Close: Set rst = Nothing
        Err.Raise vbObjectError + 100, "Function Median", "Empty Recordset"
    End If
End Function

Der Funktion werden 2 Parameter übergeben, einen für den Tabellennamen bzw. den Namen der Abfrage, und einen für das Feldnamen von dem der Median berechnet werden soll.
Die Daten stellen wir in einem Recordset-Objekt zur Verfügung, worin diese auch gleich aufsteigend sortiert werden. Um die korrekte Anzahl von Datensätzen zu erhalten, muss der Datensatzzeiger ans Ende gesetzt werden.
Danach kommt die Entscheidung ob ganzzahlig oder nicht. Mal angenommen wir haben 27 Messwerte in der Tabelle. dann benötigen wir den 14. Wert. Also müssen wir den Datensatzzeiger um 13 Stellen von der ersten Position verschieben. Daher Int(numDS / 2) – 1. Bei 26 Messwerten benötigen wir den 13. und 14. Wert für die Mittelwertbildung. Das sind die Variablen lowerValue und upperValue. Also Datensatzzeiger um 26/2 -1 = 12 Datensätze verschieben. Beim upperValue brauchen wir einfach um 1 weiter gehen, daher Methode .MoveNext. Zu Schluss noch die üblichen Aufräumarbeiten die ja bei Recordsets bekannt sein sollten.

So jetzt noch der Fall dass das Recordset keine Datensätze liefern würde. Dann wird die äußere If-Verzweigung den Code übersprungen und zum Else-Teil wird ausgeführt. Dort schließen wir erstmal ordentlich das Recordset-Objekt und setzen es Nothing um das Objekt zu zerstören. Danach geben wir eine Fehlermeldung an die aufrufende Prozedur weiter die diese entsprechend verarbeiten kann.

Das wars auch schon, aufrufen kann man die Funktion aus einem Formular, Bericht, Modul oder auch Abfrage heraus.

Bis dahin
© 2015 Andreas Vogt

Umgang mit Optionswerten

By , 15. Mai 2015

Hallo,
heute befasse ich mich mit dem Thema „Umgang mit Optionswerten“. Es gibt kaum eine Anwendung die ohne Optionen auskommt. Sei es ein bestimmtes Verzeichnis oder bestimmte Werte oder Zustände.
Per Zufall bin ich heute auf eine Sache gestoßen in Verbindung mit Arrays: Ich wollte die in der gespeicherten Tabelle in ein Array laden um auf sie zugreifen zu können. Aber ein Element eines Arrays lässt sich nur über den Index abrufen, nicht über einen aussagekräftigen Namen.
Also hab ich mal herumgetüftelt, und schließlich doch eine sehr brauchbare Lösung gefunden.
Meine Tabelle hat 6 Optionsfelder. Ich erstelle also zuerst einen Öffentlichen Enum mit den Feldnamen:

Public Enum ltOption
    sportjahr = 0
    old_sportjahr = 1
    vereinsname = 2
    vereinsNummer = 3
    adminpassword = 4
    usestartgeld = 5
End Enum

Als nächstes benötigen wird 2 Variablen:

Public myOptions(5) As Variant
Private FieldNameArr As Variant

Zum Setzen der Optionswerte und zum Auslesen benötigen wird Get/Let Properties:

Public Property Get Settings(cOption As ltOption) As Variant
    Settings = myOptions(cOption)
End Property

Public Property Let Settings(cOption As ltOption, vValue As Variant)
    myOptions(cOption) = vValue
    CurrentDb.Execute "Update tabOptionen set " & FieldNameArr(cOption) & " = " & OptionDataType(vValue, cOption)
End Property

In der Get-Property wird als Optionswert die Nummer aus dem Enum übergeben. Daher kann man nun den entsprechenden Optionswert aus dem Array myOptions entnehmen.
In der Let-Property kommt jetzt eine Besonderheit zum Zuge, die ich vorher gar nicht kannte.
Definiert man eine Let-Property mit 2 Parametern, so kann man die Property wie folgt aufrufen:

PropertyName(ErsterParameter) = ZweiterParameter

Außerdem wird die Parameteränderung auch gleich in die Datenbank geschrieben damit sie permanent ist. Der Feldname erhalten wir aus dem Array FieldNameArr() mit dem Enum-Wert als Index.
Da wir hier an dieser Stelle nicht wissen, welcher Datentyp das Feld hat, muss man dies anhand des Enum-Wertes cOption überprüfen. Dies geschieht in einer kleinen Prozedur:

Private Function OptionDataType(vValue As Variant, cOption As Long) As Variant
    OptionDataType = vValue
    If cOption = 2 Or cOption = 4 Then
        OptionDataType = "'" & OptionDataType & "'"
    End If
End Function

D.H. Die Optionswerte 2 und 4, also die Felder vereinsname und adminpassword werden als Strings behandelt und in einfache Hochkommatas eingeschlossen.

Was noch fehlt ist die Initialisierung wo erstmals die Optionen ausgelesen und in das Array geschrieben werden. Dazu habe ich eine Public Sub Main() erstellt, die von einem Autoexec-Makro aufgerufen wird:

Public Sub Main()
    Dim i As Long
    FieldNameArr = Split("sportjahr,old_sportjahr,vereinsname,vereinsnummer,adminpassword,useStartgeld", ",")
    For i = 0 To 5
        myOptions(i) = DLookup(CStr(FieldNameArr (i)), "tabOptionen")
    Next i
    DoCmd.OpenForm "frmStart"
End Function

Im Array FieldNameArr stehen jetzt die tatsächlichen Feldnamen der Tabelle tabOptionen drin, wogegen man es oben beim Enum nicht so genau nehmen muss.
Anstelle der DLookup-Funktion kann man auch dessen Ersatzfunktionen verwenden die schneller arbeiten.

So unser Modul ist somit fertig und kann eingesetzt werden. Ich habe dazu mal ein Beispiel erstellt, das sich leicht nachbauen lässt. Man benötigt dazu:
1 Textfeld Text0, 3 CommandButtons cmdSet, cmdReset und cmdView.
Der Code sieht dann wie folgt aus:

Private Sub cmdReset_Click()
    Settings(sportjahr) = Settings(old_sportjahr)
End Sub

Private Sub cmdView_Click()
    Me.Text0 = Settings(sportjahr)
End Sub

Private Sub cmdSet_Click()
    Settings(old_sportjahr) = Settings(sportjahr)
    Settings(sportjahr) = 2017
End Sub

Private Sub Form_Load()
    Me.Text0 = Settings(sportjahr)
End Sub

Beim Formular laden wird das Sportjahr angezeigt. Bei cmdSet wird zuerst das aktuelle Sportjahr gesichert indem man dessen wert der Option old_sportjahr zuweist. danach wird das sportjahr mit 2017 überschrieben. Beim Reset wird nun der Wert aus old_sportjahr genommen und mit diesem der die Option sportjahr überschrieben.

Jetzt erahnt man auch warum das Enum im Modul als Public deklariert wurde, den wenn man jetzt schreibt „Settings(“ erscheinen die Werte aus dem Enum als Intellisense.
Um das speichern der Werte in der Tabelle kümmert sich auch das Modul, egal um was für ein Datentyp es sich handelt.
Probiert es aus, setzt Breakpoints um den Codeablauf zu verstehen, es ist ziemlich cool.

Bis dahin
©2015 Andreas Vogt

Audit Trail – oder wie protokoliert man Datenänderungen

By , 11. Mai 2015

Hallo,
das Thema heute handelt von der Protokollierung von Datensatzänderungen auf Formularbasis, der Fachbegriff dazu lautet Audit Trail. Datensatzänderungen in einer Abfrage oder direkt in einer Tabelle können in Access Jet nicht überwacht werden. Deshalb ist dem Anwender der Zugriff auf beide zu verwehren mit verschiedenen Techniken.

1. Definition und Motivation top

Laut Gabler Wirtschaftslexikon dient ein Audit Trail einerseits der „Prüfung bzw. Überwachung der Handelnden und ihrer Aktivitäten, anderseits kann dadurch im Schadensfall bzw. zwecks Aufdeckung doloser Handlungen die Handlungsfolge rückverfolgt und auch eine System- bzw. Datenwiederherstellung vereinfacht werden. So sind gezielte Prüf- und Korrekturmöglichkeiten von fehlerhaften bzw. inkriminierten Benutzereingaben möglich“

Für uns in der EDV heruntergebrochen dient ein Audit Trail zur Prüfung bzw. Überwachung von Dateneingaben, -Änderungen bzw. -Löschungen um z.b. im Fall eines Schadens die Datenintegrität wieder herzustellen oder auch bei juristischen Auseinandersetzungen Beweise für Fehleingaben (oder eben nicht) zu besitzen.

Die Motivation zur Durchführung des Audit Trails können sehr unterschiedlich sein. Z.B. zur Überwachung einzelner Arbeitsplätze wo gehäuft Fehleingaben im EDV-System vorgefallen sind, oder auch um Datenanalyse durchführen zu können, und nicht zuletzt auch zur Einhaltung der Datenintegrität.
Aber bei aller Liebe zur Technik, ein Audit Trail darf kein Selbstzweck sein, denn es bindet Ressourcen im Unternehmen und kostet somit reales Geld – eventuelle rechtliche Fragen jetzt mal ausgeschlossen.

In diesem Beitrag möchte ich aber nur auf die Technik dazu eingehen, denn das interessiert wohl die meisten Leser.
Fangen wir an beim Formular. Die zu überwachenden Steuerelemente sind im Wesentlichen TextBox, ComboBox, ListBox, CheckBox, Optionsfeld und Umschaltfläche. Um diese zu Überwachen müssen die Steuerelemente an eine Datenquelle, z.B. Abfrage oder Tabelle, gebunden sein.

2. Verwendete Formular-Ereignisse top

BeforeUpdate: Dieses Ereignis tritt ein, wenn nach einer Datenänderung der Datensatz gewechselt wird, oder wenn z.B. durch klicken der Focus in ein Unterformular gesetzt wird. Zur Datensatzänderung zählt hier neben der Änderung auch die Neuanlage. Beide unterscheidet man wie folgt:

Private Sub form_BeforeUpdate(Cancel As Integer)
If Me.NewRecord Then
MsgBox "NeuerDatensatz"
Else
MsgBox "Datensatzänderung"
End If
End Sub

Delete: Dieses Ereignis tritt ein, wenn ein Datensatz gelöscht wurde. Dieses Ereignis verwenden wir um die Löschung zu protokollieren:

Private Sub form_Delete(Cancel As Integer)
'Audit Trail Delete
End Sub

AfterDelConfirm: Dieses Ereignis tritt ein, wenn – bei aktivierter Option „Datensatzänderungen“ – ein Datensatz gelöscht wird und bei dem folgenden Bestätigungsdialog entweder der Löschung zugestimmt wird oder diese abgebrochen wird. Diese Option befindet sich unter Access-Optionen/Clienteinstellungen/Bestätigen. Das Event liefert einen Status, mithilfe man unterscheidet ob der Löschvorgang abgebrochen wurde:

Private Sub form_AfterDelConfirm(Status As Integer)
If Status <> acDeleteOK Then MsgBox "Datensatz wurde gelöscht"
End Sub

Die Aktivierungsreihenfolge der Ereignisse ist: Delete, AfterDelConfirm.
Doch wozu benötigen wir AfterDelConfirm wenn im Ereignis Delete bereits das Löschen protokolliert wurde? Nun, es könnte sein, dass der Nutzer im Bestätigungsdialog das Löschen rückgängig macht, dann müssen wir dafür sorgen dass die Log-Datei bereinigt wird, denn es steht ja das Löschen bereits drin.

Das waren die Ereignisse (Events), welche wir für das Audit Trail benötigen. Was im wesentlichen noch fehlt ist eine Funktion die die Log schreibt, und eine Funktion die ggf. die Log bereinigt.
Jetzt gehen wir aber nicht hin und „müllen“ unsere Formularmodule mit redundantem Code zu, sondern machen das ganz elegant per Klassenmodul, wo wir auch die Events der Formulare abbilden werden.
Der interessierte Leser weiss ja, dass ich auf OOP abfahre und alles möglichst Objektorientiert programmiere.

3. Implementierung im Formular top

Zuerst benötigen wir eine Referenzierung auf die Klasse um davon ein Objekt ableiten zu können:

Dim myAudit As clsAuditTrail
Private Sub Form_Load()
Set myAudit = New clsAuditTrail
Set myAudit.FormObj = Me
End Sub
Private Sub Form_Unload(Cancel As Integer)
Set myAudit = Nothing
End Sub

Es wird ein Objekt erstellt „myAudit“, und die Eigenschaft „FormObj“ mit dem Formular-Objekt des aktuellen Formulares belegt.
Verwenden Sie Unterformulare? Dann können Sie auch dort das Audit Trail einsetzen, indem sie das Formular öffnen und dort obigen Code hinein kopieren. Nicht vergessen die Marke für die zu überwachenden Datenfelder zu setzen. Wenn Sie allerdings innerhalb des Unterformular-Steuerelementes eine Abfrage oder Tabelle einsetzen, dann können Sie keine Änderungen protokollieren da weder Abfragen noch Tabellen Eventhandler besitzen.

4. Funktionsweise der Klasse top

Werfen wir mal einen kurzen Blick in die Klasse.
Um nun innerhalb der Klasse auf die oben genannten Formular-Ereignisse reagieren zu können, muss zuerst das Form-Object übernommen und in einer Variablen gespeichert werden, und danach die Eventhandler definiert werden:

Public Property Set FormObj(ByRef FRM_ As Access.Form)
    Set m_frm = FRM_
    m_IdFieldName = getIDField(FRM_)
    m_frm.BeforeUpdate = "[Event Procedure]"
    m_frm.OnDelete = "[Event Procedure]"
    m_frm.AfterDelConfirm = "[Event Procedure]"
End Property

In der Variable m_IdFieldName wird der Name des Formulars gespeichert, da dieser an späterer Stelle benötigt wird. Die Eventhandler werden auf einfachste Weise definiert, indem man dem möglichen Ereignis den Textstring „[Event Procedure]“ zuweist. Ohne diese Zuweisung werden keine Events gefeuert.
Die Programmierung der Formular-Ereignisse in der Klasse geschieht auf identische Weise wie im Formular, nur haben wir hier nicht das Form-Object sondern seinen Stellvertreter m_frm.
Ein Beispiel:

Private Sub m_frm_AfterDelConfirm(Status As Integer)
    If Status <> acDeleteOK Then Call DataRedoDelete
End Sub

Verwendet man innerhalb der Formulare ebenfalls die gleichen Eventhandler, so ist zu bedenken dass immer zuerst der Eventhandler des Formulars feuert, und dann erst der in der Klasse. Das kann funktionierten, kann aber auch zu problemen führen.

Kommen wir nun zum eigentlichen Protokollieren. Dieser Algorithmus ist eigentlich ziemlich einfach.
Man prüft alle Steuerelemente des Formulars in einer Schleife ob die Eigenschaft Value <> der Eigenschaft OldValue ist. Trift dies zu hat man eine Datensatzänderung. Da nur Steuerelemente die Eigenschaft Value haben, die auch einen Steuerelementinhalt besitzen, ist es ratsam einfach alle zu überwachenden Steuerelemente mit einer Marke auszustatten. Ich habe im Beispiel als Marke den Text Audit verwendet.

Da man nicht nur die Änderung sondern auch Wer Wann und Wo geändert hat, benötigen wir dazu noch ein paar Werte. Die Vaiable lngInsertedID wird benötigt, um beim Löschen die ID des eingefügten Datensatzes in der Log zu behalten. Wird das Löschen nämlich abgebrochen werden wir anhand dieser Variable den zu entfernenden Datensatz aus der Log identifizieren.

Private Sub DataChanges()
    Dim CTL As Control
    Dim lngInsertedID As Long

    dtmCurrentDateTime = Now()
    strUserName = Environ("USERNAME")
    Select Case UserAction
    Case "EDIT"
        For Each CTL In m_frm.Controls
            If CTL.Tag = "Audit" Then
                If Nz(CTL.Value) <> Nz(CTL.OldValue) Then
                    WriteEditLog CTL
                End If
            End If
        Next CTL
    Case Else
        lngInsertedID = WriteNewDeleteLog
        If UserAction = "DELETE" Then lastIdentifier = lngInsertedID
    End Select
End Sub

Die Prozedur WriteEditLog übernimmt das Steuerelement-Objekt CTL, welches sich geändert hat. Aus diesem Objekt erhalten wir die Werte für Value, OldValue und ControlSource, also der Name des Tabellenfeldes das an dieses Control gebunden ist:

Private Sub WriteEditLog(ByRef CTL_ As Access.Control)
    Dim strSQL As String
    strSQL = "Insert into tblAuditTrailLog (AuditTime, UserName, FormName, FieldName, ActionType, RecordID, OldValue, NewValue) Values (" & _
             "'" & dtmCurrentDateTime & "', " & _
             "'" & strUserName & "', " & _
             "'" & m_frm.Name & "', " & _
             "'" & CTL_.ControlSource & "', " & _
             "'" & UserAction & "', " & _
             "'" & m_frm.Controls(m_IdFieldName).Value & "', " & _
             "'" & CTL_.OldValue & "', " & _
             "'" & CTL_.Value & "' " & _
             ")"
    CurrentProject.Connection.Execute strSQL
End Sub

Bei der Löschung / Neuanlage ist es ähnlich, nur benötigen wir dort die Felder AuditTime, UserName, FormName, ActionType, RecordID. Was zum Schluss noch fehlt sind ein paar Properties, Hilfsfunktionen und Variablendeklarationen.

5. Setup und Initialisierung top

Was noch fehlt ist ein Setup, welches beim erstmaligen Verwenden der Klasse die Log-Tabelle für das Audit Trail anlegt.
Um diese Tabellenerstellungs-Abfrage nur 1-mal auszuführen, muss geprüft werden ob die Tabelle bereits existiert. Dazu greifen wir per Dlookup() auf die System-Tabelle MSysObjects zu, in der alle angelegten Access-Objekte gespeichert werden. Ausgeführt wird das beim Klassen-Ereignis „Initialize“:

Private Sub Class_Initialize()
    Dim strSQL As String
    strSQL = "Create Table tblAuditTrailLog (" & _
             "ID COUNTER CONSTRAINT ID PRIMARY KEY, " & _
             "AuditTime date, " & _
             "UserName Text, " & _
             "FormName Text, " & _
             "FieldName Text, " & _
             "ActionType Text, " & _
             "RecordID Text, " & _
             "OldValue Text, " & _
             "NewValue Text" & _
             ")"
    If Not AuditTabExists Then
        CurrentProject.Connection.Execute strSQL
    End If
End Sub

In der Variable strSQL wird die Tabellenerstellungs-Abfrage zusammengestellt. Man beachte dass ein AutoWert-Feld als COUNTER CONSTRAINT definiert wird. Die Funktion AuditTabExists prüft in MSysObjects ob bereits ein Eintrag mit Namen „tblAuditTrailLog“ vorhanden ist, falls nicht wird die Tabelle erstellt.

6. Die Klasse clsAuditTrail top

Die fertige Klasse clsAuditTrail sieht nun so aus:

Option Explicit
Option Compare Text

' ================================================
' Code by Andreas Vogt Email: info@accessblog.de
' ================================================

Private WithEvents m_frm As Form

Private m_Identifier As Long
Private m_IdFieldName As String
Private m_UserAction As String
Private m_AuditTabExists As Boolean

Private dtmCurrentDateTime As Date
Private strUserName As String

Public Property Set FormObj(ByRef FRM_ As Access.Form)
    Set m_frm = FRM_
    m_IdFieldName = getIDField(FRM_)
    m_frm.BeforeUpdate = "[Event Procedure]"
    m_frm.AfterUpdate = "[Event Procedure]"
    m_frm.OnDelete = "[Event Procedure]"
    m_frm.AfterDelConfirm = "[Event Procedure]"
End Property

Private Property Get AuditTabExists() As Boolean
    If Not m_AuditTabExists Then
        m_AuditTabExists = IsNull(DLookup("[Name]", "MSysObjects", "[Name] = 'tblAuditTrailLog' AND (Type = 1 Or Type = 6)")) = False
    End If
    AuditTabExists = m_AuditTabExists
End Property

Private Property Get UserAction() As String
    UserAction = m_UserAction
End Property

Private Property Let UserAction(ByVal UserAction_ As String)
    m_UserAction = UserAction_
End Property

Private Property Get lastIdentifier() As Long
    lastIdentifier = m_Identifier
End Property

Private Property Let lastIdentifier(ByVal lastident As Long)
    m_Identifier = lastident
End Property

Private Sub Class_Initialize()
    Dim strSQL As String
    strSQL = "Create Table tblAuditTrailLog (" & _
             "ID COUNTER CONSTRAINT ID PRIMARY KEY, " & _
             "AuditTime date, " & _
             "UserName Text, " & _
             "FormName Text, " & _
             "FieldName Text, " & _
             "ActionType Text, " & _
             "RecordID Text, " & _
             "OldValue Text, " & _
             "NewValue Text" & _
             ")"
    If Not AuditTabExists Then
        CurrentProject.Connection.Execute strSQL
    End If
End Sub

Private Sub Class_Terminate()
    Set m_frm = Nothing
End Sub

Private Sub m_frm_Delete(Cancel As Integer)
    UserAction = "DELETE"
    DataChanges
End Sub

Private Sub m_frm_AfterDelConfirm(Status As Integer)
    If Status <> acDeleteOK Then Call DataRedoDelete
End Sub

Private Sub m_frm_BeforeUpdate(Cancel As Integer)
    If m_frm.NewRecord Then
        UserAction = "NEW"
        DataChanges
    Else
        UserAction = "EDIT"
        DataChanges
    End If
End Sub

Private Sub DataChanges()
    Dim CTL As Control
    Dim lngInsertedID As Long

    dtmCurrentDateTime = Now()
    strUserName = Environ("USERNAME")
    Select Case UserAction
    Case "EDIT"
        For Each CTL In m_frm.Controls
            If CTL.Tag = "Audit" Then
                If Nz(CTL.Value) <> Nz(CTL.OldValue) Then
                    WriteEditLog CTL
                End If
            End If
        Next CTL
    Case Else
        lngInsertedID = WriteNewDeleteLog
        If UserAction = "DELETE" Then lastIdentifier = lngInsertedID
    End Select
End Sub

Private Sub WriteEditLog(ByRef CTL_ As Access.Control)
    Dim strSQL As String
    strSQL = "Insert into tblAuditTrailLog (AuditTime, UserName, FormName, FieldName, ActionType, RecordID, OldValue, NewValue) Values (" & _
             "'" & dtmCurrentDateTime & "', " & _
             "'" & strUserName & "', " & _
             "'" & m_frm.Name & "', " & _
             "'" & CTL_.ControlSource & "', " & _
             "'" & UserAction & "', " & _
             "'" & m_frm.Controls(m_IdFieldName).Value & "', " & _
             "'" & CTL_.OldValue & "', " & _
             "'" & CTL_.Value & "' " & _
             ")"
    CurrentProject.Connection.Execute strSQL
    
End Sub

Private Function WriteNewDeleteLog() As Long
    Dim strSQL As String
    Dim RecordsAffected As Long
    Dim cnn As New ADODB.Connection
    Dim rstTemp As ADODB.Recordset

    strSQL = "Insert into tblAuditTrailLog (AuditTime, UserName, FormName, ActionType, RecordID) Values (" & _
             "'" & dtmCurrentDateTime & "', " & _
             "'" & strUserName & "', " & _
             "'" & m_frm.Name & "', " & _
             "'" & UserAction & "', " & _
             "'" & m_frm.Recordset.Fields(m_IdFieldName).Value & "')"
    Set cnn = CurrentProject.Connection
    cnn.Execute strSQL, RecordsAffected
    If RecordsAffected > 0 Then
        Set rstTemp = cnn.Execute("SELECT @@IDENTITY")
        WriteNewDeleteLog = rstTemp(0)
        rstTemp.Close
    End If

    Set rstTemp = Nothing
    Set cnn = Nothing
End Function

Private Sub DataRedoDelete()
    Dim rstTemp As ADODB.Recordset

    Set rstTemp = New ADODB.Recordset
    With rstTemp
        .Open "SELECT * FROM tblAuditTrailLog", CurrentProject.Connection, adOpenDynamic, adLockOptimistic
        .Find "ID = " & lastIdentifier
        If Not .EOF Then
            .Delete
        End If
    End With

    rstTemp.Close
    Set rstTemp = Nothing
End Sub

Private Function getIDField(FRM_ As Access.Form) As String
    Dim i As Long
    With FRM_.Recordset
        For i = 0 To .Fields.Count - 1
            If .Fields(i).Type = 4 Then
                getIDField = .Fields(i).Name
                Exit For
            End If
        Next i
    End With
End Function

Bis dahin
© 2015 Andreas Vogt

Zippen und Unzippen aus Access heraus

By , 6. März 2014

Erstellt man z.B. aus Access heraus PDF-Dateien – so wie im vorigen Artikel gezeigt – dann möchte man diese unter Umständen auch per Email verschicken, z.B. einen regelmäßigen Umsatzbericht an die Geschäftsleitung. Da würde es Sinn machen, vorher die PDF-Dateien in einem Zip-Archiv zusammenzufügen.
Ein anderes Szenario wäre, Sie erhalten regelmäßig Textdateien in gezippter Form, und möchten diese in eine Access-Tabelle importieren.
Die Möglichkeiten die man dazu hat sind überschaubar. Man könnte z.B. Winzip oder Winrar automatisieren. Was aber wenn es nicht gestattet ist 3th Party Software auf dem Rechner zu installieren, und weder Winzip noch Winrar sind verfügbar.

Für so einen Fall gibt es verschiedene DLL-Bibliotheken von externen Anbietern, die die Zip/Unzip Funktionaliät bieten. Und so eine möchte ich heute vorstellen, nämlich „SawZipNG“. Leider ist dieses geniale Projekt eingestellt worden, und der Entwickler auch nicht mehr erreichbar. Zum Glück hat sich noch eine Kopie der DLL und der Dokumentation auftreiben lassen, die ich im Downloadbereich zur Verfügung stellen werde.

Laden Sie sich die DLL aus dem Downloadbereich herunter, und führen Sie die Anwendung aus. Die DLL-Datei wird automatisch registriert.

Danach steht Ihnen die die Zip/Unzip Funktionalität zur Verfügung. Öffnen Sie Ihre Access Anwendung und den VBA-Editor, und wählen Sie unter Extra/Verweise den Eintrag SAWZIPNG aus. Im Folgenden Beispiel zeige ich Ihnen wie Sie z.B. eine zuvor erstellte PDF-Datei zippen bzw. ein Archiv unzippen können. Sehen Sie sich dazu auch die Dokumentation an (Downloadbereich).
Beispiel 1: Eine einzelne Datei Zippen

Public Sub CreateZIPSingle(ByVal cSourceFileName AS String, _
                           ByVal cZipFileName AS String, _
                           Optional ByVal bFullpath As Boolean = False, _
                           Optional ByVal cComment As String = "", _
                           Optional ByVal cPassword AS String = "")

    Dim objZipArchive As SAWZipNG.Archive
    Set objZipArchive = New SAWZipNG.Archive

    With objZipArchive 
        .Create cZipFileName
        .Password = cPassword
        .AddFile cSourceFileName, bFullpath
        .Comment = cComment 
        .Close
    End With
    
    Set objZipArchive = Nothing
End Sub

Wichtig ist dass der Pfad zur neu erstellen Zip-Datei existiert, ggf. mit Dir() vorher abprüfen.
Weiterhin Interessant sind die Optionalen Parameter:
Setzt man bFullPath auf True dann wird im Zip-File der gesamte Verzeichnispfad abgebildet bis zur gezippten Datei.
Mit cComment kann man einen Kommentar in den Dateieigenschaften hinterlegen.
Ganz interessant ist der letzte Parameter cPassword. Damit kann man eine Zip-Datei mit einem Passwort schützen. Es ist aber zwingend dass im Code zuerst das Passwort gesetzt wird und dann Dateien mit AddFile in das Archiv eingefügt werden.

Möchte man mehrere Dateien in das Zip-Archiv einfügen, so schreibt man einfach mehrere AddFile() Anweisungen untereinander. Dann könnte man statt des cSourceFileName ein Array übergeben und in einer Schleife die Dokumente in das Archiv einfügen. Hier ein Beispiel dazu:

Beispiel 2: Eine mehrere Dateien in einem Array Zippen

Public Sub CreateZIPMulti(ByVal vSourceArrayName AS Variant, _
                           ByVal cZipFileName AS String, _
                           Optional ByVal bFullpath As Boolean = False, _
                           Optional ByVal cComment As String = "", _
                           Optional ByVal cPassword AS String = "")

    Dim i AS long
    Dim objZipArchive As SAWZipNG.Archive
    Set objZipArchive = New SAWZipNG.Archive

    With objZipArchive 
        .Create cZipFileName
        .Password = cPassword
        For i = lbound(vSourceArrayName) to ubound(vSourceArrayName)
            .AddFile vSourceArrayName(i), bFullpath
        Next i
        .Comment = cComment 
        .Close
    End With
    
    Set objZipArchive = Nothing
End Sub

Der Aufruf könnte z.B. so aussehen:

Sub MultiZipTest()
    Dim FileArray As Variant
    FileArray = Array("c:\test\Dok1.pdf", "c:\test\Dok2.pdf", "c:\test\Dok3.pdf")
    CreateZIPMulti FileArray, "c:\Test\Dok1bis3.zip", false
End sub

Beispiel 3: Ein ganzes Verzeichnis Zippen

Public Sub CreateZIPFolder(ByVal cSourceFolderName AS String, _
                           ByVal cZipFileName AS String, _
                           Optional ByVal bSubDirs As Boolean = False,
                           Optional ByVal bFullpath As Boolean = False, _
                           Optional ByVal cComment As String = "", _
                           Optional ByVal cPassword AS String = "")

    Dim objZipArchive As SAWZipNG.Archive
    Set objZipArchive = New SAWZipNG.Archive

    With objZipArchive 
        .Create cZipFileName
        .Password = cPassword
        .AddFolder cSourceFolderName, bSubDirs, bFullpath
        .Comment = cComment 
        .Close
    End With
    
    Set objZipArchive = Nothing
End Sub

Hier ist jetzt neu hinzugekommen der Parameter bSubDirs, wird dieser auf True gesetzt werden alle Unterverzeichnisse mit Dateien mit in das Archiv genommen.

Beispiel 4: zu Zippende Dateien im Archiv umbenennen
Manchmal kann es sinnvoll sein, dass man Dateien, die man Zippen will, im Zip-Archiv anderst benennen möchte. Dazu gibts die Methode .AddFileAs():

Public Sub CreateZIPSingleAs(ByVal cSourceFileName AS String, _
                             ByVal cTargetFileName AS String, _
                             ByVal cZipFileName AS String, _
                             Optional ByVal cComment As String = "", _
                             Optional ByVal cPassword AS String = "")

    Dim objZipArchive As SAWZipNG.Archive
    Set objZipArchive = New SAWZipNG.Archive

    With objZipArchive 
        .Create cZipFileName
        .Password = cPassword
        .AddFileAs cSourceFileName, cTargetFileName
        .Comment = cComment 
        .Close
    End With
    
    Set objZipArchive = Nothing
End Sub

Bei dieser Methode gibt es den Parameter bFullpath nicht, siehe Dokumentation.
Im folgenden Artikel werde ich auf dieses Thema nochmal eingehen, und Beispiele zu Unzip bringen.

Bis dahin
© 2014 Andreas Vogt

Bericht mit Filterkriterien nach PDF

By , 5. März 2014

Bis einschließlich der Version Access 2003 gab es keine eingebaute Funktion um einen Bericht nach PDF zu speichern. Zum Glück für diejenigen, die noch mit Access 2003 oder älter arbeiten, hat Stephen Lebans dafür eine DLL erstellt, mit der ab Access 2000 bis 2003 Berichte nach PDF gespeicherte werden können.
Hier gehts zum Link: http://www.lebans.com/reporttopdf.htm

Unter Access 2007 und neuer gibt es jetzt eine Möglichkeit, direkt einen Bericht nach PDF zu speichern: Docmd.OutputTo… mit dem AusgabeFilter acFormatPDF.
Will man aber dem Bericht Filterkriterien mitgeben, um z.B. nach einem bestimmten Kunden zu filtern, dann muss man schon ein bisschen mehr coden. Ich hab dazu mal ein Beispiel gemacht:

Private Sub ReportToPDF(ByVal cReportName As String, _
                        ByVal cReportFilter As String, _
                        ByVal cPDFName As String, _
                        Optional ByVal cHeadline As String = "Umsatzbericht")
    
    DoCmd.OpenReport cReportName, acPreview, , cReportFilter, acHidden
    Reports(cReportName).Titelzeile.Caption = cHeadline
    DoCmd.OutputTo acOutputReport, cReportName, acFormatPDF, cPDFName, False
    DoCmd.Close acReport, cReportName, acSaveNo
End Sub

Zuerst wird der Bericht in der Entwurfsansicht unsichtbar geöffnet, mit den gewünschten Filterkriterien. Dann kann man Änderungen am Bericht vornehmen, z.B. das Überschriftsfeld „Titelzeile“ entsprechend dem Filterausdruck anpassen. Oder wenn man nach einer Kundennummer filtern würde, könnte man jetzt die Adresse des Kunden eintragen etc.
Dann wird das eigentlich PDF erstellt, mit dem Befehl „DoCmd.OutputTo“. Die relevanten Parameter sind ObjektTyp, ObjektName (Berichtsname), OutputFormat, OutputName, AutoStart.
Danach wird die Entwurfsansicht des Berichtes ohne zu speichern wieder geschlossen.

Die Parameter sind eigentlich selbsterklärend, wir wählen als ObjektTyp den Bericht, also acOutputReport, als ObjektName den Berichtsname, als OutputFormat acFormatPDF etc.
Wer sich die Methode OutputTo genauer ansehen möchte kann das hier:
http://msdn.microsoft.com/en-us/library/office/ff192065.aspx

Fehlt nur noch die aufrufende Funktion, die könnte z.B. wie folgt aussehen:

Sub ReportToPDF_Test()
    Dim ZielPfad As String
    ZielPfad = CurrentProject.Path & "\PDFAusgabe\Umsaetze.pdf"
    ReportToPDF "Umsaetze", "Monat='Januar'", ZielPfad, "Umsätze im Januar"
    If Dir(ZielPfad) <> "" Then
        MsgBox "Bericht wurde als PDF gespeichert"
    End If
End Sub

Auch hier sollte alles verständlich sein: Aufruf der Prozedur mit den entsprechenden Parametern, anschließendes Überprüfen ob PDF-Datei erstellt wurde. Zu Beachten ist, dass wenn man PDF-Dateien mit einem vorhandenen Namen speichert, wird die alte Datei ohne Nachfrage überschrieben. Deshalb sollte man in der Prozedur ReportToPDF vor dem Speichern mittels Dir() prüfen ob die Datei bereits vorhanden ist. Aber das, wie auch die Fehlerbehandlung, überlasse ich euch. Achtet beim Filter noch darauf, dass Textfelder in einfachen Hochkommatas zu stehen haben, wie oben im Code dargestellt.

Bis dahin
© 2014 Andreas Vogt

Daten Auswerten mit Kreuztabelle & Co

By , 4. März 2014

Will man z.B. Umsätze verschiedener Standorte auswerten um die Standorte miteinander zu vergleichen, dann ist das Übliche Mittel eine Kreuztabellenabfrage zuerstellen.
Unter Access 2010 klickt man in der Registerkarte „erstellen“ auf „Abfrageassistent“ und wählt dann „Kreuztabellenabfragen-Assistent“ aus.

Der restliche Dialog sollte selbsterklärend sein. Man wählt die Tabelle oder Abfrage aus, welche die Grundlage für die Kreuztabellen-Abfrage ist, und klickt auf weiter. Danach wird nach den Zeilenüberschriften gefragt, wir wählen hier das Feld „Monat“ aus. Nach klick auf Weiter wählt man die Spaltenüberschrift, wir nehmen hier das Feld „Standort“. Zum Schluss wird nach den Kreuzungspunkten gefragt, also nach den darzustellenden Werten. Bei mir heißt das Feld treffend „Umsatz“. Also das Feld Umsatz auswählen und rechts daneben die Funktion „Summe“ anklicken, da wir ja die Summen pro Monat und Standort möchten. Bei Access 2010 ergibt sich jetzt folgendes Bild:

Bild leider verloren gegangen

Klick auf Weiter und Fertig stellen erstellt uns die gewünschte Kreuztabellenabfrage, und zeigt diese gleich an. Aber das ist ja hinlänglich bekannt.

Will man jetzt aber das ganze in einem Formular haben, gibt es 2 Formular-Assistenten für PivotChart und PivotTable. Beginnen wir mit letzterem. Markieren Sie die entsprechende Tabelle oder Abfrage und klicken Sie im Ribbon auf Erstellen / Weitere Formulare / PivotTable. Sollte die Feldliste ausgeblendet sein, klicken Sie im Ribbon auf Feldliste, ggf. 2x.
Sie sehen jetzt oben die Zeile „Spaltenfelder hierher ziehen“ und links die Spalte „Zeilenfelder hierher ziehen“, und in der Mitte eine große leere Fläche. Klicken Sie in der Feldliste auf Monat und ziehen Sie diesen nach links in die Spalte, lassen Sie die Maustaste los. Sie sehen, unsere Monate wurden erstellt. Danach klicken Sie in der Feldliste auf Standort, und ziehen Sie diesen in die Zeile mit der Bezeichnung „Spaltenfelder hierher ziehen“, Maustaste loslassen. Jetzt werden die Standorte angezeigt, fehlen nur noch die Umsätze. Klicken Sie erneut in der Feldliste diesmal auf Umsatz, und ziehen Sie diesen in das mittlere leere Feld. Taste loslassen, unsere Tabelle ist jetzt komplett. Die Reihenfolge wie Sie die Felder plazieren spielt keine Rolle.

Wollen Sie jetzt noch Spalten- und Zeilensummen darstellen, gehen Sie wie folgt vor:
Klicken Sie mit der Maus in die Zeile mit den Bezeichnungen „Umsatz“, die Felder werden automatisch markiert.
Klicken Sie im Ribbon auf „AutoBerechnen“, jetzt werden die Summen aber leider noch weitere Felder angezeigt, die wir in der Anzeige nicht wünschen.
Klicken Sie im Ribbon auf „Details ausblenden“, ggf. 2x, jetzt wird die Tabelle wie gewünscht dargestellt:

Bild leider verloren gegangen

Um ein PivotChat Formular zu erstellen gehen wir ähnlich vor. Markieren Sie die betreffende Tabelle oder Abfrage, klicken Sie im Ribbon Erstellen auf „Weitere Formulare“, dann auf „PivotChart“. Sie sehen ein leeres Diagramm mit der Zeile darüber „Datenfelder hierher ziehen“. In diese Zeile ziehen Sie wie oben das Feld „Umsatz“. Unter dem Diagramm ist die Zeile „Rubrikenfelder hierher ziehen“, in diese Feld Ziehen Sie das Feld Standort. Jetzt gibt es noch rechts das Feld „Reihenfelder hierher ziehen“, ziehen Sie hierhinein das Feld Monat.

Das Diagramm ist erstellt, jetz müssten nur noch die beiden Achsentitel entsprechend umbenannt werden. klicken Sie diese nacheinander an und im Ribbon auf Eigenschaften-Blatt. Unter Format können Sie den Namen ändern. Um eine Legende anzuzeigen klicken Sie im Ribbon auf „Legende“. Jetzt könnte es so wie folgt aussehen:

Bild leider verloren gegangen

So das war es wieder einmal für Heute, hoffe der Artikel hat euch gefallen.

Bis dahin
© 2014 Andreas Vogt

Arbeiten mit ini-Dateien

By , 3. März 2014

ini-Dateien eignen sich hervorragend um z.B. Einstellungen und Optionen der Access-Anwendung zu speichern. Sei es Farben, Schriftarten, Schriftgröße oder auch Pfade. ini-Dateien sind nichts anderes als einfache Text-Dateien, die z.B. mit Notepad erstellt werden können. Das Besondere ist der Aufbau des Inhaltes. Hier mal ein Beispiel:

[Colors]
backcolor=255
forecolor = 0
[Fonts]
fontname=Arial
fontsize = 14

Erstellen Sie mit Notepad eine leere Textdatei und benennen Sie diese z.B. meinTest.ini. Im Explorer sehen sie sofort, dass der Text-Datei ein anderes Datei-Icon als das für Textdateien zugeordnet wurde. An diesem Icon können Sie ini-Dateien erkennen. Kopieren Sie nun obigen Code in die ini-Datei und speichern Sie diese.

Um nun lesend oder schreibend darauf zugreifen zu können, benötigen wir 2 API-Funktionen, die wir in ein Standardmodul hinein kopieren:

Public Declare Function GetPrivateProfileString Lib _
                                                "kernel32" Alias "GetPrivateProfileStringA" _
                                                (ByVal lpApplicationname As String, _
                                                 ByVal lpKeyName As Any, _
                                                 ByVal lpDefault As String, _
                                                 ByVal lpReturnedString As String, _
                                                 ByVal nSize As Long, _
                                                 ByVal lpFileName As String) As Long

Public Declare Function WritePrivateProfileString Lib "kernel32" _
                                                  Alias "WritePrivateProfileStringA" _
                                                  (ByVal lpApplicationname As String, _
                                                   ByVal lpKeyName As Any, _
                                                   ByVal lpString As Any, _
                                                   ByVal lpFileName As String) As Long

Was wir jetzt noch benötigen sind Prozeduren, mittels denen der jeweilige ini-Wert ausgelesen bzw. geschrieben werden kann. Und da wir wo möglich Techniken der Objektorientieren Entwicklung einsetzen möchten, erstellen wir eine Klasse, und benennen diese z.b. „Settings“. Für das Auslesen bzw Schreiben der ini-Werte benötigen wir jeweils eine Get und eine Let-Property:

Option Explicit

Private m_fontname As String
Private m_fontsize As Long
Private m_backcolor As Long
Private m_forecolor As Long

Public Property Get Fontname() As String
    If m_fontname = "" Then
        m_fontname = iniRead("Fonts", "fontname", "Arial")
    End If
    Fontname = m_fontname
End Property

Public Property Let Fontname(ByVal cFontname As String)
    m_fontname = cFontname
    iniWrite "Fonts", "fontname", cFontname
End Property

Public Property Get Fontsize() As Long
    If m_fontsize = 0 Then
        m_fontsize = iniRead("Fonts", "fontsize", "10")
    End If
    Fontsize = m_fontsize
End Property

Public Property Let Fontsize(ByVal lFontsize As Long)
    m_fontsize = lFontsize
    iniWrite "Fonts", "fontsize", lFontsize
End Property

Public Property Get Forecolor() As Long
    If m_forecolor = 0 Then
        m_forecolor = iniRead("Colors", "forecolor", "0")
    End If
    Forecolor = m_forecolor
End Property

Public Property Let Forecolor(ByVal lForecolor As Long)
    m_forecolor = lForecolor
    iniWrite "Colors", "forecolor", lForecolor
End Property

Public Property Get Backcolor() As Long
    If m_backcolor = 0 Then
        m_backcolor = iniRead("Colors", "backcolor", "16777215")
    End If
    Backcolor = m_backcolor
End Property

Public Property Let Backcolor(ByVal lBackcolor As Long)
    m_backcolor = lBackcolor
    iniWrite "Colors", "backcolor", lBackcolor
End Property

Die Funktionsweise von Properties sollte hinlänglich bekannt sein. Es gibt für jeden ini-Wert eine private Variable. Bei dem erstmaligen Aufruf einer Get-Property wird der iniWert mit der Funktion iniRead() ausgelesen und der privaten Variablen zugewiesen. Die Parameter sind: Section, Key und Default-Value. mittels letzterem Wert kann ein Standard-Wert eingestellt werden, falls in der ini-Datei der Wert nicht gesetzt ist – oder die ini-Datei nicht gefunden wurde.
Der Pfad zur ini-Datei kann natürlich nicht in der ini-Datei selbst gespeichert werden, das wäre vergebends. Ich habe z.B. dazu einfach eine weitere Property geschaffen, man hätte es auch in einer Variablen speichern können.

Private Property Get getIniFile() As String
    getIniFile = CurrentProject.Path & "\meinTest.ini"
End Property

Kommen wir zu den Funktionen iniRead und iniWrite. Diese bestehen im wesentlichen aus dem Aufruf der API-Funktionen mit den jeweiligen Parametern. Diese stehen ebenfall in der Klasse „Settings“.

Private Function iniRead(ByVal Section As String, _
                         ByVal Key As String, _
                         Optional ByVal Default As String = "", _
                         Optional ByVal nSize As Integer = 256) As String

    Dim lResult As Long
    Dim cValue As String

    cValue = Space$(nSize)
    lResult = GetPrivateProfileString(Section, Key, Default, cValue, nSize, getIniFile)

    iniRead = Left$(cValue, lResult)
    If iniRead = "" Then iniRead = Default
End Function

Private Sub iniWrite(ByVal Section As String, _
                     ByVal Key As String, _
                     ByVal cValue As String)

    Dim lResult As Long
    lResult = WritePrivateProfileString(Sektion, Key, cValue, getIniFile)
    If globCodeAbord Then Exit Sub
End Sub

Anwendung:
Die Verwendung der Klasse findet wohl hauptsächlich im Formularmodul statt, z.B. im „Form_Open“ Ereignis. Nachfolgender Beispielcode zeigt wie alle Textfelder und Bezeichnungsfelder in Schrift und Farbe angepasst werden. Die Anpassung ist nicht modal, nach dem Schließen des Formulars sind die Orginaleinstellungen wieder sichtbar:

Private Sub Form_Load()
    Dim ctl As Control
    Dim frmSetting As Settings
    Set frmSetting = New Settings
    With frmSetting
        For Each ctl In Me.Controls
            If ctl.ControlType = 100 Or ctl.ControlType = 109 Then
                ctl.Fontsize = .Fontsize
                ctl.Fontname = .Fontname
                ctl.Forecolor = .Forecolor
            End If
        Next
        Me.Detailbereich.Backcolor = .Backcolor
    End With
    Set frmSetting = Nothing
End Sub

Es wird ein neues Objekt der Klasse Settings erstellt, und in einer Schleife über alle Steuerelemente werden die mit der ControlType-Eigenschaft von 100 bzw. 109 (Bezeichnungs- und Textfelder) entsprechend verändert. Der Code sollte eigentlich selbsterklärend sein.
Was in den ganzen Codes noch fehlt ist die Fehlerbehandlung, insbesondere sollte man in iniRead/iniWrite das Vorhandensein der ini-Datei abprüfen.

Bis dahin
© 2014 Andreas Vogt

Service Pack für Office 2013 verfügbar

By , 2. März 2014

Vor wenigen Tagen hat Microsoft das Service Pack 1 für Office 2013 und SharePoint 2013 veröffentlicht. Das Service Pack soll vorallem die Stabilität und Sicherheit von Office 2013 und SharePoint 2013 erhöhen.

Die wichtigsten Änderungen:
Kompatibilität zu Windows 8.1 und Internetexplorer 11 wurde verbessert
Neue Apps für Office und APIs für Entwickler
Neues 3D Virtualisierungs Tool für Excel: Power Map
etc.

Wer viel Neues vom ServicePack erwartet, wird wahrscheinlich enttäuscht werden, das Hauptaugenmerk liegt hauptsächlich auf Stabilität und Sicherheit.
Beide Updates sind verfügbar im Microsoft Download Center:
Office 2013 SP1 32 Bit Version: http://www.microsoft.com/en-us/download/details.aspx?id=42017
Office 2013 SP1 64 Bit Version: http://www.microsoft.com/en-us/download/details.aspx?id=42006

Bis dahin
© 2014 Andreas Vogt

Factory Module verwenden

By , 2. März 2014

Man stelle sich vor man hat eine Klasse namens „Fahrzeug“, abgeleitet von dem Interface im Artikel zuvor, und Sie möchten neue Objekt erstellen. Üblicherweise geschieht das in folgender Weise:

    Dim car1 As Fahrzeug
    Set car1 = New Fahrzeug
    With car1
        .iFahrzeug_Farbe = 255
        .iFahrzeug_Geschwindigkeit = 100
        .iFahrzeug_Richtung = "Gerade aus"
        .iFahrzeug_Fahren
    End With

Mit der Anweisung Set car1 = New Fahrzeug wird das Objekt „car1“ zwar erstellt, das Objekt ist aber komplett ‚leer‘. Erst in den nachfolgenden Zeilen wird das Objekt mit Daten belegt. D.H. zum Erstellungszeitpunkt des Objektes stehen alle Eigenschaften auf 0 bzw. auf vbNullString.

Wäre es nicht Cool wenn man, um beim obigen Beispiel zu bleiben, das Objekt so erstellen könnte:

    Dim car1 As Fahrzeug
    Set car1 = New Fahrzeug(255,100,"Gerade aus")

Diese Möglichkeit gibt es in VBA leider nicht, aber wir können uns behelfen mit einer sogenannten Factory.

Zuerst benötigen wir in der Klasse „Fahrzeug“ einen Ersatz für den Standard-Konstruktor. Dazu erstellen wir eine öffentliche Prozedur, welche alle beim Start benötigten Werte als Parameter verlangt. Diese Parameter-Werte werden in dieser Prozedur einfach den Privaten Variablen zugewiesen. Und so sieht diese Prozedur für obiges Beispiel aus:

Public Sub Init(ByVal Color As Long, ByVal Speed As Long, ByVal Direction As String)
    m_farbe = Color
    m_geschwindigkeit = Speed
    m_richtung = Direction
End Sub

Danach erstellen wir ein Klassenmodul und benennen es z.B. „Factory“. Darin steht folgender Code:

Public Function CreateCar(lColor As Long, lSpeed As Long, cDirection As String) As Fahrzeug
    Dim objFahrzeug As Fahrzeug
    Set objFahrzeug = New Fahrzeug

    objFahrzeug.Init Color:=lColor, Speed:=lSpeed, Direction:=cDirection
    Set CreateCar = objFahrzeug
End Function

D.H. die Factory -Prozedur ruft den künstlichen Konstruktor in der Klasse „Fahrzeug“ auf, und übergibt die Startwerte.
Diese Factory Klasse kann viele Factory-Prozeduren für die unterschiedlichsten Klassen beinhalten, man muss nicht immer eine neue Factory-Klasse erstellen.

Verwendung:
Kommen wir zum obigen Beispiel zurück. Was sich als erstes ändert ist die Deklaration der Factory. Diese kann in einem Modul Private oder Public declariert werden. Wichtig ist dass die Factory mit dem Schlüsselwort New deklariert wird, da sonst das Objekt nicht erstellt wird:

Private CarFactory As New Factory

Das Erstellen des Objektes erfolgt dann so:

    Dim car1 As Fahrzeug
    Set car1 = CarFactory.CreateCar(255, 120, "links")
    car1.iFahrzeug_Fahren
    Set car1 = Nothing

Zusammenfassung
Durch die Factory können Klassen unabhängig von ihrer jeweiligen Implementierung verwendet werden, d.h. Klasse und aufrufender Code sind voneinander entkoppelt, und VBA rückt wieder ein Stück weiter in Richtung Objektorientierte Entwicklung. Daneben gibt es aber noch einen Handfesten Vorteil, der es Wert ist, sich mit Factories zu beschäftigen:
Jedes mal wenn ein Objekt instanziert wird, werden auch mit den notwendigen Eigenschaften gesetzt – da die Parameter in der Factory ja Muss-Parameter sind.

Viel Spass beim Experimentieren mit Klassen, Interface, Factory und Co.
Wenn es die Zeit erlaubt werde ich in den nächsten Tagen mal ein komplett-Beispiel hochladen mit Interface und Factory.

Bis dahin
© 2014 Andreas Vogt

OfficeFolders theme by Themocracy