EdgeBrowser - Ein Slider Control
Access kennt keinen Schieberegler - der acEdgeBrowser schon. Dieser Artikel zeigt, wie ein HTML-Slider im Browser-Steuerelement aufgebaut wird, wie der Wert zurück nach VBA gelangt und warum der richtige Zeitpunkt dabei wichtiger ist als die Geschwindigkeit.
Zwei Welten, eine Brücke
Access hat keinen Schieberegler. Der acEdgeBrowser dagegen zeigt jedes Control an, das ein moderner Browser kennt - also auch ein <input type="range">. Die Anzeige ist der einfache Teil. Interessant wird die Frage, wie der Wert aus der Seite zurück nach VBA kommt.
Denn hier treffen zwei getrennte Welten aufeinander:
| Access / VBA | WebView2 / HTML + JavaScript | |
|---|---|---|
| Sprache | VBA | JavaScript |
| Ausführung | ein einziger UI-Thread | eigener Prozess, eigener Speicher |
| Arbeitsweise | Schritt für Schritt, wartet auf jede Antwort | ereignisgesteuert, wartet auf niemanden |
| Takt | so schnell wie der Code | 60 Bilder pro Sekunde, ein Frame = 16,7 ms |
Beide Seiten teilen sich weder Speicher noch Variablen, und sie können sich nicht gegenseitig aufrufen. Was fehlt, ist eine Brücke.
Was der acEdgeBrowser bereitstellt
Das Steuerelement ist nicht die vollständige WebView2-API. Verfügbar ist:
| Aufgabe | Im acEdgeBrowser |
|---|---|
| JavaScript ausführen | ExecuteJavascript - ohne Rückgabewert, günstig |
| Wert aus der Seite lesen | RetrieveJavascriptValue - synchron, teuer |
| Seite ruft VBA auf | gibt es nicht - kein Web-Message-Event |
| Navigation | Navigate "https://msaccess/" & <Pfad> |
| Rückmeldung nach dem Laden | DocumentComplete - feuert zweimal pro Navigation |
| Maus-Ereignisse | Click, MouseUp(Button, Shift, X, Y) |
Die dritte Zeile prägt die gesamte Architektur: Die Seite kann nicht anklopfen. Access muss nachsehen - und entscheidet damit selbst, wann Verkehr über die Brücke läuft.
Der Aufbau: drei Module
| Modul | Aufgabe |
|---|---|
clsWebBridge | Der generische Transport. Diese Klasse ist für jedes webControl identisch - Slider, ComboBox, TreeView. Sie kennt keinen Slider. |
clsSlider | Die Fachlogik. Minimum, Maximum, Vorgabewert, Farben; macht aus rohem Browser-Verkehr ein einziges ValueChanged-Ereignis. |
modSliderHtml | Die Seite selbst, als VBA-String im Projekt. Kein externes HTML-File, das verloren gehen oder nicht zum Code passen kann. |
modWebBridge | Zustandslose Helfer: zentrale Fehlermeldung, JSON-Parser, UTF-8-Datei schreiben. |
Dazu kommt das Hostformular mit einem acEdgeBrowser-Steuerelement und einem Textfeld für die Ausgabe.
Die Seite: HTML, das VBA erzeugt
modSliderHtml liefert die komplette Seite - CSS, Body und JavaScript - als String. Weil die Seite im Projekt liegt, kann sie nicht von der Codebasis abweichen.
Marker statt Platzhalter
In der Seite stehen an den variablen Stellen Marker, die für sich genommen gültige JavaScript-Kommentare sind. clsSlider ersetzt sie vor dem Laden:
sHtml = Slider_EmbeddedHtml()
sHtml = Replace(sHtml, "/*__STYLE__*/{}", StyleJson())
sHtml = Replace(sHtml, "/*__MIN__*/0", CStr(m_lngMin))
sHtml = Replace(sHtml, "/*__MAX__*/100", CStr(m_lngMax))
sHtml = Replace(sHtml, "/*__DEFAULT__*/50", CStr(m_lngDefault))
sHtml = Replace(sHtml, "/*__SHOWFIELDS__*/false", IIf(m_bolShowFields, "true", "false")) Das hat zwei Vorteile. Erstens stimmt bereits das erste gezeichnete Bild - Bereich, Farben und Feldsichtbarkeit stehen im HTML, es muss nichts nachgereicht werden. Zweitens lässt sich die Seite unverändert im normalen Browser öffnen: Ohne Ersetzung sind die Marker schlicht Kommentare, und die Vorgabewerte greifen.
Vor Init setzen ist günstiger: ShowFields und die vier Farbeigenschaften werden vor dem Aufruf von Init in das HTML eingebacken. Wer sie danach setzt, erreicht dasselbe - dann aber über einen Aufruf in die geladene Seite hinein.
Der Briefkasten in der Seite
Weil die Seite VBA nicht rufen kann, legt sie ihre Nachrichten in einen Briefkasten. Access holt sie dort ab, wenn es passt. Der Briefkasten ist ein kleines JavaScript-Objekt, das modSliderHtml mit ausliefert:
s = s & " var WebBridge={queue:[]," & vbLf
s = s & " send:function(a,d){var o={action:a};if(d){for(var k in d)o[k]=d[k];}" & _
"for(var i=0;i<this.queue.length;i++){if(this.queue[i].action===a){this.queue[i]=o;return;}}" & _
"this.queue.push(o);}," & vbLf
s = s & " collect:function(){if(!this.queue.length)return '';var r=JSON.stringify(this.queue);this.queue=[];return r;}};" & vbLf send() wirft einen Brief ein, collect() leert den Kasten in einem Zug und gibt den Inhalt als JSON zurück. Der entscheidende Kniff steckt in send(): Ein Brief ersetzt einen bereits wartenden Brief derselben Art, statt sich dahinter anzustellen. Der Kasten enthält damit immer genau einen Brief, und der trägt immer den aktuellen Wert.
Der Weg des Wertes
Was die Seite tut
Beim Ziehen feuert das input-Ereignis einmal pro Mausbewegung. Die Seite tut dann zweierlei - beides bleibt vollständig innerhalb des Browsers:
s = s & " slider.addEventListener('input',function(){var v=parseInt(this.value,10);" & _
"showValue(v);WebBridge.send('change',{value:v});});" & vbLf showValue() merkt sich die Zahl nur und lässt sie im nächsten Animationsframe zeichnen - so wird die Anzeige höchstens einmal pro Bild aktualisiert, statt mehrmals pro Bild vergeblich. send() frischt den wartenden Brief auf.
Warum bei jeder Bewegung gesendet wird: Weil send() ersetzt statt anhängt, kostet das nichts - es bleibt bei einem einzigen Eintrag. Der Nutzen ist, dass der Brief schon bereitliegt, bevor der Anwender loslässt. Access findet also unabhängig vom Zeitpunkt immer den aktuellen Wert vor.
Beim Loslassen feuert zusätzlich change. Dort wird der Wert endgültig übernommen und in das Feld vba_output_value geschrieben.
Wann VBA nachsieht
RetrieveJavascriptValue ist synchron: Der Aufruf hält den Access-UI-Thread an und zwingt den Renderer, mitten im Zeichnen eines Frames stehenzubleiben. Er ist nicht langsam, aber er ist teuer - und deshalb entscheidet die Brücke sehr bewusst, wann er stattfindet.
Zwei Windows-API-Funktionen liefern die nötige Information:
#If VBA7 Then
Private Declare PtrSafe Function GetLastInputInfo Lib "user32" (ByRef plii As LASTINPUTINFO) As Long
Private Declare PtrSafe Function GetTickCount Lib "kernel32" () As Long
Private Declare PtrSafe Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Integer
#Else
Private Declare Function GetLastInputInfo Lib "user32" (ByRef plii As LASTINPUTINFO) As Long
Private Declare Function GetTickCount Lib "kernel32" () As Long
Private Declare Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Integer
#End If GetAsyncKeyState beantwortet die Frage “ist die linke Maustaste gerade gedrückt?” - und damit “wird gerade gezogen?”:
Private Function MouseIsDown() As Boolean
On Error Resume Next
MouseIsDown = (GetAsyncKeyState(C_VK_LBUTTON) < 0)
End Function Weil das bei jedem Timer-Schlag geprüft wird, kennt die Brücke auch die Flanke von gedrückt nach losgelassen. Diese Flanke ist das Ende des Ziehens. Der Kern von HandleTimer besteht daher aus vier Fällen:
' ---- (1) Die Seite ist noch nicht da: dafuer sorgen, dass sie es wird ----
If Not m_bolReady Then
CheckLoadStalled
GoTo END_PROCEDURE
End If
' ---- (2) Taste gedrueckt: ein Drag wird niemals unterbrochen ----
bolDown = MouseIsDown()
If bolDown Then
m_bolMouseWasDown = True
GoTo END_PROCEDURE
End If
' ---- (3) Taste gerade losgelassen: Interaktion vorbei, Wert SOFORT holen ----
If m_bolMouseWasDown Then
m_bolMouseWasDown = False
ReadQueue
GoTo END_PROCEDURE
End If
' ---- (4) Tastatur und Rest: erst nach einer Eingabepause lesen ----
If TickDelta(m_lngLastRead, GetTickCount()) < m_lngMaxWaitMs Then
If IdleMs() < m_lngGateMs Then GoTo END_PROCEDURE
End If
ReadQueue Fall 2 sorgt dafür, dass während des Ziehens kein einziger Lesevorgang stattfindet. Fall 3 holt den Wert im ersten Schlag nach dem Loslassen. Fall 4 fängt alles ab, was keine Maus ankündigt - vor allem die Bedienung per Tastatur: gelesen wird, sobald der Anwender 150 ms still war, spätestens aber nach 3 Sekunden.
Weil ein Schlag damit fast nichts kostet, darf der Timer schnell laufen: 60 ms. Das ist zugleich die maximale Verzögerung, mit der ein Wert im Formular erscheint.
Liefert das Steuerelement MouseUp oder Click, kann das Hostformular zusätzlich FlushNow aufrufen und den Wert noch einen Schlag früher holen. Notwendig ist das nicht - die Brücke erkennt das Ende des Ziehens ohnehin selbst.
Was aus dem Brief wird
ReadQueue ist die einzige Stelle, die tatsächlich in die Seite hineingreift. Ein Aufruf leert den gesamten Briefkasten, egal ob ein oder zwanzig Einträge darin liegen:
sRaw = Nz(m_objWebCtrl.RetrieveJavascriptValue("WebBridge.collect()"), "")
If Len(sRaw) = 0 Then GoTo END_PROCEDURE
sJson = UnwrapJsString(sRaw)
If Len(sJson) = 0 Or sJson = "[]" Or sJson = "null" Then GoTo END_PROCEDURE
Set colCmd = SplitJsonArray(sJson)
For Each vItem In colCmd
sCommand = CStr(vItem)
sAction = ExtractJSONValue(sCommand, "action")
If Len(sAction) > 0 Then
RaiseEvent CommandReceived(sAction, sCommand)
End If
Next vItem clsSlider nimmt dieses Ereignis entgegen, prüft den Inhalt und reicht ihn als typisiertes Ereignis an das Formular weiter:
If sAction = "change" Then
sValue = ExtractJSONValue(sPayload, "value")
If IsNumeric(sValue) Then
RaiseEvent ValueChanged(ClampLong(CLng(sValue), m_lngMin, m_lngMax))
End If
End If Alles, was über die Brücke kommt, ist Text. Deshalb wird geprüft, ob wirklich eine Zahl vorliegt, und der Wert anschließend in den gültigen Bereich geklemmt.
Der Lebenszyklus
Öffnen
Form_Load bereitet nur vor und startet einen Einmal-Schuss. Geladen wird die Seite erst aus dem ersten Timer-Schlag heraus, wenn das Formular vollständig aufgebaut ist:
WireEvents
m_bolStarted = False
Me.TimerInterval = C_PACE_BOOT ' 200 ms If Not m_bolStarted Then
m_bolStarted = True
Me.TimerInterval = C_PACE ' 60 ms
StartSlider
GoTo END_PROCEDURE
End If StartSlider erzeugt clsSlider, setzt die Eigenschaften und ruft Init. Die Brücke schreibt die fertige Seite als UTF-8-Datei nach %TEMP% und navigiert dorthin. Jeder Ladevorgang bekommt dabei eine eigene Adresse, ältere Dateien desselben Controls werden vorher entfernt:
Kill Environ$("TEMP") & "\ws_" & m_sControlName & "_*.html"
m_sTempFile = NextTempHtmlPath(m_sControlName, CStr(GetTickCount()) & "_" & m_lngLoadTries)
If Not WriteUtf8File(m_sTempFile, m_sHtml) Then
ErrBox 9102, "Could not write temp HTML file.", C_MODULE, C_PROC, "Path: " & m_sTempFile
GoTo END_PROCEDURE
End If
m_bolReady = False
m_bolDocExpected = True
m_lngNavStart = GetTickCount()
m_objWebCtrl.Navigate "https://msaccess/" & m_sTempFile Mit m_lngNavStart beginnt die Überwachung des Ladevorgangs. Meldet die Seite innerhalb von 1,5 Sekunden kein DocumentComplete, wird die Navigation wiederholt - bis zu dreimal, danach folgt eine eindeutige Meldung:
If Not m_bolDocExpected Then GoTo END_PROCEDURE
If TickDelta(m_lngNavStart, GetTickCount()) < C_LOAD_WAIT_MS Then GoTo END_PROCEDURE
If m_lngLoadTries >= C_LOAD_MAXTRY Then
m_bolDocExpected = False
ErrBox 9103, "The page never reported DocumentComplete - the browser control stayed " & _
"empty.", C_MODULE, C_PROC, "Control: [" & m_sControlName & "]"
GoTo END_PROCEDURE
End If
m_lngLoadTries = m_lngLoadTries + 1
NavigateNow Trifft DocumentComplete ein, wird nur die erste Meldung ausgewertet - das Ereignis feuert zweimal pro Navigation. Ab diesem Moment ist die Brücke bereit.
Schließen
Beim Schließen zählt die Reihenfolge. Form_Unload stoppt als Erstes den Timer, danach gibt Destroy die Brücke frei - Flags zuerst, Referenz zuletzt:
On Error Resume Next
m_bolReady = False
m_bolDocExpected = False
m_bolBusy = False
m_bolMouseWasDown = False
Set m_objWebCtrl = Nothing
If Len(m_sTempFile) > 0 Then Kill m_sTempFile Ab der ersten Zeile spricht die Klasse das Steuerelement nicht mehr an - weder lesend noch schreibend, und auch die Ladeüberwachung ruht. Der zuletzt eingestellte Wert ist zu diesem Zeitpunkt längst abgeholt.
Kosten der einzelnen Schritte
| Schritt | Größenordnung | Bewertung |
|---|---|---|
| HTML im Speicher zusammenbauen | ca. 1 ms | vernachlässigbar |
| Temp-Datei schreiben (ADODB.Stream) | 2 - 5 ms | vernachlässigbar |
ExecuteJavascript (Wert hineinschieben) | ca. 1 ms | niemand wartet auf Antwort |
RetrieveJavascriptValue (ein Lesevorgang) | 1 - 10 ms | hält beide Seiten an |
| Seite laden, parsen, rendern | 150 - 400 ms | einmalig beim Öffnen |
Zur Einordnung: Das sind Größenordnungen, keine Messreihe - sie hängen von Maschine, WebView2-Version und Seitengröße ab. Das Verhältnis zwischen den Zeilen ist die Aussage, nicht die absolute Zahl.
Der Seitenaufbau dominiert alles andere, lässt sich aber nicht beeinflussen: Dort baut eine vollständige Browser-Engine in einem eigenen Prozess ein Dokument auf. Genau deshalb wird dieser Preis einmalig beim Laden bezahlt und alles Weitere über die günstige Richtung abgewickelt. Der Lesevorgang dagegen ist klein - und er ist der einzige Posten, dessen Zeitpunkt frei wählbar ist.
Eigenschaften, Methoden, Ereignis
clsSlider ist die Schnittstelle für das Hostformular:
| Element | Bedeutung |
|---|---|
Init web, sName, lngMin, lngMax, lngDefault | Bindet das Steuerelement und lädt die Seite. Einmal aufrufen. |
HandleTimer | Aus Form_Timer aufrufen. Die Brücke entscheidet selbst, ob der Moment passt. |
HandleNavigationComplete | Aus DocumentComplete aufrufen. |
FlushNow [bolForce] | Optional aus MouseUp / Click aufrufen - holt den Wert ohne Wartezeit. |
Reset | Setzt auf den Vorgabewert zurück, live und ohne Neuladen. |
ApplyRange lngMin, lngMax, lngDefault | Ändert den Bereich zur Laufzeit, live und ohne Neuladen. |
Destroy | Aus Form_Close aufrufen. |
ShowFields | Blendet die vba_*-Felder und die Linie darunter ein oder aus. |
BackColor, ForeColor, AccentColor, ObjectColor | Farben als #rrggbb. Aus AccentColor werden die hellere Zieh-Variante und der Leuchtring automatisch abgeleitet. |
DefaultValue | Liest den aktuellen Vorgabewert. |
ValueChanged(lngValue) | Das eine Ereignis, auf das das Formular hört. |
Zusätzlich lässt sich das Leseverhalten der Brücke justieren: PollGateMs ist die geforderte Eingabepause (Vorgabe 150 ms), PollMaxWaitMs die Obergrenze, nach der auch ohne Pause gelesen wird (Vorgabe 3000 ms).
Konventionen und Grenzen
| Punkt | Was zu beachten ist |
|---|---|
| Ereigniseigenschaften | Ein .cls-Import setzt sie nicht. Me.OnTimer = "[Event Procedure]" gehört in den Code, sonst feuert Form_Timer nie - fehlerfrei und stumm. |
DocumentComplete | Feuert zweimal pro Navigation. Nur der erste Aufruf zählt. |
| Farben | Nur #rrggbb wird akzeptiert. Die Prüfung ist zugleich der Schutz gegen eingeschleustes CSS oder JavaScript. |
| Temp-Dateien | Liegen in %TEMP%, ein Satz je Control-Name. Mehrere webControls auf einem Formular kommen sich nicht ins Gehege. |
| Drag & Drop | Natives HTML5-Drag arbeitet im gehosteten Browser nicht zuverlässig. Für eigene Controls: Maus-Ereignisse verwenden. |
| Late Binding | Durchgehend, inklusive ADODB.Stream. Keine Verweise nötig, 32 und 64 Bit laufen unverändert. |
Reihenfolge beim Schließen einhalten. Wer ein eigenes Hostformular baut: erst den Timer stoppen, dann Destroy aufrufen - und aus einem Steuerelement, das gerade abgebaut wird, nichts mehr lesen. Ein synchroner Zugriff in dieser Phase kann den WebView2-Host so hinterlassen, dass die nächste Instanz nicht mehr startet.
Einbindung
Vier Module importieren: modWebBridge, clsWebBridge, clsSlider, modSliderHtml. Auf das Formular kommt ein acEdgeBrowser-Steuerelement, hier webSlider, dazu ein Textfeld für die Ausgabe.
Der Rest ist überschaubar:
Private WithEvents m_objSlider As clsSlider
Private m_bolStarted As Boolean
Private Sub Form_Load()
Me.OnTimer = "[Event Procedure]"
Me.TimerInterval = 200 ' Einmal-Schuss: laedt gleich die Seite
End Sub
Private Sub Form_Timer()
If Not m_bolStarted Then
m_bolStarted = True
Me.TimerInterval = 60
Set m_objSlider = New clsSlider
m_objSlider.ShowFields = True
m_objSlider.AccentColor = "#cc0000"
m_objSlider.Init Me.webSlider, "webSlider", -50, 50, 0
Exit Sub
End If
If Not m_objSlider Is Nothing Then m_objSlider.HandleTimer
End Sub
Private Sub webSlider_DocumentComplete(URL As Variant)
On Error Resume Next
If Not m_objSlider Is Nothing Then m_objSlider.HandleNavigationComplete
End Sub
Private Sub m_objSlider_ValueChanged(ByVal lngValue As Long)
Me.txtOutput.Value = lngValue
End Sub
Private Sub Form_Close()
On Error Resume Next
Me.TimerInterval = 0
If Not m_objSlider Is Nothing Then m_objSlider.Destroy
Set m_objSlider = Nothing
End Sub Optional lassen sich webSlider_MouseUp und webSlider_Click ergänzen, die jeweils m_objSlider.FlushNow aufrufen.
Für einen zweiten Slider auf demselben Formular genügt eine zweite clsSlider-Instanz mit einem eigenen Steuerelement und einem eigenen Namen - clsWebBridge legt die Temp-Datei je Control-Name an, und beide Instanzen arbeiten unabhängig voneinander.
Download
Voraussetzungen: Microsoft 365, Access 2024+, 32/64-bit




