Scaling, Rotating, and Shearing with QTransformTracker

We all know and love MFC's CRectTracker class, which implements rubber banding for rectangles. With CRectTracker, the user can interactively move and scale rectangular screen objects in a number of fashions. But, there is more than just horizontal and vertical, the only directions CRectTracker can work with.

So, here is QTransformTracker. Like CRectTracker, it lets you move and scale screen objects. But, it also lets you rotate an object to an arbitrary angle, or shear an object. On top of that, it has far more options and modes of operation. QTransformTracker might be the heart of a vector drawing editor, or similar application.

QTransformTracker is built with, and meant to be used with, GDI+, the new Windows graphics API.

A demonstration application, called QTTDemo, shows QTransformTracker in action. It displays a number of screen objects. Click on any object to highlight it, and load it in the tracker.

Moving

Move the object to another location simply by dragging it. If you hold down the Shift key while dragging, moving is restricted to horizontal, vertical, or diagonal directions. Note that you not only see the Tracking Rectangle as you drag, but also an outline representation of the screen object—one of the main features of QTransformTracker.

Scaling

The object is surrounded by a Mark Rectangle with eight Handles. Drag one of the Handles to scale the object, just as you would with MFC's CRectTracker. The cursor changes when you hover above a Handle. An edge Handle lets you scale in one direction, a corner Handle in all directions. If you hold down the Shift key, the original proportions of the object are preserved.

The object also has a Center Point. If you press Alt while scaling, the location of the Center Point remains fixed. By default, the opposite Handle is the anchor point. The Center Point itself can also be moved by dragging, so it doesn't have to sit at the geometrical midpoint of the screen object. It can even be moved outside of the object.

Rotating

To rotate the object, hold Ctrl before dragging one of the corner Handles. The cursor changes to indicate rotation. After dragging has started, you may release the Ctrl key. By default, rotation is around the Center Point. Pressing Alt lets you rotate around the opposite Handle. If you hold Shift, rotation is restricted to multiples of 15 degrees. Most of these and other operation modes are similar to the user interface of applications like Adobe Illustrator.

Shearing

Hold Ctrl and start dragging one of the edge Handles to shear (also called skew) the object. By default, the opposite Handle remains fixed, but if you hold Alt, the Center Point anchors the transformation. If you hold down Shift, the object size in the other direction is preserved.

More goodies

If, in the midst of a transformation like rotation or shearing, you want to move the object, hold down the space bar. Release it to continue the original transformation.

Any transformation can be canceled by pressing the Esc key or clicking the secondary (right) mouse button.

Note that textual information on the status bar is permanently updated while dragging.

The Options menu lets you change the behaviour and appearance of QTransformTracker in several ways.

Coding with the QTransformTracker class

Using QTransformTracker is simple. First, create a QTransformTracker object. Its constructor has a pointer to the associated CWnd as parameter.

Setting the tracker to work basically involves these steps:

  1. Load a screen object.
  2. In the application's OnLButtonDown() handler, call QTransformTracker's member function Track().
  3. If Track() returns a positive value, retrieve the transformation information.
  4. Use this information to transform the object.
  5. If desired, reload the object to transform again.

A screen object is one of three things to QTransformTracker:

  • a CRect object
  • a Rect object, the GDI+ counterpart of CRect
  • a GDI+ GraphicsPath, by far the most versatile object

For each of these screen objects, QTransformTracker has its own Load() member function. There is a Clear() function to unload:

void Load(GraphicsPath& path, bool bSetCenter = true,
          CDC * pDC = NULL);
void Load(CRect& rc,  bool bSetCenter = true, CDC * pDC = NULL);
void Load(Rect& rect, bool bSetCenter = true, CDC * pDC = NULL);
void Clear(CDC * pDC = NULL);

Here, if bSetCenter is true, the Center Point is set to the geometrical midpoint. If false, it is unchanged. The latter option is useful when reloading a screen object.

If pDC is set to the screen dc, QTransformTracker draws and erases itself.

The Track() member function has the following signature:

int Track(CDC * pDC, UINT nFlags, CPoint point,
          bool bClipCursor = false);

Parameter pDC points to the (prepared) screen device context. nFlags and point are the parameters of the window's OnLButtonDown() handler. If bClipCursor is true, the cursor movement is clipped to the client area of the window.

Track() has a number of possible return values, the interesting ones being the following (file QTracker.h has them all):

TrackSucceeded We have a valid transformation
TrackFailed The user clicked outside the object, perhaps on another object
TrackCancelled The user pressed Esc, or clicked the secondary mouse button

If Track() returned TrackSucceeded, the transformation information can be retrieved with the member function:

Matrix * GetTransform(void) const;

The information is in a GDI+ Matrix object. You can use this to apply the transformation to the screen object.

To implement user feedback by changing cursors, the associated CWnd should delegate calls to its OnSetCursor() message handler to QTransformTracker's member function:

BOOL OnSetCursor(CDC * pDC);

If this returns TRUE, QTransformTracker did handle the message. Otherwise, the window should handle it, possibly by delegating it to its base class, as usual.

QTransformTracker has a static function LoadCursor() to set the displayed cursors. By default, the tracker uses some standard MFC cursors. The demonstration project comes with a few useful cursors.

While tracking, a constantly changing indicator string can be retrieved by the member function:

LPCTSTR GetIndicatorString() const;

This may be used to update a pane on the application's status bar. The demonstration project QTTDemo shows how to integrate this with MFC's update-UI scheme.

Options

QTransformTracker has quite a few options in behaviour and appearance. The public member variable m_Options can be set to a combination of the following flags:

OptionRotate Allows rotation (default)
OptionShear Allows shearing (default)
OptionAllowMirror Allows mirroring of object by scaling (default)
OptionCenter Displays Center Point, allows Alt-functionality (default)
OptionCenterMove Makes the Center Point moveable by dragging (default)
OptionRotateReverseAlt Reverses the function of the Alt key at rotation (default)
OptionMarkDotted Draws the Mark Rectangle with a dotted line
OptionTrackDotted Draws the Track Rectangle with a dotted line (default)
OptionPathDotted Draws the GraphicsPath with a dotted line

QTransformTracker also has public COLORREF variables to set the colors of the:

  • Mark Rectangle
  • Track Rectangle
  • Graphics Path
  • Handles
  • Center Point

There are functions to set and retrieve some sizes:

void SetMetrics(UINT handleSize, UINT innerMargin,
                UINT outerMargin, CDC * pDC = NULL);
void GetMetrics(UINT& handleSize, UINT& innerMargin,
                UINT& outerMargin) const;

All sizes are in logical coordinates (QTransformTracker works in all mapping modes). handleSize is the size of the Handles, as you might have guessed. innerMargin is the margin between the loaded screen object and the Mark Rectangle, and outerMargin is the margin between the Mark Rectangle and the Track Rectangle.

If you would prefer another format for the indicator string, you may derive a class from QTransformTracker and override the function:

virtual void SetIndicatorString(Mode mode, REAL x, REAL y);

The header file QTransformTracker.h has more information.

Implementation

QTransformTracker is derived from QTracker, a class that might be useful in itself. It is a universal tracking/dragging/rubber banding class, and also the raison d'ètre for my memory buffer class QBufferDC. QBufferDC is used in its 'NOT XOR'-mode, and the main reason why QTransformTracker's operation is as smooth as it is.

Another interesting programming trick is the mixture of GDI+ and 'old fashioned' Windows GDI. The latter is not only faster, but also has the 'XOR' drawing modes that GDI+ lacks. One thing I had to do is use the GDI+ GraphicsPath data for CDC::LineTo() and CDC::BezierTo() functions. Look at the source code for the Load(GraphicsPath& ...) and OnUpdate() member functions to see how I managed that.

NT/2000/XP only

Although I don't see why, QTransformTracker and the demonstration application will only work in Windows NT (3.51 and later), 2000, and XP. I refrained from calling the CDC::PolyDraw() function, because it is not supported on Windows 98. But, there apparently is something else that stops QTTDemo from running under the latter OS.

Note: Your system must support GDI+, which currently only XP does natively. However, other Windows versions can be upgraded.


Downloads