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!
Bevor Sie zu weit in dieses Kaninchenloch kommen. Haben Sie 'MouseDragElementBehavior' betrachtet und das' UIElement.Drop' Event benutzt? –
Gefunden diese [Link] (http://www.codeproject.com/Tips/105637/How-to-make-any-UI-element-drag-able-using-Behavio) von Chris Punkt. kann helfen. –
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