Das Problem ist ja, dass deaktivierte Controls keinerlei Mausereignisse auslösen bzw. behandeln und damit das Anzeigen eines Tooltips in diesem Zustand nicht möglich oder vorgesehen ist. Bei meiner Suche nach einer Lösung habe ich durchgängig nur einen Ansatz gefunden: Im Fenster (bzw. Parent-Control des Buttons) soll das MouseMove-Ereignis behandelt und damit ein Tooltip angezeigt werden, sobald der Cursor über den deaktivierten Button fährt.
Die Lösung erschien mir aber aus zwei Gründen nicht ideal:
1. In dem Projekt, in dem die Anforderung aufgetaucht ist, gibt es etliche Fensterklassen ohne gemeinsame Basisklasse und die Lösung müsste daher in mehrere Klassen eingebunden werden.
2. Mir erscheint der Ansatz auch wenig performant, bei jeder noch so irrelevanten Mausbewegung zu prüfen, ob sich der Cursor gerade über dem deaktivierten Button befindet.
Da es (glücklicherweise) nur eine Basisklasse für alle Buttons in dem Projekt gibt, habe ich im besten Fall also nach einer schnell zu implementierenden Lösung gesucht, die ich nur in dieser einen Klasse umsetzen bzw. einbinden muss.
Ich habe mir also überlegt, beim Deaktivieren eines Buttons ein transparentes Panel über den Knopf zu legen, welches den gleichen Tooltip erhält wie der Button selbst. Beim Aktivieren des Buttons wird das Panel wieder entfernt, sodass der Button auch wieder angeklickt werden kann.
Der Reihe nach:
Wie schon erwähnt, gibt es in dem Projekt bereits eine Basisklasse für alle Buttons, die unter anderem die Möglichkeit bereitstellt, einfach Tooltips anzuzeigen. Die (auf das Wesentliche reduzierte) Klasse sah dafür so aus:
public class MyTooltipButton : Button
{
private Tooltip tt;
/// <summary>
/// Ermittelt den Text, der im Tooltip angezeigt werden soll, oder legt diesen fest
/// </summary>
[DefaultValue("")]
public string Tooltip
{
get { return tt.GetToolTip(this); }
set { tt.SetToolTip(this, value); }
}
}
Um nun den Code der Klasse nicht unnötig mit Tooltip-bezogenen Dingen aufzublähen, habe ich eine Manager-Klasse erstellt, die sich um alle Dinge für das Anzeigen der Tooltips kümmert.
Wie bereits erwähnt, wird in dieser Lösung ein transparentes Panel verwendet, welches über den Knopf gelegt wird. Die Implementierung der Klasse für unsichtbare bzw. transparente Panels sieht dabei wie folgt aus:
class TransparentPanel : Panel
{
public TransparentPanel()
{
this.SetStyle(ControlStyles.Opaque, true); // keinen Hintergrund zeichnen
}
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.Style |= 0x20; // transparent
return cp;
}
}
}
In der Manager-Klasse (im Beispiel: MyTooltipManager) brauchen wir nun 3 Objekte, um das Vorhaben umzusetzen: die Instanz des Buttons, eine Instanz des transparenten Panels und natürlich eine ToolTip-Instanz. Die Klasse bekommt also 3 Instanzvariablen, um diese Objekte zu halten, sowie einen Konstruktor, der die Instanz des Buttons entgegennimmt:
public class MyTooltipManager : IDisposable
{
private Control ctrl;
private ToolTip tooltip = new ToolTip();
private TransparentPanel panel;
public MyTooltipManager(Control ctrl)
{
this.ctrl = ctrl;
// Benötigte Events des Controls registrieren
ctrl.EnabledChanged += new EventHandler(ctrl_EnabledChanged);
ctrl.VisibleChanged += new EventHandler(ctrl_VisibleChanged);
ctrl.LocationChanged += new EventHandler(ctrl_LocationChanged);
ctrl.SizeChanged += new EventHandler(ctrl_SizeChanged);
}
}
Um das Ganze später bei Bedarf auch für andere Controls einsetzen zu können, habe ich hier gleich Control als Typ verwendet und nicht Button.
Wie im Code auch zu sehen ist, werden im Konstruktor diverse Ereignisse des Controls registriert. Einmal brauchen wir die Information, wenn das Control deaktiviert bzw. aktiviert wird, um das Panel einzufügen bzw. zu entfernen. Andererseits müssen wir wissen, wenn sich die Größe oder die Position des controls ändert, um das Panel dann ebenfalls zu skalieren oder eben zu verschieben. Außerdem müssen wir noch wissen, wenn das Control sichtbar wird, um dann ggf. im deaktivierten Zustand das Panel hinzuzufügen.
Die EventHandler dafür sehen so aus:
private void ctrl_EnabledChanged(object sender, EventArgs e)
{
if (ctrl.Enabled)
{
RemovePanel();
}
else
{
// Panel nur hinzufügen, wenn das Control auch sichtbar ist
if (ctrl.Visible)
{
AttachPanel();
}
}
}
private void ctrl_VisibleChanged(object sender, EventArgs e)
{
// Panel einfügen, wenn noch nicht geschehen
if (ctrl.Visible && !ctrl.Enabled && (this.panel == null || this.panel.Parent != this.ctrl.Parent))
{
AttachPanel();
}
}
private void ctrl_LocationChanged(object sender, EventArgs e)
{
if (this.panel != null)
{
this.panel.Location = this.ctrl.Location;
}
}
private void ctrl_SizeChanged(object sender, EventArgs e)
{
if (this.panel != null)
{
this.panel.Size = this.ctrl.Size;
}
}
Ich denke, der Code bedarf keiner weiteren Erklärung. Die im EnabledChanged-Handler verwendeten Methoden AttachPanel() und RemovePanel() sehen folgendermaßen aus:
/// <summary>
/// Erstellt das Panel, falls noch nicht vorhanden, und legt es über das Control
/// </summary>
private void AttachPanel()
{
if (this.panel == null)
{
this.panel = new TransparentPanel();
}
this.panel.Location = this.ctrl.Location;
this.panel.Size = this.ctrl.Size;
this.tooltip.SetToolTip(this.panel, this.tooltip.GetToolTip(this.ctrl));
this.panel.Parent = this.ctrl.Parent;
this.panel.BringToFront();
}
/// <summary>
/// Entfernt das Panel
/// </summary>
private void RemovePanel()
{
if (this.panel != null)
{
this.panel.Parent = null;
}
}
In der Methode AttachPanel wird das transparente Panel also erstellt (falls noch nicht vorhanden) und anschließend in Größe und Postion dem Control angepasst. Dann wird noch der Tooltip des Controls übernommen und das Panel in den Vordergrund geschoben, sodass es auch in jedem Fall vor dem Control platziert wird.
Wie im bisher gezeigten Quellcode schon ersichtlich, bekommt die Manager-Klasse noch eine Dispose()-Methode verpasst:
#region IDisposable Member
public void Dispose()
{
Dispose(true);
}
#endregion
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
ctrl.EnabledChanged -= ctrl_EnabledChanged;
ctrl.VisibleChanged -= ctrl_VisibleChanged;
ctrl.LocationChanged -= ctrl_LocationChanged;
ctrl.SizeChanged -= ctrl_SizeChanged;
if (this.panel != null)
{
this.panel.Dispose();
}
this.tooltip.Dispose();
}
}
Jetzt benötigen wir nur noch eine Möglichkeit, um den Text des Tooltips festzulegen, und schon haben wir alles, was wir brauchen:
/// <summary>
/// Ermittelt den Text, der im Tooltip angezeigt werden soll, oder legt diesen fest
/// </summary>
public string Tooltip
{
get { return this.tooltip.GetToolTip(this.ctrl); }
set
{
this.tooltip.SetToolTip(this.ctrl, value);
if (this.panel != null)
{
this.tooltip.SetToolTip(this.panel, value);
}
}
}
Damit ist die Manager-Klasse schon komplett. Jetzt muss diese nur noch in die am Anfang gezeigte Button-Klasse eingebunden werden.
Die "neue" Basisklasse für die Buttons des Projekts sieht dann so aus:
public class MyTooltipButton : Button
{
private MyTooltipManager ttManager;
public MyTooltipButton()
{
ttManager = new MyTooltipManager(this);
}
/// <summary>
/// Ermittelt den Text, der im Tooltip angezeigt werden soll, oder legt diesen fest
/// </summary>
[DefaultValue("")]
public string Tooltip
{
get { return ttManager.Tooltip; }
set { ttManager.Tooltip = value; }
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
ttManager.Dispose();
}
base.Dispose(disposing);
}
}
Das war's dann auch schon. Ein fertiges Beispiel-Projekt ist im Anhang zu finden. Über Meinungen und Verbesserungsvorschläge würde ich mich natürlich freuen!
edit: Behandlung des VisibleChanged-Ereignisses hinzugefügt
Schlagwörter: Tooltip, deaktiviert, disabled