Hilfe-Knopf in Titelleiste

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!

Aussehen und Funktion

… und so sieht es aus:

Windows 95 usw.
(klassisch)
Erklärung der ZuständeWindows 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
Unter Vista-Look fehlt zurzeit jegliche Funktion ☹ Wahrscheinlich muss man hier die neuen Funktionen um DwmDefWindowProc bemühen.

… 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.

Implementation

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.

Grundgerüst: Wo ist der Knopf?

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;
}

Grundgerüst: Treffer-Tests

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);
}

Zeichnen des Knopfes

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.

Modale Schleife während Knopf-Drücken

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.

Was wir so alles in der Dialogprozedur filtern müssen …

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;
}

Dialogprozedur modifizieren, das war's dann!

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);
   ...