📄 3paint.html
字号:
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>class View
{
public:
View () : _x (0), _y (0) {}
void MoveTo (Win::Canvas & canvas, int x, int y)
{
canvas.Line (_x, _y, x, y);
_x = x;
_y = y;
PrintPos (canvas);
}
private:
void PrintPos (Win::Canvas & canvas)
{
std::string str ("Mouse at: ");
str += ToString (_x);
str += ", ";
str += ToString (_y);
canvas.Text (0, 0, &str [0], str.length ());
}
private:
int _x, _y;
};</pre>
</td></tr></table><!-- End Code -->
<p>The <var>PrintPos</var> method is interesting. The purpose of this method is to print "Mouse at:" followed by the x and y coordinates of the mouse position. We want the string to appear in the upper left corner of the client area, at coordinates (0, 0).
<p>First, we have to format the string. In particular, we have to convert two numbers to their string representations. The formatting of numbers for printing is built into standard streams so we'll just use the capabilities of a string-based stream. In fact, any type that is accepted by a stream can be converted to a string using this simple template function:
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>#include <sstream>
template<class T>
inline std::string ToString (T & val)
{
std::stringstream out;
out << val;
return out.str ();
}</pre>
</td></tr></table><!-- End Code -->
<p>Afterwards, we use operator += to concatenate the various strings. Finally, converting a string to a pointer-to-character, as required by the API, is done by taking the address of the first character in the string.
<p>You can now run this simple program, move the mouse cursor over the client area and see for yourself that it indeed leaves a trail and that the mouse coordinates are printed in the upper left corner. You will also discover several shortcomings. For instance, try minimizing the window. After you maximize it again, all the previous traces disappear and so does the mouse-position indicator. Also, if you cover part of the window with some other application window, and then uncover it again, the restored area will be empty--mouse traces will be erased. The same with resizing!
<p>Other minor annoyances are related to the fact that, when the cursor leaves the window it's position is not updated, and when it enters the window again, a spurious line is drawn from the last remembered position to the new entry point.
<p>To round out the list of complaints, try moving the mouse towards the lower right corner and back to the upper left corner. The string showing mouse coordinates becomes shorter (fewer digits), but the trailing digits from previous strings are not erased.
<p>Let's try to address these problems one by one.
<h3>The WM_PAINT Message</h3>
<p>First of all, every well-behaved Windows application must be able to respond to the <var>WM_PAINT</var> message. Ours didn't, so after its window was occludded or minimized and then restored, it didn't repaint its client area. What should we do when Windows asks us to restore the client area? Obviously, the best solution would be to redraw the mouse trace and redisplay mouse coordinates. The trouble is that we don't remember the trace. So let's start with a simple fix--redisplaying the coordinates.
<p>A new case has to be added to our generic window procedure:
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>case WM_PAINT:
if (pCtrl->OnPaint ())
return 0;
break;</pre>
</td></tr></table><!-- End Code -->
<p>Strictly speaking, <var>WM_PAINT</var> comes with a <var>WPARAM</var> that, in some special cases, having to do with common controls, might be set to a device context. For now, let's ignore this parameter and concentrate on the common case.
<p>The standard way to obtain a device context in response to <var>WM_PAINT</var> is to call the API <var>BeginPaint</var>. This device context has to be released by a matching call to <var>EndPaint</var>. The ownership functionality is nicely encapsulated into the <var>PaintCanvas</var> object:
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>class PaintCanvas: public Canvas
{
public:
PaintCanvas (HWND hwnd)
: Canvas (::BeginPaint (hwnd, &_paint)),
_hwnd (hwnd)
{}
~PaintCanvas ()
{
::EndPaint(_hwnd, &_paint);
}
int Top () const { return _paint.rcPaint.top; }
int Bottom () const { return _paint.rcPaint.bottom; }
int Left () const { return _paint.rcPaint.left; }
int Right () const { return _paint.rcPaint.right; }
protected:
PAINTSTRUCT _paint;
HWND _hwnd;
};</pre>
</td></tr></table><!-- End Code -->
<p>Notice that <var>BeginPaint</var> gives the caller access to some additional useful information by filling the <var>PAINTSTRUCT</var> structure. In particular, it is possible to retrieve the coordinates of the rectangular area that has to be repainted. In many cases this area is only a small subset of the client area (for instance, after uncovering a small portion of the window or resizing the window by a small increment). In our unsophisticated application we won't make use of this additional info--we'll just repaint the whole window from scratch.
<p>Here's our own override of the <var>OnPaint</var> method of the controller. It creates a <var>PaintCanvas</var> and calls the appropriate <var>View</var> method.
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>bool TopController::OnPaint () throw ()
{
Win::<b>PaintCanvas</b> canvas (_h);
_view.<b>Paint</b> (canvas);
return true;
}</pre>
</td></tr></table><!-- End Code -->
<p><var>View</var> simply calls its private method <var>PrintPos</var>. Notice that <var>View</var> doesn't distinguish between <var>UpdateCanvas</var> and <var>PaintCanvas</var>. For all it knows, it is being given a generic <var>Win::Canvas</var>.
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>void View::Paint (Win::Canvas & canvas)
{
PrintPos (canvas);
}</pre>
</td></tr></table><!-- End Code -->
<p>What can we do about the varying size of the string being printed? We need more control over formatting. The following code will make sure that each of the two numbers is be printed using a fixed field of width 4, by passing the <var>std::setw (4)</var> manipulator to the stream. If the number following it in the stream contains fewer than 4 digits, it will be padded with spaces.
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>void PrintPos (Win::Canvas & canvas)
{
std::stringstream out;
out << "Mouse at: " << std::<b>setw</b> (4) << _x;
out << ", " << std::<b>setw</b> (4) << _y;
out << " ";
std::string s = out.str ();
canvas.Text (0, 0, &s [0], s.length ());
}
</pre>
</td></tr></table><!-- End Code -->
<p>You may notice that, in a fit of defensive programming, I appended four spaces to the string. Actually, without these spaces, we would still have a problem of a shrinking string. The fact that the number of characters is now fixed to 24, doesn't guarantee that the displayed string will always occupy the same area. That's because Windows uses proportional fonts, in which some characters a wider than others. In particular a space is usually narrower than a non-space character. Whence the fudge factor.
<p>The <var>WM_PAINT</var> message is also sent to our program in response to the user resizing the window. This is one of the cases when it would make sense to repaint only the newly added area (if any). The coordinates of the fresh rectangle are after all available through <var>PaintCanvas</var>.
<p>This is not always as simple as it sounds. For instance, for some applications, any resizing of a window should call for a total repaint. Think of a window that stretches the picture to fill the whole client area.
<p>Yet even for such an application, outside of resizing, it might make sense to limit the repainting only to freshly uncovered areas. So is it possible to have a cake and eat it too?
<p>The answer is in two style bits that can be set when registering a window class. These bits are <var>CS_HREDRAW</var> and <var>CS_VREDRAW</var>. The first one tells the Windows to ask for the complete redraw (i.e., invalidate the whole client area) whenever a horizontal size (width) of the window changes. The second one does the same for height. You can set both by combining them using binary or.
<p>Invalidating an area not only sends a <var>WM_PAINT</var> message with the appropriate bounding rectangle to our window, but it also erases the area by overpainting it with the background brush. The background brush is set in the window class definition--our default has been the standard brush with <var>COLOR_WINDOW</var>. (If instead the background brush is set to 0, no erasing will be done by Windows--try it!).
<h3>The Model</h3>
<p>If we want to be able to do a meaningful redraw, we must store the history of mouse moves. This can mean only one thing: we are finally ready to introduce the Model part of the triad Model-View-Controller. In general, you don't want to put too much intelligence or history into the View or the Controller. Mouse trace is our program's data--its I/O-independent part. You could even imagine a command-line version of the program. It wouldn't be able to display the line visually, and you'd have to input each point by typing in it's coordinates, but the Model would be the same.
<p>I decided to use a new data structure from the standard library, the <i>deque</i> (pronounced "deck"). It works like a double-ended vector. You can push and pop items from both ends, and the methods <var>push_front</var> and <var>pop_front</var> work as efficiently as <var>push_back</var> and <var>pop_back</var>.
<p>We don't want the history to grow beyond MAX_SIZE points. So when we add a new point to it, if it would cause the deque to exceede this count, we will pop the oldest point. In fact, this is the gist of the traditional queue, or LIFO (Last In First Out), data structure.
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>#include <deque>
#include <utility> // pair
class Model
{
enum { MAX_SIZE = 200 };
public:
typedef std::deque< std::pair<int, int> >::const_iterator iter;
Model ()
{
AddPoint (0, 0);
}
void AddPoint (int x, int y)
{
if (_queue.size () >= MAX_SIZE)
_queue.pop_front ();
_queue.push_back (std::make_pair (x, y));
}
iter begin () const { return _queue.begin (); }
iter end () const { return _queue.end (); }
private:
std::deque< std::pair<int, int> > _queue;
};</pre>
</td></tr></table><!-- End Code -->
<p>As you can see, points are stored as <var>std::pair</var>s of integers. I didn't bother to create a special data structure for a two-dimensional point. The function <var>make_pair</var> comes in handy when you don't want to explicitly specify the types of the members of the pair. You simply let the compiler deduce them from the types of arguments--in this case both are integers. Were we to use a pair of shorts instead, we would have to use the more explicit construct:
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>_queue.push_back (std::pair<short, short> (x, y));</pre>
</td></tr></table><!-- End Code -->
<p>The controller must have access to both, the model and the view. In response to a mouse move, it adds a new point to the model and, as before, tells the view to move to a new position.
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>bool TopController::OnMouseMove
(int x, int y, Win::KeyState kState) throw ()
{
_model.AddPoint (x, y);
Win::UpdateCanvas canvas (_h);
_view.MoveTo (canvas, x, y);
return true;
}</pre>
</td></tr></table><!-- End Code -->
<p>The repainting can now be done more intelligently (albeit wastefully--we still repaint the whole client area instead of just the rectangle passed to us in <var>PaintCanvas</var>). We obtain the iterator from the model and pass it to the view. The iterator gives the view access to the part of the trace that is still remembered.
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>bool TopController::OnPaint () throw ()
{
Win::PaintCanvas canvas (_h);
_view.Paint (canvas, _model.begin (), _model.end ());
return true;
}</pre>
</td></tr></table><!-- End Code -->
<p>All the view has to do now is to connect the dots. We'll use a little trick from the <algorithm> section of the standard library. The <var>for_each</var> algorithm takes the starting iterator, the ending iterator and a functor.
<p>We've already seen the use of a functor as a predicate for sorting. Here we'll make use of another type of functor that can operate on objects "pointed-to" by iterators. In our case, the iterator "points to" a pair of coordinates--what I mean is that, when it's dereferenced, it returns a reference to a std::pair<int, int>.
<p>Our functor is called <var>DrawLine</var> and it draws a line from the last remembered position to the postion passed to it in a pair. We have to initialize it with the starting position and let the <var>std::for_each</var> template call it for each value from the iterator.
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>void View::Paint (Win::Canvas & canvas,
Model::iter beg,
Model::iter end)
{
PrintPos (canvas);
if (beg != end)
{
<b>DrawLine</b> draw (canvas, *beg);
++beg;
<b>std::for_each</b> (beg, end, draw);
}
}
</pre>
</td></tr></table><!-- End Code -->
<p>If you're curious to know how <var>for_each</var> is implemented, it's really simple:
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>template<class Iter, class Op>
inline Op <b>for_each</b> (Iter it, Iter end, Op operation)
{
for (; it != end; ++it)
operation (*it);
return (operation);
}</pre>
</td></tr></table><!-- End Code -->
<p><var>Op</var> here could be a pointer to a global function or an object of a class that overloads the function-call operator. This is the implementation of our functor:
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>class DrawLine
{
public:
DrawLine (Win::Canvas & canvas, std::pair<int, int> const & p)
: _canvas (canvas)
{
_canvas.<b>MoveTo</b> (p.first, p.second);
}
void operator () (std::pair<int, int> const & p)
{
_canvas.<b>LineTo</b> (p.first, p.second);
}
private:
Win::Canvas & _canvas;
};</pre>
</td></tr></table><!-- End Code -->
<p>The two new methods of <var>Canvas</var>, <var>MoveTo</var> and <var>LineTo</var>, are easy to implement.
<h3>Capturing the Mouse</h3>
<p>It doesn't make much sense to unconditionally draw the trace of the mouse. If you have ever used any Windows graphical editor, you know that you're supposed to press and hold a mouse button while drawing. Otherwise there would be no way to "lift the pen" in order to access some controls or start drawing in a different place.
<p>We could in principle use the <var>Win::KeyState</var> argument to <var>OnMouseMove</var> and check if a button is pressed. But we can do better than that and solve one more problem at the same time. I'm talking about being able to follow the mouse outside of the window.
<p>Normally, mouse messages are sent only to the window over which the mouse cursor hovers. But if your window "captures" the mouse, Windows will redirect <i>all</i> mouse messages to it--even when the cursor is outside of its area. Obviously, capturing a mouse requires some caution. As long as the mouse is captured, the user will not be able to click any controls or interact with any other application (e.g., to activate it). That's why it is customary only to capture the mouse while it's being dragged--and the drag has to originate in the capturing window. Dragging is done by pressing and holding a mouse button while moving the mouse.
<p>There are three APIs related to mouse capture: <var>SetCapture</var>, <var>ReleaseCapture</var> and <var>GetCapture</var>. <var>SetCapture</var> takes the handle to the window that wants to capture the mouse. <var>ReleaseCapture</var> ends the capture and sends the <var>WM_CAPTURECHANGED</var> message to the captor. <var>GetCapture</var> returns the handle to the window that currently has the capture, in the current application (strictly speaking, in the current thread).
<p>We will add these APIs to our class <var>Win::Dow</var> representing a window handle.
<!-- Code --><table width=100% cellspacing=10><tr> <td class=codetable>
<pre>namespace Win
{
class Dow
{
public:
Dow (HWND h = 0) : _h (h) {}
void Init (HWND h) { _h = h; }
operator HWND () const { return _h; }
void <b>CaptureMouse</b> ()
{
::SetCapture (_h);
}
void <b>ReleaseMouse</b> ()
{
if (HasCapture ())
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -