Evolution Programming Resources

  Projects

   Tooltips zu deaktivierten Fenstern und statischen Textfeldern hinzufügen


Einleitung

Eine wichtige Erweiterung die mit Windows 95 eingeführt wurde, sind die sogenannten Tooltips. Es handelt sich dabei um kleine Beschreibungsfenster, die eingeblendet werden, sobald sich der Mauszeiger einige Zeit über einem als Tool definierten rechteckigen Bereich oder Fenster befindet. Durch diese Tooltips wurden die allgegenwärtigen Werkzeugleisten zum ersten mal auch tatsächlich benutzbar.
Die meisten Klassenbiblioteken, wie z.B. MFC oder OWL, behandlen das Anzeigen von Tooltips für Werkzeugleisten selbst. Der Anwender muß sich nicht um das Tooltip Control kümmern. Will man jedoch Tooltips in Dialogen oder eigenen Fenstern verwenden, muß man sich doch mit der Funktionsweise des Tooltip Controls auseinandersetzen - besonders, da dieses Control einige Macken hat, die seine Anwendung häufig erschweren.

Das Tooltip Control

Wie jedes andere Common Control wird das Tooltip Control durch die Funktion CreateWindow(Ex) erstellt. Dabei ist als Klassenname TOOLTIP anzugeben. Die Kommunikation mit dem Control erfolgt nach seiner Erstellung nur noch über Nachrichten. Jedem Tooltip Control können über die Nachricht TTM_ADDTOOL Tools hinzugefügt werden. Ein Tool ist entweder ein ganzes Fenster oder ein rechteckiger Bereich innerhalb eines Fensters. Jedem Tool kann eine Beschreibung zugeordnet werden, die angezeigt wird, sobald sich die Maus innerhalb des Tools befindet. Jedes Tooltip kann beliebig viele Tools verwalten.
Damit das Tooltip Control korrekt arbeiten kann, muß es über die Mausnachrichten, die das Fenster erreichen, das eine Tool enthält, informiert werden. Dies geschieht normalerweise, indem man die eintreffenden Nachrichten durch die TTM_RELAYEVENT Nachricht an das Tooltip Control weiterleitet. Dadurch kann das Tooltip Control erkennen, ob sich der Mauszeiger innhalb eines Tools befindet und nach einer kurzen Wartezeit den Tooltip anzeigen. Dieses Vorgehen funktioniert natürlich nicht für Dialoge. Die Nachrichtenschleife eines Dialogs liegt innerhalb von Windows und man hat keine Möglichkeit an diese Nachrichten zu gelangen und kann sie folglich nicht an das Tooltip Control weiterleiten. (Es stimmt natürlich nicht ganz, daß man keine Möglichkeit hätte an diese Nachrichten zu gelangen - eine davon werde ich nachher sogar demonstieren)
Für diesen Fall haben jedoch die Microsoft Programmierer vorgesorgt: beim Hinzufügen eines Tools zu einem Tooltip Control kann das Flag TTF_SUBCLASS angegeben werden. Dies veranlaßt das Tooltip Control die Fensterprozedur des Fensters, das das Tool enthält, durch seine eigene zu ersetzen. So kann das Tooltip Control jede Nachricht abfangen und bearbeiten, bevor sie an die ursprüngliche Fensterprozedur weitergeleitet wird.

Probleme mit dem Tooltip Control

Will man das Tooltip Control in Dialogen oder eigenen Fenster einsetzen, um die Inhalte von anderen Controls zu kommentieren (z.B. von Editfeldern) wird man bald auf einen Bug des Tooltip Controls stoßen: zu statischen Textfeldern (static controls) und deaktivierten (disabled) Controls kann kein Tooltip angezeigt werden, auch wenn das Control korrekt als Tool registiert wurde. Dies liegt daran, daß das Tooltip Control die Funktion WindowFromPoint() verwendet, um zu ermitteln zu welchem Tool ein Tooltip angezeigt werden soll.
Erhält ein Tooltip Control eine WM_MOUSEMOVE Nachricht (entweder über TTM_RELAYEVENT oder über die subgeclasste Fensterprozedur) ermittelt es über WindowFromPoint das Fenster, das die aktuellen Mauskoordinaten enthält. Ist dieses Fenster vollständig als Tool registriert oder befinden sich die Mauskoordinaten noch zusätzlich in einem rechteckigen Toolbereich, dann wird nach einem Delay von ca. 0,5 Sekunden der Tooltip anzeigt.
Die Funktion WindowFromPoint arbeitet aber nicht ganz so, wie man es von ihr erwartet: befinden sich die übergebenen Koordinaten innerhalb eines statischen Textfeldes oder eines deaktivierten (disabled) Fensters, dann wird nicht das Fenster selbst, sondern sein Elternfenster zurückgegeben.
Erhält das Tooltip Control also eine WM_MOUSMOVE Nachricht, während sich der Cursor über einem statischen Textfeld oder einem deaktivierten Fenster befindet, dann liefert die Funktion WindowFromPoint das Elternfenster dieses Fensters und nicht das Fenster selbst. Das Tooltip Control durchsucht nun vergeblich die Liste seiner Tools nach diesem Fenster. Folglich wird kein Tooltip angezeigt.

Workaround

Der Workround ist prinzipell ziemlich einfach, jedoch nicht so einfach zu implementieren: da die statischen Textfelder und deaktivieren Fenster für die WindowFromPoint Funktion durchsichtig sind, fügt man einfach zum Parentfenster ein rechteckiges Tool hinzu, das genau unter dem Textfeld bzw. deaktivierem Fenster liegt.
Jetzt hat man nur noch das Problem, daß das Fenster zwar für WindowFromPoint durchsichtig ist, jedoch nicht für die Mausnachrichten selbst. Sie werden weiterhin an das Fenster gesendet, in dem sich der Cursor zum Zeitpunkt der Nachichtenversendung befindet - in unserem Fall also an das statisch Textfeld bzw. das deaktivierte Fenster. Folglich haben wir bis jetzt nichts gewonnen; der Tooltip wird immer noch nicht angezeigt, da das Tooltip Control für den rechteckigen Bereich im Elternfenster überhaupt keine Mausnachrichten erhält. Um dies zu vermeiden ist ein wenig Aufwand nötig. Die Mausnachrichten an das statische Textfeld bzw. disabled Control werden abgefangen und so modifiziert an das Tooltip Control weitergegeben, das sie aussehen wie Mausnachrichten, die an das Eltenfenster gerichtet sind.
Handelt es sich bei dem Elternfenster jedoch um einen Dialog, so hat man das Problem, das man wieder nicht an die Nachrichten heran kommt. Subclassing stellt dabei sicher eine Lösung dar, ich habe mich in meinem Besipielcode jedoch für einen Messagehook entschieden.

Beispielcode

Ich habe eine Klasse TTooltipEx erstellt, die das Problem für den Benutzer völlig transparent löst. Der Code dieser Klasse ist weiter unten in diesesm Dokument verfügbar.
Diese Klasse hat ein paar Schwachstellen, die bei den meisten Anwendungsfällen nicht so ins Gewicht fallen sollten, der Vollständigkeit halber jedoch angeführt sind.

  1. Der Beispielcode liegt nur als OWL Code vor, nicht als MFC Code. Die Klasse sollte jedoch praktisch eins-zu-eins umsetztbar sein.
  2. Die Klasse wurde für modale Dialoge erstellt, kann also immer nur eine Instanz von sich selbst verwalten. Die Messagehook-Prozedur ist eine statische Memberfunktion der Klasse und greift über das statische Datenelement "globalObject" auf anderen Datenelemente und Methoden der TTooltipEx Klasse zu. Dieses Problem läßt sich leicht darurch beheben, daß die Klasse eine statische Liste aller Instanzen verwaltet.
  3. Ein generelles Problem des Workarounds ist, daß er es nicht zuläßt, daß ein Fenster vom Status "disabled" nach "enabled" wechselt. In diesem Fall müßte das Fenster auch wieder normal behandelt werden und der rechteckige Tooltip im Elternfenster durch einen normalen Tooltip, der das ganze Fenster umfaßt, ersetzt werden. Um dies transparent für den Anwender durchzuführen müßte man wohl in der Hook-Prozedur die WM_ENABLE Nachricht behandeln und bei ihrem Eintreffen zwischen den beiden "Modi" wechseln.
 TooltipEx.h
#ifndef __TOOLTIP_EX_H
#define __TOOLTIP_EX_H

class TTooltipEx : public TTooltip
{
  public:
  TTooltipEx(TWindow* parent, bool alwaysTip = true, TModule* module = 0);
  virtual ~TTooltipEx();

  void AttachHook();
  void DetachHook();
  bool AssignTooltip(TWindow *window, const char *text);

  protected:
  static HHOOK       hHook;
  static TTooltipEx *globalObject;

  static LRESULT CALLBACK MessageHook(int code, WPARAM wParam, LPARAM lParam);
};

#endif // __TOOLTIP_EX_H

 TooltipEx.cpp
/******************************************************************************
 * TooltipEx.cpp                                        1998 by Thomas Krammer
 ******************************************************************************
 * Erweitert die TTooltip Klasse so, dass auch zu statischen Textfeldern und
 * disabled Controls Tooltips hinzugefuegt werden koennen.
 * Erstellt und getestet mit Borland C++ 5.02
 ******************************************************************************/

#include <owl/tooltip.h>
#include <owl/pch.h>
#pragma hdrstop
#include "tooltipex.h"

// globale Konstanten
const char * STATIC_CONTROL_CLASS_NAME = "STATIC";

// statische Datenelemente
TTooltipEx *TTooltipEx::globalObject = NULL;
HHOOK TTooltipEx::hHook = NULL;

// Konstruktor
TTooltipEx::TTooltipEx(TWindow *parent, bool alwaysTip, TModule *module) :
  TTooltip(parent, alwaysTip, module)
{
  AttachHook();
}

// Destruktor
TTooltipEx::~TTooltipEx()
{
  DetachHook();
}

// AttachHook
// Installiert den Nachrichtenhook. Wirft eine xmsg Exception, wenn die
// Installation fehlgeschlagen ist.
void TTooltipEx::AttachHook()
{
  globalObject = this;

  if(hHook == NULL) {
    hHook = SetWindowsHookEx(WH_GETMESSAGE, MessageHook, NULL, GetCurrentThreadId());

    if(hHook == NULL)
      throw xmsg("Can't install hook!");
  }
}

// DetachHook
// Traegt den Nachrichtenhook aus der Liste der Hooks aus.
void TTooltipEx::DetachHook()
{
  if(hHook) {
    UnhookWindowsHookEx(hHook);
    hHook = NULL;
  }

  globalObject = NULL;
}

// AssignTooltip
// Fuegt einem Fenster einen Tooltip hinzu. Ist das Fenster disabled oder
// ein statisches Textfeld (static control) wird nicht das Fenster als
// Tool verwendet, sondern der Bereich im darunter liegenden Fenster.
bool TTooltipEx::AssignTooltip(TWindow *window, const char *text)
{
  TToolInfo toolInfo;

  char className[255];

  if( ::GetClassName((HWND)(*window), className, sizeof(className)) <= 0)
    return false;

  HWND parent = (HWND)*(globalObject->GetParentO());

  if( strcmpi( className, STATIC_CONTROL_CLASS_NAME ) == NULL ||
      ::IsWindowEnabled((HWND)(*window)) != TRUE ) {
    // es handelt sich um ein fuer WindowFromPoint() durchsichtiges Fenster

    // WindowRect des Fensters in Clientkoordinaten des uebergeordneten
    // Fensters umrechnen.
    TRect toolRect = window->GetClientRect();
    TPoint tl = toolRect.TopLeft(), br = toolRect.BottomRight();

    window->ClientToScreen(tl);
    window->ClientToScreen(br);

    GetParentO()->ScreenToClient(tl);
    GetParentO()->ScreenToClient(br);

    toolRect = TRect(tl, br);

    toolInfo = TToolInfo(parent, toolRect, 0, text);
  } else {
    toolInfo = TToolInfo(parent, (HWND)(*window), text);
  }

  // Tool zum Tooltip Control hinzufuegen.
  return AddTool(toolInfo);
}

// MessageHook
// Nachrichtenhook, der das Weiterleiten der Nachrichten an das Tooltip
// Control uebernimmt. Dabei werden bei den disabled und static Controls
// die Nachrichten so umgewandelt, als wuerden sie vom uebergeordneten Fenster
// kommen.
LRESULT CALLBACK TTooltipEx::MessageHook(int code, WPARAM wParam, LPARAM lParam)
{
  if(code < 0 || globalObject == NULL)
    return CallNextHookEx(hHook, code, wParam, lParam);

  MSG *msg = (MSG *)lParam;

  switch(msg->message) {
    case WM_MOUSEMOVE:
    case WM_LBUTTONDOWN:
    case WM_LBUTTONUP:
    case WM_MBUTTONDOWN:
    case WM_MBUTTONUP:
    case WM_RBUTTONDOWN:
    case WM_RBUTTONUP:
    {
      HWND parent = (HWND)*(globalObject->GetParentO());

      if(msg->hwnd == parent || ::IsChild(parent, msg->hwnd)) {
        // Nachricht wurde an ein unser Parentfenster oder an ein Kind davon
        // geschickt.

        char windowClass[255];

        ::GetClassName(msg->hwnd, windowClass, sizeof(windowClass));

        if(strcmpi(windowClass, STATIC_CONTROL_CLASS_NAME) == 0 ||
           ::IsWindowEnabled(msg->hwnd) != TRUE ) {
          // Es handelt sich um ein fuer WindowFromPoint() durchsichtiges
          // Fenster. Also die Nachricht so umwandeln, als wuerde sie vom
          // uebergeordneten Fenster des Controls stammen.

          // Koordinaten von Clientkoordinaten des Controls in Client-
          // koordinaten des uebergeordneten Fensters umwanden.
          TPoint point(LOWORD(lParam), HIWORD(lParam));

          ::ClientToScreen(msg->hwnd, &point);
          ::ScreenToClient(parent, &point);

          // MSG Struct mit Daten fuellen.
          MSG modMsg;

          modMsg.message = msg->message;
          modMsg.wParam  = msg->wParam;
          modMsg.pt      = msg->pt;
          modMsg.time    = msg->time;

          modMsg.hwnd     = parent;
          modMsg.lParam  = MAKELPARAM(point.x, point.y);

          // veraenderte Nachricht an Tooltip Control weiterleiten
          globalObject->RelayEvent(modMsg);
        } else {
          // es handelt sich um ein ganz normales Control. Die Nachricht
          // kann unveraendert an das Tooltip Control weitergeleitet werden.
          globalObject->RelayEvent(*msg);
        }
      }
    }
  }

  // naechsten Hook in der Kette aufrufen.
  return CallNextHookEx(hHook, code, wParam, lParam);
}

      Top 
 

| © 1998 by 3rd-evolution