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

Comments

  • wow,brava,Sjaak Priester ,you did a good job.

    Posted by suchuhui80 on 12/09/2009 04:57am

    i think i can benefit from this project,at least i will take less time to do the remaining research over the next few days. best regard.

    Reply
  • matrix stack idea

    Posted by neticous on 01/26/2008 07:41pm

    This is nice work. However in the real world you will want to add zoom an translate to the drawing surface. This will change how you use the matrix. Each mouse point needs to be transformed to the zoomed world coordinate. I would setup a matrix stack. Something with a push and pop. The push would make a copy on the top of the stack. You could set your zoomed translated world up push the matrix use the top of the stack to do the stuff your doing now and then pop. That way the zoomed translated world stays intact. Maybe you could combine your QZoomview to this demo. It would be nice to see. My 2 cents.

    Reply
  • Try those changes to avoid flicker

    Posted by vmonster on 01/27/2005 11:08am

    CQTTDemoView: - Override OnEraseBackground to return TRUE. - Override OnDraw: where: Graphics g ( pDC->GetSafeHdc () ); to: QBufferDC bufferDC ( pDC ); Graphics g ( bufferDC.GetSafeHdc () ); QBufferDC: - On the constructor class QBufferDC: where: // Get the clipping boundary of the mother DC, in logical coordinates CRect rcClip; VERIFY ( pDC->GetClipBox ( rcClip ) != ERROR ); // Transform to device coordinates (pixels), and normalize pDC->LPtoDP ( rcClip); rcClip.NormalizeRect (); if ( m_bufferBitmap.ReserveBitmap ( pDC, rcClip.Size () ) ) { ... ... #ifdef QBUFFER_DEMO // In demo mode, change the color slightly so we can see which parts are updated if ( m_bDemoMode ) col ^= RGB ( rand () % 32, rand () % 32, rand () % 32 ); #endif // Get the mother DC's clipping boundary in logical coordinates and fill it. VERIFY ( pDC->GetClipBox ( rcClip ) != ERROR ); // Transform to device coordinates (pixels), and normalize pDC->LPtoDP ( rcClip ); if ( mapmode != MM_TEXT ) { // Other mapping modes may lead to roundof errors, causing artefacts // on the screen. To compensate, we inflate the bounding rectangle with two pixels. CSize szPixels ( 2, 2 ); DPtoLP ( &szPixels ); rcClip.InflateRect ( szPixels.cx, szPixels.cy ); } // DAKOT removed: //FillSolidRect ( rcClip, col ); // Initialize accumulation of boundary information SetBoundsRect ( NULL, DCB_ENABLE | DCB_ACCUMULATE | DCB_RESET ); // DAKOT added here: FillSolidRect ( rcClip, col ); } to: // Get the clipping boundary of the mother DC, in logical coordinates CRect rcClip, rcBitmap; VERIFY ( pDC->GetClipBox ( rcClip ) != ERROR ); // Transform to device coordinates (pixels), and normalize rcBitmap.CopyRect ( &rcClip ); pDC->LPtoDP ( rcBitmap ); rcBitmap.NormalizeRect (); if ( m_bufferBitmap.ReserveBitmap ( pDC, rcBitmap.Size () ) ) { ... ... #ifdef QBUFFER_DEMO // In demo mode, change the color slightly so we can see which parts are updated if ( m_bDemoMode ) col ^= RGB ( rand () % 32, rand () % 32, rand () % 32 ); #endif /************ COMMENTED by VMONSTER // Get the mother DC's clipping boundary in logical coordinates and fill it. VERIFY ( pDC->GetClipBox ( rcClip ) != ERROR ); // Transform to device coordinates (pixels), and normalize pDC->LPtoDP ( rcClip ); if ( mapmode != MM_TEXT ) { // Other mapping modes may lead to roundof errors, causing artefacts // on the screen. To compensate, we inflate the bounding rectangle with two pixels. CSize szPixels ( 2, 2 ); DPtoLP ( &szPixels ); rcClip.InflateRect ( szPixels.cx, szPixels.cy ); } ************/ // DAKOT removed: //FillSolidRect ( rcClip, col ); // Initialize accumulation of boundary information SetBoundsRect ( NULL, DCB_ENABLE | DCB_ACCUMULATE | DCB_RESET ); // DAKOT added here: FillSolidRect ( rcClip, col ); }

    Reply
  • Using with qzoomview

    Posted by Legacy on 02/15/2004 12:00am

    Originally posted by: Jean Guillot

    Hi,

    Many thanks for your articles.
    Is there a simple way to mix qttransformtracker and qzoomview ?

    Best regards


    Reply
  • 100% CPU usage

    Posted by Legacy on 01/12/2004 12:00am

    Originally posted by: Alex Barket

    In QTTDemo any operation like moving obj, pressing mouse key on tracker etc. goes to 100% CPU usage.

    Reply
  • Wow, wonderful job!

    Posted by Legacy on 11/05/2003 12:00am

    Originally posted by: MartyCodeHo

    What a wonderful succinct tutorial.  It is unusual to find such a large amount of functionality packed into such a small amount of code.  The code compiles and works wonderfully, I had a minor problem with atan2 ambiguity in the compiler.  I just cast the args to fix that.  Works quickly and smoothly.  
    
    

    I am very impressed with your work.

    Thanks for the good work!

    Reply
  • Great stuff

    Posted by Legacy on 09/11/2003 12:00am

    Originally posted by: Boris Monkey

    This really is excellent. Not quite what i wanted, but hey, if i always found exactly what i wanted i wouldn't have to do any programming!
    nice one.
    Boris

    Reply
  • Great Job!

    Posted by Legacy on 09/10/2003 12:00am

    Originally posted by: Zhifang Zhao

    Thank you!

    Reply
  • I need a C# Version, Can you write it, please.

    Posted by Legacy on 09/10/2003 12:00am

    Originally posted by: sunny

    Great!

    • Yeah would be great ;)

      Posted by --Fragman-- on 02/28/2006 04:54pm

      I'm working on some kind of object oriented drawing program, still having some issues with resizing of sheared rotated rectangles though ....

      Reply
    Reply
  • I am Getting Error "GdiplusMem.h" not found

    Posted by Legacy on 08/27/2003 12:00am

    Originally posted by: M.V.M.MuraliKrishna

    Hai,
    This really very good application.. . In my system if can execute your Exe no problem. But whenever if build the apllication i am getting an error "GdiplusMem.h" not found.
    for that what i have to do? Pls tell me about this Problem

    Reply
  • Loading, Please Wait ...

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • On-demand Event Event Date: September 10, 2014 Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild." This loop of continuous delivery and continuous feedback is how the best mobile …

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds