Laden...

Tooltips für deaktivierte Controls

Erstellt von typhos vor 13 Jahren Letzter Beitrag vor 13 Jahren 9.173 Views
T
typhos Themenstarter:in
243 Beiträge seit 2006
vor 13 Jahren
Tooltips für deaktivierte Controls

Nachdem in einem Projekt die Anforderung auftauchte, auch bei deaktivierten Buttons Tooltips anzuzeigen, da man ja trotzdem gern wissen möchte, was der Button machen würde bzw. warum er im Moment deaktiviert ist, habe ich mich im guten alten Internet auf die Suche nach einer Lösung gemacht.

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

B
142 Beiträge seit 2007
vor 13 Jahren

Hallo,

echt nützlich aber du hast einen Fall vergessen, den du noch ergänzen solltest.

// Panel nur hinzufügen, wenn das Control auch sichtbar ist

Wenn ich das Ding aber unsichtbar habe, deaktiviere und dann Anzeige? Panel mit dem ToolTip ist nicht da.
Sprich du müsstest noch auf das VisibleChanged-Event reagieren.

Gruß
Björn

PS: Ich habe den Code nicht getestet sondern rein von der Logik her..

T
typhos Themenstarter:in
243 Beiträge seit 2006
vor 13 Jahren

Danke für den Hinweis, das klingt logisch. Ich werde das so bald wie möglich noch nachrüsten 😃

C
2.121 Beiträge seit 2010
vor 13 Jahren

Warum versuchst du nicht einfach den Button an sich umzubauen?
Das Property Enabled könnte man überschreiben, dann die Events dazu so ändern dass keine Ereignisse ausgelöst werden wenn Enabled == false, die Darstellung ändern so dass der Button gleich aussieht und reagiert wie vorher, nur ist er halt weiterhin Enabled.
Vielleicht wär das ja ein durchgängigeres Konzept.

Oder du umgehst dein Problem von vorne her und machst die Buttons unsichtbar wenn sie disabled sind. Dann will auch keiner einen Tooltip sehen 😃 und Formulare mit mehreren disabled Buttons sehen dann auch etwas aufgeräumter aus.

1.820 Beiträge seit 2005
vor 13 Jahren

Hallo!

@chilic:
Es scheint ja eine Anforderung des Projekts zu sein,dass auch von deaktivierten Buttons der Tooltip darstellbar sein soll. Warum das so sein soll, spielt für uns keine Rolle. Und die Buttons zu entfernen, ist nur dann sinnvoll, wenn komplette Gruppen wegfallen. Wenn nur einzelne Buttons zwischen anderen unsichtbar werden, würde eine Oberfläche u.U. sogar unaufgeräumter wirken, ausser man schiebt dann die übrigen wieder zusammen.

Nobody is perfect. I'm sad, i'm not nobody 🙁

C
2.121 Beiträge seit 2010
vor 13 Jahren

Klar, wenns so sein muss dann solls auch so sein. Aber manchmal ist ein Problem am einfachstens dadurch gelöst, dass man es nicht mehr als Problem ansieht 😉

T
typhos Themenstarter:in
243 Beiträge seit 2006
vor 13 Jahren

@ Björn: Ich habe jetzt noch die Behandlung des VisibleChanged-Events eingebaut.

@ chilic: Ein Ausblenden der Buttons kam in diesem Fall leider nicht infrage, weil der Nutzer sehen soll, dass es in bestimmten Modi noch weitere Funktionen gibt. Auch das Überschreiben bzw. Ändern der Enabled-Eigenschaft war nicht wirklich attraktiv, da diese Eigenschaft leider nicht überschrieben werden kann (zumindest nicht im Framework 2.0).

C
2.121 Beiträge seit 2010
vor 13 Jahren

da diese Eigenschaft leider nicht überschrieben werden kann (zumindest nicht im Framework 2.0).

Mit "new" auch nicht?
public new bool Enabled { ... }

T
typhos Themenstarter:in
243 Beiträge seit 2006
vor 13 Jahren

Hallo,
mit "new" geht es natürlich. Aber dann hast du ja das Problem, dass die Eigenschaft nicht verwendet wird, wenn du irgendwas mit allen Controls machen willst (z.B. über die Controls-Collection eines Fensters) - dann wird ja die Standard-Enabled-Eigenschaft verwendet. Daher kam das für mich hier nicht infrage.

49.485 Beiträge seit 2005
vor 13 Jahren

Hallo chilic,

Mit "new" auch nicht?

nein, new ist kein Überschreiben, sondern eine Krankheit. new ist insbesondere kein Ersatz für override. Wenn eine Eigenschaft nicht virtuell ist, kann man sie nicht überschreiben. new ändert nichts an dieser Tatsache.

herbivore

D
4 Beiträge seit 2010
vor 13 Jahren

Man könnte auch einen MessageFilter erstellen, der anhand der aktuellen Mausposition MouseEnter, MouseMove und MouseLeave Ereignisse generiert und an das deaktivierte Control weiterleitet oder selbst auslöst.

Hat den Vorteil, dass man nicht extra Klassen ableiten muss und das OnMouseMove der Formulare bleibt ebenfalls unberührt. Außerem lässt sich ein Filter für jedes Control je nach Bedarf ein- und abschalten.