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 SubProblem 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 SubDer "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 der
DEUTSCH. 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) ThenIn 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 FunctionEine 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 FunctionDer 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 SubInteressant 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 SubIch 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 SubUnd 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 SubDer 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 SubVergleichbar 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 SubDiese 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 Subund 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 Subund für Navigationsfetischisten, die auf <Pfeil auf/ab> angewiesen sind:
Sub BNr_KeyDown (KeyCode As Integer, Shift As Integer) NavArrowKeysUpDown KeyCode End Subjetzt 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 Subund
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 Subsowie
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 Functionund 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 SubMan 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 ifMan 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 SubGenau 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 SubDas 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.NameErst 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 SubNun 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 SubUnd 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 SubWunderbar, 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 SubZu 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 Korrektur
Auflö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 RestoreIn 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!cmbSchlagwund 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 Subna 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 IfAuf 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 FunctionFü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 SubPerfekterweise 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 SubDas 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_NotInListResponse = 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 SubInteressant 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 SubHier 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 SubAlles 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 FunctionDie 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 Ifsorgt 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_AfterUpdateoder 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. Aufrufin 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 SubDie 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 IfNur 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 Ifdafü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 SubDa 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 IfRelevant 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 IfDas 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 = 0Das 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 ' Damitnicht 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 ifeinfü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 Ifsiehe 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 FunctionDiese 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 SubHier 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 iFieseliger 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 iHier 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 acCmdSaveRecordEin 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 BY
gedrü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 SubIm 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 SubObendrein 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 FunctionJetzt 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 IfUnd 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$ LoopDieses 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 SubEbenso 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.SetFocusDer 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 SubFü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 SubDie 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.First_Or_Last_FormRecord ermittelt, ob ein Formulardatensatz evtl. der erstegeht 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
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 FunctionBeinahe 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