Responding to the Mouse

The mouse has two buttons: left and right.

Each button can be depressed and can be released.

Clicking: depressing and then releasing a button without moving. Dragging: depressing, moving, then releasing a button.

Double-clicking: clicking twice within the double-click interval.

These actions generate hardware interrupts, which Windows processes by constructing messages:

WM_LBUTTONDOWN: the left button was depressed.

WM_RBUTTONDOWN: the right button was depressed.

WM_LBUTTONUP: the left button was released.

WM_RBUTTONUP: the right button was released.

WM_MOUSEMOVE: the mouse changed positions.

WM_LBUTTONDBLCLK: the left button was double-clicked

WM_RBUTTONDBLCLK: the right button was double-clicked.

There are more mouse-related messages--these are just the most common. But these are sufficient for most programs.

What window gets the mouse message?

The cursorlocation 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.

That is not always true. When the user is dragging something, usually the programmer has "captured the mouse" by calling SetCapture, which is a member of the CWnd class. This is needed so the same window that got the WM_LBUTTONDOWN message to start the dragging can also get the WM_LBUTTONUP that ends the dragging, even if the user moves the cursor off the window. We will discuss dragging in detail in the next lecture.

Handling messages (in general)

Mouse messages are usually handled in your view class. We have already learned how to use the Event Handler Wizard to add handlers for the WM_COMMAND messages sent by menu items. But to open the event handler wizard, we clicked the menu in the menu editor, and that method won’t work for handling mouse messages.

Here are the general instructions for adding a handler for any message (cut and pasted from the online help—see below for exactly how to find this part of help).

To define or remove a message handler using the Properties window

  1. In Class View, click the class.
  2. In the Properties window, click the Messages button. If your project has a handler for a message, then the name of the handler appears in the right column next to the message.
  3. If the message has no handler, then click the cell in the right column in the Properties window to display the suggested name of the handler as <add>HandlerName. (For example, the WM_TIMER message handler suggests <add>OnTimer).
  4. Click the suggested name to add stub code for the function.
  5. To edit a message handler, double-click the message in Class View and edit the code in the source window.

To remove a message handler, double-click the handler in the right column and select <delete>HandlerName. The function's code is commented out.

Here’s a screen shot to show where I found this. Please look it up yourself and get used to using the online help. It may seem technical but remember it’s meant for professional programmers and you intend to become a professional programmer. The help constitutes the other half of the material in this course—these lecture notes are only one half. You need to learn your way around the online help. Note, in this example, the entry for Filtered By at the top.

Handling mouse messages (in particular)

OK, let’s follow the instructions above. We right-click our view class in Class View to get the properties window. [Be sure you right-click in Class View, not in Solution Explorer. If you click the file name instead of the class name, you also get a properties window, but it’s not the right one.]

One of those buttons in the toolbar is the Messages button. It’s the hard-to-describe one to the right of the lightning bolt. Run the mouse slowly over the buttons without clicking to see which one it is. After clicking that button, and scrolling down a bit, you’ll see:

The left column gives a long list of the message identifiers of various Windows messages for which you could add handlers.

Find the row that says WM_LBUTTONDOWN. That message is sent when the left mouse button is pressed. Left-click in the second column, and select Add OnLButtonDown. You’ll see the definition of the message handler OnLButtonDown appear in the source code editor window. You can close the properties window now and go write the code (or write it later).

A mouse programming example

To make a simple MouseTest program, let's add a CRect member variable to the document class, call it m_theRect. (You do that by right-clicking the document class in Class View.) Initialize m_theRect in the document's constructor:

CMouseTestDoc::CMouseTestDoc()

{m_theRect.SetRect(10,10,100,100);

}

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 in the document class, m_color. The type of that variable should be COLORREF.

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.

We can start our handler like this:

void CMouseTestView::OnLButtonDown(UINT nFlags, CPoint point)

{

CMouseTestDoc* p = GetDocument();

if(PtInRect(&p->m_theRect,point))

{ // the mouse button was depressed in the rectangle

// so make something happen

}

}

Now what we want to have happen is that the color should change.

But we do NOT draw in OnLButtonDown

Instead, we change some data, and call Invalidate.

For example, let's say we want the color to switch from red to blue or vice-versa. To achieve this, we add two member variables m_blue and m_red of type COLORREF to the view class, and initialize them in the constructor:

CMouseTestView::CMouseTestView()

{

m_blue = RGB(0,0,255);

m_red = RGB(255,0,0);

}

Then, the hit-testing code is completed like this:

void CMouseTestView::OnLButtonDown(UINT nFlags, CPoint point)

{

CMouseTestDoc* p = GetDocument();

if(PtInRect(&p->m_theRect,point))

{ if(p->m_color == m_red)

p->m_color = m_blue;

else

p->m_color = m_red;

}

// Invalidate(); causes flicker

Invalidate(FALSE); // no flicker, see last lecture

}

The call to Invalidate() results in a WM_PAINT message being posted to the view window. Processing this message results in a call to OnDraw.

AppWizard has written the first two lines; we only have to add one line to complete OnDraw:

void CMouseTestView::OnDraw(CDC* pDC)

{

CMouseTestDoc* pDoc = GetDocument();

ASSERT_VALID(pDoc);

pDC->FillSolidRect(pDoc->m_theRect,pDoc->m_color);

}

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, your OnLButtonDown will be called before your OnLButtonDblClk. So, 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, the messages generated are as follows:

WM_LBUTTONDOWN

WM_LBUTTONUP

WM_LBUTTONDBLCLK

You do not get a second WM_LBUTTONDOWN and WM_LBUTTONUP.

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.

In our test program (and in your homework!) you must add a handler for double clicks, even though you don't want anything special to happen on double clicks. You just want the second click treated like any other click, and not ignored. But, unless you handle the double click message, it WILL be ignored.

void CMouseTestView::OnLButtonDblClk(UINT nFlags, CPoint point)

{

OnLButtonDown(nFlags,point);

}

Capturing the Mouse

In order to allow the user to drag something, you need to keep track of whether the mouse is "down" or "up". It is "down" between WM_LBUTTONDOWN and ths subsequent WM_LBUTTONUP. You keep track of that by adding a member variable m_MouseDown to your view class, setting it to TRUE in OnLButtonDown, and back to FALSE in OnLButtonUp.

But what if the user moves the mouse off of your window while it is down? Then the WM_LBUTTONUP message would not go to your window and you would not find out that the mouse button had been released. Your variable m_MouseDown would get out of sync with reality.

To address this problem we have the SetCapture member function in the CWnd class. Here's the manual entry:

CWnd*Set Capture()

Return Value

A pointer to the window object that previously received all mouse input. It is NULL if there is no such window. The returned pointer may be temporary and should not be stored for later use.

Remarks

Causes all subsequent mouse input to be sent to the current CWnd object regardless of the position of the cursor.

When CWnd no longer requires all mouse input, the application should call the ReleaseCapture function so that other windows can receive mouse input.

To use SetCapture, you call it in OnLButtonDown and then you call ReleaseCapture() in OnLButtonUp.

Dragging

To let the user drag something, you use a variable m_MouseDown as described above. Then, in OnMouseMove, you check the value of m_MouseDown. If it is true, the user is dragging something, so you do whatever updating you must do. If not, you just return without doing anything.

According to the principles set out before, if we are trying to drag something, for example, one corner of a rectangle, we should simply change the coordinates and call Invalidate(). However, the resulting WM_PAINT messages will not be processed as long as there are other WM_MOUSEMOVE messages in the queue, so dragging will not work correctly. We must draw on WM_MOUSEMOVE. This is the one exception to the rule about drawing only in OnDraw.

Using nFlags

It is possible to use the nFlags variable passed to OnLButtonDown to determine whether the mouse is down or up. Nevertheless, the

member-variable method with m_MouseDown is recommended. In more sophisticated applications you may need to distinguish mouse clicks with the control key held down, etc.:

nFlags

Indicates whether various virtual keys are down. This parameter can be any combination of the following values:

  • MK_CONTROL Set if the CTRL key is down.
  • MK_LBUTTON Set if the left mouse button is down.
  • MK_MBUTTON Set if the middle mouse button is down.
  • MK_RBUTTON Set if the right mouse button is down.
  • MK_SHIFT Set if the SHIFT key is down.

You use these flags as discussed in the first lecture of this course, by

using a bitwise and operator to check whether the corresponding bit is set:

if(nFlags & MK_CONTROL)

{ // then the control key was depressed while the mouse was clicked

}

Example

To allow the user to select a rectangle: Have a member variable m_Rect of type CRect for the rectangle, and another variable m_OldRect.

In OnLButtonDown set the upper left corner and initialize the lower right corner to be the same as the upper left, and m_OldRect=m_Rect. Also set m_MouseDown to TRUE.

In OnMouseMove, if m_MouseDown == FALSE do nothing at all. Otherwise, if m_OldRect is not empty (has nonzero width or height) then erase it (by drawing it in the background color for example).

Then set m_OldRect = m_Rect, and update m_Rect using the current mouse position for the lower right corner. (which might not be geometrically lower-right, but no matter).

In OnLButtonUp, do the same erasing and drawing as in OnMouseMove, and set m_MouseDown to FALSE.

To draw a rectangle without drawing the interior, select a null brush before calling Rectangle. To get a null brush use

Cbrush b;

b.CreateStockObject(NULL_BRUSH);