Reputation: 48
I'm working on making a screenshot program in WPF. What I don't understand is how I should mouse events. All of my mouse events are handled in the code-behind, which works great, but I want to convert my project to MVVM-style. Here is all of my ScreenshotTab code, which is the usercontrol which hosts the screenshot (it's actually designed to be placed into a tabcontrol tabitem):
public partial class ScreenshotTab : UserControl
{
public ScreenshotTab()
{
InitializeComponent();
}
public ScreenshotTab(BitmapImage screenshot, Window window)
{
InitializeComponent();
imgControl.Source = screenshot;
imgControl.MaxWidth = screenshot.Width;
imgControl.MaxHeight = screenshot.Height;
canvas.Height = screenshot.Height;
canvas.MinHeight = screenshot.Height;
canvas.MaxHeight = screenshot.Height;
canvas.Width = screenshot.Width;
canvas.MinWidth = screenshot.Width;
canvas.MaxWidth = screenshot.Width;
mainWindow = window as MainWindow;
}
public bool isSaved
{
get;
set;
}
public BitmapImage Screenshot
{
get
{
if (imgControl.Source is BitmapImage)
{
return imgControl.Source as BitmapImage;
}
else
{
return null;
}
}
}
// Listed in the order they are needed in the logic flow inside the event handlers.
private MainWindow mainWindow;
private Ellipse circle;
private Polyline polyLine;
private Point startPoint;
private Point endPoint;
private Line line;
private Rectangle rectangle;
private Point topLeft;
// I probably don't need a bool.
private bool isDrawing;
private void imgControl_MouseDown(object sender, MouseButtonEventArgs e)
{
// MessageBox.Show("Mouse down event is working...");
// MessageBox.Show("mainwindow.toolselected = " + mainWindow.toolSelected.ToString());
// Sets the starting point of the line, which doesn't change
// as the user moves their mouse.
// Turns on a flag that represents that we are drawing when on.
isDrawing = true;
switch (mainWindow.toolSelected)
{
case DrawingTool.FreeDraw:
polyLine = new Polyline();
SetStrokeProperties(polyLine);
polyLine.Points.Add(e.GetPosition(canvas));
canvas.Children.Add(polyLine);
break;
case DrawingTool.Line:
line = new Line();
SetStrokeProperties(line);
line.X1 = e.GetPosition(canvas).X;
line.Y1 = e.GetPosition(canvas).Y;
line.X2 = e.GetPosition(canvas).X;
line.Y2 = e.GetPosition(canvas).Y;
canvas.Children.Add(line);
break;
case DrawingTool.Rectangle:
rectangle = new Rectangle();
SetStrokeProperties(rectangle);
startPoint = e.GetPosition(this);
endPoint = e.GetPosition(this);
Canvas.SetLeft(rectangle, startPoint.X);
Canvas.SetTop(rectangle, startPoint.X);
canvas.Children.Add(rectangle);
break;
case DrawingTool.Circle:
circle = new Ellipse();
SetStrokeProperties(circle);
startPoint = e.GetPosition(this);
endPoint = e.GetPosition(this);
Canvas.SetLeft(circle, startPoint.X);
Canvas.SetTop(circle, startPoint.X);
canvas.Children.Add(circle);
break;
case DrawingTool.Eraser:
break;
default:
break;
}
}
private void SetStrokeProperties(Shape shape)
{
shape.Stroke = new SolidColorBrush(mainWindow.color);
shape.StrokeThickness = 3;
}
// This is our eraser event.
// It simply removes the element from the object when the tool selected
// is the eraser tool and we have the mouse down.
private void OnMouseOver(object sender, MouseEventArgs e)
{
// MessageBox.Show("Yay, mouse over event called...");
if (isDrawing && (mainWindow.toolSelected == DrawingTool.Eraser))
{
canvas.Children.Remove(sender as UIElement);
}
}
private void imgControl_MouseMove(object sender, MouseEventArgs e)
{
if (isDrawing)
{
switch (mainWindow.toolSelected)
{
case DrawingTool.FreeDraw:
polyLine.Points.Add(e.GetPosition(canvas));
break;
case DrawingTool.Line:
line.X2 = e.GetPosition(canvas).X;
line.Y2 = e.GetPosition(canvas).Y;
break;
case DrawingTool.Rectangle:
endPoint = e.GetPosition(this);
rectangle.Width = Math.Abs(endPoint.X - startPoint.X);
rectangle.Height = Math.Abs(endPoint.Y - startPoint.Y);
// down and to the right.
if ((endPoint.X >= startPoint.X) && (endPoint.Y >= startPoint.Y))
{
// In this case, the start point is the top left point of the rectangle.
topLeft.X = startPoint.X;
topLeft.Y = startPoint.Y;
Canvas.SetLeft(rectangle, topLeft.X);
Canvas.SetTop(rectangle, topLeft.Y);
}
// up and to the right.
else if ((endPoint.X >= startPoint.X) && (endPoint.Y <= startPoint.Y))
{
topLeft.X = endPoint.X - rectangle.Width;
topLeft.Y = endPoint.Y;
Canvas.SetLeft(rectangle, topLeft.X);
Canvas.SetTop(rectangle, topLeft.Y);
}
// up and to the left...
else if ((endPoint.X <= startPoint.X) && (endPoint.Y <= startPoint.Y))
{
topLeft.X = endPoint.X;
topLeft.Y = endPoint.Y;
Canvas.SetLeft(rectangle, topLeft.X);
Canvas.SetTop(rectangle, topLeft.Y);
}
// down to the left...
else if ((endPoint.X <= startPoint.X) && (endPoint.Y >= startPoint.Y))
{
topLeft.X = startPoint.X - rectangle.Width;
topLeft.Y = endPoint.Y - rectangle.Height;
Canvas.SetLeft(rectangle, topLeft.X);
Canvas.SetTop(rectangle, topLeft.Y);
}
break;
case DrawingTool.Circle:
endPoint = e.GetPosition(this);
circle.Width = Math.Abs(endPoint.X - startPoint.X);
circle.Height = Math.Abs(endPoint.Y - startPoint.Y);
// down and to the right.
if ((endPoint.X >= startPoint.X) && (endPoint.Y >= startPoint.Y))
{
// In this case, the start point is the top left point of the rectangle.
topLeft.X = startPoint.X;
topLeft.Y = startPoint.Y;
Canvas.SetLeft(circle, topLeft.X);
Canvas.SetTop(circle, topLeft.Y);
}
// up and to the right.
else if ((endPoint.X >= startPoint.X) && (endPoint.Y <= startPoint.Y))
{
topLeft.X = endPoint.X - circle.Width;
topLeft.Y = endPoint.Y;
Canvas.SetLeft(circle, topLeft.X);
Canvas.SetTop(circle, topLeft.Y);
}
// up and to the left...
else if ((endPoint.X <= startPoint.X) && (endPoint.Y <= startPoint.Y))
{
topLeft.X = endPoint.X;
topLeft.Y = endPoint.Y;
Canvas.SetLeft(circle, topLeft.X);
Canvas.SetTop(circle, topLeft.Y);
}
// down to the left...
else if ((endPoint.X <= startPoint.X) && (endPoint.Y >= startPoint.Y))
{
topLeft.X = startPoint.X - circle.Width;
topLeft.Y = endPoint.Y - circle.Height;
Canvas.SetLeft(circle, topLeft.X);
Canvas.SetTop(circle, topLeft.Y);
}
break;
default:
break;
}
}
}
private void imgControl_MouseUp(object sender, MouseButtonEventArgs e)
{
isSaved = false;
switch (mainWindow.toolSelected)
{
case (DrawingTool.FreeDraw):
polyLine.MouseEnter += OnMouseOver;
break;
case (DrawingTool.Line):
line.MouseEnter += OnMouseOver;
break;
case (DrawingTool.Rectangle):
rectangle.MouseEnter += OnMouseOver;
break;
case (DrawingTool.Circle):
circle.MouseEnter += OnMouseOver;
break;
}
isDrawing = false;
// MessageBox.Show("Mouse up event fired...");
}
}
Obviously, that is quite a mess of code to handle, especially if I decide to add more shapes to draw in the future. So how would I go about setting up this up in MVVM if I want the user to be able to draw different kinds of shapes? Thanks for taking the time to look over this.
Upvotes: 0
Views: 292
Reputation: 16148
I handle this with an interface to a class that my view model can interact with:
public interface IMouseArgs
{
Point Pos { get; }
void Capture();
void Release();
bool Handled { get; set; }
... lots of other stuff
}
So basically whenever a mouse event or drag-n-drop operation occurs my view layer creates an object that implements this interface and passes it to the view model. If the view model decides to Capture or Release then mouse (e.g. a drag operation) then there are functions for it to request that. Similarly, it can set the Handled flag to indicate to the view that it is handling the mouse now (so that the view doesn't bubble the message up to parent controls in the view hierarchy.
As for the view, I use an interaction trigger to convert from the mouse events to the view model command handlers:
<SomeControl>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDown">
<cmd:EventToCommand Command="{Binding MouseDownCommand}"
PassEventArgsToCommand="True"
EventArgsConverter="{StaticResource ClickConverter}"
EventArgsConverterParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:SomeParentWindow}}"/>
</i:EventTrigger>
Again, this is a simplified example, but shows the general idea. You could just do only the cmd:EventToCommand
and PassEventArgsToCommand="True"
, but then your view model will receive the Windows data objects associated with the event (which isn't good SOC and means your view model is now forced to reference the Windows libraries). Specifying an EventArgsConverter, with optional EventArgsConverterParameter, means you can write a converter in the view to accept the event arg and convert it into something that implements IMouseArgs.
Sounds a bit complex, but it allows for very complex mouse interactions (drags, resizes, drag-n-drop etc) and is fully unit-testable, all while maintaining complete separation between your view model and view layers.
Upvotes: 1