Was tut man als Programmierer, wenn man die ungeteilte Aufmerksamkeit des Nutzers moechte? Es gibt zwei Moeglichkeiten: Ein aus User-Interface-Design-Aspekten optimales Programm wird sich die Aufmerksamkeit nicht mit Gewalt holen -- sondern der Nutzer wird sich dem Programm freiwillig zuwenden. Der Rest der Programme holt sich die Aufmweksamkeit mit Gewalt. In diesem Artikel moechte ich beschreiben, wie man das auf den heute aktuellen Systemen der NT-Schiene machen kann. Wenn es denn unbedingt sein muss.

Vorrede fuer alle, die dem Begriff der Systemmodalitaet nichts zuordnen koennen: Ein Fenster ist systemmodal, wenn der Nutzer nur mit diesem Fenster arbeiten und zu keinem anderen Fenster wechseln kann. Er kann dies vielleicht zu einem spaeteren Zeitpunkt wieder, allerdings muss der systemmodale Dialog vorher geschlossen werden. Ein einfaches Beispiel waere der "Arbeitsstation sperren"-Dialog, den man nach Druck auf Windowstaste-L bekommt.

SetSysModalWindow -- historisch gesehen

Frueher, in der guten alten Zeit von Windows 3.11 war noch alles einfach. Wenn der Programmierer die ungeteilte Aufmerksamkeit des Nutzers auf sein Fenster richten wollte, dann rief er die Funktion SetSysModalWindow(HWND) mit dem Fenster-Handle des Fensters, dass die Aufmwerksamkeit bekommen sollte auf. Dieser Aufruf fuehrte dazu, dass kein Wechsel des aktiven Fensters mehr moeglich war, bis dieses geschlossen wurde (oder SetSysModalWindow(HWND) mit 0 als Parameter aufgerufen wird). Eine offensichtliche Anwendung waere ein kleines Programm, um den Rechner gegen unerwuenschten Zugriff zu sperren.
Zu Zeiten von 16-Bit-Windows war diese Funktion state-of-the-art.

Dann kam Windows 95 bzw. Windows NT 4 auf den Markt. Zusammen damit wurde des User-Interface-Konzept von Windows umgekrempelt. Aus kooperativem Multitasking (ein Programm kann den Eingabefokus weitergeben wenn es will -- wenn es nicht will, hat es z.B. vorher SetSysModalWindow(HWND) aufgerufgen) wurde das praeemptive Multitasking (der Nutzer bestimmt, welches Programm den Eingabefokus hat, und andere Programme koennen nichts dagegen machen). Allerdings, die Kompatibilitaet erzwang ihre Kompromisse -- und so entstanden zwei moegliche Verhaltensweisen des Betriebssystemes auf einen Aufruf von SetSysModalWindow:

SetSysModalWindow -- aus Sicht des Interface Design

Katastrophe. Schlimmstes Mittelalter. Die Systemmodalitaet bring den Habitus von DOS-Programmen in die Welt des Multitasking und beraubt sie eben dieser herausragenden Eigenschaft, mehrere Programme gleichzeitig nutzen zu koennen. Systemmodale Dialoge sind schlicht fuer "normale" Programme nicht notwendig, denn kein Programm sollte sich so wichtig nehmen, dass es den Nutzer einsperrt. Systemmodalitaet hat in Anwenderprogrammen nichts verloren. So einfach ist das.

Die Realitaet (und wie ich hierauf gekommen bin)

Im Folgenden werde ich von den Betriebssystemen Microsoft Windows 2000, Windows XP, Windows Vista und Windows 7 ausgehen.
Gibt es noch Systemmodalitaet? Natuerlich gibt es sie. Da wo sie hingehoert -- aus der Hand des Systems. Wer Ctrl-Alt-Del drueckt, der hat einen systemmodalen Dialog vor sich. Ganz einfach, ganz trivial. Kennt eigentlich auch jeder. Auf Windows ab Vista gibt es noch einen -- die Dialoge der UAC. Auch sie sind systemmodal.
Bei dem Dialog, der nach Ctrl-Alt-Del erscheint, ist wohl unstreitig, dass er sinnigerweise systemmodal ist. Bei UAC -- nun, wuerde es nicht reichen, wenn UAC sich nur vor das betreffende Programm haengen wuerde? Vielleicht nicht, vielleicht wuerden dann Programme im Hintergrung nicht weiterarbeiten, nur weil der Nutzer die UAC-Anforderung uebersehen hat...
Unstreitig duerfte auch sein, dass systemmodale Dialoge vom Betriebssystem ausgehen... und sonst niemandem.
Und dann gibt es da noch ein Beispiel, dass mich ueberhaupt erst hierauf gebracht hat: "Microsoft CardSpace". CardSpace bringt ein Plugin fuer die Systemsteuerung mit sich. Wenn man dieses startet, dann erzeugt es einen systemmodalen Dialog. Schockierend.
Das bekommen Sie entweder mit einem undokumentierten Aufruf in der Windows-API hin (was mal so gar nicht typisch Microsoft waere) -- oder aber die API bietet einen anderen Weg, Systemmodalitaet zu emulieren. An diesem Punkt war meine Neugierde geweckt.

Wenn man etwas erschaffen will, was andere schon hinbekommen, dann ist ein vielversprechender Ausgangspunkt, sich anzuschauen wie das die anderen den machen. Also habe ich mich dafuer interessiert, wie die anderen Windows-Dialoge ihren Sonderstatus bekommen. Das Zauberwort heisst Desktopwechsel.
Ja, Windows kann mehrere Desktops verwalten. Einer davon ist als "secure" definiert und wird bei Druck auf Ctrl-Alt-Del angesprungen. Bei UAC wird der Desktop ebenfalls gewechselt -- mit dem ausgegrauten anderen Desktop als Hintergrundbild. Und Cardspace wird es ebenso machen.

Die Windows-Desktop-API

Ganz so trivial, wie es einst mit SetSysModalWindow(HWND) war, wird es nicht mehr. Im eher im Gegenteil. Aber dennoch, mit ein wenig Geduld bekommt man einen systemmodalen Dialog hin. Dazu muss man essenziell nur folgendes machen:

...wozu diese Aufrufe der Windows-API zu kennen und zu nutzen sind: OpenDesktop, CreateDesktop, SwitchDesktop, und SetThreadDesktop. Die erste Funktion ist notwendig, um spaeter zum "Normaldesktop" zurueckkehren zu koennen. Die Zweite, um einen leeren neuen Desktop anzulegen. Die Dritte wird den Desktop wechseln und mit der vierten sagen wir, auf welchem wir unsere Programmausgabe platzieren moechten.

Ein systemmodales Delphi-Beispiel

Natuerlich soll hier das Proof-of-Concept-Beispiel nicht fehlen. Als weitere Schwierigkeitssteigerung moechte ich das Beispielprogramm fuer Delphi schreiben -- warum wird gleich erklaert. Vorher soll noch gesagt sein, dass das Erstellen von Fenstern auf einem anderen Desktop nur dann funktioniert, wenn das Programm zuvor keine anderen Fenster geoeffnet hat. Also muss man sehr genau aufpassen. Wenn man die Fenster mit C und der Windows-API erstellt, dann kann man das. Hat man aber ein Toolkit unter sich (heisse es nun QT oder VCL oder wie auch immer), dann wird dies ungleich schwieriger. Denn sobald eine VCL-Form beteiligt ist, klappt es scheinbar nicht mehr, denn die Form tut schon beim Aufgefuehrtsein in der uses-Klausel etwas scheinbar stoerendes. Andererseits sind VCL-Formen sehr viel einfacher zu designen als solche, die man in muehseliger Windows-API-Handarbeit zusammenkloeppelt. Wie also kann genau geklaert werden, dass die VCL-Form erst dann erste Aufrufe startet, wenn sie dran ist?
Die einfachste Loesung, die mir eingefallen ist, ist die, die VCL-Form in eine DLL auszulagern. Man gehe also wie folgt vor:

Nun haben wir einen DLL vor uns, die eine Funktion/Prozedur exportiert, die eine VCL-Form anzeigt. Jetzt brauchen wir etwas, was diese nach dem oben gegeben Schema nutzt, nur, dass wir jetzt zu gegebener Zeit die DLL laden, bevor wir den Code aus ihr ausfuehren. Hierzu wird die DLL mit LoadLibrary geoeffnet und die Einsprungsadresse mit GetProcAddress gesucht. Voila.

In Code schaut das dann wie folgt aus: Das aufrufende Programm:

program sysmodal;

uses
 Windows;

var
 d: HDESK;
 h: THandle;
 p: procedure;
begin
 d := CreateDesktop('SysmDesk',nil,nil,0,
            DESKTOP_CREATEMENU or DESKTOP_CREATEWINDOW or 
            DESKTOP_ENUMERATE or DESKTOP_HOOKCONTROL or 
            DESKTOP_WRITEOBJECTS or DESKTOP_READOBJECTS or 
            DESKTOP_SWITCHDESKTOP or GENERIC_WRITE, 
            nil); 
 SwitchDesktop(d);
 SetThreadDesktop(d);

 h := LoadLibrary('client.dll');
 p := GetProcAddress(h, 'dlgProc');
 p;
 FreeLibrary(h);

 d := OpenDesktop('default', 0, FALSE, 
            DESKTOP_CREATEMENU or DESKTOP_CREATEWINDOW or 
            DESKTOP_ENUMERATE or DESKTOP_HOOKCONTROL or 
            DESKTOP_WRITEOBJECTS or DESKTOP_READOBJECTS or 
            DESKTOP_SWITCHDESKTOP or GENERIC_WRITE);
 SwitchDesktop(d);
 SetThreadDesktop(d);
end.
Der Rest findet sich in einem anderen Projekt, dem, aus dem die DLL erstellt wird. Bei mir hiess es "client". Hier soll nur kurz die Projektdatei angegeben sein; die Dialogform sei als frmMain in der Datei frmMainU abgelegt.
library client;

uses
 frmMainU in 'frmMainU.pas' {frmMain}

{$R *.res}

procedure dlgProc;
var
 frmMain : TfrmMain;
begin
 frmMain := TfrmMain.create(nil);
 frmMain.showmodal;
 frmMain.free;
end;

exports
 dlgProc;
Fertig! Der Dialog aus der Library erscheint als systemmodaler Dialog auf einem eigenen Desktop. Man kann sich zwar abmelden (nach Ctrl-Alt-Del bekommt man den ueblichen Dialog), aber man kann das aktuelle Programm nicht beenden. Man kann den Task-Manager aufrufen. Aber dieser geht auf dem falschen Desktop auf und hilft also nichts.

Es soll sich niemand beschweren, dass ich hiermit irgendwelchen Systemmodaldialogprogrammen den Weg ebne. Haette man bei CardSpace diesen Unsinn nicht gemacht, ich haette nicht angefangen nachzuforschen...

Stichworte:


Impressum