Responding to the Mouse
The
mouse has two buttons: left and right.
Each
button can be depressed and can be released.
Here,
for reference are the definitions of three common terms for actions performed
by a user with the mouse:
Click: to
depress and then release a button without moving the mouse. [Technically, there
may be a pixel or two of motion allowed between the depress
and the release.]
Drag: to depress, move, and then release a mouse
button.
Double-click: to click twice within the double-click interval.
The
double-click interval can be changed by the user through the Windows Control
Panel. People with disabilities, for
example, may want it to be longer. It
can also be changed under program control but I have never heard of this being
done, and it doesn't seem like a good idea.
These
actions generate hardware interrupts, which Windows processes by constructing
messages, which in turn are wrapped by Application.Run into FCL events.
What window gets the mouse
message?
The
cursor location is a point (pixel) on the screen. The cursor may be invisible but it always has
a location. The cursor location is sometimes called the "hot spot" of
the cursor. The cursor itself is
indicated by an icon, whose appearance changes according to the function of the
mouse at that point in the program.
Normally,
the mouse messages will go to the window that contains the cursor location and
is currently visible; or in FCL terminology, mouse events should be processed
by the form whose window contains the cursor location. Technically, your form’s mouse event
handlers will normally get mouse events only if those events occur over the
form’s client area. Events that occur
over the border, caption bar, menu bar, toolbar, scroll bars, minimize box, maximize box, or
close box, will be handled by Windows
rather than by your program.
There
is one exception to this rule: when the
user is dragging something, usually the programmer has "captured the mouse" by calling SetCapture.
We will return to this issue later.
As long as we’re only talking about clicks (mouse-down and mouse-up, or
double-click) and not about
dragging, it should be true that mouse
events are processed by the form whose window contains the cursor location.
Mouse Events in .NET
Create
a new Visual C# Windows Application project, say MouseTest. Right-click the form in the form designer, select Properties, click the lightning bolt, and look for
the mouse events. There are five events
whose names begin with Mouse:
MouseDown
MouseUp
MouseEnter
MouseLeave
MouseHover
For
now, we will
concentrate on the most common mouse event, MouseDown. Add a handler for that event (by
double-clicking its name in the list of events).
A mouse programming example
To
make a simple MouseTest
program, let's
add a Rectangle member variable to the form class, call it
m_theRect. We will make something happen when the
user clicks in the rectangle, or more precisely when the left mouse button is
depressed while the cursor location is in the rectangle. For example, change the color of the
rectangle. We will keep the current
color value in another member variable, m_color.
private Rectangle m_theRect;
private Color m_theColor;
Initialize
these variables in the form class constructor:
m_theRect = new Rectangle(10,10,100,100);
m_theColor = Color.RoyalBlue;
As
we have done before, we add a handler for the Paint event, and use FillRectangle to paint m_theRect using the color m_theColor:
private void
Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs
e)
{ Brush b = new SolidBrush(m_theColor);
e.Graphics.FillRectangle(b,m_theRect);
}
Hit-testing
This
refers to checking whether the mouse button has been pressed in a certain
region or not. It is simplest if the
region is a rectangle. The Rectangle class has a method Contains, which can take either a Point or two int arguments. The following short code sample illustrates
simple hit-testing.
private void
Form1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{ if(m_theRect.Contains(e.X,e.Y))
{ // toggle the color
if(m_theColor == Color.RoyalBlue)
m_theColor
= Color.Red;
else
m_theColor
= Color.RoyalBlue;
Invalidate(m_theRect);
}
}
This
code sample also illustrates another important point: in responding to the mouse, we do not do any drawing. Instead, we only change some data, in this
case, m_theColor. We leave it up to Form1_Paint to do the drawing. The key to this is Invalidate. Comment out the
call to Invalidate and run the
program. You will see that the color
doesn’t change when you click. But of
course the variable m_theColor
has changed—you just don’t see the changed data presented on the screen! Resize the window to partly obscure the
rectangle and then reveal it again—then you’ll see the change on the newly
redrawn part of the window. Please
review the discussion of Invalidate
in the last lecture with this sample program running. You should re-read the discussion and re-run
the program several times if necessary, until you fully understand what is
going on. This point is extremely
fundamental for successful Windows programming and you must put in the effort
required to master the points in question.
Just to summarize, there are actually two related points: (1) Don’t draw anywhere but in your Paint handler; (2) elsewhere, just change data and then
cause a new Paint event to be
generated by calling Invalidate.
Handling Double Clicks
When
you get the first click, you don't know if it is a single click, or the first
click of a double click. Therefore, you
have to design your program accordingly--you can't do something on double-click
that will contradict what is done on a single click.
For
example, a single click sometimes highlights ("selects") a filename
and a double-click opens the file. No problem there; but it wouldn't work to
have a single click open the file and a double click merely select it.
When
the mouse is clicked twice within the double-click interval, you will, in .NET
programming, get two MouseDown events, as you can verify by running the
example program above. There is no
difference, in that program, between a double click and two single clicks. This contrasts with the behavior of a
similar program written in MFC or the Win32 API, where programmers had to explicitly
process the double-click message in order to get this behavior.
In
FCL programming, you have the opposite problem:
suppose you want to detect a double click and do something in that
case. For example, in our MouseTest
program, we might want to have a double-click turn the
rectangle green. That means we have to
detect the second click of a double-click. That is done using the Clicks field of the MouseEventArgs class:
private void
Form1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{ if(m_theRect.Contains(e.X,e.Y))
{ if(e.Clicks == 2)
// a
double-click
m_theColor =
Color.Green;
else if(m_theColor
== Color.RoyalBlue)
m_theColor =
Color.Red;
else
m_theColor =
Color.RoyalBlue;
Invalidate(m_theRect);
}
}
This program illustrates (by
giving a bad example) the point that what happens on a
double-click should be compatible with what happens on a single
click. Although double-click turns the
rectangle green, the first click already turns it red or blue, which is awkward.
Which button was clicked?
The program we wrote above
responds indiscriminately to a click of either the left or the right
button. In MFC or the Windows API, you had to write a
separate handler for the left or the right button. In FCL, there is only one MouseDown event,
and you use the Button member of the MouseEventArgs
class to determine which button was clicked.
The type of that member is MouseButtons; this type has exactly six members:
None, Left, Right, Middle, XButton1, XButton2.
Some mice have a middle
button; many mice nowadays have a “scroll wheel” which can also function as a
middle button; and
the Intellimouse Explorer has five buttons, so XButton1 and XButton2
can be used for that device.
To make our sample program
respond only to the left mouse button, we can put this code at the beginning
of our MouseDown
handler:
if(e.Button != MouseButtons.Left)
return;