Dieser Artikel beschreibt, wie man ein ChildWindow zur Laufzeit mit einem einfachen Mausklick auf den Overlay, also einer einzigen Aktion, umpositionieren kann.
Grundsätzliche Funktionsweise
Der Code funktioniert so, dass wir uns an das MausLeftButtonUp Event des Overlay zur Laufzeit anhängen, die aktuelle Position des Mauszeigers im Root abfragen und das ChildWindow an die Mausklick-Position verschieben. Zu Demonstrationszwecken wird das ChildWindow ausgerichtet an seiner oberen linken Ecke an die neue Position verschoben.
Schritt für Schritt
In einem neuen Silverlight 3 Projekt fügen wir als neues Element ein ChildWindow Steuerelement hinzu, dessen Datei wir mit ClickChildWindow.xaml benennen. In MainPage.xaml fügen wir einen Button hinzu, den wir btShow nennen. Der Aufruf des ChildWindow erfolgt im Click EventHandler von btShow:
Private cwC As New ClickChildWindow
Private Sub btShow_Click(ByVal sender As Object, _
ByVal e As System.Windows.RoutedEventArgs) _
Handles btShow.Click
cwC.Show()
End Sub
Im CodeBehind der Datei ClickChildWindow.xaml brauchen wir einen Zugriff auf einzelne Elemente des Control Templates, genau gesagt, auf die Template Parts Root, Overlay und ContentRoot. Als erstes brauchen wir daher drei Variablen vom Typ FrameworkElement, denen wir später diese Parts zur weiteren Verarbeitung zuweisen können. Diese Variablen deklarieren wir als Private, damit sie solange im Speicher bleiben, wie das ChildWindow geladen ist.
Private root As FrameworkElement
Private overlay As FrameworkElement
Private contentroot As FrameworkElement
Um diesen Variablen die entsprechenden Template Parts (Root, Overlay und ContentRoot) zuweisen zu können, brauchen wir Zugriff auf den VisualTree des Control Templates, also auf dessen visuelle Element-Struktur. Diesen Zugriff erhalten wir an oberster Stelle über die Methode GetChild der Klasse VisualTreeHelper. VisualTreeHelper.GetChild setzt voraus, dass die visuelle Struktur eines Control Template bereits vollständig geladen ist. Wenn das nicht der Fall ist, gibt VisualTreeHelper.GetChild einfach Nothing zurück und der Code wirft einen Fehler.
Genau an dieser entscheidenden Stelle gibt es ein kleines Problem. Wie stellt man sicher, dass im Zeitpunkt des Einsatzes von VisualTreeHelper.GetChild die visuelle Strukur auch bereits vollständig geladen ist?
Eine Lösung besteht darin, VisualTreeHelper.GetChild im EventHandler eines Steuerelements abzufragen, also z.B. im Click-EventHandler eines Buttons. Das funktioniert nach meiner Erfahrung immer, ist aber ziemlich unperformant. Schicker wäre es, den VisualTree schon beim Laden des ChildWindow abzufragen.
Wenn man aber den VisualTree beim Laden des Steuerelements abfragen möchte, gibt es das Problem, dass beim Laden des ChildWindow der VisualTree nicht immer schon vollständig geladen ist. Der folgende Code berücksichtigt dieses Problem und zeigt eine Lösung auf, deren grundsätzliche Funktionsweise auch an anderer Stelle verwendet werden kann.
Zunächst fügen wir mit AddHandler dem Loaded-Event unseres ChildWindow einen neuen RoutedEventHandler hinzu.
Public Sub New()
InitializeComponent()
AddHandler Me.Loaded, (New RoutedEventHandler _
(AddressOf ThisChildWindow_Loaded))
End Sub
Wie man sieht, habe ich AddHandler bereits in den Contructor New() des ChildWindow eingebaut. Wenn man AddHandler demgegenüber im LoadedEvent selbst einfügen würde, würde die Zuweisung mal funktionieren, und mal nicht.
Der mit AddHandler dem Loaded-Event des ChildWindow hinzugefügte RoutedEventHandler ThisChildWindow_Loaded wird jetzt verläßlich immer aufgerufen, nachdem das ClickChildWindow_Loaded Event durchgelaufen ist.
Wenn man jetzt die Abfrage des VisualTree mit dem folgenden Code in die Routine ThisChildWindow_Loaded einbauen würde ...
Private Sub ThisChildWindow_Loaded(ByVal sender As Object, _
ByVal e As RoutedEventArgs)
If root Is Nothing Then
root = VisualTreeHelper.GetChild(Me, 0) ' FEHLER !!!
' ...
End If
End Sub
.. wäre nicht sichergestellt, dass der VisualTree bereits vollständig geladen ist. Wir würden also Gefahr laufen, dass unsere Variable root den Wert Nothing erhalten würde und ein Fehler geworfen wird oder der nachfolgende Code nicht funktioniert. Wenn man den Code von ThisChildWindow_Loaded erweitert, und vorher noch abfragt, ob der VisualTree mehr als 0 Kindelemente hat ...
Private Sub ThisChildWindow_Loaded(ByVal sender As Object, _
ByVal e As RoutedEventArgs)
' Kein Fehler mehr. Aber mal funktioniert's
' und mal nicht ...
If VisualTreeHelper.GetChildrenCount(Me) > 0 Then
If root Is Nothing Then
root = VisualTreeHelper.GetChild(Me, 0)
' ...
End If
End If
End Sub
... dann funktioniert der Code manchmal und manchmal nicht. Probiert es selbst aus. Mal wird der VisualTree vollständig geladen und mal nicht.
Die Lösung besteht darin, die Routine ThisChildWindow_Loaded erforderlichenfalls nochmal aufzurufen, wenn der VisualTree (noch) nicht vollständig geladen ist. Dafür brauchen wir einen Delegate für unsere Routine ThisChildWindow_Loaded, über den wir dann innerhalb der Routine ThisChildWindow_Loaded mithilfe eines Dispatcher die Routine immer dann nochmal aufrufen, wenn der VisualTree (noch) nicht vollständig geladen ist. Klingt schräg, funktioniert aber. Und so sieht das Ganze dann aus:
Public Delegate Sub ThisChildWindow_LoadedDelegate _
(ByVal sender As Object, ByVal e As RoutedEventArgs)
Private Sub ThisChildWindow_Loaded(ByVal sender As Object, _
ByVal e As RoutedEventArgs)
If VisualTreeHelper.GetChildrenCount(Me) = 0 Then
Dispatcher.BeginInvoke(New ThisChildWindow_LoadedDelegate _
(AddressOf ThisChildWindow_Loaded), Me, e)
Return
Else
If root Is Nothing Then
root = VisualTreeHelper.GetChild(Me, 0)
overlay = root.FindName("Overlay")
contentroot = root.FindName("ContentRoot")AddHandler overlay.MouseLeftButtonUp, _
AddressOf SetNewPosition
End If
End If
End Sub
Nach der Else-Verzweigung wird der Variablen root über VisualTreeHelper.GetChild(0) dann der Template Part Root zugewiesen. Den Template Part Overlay für die Zuweisung zu der Variablen overlay finden wir über die Methode FindName. FindName sucht im VisualTree nach dem Element namens "Overlay". Gleichermaßen weisen wir der Variablen contentroot den Template Part ContentRoot zu.
Mit AddHandler fügen wir dann noch dem MouseLeftButtonUp Event des Overlay eine neue Routine (SetNewPosition) zu, in der dann die eigentliche Neupositionierung des ChildWindow stattfindet. Der Code von SetNewPosition sieht so aus:
Private Sub SetNewPosition(ByVal sender As Object, _
ByVal e As MouseEventArgs)
Me.HorizontalAlignment = Windows.HorizontalAlignment.Left
Me.VerticalAlignment = Windows.VerticalAlignment.Top
Dim targetPosition As Point = e.GetPosition(root)
Dim mgLeft As Double = targetPosition.X
Dim mgTop As Double = targetPosition.Y
Dim mgCW As New Thickness
mgCW.Left = mgLeft
mgCW.Top = mgTop
contentroot.Margin = mgCW
End Sub
In SetNewPosition wird das ChildWindow zunächst Top Left ausgerichtet.
Me.HorizontalAlignment = Windows.HorizontalAlignment.Left
Me.VerticalAlignment = Windows.VerticalAlignment.Top
Dann erzeugen wir eine neue Variable namens targetPosition vom Typ Point, der wir die Position des Mauszeigers innerhalb des Root durch Auswertung des Rückgabewerts von e.GetPosition zuweisen. e ist vom Typ MouseEventArgs und GetPosition ist Methode dieser Klasse, die die Mauszeigerposition relativ zu einem UIElement zurückgibt. e.GetPosition braucht als Parameter das UIElement, zu dem die Mauszeigerposition realtiv abgefragt werden soll. Hier übergeben wir e.GetPosition unsere Variable root. Root stellt die gesamte Fläche des ChildWindow-Steuerelements dar, definiert durch die Grenzen des Silverlight-Plugin im Browser. Also genau was wir brauchen.
Dim targetPosition As Point = e.GetPosition(root)
Zwei weitere Variablen vom Typ Double nehmen den .X bzw. .Y Wert von targetPosition auf:
Dim mgLeft As Double = targetPosition.X
Dim mgTop As Double = targetPosition.Y
Dann erzeugen wir eine Variable (mgCW) vom Typ Thickness. Dem .Top-Wert von mgCW wird der Abstand der Mausposition zur oberen Grenze des Root übergeben (enthalten in mgTop). Und dem .Left-Wert von mgCW wird der Abstand der Mausposition zum linken Rand des Root übergeben (enthalten in mgLeft).
Dim mgCW As New Thickness
mgCW.Left = mgLeft
mgCW.Top = mgTop
Zum Schluß übergeben wir unserer Variablen contentroot den Thickness-Wert mgCW als neue Margin.
contentroot.Margin = mgCW
Contentroot entspricht visuell dem, was der Nutzer als das eigentliche ChildWindow (das Window) wahrnimmt. Veränderungen der Margin-Werte an dem Template Part ContentRoot bzw. unserer Variablen contentroot wirken sich also auf die Margin-Werte des visuellen ChildWindow aus.
Das war's. Jedesmal, wenn der Nutzer jetzt mit Maus auf das Overlay klickt wird das ChildWindow, ausgerichtet an seiner oberen linken Ecke, an die Mausklick-Position verschoben.
Ich hoffe der Code hilft.
Beste Grüße,
Martin (SilverLaw)
Keine Kommentare:
Kommentar veröffentlichen