Seit Windows95 gibt es ihn, den Hilfe-Knopf in der Titelleiste zur Anforderung von kontextsensitiver Hilfe. Allerdings nie in Kombination mit größenveränderbaren Fenstern! Warum eigentlich nicht? Es wird oft gefragt. Deshalb habe ich die Microsoft-Hausaufgabe hier nachprogrammiert. Natürlich mit minimaler Code-Größe, in Win32, ohne Laufzeitbibliothek sowie ohne irgendwelche Bitmaps ablegen zu müssen!
… und so sieht es aus:
Windows 95 usw. (klassisch) Erklärung der Zustände | Windows XP mit Luna-Stil | („Teletubbie-Optik“) Normalzustand: Fenster hat den Eingabefokus. Vier statt drei Knöpfe |
| Fenster hat keinen Eingabefokus. | Verschmerzbare Fehldarstellung bei XP-Stil (Knopf nicht grau), das hat wohl irgendetwas mit den unten erwähnten undokumentierten Nachrichten zu tun
| Schaltfläche gedrückt – erst beim Loslassen auf derselben Schaltfläche geht's los |
| Hilfesuche in Aktion – jetzt nur noch auf das Dialogelement klicken | Früher wurde das mittels Umschalt+F1 gemacht
| Rechts: Leichte Hervorhebung des Knopfes beim Mauszeiger-Drüberhalten („hot-tracking“) | Nur Mit Teletubbie-Optik verfügbar Um den Text „Hilfe“ kümmert sich Windows ohne Zutun
| |
---|
… und so funktioniert's:
Eigentlich genauso wie gehabt, bei den nicht größenveränderlichen Fenstern.
Ein bedienungstechnischer Unterschied existiert: Nicht nur beim Drücken der Esc-Taste,
sondern auch beim Drücken der Alt-Taste wird die Hilfesuche beendet.
Beim „richtigen“ Hilfeknopf wirken nur Esc und Alt+Tab.
Es ist relativ leicht, die Routine so zu schreiben, dass beim inaktiven Fenster
der Hilfeknopf verschwindet. Das dürfte aber mehr verwirren als nützen.
Man kann den passenden Fenster-Stil WS_EX_CONTEXTHELP
setzen oder nicht.
Gängige Dialog-Editoren weigern sich allerdings,
diesen Stil mit WS_MINIMIZEBOX
usw. zu kombinieren.
Nun ja, in MSVC mit nacktem Win32, wie immer bei mir.
Ich habe es so geschrieben, dass die Fensterprozedur nur wenig verändert werden muss. Also nicht so plump wie die MSDN-PSDK-Beispiele (auch: MFC) mit der Umschalt+F1-Taste, sondern etwas eleganter mit modalen Schleifen.
So wie hier dargestellt funktioniert es nur für Dialoge.
Diese bekommen – genau wie sonst auch – eine WM_HELP
-Nachricht zugesandt,
auf die man wie gewohnt reagieren kann.
Für normale Fenster ist der Kode etwas anzupassen,
da hier statt BOOL
-Rückgabe der Aufruf von DefWindowProc()
erfolgt.
Die folgende Routine ermittelt die Position und Größe des Knopfes, so gut es geht.
Es funktionierte nicht mit GetSystemMetrics(SM_CXSIZE)
,
diese Funktion lieferte offensichtlichen Müll.
Deshalb wurden die Größen empirisch aus Daten von SystemParametersInfo()
ermittelt.
static void GetQuestionButtonRect(HWND Wnd, LPRECT r) { NONCLIENTMETRICS ncm; ncm.cbSize=sizeof ncm; SystemParametersInfo(SPI_GETNONCLIENTMETRICS,sizeof ncm,&ncm,0); GetWindowRect(Wnd,r); // hier: X-Ausdehnung ermitteln r->left=r->right-r->left -GetSystemMetrics(SM_CYFRAME) -ncm.iCaptionHeight*4 +1; r->right=r->left+ncm.iCaptionHeight-2; r->top=GetSystemMetrics(SM_CXFRAME)+2; r->bottom=r->top+ncm.iCaptionHeight-4; }
Eine für Bildschirm-Koordinaten, eine für Client-Koordinaten.
Die o.g. Funktion GetQuestionButtonRect()
liefert Fenster-Koordinaten.
static BOOL NcInQuestionRect(HWND Wnd, POINT p) { RECT r; GetWindowRect(Wnd,&r); p.x-=r.left; p.y-=r.top; GetQuestionButtonRect(Wnd,&r); return PtInRect(&r,p); } static BOOL InQuestionRect(HWND Wnd, POINT p) { ClientToScreen(Wnd,&p); return NcInQuestionRect(Wnd,p); }
Zum Zeichnen von Standardelementen gibt es die nützliche Funktion
DrawFrameControl()
, die allerdings bei XP-Stil
einen recht „altmodischen“ Knopf produziert, der dann recht hässlich aussieht.
Das Windows-XP-Pedant dazu ist DrawThemeBackground()
.
Damit's auch unter Nicht-XP läuft, werden alle drei notwendigen
DLL-Eintrittspunkte dynamisch beschafft.
static void DrawQuestionButton(HWND Wnd, bool Pushed, bool Hot) { HDC dc=GetWindowDC(Wnd); RECT r; HTHEME th=0; HANDLE hLib; GetQuestionButtonRect(Wnd,&r); hLib=LoadLibraryA("uxtheme"); if (hLib) { th=(HTHEME(_stdcall*)(HWND,LPCWSTR))GetProcAddress(hLib,"OpenThemeData")(Wnd,L"WINDOW"); } if (th) { // Teletubbie-Optik HRESULT (_stdcall*pDrawThemeBackground)(HTHEME,HDC,int,int,const RECT*,const RECT*); (FARPROC)pDrawThemeBackground=GetProcAddress(hLib,"DrawThemeBackground"); pDrawThemeBackground(th,dc,WP_HELPBUTTON,Pushed?HBS_PUSHED:Hot?HBS_HOT:HBS_NORMAL,&r,NULL); (HRESULT(_stdcall*)(HTHEME))GetProcAddress(hLib,"CloseThemeData")(th); }else{ DrawFrameControl(dc,&r,DFC_CAPTION,Pushed?DFCS_CAPTIONHELP|DFCS_PUSHED:DFCS_CAPTIONHELP); } if (hLib) FreeLibrary(hLib); ReleaseDC(Wnd,dc); }Bei den einzeiligen Aufrufen mittels
GetProcAddress()
nicht schwindlig werden!
Ich erspare mir hier unnötige(?) NULL-Zeigertests.
Die Funktion GetWindowTheme()
ging merkwürdigerweise nicht.
Es ist die Methode, um ohne allzu weitreichende Eingriffe den heruntergedrückten Hilfe-Knopf darzustellen, solange der Mauspfeil draufzeigt, und den Knopf herausspringen zu lassen, wenn der Mauspfeil (bei immer noch gedrückter linker Maustaste) herausbewegt wird.
Schließlich darf erst beim Loslassen des Hilfe-Knopfes die Aktion losgehen, nicht schon beim Draufdrücken. Sonst gäbe es eine Diskrepanz von Look&Feel.
static void QuestionModalLoop(HWND Wnd) { MSG Msg; bool Pushed=true; SetCapture(Wnd); DrawQuestionButton(Wnd,true,false); while (GetMessage(&Msg,0,0,0)) { switch (Msg.message) { case WM_MOUSEMOVE: { POINT p={GET_X_LPARAM(Msg.lParam),GET_Y_LPARAM(Msg.lParam)}; bool InQuest=InQuestionRect(Wnd,p)!=0; if (Pushed ^ InQuest) { Pushed=InQuest; DrawQuestionButton(Wnd,Pushed,false); } }break; case WM_LBUTTONUP: { if (Pushed) { PostMessage(Wnd,WM_SYSCOMMAND,SC_CONTEXTHELP,Msg.lParam); // SendMessage geht hier nicht DrawQuestionButton(Wnd,false,false); } ReleaseCapture(); }return; // modale Schleife verlassen } DispatchMessage(&Msg); } }
Die anschließende modale Schleife, die sich um das Point-And-Shoot kümmert,
befindet sich irgendwo in DefWindowProc()
und wird durch PostMessage(… SC_CONTEXTHELP …)
aufgerufen.
Es sind nur ein paar WM_NC…
-Nachrichten, die uns interessieren.
In den meisten Fällen müssen wir verhindern, dass (zeitlich) hinter uns
DefWindowProc
gerufen wird. Das würde alle Arbeit vernichten.
Eine statische Variable (Hot
) für den Hot-Tracking-Effekt unter XP-Stil
ließ sich nicht so recht vermeiden.
Zeichnen bei jeder Mausbewegung flackert zu sehr.
Die undokumentierten Nachrichten 0xAE und 0xAF müssen unbedingt weggefiltert werden,
sonst verschwindet die ganze Arbeit unter XP-Stil.
// Filtert diverse NC-Nachrichten zur Darstellung und Behandlung des Hilfe-Fragezeichen-Knopfes // Nur für Dialoge! Hier: Nicht für normale Fenster geeignet. // Die Dialogprozedur muss TRUE zurückgeben, wenn diese Routine TRUE liefert. BOOL HandleDlgNcMessagesForHelp(HWND Wnd,UINT Msg,WPARAM wParam,LPARAM lParam) { switch (Msg) { case WM_NCPAINT: case WM_NCACTIVATE: { SetWindowLong(Wnd,DWL_MSGRESULT,(LONG)DefWindowProc(Wnd,Msg,wParam,lParam)); DrawQuestionButton(Wnd,false,false); }return TRUE; case WM_NCHITTEST: { // 1. Verhindert das Verschieben des Fensters beim Knopf drücken // 2. Liefert wParam==HTHELP bei WM_NCLBUTTONDOWN POINT p={GET_X_LPARAM(lParam),GET_Y_LPARAM(lParam)}; if (NcInQuestionRect(Wnd,p)) return SetDlgMsgResult(Wnd,WM_NCHITTEST,HTHELP); }break; case WM_NCMOUSEMOVE: { // Hot-Tracking (nur Teletubbie-Optik) static bool Hot; if (Hot!=(wParam==HTHELP)) { Hot=!Hot; DrawQuestionButton(Wnd,false,Hot); } }break; case WM_NCLBUTTONDOWN: case WM_NCLBUTTONDBLCLK: { if (wParam==HTHELP) {QuestionModalLoop(Wnd); return TRUE;} // Ereignis schlucken!! }break; case 0xAE: // Als Ergebnis bleibt der Knopf beim inaktiven Fenster hervorgehoben case 0xAF: /*MessageBeep(0);*/ return TRUE; // unbekannte XP-Nachrichten // scheinen die Titelleiste aufzuhellen (Kontrast reduzieren) } return FALSE; }
Es genügt der Aufruf der o.g. Prozedur.
Es wäre auch gut möglich, die wenigen Nachrichten auch in MainDlgProc()
zu filtern.
Für mehrere größenveränderliche Fenster im Programm ist's aber besser so.
Weiterhin wird der Gebrauch von PointHelpModalLoop()
dargestellt,
der bspw. von einem Menüpunkt oder einer Hotkey (Umschalt+F1) aufgerufen wird.
BOOL CALLBACK MainDlgProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam) { if (HandleDlgNcMessagesForHelp(Wnd,Msg,wParam,lParam)) return TRUE; switch (Msg) { ... case WM_COMMAND: switch (LOWORD(wParam)) { ... case 1902: { // Kontexthilfe SetCapture(Wnd); PointHelpModalLoop(Wnd); ReleaseCapture(); }break;Das Ergebnis ist dann wie oben zu sehen.
Zu beachten ist, dass SetWindowPlacement()
nicht funktioniert!
Solcher Kode muss durch SetWindowPos()
ersetzt werden.
// SetWindowPlacement ist Ursache für verschwindenden Fragezeichen-Knopf! // SetWindowPlacement(ghMainWnd,&wp); // Deshalb hier: SetWindowPos SetWindowPos(ghMainWnd,0, wp.rcNormalPosition.left, wp.rcNormalPosition.top, wp.rcNormalPosition.right-wp.rcNormalPosition.left, wp.rcNormalPosition.bottom-wp.rcNormalPosition.top, SWP_NOZORDER); // Fenster in Bildschirm ziehen... SendMessage(ghMainWnd,DM_REPOSITION,0,0); ...