Handling User Input

In this section, we will write a simple drawing program called scribble. Scribble works by creating a set of polylines in response to the user's input. When the user clicks the left mouse button, we will create a new polyline at that point in the window. Then, as the user moves the mouse, the windowing system will report the new position to us, which we then add as a new point in the current polyline.

Sounds simple, but in order for this to work, need to be able to receive messages about the mouse from the windowing system. We do this by subclassing the Wnd class and overriding the appropriate message handlers.

Message handlers are protected member functions of the Wnd class that are called in response to a specific message sent by the windowing system. Take a look at the definition of our Wnd-derived class:

#include <lvt/lvt.h>

using namespace LVT;

class ScribblerWnd : public Wnd
{
public:
    ScribblerWnd();

protected:
    virtual void OnMenuCommand(int id);
    virtual void OnPaint();
    virtual void OnLButtonDown(int x, int y);
    virtual void OnLButtonUp(int x, int y);
    virtual void OnMouseMove(int x, int y);

private:
    std::vector<Polyline> m_lines;
    bool m_mouseDown;
};

The message handlers are the protected member functions that begin with On. From looking at the class definition, you can see that we are interested in messages regarding whether the left mouse button has been clicked (OnLButtonDown), released (OnLButtonUp), or if the mouse has been moved (OnMouseMove). (We'll cover the other two functions -- OnMenuCommand and OnPaint -- a little later). All three messages take as parameters the coordinates of the mouse at the time the message was sent.

You can also see that we have a vector of Polylines. A Polyline is an LVT class that encapsulates a set of points (or Vecs) that are sequentially connected with lines (think connect-the-dots). We also have a boolean variable that records whether or not the left mouse button is currently pressed.

Lets look at the implementation of the three mouse message handlers:

void ScribblerWnd::OnLButtonDown(int x, int y)
{
    // Start tracking the mouse.
    m_mouseDown = true;

    // Create a new polyline
    Polyline newLine;
    newLine.AddPoint((scalar) x, (scalar) y);
    m_lines.push_back(newLine);
}

We start by recording that the mouse button is currently pressed. We then create a new Polyline and add the first point to it at the current mouse position. A Polyline, like all drawable objects in LVT, expects the coordinates of its points to be in floating-point format, so we cast them to scalars before passing them in. Once we're done, we push the new Polyline onto our vector of Polylines.

void ScribblerWnd::OnLButtonUp(int x, int y)
{
    // Stop tracking the mouse
    m_mouseDown = false;
}

Not much exciting going on here. When we're notified that the left mouse button has been released, we simply mark it as such.

void ScribblerWnd::OnMouseMove(int x, int y)
{
    if (!m_mouseDown)
         return;

    //Add the current mouse position as a new point in the current line
    m_lines.back().AddPointAndDraw((scalar) x, (scalar) y);
    glFlush();
}

Here we finally make use of that m_mouseDown variable we were concerned with in the previous two message handlers. If the mouse button is not currently pressed, we don't care about the mouse being moved, so we return. Otherwise, we add the current mouse position to the Polyline that's at the end of our vector of Polylines, again casting the coordinates to scalars. Notice that we're using AddPointAndDraw, as opposed to AddPoint, which, as you've probably guessed, adds the point to the Polyline and then draws the new portion of it.

One last detail is to call glFlush, which informs OpenGL that we want the drawing to take place immediately.

We have one last detail we need to take care of in order for our drawing program to be functional. We need to handle the case when our window needs to be redrawn. This occurs after a portion of the window that was obscured becomes uncovered. We take care of this case in the OnPaint message handler:

void ScribblerWnd::OnPaint()
{
    Wnd::OnPaint();

    std::for_each(m_lines.begin(), m_lines.end(),
        std::mem_fun_ref(&Polyline::Render));
}

Right away, you'll notice that this message is handled slightly differently than the others in that we call the base class's handler before we doing our own handling. The default Paint handler clears the frame buffer and initializes modelview matrix to the identity, it then renders a scene graph attached with Wnd::ShowScene. (We will cover scene graphs in part III.) Unless you have a good reason to override this behavior (and such reasons do exist, which is why it's allowed), it is recommended that you call the base OnPaint function in your own handler.

Beta Note: This may change in a future version of LVT. It is possible that the default behavior of Wnd::OnPaint will be moved to a separate function, removing the need to call it in a derived OnPaint method.

Once the base class takes care of its portion of the message, we use a little STL magic to call the Render member function on each Polyline in our vector. Every drawable LVT object has a Render member function which draws the object. Having each Polyline in our list draw itself ensures that the contents of our window are up to date.

You might be wondering what the difference between calling AddPointAndDraw vs calling AddPoint and then Render is. The difference is that AddPointAndDraw draws only the portion of the object that has changed. In the case of Polyline, this is the line segment from the last point to the point we just added. Render, on the other hand, draws the entire object, from start to finish. Calling Render on a Polyline for each new point added would cause the drawing progressively get slower and slower as the Polyline gets longer. AddPointAndDraw, on the other hand, will always take the same amount of time.

This is all we need to complete the functionality of our drawing program. All we need to do to complete it is to take the simple program we created in the last section and modify it to use a ScribblerWnd instead of a Wnd:

int main(int argc, char *argv[])
{
    LVT::App myApp;
    ScribblerWnd myWnd;

    myApp.Init(&argc, &argv);

    myWnd.Create("Scribble");

    myApp.Run(myWnd);

    return 0;
}

One issue we have yet to take up is the possibility that the user might make a mistake in his masterpiece, requiring him to start over. One way to allow him to do this is with a Menu, which is the subject of the next section.