Category: VBA-Code

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

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

Benutzerdefinierte Collections

By , 1. März 2014

Collections, auf Deutsch Auflistungen, begegnen uns beim Entwickeln in VBA eigentlich ständig, sei es die Auflistung aller Formulare (Forms), oder aller Steuerelemente in einem Formular (Controls). Neben diesen bereits vorhandenen Collections, ist es möglich auch eigene Collections zu erstellen. Dies geschieht mit der simplen Anweisung:
Dim CollectionName As New Collection.

Im Unterschied zu einem Array kann eine benutzerdefinierte Collection Werte und Objekte jeglichen Typs aufnehmen, eine Collection ist also nicht Typ gebunden. Ein weiterer, nicht zu unterschätzender Vorteil ist dieser, dass im Gegensatz zu einem Array – wo die Items nur durch ihre Ordnungszahl definiert sind – bei einer Collection als optionalen Parameter einen eindeutigen Bezeichner angegeben werden kann, über den auf das entsprechende Item zugegriffen werden kann.

Eine Benutzerdefinierte Collection verfügt über die Methoden „ADD“, „REMOVE“, „COUNT“ und „ITEM“, d.h. es gibt keine Edit-Methode. Will man ein Item ändern muss man es aus der Collection entfernen und geändert neu einfügen. Eine weitere Besonderheit benutzerdefinierter Collections ist, dass sie immer 1-Basiert sind, also das erste Element in einer Collection hat die Ordnungszahl 1, und nicht 0 wie bei einem Array.

Ein Beispiel:

Sub CollectionTest()
    Dim cBuch As New Collection
    Dim Ausgabe As String
    
    With cBuch
        .Add "Ken Getz", "Autor"
        .Add "VBA Developers Handbook", "Titel"
        .Add 922, "Seiten"
    End With
    
    Ausgabe = cBuch("Autor") & vbCrLf
    Ausgabe = Ausgabe & cBuch(2) & vbCrLf
    Ausgabe = Ausgabe & cBuch("Seiten") & " Seiten"
    MsgBox Ausgabe
End Sub

Wie man an diesem kleinen Beispiel sieht ist es egal ob ich cBuch(2) oder cBuch(„Titel“) schreibe. Der große Vorteil des Bezeichners ist, dass ich mir keine Gedanken machen muss an welcher Stelle ein Item steht, mit dem Bezeichner wird immer der richtige Wert verwendet.

Bis dahin
© 2014 Andreas Vogt

Arbeiten mit Interface-Klassen

By , 28. Februar 2014

Dass man mit Access auch objektorientiert entwickeln kann – wenn auch nicht völlig – dürfte hinlänglich bekannt sein. Klassenmodul erstellen, Methoden und Eigenschaften in als Prozeduren und Properties definieren, alles längst bekannt. Weniger bekannt aber ist die Verwendung von Interfaces bzw. Interface-Klassen.

Man stelle sich vor, man hat eine Klasse Auto, eine Klasse Motorrad und eine Klasse Fahrrad. 3 verschiedene Klassen, und doch werden die Methoden und Eigenschaften dieser Klassen in vielen Punkten identisch sein. Die Objekte aller 3 Klassen haben sicherlich die gemeinsamen Methoden „Fahren“, „Bremsen“ und „Lenken“. Außerdem die gemeinsamen Eigenschaften „Geschwindigkeit“, „Richtung“ und „Farbe“, um ein paar Beispiele zu nennen.

Diese Zusammenhänge lassen sich durch ein Interface abbilden, und das geschieht einfacher als man denkt. Ein Interface ist zuerst einmal nichts anderes als ein Klassenmodul, das man speziell benennt. Eingebürgert hat sich, dass man vor den Interface-Namen ein „i“ setzt. Wir erstellen also ein Klassenmodul, und benennen es „iFahrzeug“. Um jetzt die Struktur für Fahrzeuge in diesem Interface abzubilden erstellen wir darin alle Properties und Prozeduren wie oben benannt, aber ohne weiteren Code. Unser Interface sieht jetzt wie folgt aus:

Option Explicit

Property Get Geschwindigkeit() As Long
End Property
Property Let Geschwindigkeit(ByVal lSpeed As Long)
End Property

Property Get Richtung() As String
End Property
Property Let Richtung(ByVal cRichtung As String)
End Property

Property Get Farbe() As Long
End Property
Property Let Farbe(ByVal lColor As Long)
End Property

Sub Fahren()
End Sub

Sub Bremsen()
End Sub

Sub Lenken()
End Sub

Die Prozeduren und Properties dürfen nicht als private deklariert werden, sonst sind diese bei der Verwendung nicht verfügbar.
Interfaces können nur innerhalb Klassenmodulen und Formularmodulen (Formulare sind praktischer Weise ja auch Klassen) verwendet werden, nicht in Standard-Modulen. Die Einbindung in ein Klassenmodul findet mit dem Schlüsselwort „Implements“ statt. Dies erfolgt in der Klasse direkt nach den Optionen-Deklaration, in unserem Fall also: Implements iFahrzeug.

Sobald man das getan hat steht das Interface zur Nutzung bereit. Dies macht sich durch den Eintrag von iFahrzeug in der linken oberen Auswahlliste bemerkbar (Unterhalb der Symbolleiste). Wählt man darin iFahrzeug aus, wird die erste Property bereits in die Klasse eingefügt. Klickt man anschließend auf die rechte obere Auswahlliste, so sind die restlichen Properties und Prozeduren auswählbar, und können einfach in die Klasse eingefügt werden. Spätestens jetzt sollte die Verwendung von Interfaces klar geworden sein.

Mittels einem Interface wird definiert welche Methoden und Eigenschaften eine Klasse mindestens haben muss!. Das Bedeutet alle in dem Interface definierten Prozeduren und Properties müssen auch in die Klasse übernommen werden. Macht man das nicht so erhält man einen Fehler wenn man das Projekt Kompelliert.

Fügt man die restlichen Properties und Methoden ein, sollte die Klasse jetzt wie folgt aussehen:

Option Compare Database
Option Explicit

Implements iFahrzeug

Private Sub iFahrzeug_Fahren()
End Sub

Private Sub iFahrzeug_Bremsen()
End Sub

Private Sub iFahrzeug_Lenken()
End Sub

Private Property Get iFahrzeug_Farbe() As Long
End Property

Private Property Let iFahrzeug_Farbe(ByVal RHS As Long)
End Property

Private Property Let iFahrzeug_Geschwindigkeit(ByVal RHS As Long)
End Property

Private Property Get iFahrzeug_Geschwindigkeit() As Long
End Property

Private Property Let iFahrzeug_Richtung(ByVal RHS As String)
End Property

Private Property Get iFahrzeug_Richtung() As String
End Property

Jetzt kann man relativ einfach jede Klasse (Auto, Motorrad, Fahrrad) nach dieser „Vorschrift“ erstellen, auscoden und nach Bedarf erweitern.

Fassen wir mal zusammen:

  • Interfaces sind Daten- und Zugriffsstrukturen, welche den Zugriff und die Steuerung von Objekten regeln
  • Vereinfacht gesagt ist ein Interface eine Struktur an Methoden und Eigenschaften.
  • Interfaces sind bestimmte Klassenmodule, die nach einem Schema benannt werden.
  • Interfaces enthalten nur die Rümpfe der Prozeduren und Properties. Code darin würde ignoriert werden.
  • Eingebunden in Klassenmodulen (oder Formular-Code) werden diese mit dem Schlüsselwort „Implements“
  • Objektorientiert Entwickeln ist Cool 😉

Und nun viel Spass beim Experimentieren mit OOP und Interfaces.

Bis dahin
© 2014 Andreas Vogt

Array definieren mal anderst

By , 27. Februar 2014

Arrays werden entweder per Array() Anweisung definiert, oder auch durch den Split() Befehl.
Auf eine andere bzw. erweiterte Möglichkeit bin ich kürzlich gestoßen beim Entwickeln eines Benutzersteuerelementes mit VB6.
Dort ging es u.a. darum, Steuerelemente beim Klick auf einen Button zu plazieren und sichtbar zu machen.

Die Größe und Position der Steuerelemente habe ich dabei wie folgt definiert:

Private Sub Form_Open(Cancel As Integer)
    Dim sizeArr(8) As Variant
    Dim i As Long
    sizeArr(0) = Array("cmdClose", 2350, 23, 306, 975)
    sizeArr(1) = Array("Linie1", 47, 2412, 2066, 2066)
    sizeArr(2) = Array("Linie2", 47, 2412, 697, 697)
    sizeArr(3) = Array("lblMonat", 23, 47, 454, 2381)
    sizeArr(4) = Array("zurueck", 142, 120, 227, 227)
    sizeArr(5) = Array("vor", 142, 2160, 227, 227)
    sizeArr(6) = Array("MonatJahr", 120, 480, 255, 1575)
    sizeArr(7) = Array("Heute", 2066, 47, 255, 2381)
    sizeArr(8) = Array("Rechteck1", 23, 47, 2280, 2381)

Das bedeutet also, jedem einzelnen Array-Element wurde ein weiteres Array zugewiesen.
Der Zugriff auf einen bestimmten Wert, z.B. auf „lblMonat“ lautet dann: sizeArr(3)(0)

Und um wieder zum Beispiel aus der Praxis zurückzukommen, hier der restliche Code, der die Steuerelemente dimensioniert, positioniert und sichtbar schaltet:

    For i = LBound(sizeArr) To UBound(sizeArr)
        With Controls(sizeArr(i)(0))
            .Top = sizeArr(i)(1)
            .Left = sizeArr(i)(2)
            .Height = sizeArr(i)(3)
            .Width = sizeArr(i)(4)
            .Visible = True
        End With
    Next i
End Sub

Bis dahin
© 2014 Andreas Vogt

OfficeFolders theme by Themocracy