[Win32] Mein Kampf mit den Tabs

Programmiersprachen, APIs, Bibliotheken, Open Source Engines, Debugging, Quellcode Fehler und alles was mit praktischer Programmierung zu tun hat.
Antworten
Benutzeravatar
Krishty
Establishment
Beiträge: 7200
Registriert: 26.02.2009, 12:18
Benutzertext: state is the enemy
Kontaktdaten:

[Win32] Mein Kampf mit den Tabs

Beitrag von Krishty » 19.10.2019, 13:35

„Gut zu wissen“-Aufsatz über Tabs mit Win32. Ich glaube, dass ich die Probleme nun im Griff habe; aber Verbesserungsvorschläge sind immer willkommen.

Mein Optimizer hat eine Kopfleiste mit Tabs für Optionen. Ein Tab pro unterstütztem Dateiformat.

Anfang des Monats fiel mir auf, dass die Tabs unter Windows 10 nicht richtig anzeigen:

Bild

Wie ihr seht, haben die Tabs einen grauen Hintergrund und einen schmalen weißen Rand. Normalerweise sind Tabs in Windows 10 aber komplett weiß (siehe Dateieigenschaftsdialog):

Bild

(Ignoriert, dass das Symbol falsch angezeigt wird. Das kriegt Windows wahrscheinlich 2030 noch nicht richtig hin.)

Also, wie kriege ich das komplett weiß, wie es sein sollte?

Grundlagen

Ich benutze pure Win32-API ohne MFC, WinForms, WPF, oder ähnliche Scheiße. Bloß eine Dialogbox, wie man sie aus dem Ressourcen-Editor kriegen würde, mit den Standard-Common Controls (Buttons, Edits Checkboxes, usw).

Die Tabs sind durch ein Tab Control realisiert. Das wurde vor rund 30 Jahren eingeführt und hat dementsprechend viele Fallstricke. Es ist zum Beispiel kein vollwertiger Container, sondern bloß ein leerer Rahmen, den man im Dialog platziert. Was darin angezeigt wird und was sich ändert, wenn man von einem Tab zum nächsten schaltet – das muss man alles selber machen.

Einen der Fallstricke hat – wie es der Zufall will – Raymond Chen letzte Woche erklärt.

Nämlich ist der Standard-Weg, wie man ein Tab Control befüllt, folgender:
  1. Alles, was in einzelnen Tabs angezeigt werden soll, in eigene Sub-Dialoge packen. Die Sub-Dialoge bekommen keinen Rahmen und dürfen nicht modal sein.
  2. Die Sub-Dialoge erzeugen zu einem Kind des Hauptdialogs machen – nicht zu einem Kind des Tab Controls!
  3. Wann immer das Tab Control meldet, dass jemand draufklickte: Alle Sub-Dialoge unsichtbar schalten und nur den einen sichtbar machen, der zu dem Tab gehört, das gerade ausgewählt wurde.
Da lag dann auch mein erster Fehler: Ich hatte die Dialoge zu einem Kind des Tabs gemacht. Kaum war das korrigiert, hatten die Tabs (ihr wisst jetzt, dass es Sub-Dialoge sind) die korrekte Hintergrundfarbe:

Bild

Naja … fast :( Der Hintergrund hat die korrekte Farbe, aber der Inhalt (also Checkbox, Static) nicht.

Windows XP

Also Google angeschmissen. Problem auf StackOverflow gefunden.

https://stackoverflow.com/questions/776 ... -xp-styles
Selbes Problem. Die markierte Lösung ist falsch. Die mit den meisten Upvotes funktionierte bei mir nicht.

https://stackoverflow.com/questions/270 ... ize-window
Selbes Problem nach Resize. Keine Lösung.

https://stackoverflow.com/questions/104 ... xt-widgets
Selbes Problem. Die markierte Lösung ist falsch. Die mit den meisten Upvotes ist die gleiche wie oben vom selben Autor.

Dazu kamen zahllose MSDN-Foreneinträge.

Der Konsens ist:
  • Du rufst bei der Initialisierung des Sub-Dialogs für dein Tab die Funktion EnableThemeDialogTexture(tab, ETDT_ENABLETAB) auf. Dann funktioniert das.
  • Falls es dann nicht funktioniert, bist du am Arsch. Mach irgendwas mit WM_ERASEBACKGROUND oder so.
Ich lehne Änderungen an der Nachrichtenbehandlung strikt ab (WM_ERASEBACKGROUND – bereitet den Fensterhintergrund aufs Zeichnen vor; WM_CTLCLRSTATIC – fragt nach der Hintergrund-Brush für Controls). Ich glaube fest daran, dass es im Window Manager Code gibt, der auf jeder Windows-Version das korrekte Ergebnis liefert; ohne zusätzlichen Code; nur durch die korrekten Flags im Dialog. Das mag naiv klingen, aber bisher hatte ich damit immer recht.

… ich war also am Arsch. Aber zumindest haben mir die Antworten gezeigt: Auf Windows XP sieht man den Unterschied noch viel deutlicher als auf 10! Also habe ich meine VM angeschmissen und das hier bekommen:

Bild

Japp. Windows XP mit Luna-Theme gibt Tabs haben einen Farbübergang im Hintergrund, und der ist hier eindeutig kaputt.

Nachdem ich EnableThemeDialogTexture(tab, ETDT_ENABLETAB) eingebaut habe:

Bild

Das sieht jetzt ganz genau wie in den StackOverflow-Problemen aus. Insbesondere wie in dem hier:

https://stackoverflow.com/questions/267 ... trol-it-be
Die Antwort ist nicht „straight-forward“ und auch sonst komplett falsch. Aber dazu kommen wir später.

Property Sheets

Der Konsens des Internets ist: Wenn du an diesem Punkt bist, und es funktioniert nicht, kann dir niemand mehr helfen.

Zwischen den Zeilen liest man aber immer, dass man statt Tab Controls sowieso besser Property Sheets benutzen sollte. Was ist das?

Property Sheets wurden mit Windows 95 eingeführt. Sie sind Gottklassen, die einen kompletten Dialog mit Tabs abhandeln. Oder, falls man keine Tabs mag, Assistenten mit mehreren Seiten. Es ist wirklich schwer zu erklären, aber: Die Dateieigenschaften, die ich oben gezeigt habe? Das ist kein Dialog mit einem Tab Control. Das ist in Wirklichkeit ein Property Sheet mit fünf Property Pages Allgemein, Kompatibilität, … Seit Windows XP ist quasi jeder Dialog, an der ihr Tabs seht, in Wirklichkeit ein Property Sheet. Einzige Ausnahme ist der Task Manager unter Windows XP & Windows 7. Dessen Tabs sind ein echtes Tab Control.

Okay. Wenn die Welt das so will … steige ich auf Property Sheets um. Gesagt, (zwei Tage später) getan.

Bild

Der Hintergrund ist endlich richtig. Aber alles andere war vernichtend.

Property Sheets sind als Standalone-Dialoge entworfen. Sie in einen bestehenden Dialog mit anderem Kram einzubinden ist eine riesen Qual:
  • Die Rahmen müssen abgeschaltet werden, indem man während der Initialisierung in die Binärdaten eines Dialog-Templates greift (Ja, wirklich!!!)
  • Die Knöpfe OK und Cancel müssen von Hand unsichtbar geschaltet werden.
  • Weil die Klasse Assistenten von Setups ebenso wie Tabs abdeckt, hat man überall spezial-Flags, auf die man höllisch achten muss
  • Das absolute No-Go: Man kann sie nicht vergrößern.
Ein Bild sagt mehr als tausend Worte:

Bild

Die Dinger sind verschoben weil sie ihre Margin anders berechnen als normale Controls. (Klar, sie sind ja darauf ausgelegt, allein angezeigt zu werden). Die Rahmen passen nicht weil sich die Dinger nicht richtig vergrößern und verkleinern lassen. Das Internet behauptet zwar anderes, aber ich habe es nicht geschafft. Und Gott weiß, dass ich es versucht habe.

Auf dem Weg musste ich außerdem Microsoft melden, dass ihre Property Sheet-Dokumentation völlig zerstört wurde.

Die Lösung

Also zurück zu Tabs. Ich habe in Visual Studio ein simples Win32-Programm zusammengeklickt, das nichts anderes hat als:
  1. Die Common Controls 6 via Manifest referenzieren.
  2. InitCommonControlsEx()
  3. Einen Hauptdialog mit einem Tab-Control.
  4. Einen Sub-Dialog mit dem Tab-Inhalt (Sibling des Tab-Controls; Child des Hauptdialogs).
  5. Im Sub-Dialog eine Checkbox und ein Static (deren Rendering geht immer als erstes kaputt).
  6. Keine zusätzliche Nachrichtenbehandlung (kein WM_PRINTCLIENT oder so).
  7. Nur EnableThemeDialogTexture() in der WM_INITDIALOG-Nachricht im Sub-Dialog.
… das Ding zeigte auf Anhieb richtig an. Auf Windows XP, Windows 7, Windows 10. WTF.

Also habe ich es Stück für Stück verändert, in Richtung meiner Anwendung. Die Styles. Die Reihenfolge der Controls. Und so weiter und sofort.

… und alles ging kaputt, sobald der Hauptdialog das Style-Bit WM_CLIPCHILDREN bekam.

Und zwar ging es auf ganz genau die Art und Weise kaputt wie in den ganzen unbeantworteten Fragen auf StackOverflow oder in den MSDN-Foren.

WM_CLIPCHILDREN ist essentiell, um Flackern beim Verschieben und Vergrößern/Verkleinern des Dialogs zu verhindern. Aber dazu schreibe ich später mehr. Jetzt ergötzen wir uns erstmal an dem korrekten Tab-Rendering auf Windows XP …

Bild

… bis Windows 10.

Bild
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne

Benutzeravatar
Krishty
Establishment
Beiträge: 7200
Registriert: 26.02.2009, 12:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [Win32] Mein Kampf mit den Tabs

Beitrag von Krishty » 07.12.2019, 02:21

Warum nicht einfach WS_EX_COMPOSITED verwenden?

Dieses Style-Bit wurde mit Windows XP eingeführt um Double Buffering für ein Fenster zu aktivieren. Damit wird das komplette Fenster in einen Puffer gezeichnet, und zwar alle Elemente von unten nach oben. Erst, wenn der Puffer fertig ist, wird er auf den Bildschirm kopiert. Damit gibt es kein Flackern mehr.

Es gibt zwei Antworten.

Die erste ist wenig spektakulär, aber die Lösung für das ganze Tab-Desaster: Mit WS_EX_COMPOSITED sind gar keine Tabs mehr zu sehen. WTF dachte ich mir, aber dann wurde alles deutlich: Die Reihenfolge der Fenster war falsch!

Wenn Windows ein Kind-Fenster hinzufügt, fügt es das unter den anderen Fenstern hinzu. Damit wäre das ganze Tab-Rätsel gelöst: Das irre Flackern, das Fehlen mit WS_EX_COMPOSITED (das ein Zeichnen von unten nach oben garantiert) war nur, weil das Fenster mit dem Tab-Inhalt in der Z Order unter dem eigentlichen Tab gezeichnet wurde. Des Rätsels Lösung ist, das Fenster mit dem Inhalt über das Tab zu legen:

Code: Alles auswählen

SetWindowPos(tab, HWND_TOP, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
So steht’s nun auch überall auf StackOverflow.

Nun zum zweiten Grund. Der klingt erstmal abenteuerlich: Mein Fenster enthält ein List View. Und Windows kann keine List Views mit Double Buffering, wenn eine Zeile für die Spaltennamen angezeigt wird. Die List Views sind dann unsichtbar und die Anwendung ruckelt bei 100 % CPU-Auslastung vor sich hin.

Ich habe erstmal verzweifelt auf meiner Seite nach Fehlern gesucht. Dann habe ich aber eine verdächtige Häufung von Berichten gefunden, deren Verfasser noch gründlicher gesucht haben als ich:
https://rsdn.org/forum/winapi/4873392.all hat geschrieben:The dialog starts to slow down if you put on it a list view with the LVS_REPORT style (with LVS_ICON - no problem). Everything passes, if you remove the header (LVS_NOCOLUMNHEADER), if you remove WS_EX_COMPOSITED, too.
Reproduziert hier exakt.
I'd add that the "heap of messages" is WM_NOTIFY with the code NM_CUSTOMDRAW although the listview is created without the LVS_OWNERDRAWFIXED style, this message clogs percent 100% since it chases in a cycle
Jup.
5) added to the bottom window (it’s exactly SysListView32, in which SysHeader32 is child) - WS_CLIPCHILDREN,
6) - and the storm subsided, about 3% in the CPU.
… auf Windows 7 wird es etwas besser, ja. Aber das Ding zeigt trotzdem nicht richtig an. Auf Windows XP hängt alles total.
https://groups.google.com/forum/#!topic/microsoft.public.win32.programmer.ui/hZer0Xidf3A hat geschrieben:From some further search on this ex style, it appears to be a serious bug with using it on a dialog that contains listview control that is in report mode.
Genau der Fall.
http://www.asmcommunity.net/forums/topic/?id=30470 hat geschrieben:In order to eliminate the flicker that occurs while resizing a window, I have used the WS_EX_COMPOSITED style when creating the window. Everything goes fine until I try to add a SysListView32 to my window. The list is created and added without an error, but never becomes visible.
Er hat die 100 % CPU-Auslastung vergessen, aber … japp.

Also: WS_EX_COMPOSITED funktioniert schlicht nicht mit List-Views. Stattdessen muss man Old School aufpassen, dass man flackerfrei zeichnet. Bis 2020 jedenfalls – denn ab Windows 10 läuft alles Zeichnen nur noch im Composited Mode, und da gibt es kein Flackern mehr.

So. Auf StackOverflow gibt es eine Handvoll ungelöster Fragen dazu. Schnappt euch ruhig die Bounties. Ich habe keinen Bock mehr auf die Scheiße.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne

Antworten