2016-04-19 14 views
6

Seit den letzten zwei Tagen bin ich auf einem (einfachen?) Problem festgefahren. Ich habe viel im Internet gesucht, aber ich finde kein Beispiel, das die Situation, die ich vollständig habe, löst (jedes Mal, wenn ein Aspekt ausgelassen wird, was der Bruchfaktor in meiner eigenen Implementierung ist).WPF MVVM bewegliche Formen mit itemscontrol

Was will ich:

meine eigene Kontrolle WPF erstellen, die mit einem Bild zeigt auf Rechtecke (oder Formen tatsächlich im Allgemeinen), die fixiert bleiben, während Zoomen und Schwenken. Außerdem müssen diese Rechtecke in der Größe geändert werden (noch nicht) und bewegbar sein (gerade jetzt).

Ich möchte, dass diese Steuerung das MVVM-Designmuster einhält.

Was muss ich:

Ich habe eine XAML-Datei mit einem Itemscontrol. Dies zeigt eine dynamische Anzahl von Rechtecken an (die von meinem ViewModel stammen). Es ist an die RectItems meines ViewModel (eine ObservableCollection) gebunden. Ich möchte die Elemente als Rechtecke darstellen. Diese Rechtecke müssen von einem Benutzer mit seiner Maus bewegt werden können. Beim Verschieben sollte es meine Modell-Entitäten im ViewModel aktualisieren.

XAML:

<ItemsControl ItemsSource="{Binding RectItems}"> 
<ItemsControl.ItemsPanel> 
     <ItemsPanelTemplate> 
      <Canvas IsItemsHost="True"> 

      </Canvas> 
     </ItemsPanelTemplate> 
    </ItemsControl.ItemsPanel> 
<ItemsControl.ItemContainerStyle> 
    <Style> 
     <Setter Property="Canvas.Left" Value="{Binding TopLeftX}"/> 
     <Setter Property="Canvas.Top" Value="{Binding TopLeftY}"/> 
    </Style> 
</ItemsControl.ItemContainerStyle> 
<ItemsControl.ItemTemplate> 
    <DataTemplate> 
     <Rectangle Stroke="Black" StrokeThickness="2" Fill="Blue" Canvas.Left="0" Canvas.Top="0" 
      Height="{Binding Height}" Width="{Binding Width}"> 
      <i:Interaction.Triggers> 
       <i:EventTrigger EventName="MouseLeftButtonUp"> 
        <i:InvokeCommandAction Command="{Binding ElementName=border,Path=DataContext.MouseLeftButtonUpCommand}" CommandParameter="{Binding}" /> 
       </i:EventTrigger> 
       <i:EventTrigger EventName="MouseLeftButtonDown"> 
        <i:InvokeCommandAction Command="{Binding ElementName=border,Path=DataContext.MouseLeftButtonDownCommand}" CommandParameter="{Binding}" /> 
       </i:EventTrigger> 
       <i:EventTrigger EventName="MouseMove"> 
        <i:InvokeCommandAction Command="{Binding ElementName=border,Path=DataContext.MouseMoveCommand}" CommandParameter="{Binding}" /> 
       </i:EventTrigger> 
      </i:Interaction.Triggers> 
     </Rectangle> 
    </DataTemplate> 
</ItemsControl.ItemTemplate> 

Ansichtsmodell:

public class PRDisplayViewModel : INotifyPropertyChanged 
{ 
    private PRModel _prModel; 

    private ObservableCollection<ROI> _ROIItems = new ObservableCollection<ROI>(); 

    public PRDisplayViewModel() 
    { 
     _prModel = new PRModel(); 

     ROI a = new ROI(); 
     a.Height = 100; 
     a.Width = 50; 
     a.TopLeftX = 50; 
     a.TopLeftY = 150; 

     ROI b = new ROI(); 
     b.Height = 200; 
     b.Width = 200; 
     b.TopLeftY = 200; 
     b.TopLeftX = 200; 

     _ROIItems.Add(a); 
     _ROIItems.Add(b); 

     _mouseLeftButtonUpCommand = new RelayCommand<object>(MouseLeftButtonUpInner); 
     _mouseLeftButtonDownCommand = new RelayCommand<object>(MouseLeftButtonDownInner); 
     _mouseMoveCommand = new RelayCommand<object>(MouseMoveInner); 
    } 

    public ObservableCollection<ROI> RectItems 
    { 
     get { return _ROIItems; } 
     set { } 
    } 

    private bool isShapeDragInProgress = false; 
    double originalLeft = Double.NaN; 
    double originalTop = Double.NaN; 
    Point originalMousePos; 

    private ICommand _mouseLeftButtonUpCommand; 

    public ICommand MouseLeftButtonUpCommand 
    { 
     get { return _mouseLeftButtonUpCommand; } 
     set { _mouseLeftButtonUpCommand = value; } 
    } 

    public void MouseLeftButtonUpInner(object obj) 
    { 
     Console.WriteLine("MouseLeftButtonUp"); 

     isShapeDragInProgress = false; 

     if (obj is ROI) 
     { 
      var shape = (ROI)obj; 
      //shape.ReleaseMouseCapture(); 
     } 
    } 


    /**** More Commands for MouseLeftButtonDown and MouseMove ****/ 

Klasse ROI (dies würde innerhalb PRModel residieren später):

public class ROI 
{ 
    public double Height { get; set; } 

    public double TopLeftX { get; set; } 

    public double TopLeftY { get; set; } 

    public double Width { get; set; } 
} 

So wie ich es sehe ist das Folgende:

ItemsControl rendert ein ROI-Objekt in ein Rechteck. Mausereignisse im Rectangle werden von Befehlen im ViewModel gehandhabt. Das ViewModel verarbeitet beim Empfang eines Mausereignisses Aktualisierungen direkt auf dem ROI-Objekt. Dann sollte die Ansicht neu gezeichnet werden (vorausgesetzt, das ROI-Objekt hat sich geändert) und daher neue Rechtecke usw. generiert.

Was ist das Problem?

In den Mouse-Ereignishandlern muss ich die CaptureMouse() -Methode des Rectangle aufrufen, auf dem das Mausereignis aufgetreten ist. Wie bekomme ich Zugriff auf dieses Rechteck?

Wahrscheinlich ist das eigentliche Problem, dass meine Sicht auf MVVM hier falsch ist. Sollte ich tatsächlich versuchen, die ROI-Objekte in den Maus-Event-Handlern im ViewModel zu aktualisieren? Oder sollte ich nur die Rectangle-Objekte aktualisieren? Wenn letzteres gilt, wie werden dann Aktualisierungen an die tatsächlichen ROI-Objekte weitergegeben?

ich viele andere Fragen geprüft, unter denen die, die unten, aber ich immer noch nicht gelingt, mein Problem zu lösen:

Add n rectangles to canvas with MVVM in WPF

Dragable objects in WPF in an ItemsControl?


EDIT: Thank Sie alle für Ihre Antworten. Ihre Eingabe hat mir sehr geholfen. Leider kann ich immer noch nicht funktionieren (ich bin neu bei WPF, aber ich kann mir nicht vorstellen, dass das so hart ist).

Zwei neue Versuche, bei denen ich die INotifyPropertyChanged-Schnittstelle in der ROI-Klasse implementiert habe.

Versuch 1: Ich setzte die mit MouseDragElementBehavior wie folgt ziehen:

   <ItemsControl ItemsSource="{Binding RectItems}"> 
       <ItemsControl.ItemsPanel> 
         <ItemsPanelTemplate> 
          <Canvas IsItemsHost="True"> 
          </Canvas> 
         </ItemsPanelTemplate> 
       </ItemsControl.ItemsPanel> 
       <ItemsControl.ItemContainerStyle> 
        <Style TargetType="ContentPresenter"> 
         <!-- //--> 
         <Setter Property="Canvas.Left" Value="{Binding TopLeftX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> 
         <Setter Property="Canvas.Top" Value="{Binding TopLeftY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> 
        </Style> 
       </ItemsControl.ItemContainerStyle> 
       <ItemsControl.ItemTemplate> 
        <DataTemplate> 
         <Grid> 
         <Rectangle Stroke="Black" StrokeThickness="2" Canvas.Left="0" Canvas.Top="0" 
          Height="{Binding Height}" Width="{Binding Width}"> 
           <i:Interaction.Behaviors> 
            <ei:MouseDragElementBehavior/> 
           </i:Interaction.Behaviors> 
          </Rectangle> 
         </Grid> 
        </DataTemplate> 
       </ItemsControl.ItemTemplate> 
      </ItemsControl> 

Das funktionierte perfekt! Alles ist kostenlos, auch wenn ich einen Rahmen heranzoome (was ein Elternelement für alles ist).

Problem hier: Nach dem Ziehen eines Rechtecks, sehe ich dies in der Benutzeroberfläche, aber mein ROI-Objekt (mit diesem Rechteck verknüpft) wird nicht aktualisiert? Ich habe TwoWay-Bindung mit dem PropertyChanged UpdateSourceTrigger angegeben, aber das funktioniert immer noch nicht.

Frage hier: Wie bekomme ich, in dieser Situation, meine ROI-Objekte aktualisiert? Ich kann das DragFinished-Ereignis von MouseDragElementBehavior implementieren, aber hier bekomme ich nicht das entsprechende ROI-Objekt? @Chris W., wo könnte ich einen UIElement.Drop-Handler binden? Kann ich das entsprechende ROI-Objekt dort bekommen?

Versuch 2: Wie Versuch 1, aber jetzt habe ich auch die EventTriggers implementiert (wie in meinem ursprünglichen Beitrag). Dort habe ich nichts getan, um das Rechteck in der Benutzeroberfläche zu aktualisieren, aber ich habe nur das entsprechende ROI-Objekt aktualisiert.

Problem hier: Das hat nicht funktioniert, weil die Rechtecke wir 'zweimal' den Vektor bewegten, den sie sollten. Wahrscheinlich ist die erste Bewegung vom Ziehen selbst, und die zweite Bewegung ist das manuelle Update (von mir) in den entsprechenden ROIs.

Versuch 3: Anstatt MouseDragElementBehavior zu verwenden, implementieren Sie "einfach" das Ziehen mit den EventTriggern (wie in meinem ursprünglichen Beitrag). Ich habe keine Rectangle (UI), sondern nur die ROIs (ihre TopLeftX und TopLeftY) aktualisiert.

Problem hier: Dies funktionierte tatsächlich, außer im Fall des Zoomens. Außerdem war das Ziehen nicht nett, weil es ein wenig "flimmerte" und während es die Maus zu schnell bewegte, würde es sein Rechteck verlieren. Im MouseDragElementBehavior wurde eindeutig mehr Logik verwendet, um dies zu vereinfachen.

@Mark Feldman: Vielen Dank für Ihre Antwort. Es hat mein Problem noch nicht gelöst, aber ich mag die Idee, keine GUI-Klassen in meinem ViewModel zu verwenden (wie Mouse.GetPosition, um Draggin selbst zu implementieren). Um mit einem Konverter zu entkoppeln, werde ich später implementieren, wenn die Funktionalität funktioniert.

@Will: Sie haben Recht (MVVM!= kein Code dahinter). Leider sehe ich momentan nicht, wie ich das ausnutzen kann, da die Event-Handler von MouseDragElementBehavior nichts von einem ROI wissen (und dazu müsste ich das ViewModel updaten).


EDIT 2:

Etwas, das arbeitet (zumindest scheint es so) ist die folgende (unter Verwendung von MouseDragElementBehavior):

<ItemsControl ItemsSource="{Binding RectItems}"> 
       <ItemsControl.ItemsPanel> 
         <ItemsPanelTemplate> 
          <Canvas IsItemsHost="True"> 
          </Canvas> 
         </ItemsPanelTemplate> 
       </ItemsControl.ItemsPanel> 
       <ItemsControl.ItemContainerStyle> 
        <Style TargetType="ContentPresenter"> 

        </Style> 
       </ItemsControl.ItemContainerStyle> 
       <ItemsControl.ItemTemplate> 
        <DataTemplate> 
         <Grid> 
         <Rectangle Stroke="Black" StrokeThickness="2" Canvas.Left="0" Canvas.Top="0" 
          Height="{Binding Height}" Width="{Binding Width}"> 
           <i:Interaction.Behaviors> 
            <ei:MouseDragElementBehavior X="{Binding TopLeftX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Y="{Binding TopLeftY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> 

            </ei:MouseDragElementBehavior> 
           </i:Interaction.Behaviors> 
          </Rectangle> 
         </Grid> 
        </DataTemplate> 
       </ItemsControl.ItemTemplate> 
      </ItemsControl> 

I die X- und Y-Eigenschaft gebunden an MouseDragElementBehavior für die entsprechende Eigenschaft von ViewModel. Wenn ich das Rechteck herum ziehe, sehe ich die Werte in der entsprechenden ROI aktualisiert! Außerdem gibt es kein "Flackern" oder andere Probleme beim Ziehen.

das Problem immer noch: Ich musste den Code in ItemContainerStyle entfernen, um die Positionen der Rechtecke zu initialisieren. Wahrscheinlich führt auch ein Update in der Bindung von MouseDragElementBehavior zu einem Update. Dies ist beim Ziehen sichtbar (das Rechteck springt schnell zwischen zwei Positionen).

Frage: mit diesem Ansatz, wie kann ich die Position der Rechtecke initialisieren? Außerdem fühlt sich das wie ein Hack an, also gibt es einen geeigneteren Ansatz?


EDIT 3:

Der folgende Code wird auch die Rechtecke auf die entsprechende Position initialisieren:

<ItemsControl ItemsSource="{Binding RectItems}"> 
       <ItemsControl.ItemsPanel> 
         <ItemsPanelTemplate> 
          <Canvas IsItemsHost="True"> 
          </Canvas> 
         </ItemsPanelTemplate> 
       </ItemsControl.ItemsPanel> 
       <ItemsControl.ItemContainerStyle> 
        <Style TargetType="ContentPresenter"> 
         <!-- //--> 
         <Setter Property="Canvas.Left" Value="{Binding TopLeftX, Mode=OneTime}"/> 
         <Setter Property="Canvas.Top" Value="{Binding TopLeftY, Mode=OneTime}"/> 
        </Style> 
       </ItemsControl.ItemContainerStyle> 
       <ItemsControl.ItemTemplate> 
        <DataTemplate> 
         <Grid> 
         <Rectangle Stroke="Black" StrokeThickness="2" Canvas.Left="0" Canvas.Top="0" 
          Height="{Binding Height}" Width="{Binding Width}"> 
           <i:Interaction.Behaviors> 
            <ei:MouseDragElementBehavior X="{Binding TopLeftX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Y="{Binding TopLeftY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> 

            </ei:MouseDragElementBehavior> 
           </i:Interaction.Behaviors> 
          </Rectangle> 
         </Grid> 
        </DataTemplate> 
       </ItemsControl.ItemTemplate> 
      </ItemsControl> 

Problem noch: Das fühlt sich 'Hacky'. Gibt es eine "bessere" Art und Weise dies zu tun (für so weit scheint es zu funktionieren)?


EDIT 4: landete ich Thumbs up verwenden. Mit DataTemplates konnte ich das Aussehen eines Gegenstandes aus meinem ViewModel (also zB Thumbs) definieren und über ControlTemplates konnte ich definieren, wie die Daumen eigentlich aussehen sollten (zB ein Rechteck). Vorteil der Daumen ist, dass Drag & Drop ist bereits implementiert!

+2

Bevor Sie zu weit in dieses Kaninchenloch kommen. Haben Sie 'MouseDragElementBehavior' betrachtet und das' UIElement.Drop' Event benutzt? –

+0

Gefunden diese [Link] (http://www.codeproject.com/Tips/105637/How-to-make-any-UI-element-drag-able-using-Behavio) von Chris Punkt. kann helfen. –

+1

MVVM! = Kein Codebehind. Ihre Benutzeroberfläche sollte steuern, was in der Benutzeroberfläche vor sich geht, einschließlich der Bewegung von Elementen. Die beste Möglichkeit, ein solches Overlay zu erstellen, wäre ein Benutzersteuerelement mit einem Canvas-Element, das die Rechtecke anzeigt und verschiebt. – Will

Antwort

0

Nun, Ihr erstes Problem ist, dass ROI INotifyPropertyChange nicht unterstützt. Wenn Sie sie also ändern, werden Ihre Grafiken nicht aktualisiert. Das muss zuerst behoben werden.

In Bezug auf das Ereignis Problem werfen Sie einen Blick auf meine Perfy Projekt. Genauer gesagt, schauen Sie sich die Klasse MouseCaptureBehavior an, die dafür verantwortlich ist, Mausnachrichten abzufangen und sie für den Verbrauch durch das Ansichtsmodell zu verpacken, einschließlich der Bereitstellung von Erfassungs- und Freigabefunktionen. In meinen komplexere Anwendungen erstelle ich einen Vertrag zwischen der Ansicht und Ansicht Modell, das typischerweise etwa wie folgt aussieht:

public interface IMouseArgs 
{ 
    Point Pos { get; } 
    Point ParentPos { get; } 
    void Capture(bool parent = false); 
    void Release(bool parent = false); 
    bool Handled { get; set; } 
    object Data { get;} 
    bool LeftButton { get; } 
    bool RightButton { get; } 
    bool Shift { get; } 
    void DoDragDrop(DragDropEffects allowedEffects); 
} 

Es gibt ein paar Dinge zu beachten, über diese Schnittstelle. Erstens interessiert das Ansichtsmodell nicht, was die Implementierung ist, das ist ganz der Ansicht überlassen. Zweitens gibt es Capture/Release-Handler für das Aufrufmodell, die wiederum von der Ansicht bereitgestellt werden.Schließlich gibt es ein Datenfeld, das normalerweise den DataContext des Objekts enthält, auf das geklickt wurde. Dies ist nützlich, um es an die Ansicht weiterzuleiten, damit es weiß, von welchem ​​Objekt Sie sprechen.

Das deckt Dinge von der Ansicht Modellseite, jetzt müssen wir diese Ansichtsseite implementieren. Normalerweise mache ich das mit einem Ereignisauslöser, der direkt an die Befehlshandler im Ansichtsmodell bindet, aber einen Konverter verwendet, um die sichtspezifischen Ereignisargumente in etwas umzuwandeln, das das IMouseArgs-Objekt unterstützt, das mein Ansichtsmodell erwartet:

<!--- this is in the resources block --> 
<conv:MouseEventsConverter x:Key="ClickConverter" /> 

<!-- and this is in the ItemControl's ItemTemplate --> 
<ContentControl> 
    <i:Interaction.Triggers> 
     <i:EventTrigger EventName="PreviewMouseDown"> 
      <cmd:EventToCommand 
       Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:CanvasView}, 
       Path=DataContext.MouseDownCommand}" 
       PassEventArgsToCommand="True" 
       EventArgsConverter="{StaticResource ClickConverter}" 
       EventArgsConverterParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:SchedulePanel}}"/> 
     </i:EventTrigger> 
    </i:Interaction.Triggers> 
</ContentControl> 

Die Bindung selbst braucht einiges zu erklären ...

1) Befehl. Diese Bindung wird an dem Objekt vorgenommen, auf das der Benutzer klickt (z. B. Ihre Rechtecke), aber der Befehlshandler befindet sich normalerweise im DataContext-Objekt des übergeordneten ItemsControls. Also müssen wir eine RelativeBinding verwenden, um das Objekt zu finden und an seinen Handler zu binden.

2) Path=DataContext.MouseDownCommand. Sollte ziemlich einfach sein.

3) PassEventArgsToCommand="True". Dies teilt EventToCommand mit, dass unser Handler einen Parameter vom Typ MouseArgs erwartet. Wir könnten dies tun, wenn wir es wollten, aber dann würde unser Ansichtsmodell mit GUI-Objekten herumspielen, also müssen wir es in unseren eigenen Typ IMouseArgs konvertieren.

4) EventArgsConverter="{StaticResource ClickConverter}" Dieser Konverter wird diese Übersetzung für uns tun, ich werde den Code unten zeigen.

5) EventArgsConverterParameter="... Manchmal müssen wir andere Daten als zusätzlichen Parameter in unseren Konverter eingeben. Ich werde nicht auf alle spezifischen Fälle eingehen, in denen dies erforderlich ist, aber denken Sie daran. In diesem speziellen Fall musste ich den Punkt relativ zum ItemControl-Elternelement anstelle von ItemControl selbst finden.

Die Konverter Klasse selbst etwas haben kann man so will, eine einfache Implementierung in etwa so wäre:

public class MouseEventsConverter : IEventArgsConverter 
{ 
    public object Convert(object value, object parameter) 
    {   
     var args = value as MouseEventArgs; 
     var element = args.Source as FrameworkElement; 
     var parent = parameter as IInputElement; 
     var result = new ConverterArgs 
     { 
      Args = args, 
      Element = element, 
      Parent = parent, 
      Data = element.DataContext 
     }; 
     return result; 
    } 

Beachten Sie, dass meine spezielle Implementierung hier (ConverterArgs) tatsächlich das Rahmenelement speichert, kann man immer bekommen es mit einer Umwandlung, wenn das Ansichtsmodell dieses Objekt an beliebiger Stelle an die Ansicht zurückgibt.

Sinn machen? Es sieht komplex aus, aber es ist tatsächlich ziemlich einfach, wenn Sie es erst einmal programmiert haben.