Flackern bei Bildschirmaktualisierungen verhindern

Veröffentlicht: 27. Jul 2005

Von Mathias Schiffer

Die Microsoft Newsgroups sind eine Quelle schier unerschöpflichen Wissens, das nahezu auf Knopfdruck abrufbar ist: Hunderte deutschsprachige Entwickler vom Einsteiger bis zum Profi treffen sich hier täglich virtuell, um Fragen zu stellen oder zu beantworten. Auch themennahe Probleme, Ansichten und Konzepte werden miteinander diskutiert. Sie kennen die Microsoft Newsgroups noch nicht? Detaillierte Information für Ihre Teilnahme finden Sie auf der Homepage der Microsoft Support Newsgroups.

Diese Kolumne greift regelmäßig ein besonders interessantes oder häufig nachgefragtes Thema aus einer der Entwickler-Newsgroups auf und arbeitet es aus.

Auf dieser Seite

 Aus der Visual Basic Newsgroup microsoft.public.de.vb:
 Abschalten von "Full Window Drag"
 Neuzeichnen unterbinden per LockWindowUpdate
 Neuzeichnen unterbinden per WM_SETREDRAW

Aus der Visual Basic Newsgroup microsoft.public.de.vb:

Wie kann ich verhindern, dass Forms und Steuerelemente bei mehrfachen Aktualisierungen flackern?

Es gibt viele Situationen, in denen Steuerelemente oder Forms "flackern", weil ihre Inhalte aktualisiert werden. Das kann etwa ein ListBox-Steuerelement sein, das regelmäßig neue Einträge erhält, oder auch ein TreeView-Control. Auch eine Form, deren Steuerelemente bei einer Größenveränderung der Form angepasst werden sollen, neigt zum Flackern - sofern unter Windows die Darstellungsoption Fensterinhalt beim Ziehen anzeigen aktiviert ist: Während der Größenänderung wird dann das Resize-Ereignis der Form ständig gefeuert. Durch das wiederholte Neuzeichnen der Oberfläche entsteht dabei der unschöne Flackereffekt, der mit steigender Anzahl der sichtbaren Steuerelemente immer auffälliger wird.

 

Abschalten von "Full Window Drag"

Eher der Vollständigkeit halber vorab noch ein Tipp, der sich ausschließlich auf Forms bezieht (alle anderen Hinweise in diesem Artikel beziehen sich hingegen auf jegliche Fenster, nicht nur auf Forms).

Bei der Größenänderung von Forms tritt unser Problem nur dann auf, wenn der Anwender die Option Fensterinhalt beim Ziehen anzeigen aktiviert hat. Diese Einstellmöglichkeit findet sich im Systemsteuerungs-Applet Anzeige auf dem Reiter Effekte. Ist diese Option nicht aktiviert, wird bei Mausoperationen zum Verschieben und zur Größenveränderung von Forms lediglich ein angedeuteter Rahmen bewegt - erst nach Abschluss der Operation wird die betroffene Form einmalig neu gezeichnet.

Sie könnten also versuchen, das Problem dadurch anzugehen, dass Sie dem Anwender das Abschalten dieser Option empfehlen. Auch können Sie diese Einstellung für den Anwender aus Ihrem Code heraus ändern. Der folgende Code macht's möglich:

' --- API Deklarationen
  
Private Const SPI_GETDRAGFULLWINDOWS As Long = 38&  ' Auslesen der Einstellung
Private Const SPI_SETDRAGFULLWINDOWS As Long = 37&  ' Schreiben der Einstellung
Private Const SPIF_SENDWININICHANGE  As Long = &H2& ' Andere Anwendungen informieren
Private Const SPIF_UPDATEINIFILE     As Long = &H1& ' Wert im Benutzerprofil speichern
  
' SystemParametersInfo ermittelt oder setzt Systemparameter.
Private Declare Function SystemParametersInfo _
  Lib "user32" Alias "SystemParametersInfoA" ( _
  ByVal uAction As Long, _
  ByVal uParam As Long, _
  ByRef lpvParam As Any, _
  ByVal fuWinIni As Long _
  ) As Long
  
' ...
  
Private Function SetFullWindowDrag(ByVal ShowFullWindow As Boolean) As Boolean
' Aktiviert oder deaktiviert die Anzeige von Fensterinhalten während des
' Ziehens oder der Größenänderung von Anwendungsfenstern.
' Der Rückgabewert gibt Aufschluss darüber, ob der Aufruf zu einer Änderung
' gegenüber der vorherigen Einstellung geführt hat (True) oder nicht (False).
Dim lOldSetting As Long
Dim lSuccess As Long
  
  ' Bisherige Einstellung ermitteln:
  lSuccess = SystemParametersInfo(SPI_GETDRAGFULLWINDOWS, 0&, lOldSetting, 0)
  
  If lSuccess = 0 Then
    ' Der abfragende Aufruf war nicht erfolgreich
    ' (z.B. Windows 95 ohne "PLUS!"-Pack).
    Exit Function ' Rückgabewert bleibt False
  End If
  
  ' Eine Aktion ist nur dann notwendig, wenn sich der bisherige
  ' vom gewünschten Wert unterscheidet:
  If CBool(lOldSetting) <> ShowFullWindow Then
    ' Wert ändern und Erfolg prüfen:
    lSuccess = SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, Abs(CLng(ShowFullWindow)), _
                                    ByVal 0&, SPIF_UPDATEINIFILE Or SPIF_SENDWININICHANGE)
    If lSuccess Then
      SetFullWindowDrag = True
    End If
  End If
  
End Function

Nachteilig an dieser Lösungsidee ist jedoch nicht nur, dass der Anwender nicht die von ihm erwünschte Optik erhält. Schlimmer noch: Diese Einstellung bezieht sich generell auf alle Anwendungen, nicht nur auf Ihre. Der Schaden lässt sich begrenzen, indem man die Einstellung zum Start der Anwendung deaktiviert und zum Programmende hin wieder aktiviert.

 

Neuzeichnen unterbinden per LockWindowUpdate

Einem anderen Ansatz folgt die Idee, die Ausgabe von neu gezeichneten Grafikelementen zeitweise zu blockieren. Das bedeutet nicht, dass keine grafischen Elemente mehr gezeichnet werden können, sondern dass ein bereits gezeichnetes Element erst später dem Auge des Betrachters zugeführt wird.

Vergleichen Sie diese Idee etwa mit dem gängigen Vorgehen der aus dem Urlaub bekannten Straßenkünstler, die karikierend oder realistisch eine Zeichnung des Kunden anfertigen: Das Ergebnis bekommen Sie dort erst zu Gesicht, wenn das Werk fertig ist. Vom Zeichnen der vielen Linien und Farbschattierungen haben Sie nichts gesehen - Sie sehen nur das komplette Bild. Vielleicht hätten Sie gerne bei der Entstehung zugeschaut und sich so erklären können, warum die Passanten hinter dem Künstler die Entstehung Ihres Portraits mit einem breiten Grinsen quittierten? Am Bildschirm hingegen möchten Sie ganz sicher nicht infinitesimale Fortschritte einer eher trivialen Aufgabe betrachten können.

Für die Implementierung dieses Konzepts bietet das Win32 API die Funktion LockWindowUpdate an, die Sie bereits im MSDN Quickie "Neuzeichnen von Steuerelementen zeitweise unterbinden" kennen lernen konnten. Die Anwendung ist denkbar einfach:

' Deklaration für LockWindowUpdate:
Private Declare Function LockWindowUpdate _
  Lib "user32" ( _
  ByVal hWnd As Long _
  ) As Long
  
' ...
  
' Codebeispiel:
  
  ' Fenster sperren:
  Call LockWindowUpdate(Form1.hWnd)
  
  ' [... mehrere Zeichenoperationen ...]
  
  ' Fenster entsperren und neu zeichnen
  Call LockWindowUpdate(0)

Die einfache Funktion hat allerdings mehrere Nachteile:

  • Es gibt systemweit nur ein einziges Fenster, das so gesperrt werden kann. Ruft eine beliebige andere laufende Anwendung die Funktion mit einem Fensterhandle auf, so wird die bisherige Sperrung aufgehoben. Auch anders herum gilt: Rufen Sie die Funktion auf, verliert vielleicht ein anderes Fenster die notwendige Sperrung.

  • Unter bestimmten Umständen führt das Aufheben der Sperrung eines inzwischen verkleinerten Fensters mittels LockWindowUpdate dazu, dass statt der frei gewordenen Fläche das hinter dem betroffenen Fenster liegende Fenster, mitunter gleich der gesamte Desktop, neu gezeichnet wird. Das daraus resultierende Flackern der gesamten Benutzeroberfläche ist dem gesetzten Ziel natürlich diametral entgegengesetzt.

  • Ein derart gesperrtes Fenster lässt sich nicht verschieben.

Im Folgenden lernen Sie deswegen eine bessere Möglichkeit kennen.

 

Neuzeichnen unterbinden per WM_SETREDRAW

Das Windows API hält noch eine weitere Möglichkeit bereit, das Neuzeichnen grafischer Elemente in ein Fenster zeitweise zu unterbinden: die Fensternachricht WM_SETREDRAW. Dabei lassen sich beliebig viele Fenster so behandeln, Konflikte bleiben also ausgeschlossen. Flackereffekte bei Größenveränderungen sind damit gleichzeitig ausgeschlossen, da keinem anderen Fenster die eingesetzte Sperre "weggenommen" wird. Allein der dritte Nachteil der vorherigen Lösung bleibt identisch: Die Verschiebung eines gesperrten Fensters ist nicht möglich. Dies ist jedoch zu verkraften, da die Sperre ja zum Vermeiden eines Flackereffekts ohnehin jeweils nur für einen kurzen Moment eingesetzt werden soll.

Um die nützliche Nachricht an ein betroffenes Fenster schicken zu können, setzen Sie die wohl meistgenutzte API-Funktion ein, die Windows zu bieten hat: Sie übergeben an SendMessage das Handle des betroffenen Fensters, den Nachrichtenwert selber sowie eine 0 (entspricht in C dem Wert von FALSE) oder eine 1 (entspricht TRUE). Im ersten Fall wird jedes weitere Zeichnen als falsch unterbunden, bei Übergabe der 1 wird das Zeichnen wieder erlaubt. Der vierte Parameter bleibt unberührt, hier übergeben Sie immer den Long-Wert 0. Die Deklarationen der eingesetzten API-Funktionen und Konstantenwerte finden Sie im nachfolgenden Beispielcode.

Um ein Fenster zu blockieren, verwenden Sie SendMessage also folgendermaßen:

Call SendMessage(hWnd, WM_SETREDRAW, 0, ByVal 0&)

Soll ein Zeichnen in dem Fenster wieder möglich werden, heben Sie die zuvor gesetzte Sperre wie folgt wieder auf:

Call SendMessage(hWnd, WM_SETREDRAW, 1, ByVal 0&)

Leider ist es damit noch nicht ganz getan: Im Gegensatz zu LockWindowUpdate nämlich wird alleine durch das Aufheben der Sperrung noch kein Neuzeichnen ausgelöst. Ein Neuzeichnen der betroffenen Fläche ist zwar nicht zwingend notwendig, jedoch der Regelfall: Schließlich werden Sie wollen, dass die diversen "im Versteckten" vorgenommenen Änderungen letztlich auch sichtbar werden.

Am besten eignet sich für diesen Zweck die API-Funktion RedrawWindow. Sie erwartet im Regelfall eine RECT-Struktur, die die betroffene Fläche bezeichnen soll (alternativ kann auch eine Windows-Region benannt werden). Diese Werte lassen sich mithilfe der API-Funktionen GetWindowRect oder GetClientRect vom betroffenen Fenster ermitteln. Durch ein wenig Geschick bei der Deklaration der Zeichenfunktion ist dieser zusätzliche Aufwand jedoch zu vermeiden: Wird der Parameter, der die RECT-Struktur erwartet, als "Any" deklariert, ist die Ermittlung und Übergabe sowie die Deklaration des Datentyps RECT und der zugehörigen Hilfsfunktionen nicht notwendig. RedrawWindow zeichnet dann einfach das komplette Fenster neu - für unseren Zweck ist das ideal.

Zum Vergleich zunächst der klassische Ansatz:

' Der Datentyp RECT beschreibt ein Rechteck
Private Type RECT
  Left As Long
  Top As Long
  Right As Long
  Bottom As Long
End Type
  
' RedrawWindow zeichnet ein Fenster neu
Private Declare Function RedrawWindow _
  Lib "user32" ( _
  ByVal hWnd As Long, _
  ByRef lpRect As RECT, _
  ByVal hRegion As Long, _
  ByVal RedrawFlags As Long _
  ) As Long
  
' GetClientRect ermittelt den Nutzbaren Bereich eines Fensters
Private Declare Function GetClientRect _
  Lib "user32" ( _
  ByVal hWnd As Long, _
  ByRef lpRect As RECT _
  ) As Long
  
  ' ...
  
  ' Codebeispiel:
  Dim rc As RECT
  Call GetClientRect(Form1.hWnd, rc)
  Call RedrawWindow(Form1.hWnd, rc, 0, 0)

Mit dem angesprochenen Deklarationstrick geht das viel einfacher:

' RedrawWindowAny zeichnet ein Fenster neu
Private Declare Function RedrawWindowAny _
  Lib "user32" Alias "RedrawWindow" ( _
  ByVal hWnd As Long, _
  ByRef lpRect As Any, _
  ByVal hRegion As Long, _
  ByVal RedrawFlags As Long _
  ) As Long
  
  ' ...
  
  ' Codebeispiel:
  Call RedrawWindowAny(Form1.hWnd, ByVal 0&, 0, 0)

Im Beispielcode wird der Zeichenfunktion noch ein Bündel an Werten für den letzten Parameter übergeben. Diese sorgen dafür, dass der Fensterhintergrund zunächst gelöscht wird und der gesamte Inhalt des Fensters neu gezeichnet wird. Zudem bestimmen sie, dass mit eventuellen Kindfenstern im Fenster identisch umgegangen wird und das Neuzeichnen sofort stattfindet. Sie finden die Konstanten im Sourcecode entsprechend kommentiert.

Aus den vorstehenden Informationen lassen sich nun einfach zwei Hilfsfunktionen erstellen, die das Sperren und Entsperren samt Neuzeichnen für uns übernehmen. Rufen Sie entsprechend die Methode LockWindow auf, wenn Sie das Neuzeichnen eines Fensters vorübergehend unterdrücken möchten. Durch einen Aufruf von UnlockWindow kehren Sie zum Normalzustand zurück und aktualisieren die Bildschirmanzeige.

' --- API-Deklarationen ---
  
Private Const WM_SETREDRAW    As Long = &HB&    ' Fenstersperrung ein-/ausschalten
' Werte für RedrawWindow
Private Const RDW_ERASE       As Long = &H4&    ' Fensterinhalt vorab löschen
Private Const RDW_INVALIDATE  As Long = &H1&    ' Fensterinhalt ungültig => Neuzeichnen nötig
Private Const RDW_ALLCHILDREN As Long = &H80&   ' Kindfenster des Fensters mit einbeziehen
Private Const RDW_UPDATENOW   As Long = &H100&  ' Die Aktualisierung sofort durchführen
  
' SendMessage schickt einem Fenster eine Nachricht
Public Declare Function SendMessage _
  Lib "user32" Alias "SendMessageA" ( _
  ByVal hWnd As Long, _
  ByVal wMsg As Long, _
  ByVal wParam As Long, _
  ByRef lParam As Any _
  ) As Long
  
' RedrawWindow zeichnet ein Fenster neu, RedrawWindowAny
' erlaubt in diesem Fall das Auslassen der RECT-Angabe
' (spart hier die Verwendung der Funktion GetClientRect).
Private Declare Function RedrawWindowAny _
  Lib "user32" Alias "RedrawWindow" ( _
  ByVal hWnd As Long, _
  ByRef lpRect As Any, _
  ByVal hRegion As Long, _
  ByVal RDW_RedrawFlags As Long _
  ) As Long
  
  
' --- Code ---
  
  
Public Sub LockWindow(ByVal hWnd As Long)
' Setzt eine Fenstersperrung.
  
  ' Das Neuzeichnen von Elementen in diesem Fenster unterbinden
  Call SendMessage(hWnd, WM_SETREDRAW, 0, ByVal 0&)
  
End Sub
  
  
Public Sub UnlockWindow(ByVal hWnd As Long)
' Hebt eine Fenstersperrung auf und zeichnet das Fenster aktuell neu.
  
  ' Das Neuzeichnen von Elementen in diesem Fenster wieder zulassen
  Call SendMessage(hWnd, WM_SETREDRAW, 1, ByVal 0&)
  
  ' Die Fensterdarstellung schließlich aktualisieren
  Call RedrawWindowAny(hWnd, ByVal 0&, 0, RDW_ERASE Or _
                                          RDW_INVALIDATE Or _
                                          RDW_ALLCHILDREN Or _
                                          RDW_UPDATENOW)
  
End Sub

Mathias Schiffer widmet sich als freier Softwareentwickler und Technologievermittler größeren Projekten ebenso wie arbeitserleichternden Alltagslösungen. Seit Jahren gibt er sein Wissen in unzähligen Publikationen auch an andere Entwickler und Entscheider weiter. Sie erreichen ihn per E-Mail an die Adresse *Schiffer@mvps.org*.