Silverlight 2 supports
these five mouse events, three of which you have already
seen:
-
MouseEnter
-
The mouse pointer entering the display area of an object
- MouseLeave
-
The mouse pointer leaving the display area of an object
- MouseMove
-
The mouse pointer moving
- MouseLeftButtonDown
-
The left mouse button being clicked down
- MouseLeftButtonUp
-
The left mouse button being clicked and released
The events themselves are self-explanatory, but we should probably
discuss the difference between MouseLeftButtonDown and
MouseLeftButtonUp. When a user clicks on an element, first
MouseLeftButtonDown occurs, and then
MouseLeftButtonUp. So, a mouse click is actually complete
only when MouseLeftButtonUp has been fired. In the real
world, the distinction makes sense in only one special case: the user
hovers the mouse over an element, clicks the button, holds the button, and
then moves the mouse away again. When you use MouseLeftButtonUp, it isn't fired over the
target object, which is desirable in some scenarios (think buttons) and
undesirable in other scenarios (think drag and drop).
NOTE
The mouse event handling mechanism of the vector graphics format
SVG, for instance, supports three mouse events: button pressed, button
released, and a completed mouse click.
1. Mouse Position
When you capture a mouse event, you want to know where the mouse
pointer currently is. The "where" question has previously been answered
with "on this object." More specifically is the question "At which
position?" This is where the second argument passed to event handler
functions, eventArgs, comes into play. It provides access
to this very information, by supporting the getPosition() method.
The getPosition() method supports an optional
argument, which is any XML element. If this is set, getPosition() retrieves the
relative position of the mouse to the given
element. Otherwise, you get the absolute coordinates (i.e.,
if you do not provide an argument or if you provide
null).
The return value of a getPosition() call is an object
with the two properties, x and y, which of
course contain the horizontal and vertical positions of the mouse
pointers. As always with the Web, the origin is in the top-left
corner.
Example 1 contains the XAML markup to track
mouse movements. Note how the property is used to
display the location of the mouse pointer. Also, note that the main
canvas's Loaded event again executes a method called
canvasLoaded().
Example 1. Determining the mouse position, the XAML file (Page.xaml,
project MousePosition)
<UserControl x:Class="MousePosition.Page" xmlns="http://schemas.microsoft.com/client/2007" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="400" Height="300"> <Grid x:Name="LayoutRoot" Background="White"> <Canvas Loaded="canvasLoaded"> <Rectangle Width="200" Height="75" Stroke="Orange" StrokeThickness="15" /> <TextBlock FontFamily="Arial" FontSize="28" Canvas.Left="25" Canvas.Top="25" Foreground="Black" Text="X: ?? Y: ??" x:Name="MousePosition" /> </Canvas> </Grid> </UserControl>
|
The task of the C# code is to determine and output the current mouse pointer
position whenever the mouse is moving. The associated event name is
MouseMove, and an event listener is ideal for this:
private void canvasLoaded(object sender, RoutedEventArgs e)
{
Canvas c = sender as Canvas;
c.MouseMove += new MouseEventHandler(mouseMove);
}
All that's left to do is to write the event listener, so we will
use an anonymous function here. The code determines the current mouse
position using GetPosition() and writes it into the text
box. Refer to Example 2 for the complete C# code, and
to Figure 1 to see how this
sample looks in the browser.
Example 2. Determining the mouse position, the XAML C# file (Page.xaml.js, project
MousePosition)
private void mouseMove(object sender, MouseEventArgs e) { double x = e.GetPosition(null).X; double y = e.GetPosition(null).Y; MousePosition.Text = String.Format("X: {0} Y: {1}", x, y); }
|
As mentioned before, you can also remove event listeners. To do
this, you "remove" from the event listener the object to which the event
listener has been attached. To demonstrate this mechanism, we implement
the hover effect again, but this time it can be enabled and disabled by
clicking on the text. We start with the previous XAML markup from Example 1. The C# code, however, changes quite a bit. At
first, we define two global properties. One will be used to save the
attached event handler, and the other one is a Boolean value that tells
the script whether we currently want to trace the mouse:
private Boolean traceMouse = false;
private MouseEventHandler handler = null;
Assigning the event handler to the canvas's Loaded
event changes a bit, too. We assign a new event handler method called
toggle() to it:
private void canvasLoaded(object sender, RoutedEventArgs e)
{
Canvas c = sender as Canvas;
c.MouseLeftButtonDown += new MouseButtonEventHandler(toggle);
}
The toggle() method first needs to check whether the
mouse pointer coordinates are traced. If not, tracing must be enabled
(since we want to toggle the behavior). We use the same code as before:
whenever the mouse pointer moves, the new coordinates are displayed.
Notice how the new event handler is saved in the handler property before it is used (which
facilitates removing this event handler later on):
private void toggle(object sender, MouseEventArgs e)
{
if (!traceMouse)
{
handler = new MouseEventHandler(mouseMove);
(sender as Canvas).MouseMove += handler;
}
If the mouse has been traced before (traceMouse
equals true), it must be deactivated; also, the event
listener must be removed. A call to removeEventListener()
takes care of that; remember that you have to use the
addEventListener() return values as the second
argument!
else
{
(sender as Canvas).MouseMove -= handler;
}
Don't forget to toggle the traceMode variable from
true to false or from false to
true:
traceMouse = !traceMouse;
}
Example 3 contains the complete code of the XAML
C# file. If you run this example in the browser, you will need to click
on the text to start seeing the mouse pointer coordinates. Clicking on
the text again stops this.
Example 3. Adding and removing event listeners, the XAML JavaScript code
(Page.xaml.js, project MousePositionToggle)
Code View: using System; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes;
namespace MousePositionToggle { public partial class Page : UserControl { private Boolean traceMouse = false; private MouseEventHandler handler = null;
public Page() { // Required to initialize variables InitializeComponent(); }
private void canvasLoaded(object sender, RoutedEventArgs e) { Canvas c = sender as Canvas; c.MouseLeftButtonDown += new MouseButtonEventHandler(toggle); }
private void toggle(object sender, MouseEventArgs e) { if (!traceMouse) { handler = new MouseEventHandler(mouseMove); (sender as Canvas).MouseMove += handler; } else { (sender as Canvas).MouseMove -= handler; } traceMouse = !traceMouse; }
private void mouseMove(object sender, MouseEventArgs e) { double x = e.GetPosition(null).X; double y = e.GetPosition(null).Y; MousePosition.Text = String.Format("X: {0} Y: {1}", x, y); } } }
|
2. Drag and Drop
One of the most difficult JavaScript effects to implement is
drag and drop. Not only can it be hard to control individual elements on
the page, but browser incompatibilities ultimately break the developer's
neck. Silverlight does not come with built-in drag-and-drop support, but
it is possible to implement this with rather little effort. If you plan
it appropriately, the code will come together quickly.
Drag and drop always consists of three phases, which can be
directly mapped on Silverlight
mouse events:
-
MouseLeftButtonDown
-
The user clicks on a draggable element and the application
enters drag mode.
- MouseMove
-
The user moves the mouse, while the mouse button remains clicked. The selected
object changes position according to the current position of the
mouse pointer.
- MouseLeftButtonUp
-
The user releases the mouse button; the application leaves drag
mode.
Actually, this is about 50% of the solution. The other 50% comes
from a different challenge. Let's assume that the draggable element is a
10 x 10 pixel square. The user
clicks somewhere on the square and drags it. Let's further assume that
the user releases the square at some position—for instance, at (50,40)
so that the x coordinate is 50 pixels and the
y coordinate is 40 pixels. Where should the
JavaScript code now put the square? At (50,40)? This would place the
top-left corner of the square, so the position would be correct only if
the user initially dragged the square by clicking exactly on the
top-left corner. This is rarely the case, of course.
So, working with the absolute position is not a good solution.
Instead, we will work with deltas: how far did the user move the mouse?
In the first phase of drag and drop, the C# code records the current
position of the mouse pointer. Whenever the mouse is moved, the new
position of the mouse pointer is retrieved. Based on these two values,
C# can calculate by how many pixels the mouse has been moved—for
example, 15 pixels to the right and 20 pixels to the bottom. These delta
values can then be applied to the actual object that will be moved: it
also must be moved 15 pixels to the right and 20 pixels to the bottom.
(In reality, these pixel values are usually much smaller, since the
MouseMove event is fired so often.)
This algorithm is the missing half of drag and drop. Writing the
code is no big challenge anymore. We start with the XAML code in Example 4. We once again have the surrounding rectangle and
a black circle that will serve as the draggable object. Notice how the
circle uses attributes to handle the three relevant mouse events.
Example 4. Drag and drop, the XAML file (Page.xaml, project
DragDrop)
<UserControl x:Class="DragDrop.Page" xmlns="http://schemas.microsoft.com/client/2007" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="400" Height="300"> <Grid x:Name="LayoutRoot" Background="White"> <Canvas> <Rectangle Width="300" Height="150" Stroke="Orange" StrokeThickness="15" /> <Ellipse Width="50" Height="50" Fill="Black" Canvas.Left="20" Canvas.Top="20" MouseLeftButtonDown="mouseInit" MouseLeftButtonUp="mouseRelease" MouseMove="mouseMove" /> </Canvas> </Grid> </UserControl>
|
We can now start to implement the actual drag and drop. Before we
do that, one word of caution. If the draggable object has processed a
mouse event, it hands it over to its parent element. And even worse,
other elements farther down in the object chain could process and act
upon this event as well (this is also called "event tunneling").
Therefore, other elements could work with those events too, which is
usually undesirable in this scenario. So, Silverlight provides a
CaptureMouse() method. If an object calls this method, all mouse events are
directly routed to the object; other objects do not receive them any
longer. When the left mouse button is pressed (the MouseLeftButtonDown event), the initial
position of the draggable object is retained and the mouse capturing
mode is activated. The (global) moving property remembers
whether the application is in drag-and-drop mode (true) or
not (false):
function mouseInit(sender, eventArgs) {
sender.captureMouse();
lastX = eventArgs.getPosition(null).x;
lastY = eventArgs.getPosition(null).y;
moving = true;
}
If the mouse is moving, the position of the draggable object needs
to be updated properly, according to the algorithm we designed at the
beginning of this section. Remember, the current mouse position is
determined, and then the script code calculates the delta between the
current and the last known positions. The position of the draggable
object is updated accordingly. Finally, the variables holding the last
known coordinates are updated.
If you are updating the circle's position, you have to take care
of one special issue: the property names you need to set are represented
by the Canvas.Left and Canvas.Top attributes.
However, these properties are not directly accessible for C#, since they
are so-called dependency properties (they depend on their parent
elements). However, there is a workaround. The static Canvas.GetLeft() and Canvas.GetTop() methods calculate the
x and y positions of any
element. In an analogous fashion, Canvas.SetLeft() and Canvas.SetTop() set these values. Now all you
have to remember is that the first argument of the event handler
function is the object firing the event—in this case, the element we
want to position. Then the rest of the code is easy:
private void mouseMove(object sender, MouseEventArgs e)
{
if (moving) {
Ellipse el = sender as Ellipse;
var x = e.GetPosition(null).X;
var y = e.GetPosition(null).Y;
Canvas.SetLeft(el, Canvas.GetLeft(el) + x - lastX);
Canvas.SetTop(el, Canvas.GetTop(el) + y - lastY);
lastX = x;
lastY = y;
}
}
NOTE
You now see why we needed the moving variable.
Mouse moving events happen all the time, but you want the circle to
move only if the user is dragging it!
The final step happens when the user releases the mouse button
(the LeftMouseButtonUp event). You can
reset moving to false and unlock access to mouse events for
other elements on the page by calling the ReleaseMouseCapture() method. We will implement one additional feature here. As you
have seen, there is an orange rectangle in the XAML file. This serves as
the barrier for the draggable element: the element must not touch or
even leave this border.
We need to define a couple of class properties that provide us
with the minimum and maximum coordinates where the circle is
allowed:
private double minX = 15;
private double maxX = 235;
private double minY = 15;
private double maxY = 85;
We will also save the position of the circle when the user starts
a drag-and-drop operation (this
code obviously belongs in the mouseInit() method):
startX = Canvas.GetLeft(el);
startY = Canvas.GetTop(el);
Finally, when the user releases the mouse button, the current
position is determined and checked against the valid coordinates. If the
position is out of bounds, the draggable object is placed at the
position it had at the beginning of the drag-and-drop operation:
var x = Canvas.GetLeft(el);
var y = Canvas.GetTop(el);
if (x < minX || x > maxX || y < minY || y > maxY) {
Canvas.SetLeft(el, startX);
Canvas.SetTop(el, startY);
}
Example 5 sums up the complete C# code. Figure 2 shows the example in action: if the user releases
the mouse button now, the circle will jump back to its original
position.
Example 5. Drag and drop, the XAML C# file (Page.xaml.cs, project
DragDrop)
Code View: using System; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes;
namespace DragDrop { public partial class Page : UserControl {
double startX, startY, lastX, lastY; private double minX = 15; private double maxX = 235; private double minY = 15; private double maxY = 85;
private Boolean moving = false;
public Page() { // Required to initialize variables InitializeComponent(); }
private void mouseInit(object sender, MouseButtonEventArgs e) { Ellipse el = sender as Ellipse; el.CaptureMouse(); startX = Canvas.GetLeft(el); startY = Canvas.GetTop(el); lastX = e.GetPosition(null).X; lastY = e.GetPosition(null).Y; moving = true; }
private void mouseRelease(object sender, MouseButtonEventArgs e) { Ellipse el = sender as Ellipse; el.ReleaseMouseCapture(); moving = false; var x = Canvas.GetLeft(el); var y = Canvas.GetTop(el); if (x < minX || x > maxX || y < minY || y > maxY) { Canvas.SetLeft(el, startX); Canvas.SetTop(el, startY); } }
private void mouseMove(object sender, MouseEventArgs e) { if (moving) { Ellipse el = sender as Ellipse; var x = e.GetPosition(null).X; var y = e.GetPosition(null).Y; Canvas.SetLeft(el, Canvas.GetLeft(el) + x - lastX); Canvas.SetTop(el, Canvas.GetTop(el) + y - lastY); lastX = x; lastY = y; } } } }
|