Sub Form_Open (Cancel As Integer)
If Not IsNull(Me.Openargs) Then
Me.Recordsetclone.FindFirst "ID='" + Me.Openargs + "'"
s$ = Me.Recordsetclone.Bookmark
Me.Bookmark = s$
End If
End Sub
Problem 3:'DoCmd OpenForm ,,,,,, SQL-String'übergeben werden. In FormOpen des aufgerufenen Formulares wird dies dann aufbewährte Weise abgecheckt. Das Formular erhält und behält nun die Datensatzmenge '1 von 1 Datensätze'. Beim Vorwärtsbewegen ist zwar noch ein verunsicherndes Flackern im Formular zu sehen, so als ob ACCESS es doch noch verbocken möchte, aber der Fehler tritt nicht mehr auf. Diesem Problem könnte man auch über ein Ereignis-gebundenes Wechseln der DefaultEditing-Eigenschaft begegnen ( je nach
DoCmd OpenForm Nameerfolgt, wird z.Bsp. diese Eigenschaft überschrieben.
DoCmd OpenForm Name , , , , A_READONLYsetzt die sowieso vergebene Eigenschaft von Anfang an auf den richtigen Wert. Bei näherem Hinsehen erweisen sich solche Sachen einfach nur als Scheiße. Ein einfaches 'DoCmd OpenForm' müßte ein Formular mit den gespeicherten Standard-Einstellungen öffnen, so, wie wenn es aus dem Datenbankfenster heraus geöffnet worden wäre.
For i% = 0 To Forms.Count - 2
Forms(i%).Visible = False
Next i%
Das kommt auch Ergonomie-mäßig gut, da die Augen des Anwenders lediglich das kleine Formular ohne umliegende Informationen verarbeiten muß.
Beim Schließen dieses Formulares können die unsichtbaren Fenster wieder angezeigt werden. Sollten sich darunter Popup- oder Modal-Fenster befinden, die durch nachfolgende, normale Formulare bereits ausgeblendet wurden, werden diese beim Einblenden übergangen, da sie, wenn die Kette der Fenster vom Anwender wieder geschlossen wird, (hoffentlich) wieder eingeblendet werden, ansonsten würde ein wiedereingeblendetes Modal-Formular die Berabeitung eines folgenden, normalen nicht verhindern.
' Alle Formulare, die weder Popup, noch modal sind,
' wieder einblenden.
For i% = 0 To Forms.Count - 2
If Forms(i%).Popup = False And Forms(i%).Modal = False Then
Forms(i%).Visible = True
End If
Next i%
Sub Form_Current ()
If iLastView = 88 Then iLastView = Me.CurrentView
If iLastView <> Me.CurrentView Then
If Me.CurrentView = 2 Then ' Datenblatt
iLastView = 88
DoCmd Maximize
Me.AllowEditing = False
Me.DefaultEditing = 3
Else ' Formular
iLastView = 88
DoCmd Restore
Me.AllowEditing = True
Me.DefaultEditing = 2
End If
iLastView = Me.CurrentView
End If
End Sub
Der "Umweg" mit der 88er-Zuweisung für die Variable iLastView muß gemacht werden, weil alleine die Änderung von AllowEditing ein Neu-Darstellen des Fenster bewirkt und deswegen dieser Code-Teil erneut ausgeführt wird, was nach wenigen Malen zu einem Überlaufen eines internen Stapelspeichers führt.
Klar, daß auch das nicht ohnr Ärger von statten geht, wenn das Formular keine Datensätze enthält. In der Datenblattansicht werden in diesem Fall gar keine Datensätze angezeigt und der Wechsel zur Formularansicht stellt nur ein voll leeres Formular dar, da mangels Datensätze auch kein Form_Current eintritt. Sämtliche Versuche, daran mittels Makro-Aufruf in einem der Ereignisse Laden, Öffnen, Anzeigen etwas zu drehen, schlugen fehl.
Die einzige Möglichkeit scheint darin zu bestehen, nach der Codezeile, in der das Formular geöffnet wurde, folgendes einzufügen:
If Forms("Ventile_Filter").RecordsetClone.RecordCount = 0 Then
DoCmd RunMacro "Menübefehle.Ansicht_Formular"
End If
DoCmd OpenForm "UF_Projekte" DoCmd DoMenuItem A_FORMBAR, 2, 2, , A_MENU_VER20Um dieses Datenblatt in einer bestimmten Größe erscheinen zu lassen (z.B. den ganzen Arbeitsbereich ausfüllend, rahmenlos, oder mit dünnem Rahmen, um die Titelleiste erscheinen zu lassen, die ansonsten nur im Vollbildmodus in der
' **** widerspenstige Spalten auf altmodischste Weise einstellen ****
Forms!Webalbum!Projekt.SetFocus
SendKeys "%tb%a", True
Forms!Webalbum!Arbeitsverzeichnis.SetFocus
SendKeys "%tb0{ENTER}", True
Forms!Webalbum!Keywords.SetFocus
SendKeys "%tb55{ENTER}", True
Forms!Webalbum!Projekt.SetFocus
%a steht für Anpassen, 0 für Ausblenden und numerische Angaben für eine gewünschte Breite.
KeyAscii = Asc(UCase(Chr$(KeyAscii)))
Durch Zuweisen eines Format-Strings können z.B. Einheiten angezeigt werden: <0,00" DM"> zeigt bei einer Eingabe von <2> folgendes an: <2,00 DM>. Das erscheint auf den ersten Blick toll. Versucht man aber in einem Formular verschiedene Werte so unterzubringen, daß die Zahlen bündig untereinander stehen, kann dies an verschieden langen Einheiten-Bezeichnung scheitern. In diesem Fall erhalten die Steuerelemente max. Zahlenformat-Anweisungen mit derDEUTSCH. FORMAT FORMAT BASIC Standarddatum General Date Datum, lang Long Date Datum, mittel Medium Date Datum, kurz Short Date Zeit, lang Long Time Zeit, 12Std Medium Time Zeit, 24Std Short Time Allgemeine Zahl General Number Währung #,##0.00" DM";-#,##0.00" DM" Festkommazahl Fixed Standardzahl Standard Prozentzahl Percent Exponentialzahl Scientific Wahr/Falsch True/False Ja/Nein Yes/No An/Aus On/OffMan beachte Zeit, 12Std - Medium Time und Zeit, 24Std - Short Time !
If OBJSTATE_OPEN = SysCmd(SYSCMD_GETOBJECTSTATE, A_FORM, Fenster) Then
In Datenformularen kann es allerdings vorkommen, daß der Anwender in der Datenblattansicht Spaltenbreiten verändert und das Formular den Status OBJSTATE_DIRTY erhält. Deswegen eine WorkAround-Erweiterung und die Sache ist perfekt. Das mußte aber im September gleich nochmal überarbeitet werden, da sich im Hause Klumpp - SEL ein bisher unbekannter und natürlich auch nicht dokumentierter OBJSTATE=3 ergab.
Das tauchte dann später auch selbstverständlich unter Access97 auf (dort als acObjState = 3 (in der webmanag-Anwendung, wenn das gebundene Formular "Aktueller_Besitzer" über dem Formular "Menue" steht).
ABER: Gleich beim Nachziehen älteren Codes ging das prompt mehrfach schief, da IsFormOpen den WIRKLICHEN Datenbank-Objekt-Namen checkt, während die Mutterfenster- und ExistsForm-Funktionen auf den Caption-String Bezug nehmen, der auch in der Liste geöffneter Fenster erscheint.
Function IsFormOpen (Fenster As String) As Integer
Dim V As Variant
Const FORM_DIRTY = 3 ' nicht dokumentierter OBJSTATE=3, wenn z.B.
' die Spaltenbreiten in der Datenblattansicht
' geändert wurden.
V = SysCmd(SYSCMD_GETOBJECTSTATE, A_FORM, Fenster)
If V = OBJSTATE_OPEN Or V = OBJSTATE_DIRTY Or V = FORM_DIRTY Then
IsFormOpen = True
End If
End Function
Eine vereinfachte Version, die auch kombinierte Werte, die Bitmasken bilden,
Function IsFormOpen(Fenster As String) As Integer
If SysCmd(acSysCmdGetObjectState, acForm, Fenster) > 0 Then
IsFormOpen = True
End If
End Function
Der häufiger eintretende Fall, daß das Vorhandensein von Fenstern geprüft werden muß, um diese anschließend ein- oder auszublenden, kann in folgender Routine gekapselt werden:
Sub Form_Visible (sFormName As String, iVisible As Integer)
If IsFormOpen(sFormName) = True Then
Forms(sFormName).Visible = iVisible
End If
End Sub
Interessant ist, daß SYSCMD_GETOBJECTSTATE für alle Arten von darstellbaren (und geöffneten) Access-Objekten angewendet werden kann:
A_TABLE A_QUERY A_FORM A_REPORT A_MACRO A_MODULEund außerdem noch die Zustände Neu und geändert feststellen kann:
OBJSTATE_OPEN Geöffnet OBJSTATE_NEW Neu OBJSTATE_DIRTY GeändertABER es muß auch gesagt werden, daß es so aussieht, daß OBJSTATE_OPEN auch benutzt werden kann, um das Vorhandensein eines Access-Objektes zu prüfen, das zwar in der Datenbank vorhanden, aber NICHT geöffnet ist. Das sah schon beinah positiv aus, aber eigentlich ein Unsinn und obendrein 'tut' es nur bei Tabellen, wenn im 'normalen' Access das Datenbankfenster geöffnet ist. Wieder mal DRECK !, zur Lösung siehe auch ExistsObject. Interessant sind in diesem Zusamenhang auch CurrentObjectType und CurrentObjectName. Obendrein konnte beim Durchforsten der in den Access-Libraries verborgenen Routinen ein undokumentierter SYSCMD-Schalter SYSCMD_CLEARHELPTOPIC = 11 entdeckt werden, der z.B. in der zweiten, nachgelagerten GetFileName-Funktion
unused = SysCmd(SYSCMD_CLEARHELPTOPIC)
Der Name des Return-Longs 'unused' sagt eigentlich alles und wofür das gut ist, weiß keiner.
Private Sub datum_GotFocus()
On Error Resume Next
Me!Datum.SelStart = 0
Me!Datum.SelLength = 1
End Sub
Ich habe das dann doch nochmal im Access 2.0 getestet und in der Tat greift diese Lösung dort nicht ganz.
Private Sub datum_GotFocus()
On Error Resume Next
SendKeys "{F2}", True
Me!Datum.SelStart = 0
Me!Datum.SelLength = 1
End Sub
Und hier zu den Altlasten:
Speziell bei Memo- und Langtextfeldern ist das Standardverhalten von MS-Access 2.0, den gesamten Feldinhalt zu markieren und damit im Navigationsmodus zu bleiben, ungünstig. Bisherige Versuche, dies mit einem beherzten SendKeys "{F2}" zu umgehen, führte abhängig davon, ob mit der Maus oder der Tastatur gearbeitet wurde, zu unterschiedlichen und fehlerhaften Verhaltensweisen. Eine tragbare Lösung schien mit SelLength und SelStart in Reichweite zu liegen, da ein Hineinklicken mit der Maus sogar den Cursor an die richtige Position springen läßt und sich die SendKeys-Geschichten vermeiden lassen. Dummerweise befindet sich Access beim Feld-Wechseln per Tastatur erst mal grundsätzlich im Navigationsmodus, und selbst wenn der Cursor nach einer Sel...-Anweisung uns an der ersten oder letzten Stelle freundlich anblickt, können wir uns nicht im Feld bewegen, da der Navigationsmodus immer noch aktiv ist.
Sub Info_Enter ()
SendKeys "{F2}", True
Me!Info.SelLength = 0
Me!Info.SelStart = 0
End Sub
Der häufigere, weil sinnvollere Fall dürfte wohl das Setzen des Cursors an das Ende eines Memo-Feldes sein. Zum einen vermeidet man die potentielle Gefahr, einen umfangreichen und defaultmäßig markierten Memotext in der Hektik komplett zu überschreiben und zum anderen kann man am Ende direkt weitertippen.
Der Aufruf Cursor_am_Ende erledigt das allerdings auch mit einer der manchmal eher zweifelhaften SendKeys-Aktionen, um den Pfeiltasten-kompatiblen Editiermodus zu erreichen:
Sub Cursor_am_Ende ()
On Error Resume Next
SendKeys "{F2}", True
Screen.ActiveControl.SelLength = 0
Screen.ActiveControl.SelStart = Len(Screen.ActiveControl)
End Sub
Vergleichbar ist auch noch eine Version, die ich aus Langeweile im Hause SEL angefertigt habe:
Sub Focus2EOC (C As Control)
' **** Alternative zur ewig nervigen SendKeys-Scheisse ****
' und das allerallergeilste an dieser Lösung ist, daß das Mausverhalten
' - Klicken auf Caption -> kompletter Feldinhalt markiert
' - Klicken in Text -> Fokus an Klickstelle
' stimmig ist und beim Betreten des Elementes per Tastatur der Fokus OHNE
' Geistereffekte zuverlässig an das Ende des Elementes gesetzt wird.
C.SetFocus
If Not IsNull(C) Then
C.SelStart = Len(C)
C.SelLength = 0
End If
End Sub
Diese hilfreichen Routinen geraten immer wieder an das Problem, das abhängig von irgendwelchen Zeitscheiben-Ereignis-etc-Scheißgeschichten nicht unbedingt zuverlässig davon ausgegangen werden kann, ob gerade z.B. der Navigationsmodus aktiv ist oder nicht.
Selbst Cursor-Contraprogrammierungen, die durchaus erfolgreich den einen oder anderen Modus erkennen und gewollte Zustände herstellen, scheitern u.U. daran, ob ein Formular mit angezeigtem oder ausgeblendetem Datenbankfenster geöffnet
Sub Form_Activate ()
DoCmd SelectObject A_FORM, Me.Name
End Sub
und nun die Ereignisroutinen:
Sub BNr_GotFocus ()
BoschFeld_GotFocus Me!BNr, sBNr_Def
End Sub
Sub BNr_MouseUp (Button As Integer, Shift As Integer, X As Single, Y As Single)
BoschFeld_MouseUp Me!BNr
End Sub
und für Navigationsfetischisten, die auf <Pfeil auf/ab> angewiesen sind:
Sub BNr_KeyDown (KeyCode As Integer, Shift As Integer)
NavArrowKeysUpDown KeyCode
End Sub
jetzt noch die aufgerufenen Routinen:
Sub BoschFeld_MouseUp (C As Control)
On Error GoTo err_BoschFeld_MouseUp
Dim s As String
If C.SelLength > 1 Then
SendKeys "{F2}"
End If
s = "{RIGHT " & BoschFeld_GetSelStart(C.InputMask, C, True) & "}"
C.SelStart = 1
SendKeys s
Exit Sub
err_BoschFeld_MouseUp:
Fehler "BoschFeld_MouseUp"
Exit Sub
End Sub
und
Sub BoschFeld_GotFocus (C As Control, sDefault As String)
On Error GoTo err_BoschFeld_GotFocus
Dim s As String
If Screen.ActiveForm.Name = "Start" Then
C = sDefault ' Default zuweisen
End If
s = ClearStringFrom(sDefault, """") ' entferne enthaltene Anführungszeichen
SendKeys "{F2}", True
C.SelStart = BoschFeld_GetSelStart(C.InputMask, s, False)
C.SelLength = 1
Exit Sub
err_BoschFeld_GotFocus:
Fehler "BoschFeld_GotFocus"
Exit Sub
End Sub
sowie
Private Function BoschFeld_GetSelStart (vInputMask As Variant, vDef As Variant, iMouse As Integer) As Integer
On Error GoTo err_BoschFeld_GetSelStart
Dim i As Integer
' **** hardcodierte Interpretation ****
' **** der Bosch-Nummerdarstellung ****
' **** Regel: ">A\ 000\ 000\ 000" ****
If IsNull(vDef) Then
i = 0
Else
i = Len(vDef)
End If
If iMouse = True Then
BoschFeld_GetSelStart = i
Exit Function
End If
If IsNull(vInputMask) Then
' keine Literal-Zeichen enthalten, nichts überspringen
Else
Select Case i
Case Is >= 7: i = i + 3
Case Is >= 4: i = i + 2
Case Is >= 1: i = i + 1
End Select
End If
BoschFeld_GetSelStart = i
Exit Function
err_BoschFeld_GetSelStart:
Fehler "BoschFeld_GetSelStart"
Exit Function
End Function
und schließlich
Sub NavArrowKeysUpDown (iKeyCode As Integer)
On Error Resume Next
If iKeyCode = 38 Then
SendKeys "+{TAB}"
ElseIf iKeyCode = 40 Then
SendKeys "{TAB}"
End If
End Sub
Man beachte bei der MouseUp-NavArrowKeysUpDown-"Lösung" den weggelassenen Pause-Parameter einer peinlichen SendKeys-Aktion.
* * * *
Teilweise kann es sogar zu SCHWEREN Fehlern kommen:
Sub Set_Cursor (c As Control, vWert As Variant, iPause As Integer)
On Error GoTo err_Set_Cursor
' **** setze im Mastermodus Cursor ****
Dim i As Integer, j As Integer, s As String
If Not IsNull(vWert) Then
s = "{F2}"
c.SetFocus
If InStr(vWert, """") Then ' bei entahltenen Anführungszeichen
j = Len(vWert) - 2 ' würde der Cursor zu weit nach hinten
Else ' gesetzt werden.
j = Len(vWert)
End If
For i = 1 To j
s = s & "{Right}"
Next i
SendKeys s, iPause
Else
Beep
MsgBox "Ungültiger Wert NULL für Parameter vWert !", 16, "Set_Cursor"
End If
Exit Sub
err_Set_Cursor:
Fehler "Set_Cursor"
Exit Sub
End Sub
' Der Bezug auf Screen.ActiveControl.Name erzeugt einen
' gewollten Fehler, der so extra behandelt werden kann.
x% = IsNull(Screen.ActiveControl.Name)
Exit Sub
err_Form_Vollbild_Unload:
If Err = 2474 Then
' fokussiere das erste Steuerelement in der Auflistung
Me(0).SetFocus
End if
Man kann es sich auch etwas einfacher machen, indem man beim Schließen gebundener Formulare einfach eine ExistsForm-abhängige SetFocus-Anweisung ausführt.
Zuweilen kommt es sogar (als ob es mich unerwartet treffen würde) vor, daß ein SetFocus nicht ausricht. Ein besonders hartnäckiger Fall ergab sich im LaserJob-Manager, bei dem von einem Auftragsformular aus ein gebundenes Druck-Menü geöffnet werden kann, daß bei 'Ok' sich selbst schließt und den entsprechenden Druckvorgang als Preview anzeigt. Nach Schließen des Previewfensters wollte der Focus auch per SetFocus nicht zum Objekt der Begierde gelangen. Ein möglicher Grund für die besondere Hartnäckigkeit besteht u.U. darin, daß das Auftragsformular ein Popup-Status-Formular mit sich führt. Erst eine zentrale Funktion, die beim Schließen aller auftragsbezogenen Berichte aufgerufen wird, konnte Abhilfe schaffen:
Sub ActivateForm (FName As String)
If ExistsForm(FName) = True Then
DoCmd SelectObject A_FORM, FName
Forms(FName).SetFocus
End If
End Sub
Genau dieses Problem trat dann nochmal auf, und konnte witzigerweise durch ZWEIMALIGES Einfügen einer SetFocus-Anweisung behoben werden !
Aber das Bemühen einer eigenen ActivateForm-Routine sieht dann doch ein bißchen professioneller aus. Beide Lösungen scheinen auch mit dem Problem verloren gegangener Statuszeilen klarzukommen.
Beim Beenden eines Formulares mit einer msgbox-Abfrage a'la Access beenden / Anwendung schließen / Zurück löst die Aktion Zurück ebenfalls den Fokusverlust-Fehler aus, ohne daß dem beizukommen wäre.
Aber so kann's ja nicht weitergehen:
Zum zuletzt beschrieben Problem:
Die klassische 'Abbrechen'-Rückkehr aus der 'Anwendung beenden'-msgbox hat z.B. in pb_Ende_Click wie folgt auszusehen:
Sub pb_Ende_Click ()
On Error GoTo err_Ende
DoCmd Close A_FORM, Me.Name
Exit Sub
err_Ende:
Me!pb_Ende.SetFocus
Exit Sub
End Sub
Das Problem des Zusammenspiels meiner gebundenen Menü-Formulare kann entweder dadurch gelöst werden, daß diese Formulare entbunden werden (alles Windows, oder was?), oder mit folgender Vorgehensweise:
In der Control_Click-Sub steht:
Me.Visible = False
DoCmd OpenForm "Import_Export"
und zwar unabhängig davon, ob ein weiteres, gebundenes Menüformular oder ein Datenformular geöffnet werden soll. Eine Form_Open-Routine des zu öffnenden Formulares ist nicht mehr notwendig. Bei Form_Close der geöffneten Formulare steht dann das bewährte:
If ExistsForm("Haupt") Then Forms!Haupt.SetFocus
* * * *
FOKUS auf ein bestimmtes Anwendungsfenster setzen und dieses nach vorne stellen:
Declare Function SetForegroundWindow Lib "user32.dll" (ByVal hwnd As Long) As Long* * * * SETFOCUS / GOTOCONTROL / SELECTOBJECT: Mit SetFocus hat's schon manche Probleme gegeben. Mit GotoControl kann man zumindest Felder in auf dem Desktop geöffneten Tabellen fokussieren, nachdem diese mit einem herzhaften SelectObject bestimmt wurde. Unterschiede sind laut Dokumentation nicht erkennbar, aber dafür jetzt wieder was aus dem Gruselkabinett: Ein ein Kalenderobjekt anzeigendes Popup-Formular schreibt bei einem Doppelklick den Datumswert in ein dahinterliegendes Textfeld, um per Menübefehl 'Kopieren' das Datum in die Zwischenablage zu übernehmen. Wenn das nicht geklappt hätte, weil das Formular ein Popup ist, hätte ich das noch verstanden, daß das aber immer nur schief geht, wenn ich in dmt.mdb das Termine-Formular über ToDo öffne, wollte mir nicht in den Sinn. Fast sah es so aus, als ob das mit einem auferlegten Filterkriterium zusammenhing, aber selbst da gab es vereinzelt Ausnahmen. Eine Lösung (?) konnte in der folgenden, für mich unsinnigen Anweisungskombination gefunden werden: Nachdem der Anwender (tragischer Weise ich selbst) auf ein Datum doppelklickt und im Code der Wert an das Textfeld übergeben wurde, werden folgende Zeile ausgeführt:
Me!ClipDate.SetFocus
DoCmd SelectObject A_FORM, Me.Name
Erst das spätere Einfügen der SelectObject-Anweisung scheint den Fehler der Kopieren-Menü-Anweisung bei aktivem Formularfilter behoben zu haben. Ein testweises Deaktivieren der SetFocus-Anweisung ließ den Fehler zwar nicht mehr auftreten, führte aber zu einer scheinbaren Deaktivierung des Festplattencache's. Vielleicht sollte ich ab jetzt Anweisungen per Zufallsgenerator im Code verteilen, dann klappt's auch mit der Software.
Mehr zum Thema Daten in Zwischenablage übernehmen s.a. DataToClipboard().
Sub Fit2Res (Fenster As Variant, Modus As Variant)
On Error GoTo err_Fit2Res
Dim iH As Integer, iW As Integer, F As Form
Select Case Modus
Case "640 * 480": iH = 6350: iW = 9580
Case "800 * 600": iH = 8150: iW = 12000
Case "1024 * 768": iH = 10660: iW = 15300
Case "1280 * 1024": iH = 14500: iW = 19140
Case "1600 * 1200": iH = 17140: iW = 24000
Case Else: Beep
MsgBox "Ungültiger Parameter Modus='" & Modus & "' !", 16, "Fit2Res"
Exit Sub
End Select
Set F = Forms(Fenster)
DoCmd SelectObject A_FORM, Fenster
DoCmd MoveSize 0, 0, iW, iH
F.Section(0).Height = iH
F!Object.Width = iW
F!Object.Height = iH
Exit Sub
err_Fit2Res:
Fehler "Fit2Res"
Exit Sub
End Sub
Nun zum wahren Objekt der Begierde:
Nachdem ein erneuter Blick ins Win31-SDK (zum tausendsten Mal) Unerhofftes zum Thema Auflösung (bisher krampfhafte, grafikkarten- und syntaxbezogene Auswertung der system.ini) zum Vorschein brachte, sieht alles anders aus (diesmal besser) und scheint folgende Möglichkeiten zu bieten:
- Vollbildformulare, die per API die auflösungsbezogenen Maße des Desktops auslesen und deswegen bei jeder Auflösung als Vollbild dargestellt werden können, und deren zentrales Steuerelement (Memo oder OLE-Grafik) anschließend Access-intern an die Größe des aufgeblasenen Formulares angepasst wird.
- klassische Stammdaten-Pflegeformulare, die zum einen ihre Größe je nach Formular- oder Datenblattdarstellung getrennt einstellen können und sich per API-Desktop-Maße bei jeder aktuellen Auflösung zentrieren. Das muß dann leider jedesmal im ereignis-kritischen Form_Current abgecheckt werden, kann aber per formularglobalem Flag auf reine CurrentView-Wechsel begrenzt werden, bei dem eine Zentrierroutine aufgerufen wird. Diese Routine berechnet ausgemittelte Koordinaten für das übergebene Formular (gilt absolut innerhalb der Parent-Anwendung, z.B. MS-Access im Vollbildmodus) und plaziert es gemäß der übergebenen Maße. Es erfolgt eine Korrektur um 30 Pixel (Anwendungstitel-und Menüleiste) sowie eine leichte, psychologisch geschmackvolle Korrektur nach oben. Bei übergebenen 0-Maßen kann sogar ein Currentview- und Auflösungs-bezogenes Aufblasen zum Quasi-Vollbild erfolgen (z.B. ein kleine Maske, die als Liste aber den gesamten Schirm nutzen sollte).
Den erfreulichen Abschuss des vielzitierten Vogels kann ich mit Blick auf die Mertlik-dat013el.mdb vermelden, in der die beschriebenen Features schnuckeligst realisiert wurden. Das Formular Zoom_Objekt enthält ein Objekt-Feld, in dem ein
Type RECT ' Datenstruktur für die
Left As Integer ' Eckpunkte eines Fensters
Top As Integer ' -> GetWindowRect
Right As Integer
Bottom As Integer
End Type
Declare Function GetDesktopHwnd Lib "User" () As Integer
Declare Sub GetWindowRect Lib "User" (ByVal hWnd As Integer, lpRect As RECT)
Declare Sub SetWindowPos Lib "User" (ByVal hWnd As Integer, ByVal hWndInsertAfter As Integer, ByVal X As Integer, ByVal Y As Integer, ByVal cx As Integer, ByVal cy As Integer, ByVal wFlags As Integer)
Sub GetResolution (x As Integer, y As Integer)
On Error GoTo err_GetResolution
Dim tRS As RECT
GetWindowRect GetDesktopHwnd(), tRS
x = tRS.Right - tRS.Left
y = tRS.Bottom - tRS.Top
Exit Sub
err_GetResolution:
Fehler "GetResolution"
Exit Sub
End Sub
Und hier das Objekt der Begierde:
Sub CenterForm (F As Form, WF As Integer, HF As Integer, WD As Integer, HD As Integer)
On Error GoTo err_CenterForm
Dim xDT As Integer, yDT As Integer
Dim x As Integer, y As Integer
Const TITLEBAR = 29 ' Höhe in Pixeln
Const HEIGHTKORR = 57 ' Höhen-Korrektur Acess-Elemente
GetResolution xDT, yDT ' Desktop-Maße
If WF = 0 Then WF = xDT ' wenn Maße-Parameter = 0
If HF = 0 Then HF = yDT - HEIGHTKORR ' Quasi-Vollbild-Darstellung
If WD = 0 Then WD = xDT
If HD = 0 Then HD = yDT - HEIGHTKORR
If F.CurrentView = 2 Then ' bei Datenblattansicht
WF = WD ' Werte an WF/HF übrgeben
HF = HD
End If
x = ((xDT - WF) / 2) ' x ausmitteln
y = ((yDT - HF) / 2) - TITLEBAR ' y ausmitteln innerhalb der Parent-Anwendung
y = y - (y / 6) ' dynamische Geschmacks-Höhenanpassung
SetWindowPos F.hWnd, 0, x, y, WF, HF, 0 ' do it
Exit Sub
err_CenterForm:
Fehler "CenterForm"
Exit Sub
End Sub
Wunderbar, und im Formular, dem eigentlichen Objekt unserer Begierde, passiert dann nicht mehr viel:
Dim iLastView As Integer im Deklarationsteil, (Spreu vom CurrentView-Wechsel trennen)und
Sub Form_Current ()
If Me.CurrentView <> iLastView Then
iLastView = Me.CurrentView
CenterForm Me, 281, 95, 1024, 711
End If
End Sub
Zu CenterForm nur soviel:
Im Fenstermodus sollten bei kleinen Masken die Navigationsbuttons ganz dargestellt werden (siehe Ausführungen zum Thema Ränder im Vorfeld). Die maximale Breite darf der Auflösungsbreite entsprechen, bei der maximalen Höhe müssen 57 Pixel für Access-Elemente abgezogen werden. Der Clou besteht aber darin, daß CenterForm das selbst erledigt. Ein flockiges CenterForm Me, 281, 95, 0, 0 stellt in der Formularansicht eine niedrige Maske dar, deren Navigationsbuttons ganz sichtbar sind (veränderbare Rahmen); in der Datenblattansicht wird dieses Formular in CenterForm mit KorrekturAuflösung cm Twips 640 * 480 10,801 6122 800 * 600 13,959 7915 1024 * 768 18,4 10433 1152 * 864 20,951 11879 1280 * 1024 25,182 14278 1600 * 1280 29,852 16926
FormOpen ->
CenterForm Me, 425, 344, 425, 344 ' Anpassung z.B. an native Maskengröße
Form_Current ->
If iLastView <> Me.CurrentView Then
If Me.CurrentView = 2 Then ' Datenblatt
DoCmd Maximize
Else ' Formular
DoCmd Restore
End If
iLastView = Me.CurrentView
End If
Form_Close und Form_Deactivate -> DoCmd Restore
In Form_Open wird eine Standardgröße eingestellt, die per CenterForm Auflösungs-bezogen mittig dargestellt wird. Bei Ansichtswechsel in die Datenblattansicht erfolgt ein simples DoCmd Maximize, das sicherheitshalber bei Form_Close wieder zurückgesetzt werden muß. iLastView noch formularglobal definieren und fertig.
* * * *
Positionieren von Fenstern mit MOVESIZE:
Die Hilfe zu MoveSize weist je nach Einstiegspunkt Fehler auf.
Die richtige Syntax lautet 'DoCmd MoveSize X, Y, Breite, Höhe'.
Ferner muß zum Positionieren von Fenstern gesagt werden, daß Access Fenster, die ohne Titelleiste definiert wurden, in verschiedenen Höhen positioniert, abhängig davon, ob das Fenster als GEBUNDEN oder nicht ausgelegt ist, da Access bei der Positionierung scheinbar die ohnehin nicht dargestellte Fenstertitelleiste bei der Positionierung mit berücksichtigt.
Trotz allem kann die Aktion MoveSize als probates Mittel angesehen werden, um folgendes Problem zu beheben:
Ein Fenster soll in einer bestimmten Größe und einer bestimmten Position angezeigt werden. Hierfür bietet sich eigentlich an, die entsprechenden Formulareigenschaften zu benutzen. Durch Änderungen des Formular-Entwurfes wird aber eigenartigerweiser auch die Größe des Formulares in kleinerer Form neu abgespeichert. Bevor man an diesem Phänomen verzweifelt, sollte man z.B. im Ereignis FORM_OPEN die Aktion MoveSize bemühen.
Das Formular wird ohne störende Effekte in der gewünschten Form geöffnet.
DoCmd MoveSize 0, 0, 9600, 6350 positioniert ein Formular mit veränderbarem Rahmen so, daß es den gesamten Platz einnimmt, der von den seitlichen Bildrändern sowie der Menüleiste und der unteren Statusleiste begrenzt wird,
Option Explicit
Dim iLastView As Integer
Sub Form_Activate ()
If Me.Currentview = 2 Then DoCmd Maximize
DoCmd ShowToolbar "Standard", A_TOOLBAR_YES
End Sub
Sub Form_Deactivate ()
DoCmd Restore
DoCmd ShowToolbar "Standard", A_TOOLBAR_NO
End Sub
Sub Form_Close ()
Form_Deactivate
End Sub
Sub Form_Current ()
If iLastView <> Me.Currentview Then
If Me.Currentview = 2 Then ' Datenblatt
DoCmd Maximize
Else ' Formular
DoCmd Restore
End If
iLastView = Me.Currentview
End If
End Sub
SendKeys "{F4 2}" ' 2-maliges Betätigen der Listenfunktion
Add_ComboBox_Value Me!SCHLAGW, Me!cmbSchlagw
und dann noch die zentrale Rund-um-sorglos-Routine, bei der C1 für das Textfeld und C2 für das Kombinationsfeld steht:
Sub Add_ComboBox_Value (C1 As Control, C2 As Control)
On Error GoTo errAdd_Combobox_Value
Dim iPos As Integer
If C2.ListIndex > -1 Then ' Nur, wenn ein gültiger ListIndex vorliegt !
C1.SetFocus ' Focus auf das eigentliche Feld setzen
iPos = InStr(C1, C2) ' ist der neue Wert bereits enthalten ?
If iPos Then ' Wenn ja, dann
Beep ' Signalton
C1.SelStart = iPos - 1 ' und den bereits vergebenen String
C1.SelLength = Len(C2) ' markieren
Else
C1 = C2 & "; " & C1 ' Neuen Wert + ';' dem alten voranstellen
C1.SelLength = 0 ' und Cursor an Anfang setzen.
End If
C2 = Null ' Klappfeld-Wert leeren.
End If
Exit Sub
errAdd_Combobox_Value:
If Err = 2448 Then
Beep
MsgBox "Das Feld ist voll"
ElseIf Err = 94 Then
Resume Next
Else
Fehler "Add_Combobox_Value: " & C2.Name
End If
Exit Sub
End Sub
na denn mal los ...
Dim gesch% as Integer
If Me!Grund <> Me!Grund.Column(0) Then
gesch% = DLookup("Geschäftlich", "Termin_Gründe", "Bezeichnung='" & Me!Grund & "'")
Else
gesch% = Me!Grund.Column(1)
End If
Auf deutsch heißt das, wenn der WAHRE Wert des gebundenen Tabellenfeldes des Kombinationsfeld mit dem angeblichen Wert der ersten Zeile Column(0) NICHT übereinstimmt, dann wird der WAHRE Wert der dem Kombinationsfeld eigentlich bekannten Eigenschaft 'geschäftlich' manuell per DLookup aus der Tabelle geholt, ansonsten kann er ja unbedenklich dem Kombinationsfeld entnommen werden.
Da dieser Fehler nicht auschließlich innerhalb des Form_Current-Ereignisses, sondern auch in Steuerelement_AfterUpdate auftritt, muß von der Code-mäßigen Column-Auswertung völlig zugunsten eines doch recht schnellen DLookup abgesehen... hammerhart: die Spaltenüberschriften sinds: an: Access verrutscht in der Zeile aus: alles okay* * * * Aber selbst für diese 'Lösung' kennt Microsoft noch eine Variante, in der das oben beschriebene Problem unbefangen zu Tage tritt: Gegeben sei in einem Feld 'Vorwarnung' der Standardwert-Ausdruck
=[Datum]+[von]-[Grund].Spalte(2)'
Hier wird ein Datums-Single ermittelt, der dem Zeitpunkt-genauem Datum des Termines entspricht und von dem ein Wert, der als 'Vorwarnung' in Tagen in der Tabelle 'Termin_-Gründe' hinterlegt ist. Das Kombinationsfeld zeigt unter anderem auch den für diese Terminart eingestellten Vorwarnwert an.
Nicht nur, das auch hier das oben beschriebene Problem eintritt.
Es kommt noch besser:
Da dieser Ausdruck in den Eigenschaften des Entwurfsmodus' festgelegt ist, wird er, wie so manch' anderes auch, zu einem Zeitpunkt ausgewertet, für den für einen Programmierer kein Zugriff besteht. Der Teilausdruck '[Grund].Spalte(2)'
SELECT Anbieter FROM Tarife_Telekom WHERE Anbieter<>[Formular]![Anbieter];
als Datenherkunft löst das Problem. Ähnliches im Mertlik-Projekt ließ sich ums Verrecken nicht bewerkstelligen, da mußten die Formular-Feld-Bezüge dann in einer gespeicherten Abfrage untergebracht werden. Dreck, Dreck, Dreck !
Außerdem kam es auch schon mal vor, daß eine derartige Zuweisung 'on the fly' nicht zu einer Aktualisierung des Klappfeldinhaltes kam, so daß in der entsprechenden Set-Routine ein explizites Requery gesetzt werden mußte.
Response = NichtinListe("Adressen", "Suchname", NewData)
Zusatz: Leider kommt es sehr häufig, wenn nicht immer vor, daß nach dem Ereignis NotInList das Ereignis AfterUpdate nicht mehr eintritt. Wenn also mit diesen 'potenten' Kombinationsfeldern gearbeitet werden soll, dann ab jetzt NOCH mehr
Vorsicht. Wenn Before- und AfterUpdate relevant sind, dann siehe AFTERUPDATE.
Um es für den Anwender Rolls-Royce-mäßig zu machen, werden Statuszeile sowie Maus- und Tastaturereignisse abgecheckt:
Statuszeile -> "<Doppelklick/Strg+Leer> öffnet Stammdaten".
Maus -> Doppelklick: ZeigeStammdaten TabName
Taste ab:
If KeyCode = 32 And Shift = CTRL_MASK Then
ControlName_DblClick (0)
End If
Der folgende NichtInListe()-Code sollte zu den ganz alten Geschichten gehören, da er noch mit Typ-Suffix ($ für String-Variablen) und einer globalen Variable arbeitet.
Die Funktion NichtInListe ist ein bißchen größer geworden, kann aber nun: - die entscheidende Variable Response direkt steuern - Werte sowohl in eine andere Basistabelle wie auch in das Kombinationsfeld selbst einfügen. - die Anzeige "Sie müssen einen Eintrag ..." elegant unterdücken - und zum nächsten Feld springen, als ob nichts gewesen wäre - und obendrein aussagekräftig auf eine Basistabellenfeld-Eingabepflicht reagieren. - Öffnen eines Basistabellen-Formulares für umfangreiche Eingaben - Setzen globaler Control/String-Werte für Aktualisierung nach Verlassen des Pflegeformulares.
Function NichtinListe (Basistabelle$, Feldname$, NeuerWert$) As Integer
On Error GoTo err_NichtinListe
Dim ret As Integer
ret = DATA_ERRDISPLAY
Set gcActiveControl = Screen.ActiveControl
gsActiveControlValue = NeuerWert$
If NeuerWert$ <> "" Then
Beep
Antwort% = MsgBox("'" + NeuerWert$ + "' in Liste '" + Basistabelle$ + "' einfügen ?", 36, "Wert in Liste nicht vorhanden")
If Antwort% = 6 Then
Anweisung$ = "INSERT INTO " + Basistabelle$ + " (" + Feldname$ + ") VALUES ('" + NeuerWert$ + "')"
DoCmd SetWarnings False
DoCmd RunSQL Anweisung$
DoCmd SetWarnings true
ret = DATA_ERRADDED
Else
ret = DATA_ERRCONTINUE
End If
ElseIf TypeOf gcActiveControl Is ComboBox Then
If gcActiveControl.LimitToList = True Then
gcActiveControl = Null ' -> Eingabepflichtfehler oder weiter
SendKeys "{TAB}", True
ret = DATA_ERRCONTINUE
End If
End If
NichtinListe = ret
Exit Function
err_NichtinListe:
If Err = 3314 Then ' = Feld '|' kann keinen Nullwert enthalten
' Fehler beim Anlegen eines Stammdaten-Satzes, für den
' weitere Felder mit Eingabepflicht bestehen.
' Stattdessen versuchen, ein gleichnamiges Formular
' zu öffnen und Neueingabe vorzubereiten
DoCmd OpenForm Basistabelle$, , , , A_ADD
Forms(Basistabelle$)(Feldname$) = NeuerWert$
ElseIf Err = 2448 Then ' Wert kann nicht gesetzt werden.
Beep ' Fehler bei Eingabepflicht
MsgBox "Das Feld '" & Feldname$ & "' kann keinen Nullwert enthalten !",
16, "Funktion NichtinListe"
Else
Fehler "NichtinListe"
End If
NichtinListe = DATA_ERRCONTINUE
Exit Function
End Function
Für den Trick mit dem Aktualisieren mußten leider globale Variablen bemüht werden:
Global gcActiveControl As Control
Global gsActiveControlValue As String
damit in der folgenden Sub das Steuerelement, von dem aus NichtInListe aufgerufen wurde, aktualisiert werden kann.
Sub AktualisiereSteuerelement ()
On Error GoTo err_AktualisiereSteuerelement
gcActiveControl = Null
gcActiveControl.Requery
gcActiveControl = gsActiveControlValue
Exit Sub
err_AktualisiereSteuerelement:
If Err = 2467 Then
' Feld im Ausdruck nicht mehr vorhanden, wenn Basis-
' tabellenformular solo geschlossen wird
Exit Sub
Else
Fehler "AktualisiereSteuerelement"
Exit Sub
End If
End Sub
Perfekterweise erfolgt die Zuweisung globaler Variablen auch in ZeigeStammdaten
Sub ZeigeStammdaten (FName As String)
On Error GoTo err_ZeigeStammdaten
Dim v As Variant
v = Screen.ActiveControl
Set gcActiveControl = Screen.ActiveControl
gsActiveControlValue = gcActiveControl
DoCmd OpenForm FName
If Not IsNull(v) Then DoCmd FindRecord v
Exit Sub
err_ZeigeStammdaten:
Fehler "Modul ZeigeStammdaten"
Exit Sub
End Sub
Das mutet gegenüber älteren Versionen zwar ein bißchen umständlich an, zumal in jedem Pflege-Formular bei DeActivate (gibt's eh' schon wegen Symbolleisten ausblenden) der Aufruf AktualisiereSteuerelement erfolgen muß, aber dafür ist das jetzt so richtig narrensicher. Selbst wenn der Anwender nach Änderungen in einem Pflege-Formular 'danebenklickt', tritt bei erfolgter Variablenzuweisung eine Aktualisierung des ursprünglichen Steuerelementes ein.
Für Eilige: Die Modul-Komponenten (globale Variablen, Sub AktualisiereSteuerelement, Sub ZeigeStammdaten und die Funktionen NichtinListe und NichtinListe2) können in einem Modul 'Kombinationsfeld' zusammengefasst werden.
Die Aufrufe erfolgen:
Sub AktualisiereSteuerelement in Sta-Da-Formular /FormDeactivate (warum eigentlich?)
Sub ZeigeStammdaten in KombiFeld_DblClick und BeiTasteab
Funktion NichtinListe in KombiFeld_NotinList (dito für NichtinListe2)
Kritische Betrachtungen:
Wenn für die betroffenen Tabellen-/Felder Beziehungen mit referentieller Integrität vereinbart wurden, werden die Kombifelder scheinbar automatisch aktualisiert. Bravo !
Wo das nicht funktioniert, war AktualisiereSteuerelement in Form_Deactivate des aufgerufenen Stammdatenformulares angedacht, ist aber nicht so ganz sinnig, also erst mal auf Eis legen.
Die Methode FindRecord in ZeigeStammdaten greift nur für die Inhalte des ersten Formular-Feldes (wie der Bearbeiten-Suchen-Dialog)
Und für Kombinationsfelder, die nicht editierbare Daten wie Monat 1-12 enthalten, aber online mit NULL überschrieben werden dürfen, tut's dann auch ein
Response = NichtinListe("", "", NewData)
im Element_NotInList-Ereignis und alles geht klar.
* * * *
Diese Version von NichtinListe () stammt ebenfalls aus frühen Access 2.0 - Tagen, ist aber einen hauch moderner (explizite Typenzuweisung und keine globalen Variablen) und auch etwas einfacher:
Aufruf wie üblich in xyz_NotInList
Response = NichtinListe("Adressen", "Suchname", NewData)
und dann
Function NichtinListe (Basistabelle As String, Feldname As String, NeuerWert As String) As Integer
On Error GoTo err_NichtinListe
Dim Steuerelement As Control
Dim sAnweisung As String, iRet As Integer, iAntwort As Integer
iRet = DATA_ERRDISPLAY
Set Steuerelement = Screen.ActiveControl
If NeuerWert <> "" Then
Beep
iAntwort = MsgBox("'" + NeuerWert + "' in Liste '" + Basistabelle + "' einfügen ?", 36, "Wert in Liste nicht vorhanden")
If iAntwort = 6 Then
sAnweisung = "INSERT INTO " + Basistabelle + " (" + Feldname + ") VALUES ('" + NeuerWert + "')"
DoCmd SetWarnings False
DoCmd RunSQL sAnweisung
DoCmd SetWarnings True
iRet = DATA_ERRADDED
Else
iRet = DATA_ERRCONTINUE
End If
ElseIf TypeOf Steuerelement Is ComboBox Then
If Steuerelement.LimitToList = True Then
Steuerelement = Null ' -> Eingabepflichtfehler oder weiter
SendKeys "{TAB}", True
iRet = DATA_ERRCONTINUE
End If
End If
NichtinListe = iRet
Exit Function
err_NichtinListe:
If Err = 2448 Then ' Fehler, wenn Eingabepflicht gesetzt
Beep
MsgBox "Für das Feld '" & Feldname & "' besteht eine Eingabepflicht !", 16, "Funktion NichtinListe"
Else
Fehler "NichtinListe"
End If
NichtinListe = DATA_ERRCONTINUE
Exit Function
End Function
* * * *
NOTINLIST / STAMMDATEN NEU ANLEGEN:
1-feldige Datensätze in Stammdatentabellen können per den oben beschriebenen NotInList-Routinen mit minimaler Benutzerinteraktion angelegt werden.
Schwieriger wird es hingegen, wenn aus einem Klappfeld heraus Datensätze mit umfangreicherer Struktur angelegt werden müssen.
Da gab es schon manche praktikable Lösung und jetzt auch noch diese. Sie erzeugt zwar etwas redundanten Code, ist aber im Bedarfs-Einzel-Fall besser anpassbar, als wenn man alle Register der Kapselung gezogen hätte.
Für das Klappfeld gilt:
Sub Suchname_NotInList (NewData As String, Response As Integer)
Beep
If MsgBox("Wollen Sie einen neuen Adressen-Datensatz '" + NewData + "' anlegen ?", 52, "Unbekannter Adressen-Suchname") = 6 Then
Response = DATA_ERRCONTINUE
DoCmd OpenForm "Adressen", , , , A_ADD, , "Neu"
Forms!Adressen!Suchname = NewData
End If
End Sub
Interessant ist die unspektakuläre Zuweisung des neuen Wertes nach dem OpenForm, während als OpenArg nur ein simples "Neu" übergeben wird.
Im Formular der Begierde steht dann:
Sub Form_AfterUpdate ()
On Error GoTo err_AfterUpdate_Adressen
If Not IsNull(Me.OpenArgs) Then
If Me.OpenArgs = "Neu" Then
If IsFormOpen("Termine") Then
Forms!Termine.SetFocus
Forms!Termine!Suchname = Forms!Termine!Suchname.OldValue
Forms!Termine!Suchname.Requery
Forms!Termine!Suchname = Me!Suchname
DoCmd Close A_FORM, Me.Name
End If
End If
End If
Exit Sub
err_AfterUpdate_Adressen:
If Err = 2448 Then
DoCmd RunMacro "Menübefehle.RückgängigFeld"
Resume Next
Else
Fehler "AfterUpdate_Adressen"
End If
Exit Sub
End Sub
Hier kann sogar während der Eingabe der ursprünglich neue Wert geändert werden, die OldValue-Zuweisung erlaubt ein folgendes, meckerfreies Neuabfragen des Klappfeldes und selbst mit dem Fokus sollte alles klargehen.
* * * *
KONSISTENTE EINHEITLICHE SCHREIBWEISE / KONSISTENZ / INNERHALB EINES FELDES DER AKTIVEN TABELLE:
kann wohl am besten mit Kombinationsfeldern erreicht werden, deren Eigenschaft 'Nur Listeneinträge' wahr ist. Leider muß man sich dann mit den Unwirtlichkeiten der NotInList-Problematik herumschlagen. Für die Pflege einheitlicher Begriffe in eigenen Stammdaten-Tabellen siehe NOTINLIST.
Es können jedoch auch einheitliche Begriffe innerhalb eines Feldes der aktiven Tabelle verwaltet werden.
Das Kombinationsfeld wird hier lediglich dazu benutzt, um Werte, die nicht in eigenen Stammdaten-Tabellen, sondern in Felder der Muttertabelle des Formulares stehen, übersichtlichkeitshalber zu pflegen und trotz LimitToList (Schreibergänzung) auch Leereinträge zuzulassen (oder auch nicht):
Im NotinList-Code steht nur eine Anweisung:
Sub Zahlart_NotInList (NewData As String, Response As Integer)
Response = NichtinListe2(Me!Zahlart, NewData)
End Sub
Alles weitere wird durch die 'Stammdaten-Tabellen-lose' NichtinListe2-Funktion gemanagt, die sowohl Leereinträge toleriert, wie auch die Begriffsvergabe steuert (Stand 06.2002):
Function NichtinListe2 (C As Control, ND As String) As Integer
On Error GoTo err_NichtinListe2
Dim ret As Integer
ret = DATA_ERRDISPLAY
If ND = "" Then
'ND = Null
ret = DATA_ERRCONTINUE
GoSub NichtinListe2_weiter
End If
Beep
If MsgBox("Möchten Sie '" + ND + "' als neuen Begriff aufnehmen ?", 36, "Wert in Liste nicht vorhanden") = 6 Then
ret = DATA_ERRADDED
GoSub NichtinListe2_weiter
Else
GoSub NichtinListe2_exit
End If
NichtinListe2_weiter:
C.LimitToList = False
Rueckgaengig_Feld
SendKeys "{TAB}", True
C.LimitToList = True
If ND = "" Then
C = Null
Else
C = ND
End If
If ret = DATA_ERRADDED Then
ret = DATA_ERRCONTINUE
'C.Requery
SendKeys "{F4}"
End If
NichtinListe2_exit:
NichtinListe2 = ret
Exit Function
err_NichtinListe2:
If Err = 2448 Then
Beep
MsgBox "Sie dürfen das Feld " & C.Name & " nicht leer lassen !", 16, "NichtInListe2"
GoSub NichtinListe2_exit
Else
Fehler "NichtinListe2"
End If
Exit Function
End Function
Die erste Version kam ohne ein umständliches 'Rueckgaengig_Feld' aus, litt aber unter übelsten Formularfehlern in der Datenblattansicht. Da aber selbst in der Access-Dokumentation mit 'Rueckgaengig_Feld' gearbeitet wird, braucht sich meine neueste Version dafür auch nicht zu schämen. Das Requery des Steuerelementes macht bei NichtInListe2 keinen Sinn, da ein neuer Wert für das Feld erst dann erkannt werden kann, wenn der bearbeitete Datensatz gespeichert wurde.
Das mit dem Aktualisieren ist so eine Sache, vor allem bei "selbstlernenden" Kombinationsfeldern, die auf Abfragen a'la "SELECT DISTINCT Land FROM ..." beruhen. Während der Verarztung des NotInList-Ereignisses kann das nicht erfolgen, da der Datensatz selbst ja noch gar nicht gespeichert ist. Ein "Schlag mich tot"-Speichern des Datensatzes beißt sich evtl. mit unvollständigen Eingaben.
Eine ein wenig umständliche Lösung per Code kann so aussehen:
Dim iComboFieldEdited As Integer ' Im Deklarationsteil
Sub Land_AfterUpdate ()
iComboFieldEdited = True
End Sub
Sub Form_Current ()
If iComboFieldEdited = True Then
Me!Land.Requery
Me!Genre.Requery
Me!Bewertung.Requery
End If
End Sub
* * * *
Leider treten bei mehreren Klappfeldern unsinnige Aufklappungen folgender Felder an der Position des gerade verarzteten auf, nachdem NotInList2 im NotInList-Ereignis ausgewertet wird. Ein verschissenes SendKeys "{F4}" OHNE WAIT-Parameter scheint Abhilfe zu schaffen. Kein Wunder, daß in den neueren Access-Versionen erweiterte Methoden für diese Drecks-Klappscheisse eingeführt wurden. Dieses dumme Verhalten der Klappfelder tritt sogar innerhalb eines Formulares uneinheitlich auf.
* * * *
Was Positives:
Erfolgt in der Ereignisprozedur NotInList der Funktionsaufruf mit "leerem" ND-Parameter a'la
Response = NichtinListe2(Me!Zahlart, ""), dann wird lediglich das meckerfreie Entfernen eines Wertes aus einem an sich "scharfen" Kombinationsfeld ermöglicht, wenn auch mit vereinzelten Aufklapp-Fehlern.
Und noch besser:
If NewData = "" Then
Response = NichtinListe2(Me!Symbol_ID, "")
End If
sorgt dafür, daß zwar Löschen der Einträge meckerfrei vonstatten geht, aber das Eintragen Listen-fremder Werte mit der Access-eigenen Standardmeldung bestraft werden.
* * * *
KOMBINATIONSFELDER / AFTERUPDATE:
Was man beachten sollte: Bei erfolgten NotInList-Verarztungen scheint das Ereignis AfterUpdate für das betroffenen Steuerelement nicht mehr einzutreten !
In einem solchen Fall folgende Anweisung in der NotinList-Sub, wenn ein Wert zugewiesen wurde:
If Response = DATA_ERRADDED Then Steuerelement_AfterUpdate
oder gleich grundsätzlich in Steuerelement_AfterUpdate schicken, wenn NichtInListe ausgelöst wurde.
Klappt aber auch nicht immer. Bei Kombinationsfeldern scheint es vorzukommen, daß die Ereignisse BeforeUpdate und AfterUpdate nicht eintreten !!!
*
Erfolgreich verarztet werden konnte folgender Fall in bestatt.mdb:
Das Feld Adressen.Art (Klappfeld) wird in AfterUpdate auf verschiedene Plausibilitäten geprüft. Ein einfaches Entfernen eines Eintrages scheint dieses Ereignis aber nicht mehr auszulösen, da bereits zuvor NotInList eingetreten ist. Es ist noch zu früh, die These aufzustellen, ob nach Eintreten von NotInList NIE AfterUpdate eintritt, aber der gerade beschriebene Fall kann durch Einfügen von
If NewData = "" Then ' beim Entfernen eines Eintrages
Art_AfterUpdate ' tritt das Ereignis AfterUpdate
End If ' nicht ein, deswegen expl. Aufruf
in Art_NotInList erfolgreich bekämpft werden.
*
Hier muß hinzugefügt werden, daß das Ereignis AfterUpdate für Formulare u.a. eintritt, wenn ein Formulardatensatz geändert oder auch hinzugefügt wurde. Toll, aber nichts dergleichen passiert, wenn ein neuer Datensatz hinzugefügt wird;
hier müssen offensichtlich redundant die entsprechenden Befehle noch mal abgesetzt werden.
Zu einem anderen Fall gehört folgender Aufschrieb, dessen Notwendigkeit sich mir mittlerweile nicht erschließt:
* * * *
Ein weiterer Aspekt einer Kombinationsfeld-gestützten Pflege (hier ein weiches Beispiel) ist ein Kombinationsfeld, das in einem Unterformular steht, das 1:n-Daten eines Hauptformulares anzeigt.
Die mittlerweile klassische NichtInListe-Verarztung schlägt bei dem Versuch, einen neuen Datensatz direkt in der Tabelle anzulegen, fehl, da z.B. ein Eingabe-Muß-Ident nicht leer gelassen werden darf. Witzigerweise besitzt das evtl. ausgeblendete Identfeld im Unterformular bereits automatisch als Standardwert den Wert des mit dem Hauptformular verknüpften Ident-Feldes, obwohl dies in den Eigenschaften (DefaultValue) nicht eingetragen wurde. Das macht Access wohl selber richtig. Ok.
In diesem Fall werden die nötigen Verarztungen im NotinList-Ereignis selbst vorgenommen:
Sub Personen_NotInList (NewData As String, Response As Integer)
If MsgBox("Möchten Sie '" + NewData + "' als neuen Begriff aufnehmen ?", 36, "Wert in Liste nicht vorhanden") = 6 Then
Me!Personen = NewData
Response = DATA_ERRCONTINUE
iRequeryComboBox = True
SendKeys "{TAB}", True
End If
End Sub
Die Zuweisung 'Me!Personen = NewData' wird benötigt, damit Access die bestätigte Änderung akzeptiert. 'Response = DATA_ERRCONTINUE' sorgt dafür, das Access die Fresse hält, 'iRequeryComboBox = True'
* * * *
Im Deklarationsteil des Formulares wird ein Integer a'la 'FeldXEdit' vereinbart.
In Form_BeforeUpdate wird folgendes, eigenartige Konstrukt eingefügt:
If Me!Art = Me!Art.OldValue = True Then
' nix
Else
ArtEdit = True
End If
Nur bei dieser Schreibweise können eventuelle NULL-Fälle ( bei Neueingabe ) und andere Scheiße abgefangen werden. Der Ausdruck sollte sich nach Regeln der formalen Aussagelogik zwar auch direkt formulieren lassen, aber Access 2.0 - Basic erkennt das dann halt eben nicht !
In Form_Current sorgt
If ArtEdit = True Then
ArtEdit = False
Me!Art.Requery
End If
dafür, daß alles zurückgesetzt wird und das Kombinationsfeld neu abgefragt wird.
Auf dem Weg dorthin kommt einem selbstverständlich das Ereignis NotInList in die Quere, aber dafür gibts ebenso selbstverständlich unter diesem Stichwort einen Lösungsvorschlag.
If KeyCode = 32 And Shift = CTRL_MASK Then als Tastaturäquivalent zu einem Doppelklick ist nicht immer 100 %-ig zuverlässig. Bei einem Memofeld hinterläßt <Strg+Leer> Editierspuren, die erst mit einer formular-globalen Variablen, die in KeyPress ausgelesen wird, wieder bekämpft werden können.
Bemerkungen_KeyDown (KeyCode As Integer, Shift As Integer)
If KeyCode = 32 And Shift = CTRL_MASK Then
iCtrlBlank = True
Bemerkungen_DblClick (0)
End If
End Sub
Sub Bemerkungen_KeyPress (KeyAscii As Integer)
If iCtrlBlank = True Then
iCtrlBlank = False
KeyAscii = 0
End If
End Sub
Da KeyDown vor KeyPress eintritt (die Reihenfolge der Ereignisse für ein tastaturbedientes Steuerelement ist KeyDown, KeyPress, Update und KeyUp), kann hier der Flag gesetzt und die Sonderaktion ausgeführt werden. Im direkt danach eintretenden Ereignis KeyPress wird der Flag abgefragt, zurückgesetzt und die die Aktion auslösende Tastatureingabe mit KeyAscii=0 aus diesem unserem Softwareuniversum hinausgefegt.
Beep
If MsgBox("Möchten Sie '" + NewData + "' als neuen Begriff aufnehmen ?", 36, "Wert in Liste nicht vorhanden") = 6 Then
DoCmd Hourglass True
Me!Kuerzel = NewData
Response = DATA_ERRCONTINUE
iRequeryComboBox = True
SendKeys "{TAB}", True
DoCmd Hourglass False
End If
Relevant sind die Anweisungen Me!Kuerzel = NewData und Response = DATA_ERRCONTINUE, die dem Klappfeld mitteilen, den neuen Wert anzunehmen und erstmal weiterzumachen, als ob alles in Ordnung wäre.
Ein DATA_ERRADDED scheint an dieser Stelle angebrachter zu sein, zumal die Dokumentation verspricht, den neuen Wert der Liste auch wirklich hinzuzufügen. Toll, zumal das sogar passiert, aber dummerweise haben die meisten, wirklich geilen Klappfeld-Listen auch was mit echten Daten zu tun und da gehts halt schon wieder los. Access weigert sich in dem Unterformular zu einem neuen Datensatz zu gehen und fängt das Spinnen an. Deswegen also brav mit einem ignoranten DATA_ERRCONTINUE und weiter gehts, denn das beste kommt erst noch:
Nachdem ein formularglobales Flag und der Fokus mit einem peinlichen SendKeys auf das nächstes Feld gesetzt wurde (wir imitieren hier das eigentlich sinnvolle Verhalten von Access, bei einer mit bestätigten Eingabe zum nächsten Feld zu springen), geht es dann mit Form_Current weiter:
If iRequeryComboBox = True Then
Me!Kuerzel.Requery
iRequeryComboBox = False
Me.TimerInterval = 300
End If
Das Requery sorgt dafür, daß das datenzeigende Klappfeld auch wirklich Kenntnis von dem neu eingefügten Wert besitzt. Nachdem Rücksetzen des Flags, das ja nur den 'Eingabe hat stattgefunden'-Fall anzeigt, wird der defaultmäßig auf 0 eingestellte TimerInterval des Formulares auf ca. 1/3 Sekunde gesetzt. Das ist in dem konkreten Fall die Zeit, innerhalb der Access meinte, nach Abschluß aller zur Verfügung stehenden Ereignisse die Drecks-Liste aufklappen zu müssen, die aber jetzt vom Formulartimer abgewartet wird:
DoCmd RunMacro "Menübefehle.Anzeige_aktualisieren"
Me.TimerInterval = 0
Das mit dem Makro haut hin, der Timer hält danach auch die Fresse, und, von einem zaghaften Aufflackern der Liste, dem selbst mit abenteuerlichen 'Application SetWarnings'-Anweisungen nicht beizukommen war, mal abgesehen, tut das zu meiner Zufriedenheit. Der Laie freut sich, der Fachmann wundert sich.
Allerdings muß an dieser Stelle hinzugefügt werden, daß dem Spuk, als das Problem auch ein anderes Mal auftrat, mit dem Entfernen des true-Zusatzes der SendKeys-Anweisung ein Ende bereitet werden konnte.
Dafür tat's dann ein paar Wochen später wieder nicht mehr. Egal !
Dim CR As String, iStrg_Alt As Integer
On Error GoTo exit_Bezeichnung_KeyDown
' **** Bei <Strg+Einfg> in Stammdaten aufnehmen ****
iStrg_Alt = (Shift And CTRL_MASK) > 0 And (Shift And ALT_MASK) > 0
If KeyCode = 45 And iStrg_Alt Then
Beep
KeyCode = 0 ' Damit nicht den Überschreibe-Modus aktiviert
CR = Chr$(13) & Chr$(10)
SendKeys "{TAB}+{TAB}", True ' Evaluierung der Eingabe und Focus wieder zurück
If MsgBox("Möchten Sie " & CR & CR & "'" & Me!Bezeichnung & "'" & CR & CR & "in die Liste der Positionsbezeichnungen aufnehmen ?", 33, "Bezeichnung speichern") = 1 Then
Anweisung$ = "INSERT INTO Auftragspositionen_Bezeichnung (Bezeichnung) VALUES ('" + Me!Bezeichnung + "')"
DoCmd RunSQL Anweisung$
Me!Bezeichnung.Requery
End If
End If
exit_Bezeichnung_KeyDown:
Exit Sub
if ineu then
SendKeys "{F2}"
end if
einfügt.
Navigiert man durch einen bereits vorhandenen und vollständig ausgefüllten Datensatz, verhält sich alles ganz Access-normal. Bei der Neueingabe erscheinen einem Default gleich die Literale des Eingabeformates und es rutscht der Cursor im entsprechenden Feld an die erste Stelle, an der in dem Eingabeformat ein einzugebendes Zeichen vorgesehen wurde.
For i = 0 To Me!Dateitypen.ListCount - 1
iSummeAktiv = iSummeAktiv + Me!Dateitypen.Column(1, i)
Next i
* * * *
Eine Besonderheit der Eigenschaft ListIndex besteht im Zusammenhang mit der Eigenschaft BoundColumn (GebundeneSpalte).Me!Liste = i ein numerisches Ansteuern bestimmter Listeneinträge erreicht werden.
* * * *
Zusätzlich gibt es ein paar spezielle LISTENFELD-Funktionen, die, wenn Access sowas wie dynamisch übergeb-, zuweis- und ausführbaren Code beherrschen würde (a'la php), auch noch ausgelagert werden könnten.
Private Function DeleteDateitypen(bolSpeichern As Boolean)
On Error GoTo err_DeleteDateitypen
Dim s As String, i As Integer
If HasListfieldSelections(Me!Dateitypen) = False Then Exit Function
For i = 0 To Me!Dateitypen.ListCount - 1
If Me!Dateitypen.Selected(i) = True Then
If bolSpeichern = True Then
ManageStopWords Me!Dateitypen.ItemData(i), True
End If
Else
s = s & Me!Dateitypen.ItemData(i) & ";"
End If
Next i
Me!Dateitypen.RowSource = s
Exit Function
err_DeleteDateitypen:
Fehler "DeleteDateitypen"
Exit Function
End Function
* * * *
Das Programm-gesteuerte AUFFÜLLEN von LISTENFELDERN:
Macht einen komplexen Eindruck, geht aber sehr fix über die Bühne.
Function Listenfeld (Feld As Control, ID As Long, Zeile As Long, Spalte As Long, Code As Integer)
Select Case Code
Case 0 ' Initialisieren.
Check_Dateien ' fülle Array
Listenfeld = True
Case 1 ' Öffnen.
Listenfeld = Timer ' Eindeutige ID für Steuerelement
Case 3 ' Zeilenanzahl.
Listenfeld = UBound(DF_Dateien)
Case 4 ' Spaltenanzahl.
Listenfeld = 1
Case 5 ' Spaltenbreite.
Listenfeld = -1 ' Standardbreite verwenden.
Case 6 ' Mit Daten füllen.
Listenfeld = DF_Dateien(Zeile)
Case 7 ' Abschluß
End Select
End Function
If Len(s) > 2048 Then
WordsToArray s, ";", True
Me!Wortliste.RowSourceType = "FuelleWortliste"
Else
Me!Wortliste.Rowsource = s
End If
siehe auch WordsToArray
* * * *
KOMBINATIONSFELDER / KOMBINATIONSFELDER FÜLLEN:
Da gibt es zum einen die sehr komplizierte, aber schnelle Callback-Geschichte (s.o.) und zum anderen das einfache Verfahren, per Funktion einen String generieren zu lassen, der die Werteliste darstellt.
Die Callback-Lösung ist sehr flott.Dim arrWortliste() As Stringund dann:
Function FuelleWortliste (Feld As Control, ID As Long, Zeile As Long, Spalte As Long, Code As Integer)
Select Case Code
Case 0 ' Initialisieren.
FuelleWortliste = True
Case 1 ' Öffnen.
FuelleWortliste = Timer ' Eindeutige ID für Steuerelement
Case 3 ' Zeilenanzahl.
FuelleWortliste = UBound(arrWortliste)
Case 4 ' Spaltenanzahl.
FuelleWortliste = 1
Case 5 ' Spaltenbreite.
FuelleWortliste = -1 ' Standardbreite verwenden.
Case 6 ' Mit Daten füllen.
FuelleWortliste = arrWortliste(Zeile)
Case 7 ' Abschluß
End Select
End Function
Diese Funktion wird innerhalb von Access 8 mal aufgerufen, um verschiedene Informationen zur zu füllenden Liste abzurufen.
Interessant ist auch das in der Funktion selbst nicht erkennbare Handling der verwendeten Parameter, daß irgendwo außerhalb in den Untiefen von Access erfolgt.
Sub FuckListBox (F As Form)
' **** Listenfelder, oder wie ich sie hasse, die Microärsche ****
Dim s As String
DoEvents
Application.Echo False
DoCmd SelectObject A_FORM, F.Name
F!Institutionen.SetFocus
F!Institutionen.Requery
DoEvents
If Not IsNull(F!UF.Form!Adresse1) Then
s = Left$(F!UF.Form!Adresse1, 1)
SendKeys "^{Ende}", True
SendKeys s, True
End If
Application.Echo True
DoEvents
End Sub
Hier scheint DoEvents zum ersten Mal etwas zu bewirken. Eigentlich wird das Listenfeld nur (krampfhaft) fokussiert, per Sendkeys ans Listenende gesprungen (dann klappts auch mit den Daten) und dann per variablem Sendkey an den ersten, alphabetisch entsprechenden Datensatz der Liste. Das scheint ganz gut zu klappen, trotzdem gibt es danach vereinzelt weitere Probleme: sei es, daß die Bildschirmanzeige der Listbox quasi zusammenbricht (GUI oder Gülle ?), oder daß sich das Parent-Formular mit der idiotischen Fehlermeldung hervortut, daß irgendwelche Datensätze während der Gültigkeitsüberprüfung nicht gespeichert werden können.
Die Bildschirmkacke kann witzigerweise mit <Strg+Pos1>oder
<Strg+Ende>wieder gerade-gebogen werden, die Fehlermeldung wird in Form_Error abgestellt. Was lernen wir daraus ? Wenn es brenzlig wird, Finger WEG von Kombinations- und Listenfeldern. Die sind nämlich scheisse programmiert. * * * * In den auf 2.0 folgenden Access-Versionen hat sich dann auch einiges getan, was Eigenschaften und vor allem Methoden betrifft. Nur der Vollständigkeit halber noch ein Beitrag zum Thema "Durchlaufen der Einträge eines Listenfeldes" oder auch "Listenfeld-Daten prüfen" bzw. "Werte in Listenfeldern finden" unter Access-2.0: Wenn die Einträge einer Liste nur durchlaufen werden müssen, geht das mit dem einfachen Abfragen der Itemdata(listindex)-Funktion:
For i = 0 To C.ListCount - 1
Msgbox C.ItemData(i)
Next i
Fieseliger wird es, wenn das Listenfeld mehrspaltige Daten enthält, dann müssen in Access 2.0 die Listeneinträge allen Ernstes nicht nur durchlaufen, sondern der Wert dieses Felder einzeln gesetzt werden, um dann für den aktuellen Wert die Werte der zusätzlichen Spalten abfragen zu können.
For i = 0 To Me!Liste.ListCount - 1
Me!Liste = i
If Me!Liste.Column(0) = Me!von And Me!Liste.Column(1) = Me!zu Then
Exit For
End If
Next i
Hier wird allen Ernstes der Wert eines NICHT-GEBUNDENES Listenfeld mit einem Schleifenzähler gleichgesetzt (Access 2.0 bildet bei ungebundenen Listenfeldern solche Index-Werte, übrigens von 0 an zählend).
Type POINTAPI ' Datenstruktur für eine
x As Integer ' API-Koordinate
Y As Integer ' -> Set/Get-CursorPos
End Type
Dim lpPoint As POINTAPI
Declare Sub GetCursorPos Lib "User" (lpPoint As POINTAPI)
Declare Sub SetCursorPos Lib "User" (ByVal x As Integer, ByVal Y As Integer)
Aufrufe:
GetCursorPos lpPoint SetCursor x, y
If Me!Register.Value = Me!Register.Pages.Count - 1 Then ' letzte Seite erreicht
Me.Register.Pages(0).SetFocus
Else
Me.Register.Pages(Me!Register.Value + 1).SetFocus
End If
DoCmd.SelectObject acForm, Me.Name
RunCommand acCmdSaveRecord
Ein vorangestelltes SelectObject hat da geholfen.
* * * *
Heute schon gekotzt ?
Wenn nicht, dann sollte man mal versuchen, in einem Formular mit der Einstellung 'Aktualisierung zulassen = Keine Tabellen' Daten anzuzeigen. Obwohl die Tabelle über einen Primärindex verfügt, weigert sich dieses Drecks-Formular, die Daten in der korrekten Reihenfolge anzuzeigen. Selbst wenn man dem Formular als Datenherkunft eine Abfrage zuweist, die die Daten selbst richtig anzeigt, tritt dieser Fehler auf. Erst wenn die Abfrage unnötigerweise eine explizite "ORDER BYgedrückt wurde. Obwohl das Formular auf's penibelste exakt ausgerichtet war, trat eine scheinbare Formularverlängerung um ca. 50 % auf, als ob das Formular sich plötzlich in ein Endlosformular verwandelt hätte. Dies schien daran gebunden zu sein, daß das Formular CheckBoxen sowie ein Formularansicht-Unterformular enthielt, in dem sich ebenfalls CheckBoxen befanden. Bevor für jede CheckBox ein
<Druck>wird der gesamte Bildschirminhalt als Grafik in die Zwischenablage kopiert. Mit
<Alt+Druck>gilt das für den Inhalt des aktuellen Programmes. Wird in diesem Programm gerade ein System-Modal-Fenster angezeigt, so wird in diesem Fall nur dieses Fenster kopiert. Um also in Access per bequemen Formular-Entwurf ein Logo zu erstellen, kann dieses als modal und gebunden angelegt werden und nach Öffnen mit
<Alt+Druck>direkt in ein Grafikprogramm übernommen werden.
DoCmd Formular.Repaintzu sein. Im konkreten Fall wird innerhalb einer Schleife ein Fortschrittswert an eine selbstgeschriebene Fortschrittsroutine übergeben. Die Sub Meter_Update errechnet die Width des Meter-Rechteck-Objektes und den Wert des Textfeldes Text_Prozent ( Beispiel für einen selbergemachten FORTSCHRITTSZEIGER in dmttrial.mdb, Formular Trenne_Zeichenkette). Soweit, so gut. Dummerweise wird die Anzeige erst dann aktualisiert, wenn das Programm mit
<Strg+Pause>unterbrochen wird. Die Access-Hilfe meint dazu nur lapidar, das Access manchmal den Formularinhalt nicht aktualisiert. Von den angebotenen Methoden / Aktionen hat sich DoCmd Formular.Repaint als geeignet herausgestellt. Es konnte mittlerweile gezeigt werden, daß ein Fortschrittszeiger auch ohne Drecks-Maßnahmen tun kann: Die wohl mit Abstand allergeilste Lösung zu diesem Problem kann betrachten werden in dmttrial.mdb / Formular Status_Anzeige_Test.
<Alt+F4>(es ist Pop-Up, weil es sich sonst nicht außerhalb des Mutterformulares plazieren lassen würde) getrennt geschlossen werden könnte, wird dort bei
<Alt+F4>ein Flag gesetzt, der in im unweigerlich ausgelösten Form_Unload Cancel setzt. Ein Mausklick auf eines der sichtbaren Steuerelemente bringt eine Klartext-Meldung zum Vorschein(die auch per pb im Mutterformular erscheinen könnte) und setzt den Focus wieder auf das Mutterformular. Da für ein Pop-Up-Formular leider keine GotFocus- und Activate-Ereignisse eintreten (die bleiben in diesem Fall beim zuletzt aktiven Formular), ist es als einziger Schönheitsfehler möglich, per
<Strg+F6>den Formularfokus der Reihe nach zu verschieben, bis er beim Statusanzeiger-Formular landet. Allerdings wird der der Anwender beim Betätigen einer x-beliebigen Taste mit dem Info konfrontiert und aus die Maus. YES ! Und es geht noch geiler: In dmttrial.mdb wurde das Erscheinungsbild des Statuszeigers an das der dezent vertieften Anzeigefelder für 'ÜB' usw. rechts unten in der Statusleiste angepaßt. Die komplette Geschichte kann über einen simplen Sub-Aufruf erfolgen, in dem ein Text übergeben und sogar der Darstellungsmodus (Grün auf rot oder Access 2.0-Balken) gewählt werden kann. Einziger Wermutstropfen: Bei schnellen Werte-Folgen, bei denen kein Fokus-Erhalt für ein Steuerelement eintritt, wird der Anzeiger nicht aktualisiert; hier hilft dann nur ein DoEvents oder ein Form.Repaint, was bei dem kleinen Zeiger aber zu einer etwas flackrigen Darstellung führt. Klar, daß auch hier der eine oder andere Wermutstropfen überläuft: Beim erfolgreichen Einsatz des Status-Anzeige-Formulares im Laser-Job-Manager (dort wird für jeden Auftragsdatensatz der Status ermittelt und angezeigt; ein Klicken auf den Fort-schrittsbalken bringt ein Auswertungsformular zum Vorschein, das dem Anwender Klartext-Informationen bietet) konnte ein Phänomen beobachtet werden, das schon beim Öffnen und Schließen modaler Submenü's a'la Stammdaten auftrat: Die feldbezogenen Statuszeilen-Eigenschaften eines Formulares schienen verloren gegangen zu sein; Symbolleisten und Access-eigenen Meldungen wurden zwar noch angezeigt, aber die Felder informieren nur noch mit einem stereotypen 'Bereit'. Bei den modalen Submenü's half ein
If ExistsForm("Haupt") Then
Forms!Haupt.SetFocus
End If
, aber beim Öffnen des Status-Anzeige-Formulares in Form_Open des Server-Formulares konnte dieser Effekt nur mit einem abschließendem
Me.SetFocusim Server-Formular beseitigt werden. Diese schönen Beispiele sollen aber nicht zu der Annahme verleiten, daß Access grundsätzlich dazu in der Lage ist, Code-Befehle auch auszuführen. * * * * Wenn's hart auf hart kommt, kann REPAINT einen entnervten Programmierer schon mal davor bewahren, sich oder seiner Umwelt Schaden anzutun, vorausgesetzt natürlich, daß ihn rechtzeitig die Eingebung trifft, daß genau dieses REPAINT die Lösung seines Problemes ist. Folgendes Beispiel: In einem Formular soll unter gewissen Umständen der Text eines Beschriftungsfeldes ('Label') durch einen anderen ersetzt werden. Erreicht wird dies durch einen Befehl a'la Labelname.Caption = "Neuer Text:" . Jetzt kann es dummerweiser passieren, daß die Abarbeitung dieser Programmzeile absolut ohne jede Auswirkungen bleibt. Der Befehl wurde zwar ausgeführt, ein code-mäßiges Abfragen des Caption-Inhaltes ergibt auch den neuen, zugewiesenen Text, nur angezeigt wird natürlich der alte. Genau dann ist es höchste Zeit für ein verschissenes Form.Repaint. Die Ursache liegt nach meiner unmaßgeblichen Meinung in einer Reihe von Problemen, die MS-ACCESS mit der Grafik-Engine von Windows 'GDI' (große dumme Idiotie) hat. Siehe auch das haarsträubende Problem mit dem Kopieren von Ventildatensätzen in hp_vhp.mdb (und auch das fehlerhafte Drucken von grauen Linien auf dem Ventildatenblatt). Nachdem das Formular des neuen Datensatzes Stück für Stück mit den Werten der Vorlage ausgefüllt wurde, hat Access doch glatt 10 % der Bildschirm-Daten unterschlagen. Erst nach dem Einblenden und Herum-Draggen diverser MessageBoxen wurden die von 'losgelassenen' MessageBoxen verdeckten Bildschirm-Bereiche korrekt dargestellt. Hier half selbst ein Repaint nichts, oder hätte ich etwa jedes Steuerelement einzeln repainten sollen ? Abhilfe konnte damals gefunden werden, indem die Übertragung im Symbolzustand erfolgte und das aktualisierte Formular danach einfach wiederhergestellt wurde. Hierzu hat sich nach mehrjähriger Arbeit sogar eine beinahe akzeptable Alternativ-Lösung ergeben: Eine selbstgeschriebene Routine Sub Redraw (F as Form) führt lediglich die beiden Anweisungen F.Visible=false und F.Visible=true aus, dann klappt's auch mit dem Fensterl'n.
Sub Redraw (F As Form)
' Ein Bildschirm-Refresh will um's Verrecken nicht ?
' Das woll'n wir doch mal sehen ...
F.Visible = False
F.Visible = True
End Sub
Im Zweifelsfall kann man das auch für alle sichtbaren Formulare durchführen:
Sub RedrawAllForms ()
' **** In Härtefällen Redraw aller sichtbaren Formulare ****
Dim i As Integer
For i = 0 To Forms.Count - 1
If Forms(i).Visible = True Then
Redraw Forms(i)
End If
Next i
End Sub
Obendrein könnte es ja passieren, daß ein Formular nicht nur nur bunte Smarties, sondern auch mal Daten anzeigt, die durch Programm-Routinen auch ab und zu aktualisiert werden müssen. So kann z.Bsp. in hp_vhp.mdb vom Formular KV_Lizenznehmer aus eine Routine zur Erstellung von Vollversions- und Update-Disketten gestartet werden, die letzten Endes auch den aktuellen Lizenznehmer-Datensatz aktualisieren muß. Prompt kommt es zur Fehlermeldung, daß der Datensatz durch 'eine andere Sitzung gesperrt' sei.
Dazu folgendes Beispiel aus Form KV_Diskettenerstellung 'Erstelle_Disketten':
Anmerkung: Die Anwendung wird immer exklusiv geöffnet !
' **** Aktualisiere Lizenznehmer-Datensatz ****
' Hier tritt das Problem auf, daß der Lizenznehmer-Datensatz nicht über
' ein Recordset, das die Tabelle direkt anspricht, aktualisiert werden
' kann, da er ja noch im Formular 'KV_Lizenznehmer' geöffnet ist. Dies
' würde einen Mehrbenutzerfehler erzeugen. Eine Contra-Programmierung
' über andere Lock-Verfahren fangen wir besser gar nicht erst an.
' Witzigerweise kann der Datensatz im Clone-Verfahren sehr wohl ak-
' tualisiert werden.
Set RS = Forms!KV_Lizenznehmer.RecordsetClone
RS.Bookmark = Forms!KV_Lizenznehmer.Bookmark
RS.Edit
RS("AktVersion") = LetzteVersion
RS.Update
RS.Close
' Requery, damit das Steuerelement AktVersion die Liste der zu diesem
' Datensatz gehörenden Lizenzen in der aktuellen Form anzeigt.
Forms!KV_Lizenznehmer.Requery
'=([bis]-[von])*24'wird hingegen auch in der Datenblattansicht singulär korrekt verarbeitet ! Sollten diffizile Umstände abgecheckt werden müssen, bleibt zu testen, ob das nicht durch eine Funktion bewerkstelligt werden kann, die den gewünschten Wert an die Eigenschaft Steuer-elementinhalt zurückgibt, wie z.B. folgende: Erstellen einer kleinen Funktion, die sogar NULL-Werte abcheckt und per DLookUp jeweils gültige Werte in Abhängigkeit vom Formular-ID, das übrigens selbst als Steuerelement nicht im Formular enthalten sein muß, zurückgibt. In der Eigenschaft Steuerelementinhalt des virtuellen Feldes wird '=Funktionsname()' eingetragen. Voila'. Die folgende Funktion gibt für ein virtuelles Feld eines Formulares, das sich nur auf eine IDs enthaltende Zuweisungstabelle bezieht, die Klartext-Information des Feldes 'Art' in der Stammdaten-Tabelle 'Stichworte' zurück, wenn 'ID_Stichwort' nicht NULL ist. So wird ein häßliches '#NAME' für die letzte Zeile 'neuer Datensatz' vermieden. Die zweite Bedingung stellt sicher, daß beim Anlegen eines neuen Datensatzes, für den noch kein Zähler-ID vergeben wurde, kein ungültiger Wert '#FEHLER' zurückgegeben wird. Die Funktion selbst wird als Variant deklariert, damit '#FEHLER' nicht auftaucht, wenn beim Anlegen eines Datensaztes per 'DoCmd RunSQL' zwar ein ID als Zähler vergeben wurde, aber noch kein Art-Wert besteht. Die Funktion nimmt einen in diesem Fall doch auftretenden NULL-Wert klaglos entgegen und verhält sich per der Rückübergabe an die Eigenschaft ControlSource, als ob nichts gewesen wäre.
Function Ermittle_Art () As Variant
On Error GoTo err_Ermittle_Art
Ermittle_Art = DLookup("Art", "Stichworte", "ID=" & Me![ID_Stichwort])
Exit Function
err_Ermittle_Art:
Exit Function
End Function
Jetzt muß nur sichergestellt werden, in welchen Fällen angezeigte Daten geändert werden können, und überprüft werden, ob ein explizites Requery notwendig ist (z.B. Anlegen oder Aktualisieren durch Merker in Form_AfterUpdate und Merker in Form_AfterDelConfirm. Bei Form_Close kann dieser Merker dann ausgewertet und ein Requery des Datenblatt-Sorgenkindes ausgeführt werden.
Fairerweise muß hinzugefügt werden, daß eine ausgeklügelte Abfrage, die bei etwaigen Manipulationen nur die gewünschten Daten verändert ( TESTEN ! ), u.U. Klimmzüge, wie oben angeführt, ersparen kann. Wenn diese Abfrage funktioniert, entfallen auch die #NAME und #FEHLER-Geschichten. -> siehe WISSEN.MDB, Formular 'Stichworte', Form_Open
=': ' & Titel & ', ' & Utitelist genau so eine Zeichenkette, die incl. dem Formelzeichen '=' Teilzeichenketten mit amerikanischen Hochkommata eingrenzt und (hoffentlich) bekannte Feldnamen einfach benennt.
Dim F As Form
Set F = Forms!Ventile_Filter
For i% = 0 To F.Count - 1
If Not IsNull(F(i%).Tag) Then
' Manipulations-Routine
End If
Next i%
Hier werden alle Steuerelemente des Formulares 'Ventile_Filter' (HP_VHP.MDB) überprüft. Das sind immerhin 111 ! Entsprechend einer sinnvollen Eigenschaft (hier 'Tag', der in diesem Fall gleichzeitig den Operator '=' oder 'LIKE' enthält, der beim Zusammenbauen eines variablen SQL-String's direkt verwendet werden kann.
Beim Zusammenbau eines solchen automatisch erstellten SQL-Strings gibt es natürlich wieder einmal eine Reihe von Problemen:
- Dezimalzahlen, die für den Anwender sowie auch (halb-)intern bei Übergaben und Anzeigen mit ',' als Komma versehen sind, werden bei Verarbeitung in Access-Basic falsch interpretiert. Solche Werte müssen zuerst einer String-Manipulation übergeben werden, die das Komma durch den amerikanischen Dezimal-'.' ersetzt:
If IsNumeric(ta$) Then
ta$ = Dezimal_to_US_String(ta$)
End If
Und hier nun die Funktion selbst:
Function Dezimal_to_US_String (Zahl As Variant) As String
If IsNumeric(Zahl) Then
k$ = ","
s$ = Zahl
p% = InStr(s$, k$)
If p% > 0 Then
Mid$(s$, p%, 1) = "."
End If
Dezimal_to_US_String = s$
Else
Beep: MsgBox "Das Argument '" + Zahl + "' ist keine gültige Zahl !", 16, "Dezimal_to_US_String"
End If
End Function
- An einen derartig automatisch erzeugten SQL-String müssen natürlich auch die Namen der Tabellen-Felder übergeben werden. Hier kann es bei Namen wie 'GROUP', die u.U. mit reservierten Wörtern identisch sind, zu Problemen kommen. Um das zu vermeiden, sollte der Feldname mit '[' und ']' eingeschlossen werden !
Wenn wie in HP_VHP.MDB ein solcher String (teilweise) angezeigt werden soll, kann das aber auch Scheisse aussehen. Um diese Zeichen für die Galerie wieder zu entfernen, kann eine Routine wie folgt eingesetzt werden, die ein bestimmtes Zeichen aus einer Zeichenkette entfernt:
' Entferne aus r$ alle "[" und "]"
Do While InStr(r$, "[") > 0
p% = InStr(r$, "[")
s1$ = Left$(r$, p% - 1)
s2$ = Right$(r$, Len(r$) - p%)
r$ = s1$ + s2$
Loop
Dieses Beispiel findet sich in allgemeiner und ausgereifter Form in der Funktion ClearStringFrom wieder.
* * * *
Steuerelemente können auch auf ihren Typ (Objekttyp) hin geprüft werden:
Sub Art_des_Steuerelementes (C As Control)
If TypeOf C Is BoundObjectFrame Then s$ = "BoundObjectFrame: Gebundenes ObjektFeld": Exit Sub
If TypeOf C Is CheckBox Then s$ = "CheckBox: Kontrollkästchen": Exit Sub
If TypeOf C Is ComboBox Then s$ = "ComboBox: Kombinationsfeld": Exit Sub
If TypeOf C Is CommandButton Then s$ = "CommandButton: Befehlsschaltfläche": Exit Sub
If TypeOf C Is Graph Then s$ = "Graph: Diagramm": Exit Sub
If TypeOf C Is Label Then s$ = "Label: Bezeichnungsfeld": Exit Sub
If TypeOf C Is Line Then s$ = "Line: Linie": Exit Sub
If TypeOf C Is ListBox Then s$ = "ListBox: Listenfeld": Exit Sub
If TypeOf C Is ObjectFrame Then s$ = "ObjectFrame: Objektfeld": Exit Sub
If TypeOf C Is OptionButton Then s$ = "OptionButton: Optionsfeld": Exit Sub
If TypeOf C Is OptionGroup Then s$ = "OptionGroup: Optionsgruppe": Exit Sub
If TypeOf C Is PageBreak Then s$ = "PageBreak: Seitenumbruch": Exit Sub
If TypeOf C Is Rectangle Then s$ = "Rectangle: Rechteck": Exit Sub
If TypeOf C Is SubForm Then s$ = "SubForm: Unterformular": Exit Sub
If TypeOf C Is SubReport Then s$ = "SubReport: Unterbericht": Exit Sub
If TypeOf C Is TextBox Then s$ = "TextBox: Textfeld": Exit Sub
If TypeOf C Is ToggleButton Then s$ = "ToggleButton: Umschaltfläche": Exit Sub
s$ = "'" + C.Name + "' ist vom Typ " + s$
MsgBox s$
End Sub
Ebenso wie oben beschrieben können die Felder eines Suche-Formulares auch wieder auf z.B. einen vergebenen Tag zurückgesetzt oder automatisch gelöscht werden:
For i% = 0 To Me.Count - 1
If Not IsNull(Me(i%).Tag) Then
Me(i%) = Me(i%).DefaultValue
End If
Next i%
Für das Zurücksetzen ausgefüllter Dialoge kann folgendes verwendet werden:
On Error Resume Next
Dim i As Integer
For i = 0 To Me.Count - 1
If Not IsNull(Me(i).DefaultValue) Then
Me(i) = Me(i).DefaultValue
Else
Me(i) = Null
End If
Next i
Me!Institution.SetFocus
Der Errorhandler erlaubt wahllos durch alle Steuerelemente zu gehen, Textfelder werden geleert und Optionsgeschichten erhalten wieder ihren Standardwert. Sonderwünsche können bei Bedarf dann immer noch extra verarztet werden.
Um den sprichwörtlichen Vogel wieder mal abzuschießen, kann auch hier noch eine Steigerung stattfinden:
Hier werden die Felder eines Suche-Formulares samt denen eines eingebetteten Unterformulares gelöscht. Die explizite Namensnennung weiter unten muß natürlich angepaßt werden.
On Error Resume Next
Dim i As Integer, F As Form
Set F = Me
Clear_Form_Fields:
For i = 0 To F.Count - 1
If Not IsNull(F(i).DefaultValue) Then
F(i) = F(i).DefaultValue
Else
F(i) = Null
End If
Next i
If F.Name = "Suche_UF_Institution" Then
Me!og_Suche_nach.SetFocus
Else
Set F = Me.UF.Form
GoSub Clear_Form_Fields
End If
Dim s As String
s = Me!Formel
s = s & Wert
Me!Formel = s
Me!Formel.SetFocus
SendKeys "{F2}", True
Sub Form_Current ()
On Error GoTo err_FC
Dim RS As Recordset, PC As Control, iFuck As Integer
Set PC = Screen.PreviousControl
Set RS = Me!UF_LR.Form.RecordsetClone
RS.MoveFirst
Do While Not RS.EOF
RS.MoveNext
Loop
RS.Close
If iFuck = True Then PC.SetFocus
Exit Sub
err_FC:
If Err = 2455 Then
iFuck = True
Me!UF_LR.SetFocus
Resume
Else
Fehler "Form_Current"
Exit Sub
End If
End Sub
Für Access selbst würde es genügen, mit exit sub dieses traurige Tal der Tränen zu verlassen, aber da ja ein explizites Durchlaufen der Recordsetclone-Datensätze erforderlich ist, muß der Umweg über PreviousControl (ActiveControl macht in Form_Current einen Fehler) und Fokus hin und her gegangen werden.
quasi-manuell alle Formular-Datensätze durchlaufen werden. Am Ende solcher Durchläufe stellt
DoCmd.GoToRecord acActiveDataObject, ,acFirstwieder den ersten Datensatz als aktuellen ein. Weniger bewunderswert ist hingegen die Tatsache, daß Code teilweise nicht stapelweise, sondern mit Quasi-rekursiven Sprüngen in sich selbst ausgeführt wird und dann so Sachen passieren, daß Kontroll-Variablenwerte, die in einer Zeile gesetzt wurden (und per Debug auch ihren richtigen Wert besitzen) nach so einem "Geister"-Sprung vom selbigen nichts mehr wissen.
KeyCode = Navigate_in_MultipleData_SubForm(Me, "first", KeyCode, Shift)
"first" wird beim letzten Steuerelement durch "last" ersetzt.
Und, um noch eins draufzusetzen, jetzt auch mit gefaktem Reihenfolgen-Imitat beim Hingehen in das Unterformular-Steuerelement (vorwärts und rückwärts), welches sich selbst ja den aktiven Unterformular-Datensatz innerhalb eines Mutterformular-Datensatzes merkt. Obendrein wird, wenn Datensätze rückwärts durchlaufen werden, im Unterformular immer der erste Datensatz eingestellt. Diese beiden Eigenschaften müssen geändert werden, damit der Anwender ein konsistentes Anwendungsverhalten sieht. Angepackt wird dieses Problem im Ereignis Beim_Hingehen des Unterformular-Steuerelementes.
Sub UF_Kommunikation_Enter ()
Enter_MultipleData_SubForm Me!UF_Kommunikation, "Art", "Wert"
End Sub
Die aufgerufene Routine arbeitet jetzt mit Unterfomular-Bookmarks, da das verwendete SendKeys wieder mal Probleme im Wechsel mit Tastatur- und Mausbedienung machte.
Sub Enter_MultipleData_SubForm (C As Control, sC_First As String, sC_Last As String)
On Error GoTo err_Enter_MultipleData_SubForm
Dim RS As Recordset, sBM As String
Set RS = C.Form.RecordSetClone
If Screen.PreviousControl.TabIndex < C.TabIndex Then
C.Form(sC_First).SetFocus
RS.MoveFirst
sBM = RS.Bookmark
C.Form.Bookmark = sBM
Else
C.Form(sC_Last).SetFocus
RS.MoveLast
sBM = RS.Bookmark
C.Form.Bookmark = sBM
End If
Exit Sub
err_Enter_MultipleData_SubForm:
If Err <> 3021 Then
Fehler "Enter_MultipleData_SubForm"
End If
Exit Sub
End Sub
Function Navigate_in_MultipleData_SubForm (F As Form, sWhich As String, KeyCode As Integer, Shift As Integer)
On Error GoTo err_Navigate_in_MultipleData_SubForm
' **** überwindet in Unterformular-Steuerelementen die ****
' **** Navigationshürde, indem im Ereignis KeyDown des ****
' **** Steuerelementes die Tasten Pfeil, Tab und Enter ****
' **** abgefangen und hier auf (Shift) + Ctrl + Tab um-****
' **** geleitet werden. geht so schon i.O. ****
Dim iRet As Integer
' **** Wurde eine der Navigationstasten gedrückt ? ****
If KeyCode = 37 Or KeyCode = 38 Or KeyCode = 39 Or KeyCode = 40 Or KeyCode = 9 Or KeyCode = 13 Then
iRet = First_Or_Last_FormRecord(F)
' **** und wenn ja, welche und sind wir überhaupt am ****
' **** oberen oder unteren Ende der Datensatzgruppe ? ****
' **** Ausnahme verarzten ****
If iRet = 1 And F.RecordSetClone.RecordCount = 0 Then
If (KeyCode = 37 Or KeyCode = 38 Or (KeyCode = 9 And Shift = SHIFT_MASK)) Then
iRet = -1
End If
End If
If (KeyCode = 37 Or KeyCode = 38 Or (KeyCode = 9 And Shift = SHIFT_MASK)) And sWhich = "first" And iRet = -1 Then
If (KeyCode = 37 Or KeyCode = 38) And IsNavigationMode() = False Then
Navigate_in_MultipleData_SubForm = KeyCode
Else
Navigate_in_MultipleData_SubForm = 0
SendKeys "+^{TAB}", True ' **** rückwärts aus Unterformular springen ****
DoEvents
DoCmd SelectObject A_FORM, Screen.ActiveForm.Name
End If
ElseIf (KeyCode = 39 Or KeyCode = 40 Or (KeyCode = 9 And Shift = 0) Or KeyCode = 13) And sWhich = "last" And iRet = 1 Then
If (KeyCode = 39 Or KeyCode = 40) And IsNavigationMode() = False Then
Navigate_in_MultipleData_SubForm = KeyCode
Else
Navigate_in_MultipleData_SubForm = 0
SendKeys "^{TAB}", True ' **** vorwärts aus Unterformular springen ****
DoEvents
DoCmd SelectObject A_FORM, Screen.ActiveForm.Name
End If
Else
Navigate_in_MultipleData_SubForm = KeyCode
End If
Else
Navigate_in_MultipleData_SubForm = KeyCode
End If
Exit Function
err_Navigate_in_MultipleData_SubForm:
Fehler "Navigate_in_MultipleData_SubForm"
Exit Function
End Function
First_Or_Last_FormRecord ermittelt, ob ein Formulardatensatz evtl. der erste
Private Function First_Or_Last_FormRecord (F As Form) As Integer
' **** stellt fest, ob es sich bei dem aktuellen ****
' **** Formulardatensatz um den ersten oder den ****
' **** letzten handelt. ****
' **** Es muß geprüft werden, ob in dem Formular ****
' **** neue Daten eingegeben werden können. ****
On Error GoTo err_First_Or_Last_FormRecord
Dim RS As Recordset, v As Variant
Dim iNewDataPossible As Integer
If F.AllowEditing < 3 Then ' EOF tritt bereits beim
iNewDataPossible = True ' letzten Datensatz ein, obwohl
End If ' noch ein neuer im Formular wartet.
Set RS = F.RecordSetClone
RS.Bookmark = F.Bookmark
RS.MovePrevious
If RS.BOF Then
First_Or_Last_FormRecord = -1 ' erster Datensatz
End If
RS.MoveNext
RS.MoveNext
If RS.EOF And iNewDataPossible = False Then
First_Or_Last_FormRecord = 1 ' letzter Datensatz ohne neue Daten
End If
Exit Function
err_First_Or_Last_FormRecord:
If Err = 3021 And iNewDataPossible = True Then
If F.Dirty = True Then
First_Or_Last_FormRecord = -1 ' Neueingabe im ersten Datensatz
Else
First_Or_Last_FormRecord = 1 ' letzter Datensatz mit neuen Daten ohne Eingabe
End If
Else
Fehler "First_Or_Last_FormRecord"
End If
Exit Function
End Function
Beinahe ein Kleinod: IsNavigationMode tut genau das, was es auch verspricht, nämlich den Navigationsmodus vom Editiermodus unterscheiden.
Private Function IsNavigationMode ()
On Error GoTo err_IsNavigationMode
Dim C As Control
Set C = Screen.ActiveControl
If C.SelLength = Len(C) Then
IsNavigationMode = True
End If
Exit Function
err_IsNavigationMode:
If Err = 94 Then
IsNavigationMode = True
Else
Fehler "IsNavigationMode"
End If
Exit Function
End Function