28.2 Cursor tools


Looking at the Pop Framework, you'll notice that the user can activate different kinds of cursors . When you have a program where users can change the tool action of the cursor, it's important to change the appearance of the cursor to match the mode. That is, the appearance of the cursor should inform the user about what the effects of mouse actions are likely to be.

We'll let the tool type depend on the view, so that it's possible to have two views open with a different tool being used in each one. This means that the data about which cursor to use should live in the CPopView . And the CPopView will be responsible for resetting the cursor whenever the cursor moves over the CPopView's window.

How does the CPopView keep track of which cursor type and tool type it is to use? A general principle of good program design is

never to keep the same information in two different places.

The reason is that if a piece of data lives in two different places, then inevitably some change in your program will remember to change the data in one of its locations but not in the other. Now, since the CPopView is going to need to change the cursor's appearance, it might as well have an HCURSOR _hCursor variable that specifies the cursor's appearance the HCURSOR type is a Windows handle used for this purpose. But since we're going to use the HCURSOR to change the cursor appearance, we might as well use it as the data to tell us which tool type is being used as well. That is, we do not need, or want, to have an int _cursortype variable in the CPopView in addition to the HCURSOR _hcursor .

Changing the cursor

A first thing to realize is that the cursor is a global resource; i.e. the cursor is not the property of any one window. The user is free to move the cursor away from your program's window and onto another window. So many of the Windows functions having to do with the cursor are global functions, i.e. functions that are not members of any class.

The global function used for changing the cursor's appearance is ::SetCursor(HCURSOR hCursor) . It takes an HCURSOR handle to the new cursor resource as its argument. Remember a Windows handle is an indirect kind of pointer; it's a carry-over from the old Win32 programming that you don't see all that often in MFC. But, for whatever reason, the cursor functions are not wrapped up inside any MFC class. Remember also that, although strictly speaking we don't have to, we like to put the scope resolution operator :: in front of a global function with nothing to the left of the :: as a way of reminding ourselves that the function is not a member function of a class.

Now our CView is going to have an HCURSOR _hCursor variable, so this is probably what we want to feed into the :: SetCursor function. But where should we do this?

You might logically expect that we'd change the cursor only right after we change the _hcursor in the code that handles the menu or toolbar commands to change the active cursor type. Well, when do we change the cursor type in our program? Choosing the View Pin Cursor and View Hand Cursor menu selections calls the CPopView::OnViewDraggerCursor or the CPopView::OnViewPinCursor method. And inside these methods the value of the _hcursor does indeed change. So you might expect that in here would be the place to call ::SetCursor (_hCursor) . But you'd be wrong.

This is because you can't just change your cursor once and be done with it. If you were to change the cursor to a pin only inside the OnViewPinCursor function, then as soon as you moved the mouse, your cursor would go back to being the default Windows arrow cursor IDC_ARROW . Windows resets the cursor every time there is a mouse move . The reason for this is that the cursor appearance needs to change as it moves across various windows and window-features on your program. Remember that the cursor is a global kind of thing. For instance, the cursor needs to turn into a double headed arrow if you place it over the corner of a window frame. And if a window has a special default cursor then that should be the cursor to show.

As it happens, every time the mouse makes a move over a CWnd window, the window gets a call to the OnSetCursor method for that window, so that's the place to put the code for changing the cursor. We use View Class Wizard to open up the Class Wizard, and then tell it to add an OnSetCursor handler to our CPopView . And we edit the code to look something like the following partial listing of the Pop Framework code.

 BOOL CPopView::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)  {      /* This method get called whenever the cursor is over the client          area of the view. Don't call the baseclass handler, that is,          don't call: CView::OnSetCursor(pWnd, nHitTest, message).          DON'T CALL THIS!!!! In particular, don't call base          CView::OnSetCursor last because then you'll get the default          IDC_ARROW cursor back!*/  //(1) Set the correct cursor for this view.      ::SetCursor(_hCursor);  //(2) Use a special drag cursor if you are attached to the player.      if (pgame()->playerListenerClass() ==          RUNTIME CLASS (cListenerCursor))          ::SetCursor(((CPopApp*)::AfxGetApp())->hCursorDragger);  //(3) Save the cursor position with cGame::setCursorPos if you're the      active view  //if (isActiveView())      {      //First get the cursor position in a client area coordinates.          CPoint point;          ::GetCursorPos(&point);              //Gets screen coordinates          ScreenToClient(&point); //A CView conversion method      //Then convert point to world coordinates and save with      //setCursorPos          if (pviewpointcritter->plistener()->              IsKindOf(RUNTIME_CLASS(cListenerViewerRide)))              pgame()->setCursorPos(pixelToPlayerYonWallVector(point.x, point.y,                  0.5 * pviewpointcritter->toFarZ()));                  /* If we are riding the critter, we want to pick a                  point on the "yon" wall, that is, the viewer's far                  clip plane. Given that we're on the critter, that                  distance from us will the viewpointcritter's toFarZ(),                  inlined as {return fabs(zfar - position.z());} */          else              pgame()->setCursorPos(pixelToPlayerPlaneVector(point.x, point.y));                  /* Otherwise we pick a point in the plane of the                      player's body, that is, his tangent and normal                      plane. */      }      return TRUE;  } 

Notice that as well as setting the cursor here, we also tell the view's associated cGame object to update its current notion of where the cursor is. We do this update inside the OnSetCursor method because the method gets called essentially at every update.

Coming back to setting the cursor, how do we manage to put a valid HCURSOR value into the CView _hCursor variable?

Making a cursor in the Resource Editor

If we want to have special-looking cursors in our program, it's up to us to design their appearance. And then these cursor designs become part of our program's resources and get bound into the program executable where your application can find them at runtime.

It's pretty easy to add new resources to a program in Visual Studio. You just use the menu selection Project Add resource... [ Insert Resource New... in Version 6.0].

Either method pops up a dialog where you specify what kind of resource you want to insert. If you select Cursor you get a little Image Editing window. There are a few special things to know about cursor images.

  • A cursor only uses the colors black and white.

  • The cursor editor also uses pseudocolors that we might call 'transparent' and 'invert.' In the Visual Studio Resource Editor, these colors are represented by a grayish blue and by pink, respectively. Usually you want most of your cursor image to be 'transparent,' that is, you want the cursor to be a small shape that does not appear to have a white rectangular background.

  • A cursor has an associated hot spot whose coordinates you can edit in the Resource Workshop. The hot spot is the location in the cursor image that is used to determine the pixel coordinates that are returned by mouse messages like MouseMove or OnLButtonDown . There are two considerations in picking the hot spot. (a) If possible, the hot spot should be near the upper edge of the cursor so that the cursor ' knows ' as soon as you move it into the toolbar/ menubar region at the top of your window. And (b) the hot spot's location should make sense relative to the visual appearance of the cursor, that is, it should be at the tip of a pencil, at the nozzle of a spraycan, etc. Sometimes you need to redesign your cursor image to satisfy both conditions. [In Version 6.0 you may need to close the Resource Editor's little Color and Tool windows before you can see the button that lets you pick the hot spot.]

As usual, when you create a resource you have the option of changing the name of the resource's ID mnemonic. By default, the Developer Studio will give a cursor resource a one-size-fits-all name like IDC_CURSOR1 . You should change the name of the cursor resource to something that's easier for you to remember. To do this you need to get to the resource's Properties dialog. The easiest way to open the dialog is to press Alt + Enter . Or you can right-click on the IDC_CURSOR1 name of the cursor over on the Resource View and select Properties to see the cursor Properties box. You can either give your cursor an numerical ID mnemonic like IDC_PIN or, if you prefer, you can give it a string name like 'Pin,' being sure to put the string inside quotation marks. Remember to click on the little question mark at the corner of the dialog box for additional information.

The Resource Editor will save off a file with your cursor information. This file will have a generic name like Cursor1.cur and it will go into the .\res subdirectory of your build directory. It's fine for this file's name to be generic, because you normally don't need to directly access it, and if you did want to directly access it say to quickly open it to edit it you can tell what's in it by the appearance of the icon next to it in the Windows Explorer window. But if you really want, you can Rename the file in Windows Explorer and change its name to match inside the File Name field of the Visual Studio cursor Properties box. The first time you try and rebuild after doing a resource item file name change like this, you'll get an error message, but if you try a second rebuild, the Visual Studio will then accept your change.

If you aren't good at drawing, you can look for a cursor to copy. For the hand cursor, for instance, you might go through a process like this.

  • Look on the distribution Visual Studio disk and find a lot of cursor files. Perhaps there's one called, say, H_NW.CUR that you like.

  • Use Visual Studio File Open to open this file inside the Resource Editor.

  • Use Edit Copy to copy the cursor image to the clipboard.

  • Use Insert Resource... New to add a new cursor resource.

  • Use Edit Paste to paste the H_NW.CUR image from the clipboard onto the new cursor.

  • And perhaps name the new resource IDC_DRAGHAND .

You might think you can copy the H_NW.CUR file to your res subdirectory and use Insert Resource... Import to add this resource. But when you do that and try to build, you may get a message saying 'Can't write H_NW.CUR, file is Read Only.' So that's why you need to use the Edit Copy and Edit Paste trick instead.

Getting a cursor resource

So alright, now we've talked about how to add an IDC_PIN and an IDC_DRAGHAND cursor to the project. How do we turn them into HCURSOR handles to feed into ::SetCursor ? It turns out that there is a CWinApp::LoadCursor method. So we add HCURSOR _hCursorPin and _hCursorDragger variables to the CPopApp class, and somewhere in the initialization phase of CPopApp inside the Pop.cpp file, we put these two lines.

 _hCursorDragger = LoadCursor(IDC_HAND);  _hCursorPin = LoadCursor(IDC_PIN); 

Where exactly 'in the initialization phase' do we put this? Well, you aren't supposed to try and do any serious initialization inside the CPopApp::CPopApp constructor, because when that constructor kicks in, the app isn't really fully ready to do anything. Instead you should do it right at the start of the code for CPopApp::InitInstance() . Do it at the start of the code block, mind you, and not at the end, because it's in the middle of the InitInstance() where ParseCommandLine(cmdInfo) is called to sneakily create your app's first CDocument and CView , and these guys may want to use something like the CPopApp information about the HCURSOR .

What a zoo, huh? But it's kind of exciting to figure MFC out and try to master it. In terms of sheer complexity and gnarl it beats the stuffing out of any computer game you'll ever see. And, let's face it, Windows programs are doing complicated things in a highly customizable fashion, so it's no surprise that they're so gnarly.

Another point to mention is that we need to make those CPopApp variables _hCursorPin and _hCursorDragger be public so that the CPopView can see them. Alternatively we can put a friend class CPopView declaration inside of CPopApp . (Note that you can do this without trying to #include "popview.h" inside Pop.h , which would be a risky thing to do, possibly leading to circular includes.)

The CPopView uses these variables when it changes the cursor. For instance we have this handler for View Pin Cursor .

 void CPopView::OnViewPincursor()  {      _hCursor = ((CPopApp*)::AfxGetApp())->_hCursorPin;  } 

Let's back up and take another look at the way we did this. Why don't we just do something like _hCursor = ((CPopApp*)::AfxGetApp())->LoadCursor(IDC_PIN) inside the CPopView::OnViewPincursor() ?

Well, we have three reasons for not doing it this way.

  • First of all, since LoadCursor is a CWinApp method, it seems like good object-oriented design to let the CWinApp be the one to call it from inside one of its methods.

  • Second, if we load the cursors once at startup in the CPopApp , we then have the static fixed HCURSOR _hCursorPin and _hCursorDragger variables to use for distinguishing between the two cases of the CView _hCursor variable.

  • Third, in a few chapters we're going to start loading bitmap resources that we want to use as icons for game objects, and then wrapping them up inside cMemoryDC objects to make it easy to BitBlt them into our game image. Often we'll have multiple game objects using the same bitmap, in which case it will make for a cleaner and more efficient design to have these identical bitmaps preloaded into static global cMemoryDC objects. So having our cursor object be a CWinApp member is good practice for later on.

What happens when you load a resource anyway? When you compile and link a Windows program, the resource code you've created is bound into your executable along with your compiled C++ source code. Your app finds the resource information by searching in the *.exe code that was loaded into RAM at the program's startup. You get at this resource information by various functions. CWinApp::LoadCursor and CWinApp::LoadIcon load cursors and icons. Later we'll see that CDialog::DoModal or CDialog::Create call up dialog box resources. You get at a resource-stored bitmap with a CBitmap::LoadBitmap call. If you have an alternate menu in your resources, you can get to it with a CMenu::LoadMenu function. ::PlaySound can get a *.wav file out of your resources.

The first call to LoadCursor is in fact a slightly computation intensive operation, because at this call, the program has to find the resource somewhere inside the *.exe and convert it into a useable form in the RAM. You might think that successive calls to LoadCursor for the same resource would keep incurring the same computation cost, but in fact Windows is smart enough to not reload a resource if you've already loaded it once during a given program run. So our justification for only calling LoadCursor once has more to do with good object-oriented design than it does with execution speed.

While we're talking about loading resources, do recall that in all of these resource-loading calls the argument you give to the function is a name for the resource you want to get at. As we've mentioned before, the 'name' for your resource can either be a character string in quotes or it can be an ID_??? name that actually stands for an integer.

(Win32 programmers might possibly worry about whether or not you need to free up the resources involved in the HCURSOR handles you create. Although in Win32 you are responsible for calling DeleteObject for graphics-tool-handles like HPEN or HBRUSH you create, in the case of an HCURSOR , you don't have to delete the object. Windows will automatically get rid of it when your program terminates.)

In the Pop Framework, cGame has a cArray_arrayHCURSOR which is initialized in the game constructor to list the usable cursors.

Using the cursor tools

All the mouse message handlers take the same two arguments as in OnMouse Move (UINT nFlags, Cpoint point) . The nFlags has information about which buttons are down, and the point gives the click location in client coordinates. Since the primary method used is going to be left-clicking, we put most of our code into the cGame::onLButtonDown function that we call from CPopView::OnLButtonDown .

So here, as an example of how we use the mouse, is what the base cGame class does with left mouse clicks.

 void cGame::onLButtonDown(CPopView *pview, UINT nFlags, CPoint point)  {      if (gameover()  gamepaused())          return; /* Don't use mouse or keyborad messages until game              starts. */      if ((pview->hcursor() == ((CPopApp*)::AfxGetApp())->hCursorPlay)           playerListenerClass() == RUNTIME CLASS(cListenerCursor))          pcontroller->onLButtonDown(nFlags);          /* We put a left click into the pcontroller for the individual          critters to see if either we have the hCursorPlay cursor,          which is used for shooting, or if our player happens to be          using a ListenerCursor. */      if (playerListenerClass() == RUNTIME CLASS(cListenerCursor))          return;          /* Don't try and use the cursor as a tool if it's attached to              the player. */      cCritter* pTouched = NULL;      pTouched = pbiota->pickTopTouched(pview->pgraphics()          ->pixelToSightLine(point.x, point.y));          /* the "pickTopTouched" method picks the top relative to a              line. I need to define the sight line different ways for              2D and 3D graphics, so I let cGraphics children overload              pixelToSightLine. */      if ((pview->hcursor() != ((CPopApp*)::AfxGetApp())-> hCursorPlay))          setFocus(pTouched);          /* Click any cursor except cursor play (shoot cursor) sets          focus to a clicked critter, or to nothing if you missed 'em.          Don't let playcursor setFocus. */      if (!pTouched)          return;      pTouched->makeServiceRequest("move to front"); /* Only has visible          effect in 2D worlds. */  //Click Hand case      if (pview->hcursor() == ((CPopApp*)::AfxGetApp())->          hCursorDragger)      { /* The draggable condition checks if the critter is willing to be          dragged. */          if (pTouched->draggable())          {              bDragging = TRUE;              onMouseMove(pview, nFlags, point); /* Move to the click                  point. */          }          return; /* Note that by bailing out here we leave              the pfocus on the move critter, which has the              side effect that cBiota::move doesn't move it,              which is good. */      }  //Click Pin case      if (pview->hcursor() == ((CPopApp*)::AfxGetApp())-> hCursorPin &&          pTouched != pplayer())          pTouched->die(); /* makes "delete me" request, possibly does              more */  //Click Zap case      if (pview->hcursor() == ((CPopApp*)::AfxGetApp())-> hCursorZap)          pTouched->zap();  /* makes "zap" request for this guy. */  //Click Replicate case      if (pview->hcursor() == ((CPopApp*)::AfxGetApp())->          hCursorReplicate)      pTouched->replicate();  /* makes "replicate" request for this guy. */  //Clean up      pbiota->processServiceRequests(); /* So you don't change          critter twice. If you wait for the timer to trigger          CPopDoc::stepDoc to call the processServiceRequest,          you might possibly manage to click or drag the same          critter again. The reason is that you may have several          OnLButtonDown messages in the message queue, and when          they are processed you will get several calls to          onLButtonDown. */      setFocus(NULL); /* For all of the one-time actions, we release          the pfocus after the action, because it's confusing to see          the critter frozen in focus after you zap it for instance.          (The freeze would be because cBiota::move doesn't move the          pfocus.) We do leave in the freeze on the move cursor. */  } 

And our cGame::onMouseMove code is as follows :

 void cGame::onMouseMove(CPopView *pview, UINT nFlags, CPoint point)  {      if (gameover()  gamepaused())          return; /* Don't use mouse or keyborad messages until game              starts. */          pcontroller->onMouseMove(nFlags);              /* We pass this on, but ordinarily the critters don't do                  anything with it. */          if (playerListenerClass() == RUNTIME CLASS(cListenerCursor))              /* Don't try and use the cursor as a tool if it's attached                  to the player. */              return;  // No Drag Case      if (!(nFlags & MK LBUTTON)          return;  // Drag Hand Case, with (hcursor == ((CPopApp*)::AfxGetApp())->      hCursorDragger)      if (pFocus() && bDragging)      {          cVector cursorforcritter =              pview->pixelToCritterPlaneVector(point.x, point.y,                  pFocus());                  /* It's going to be easier, at least to start with, to                      drag only within the focus critter plane. */          pFocus()->dragTo(cursorforcritter, pcontroller->dt());              /* Feed the current dt to dragTo so as to set the                  critter's velocity to match the speed of the drag;                  this way you can "throw" a critter by dragging it. */          pbiota->processServiceRequests(); /* In case critter              reacts. If you wait for the timer to trigger              CPopDoc::stepDoc to call the processServiceRequest, you              might possibly manage to click or drag the same critter              again. The reason is that you may have several OnMouseMove              messages in the message queue, and when they are processed              you will get several calls to cGame::onMouseMove. */      }  } 

A thing to point out here is the standard trick for dragging. The idea behind dragging is that you want your mouse move code to behave differently depending on whether or not the left button is down. The trick is to preface any dragging code with two lines like this.

 if (!(nFlags & MK_LBUTTON)) //Bail unless the left button is down.      return; 

The Windows MK_LBUTTON constant is a single-bit binary number, like 1 or 2 or 4 or 8, and the nFlags will have this bit set if the left button was down at the time the mouse move took place. There are other MK_ masks as well: MK_RBUTTON , MK_MBUTTON , MK_CONTROL , and MK_SHIFT, where the last two flags allow you to have the effect of a mouse drag depending on whether the Ctrl or Shift key is down.

A second thing to point out is that since we wanted the left drag with the Pin cursor to pop bubbles just like a left click does, we could do this by simply calling the onLButtonDown method. This is a far better practice than putting a copy of the relevant bubble-popping code inside the onMouseMove . This adheres to our general principle of good program design that you should never have copies of the same code in different places. (See game.cpp for details)



Software Engineering and Computer Games
Software Engineering and Computer Games
ISBN: B00406LVDU
EAN: N/A
Year: 2002
Pages: 272

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net