Office Forum
www.Office-Loesung.de
Access :: Excel :: Outlook :: PowerPoint :: Word :: Office :: Wieder Online ---> provisorisches Office Forum <-
Eigene Controls entwickeln
zurück: Beispieldatenbank weiter: "LINQ for VBA" - Dynamisches SQL ohne Stringkaprio Unbeantwortete Beiträge anzeigen
Neues Thema eröffnen   Neue Antwort erstellen     Status: Tutorial Facebook-Likes Diese Seite Freunden empfehlen
Zu Browser-Favoriten hinzufügen
Autor Nachricht
Bitsqueezer
Office-VBA-Programmierer


Verfasst am:
18. Dez 2012, 17:16
Rufname:


Eigene Controls entwickeln - Eigene Controls entwickeln

Nach oben
       Version: (keine Angabe möglich)

Hallo,

Adventszeit ist Bastelzeit...Smile

Also soll es heute mal darum gehen, wie man unter Verwendung von Standardcontrols und VBA eigene Controls basteln kann, die mehr können oder anders funktionieren als die Standardcontrols. So kann man sich auf einfache Weise eine verbesserte Funktionalität oder mehr Möglichkeiten im Design einer Benutzeroberfläche schaffen.

Für dieses Tutorial sollte man auf jeden Fall Grundlagenwissen in VBA haben und auf herkömmliche Weise ein Formular mit Standardcontrols programmieren können.

Es werden keine externen Bibliotheken eingesetzt und die Dinge sind so einfach wie möglich gestaltet, so daß das Ergebnis unter jedem Access lauffähig sein sollte.

Als erstes soll mal eine eigene Checkbox gebaut werden. Die Standardcheckbox von Access hat eigentlich alles, was eine Checkbox haben soll: bis zu drei Schaltzustände für Ja/Nein/Nicht definiert (NULL), alle notwendigen Events, um den Zustand zu ändern und die Bindung an ein Datenfeld einer Tabelle. Das Design ist OK - aber das war's auch schon. Man kann das Design der Checkbox nur sehr geringfügig ändern, insbesondere seine Größe ist kaum veränderbar, aber auch Hintergrundfarbe oder Darstellung des Hakens.

Machen wir uns auf die Suche nach einem Ersatz. Am Anfang der Programmierung eines eigenen Controls steht, vorhandene Elemente zu suchen, die den Basiszweck bestmöglich erfüllen. Der Grund ist einfach: In VBA hat man nur begrenzte Möglichkeiten, ein wirklich neues Control zu entwickeln, das tatsächlich genauso funktioniert wie ein normales Control, da man beispielsweise nur mit viel Aufwand und Gefahr eines Absturzes direkt auf die Formularoberfläche zeichnen kann oder die Mausposition mühsam errechnen muß usw. Das Ergebnis wäre ein extrem großes Codemonster, nur um nicht auf interne Controls angewiesen zu sein.
Auf der anderen Seite gibt es aber schon viele gute interne Controls, die alle möglichen Richtungen gut abdecken, so daß man deren Funktionalität einfach als Basis verwenden kann und sie "nur noch" erweitern muß. Dazu ist natürlich auch einige Programmierung notwendig, jedoch im Regelfall recht simple Programmierung, die lediglich viel Schreibarbeit kostet.

Also welches Control könnte für unsere eigene Checkbox geeignet sein? Da fiele am Anfang der Blick z.B. auf das Rechteck. Das wäre ein Rahmen und Linien gibt es auch, daraus könnte man einen Haken zusammenbauen. Aber beide haben keine Möglichkeit, ein Datenfeld zu binden und wenn man eine neue Checkbox auf diese Weise erzeugt, müßte man es für jede Checkbox so machen, was doch ziemlich mühsam wäre. Ein Blick auf die zur Verfügung stehenden Events ist auch sehr ernüchternd: Das Rechteck hat gerade mal Klick, Doppelklick und Mausereignisse, die Line schon gar keine mehr.

Wie wäre es mit einem Label? Hier kann man immerhin schon mit dem normalerweise überall verfügbaren "Wingdings"-Zeichensatz einen schönen, geschwungenen Haken darstellen, Rahmenfarbe und Hintergrundfarbe sind veränderbar, ebenso die Textfarbe, die Schriftgröße, Special Effect usw. Das ist ein guter Anfang, aber der Blick auf die Events zeigt, daß man hier auch nur karg ausgestattet ist, nicht mehr als das Rechteck.

Also gehen wir weiter zu einem komfortableren Control: Der Textbox. Die Darstellung kann genauso wie beim Label manipuliert werden, darüber hinaus hat die Textbox aber auch noch reichlich Events, die Bindung an ein Datenfeld, man kann ein Label daran koppeln und hat sogar ein Conditional Formatting zur Verfügung. Das sieht doch nicht schlecht aus - das nehmen wir mal als Grundlage.

Wie beim Weihnachtsstern-Basteln genügt es aber nicht, Metallfolie zu kaufen, jetzt ist Handarbeit gefragt und ein bißchen Werkzeug und Klebstoff. Werkzeug ist im VBA-Werkzeugkasten reichlich vorhanden. Das erste, was wir brauchen, ist ein Klassenmodul. Warum ein Klassenmodul und kein Standardmodul? Weil das Klassenmodul über ein paar Fähigkeiten verfügt, die auch für ein Control typisch sind und die wir benötigen: Zum einen kann man aus einem einmal gebauten Klassenmodul beliebig viele Objekte erstellen (so wie man beliebig viele Checkboxen auf ein Formular stellen kann, weil der Checkbox auch nur ein Klassenmodul zugrundeliegt) und zum anderen kann man nur mit Klassenmodulen eigene Events zur Verfügung stellen - so wie auch eine Checkbox Events wie etwa den Click-Event zur Verfügung stellt.

Zum Glück müssen wir nicht alles selbst basteln, die Textbox, die wir als Grundlage nehmen, enthält schon eine Menge wichtiger Dinge. Also los.

1. Das Testformular
Zum Ausprobieren braucht es eine "Testwiese", also erst mal ein leeres Formular einfügen. In der Demodatenbank im Anhang ist das Formular "frm1_WelchesControl" dazu da, um mit verschiedenen Controls herumzuspielen und auszuprobieren, was sich als Checkbox eignet. Hier sieht man ein Label als Checkbox, eine normale Checkbox und eine Textbox. Wie oben beschrieben, entscheiden wir uns für die Textbox als Basis.
Um das richtige Zeichen aus dem Wingdings-Font wählen zu können, verwendet man am besten ein meist vergessenes kleines Hilfstool, das seit mindestens Windows 95 unter "Zubehör" - "Systemprogramme" zu finden ist: Die "Zeichentabelle". Damit kann man jedes Zeichen eines beliebigen Fonts auflisten und in die Zwischenablage stellen. Außerdem wird auch der ASCII-Code angezeigt, den man in VBA mit der Chr-Funktion verwenden kann. Hiermit ist das Häkchen schnell gefunden, kopiert und im Formular eingefügt. Damit das Zeichen in der Textbox richtig angezeigt wird, wird einfach vorerst eine Funktion
Code:
="ü"
in der Textbox eingestellt - für den ersten Versuch.

2. Das Klassenmodul
Nach dem Starten des VBA-Editors fügen wir ein neues Klassenmodul ein, mit "Einfügen" - "Klassenmodul". Mit <F4> gelangt man in das Eigenschaftenfenster, hier kann man den Namen der Klasse nun ändern. Wenn man später mehr eigene Controls entwickeln möchte, sollte man ein einheitliches Namensschema nehmen, damit die Controls in der Liste beieinander stehen. Also nennen wir die Klasse nun "clsCTRL_Checkbox" - und gleich speichern.

Ein "Option Explicit" gehört, wie üblich, in das Klassenmodul zu Beginn eingefügt, damit auch alles ordentlich deklariert werden muß.
Da wir nun die Textbox als Grundlage unserer Checkbox verwenden wollen, fügen wir eine Klassenvariable mit dem Typ "Access.TextBox" ein:
Code:
Private prv_ctlTextBox As Access.TextBox
Eine Klassenvariable ist ganz einfach eine sogenannte "modulglobale Variable", d.h., innerhalb des Klassenmoduls kann an beliebiger Stelle darauf zugegriffen werden. Der Prefix "prv_" für "Private" soll das innerhalb des Codes verdeutlichen. Als "Private" definiert, kann von außen niemand an der Textbox herummanipulieren.

Ein jedes Klassenmodul verfügt über die besonderen Event-Subs "Class_Initialize" und "Class_Terminate". Wann immer man ein Klassenmodul zu einem Objekt instantiiert, wird die Initialize-Sub aufgerufen, und wenn es auf "Nothing" gesetzt wird, die Terminate-Funktion. Damit kann man die Variablen initialisieren bzw. auch wieder terminieren. Leider gibt es hier nicht, wie in den "großen" objektorientierten Programmiersprachen, Parameter, die der Ersteller des Objektes übergeben kann, so kann man hier leider nur grundlegende Dinge erledigen.
Damit "ordentlich" aufgeräumt wird, sollte man Objektvariablen am Ende immer explizit auf "Nothing" setzen, da ansonsten Access selbst entscheidet, wann ein Objekt nicht mehr gültig ist, was schon mal zu unerwünschten Seiteneffekten führen kann.

Am Ende vom Weihnachtssternbasteln kommt das Aufräumen, also schauen wir, daß unsere Klasse am Ende auch aufräumt. Über die Kombobox über dem VBA-Editor wählen wir also "Class", was automatisch die Initialize-Sub einfügt, und mit der rechten Kombobox dann "Terminate". In die "Terminate"-Sub schreiben wir den Aufräum-Code:
Code:
    Set prv_ctlTextBox = Nothing
Somit ist der Job, nach dem Basteln aufzuräumen, nun auch fest vergeben und wir müssen da nicht mehr dran denken (praktisch, zu Hause räumt immer die Mutti bei den Kindern automatisch auf..."Set Schere = Nothing"....Wink )

Noch gibt es aber gar nichts zum Aufräumen, denn es wurde ja noch keine Textbox erstellt, lediglich eine Variable mit einem Typ festgelegt. Da es keinen "New" Constructor gibt wie etwa in VB.NET und "Class_Initialize" durch seine fehlenden Parameter kein Ersatz ist, müssen wir selbst eine Initialisierung der Klasse festlegen. Wir könnten nun eine Property festlegen, die die Textbox initialisiert. Das wäre OK, wenn das das einzige wäre, was zu initialisieren ist, aber typischerweise wird das im Lauf der Entwicklung immer mehr, und es müssen oft mehr als eine Eigenschaft initialisiert werden, was bedeutet, daß der Benutzer der Klasse daran denken muß, all das zu initialisieren, was nötig ist. Das wäre sehr unhandlich, und auch der Klassenprogrammierer müßte eine Menge mehr prüfen, um sicherzustellen, daß alles initialisiert wurde. Also verwende ich persönlich für alle Klassen eine Sub namens "Init", die alle benötigten Werte als Parameter entgegennimmt und damit ein kleiner Ersatz für die fehlende "New"-Prozedur ist.

Also schreiben wir in das Klassenmodul:
Code:
Public Sub Init(tb As Access.TextBox)
    Set prv_ctlTextBox = tb
End Sub
Das ist also unser Gegenstück zur "Terminate"-Prozedur oben, also sozusagen ein eigenes "Initialize", nur mit der Möglichkeit, Parameter zu vergeben. "tb" als Variablenname genügt, es muß kein "sprechender Name" sein, denn der Verwender der Klasse sieht per Tooltip, daß es sich um eine Textbox handeln muß und wir brauchen die Variable nur zu diesem einen Zweck.

Damit der Verwender auch ganz sicher eine Textbox übergibt, die gültig ist (die also existiert) und nicht "Nothing", sollte man solche Parameter besser prüfen und ansonsten einen Fehler ausgeben:
Code:
Public Sub Init(tb As Access.TextBox)
    If Not tb Is Nothing Then
        Set prv_ctlTextBox = tb
      Else
        Err.Raise vbObjectError + 1, cMODULENAME & "->Init", "Please provide a textbox control!"
    End If
End Sub
Im Gegensatz zu herkömmlicher Programmierung, bei der man mit "On Error Goto" arbeiten und ggf. eine Messagebox zur Fehlerausgabe verwenden würde, sollte man bei einem Klassenmodul die Fehlerverarbeitung besser von dem Verwender erledigen lassen - so wie ja auch eine echte Checkbox bei einem Fehler nicht selbst eine Messagebox ausgibt, sondern einen Fehler erzeugt, den der Verwender der Checkbox abfangen und bearbeiten muß. Der Grund ist, daß die Checkbox ja nicht entscheiden kann, wo und wofür sie verwendet wird und ob ihr Verwender im Fall eines Fehlers diesen an den Benutzer weitergeben möchte oder nicht. Ein Fehler, der in einer Prozedur auftritt und hier nicht abgefangen wird, wird solange nach oben (also an den Aufrufer) weitergereicht, bis irgendwo eine Fehlerbehandlung eintritt - oder wenn nicht, gibt VBA den Fehler aus, was dann in einer Access Runtime zum Absturz führt, da es hier keinen VBA-Editor gibt, der die Fehler anzeigen könnte. Also sollte der Verwender einer Klasse auch selbst immer für eine entsprechende Fehlerbehandlung sorgen.

Mit "Err.Raise" kann man einen Fehler "nach oben" weiterreichen, praktischerweise kann man hier auch eigene Fehlernummern und die Quelle sowie eine eigene Fehlernachricht verwenden. Laut Access Hilfe sollten die Fehler erst ab der Konstante "vbObjectError" beginnen, um nicht mit Fehlernummern von Access selbst zu kollidieren, man kann aber natürlich auch solche Nummern verwenden, die eine Standardfehlermeldung ausgeben, wenn es sinnvoll erscheint.
In einer komplexen Klasse, in der man sicherstellen möchte, daß der Programmierer anhand der Fehlernummer erkennt, was für ein Fehler aufgetreten ist, kann und sollte man die Fehlernummern z.B. als Public Enumeration deklarieren, damit man sie bequem extern abfragen kann. Das sparen wir uns hier mal, denn das würde die eigentliche Demo zum Erstellen eines eigenen Controls zu sehr aufblähen.

Die Konstante "cMODULENAME" verwende ich in allen Modulen dazu, um den Namen des Klassenmoduls in der Klasse zur Verfügung zu haben, beispielsweise für solche Fehlermeldungen. Außerdem hat es den Vorteil, daß in den "Lokal"- und "Überwachungsausdrücke"-Fenstern beim Aufklappen einer solchen Objektvariable so immer oben der Konstantenname zu sehen ist und man so das Objekt leichter auf seine Klasse hin identifizieren kann.

Also am Anfang der Klasse noch eine modulglobale Konstante hinzugefügt:
Code:
Private Const cMODULENAME As String = "clsCTRL_Checkbox"
Der Rumpf ist erstellt, wir haben jetzt ein Klassenmodul, das mit einer Textbox initialisiert werden kann. Also zunächst mal ein kleiner Test. Dazu fügen wir ein Standardmodul ein und nennen es über seine Eigenschaften (<F4>) "modTest". Natürlich achten wir auch hier auf das obligatorische "Option Explicit" bzw. aktivieren die Option "Variablendeklaration erforderlich" in "Extras"-"Optionen", damit das in Zukunft der VBA-Editor in jedes neue Modul einfügt.

In das Standardmodul schreiben wir eine kleine Testsub:
Code:
Public Sub TestCheckbox()
    Dim objCheckbox As clsCTRL_Checkbox
   
    Set objCheckbox = New clsCTRL_Checkbox
    objCheckbox.Init Nothing
    Set objCheckbox = Nothing
End Sub
Wie man sieht, muß die Objektvariable nicht zwangsläufig so heißen wie die Klasse, also etwa "objCTRL_Checkbox", wenngleich das auch keine schlechte Idee ist, um gleich am Namen zu erkennen, aus welcher Klasse das Objekt entstanden ist.

Im Direktfenster können wir das nun testen und schreiben dort "TestCheckbox". Es sollte die oben definierte Fehlermeldung erscheinen und wenn man "Debuggen" klickt, steht die Debug-Zeile auf der "Init"-Prozedur. Das ist korrekt, denn die Klasse hat ja keine eigene Fehlerverwaltung und übergibt die Fehlermeldung an die aufrufende Prozedur. Da war der letzte Befehl der "Init"-Aufruf.
Beim Schreiben der Klasse macht diese Methode natürlich etwas mehr Arbeit, da man im Fehlerfall nicht die Zeile in der Klasse zu sehen bekommt, wo der Fehler aufgetreten ist. Das macht hier nichts, da es ein selbst erstellter Fehler ist und den findet man natürlich ebenso wie seine Ursache leicht im Code. Bei Fehlern, die anderweitig aufgetreten sind, hilft hier nur, nun mit <F8> die Init-Prozedur Schritt für Schritt durchzugehen, bis man den Fehler findet. Alternativ kann man natürlich auch temporär eine Fehlerbehandlung einfügen, was aber meiner Meinung nach zu deutlich mehr Arbeit führt.

Wenn man die Testfunktion etwas abwandelt, sollte immer noch die Fehlermeldung erscheinen:
Code:
Public Sub TestCheckbox()
    Dim objCheckbox As clsCTRL_Checkbox
    Dim objTB As Access.TextBox
   
    Set objCheckbox = New clsCTRL_Checkbox
    objCheckbox.Init objTB
    Set objCheckbox = Nothing
End Sub
"objTB" ist zwar nun eine Textbox-Variable, aber da sie nicht initialisiert wurde, ist es immer noch "Nothing". Leider kann man Access-Controls nicht einfach mit "New" instantiieren, daher brauchen wir nun eine Textbox in einem Formular. Dazu nehmen wir nun das Formular "frm2_Testwiese", in dem es eine normale Checkbox und eine passend vorformatierte Textbox gibt. Die Textbox bekommt erst mal einen Namen, hier "txtMyCheckbox" und ihr Label ebenso "lblMyCheckbox".

Dann erstellen wir eine "Form_Load" Prozedur, um die neue Klasse auszuprobieren:
Code:
Option Compare Database
Option Explicit

Private prv_objCheckbox As clsCTRL_Checkbox

Private Sub Form_Load()
    Set prv_objCheckbox = New clsCTRL_Checkbox
    prv_objCheckbox.Init Me.txtMyCheckbox
End Sub
Auch hier haben wir die Objektvariable nun zu einer modulglobalen Variable gemacht, da sie sonst am Ende von "Form_Load" gelöscht werden würde. Sie soll aber solange erhalten bleiben, wie das Formular existiert. Ein Formular ist auch ein Klassenmodul, hier ist der Konstruktor aufgeteilt in "Form_Open" und "Form_Load", der Destruktor ist "Form_Close" und "Form_Unload". Meistens ist es besser "Load" und "Unload" zu verwenden, da hier schon die Daten des Formulars initialisiert wurden. Ein "Class_Initialize" und "Terminate" gibt es in einem Formular-Klassenmodul nicht, weil es schon die genannten gibt. Auch hier sollte man ordentlich aufräumen, also ergänzen wir noch:
Code:
Private Sub Form_Unload(Cancel As Integer)
    Set prv_objCheckbox = Nothing
End Sub
Noch macht unsere Klasse aber nicht mehr, als eine Textbox-Variable zu speichern, also wird es Zeit, das Klassenmodul mit Leben zu füllen.

3. Control erweitern
Wir wollen aus der Textbox nun eine Checkbox machen. Also müssen wir zuerst mal festhalten, was unser neues Control können soll. Dazu bedienen wir uns der Funktionalität der echten Checkbox und ahmen sie nach, so gut es geht.

Das erste, was man bei der echten Checkbox sieht ist, daß sie einen aktuellen Status hat, also NULL, angehakt oder nicht angehakt. Ist die Checkbox nicht gebunden, zeigt sie beim Öffnen des Formulars den NULL-Zustand durch ein ausgefülltes Kästchen an. Das wollen wir nun erst mal nachmachen.

Jedes Standardcontrol hat eine Eigenschaft namens "Value", die den Wert des Controls zurückliefert. Wenn wir die Testwiese starten und im Direktfenster "?Form_frm2_Testwiese.chkTest.Value" eingeben, gibt VBA "Null" aus.

Wir brauchen also nun eine Eigenschaft, die den Wert unseres Controls ausgibt. Die Textbox verfügt natürlich schon über eine "Value"-Eigenschaft, so daß man theoretisch die Textbox-Variable nur "Public" schalten müßte, damit man darauf zugreifen könnte. Wir wollen aber eine ordentliche Schnittstelle zum Verwender der Klasse bauen, damit es dieser so einfach wie möglich hat. Also ist die erste Eigenschaft nun "Value". Üblicherweise sollte man keine reservierten Namen in der eigenen Programmierung verwenden, allerdings gilt das nicht für Namen, die ansonsten nur als Eigenschaften oder Methoden in anderen Klassen vorkommen. Die neue Eigenschaft "Value" wird zwar als "Public" definiert, damit man von außen darauf zugreifen kann, sie kann aber immer nur in Verbindung mit dem Objektnamen verwendet werden, also in unserer Testwiese beispielsweise mit "prv_objCheckbox.Value". Daher ist es unproblematisch, "Value" als Eigenschaftsname zu verwenden.
Ein Control hat auch eine Default-Eigenschaft, so daß man auch ohne explizit ".Value" zu schreiben, den Wert ausgeben oder setzen kann. Das ist mit VBA leider nicht möglich, daher verzichten wir hier darauf. Man kann eine Default-Eigenschaft nur mit Tricks definieren (durch Zuweisung eines Attributes, dieses kann man aber nur einbauen, indem man das Modul extern speichert, es in einem Texteditor einfügt und wieder importiert, in VBA ist es dann nicht zu sehen und kann leicht wieder versehentlich gelöscht werden. Das kann jeder selbst erweitern, wenn er möchte.).

Die Textbox hat nun zwar eine Eigenschaft "Value", diese können wir aber nicht benutzen, da bekanntermaßen der Value der Checkbox sichtbar in der Textbox als Text zu sehen ist. Wir müssen uns den Wert also selbst merken und die Textbox je nach Wert passend formatieren. Das geht ganz einfach mit einer weiteren modulglobalen Variable:
Code:
Private prv_varValue As Variant
Hier eignet sich eine Variant-Variable am besten, da diese als einzige den NULL-Wert annehmen kann. Auch hier könnte man natürlich nun einfach diese Variable als "Public" definieren und damit den Verwender der Klasse in die Lage versetzen, den Wert jederzeit zu ändern. Aber was ist, wenn dieser nun einen String in den Wert schreibt? Oder ein Array? Das wäre mit einer Checkbox nicht darstellbar. Außerdem würde das Ändern des Wertes alleine nichts in unserer Textbox bewirken. Also bleibt der Wert Private und wir erstellen ein Property-Paar, mit dem der Wert gelesen und beschrieben werden kann:
Code:
Public Property Get Value() As Variant
    Value = prv_varValue
End Property

Public Property Let Value(varValue As Variant)
    prv_varValue = varValue
End Property
So würde man im einfachsten Fall eine Eigenschaft "Value" definieren, die nur die private Variable beschreibt oder ihren Wert zurückgibt. Damit wäre nichts gewonnen. Wie gesagt, muß die Textbox aktualisiert werden und außerdem sollte auch geprüft werden, ob die Textbox überhaupt gesetzt wurde.

In unserem Design haben wir festgelegt, daß die Textbox einen Wingdings-Font verwenden muß und aus diesem bestimmte Zeichen zur Darstellung des Häkchens und des NULL-Zustands. Im Gegensatz zur Original-Checkbox, die nur eine feste Darstellung verwendet, soll unsere Checkbox flexibel sein und die Anpassung durch den Verwender ermöglichen. Auf der anderen Seite soll dieser aber auch nicht gezwungen sein, eine Darstellung erst mühsam festzulegen. Also definieren wir einen Standard und ein paar Eigenschaften, die dem Verwender ermöglichen, die Standards nur im Bedarfsfall anpassen zu müssen.
Da wir diese für die Value-Eigenschaft benötigen, beginnen wir erst einmal mit diesen Eigenschaften.

Als erstes wäre da der FontName. Wir können natürlich voraussetzen, daß der Verwender eine Textbox wie auf unserer Testwiese selbst erstellt hat, die die entsprechenden Eigenschaften bereits gesetzt hat, aber komfortabler ist es, die Klasse diese lästigen Jobs automatisch vornehmen zu lassen - und da wir ja eine Init-Sub haben, können wir diese auch dazu verwenden.
Zunächst brauchen wir wieder ein paar modulglobale Variablen, die den Zustand der Eigenschaften speichern, genau wie bei "Value":
Code:
Private prv_strTxtStateNULL As String
Private prv_strTxtStateTrue As String
Private prv_strTxtStateFalse As String
Und, dazu passend, die entsprechenden öffentlichen Eigenschaften:
Code:
Public Property Get Value() As Variant
    If Not prv_ctlTextBox Is Nothing Then Value = prv_varValue
End Property

Public Property Let Value(varValue As Variant)
    If Not prv_ctlTextBox Is Nothing Then prv_varValue = varValue
End Property

Public Property Get TxtStateNULL() As String
    TxtStateNULL = prv_strTxtStateNULL
End Property

Public Property Let TxtStateNULL(strText As String)
    prv_strTxtStateNULL = strText
End Property

Public Property Get TxtStateTrue() As String
    TxtStateTrue = prv_strTxtStateTrue
End Property

Public Property Let TxtStateTrue(strText As String)
    prv_strTxtStateTrue = strText
End Property
Somit gibt es jetzt drei neue Eigenschaften, die mit "TxtState" anfangen und mit denen der Text für die Anzeige festgelegt werden kann. Hier wird nicht überprüft, wie der Text aussieht, das ist Sache des Verwenders. So könnte unsere Textbox, da es ein String ist, z.B. auch den Text "Wahr", "Falsch", "Nicht definiert" in der Textbox anzeigen, wenn der Font auf einen normalen Font gesetzt wird. Den müssen wir natürlich auch noch definieren. Also noch eine Eigenschaft, der FontName:
Code:
Public Property Get FontName() As String
    FontName = prv_ctlTextBox.FontName
End Property

Public Property Let FontName(strFontName As String)
    prv_ctlTextBox.FontName = strFontName
End Property
Hierzu wird keine Variable benötigt, da wir den FontName einfach direkt in die gleiche Eigenschaft der Textbox schreiben. Auch hier soll vorher geprüft werden, ob die Textbox auch wirklich definiert wurde (schließlich könnte der Verwender die FontName-Eigenschaft schon vor der Init-Prozedur zu schreiben versuchen und dann wäre die Textbox-Variable noch gar nicht initialisiert).

Da das häufiger notwendig wird, schreiben wir wieder eine Eigenschaft für die Textbox, aber diesmal eine private, da die Textbox ja nicht nach außen weitergereicht werden sollte (klar, der Verwender der Klasse kann natürlich sowieso auf die Textbox im Formular zugreifen, aber das soll uns nicht interessieren, es geht darum, wie man solche Dinge sauber kapselt):
Code:
Private Property Get TB() As Access.TextBox
    If prv_ctlTextBox Is Nothing Then
        Err.Raise vbObjectError + 1, cMODULENAME & "->TB", "Textbox object not initialized, please use the Init sub first!"
      Else
        Set TB = prv_ctlTextBox
    End If
End Property

Private Property Set TB(objTB As Access.TextBox)
    If Not objTB Is Nothing Then
        Set prv_ctlTextBox = objTB
      Else
        Err.Raise vbObjectError + 1, cMODULENAME & "->TB", "Please provide a textbox!"
    End If
End Property
(Statt "Let" benötigen wir hier "Set", da es sich um ein Objekt handelt.)

Die Prozedur für Set sieht der in der Init-Prozedur doch sehr ähnlich - also können wir dort den Code nun verkürzen:
Code:
Public Sub Init(objTB As Access.TextBox)
    Set TB = objTB
End Sub
Wenn man die Prozedur "TestCheckbox" aufruft, wird trotzdem weiterhin der Fehler ausgegeben, wie gehabt. Grund ist, daß wir eben keine Fehlerbehandlung haben, nirgendwo. Das heißt nach wie vor, der Fehler wird hochgereicht, bis man an eine Prozedur kommt, die den Fehler behandelt bzw. bis zur letzten aufrufenden Prozedur. Es spielt also keine Rolle, daß der Fehler nun erst in der Property generiert wird, da "Init" nicht der ursprüngliche Aufrufer war und selbst keine Fehlerbehandlung hat, geht der Fehler weiter zurück zur aufrufenden Prozedur in "TestCheckbox".

Das vereinfacht nun die Programmierung in der ganzen Klasse, denn jetzt muß man nicht mehr überall erst testen, ob die Objektvariable schon gültig ist oder noch war, man kann nun einfach auf "TB" zugreifen und im Fehlerfall wird der Fehler dann nach oben weitergereicht.

Es fällt vielleicht dem ein oder anderen auf, daß hier kein "Me." verwendet wird. Grund ist, daß es sich um eine als Private definierte Eigenschaft hat, die der Verwender der Klasse nicht nutzen können soll. "Me" funktioniert in VBA aber nur so, als wenn man von außen auf die Klasse zugreift. "Me" stellt also nur alle Eigenschaften und Methoden zur Verfügung, die die Klasse auch nach außen weiterreicht. Würde man "Me.TB" schreiben, würde spätestens der Compiler meckern, daß er diese Eigenschaft nicht kennt. Innerhalb der Klasse können wir also auf "TB" einfach so zugreifen, als wenn es eine ganz normale Variable wäre und wir wissen außerdem, daß im Problemfall ein Fehler generiert wird.

Also können wir jetzt auch die "FontName"-Eigenschaft passend umschreiben:
Code:
Public Property Get FontName() As String
    FontName = TB.FontName
End Property

Public Property Let FontName(strFontName As String)
    TB.FontName = strFontName
End Property
Wenn wir jetzt in der Testsub die Zeile "Debug.Print objCheckbox.FontName" vor dem Aufruf der "Init"-Funktion schreiben und die Testsub starten, bekommen wir korrekt die Fehlermeldung, daß die Textbox noch nicht initialisiert wurde und erst mit der Init-Funktion initialisiert werden muß. Sehr praktisch, so eine Eigenschaft, nicht wahr?

Die Init-Sub kann nun erweitert werden, so daß die Schriftart "Wingdings" als Default verwendet wird. Damit auch die Defaults leicht geändert werden können, wenn der Code mal sehr viel größer wird, ohne den Code nach Literalen durchsuchen zu müssen, verwenden wir wieder modulglobale Konstanten zu Beginn der Klasse:
Code:
Private Const cDefaultStateNULL     As String = "n"
Private Const cDefaultStateTrue     As String = "ü"
Private Const cDefaultStateFalse    As String = ""
Private Const cDefaultFont          As String = "Wingdings"
Hier auch gleich Konstanten für den Text, der in der Textbox für die drei Zustände angezeigt werden soll. Der Init-Code sieht nun so aus:
Code:
    Set TB = objTB
    With TB
        .FontName = cDefaultFont
    End With
    Me.TxtStateFalse = cDefaultStateFalse
    Me.TxtStateTrue = cDefaultStateTrue
    Me.TxtStateNULL = cDefaultStateNULL
(Das "With" wäre hier noch nicht nötig, aber da man später sicher noch mehr in der Textbox einstellt, ist das schon mal der Rumpf.)

Um aber die Klasse auch wieder einmal so flexibel wie möglich zu gestalten, lassen wir dem Verwender der Klasse die Wahl, ob er das automatische Einstellen des Textbox-Styles der Klasse überlassen will oder ob er seit Stunden mühsam an dem Outfit herumgedoktert hat und nicht möchte, daß es verändert wird. Also schnell eine kleine Option der Init-Prozedur hinzugefügt:
Code:
Public Sub Init(ByRef objTB As Access.TextBox, _
                Optional ByVal bolSetStyle As Boolean = True)
Wird die Option nicht angegeben, wird der Style automatisch eingestellt. Das muß im Code natürlich ergänzt werden, also:
Code:
    With TB
        If bolSetStyle Then
            .FontName = cDefaultFont
        End If
    End With
Die Eigenschaft "Value" unserer Klasse kann nun auch endlich angepaßt werden. In der "Class_Initialize" legen wir fest, daß der Startwert "Null" sein soll, wie es ja auch bei der normalen Checkbox der Fall ist, also einfach mit der Zeile "prv_varValue = Null".

"varValue" unserer Eigenschaft "Value" ist noch ein Problem: Der Wert kann nicht einfach in die Textbox geschrieben werden, denn diese soll ja nur drei Zustände anzeigen und nicht einfach irgendeinen Wert. Da ein Variant ziemlich viel sein kann, muß die Eigenschaft nun also genau prüfen, was da übergeben wird und nur die zulässigen Werte in einen passenden Zustand umrechnen, der wiederum in der Textbox dargestellt wird.

Das ist auch kein Hexenwerk, also sieht die Property für Value nun so aus:
Code:
Public Property Get Value() As Variant
    Value = prv_varValue
End Property
Public Property Let Value(varValue As Variant)
    If VarType(varValue) = vbBoolean Or IsNull(varValue) Then
        Select Case Nz(varValue, "")
          Case True
            TB.Value = Me.TxtStateTrue
          Case False
            TB.Value = Me.TxtStateFalse
          Case ""
            TB.Value = Me.TxtStateNULL
        End Select
        prv_varValue = varValue
      Else
        Err.Raise vbObjectError + 2, cMODULENAME & "->Value", "Wrong datatype, Value can only be True, False or NULL!"
    End If
End Property
Bei Get braucht man nichts zu machen, denn es ist ja bereits sichergestellt, daß prv_varValue von außen nicht geändert werden kann und nur durch die Let-Property gesetzt werden kann, die wiederum nun sicherstellt, daß es nur "True", "False" oder "NULL" sein kann, was in diese Variable gelangt, andernfalls wird ein Fehler ausgegeben.
Mit "VarType" kann man den Variablentyp einer Variablen testen, was für Variant sehr praktisch ist. Dann wird passend zum übergebenen Wert das Zeichen (oder der String), das in der jeweiligen "TxtState.."-Eigenschaft von uns oder dem Verwender festgelegt wurde, in der Textbox ausgegeben - fertig. Jetzt kann man im Formular probieren, im Load-Event nach der Initialisierung mit der .Value-Eigenschaft verschiedene Werte einzustellen und erhält in der künstlichen Checkbox das passende Aussehen. War gar nicht so schwer bisher, nicht wahr?

Das Aussehen und der Wert kann jetzt schon mal festgelegt werden, ebenso der Inhalt. Wenn im Form Load noch eine weitere Checkbox initialisiert werden soll, könnte das zum Beispiel so aussehen:
Code:
    Set prv_objCheckbox2 = New clsCTRL_Checkbox
    With prv_objCheckbox2
        .Init Me.txtMyCheckbox2
        .TxtStateFalse = "Nö"
        .TxtStateTrue = "Klaro"
        .TxtStateNULL = "Nix"
        .FontName = "Arial"
        .Value = Null
    End With
Die Checkbox zeigt nun statt einem Haken etc. die drei definierten Texte je nach Wert in der Value-Eigenschaft an. Man kann also schon jetzt ziemlich flexibel damit umgehen. Ganz zu schweigen davon, daß die Texte sich während der Laufzeit natürlich auch ändern könnten, wenn erforderlich.

Trotzdem ist die "Checkbox" bislang nur eine Textbox: Der User kann immer noch hineinklicken, den Text entfernen und etwas anderes hineinschreiben. Und bei Klick auf die Textbox wird auch nur der Cursor in das Feld gesetzt, mehr nicht. Eine Checkbox muß nun auch auf die passenden Vorgänge im Frontend reagieren, also brauchen wir nun Events.

Die Textbox hat ja schon eine Reihe brauchbarer Events, die wir nun "anzapfen". Dazu muß die Deklaration der Objektvariable ein wenig angepaßt werden:
Code:
Private WithEvents prv_ctlTextBox As Access.TextBox
Der kleine Zusatz "WithEvents" ermöglicht es der Objektvariablen, sich an die Events der Textbox "anzuhängen", soll heißen: Wenn die Textbox einen Event auslöst (weil der User etwa auf die Textbox geklickt hat), dann soll der Event auch an die Objektvariable übermittelt werden. Genau wie im Formular muß dazu natürlich auch eine Event-Sub geschrieben werden. Da wir hier kein Formular haben, um das einzustellen, muß es mit dem VBA-Editor gemacht werden: Einfach in der linken Kombobox über dem VBA-Editor nun die Objektvariable "prv_ctlTextBox" auswählen und sofort wird die erste Event-Sub von VBA automatisch eingefügt: "BeforeUpdate". Die brauchen wir im Moment nicht, also in der rechten Kombobox "Click" auswählen, mit dem der Zustand der Checkbox geändert werden soll.
Das alleine reicht nicht, genau wie in einem Formular muß für die Ereignisprozedur für Access-Controls noch "[Ereignisprozedur]" im Event eingetragen werden. Da wir aber kein Formular in einem Klassenmodul zur Verfügung haben, um diese Eigenschaft zu setzen, muß wieder VBA herhalten. In VBA funktioniert (zum Glück) nur die englische Variante, die da lautet "[Event Procedure]". Da man diese öfter braucht, lohnt wieder eine Konstante zu definieren, also wieder an den Anfang der Klasse:
Code:
Private Const cEVENTPROCEDURE       As String = "[Event Procedure]"
Der Job, die Eventprozeduren zu setzen, gehört natürlich zur Init-Sub, also dort hinzufügen:
Code:
        .OnClick = cEVENTPROCEDURE
(In den "With"-Block außerhalb des If-Konstruktes, denn das gehört ja nicht zum Style und muß immer gesetzt werden.)
Nun ist die Event-Prozedur "scharf" geschaltet und wird bei Klick auf die "Checkbox" aufgerufen. Damit auch etwas passiert, muß die Click-Prozedur nun den Wert zyklisch ändern:
Code:
Private Sub prv_ctlTextBox_Click()
    Select Case Nz(Me.Value, "")
      Case True
        Me.Value = False
      Case False
        Me.Value = Null
      Case ""
        Me.Value = True
    End Select
    TB.SelStart = 0
    TB.SelLength = 0
End Sub
Wie man sieht, ist auch das ganz einfach. Da wir passende Eigenschaften haben, die die "Schwerarbeit" übernehmen wie Test auf Vorhandensein der Textbox oder Auswahl der richtigen Zeichendarstellung und Zuweisung zur Textbox, müssen wir hier nichts mehr kompliziertes machen, also einfach die Werte True, False oder NULL zuweisen. Damit der Text nicht selektiert erscheint, setzen wir den Cursor vor das erste Zeichen und die Markierungslänge auf 0. Wenn man nun das Testformular startet, ohne daß dort der Code geändert werden mußte, funktioniert die kleine "Checkbox" schon ziemlich gut. Lediglich unschön, daß es nicht mehr funktioniert, wenn man etwas zu schnell klickt und einen Doppelklick auslöst. Da der Event nicht definiert wurde, passiert auch nichts. Also definieren wir auch den Doppelklick. Da bei diesem Event einfach gar nichts passieren soll, ist das Ergebnis sehr simpel:
Code:
Private Sub prv_ctlTextBox_DblClick(Cancel As Integer)
    prv_ctlTextBox_Click
    Cancel = True
End Sub
Die Maus-Events werden in der Reihenfolge "MouseDown"-"MouseUp"-"Click"-"DblClick" aufgerufen. Da bei "DblClick" also "Click" schon einmal durchgelaufen ist, lassen wir es hier mit dem Aufruf der Click-Sub nochmal laufen und setzen dann "Cancel=True", damit der systemseitige Effekt des Doubleclicks auf eine Textbox nicht ausgeführt wird. So sieht es dann nachher aus, als ob man einfach nur zweimal auf die Checkbox geklickt hätte.

Auch hier muß natürlich die "Scharfschaltung" in der Init-Sub erfolgen:
Code:
        .OnDblClick = cEVENTPROCEDURE
Und wo wir gerade dabei sind, die manuelle Änderung des Textfeldes durch den User schließen wir in der Init-Sub auch gleich aus:
Code:
        .Locked = True
Jetzt funktioniert die Checkbox schon ziemlich gut, lediglich der blinkende Cursor im Feld stört etwas. Der ließe sich durch das Ändern des Fokus auf ein anderes Control entfernen. Als Klassenprogrammierer wissen wir aber nichts von einem anderen Control oder wo die Klasse überhaupt eingesetzt wird, deswegen können wir das nicht einfach voraussetzen. Aber wir können dem Verwender wenigstens die Option anbieten, das selbst zu definieren, indem die Init-Sub einen weiteren optionalen Parameter für ein anderes Control erhält, wo wir den Cursor nach der Änderung "parken" dürfen. Das hat gleich den Vorteil, daß der Verwender diese Eigenschaft auch verwenden kann, um nach der Änderung den Fokus gleich auf die nächste "Checkbox" zu setzen oder ein anderes Eingabefeld.

Damit brauchen wir noch eine Objektvariable für das Control, das den Focus erhalten soll:
Code:
Private prv_ctlJumpTo As Access.Control
Da wir nicht wissen, was für ein Typ es ist, wird es allgemein als "Control" deklariert, was in etwa mit "Variant" für Controls gleichgesetzt werden kann. Die Init-Sub sieht nun vollständig so aus:
Code:
Public Sub Init(ByRef objTB As Access.TextBox, _
                Optional ByVal bolSetStyle As Boolean = True, _
                Optional ByRef ctlJumpToAfterUpdate As Access.Control)
    Set TB = objTB
    With TB
        If bolSetStyle Then
            .FontName = cDefaultFont
        End If
        .OnClick = cEVENTPROCEDURE
        .OnDblClick = cEVENTPROCEDURE
        .Locked = True
    End With
    Set prv_ctlJumpTo = ctlJumpToAfterUpdate
    Me.TxtStateFalse = cDefaultStateFalse
    Me.TxtStateTrue = cDefaultStateTrue
    Me.TxtStateNULL = cDefaultStateNULL
    Me.Value = prv_varValue
End Sub
Und im Click-Event erweitern wir diese Zeile nach den "Sel.."-Eigenschaften:
Code:
    If Not prv_ctlJumpTo Is Nothing Then prv_ctlJumpTo.SetFocus
Sollte hier ein Fehler auftreten, weil das Zielcontrol z.B. keinen Focus erhalten kann, soll es unsere Klasse nicht kümmern, die Fehlermeldung erhält der Verwender, der selbst dafür Sorge tragen muß, daß dieses den Fokus erhalten kann.

Damit auch die Möglichkeit besteht, per Leertaste die Checkbox zu ändern (wie bei der echten Checkbox) - schließlich gibt es auch User, die die Tastatur zur Eingabe verwenden - und damit beim Eintreten in das Feld der Text nicht selektiert erscheint, fügen wir noch zwei weitere Events ein:
Code:
Private Sub prv_ctlTextBox_Enter()
    TB.SelStart = 0
    TB.SelLength = 0
End Sub

Private Sub prv_ctlTextBox_KeyPress(KeyAscii As Integer)
    If KeyAscii = 32 Then prv_ctlTextBox_Click
End Sub
Und natürlich die bekannten Pendants in der Init-Sub.

Jetzt funktioniert die Checkbox bereits ziemlich gut wie die echte, nur daß unsere nun auch Conditional Formatting unterstützt, reichhaltig formatiert werden kann und mehr bietet als die echte Checkbox. Was noch fehlt, ist die Bindung an ein Datenfeld. Hier können wir nun nicht einfach die Textbox selbst an das Datenfeld binden, da dieses im Normalfall ein "Ja/Nein"-Feld sein wird und der Wert der Textbox ist in unserem Fall immer ein String. Man könnte sich jetzt eine aufwendige Bindung überlegen, wir machen es hier einfach und lassen den Verwender der Klasse eine echte Checkbox angeben, die die Bindung erhält und die im Formular später einfach ausgeblendet werden kann. Das ist zwar "doppelt gemoppelt", aber es ging hier ja um die Verbesserung der Darstellung und Funktion der Checkbox, und da man in VBA nicht ein neues Control nicht mit "New" erzeugen kann, muß der Verwender der Klasse diese Aufgabe im Design übernehmen.

Also wieder eine Klassenvariable:
Code:
Private WithEvents prv_ctlCheckBox As Access.CheckBox
Auch gleich mit "WithEvents" definiert. Im "Class_Terminate" setzen wir diese, wie auch die ctlJumpTo auf "Nothing" und erzeugen wieder eine private Eigenschaft für CB:
Code:
Private Property Get CB() As Access.CheckBox
    Set CB = prv_ctlCheckBox
End Property

Private Property Set CB(objCB As Access.CheckBox)
    Set prv_ctlCheckBox = objCB
End Property
Da die Checkbox nur optional angegeben werden soll, so daß der Verwender sowohl ungebundene wie auch gebundene Text-Checkboxen verwenden kann, gibt es hier keine Fehlerüberprüfung, diese muß in der Klasse selbst übernommen werden.

Die Init-Sub sieht jetzt so aus:
Code:
Public Sub Init(ByRef objTB As Access.TextBox, _
                Optional ByVal bolSetStyle As Boolean = True, _
                Optional ByRef objCB As Access.CheckBox, _
                Optional ByRef ctlJumpToAfterUpdate As Access.Control)
Und wird nach dem "With"-Block am Ende der Prozedur noch um diese Zeile ergänzt:
Code:
    If Not objCB Is Nothing Then Set CB = objCB
Wird also eine Checkbox übergeben, verwendet die Klasse diese für die Datenbindung und reicht den Wert aus prv_varValue an die echte Checkbox weiter, die wiederum über ihre Datenbindung den Wert an das Tabellenfeld weitergibt. Wann immer man nun auf "Value" zugreift, erhält man den Wert der echten Checkbox bzw. deren gebundenes Feld und umgekehrt schreibt man ihn auch wieder. Am RecordSelector kann man erkennen, das auch das Formular auf "Dirty" gesetzt wurde, es funktioniert also so, wie es soll.
Was noch nicht funktioniert, ist die Darstellung des Wertes bei Datensatzwechsel.

Hierzu wird der Form Current Event benötigt, und damit der Verwender der Klasse das nicht selbst machen muß, setzen wir eine Referenz auf das Formular, in dem sich die Checkbox befindet. Also als erstes eine Klassenvariable:
Code:
Private WithEvents prv_frmParent As Access.Form
Auch hier wieder mit "WithEvents", da ja der Form Current Event abgefangen werden soll.

In der Init-Sub weisen wir die Referenz zu, was kein Problem ist, da jedes Control über eine Parent-Eigenschaft verfügt, was immer ein Formular ist. Also wird in der Init-Sub nun ergänzt:
Code:
    Set prv_frmParent = TB.Parent
    With prv_frmParent
        If Not objCB Is Nothing Then .OnCurrent = cEVENTPROCEDURE
    End With
Da der Current-Event nur abgefangen werden muß, wenn eine Datenbindung besteht, wird hier zusätzlich noch abgefragt, ob die Standard-Checkbox mit übergeben wurde, ansonsten wird der Event nicht verwendet.

Passend dazu muß es natürlich auch eine Current-Event-Sub geben, die dann erst einmal so aussieht:
Code:
Private Sub prv_frmParent_Current()
    If Not CB Is Nothing Then
        Me.Value = CBool(CB.Value)
    End If
End Sub
Beim Ausprobieren zeigt sich aber ein Problem: Durch das Setzen des Wertes mit "Me.Value" wird ja auch in die echte Checkbox geschrieben, was damit eine Änderung des Datensatzes bewirkt. Das ist natürlich nicht gewollt, daher müssen wir die "Value"-Property ein wenig ändern. Es gäbe die Möglichkeit, dieser einen Parameter mitzugeben, aber da es eine öffentliche Eigenschaft ist, könnte das zu Verwirrungen beim Verwender führen, also nutzen wir einfach eine modulglobale Variable, die wir wieder am Anfang ergänzen:
Code:
Private bolSaveToCheckbox As Boolean
Damit kann die "Value"-Property nun ergänzt werden:
Code:
        If bolSaveToCheckbox Then
            If Not CB Is Nothing Then CB.Value = varValue
        End If
Im "Click"-Event kann man nun unterscheiden, ob es sich um eine gebundene oder ungebundene Checkbox handelt, wobei hier davon ausgegangen wird, daß ein Vorhandensein der echten Checkbox ein gebundenes Feld bedeutet, egal, ob dieses wirklich an ein Datenfeld gebunden ist oder nicht:
Code:
    bolSaveToCheckbox = (Not prv_ctlCheckBox Is Nothing)
Und weiter unten das Verhalten entsprechend steuern, da in einer gebundenen Checkbox Access selbst auch nur False und True erlaubt:
Code:
      Case False
        If bolSaveToCheckbox Then
            Me.Value = True
          Else
            Me.Value = Null
        End If
Der Form_Current-Event kann nun auch so angepaßt werden, daß der Wert beim Anzeigen nicht im Datenfeld geändert wird:
Code:
Private Sub prv_frmParent_Current()
    If Not CB Is Nothing Then
        bolSaveToCheckbox = False
        Me.Value = CBool(CB.Value)
        bolSaveToCheckbox = True
    End If
End Sub
Jetzt funktioniert die Checkbox bereits zuverlässig in einem Einzelformular. In einem Endlosformular kann man damit ebenfalls zuverlässig jeden Datensatz ändern - lediglich die Anzeige paßt nicht, da es eben ein ungebundenes Textfeld ist und somit wie jedes ungebundene Feld in einem Endlosformular in jeder Zeile den gleichen Wert anzeigt.

Also wie können wir das ungebundene Textfeld nun für ein Endlosformular tauglich machen? Das ist möglich, weil ein ungebundenes Control auch dann wie ein gebundenes verwendet werden kann, wenn es eine Formel enthält, die in jeder Zeile einen entsprechenden Wert ausgibt. Da hier der Wert für die Checkbox angezeigt werden soll und unsere bisherigen Einstellungen für die Anzeige des Wertes in der Checkbox ("TxtState...") weiterverwendet werden sollen, muß hier ein Hilfsmodul her, denn eine VBA-Funktion, die in einer Control-Formel verwendet werden soll, kann nur in einem Standardmodul angelegt werden, die Funktion muß Public deklariert werden.
Also erstellen wir uns eine Funktion, die den Wert als String zurückgibt, der String entspricht dann dem Text, der in der Textbox dargestellt werden soll (also das, was die drei "TxtState..."-Properties der Klasse als Text zurückgeben).

Fertig sieht die Funktion so aus:
Code:
Public Function fnCTRLCheckboxValue(frm As Access.Form, strControlName As String) As String
    Select Case Nz(frm.CTRLCheckbox(strControlName).Value, "")
      Case True
        fnCTRLCheckboxValue = frm.CTRLCheckbox(strControlName).TxtStateTrue
      Case False
        fnCTRLCheckboxValue = frm.CTRLCheckbox(strControlName).TxtStateFalse
      Case ""
        fnCTRLCheckboxValue = frm.CTRLCheckbox(strControlName).TxtStateNULL
    End Select
End Function
Die Parameter sind vor allem dazu da, um das Formular und das Checkbox-Objekt unterscheiden zu können, damit die Funktion universell mit jedem Formular und jeder eigenen Text-Checkbox funktioniert. Die Funktion kann so auf ein generisches "frm"-Objekt zugreifen. Damit die Funktion universell funktioniert, muß das Formular eine öffentliche Eigenschaft haben, die "CTRLCheckbox" heißt und anhand des übergebenen Namens das passende Klassenmodul-Objekt zurückliefert. Die Eigenschaft sieht so aus:
Code:
Public Property Get CTRLCheckbox(strName As String) As clsCTRL_Checkbox
    Select Case strName
      Case "Checkbox"
        Set CTRLCheckbox = prv_objCheckbox
      Case "Checkbox2"
        Set CTRLCheckbox = prv_objCheckbox2
    End Select
End Property
Um zu zeigen, daß der Name beliebig gewählt werden kann, wird hier einfach auf "Checkbox" und "Checkbox2" getestet und das jeweils passende Checkbox-Objekt zurückgeliefert. In der Praxis würde man wohl eher etwas sprechendere Bezeichungen verwenden, wie auch für die Objektvariablen.

Mit Hilfe dieser Property hat man sozusagen den gleichen Effekt, als wenn man mit "frm.Controls..." per String nach einem Control-Objekt sucht.
Die Funktion kann nun heraussuchen, welcher Textwert ("TxtState...") für den jeweiligen echten Wert verwendet werden soll und diesen an die Formel zurückliefern. Und schon ist unser Formular endlosfähig. Es fehlt nur noch ein kleiner Zusatz, der die Unterscheidung zwischen Endlos- und Einzelformular ermöglicht und dementsprechend entweder die Formel als ControlSource einstellt oder die direkte Wertänderung im Fall eines Einzelformulars zuläßt (die etwas performanter ist):

Die Init-Prozedur der Klasse bekommt nun einen zusätzlichen optionalen Parameter, mit dem für die Funktion "fnCTRLCheckboxValue" der Name festgelegt wird, der wiederum in der Property "CTRLCheckbox" verwendet wird, um das richtige Objekt zurückzuliefern. Damit sieht die Init-Prozedur nun so aus:
Code:
Public Sub Init(ByRef objTB As Access.TextBox, _
                Optional ByVal strObjName As String, _
                Optional ByVal bolSetStyle As Boolean = True, _
                Optional ByRef objCB As Access.Checkbox, _
                Optional ByRef ctlJumpToAfterUpdate As Access.Control)
    Set TB = objTB
    With TB
        If bolSetStyle Then
            .FontName = cDefaultFont
        End If
        .OnClick = cEVENTPROCEDURE
        .OnDblClick = cEVENTPROCEDURE
        .OnKeyPress = cEVENTPROCEDURE
        .OnEnter = cEVENTPROCEDURE
        .Locked = True
    End With
    Set prv_frmParent = TB.Parent
    With prv_frmParent
        If Not objCB Is Nothing Then .OnCurrent = cEVENTPROCEDURE
        If .DefaultView = 1 And strObjName <> "" Then
            prv_ctlTextBox.ControlSource = "=fnCTRLCheckboxValue([Form],""" & strObjName & """)"
          Else
            prv_ctlTextBox.ControlSource = ""
        End If
    End With
    Set prv_ctlJumpTo = ctlJumpToAfterUpdate
    Me.TxtStateFalse = cDefaultStateFalse
    Me.TxtStateTrue = cDefaultStateTrue
    Me.TxtStateNULL = cDefaultStateNULL
    Me.Value = prv_varValue
    If Not objCB Is Nothing Then Set CB = objCB
End Sub
Wird ein Objektname übergeben und die DefaultView ist 1 (=Endlosformular), dann wird die Formel z.B. so erstellt und in der ControlSource der Text-Checkbox abgelegt:
Code:
=fnCTRLCheckboxValue([Form],"Checkbox")
Etwas unbekannter ist, daß die Angabe von "[Form]" dem VBA "Me" entspricht, so daß man hier keinen konkreten Formularnamen verwenden muß, sondern eine generische Formular-Referenz an die Funktion übergeben kann.

Nun muß man noch den Let-Teil der Value-Eigenschaft in der Klasse anpassen, so daß diese Sub am Ende so aussieht:
Code:
Public Property Let Value(varValue As Variant)
    If VarType(varValue) = vbBoolean Or IsNull(varValue) Then
        If TB.ControlSource = "" Then
            Select Case Nz(varValue, "")
              Case True
                TB.Value = Me.TxtStateTrue
              Case False
                TB.Value = Me.TxtStateFalse
              Case ""
                TB.Value = Me.TxtStateNULL
            End Select
        End If
        prv_varValue = varValue
        If bolSaveToCheckbox Then
            If Not CB Is Nothing Then CB.Value = varValue
        End If
      Else
        Err.Raise vbObjectError + 2, cMODULENAME & "->Value", "Wrong datatype, Value can only be True, False or NULL!"
    End If
End Property
Hier wird einfach getestet, ob die Textbox eine ControlSource enthält oder nicht und entsprechend entweder der Text direkt geändert oder nicht.
Die ungebundene Checkbox bleibt weiterhin ungebunden und funktioniert im Endlosformular wie jedes andere ungebundene Control, die gebundene dagegen funktioniert nun auch im Endlosformular reibungslos (allerdings mit einer kleinen Verzögerung, die von Access selbst verursacht wird, bei jeder Formel). Unsere Checkbox ist in einem Endlosformular also etwas träger, funktioniert aber immerhin. In einem Einzelformular ist sie dagegen genauso schnell wie das Original, da hier nicht mit der Formel gearbeitet wird.

4. Verwendung von Events / Eigene Events
Nachdem unsere Checkbox nun fast so gut funktioniert wie eine echte, nur wesentlich freiere Gestaltung (wie etwa Conditional Formatting) erlaubt, können wir auch die Events wie bei einer echten Checkbox auswerten. Wie oben bereits gezeigt, genügt dazu einfach der Zusatz "WithEvens" bei der Objektvariablen, diesmal im Formular:

Also wird einfach im Formular die Variable so ergänzt:
Code:
Public WithEvents prv_objCheckbox As clsCTRL_Checkbox
Es fällt auf, daß nun in der linken Liste des VBA-Editors, in der die Objekte mit Events aufgelistet sind, die Objektvariable "prv_objCheckbox" nicht aufgeführt wird. Dabei haben wir eine Textbox, die genug Events zur Verfügung stellt. Was fehlt also?
Ganz einfach: Das Objekt, das hier mit "WithEvents" referenziert wurde, ist nicht die Textbox, sondern unsere eigene Klasse. Wenn diese also Events verwenden können soll, muß sie Events zur Verfügung stellen. Da es ein allgemeines Klassenmodul und kein echtes Control ist, hat die Klasse keinen Event und muß nun selbst welche erstellen. Hier hat man nun völlig freie Hand und kann die Events nennen, wie man will, ebenso beliebige Parameter übergeben.
Da wir erst mal einen Ersatz für die Standard-Checkbox erstellen wollen, bauen wir die Events so nach, wie die originale Checkbox sie anbietet, mit den gleichen Parametern, damit der Verwender auf bekannte Weise darauf zugreifen kann.

Zunächst mal müssen alle Events der Textbox, die der Verwender unserer Klasse auch verwenden können soll, weitergeroutet werden. Die Textbox löst bereits Events aus, die wir abfangen und einfach weiterleiten können. Das geht wie üblich: Den Text "[Event Procedure]" dem gewünschten Event zuweisen und dann eine Event-Prozedur schreiben, die das Routing übernimmt. Also genau wie oben. Wir haben bereits für diverse Events Eventprozeduren erstellt, etwa für den Click-Event, der die Wertänderung vornimmt. Hier muß also lediglich eine Zeile hinzu, die den Event nun an die Objektvariable des Verwenders aus dem Formular weiterleitet.

Dazu braucht es erst einmal einen Event, der ganz einfach so definiert werden kann (im Klassenmodul oberhalb der ersten Property, genauso wie modulglobale Variablen):
Code:
Public Event Click()
Das war schon alles, um einen Event zur Verfügung zu stellen. Ein Blick in den Formularcode, und oben in der Liste taucht nun auch die Variable "prv_objCheckbox" auf, bei Auswahl wird (weil der einzige vorhandene) nun die Click-Eventsub automatisch eingefügt - wie wir es ja auch von Formularen und Controls kennen.

Sobald dieser Event also ausgelöst wird, wird in allen Formularen, die eine solche "WithEvents"-Objektvariable auf dieses Klassenmodul verwenden, die zugehörige Eventsub "prv_objCheckbox_Click" ausgeführt.

Es fehlt also nur noch das Auslösen (also hier: Weiterrouten) des Click-Events (wir hätten hier beispielsweise ebensogut den Event "Mausklick" nennen können, dann würde eine Prozedur "prv_objCheckbox_Mausklick" eingefügt werden). Das Auslösen ist genauso einfach und geht mit einer einzigen Zeile in unserer bereits vorhandenen Click-Event-Prozedur in der Klasse:
Code:
RaiseEvent Click
Beim Schreiben bietet IntelliSense hier dann auch gleich alle vorhandenen Events an, was also hier "Click" ist.
Der Event wird also nun ausgelöst, man muß sich lediglich entscheiden, ob man das "RaiseEvent" vor dem sonstigen Click-Eventcode stellt oder nachher. Ebenso wäre eine Logik möglich, die den Event nur dann ausführt, wenn irgendeine Bedingung erfüllt ist. Zunächst aber mal die einfache Variante mit dem Raise-Befehl am Ende der Prozedur, damit die Wertänderung dann schon stattgefunden hat. Im Formularcode kann man nun die Eventprozedur einfügen und beispielsweise mal eine Messagebox ausgeben:
Code:
Private Sub prv_objCheckbox_Click()
    MsgBox "Klappt"
End Sub
Startet man das Formular und klickt auf die Checkbox, wird nun "Klappt" ausgegeben. So einfach kann man einen Event bauen.

Der Rest ist reine Fleißarbeit: Für jeden weiteren Event, den der Verwender auch verwenden soll, erstellt man eine passende Event-Prozedur, weist in der Init-Prozedur das "[Event Procedure]" zu, dann erstellt man einen gleichnamigen Event, der als "Public" deklariert sein muß und der dann noch ggf. eine Parameterliste erhält, wenn der Originalevent auch eine hat, dann wird in der Eventprozedur einfach ein passendes "RaiseEvent" hinzugefügt und die Parameterliste mit übergeben, also der Event quasi nur "durchgereicht". In der Demo habe ich mir das jetzt mal erspart, da könnt Ihr dann selbst mal dran basteln.
Wie gesagt, kann man hier natürlich auch eigene Events hinzubauen, die das originale Control überhaupt nicht kennt. Beispielsweise könnte man ein eigenes Textbox-Control bauen, das einen Event enthält, der auf einen bestimmten eingegebenen Text reagiert. Der Text, auf den reagiert werden soll, könnte beispielsweise in einer Init-Prozedur vom Verwender als Property der Klasse festgelegt werden und mit Public Properties nach Belieben angepaßt werden.

Das soll erst mal genügen als Tutorial. Das Prinzip dieses Controls kann man auch für beliebige andere Controls einsetzen, Controls können ebenso auch aus mehreren anderen zusammengesetzt und über so eine Klasse gemeinsam verwaltet werden, sofern dem Verwender klar ist, welche Controls benötigt werden (was man anhand von Parametern der Init-Prozedur schon ziemlich gut klarmachen kann, ansonsten aber auch über eine ausführliche Dokumentation der Klasse).

Mit solchen eigenen Controls kann man sehr elegant auch Controls erstellen, die beispielsweise automatisch eine bestimmte Formatierung ausgeben oder als Eingabe erwarten, um etwa Business Rules zu erfüllen. Ändert sich das Format im Unternehmen mal, muß man so nicht x Codes in x Formularen ändern, sondern man ändert einfach die Formatierung in der Klasse, schon funktioniert es in allen Formularen wie gewünscht.

Viel Spaß beim Experimentieren und Advents-Control-Basteln...Wink

Christian



MyControls.zip
 Beschreibung:
Textbox als Checkbox mit eigener Klasse, Demo zu "Eigene Controls entwickeln" im Format A2000-2003

Download
 Dateiname:  MyControls.zip
 Dateigröße:  78.07 KB
 Heruntergeladen:  89 mal

Neues Thema eröffnen   Neue Antwort erstellen Alle Zeiten sind
GMT + 1 Stunde

Diese Seite Freunden empfehlen

Seite 1 von 1
Gehe zu:  
Du kannst Beiträge in dieses Forum schreiben.
Du kannst auf Beiträge in diesem Forum antworten.
Du kannst deine Beiträge in diesem Forum nicht bearbeiten.
Du kannst deine Beiträge in diesem Forum nicht löschen.
Du kannst an Umfragen in diesem Forum nicht mitmachen.
Du kannst Dateien in diesem Forum nicht posten
Du kannst Dateien in diesem Forum herunterladen

Verwandte Themen
Forum / Themen   Antworten   Autor   Aufrufe   Letzter Beitrag 
Keine neuen Beiträge Access Formulare: Ansprechpartner des Kunden in eigene Tabelle auslagern 1 crack24 81 15. März 2014, 01:12
Gast150313 Ansprechpartner des Kunden in eigene Tabelle auslagern
Keine neuen Beiträge Access Formulare: Start Form öffnet zu schnell für Controls 4 Thomas30 92 06. Jun 2013, 10:04
Thomas30 Start Form öffnet zu schnell für Controls
Keine neuen Beiträge Access Formulare: Eigene Meldung bei Aktualisrungsabfrage 5 ms67 100 11. Apr 2013, 07:55
Willi Wipp Eigene Meldung bei Aktualisrungsabfrage
Keine neuen Beiträge Access Formulare: Listenfeld mit Bezug aufs "eigene" Formular 6 dropdown_2404 394 03. Okt 2010, 21:04
dropdown_2404 Listenfeld mit Bezug aufs "eigene" Formular
Keine neuen Beiträge Access Formulare: Kontextmenü mehreren Controls zuweisen. 2 just.do.it 485 21. Sep 2010, 06:50
just.do.it Kontextmenü mehreren Controls zuweisen.
Keine neuen Beiträge Access Formulare: Eigene Gültigkeitsregel wird Ignoriert? 1 Lenny B. 900 12. Sep 2010, 12:30
Der_Andi1984 Eigene Gültigkeitsregel wird Ignoriert?
Keine neuen Beiträge Access Formulare: per Mausklick Ordner aus Eigene Dateien öffnen 12 dijor70 796 25. Jun 2010, 10:25
Gast per Mausklick Ordner aus Eigene Dateien öffnen
Keine neuen Beiträge Access Tabellen & Abfragen: Hyperlink-Eigenschaft des Image Controls 1 Thomas aus München 501 02. Mai 2010, 13:57
Gast Hyperlink-Eigenschaft des Image Controls
Keine neuen Beiträge Access Formulare: Access nur über eigene Schaltfläche beenden 1 M. Seidemann 401 04. Aug 2009, 14:20
M. Seidemann Access nur über eigene Schaltfläche beenden
Keine neuen Beiträge Access Formulare: eigene Farbe in bedingter Formatierung definieren 4 macxmanGast 1508 02. Apr 2009, 16:54
JörgG eigene Farbe in bedingter Formatierung definieren
Keine neuen Beiträge Access Berichte: ACCESS Eigene Berichtsvorlage hinsichtlich des Designs 5 miketec 2203 22. Sep 2008, 09:42
Marmeladenglas ACCESS Eigene Berichtsvorlage hinsichtlich des Designs
Keine neuen Beiträge Access Tabellen & Abfragen: 'Controls' in einer Abfrage verwenden 13 mordor 484 04. Jul 2008, 13:58
MAPWARE 'Controls' in einer Abfrage verwenden
 

----> Diese Seite Freunden empfehlen <------ Impressum - Besuchen Sie auch: Word VBA