VCPP 6 - TUTORIAL - LESSON #4 - DHTML (c) Artur Marques, 15 August 1999

*You are supposed to have read Lesson #1 ; Lesson #2 ; Lesson #3

Understanding mouse and keyboard events - A small drawing program...


#1 - Wizard guidelines

#2 - Design guidelines, part 1

#3 - Design guidelines, part 2

#4 - Some Windows' Messages

#5 - Drawing on the dialog box

#6 - Try it!

#7 - Dealing with the mouse wheel

#8 - Drawing a continuous line, part 1

#9 - Drawing a continuous line, part 2

#10 - Fixing problems, part 1

#11 - The keyboard

#12 - Making the cursor changes hold

#13 - The final look

Downloads [source + win32 exe]


Do as follows:

#1 - Wizard guidelines

Create a standard MFC .EXE Dialog Based application, exactly as you did in lessons #2 and #3.


#2 - Design guidelines, part 1

Remove the static text and the two push buttons controls from the dialog box, in order to get an empty area. There is another reason for removing such controls: when they are present, the one highlighted has the "input focus" and is the only one receiving messages...


#3 - Design guidelines, part 2

Change the caption of the dialog box to something appropriate, such as "004 - Mouse and keyboard events", since this lesson [#4] is about understanding what kind of events can be perceived from such devices. Notice that current mouses / trackballs are capable of sending more events than previous hardware: not only they have higher or variable sample rates, but many now feature a wheel, that the software normally reads for scrolling purposes. Also notice that a mouse is not only about left, middle and right keys - games and drawing programs often need to know where the cursor exactly is or if you did a single or a double click... and how fast did you do it? This lesson is about a very simple drawing program that will try to show you how to deal with such situations.


#4 - Some Windows' Messages

When you move the mouse, it sends the message WM_MOUSEMOVE. When you use a mouse's wheel, it sends the message WM_MOUSEWHEEL. We will start working with these two messages. Add functions to such messages, using the Class Wizard [CTRL^W shortcut], and then edit their code, where you'll do as suggested in the next steps. If you don't know about the Class Wizard and how to use it, review the previous lessons.


#5 - Drawing on the dialog box

Type the following code to the function that will handle a WM_MOUSEMOVE message...

- I use comments to make you aware of some interesting issues.

- Comments are on this color.

- LMB means LeftMouseButton.

- UINT is just an int; CPoint is just a record of a x and a y.

- Check the new usage of the MessageBox function.

- And finally watch how to check if a flag is set: if (value_where_to_check_the_flag & FLAG_NAME) == FLAG_NAME);

- This code assumes you know about the use of BINARY OPERATORS in C / C++. A single & means a binary and; a single | means a binary or. When you have &&, you no longer have a binary and, but a logical and; when you have ||, you no longer have a binary or, but a logical or. Learn to make the distinction. Binary operators are useful for reading and setting bits; logical operators are for building and evaluating boolean expressions. Logical operators are easier to understand to non-programmers, as they are used in everyone's daily life. Binary operators are less common but also easy to understand - think of them as on / off switches; when they are set to 1, they are on; when they are set to 0, they are off. If this part is reading strange to you, it is because you were assumed to understand the basics of the binary numerical system.

- Remember that HTML does NOT handle TABs, so the following code is not indented as it should and as it is on the available sources.

void CMy004_mouse_kb_eventsDlg::OnMouseMove (UINT nFlags, CPoint point) {

int device_capabilities=0; // TODO: Add your message handler code here and/or call default

// nFlags can be any combination of the following:

// MK_CONTROL - CTRL key is down ; MK_LBUTTON - LMB is down ; MK_RBUTTON - RMB is down ; MK_SHIFT - SHIFT key is down

// NOTE: when you can the base class implementation of the method, it will use the original arguments and not what you supply - try to understand this in practice...

if ((nFlags & MK_LBUTTON)==MK_LBUTTON){

//get this Device Context - the device context of the dialog window

CClientDC dc(this);

device_capabilities=dc.GetDeviceCaps(RASTERCAPS); //you could also request many other informations...

//check if device can be used with SetPixel

// RC_BITBLT is a flag that says that the device is "capable of transferring bitmaps"

if ((device_capabilities & RC_BITBLT)==RC_BITBLT){

//draw on the device!

dc.SetPixel(point.x, point.y, RGB(0,0,0)); //draw on current cursor position, using BLACK color

} else MessageBox ("Current device is not capable of transferring bitmaps.\nCannot use SetPixel with it.","ERROR message", MB_ICONERROR|MB_OK); }; //if LMB was down

CDialog::OnMouseMove(nFlags, point);

}; //method ends


#6 - Try it!

From the VCPP6 build menu, choose "build all" and run the application. Press the LMB on the dialog box, hold it down, move the mouse and notice how it keeps drawing black points on the dialog box's area. However, if you speed the mouse movement, your points will be drawn sparsely. Why? Your application's priority is set to "NORMAL" and, unless you have an ET PC, there will be clock cycles where your computer will ignore the messages.


#7 - Dealing with the mouse wheel

Now I will show how an example of handling the WM_MOUSEWHEEL message. For that I redesigned the application's main dialog box, in order to have two controls: a static text caption and a read only edit field. Take a look at this picture.

The purpose of these two new controls are to give you feedback of what happens internally, when you use the mouse's wheel [mw]. Everytime you use the mw, the message being sent to the application carries a so called zDelta value that tells how much you scrolled, in multiples of a WHEEL_DELTA constant (120). So one scroll up is 120, two scrools up are 240 and so on... Scrolling down gets you negative values.

In order to watch zDelta, you will have to edit the code for the OnMouseWheel function, which should already exist, as you were invited to do in step #4. Now it is time to add your own - not default - code. First use the Code Wizard to associate a variable to the edit control. I did like this picture shows.

Then code what follows:

BOOL CMy004_mouse_kb_eventsDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) {

char str_zdelta[5]; //to store the zdelta of mousewheel (zDelta has up to 4 chars, plus the ending\0)

itoa (zDelta, str_zdelta, 10); //convert decimal (10) int (zDelta) to string (str_zdelta)

m_str_mousewheel_zdelta=str_zdelta; UpdateData(FALSE); //we will write to the dialog box

return CDialog::OnMouseWheel(nFlags, zDelta, pt);

}; //method ends

NOT shown here, but also to do, is editing the OnInitDialog method, so that the edit control shows "NOT SET" while the mw is not used. Figure out how to do that yourself, as teached in the previous lessons, or jump to source code, edit it, and check how it was done. A very simple indeed.


#8 - Drawing a continuous line, part 1

The next step on our small program is solving the dot drawing behavior. We want to draw solid line, not dot ones. We will do that by drawing lines between each of the points. This means that we will need to store the previous point marked on the canvas / device context, and then draw a line from it to the current one.

So that you can keep track of the previous point, of course you will need two new class member variables. You have two ways of adding them: or you directly edit the source of the file where your dialog box's class is declared (004_mouse_kb_eventsDlg.h on my case), or you use the workspace pane / choose the class view and then secondary click on the dialog box's class name, and choose "add member variable". If you do it this last way, you will end up with something like this picture shows. If you opt to edit the source code, type what follows, previous to the end of the class definition:

private:

int m_i_prev_y;

int m_i_prev_x;

}; //class def ends

Again, if you do it the easier way, using the workspace's class view interface, don't forget to add a second member variable. We will need the new variables to be private ints, with whatever name you want. I chose m_i_prev_x and m_i_prev_y. They are private because they are supposed to be accessible ONLY to the methods of the class where they are declared and to NO_ONE_ELSE, including children / derived classes.


#9 - Drawing a continuous line, part 2

After adding the two new variables, go ahead and make your OnMouseMove code look something like this:

void CMy004_mouse_kb_eventsDlg::OnMouseMove (UINT nFlags, CPoint point) {

int device_capabilities=0; // TODO: Add your message handler code here and/or call default

// nFlags can be any combination of the following:

// MK_CONTROL - CTRL key is down ; MK_LBUTTON - LMB is down ; MK_RBUTTON - RMB is down ; MK_SHIFT - SHIFT key is down

// NOTE: when you can the base class implementation of the method, it will use the original arguments and not what you supply - try to understand this in practice...

if ((nFlags & MK_LBUTTON)==MK_LBUTTON){

//get this Device Context - the device context of the dialog window

CClientDC dc(this);

device_capabilities=dc.GetDeviceCaps(RASTERCAPS); //you could also request many other informations...

//check if device can be used with SetPixel

// RC_BITBLT is a flag that says that the device is "capable of transferring bitmaps"

if ((device_capabilities & RC_BITBLT)==RC_BITBLT){

//draw on the device!

dc.MoveTo (m_i_prev_x, m_i_prev_y);

dc.LineTo (point.x, point.y);

m_i_prev_x=point.x; m_i_prev_y=point.y; //store current x and y for next reference

} else MessageBox ("Current device is not capable of transferring bitmaps.\nCannot use SetPixel with it.","ERROR message", MB_ICONERROR|MB_OK); }; //if LMB was down

CDialog::OnMouseMove(nFlags, point);

}; //method ends

The new lines you will have to add, are on this color. Notice that the SetPixel instruction is no longer needed and is out of the code.


#10 - Fixing problems

Rebuild the application, try it, and notice that it draws much better. However, there still are some problems, the most important one being that every time you release the LMB and press it another place to restart your drawing, the point where you clicked links to where you left... Another serious problem is that the 1st click on the dialog box causes a line from a mistery point to where you clicked... We will solve both things.

In fact, both things happen because of the same problem: lack of / wrong initialization. We will initialize the previous member variables (m_i_prev_x and m_i_prev_y) when the LMB is pressed and not randomly as it happens now. The random init that occurs when the application starts causes the first line to be to a mistery point; and the fact that m_i_prev_x and m_i_prev_y rest to point.x and point.y from there on, causes that when you lift the LMB to draw somewhere else, there will be a line connecting to where you left...

We want m_i_prev_x and m_i_prev_y to be exactly where you click, so that the current mouse position, always updated on every WM_MOUSEMOVE, is such that the drawing will be ok.

In other words, we need to code the function triggered by WM_LBUTTONDOWN. Use Class Wizard to do that and accept the default name for that function. Take a look at this picture.

Code the OnLButtonDown functin as follows:

void CMy004_mouse_kb_eventsDlg::OnLButtonDown(UINT nFlags, CPoint point) {

m_i_prev_x=point.x;

m_i_prev_y=point.y;

CDialog::OnLButtonDown(nFlags, point);

}; //methods ends

Rebuild the application and you should end up with something that nearly perfects what was our purpose. Look at the software running.


#11 - The keyboard

OK, so we have seen some mouse related messages, but what about the keyboard?

In order to do some tricks with the keyboard, we will make the application change our cursor's looks, at the touch of a key. Say you want the cursor to look like a car, when you press "C"... or like a plane, when you press "P"... it is exactly that, that we will do.

The keyboards sends only two event messages: WM_KEYDOWN and WM_KEYUP. They are enough for us to act. Use Code Wizard, look for the WM_KEYDOWN message, add a function to it and edit its code as follows:

void CMy004_mouse_kb_eventsDlg::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {

//2nd arg is number of repetitions of the key (key down)

//3rd arg is flags for testing is ALT key was down or if the key is an extended key - it DOES NOT say if key is control or shift!

//convert the character code to a character

char char_pressed=char(nChar); //character that will be pressed

HCURSOR hcursor; //handle to the cursor that will be displayed

switch (char_pressed){

case 'c': case 'C': //show the car cursor

hcursor=AfxGetApp()->LoadCursor (IDC_CURSOR_car);

SetCursor (hcursor); break;

case 'p': case 'P': //show the plane cursor

hcursor=AfxGetApp()->LoadCursor (IDC_CURSOR_plane);

SetCursor (hcursor); break;

case 'x': case 'X': //exit

OnOK();

default: //show the regular arrow cursor

hcursor=AfxGetApp()->LoadStandardCursor (IDC_ARROW); //access predefined windows' cursors

SetCursor (hcursor); break;

};//switch

CDialog::OnKeyDown(nChar, nRepCnt, nFlags);

}; //method ends

Above we coded:

- the cursor to change to a car when C is pressed (and held), and to a plane when P is pressed (and held).

- the application to EXIT when X is pressed.

- the cursor to become the standard arrow, when any other key is pressed.

But this new code will do NOTHING if you don't do something more: you must edit your controls' properties and check their DISABLED check box. IF the controls are enabled, then they will get the input focus, meaning that the keyboard messages will be sent to them and the cursor changes will have no effect.

When you release "C" or "P", the cursor resumes its normal appearance.

You should notice that LoadCursor receives as its argument the resource identifier of the wanted cursor: IDC_CURSOR_car or IDC_CURSOR_plane. These are cursors that I designed on purpose for this project. In order to design your own, you must choose the workspace / resources view and insert a new cursor. Don't lose too much time on the design, since our only purpose is to test the response to a keyboard message

LoadStandardCursor is another method for loading Windows' predefined cursors.

If you don't want to design cursors, go ahead and download my source code and resources. Rebuild all files, run the application and watch its behavior.


#12 - Making the cursor changes hold

One way to make the cursor changes hold, is to have a boolean member variable that goes TRUE when a cursor key is pressed and that starts FALSE.

So add a private member boolean [BOOL] variable [m_b_cursor is my suggestion] for that matter. Do that as you did for the int variables in step #8. Check this picture.

- Assure that the dialog box's OnInitDialog method inits that variable to FALSE.

- ALTER the switch statement coded on step #11, so that after cases "C", "P" and "default", you set m_b_cursor to TRUE.

- Using Code Wizard, add a function to take care of the WM_SETCURSOR message. Code it as follows:

BOOL CMy004_mouse_kb_eventsDlg::OnSetCursor (CWnd* pWnd, UINT nHitTest, UINT message) {

if (m_b_cursor) return TRUE

else return CDialog::OnSetCursor(pWnd, nHitTest, message);

}; //method ends

This code makes the OnSetCursor method return TRUE if we want to hold the cursor, as controlled by our recent m_b_cursor BOOL variable. If m_b_cursor is FALSE, then the function calls its default implementation, as it will happen until a "cursor key" is pressed. The default implementantion resets the cursor.


#13 - The final look

So this is the look of our final version of the application developed for today's lesson.


Downloads

all the source files [ZIP - 9 K]

the debug release win32 executable [ZIP - 66 K]