Seiten

Freitag, 16. April 2010

How To: ChildWindow durch Mausclick auf den Overlay neu positionieren

Der standardmäßige Ablauf beim Umpositionieren eines Silverlight 3 ChildWindow ist zur Laufzeit ein Drag-and-Drop Prozess. Der Nutzer klickt mit der linken Maustaste auf die Fensterleiste des ChildWindow, hält die Maustaste gedrückt, zieht es an die neue Position und läßt dann die Maustaste wieder los. Das sind insgesamt drei Aktionen, die der Nutzer auf dem Weg zum Ziel vornehmen muss (Maus-Klick, Maus-Bewegung, Maus-Loslassen).

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