Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Rotate Your Graphics - Advanced Memory Device Context

0.00/5 (No votes)
17 Jan 2000 1  
A memory DC that allows you to rotate your graphics

Navigation

[ Illustration | Examples | Function reference | Tips & tricks | To do | Download | Latest ]

A Memory Device Context (CMemDC)

I recently downloaded the CMemDC class published by Keith Rule (see his article here at codeguru's).
It faces the following problem: If you perform rather huge operations on a device context (in an OnPaint(), for example), your display sometimes flickers around rather badly.
To circumvent this problem, Keith has designed CMemDC which creates a compatible "memory" device context in which you draw alternatively.
After you finished drawing to this device context (in memory), you clip it to the original one - only one blit takes place in the visible context. Therefore, the output doesn't flicker anymore (nearly, at least).
However, his implementation assumes that you always want to draw the entire clipping area - even if it has already been filled up by WM_ERASEBKGND or sutff.

Why Rotating Text Isn't That Easy

Moreover, I recently designed a new button that displays it text rotated.
Now, you can create a CFont that outputs text rotated (use a LOGFONT and set lfOrientation and lfEscapement) but this works AFAIK for TrueType fonts only - it doesn't work for MS Sans Serif at last.
At least, a CDC's text functions seem to get up confused if you set up such a font - the produce "random" results ... try it if you don't believe me :O

Therefore, I had a look at Keith' CMemDC and then at the CDC::PlgBlt() function. And I decided to extend the CMemDC to a "rotatable" memory context, i.e., a memory context that does not only clip one memory bitmap to an output device context only, but one that even rotates it if you like it to.

cdxCRot90DC - A Rotatable Memory DC

Well, cdxCRot90DC takes place to solve these problems. Next to specifying a "destination" device context (your output device, normally), it allows you to even provide a rectangle out of it (CMemDC always used the current output context's clip box) and an "even angle" of 0°, 90°, 180°, 270°, ... (that's why it's called cdxCRot90DC and not cdxCRotDC :).
Note: I use the term "even angle" when I mean an angle dividable by 90 without any rest.
Moreover, note that if I talk about an angle x, the same applies to x+n*360 and x-n*360, n out of N.

An example may illustrate what the device context does (using cdxCRot90DC with an angle of 90°):

'rotated_dc.gif (7491 Byte)

You may acknowledge that the rotation is not limited to text - you can freely draw into the memory device context as you like and the result will be put into your original one as having been drawn rotated - unfortunately, you cannot draw into the cdxCRot90DC using your original rectangle - you need to use the one transformed automatically for you (get it using GetRotRect()).
If you choose an "even angle" of 0°, the cdxCRot90DC class basically acts like CMemDC with some advantages:

  • You can freely define the rectangle of your destination DC you want to work with [see constructor/Create()]
  • You can initially copy the previous device context's contents into your memory bitmap (that preserves any work done by WM_ERASEBKGND or stuff that is been drawn by an ownerdrawn control if you use its DrawItem() function) [see constructor/Create()]
  • You can discard your changes.
  • CMemDC always copies back its bitmap to your original device context - that may not be suitable (you found out a text is empty and nothing is in the bitmap, for example) [function Invalidate()]

Other features for rotated output:

  • Transparent use of the angle.
    You code doesn't need to be changed for different angles (if you use GetRotRect()).
  • Automatically clones the original DC's font, background color, text mode and text color (and restores them after use).
  • An object can be reused with another destination context rectangle (no new DC is created and the old bitmap will be reused if possible).
    [See Create()]

Please note:

  • This device context doesn't support printing (in contrast to CMemDC).
    If anybody could help me in implementing it, I'd be very happy.
    I for myself don't have a printer that seems to support rotated output ...

How to Use cdxCRot90DC

A simple OnPaint(), that draws a rotated text to its device context:
[this example makes use of the constructor, GetRotRect(), IsCreated() and the destructor that calls Finish()].

void MyWnd::OnPaint()
{
    // create "destDC"
    CPaintDC    destDC(this);
    
    // get client rect
    CRect       rectClient;
    GetClientRect(rectClient);
    
    {
        cdxCRot90DC rotDC(destDC,rectClient,90 /*90 degrees*/ );
        if(!rotDC.IsCreated())
            return;
        
        // get client rect in rotated coordinates
        CRect rectRot = dc.GetRotRect();
        
        // example: print programmer's most loved text into the
        // the center of the dest DC - rotated and centered
        CString s = _T("Hello world");
        CSize  sz = rotDC.GetTextExtent(s);
        rotDC.TextOut(rectRot.left + (rectRot.Width() - sz.cx) / 2,
            rectRot.top   + (rectRot.Height() - sz.cy) / 2,
            s);
    } 
    
    // destructor of rotDC calls rotDC.Finish()
}

An example OnPaint() that draws a 3d-border around your rotated stuff could look like (here, we use the Finish() function directly):
[This example makes use of the constructor, GetRotRect(), IsCreated() and Finish()].

void MyWnd::OnPaint()
{
  // create "destDC"
  CPaintDC    destDC(this);

  // get entire client area
  CRect       rectClient;
  GetClientRect(rectClient);

  // get rectangle you want to draw to except borders
  CRect rectInner = rectClient;
  rectInner.DeflateRect(::GetSystemMetrics(SM_CXEDGE),::GetSystemMetrics(SM_CYEDGE));

  // construct rotated device context
  cdxCRot90DC rotDC(origDC,rectInner,90/*90 degrees*/);

  // check whether there is a non-empty visible rectangle
  if(rotDC.IsCreated())
  {
    // get the rectangle rectReal of rotDC that matches rectRotated in destDC
    CRect rectRotClient = rotDC.GetRotRect();

    // draw nice things into my device context using rectRotClient
    ...
    rotDC.TextOut(rectRotClient.left,rectRotClient.top,"Left-top text"));
    ...
  }

  // now copy bitmap back to destDC
  rotDC.Finish();

  // and draw a border around rectRotated
  destDC.DrawEdge(rectClient,EDGE_RAISED,BF_RECT);
}

Function Reference

Note that the following text covers only the most important functions:

cdxCRot90DC();
cdxCRot90DC(CDC & destDC, const CRect & rectDC, int iAngle, bool bCopy = false);

cdxCRot90DC(CDC *pdestDC, const CRect & rectDC, int iAngle, bool bCopy = false);
cdxCRot90DC(CDC & destDC, int iAngle, bool bCopy = false);
cdxCRot90DC(CDC *pdestDC, int iAngle, bool bCopy = false);

Constructs a new object.
The lower four creators immediately call Create() that creates a new device context for you.
If you use them, use IsCreated() to check whether the device context has successfully been set up (otherwise, if Create() failed, cdxCRot90DC::m_hDC (incorporated from CDC) won't be properly set up and you'll get thousands of ASSERTs...).
See Create() for further information.

Note that the destructor will automatically call Finish() if the device context has been set up properly.

bool Create(CDC & destDC, const CRect & rectDC, int iAngle, bool bCopy = false);
bool Create(CDC * pdestDC, const CRect & rectDC, int iAngle, bool bCopy = false);
bool Create(CDC & destDC, int iAngle, bool bCopy = false);
bool Create(CDC * pdestDC, int iAngle, bool bCopy = false);

Creates the device context.
You can start drawing into it if this function successfully returned.
Call Finish() to copy back the bitmap of the rotated DC into your destination DC (the destructor will do so automatically - see Finish() for further information).

Note #1

Since your rectangle (see parameters below) might be rotated, you cannot use the coordinates of rectDC to draw your data (they may lay outside the rotated rectangle).
Therefore, use the GetRotRect() function to receive the device context's rectangle that matches your rectDC.

Note #2

You can call Create() several times - each time you call it, it will create a "new" cdxCRot90DC for you (it won't allocate a new bitmap if the previous one meets the new denies).
However, if you want to copy your previous bitmap back to its destination device context, you have to call Finish() first.

Parameters:

  • destDC, pdestDC
    The "original" device context; pDestDC may not be NULL.
  • rectDC
    The rectangle out of the original device context you want to draw to using this memory context.
    If not provided, the current clip box will be used.
    Note that in either case, rectDC will be intersected with the current original device's clip box.
    If the resulting rectangle is empty, this function will fail.
  • iAngle
    The rotation angle for your output.
    i.e., if you provide a "90", an arrow you draw from the left to the right will be displayed pointing from bottom to top (see example image above).
    If you use 0, the cdxCRot90DC will act as CMemDC (no rotation will take place).
  • bCopy
    Set to true if you want to copy the contents of rectDC of your destDC into your newly created device context.
    This would be useful if there're already any (non-rotated) graphics that you don't want to overdraw if the memory device context clips back its bitmap.
    Please note that CDC::PlgBlt isn't one of the fastest functions thus you may first created the rotated data, clip them back and create the other ones afterwards.

This function returns false if:

  • destDC is used for printing (I don't have a printer that is able to print rotated images...)
  • rectDC intersected with your destDC's clip box is empty or - if you don't provide rectDC - your destDC's clip box is empty.

bool IsCreated() const;

Returns true if the device context is successfully created or false, if not.

const CRect & GetRotRect() const;
operator const CRect & () const { return GetRotRect(); }

Can be used to receive the rectangle of the rotated memory device context that matches the rectangle of the destination ("output") device context.
If you refer to the example image, this function returns (-120,10,-20,250) for the destination rectangle (10,20,250,120) (rotate() can be used to transform other 2D objects).

This function returns (0,0,0,0) if the device context had not been successfully set up.

bool Finish();

Informs the cdxCRot90DC that you've done with your work - it copies its bitmap back to the destination device context.
This function is automatically called by the destructor if:

  • Create() was successful and
  • You didn't used Invalidate() and
  • You did not called Finish() by yourself.

Please note that another call to finish won't copy the bitmap back again.

void Invalidate();

Invalidates the cdxCRot90DC device context - Finish() won't draw the bitmap back to the destination DC (and therefore the destructor won't do so, too).
Moreover, this function will set the current clipping region to an empty rectangle - all further drawing operations won't affect the memory device context.
You can use this function if you found out that your rotated image would be empty or stuff to avoid the device context to blit it back to the destination device context (this will be faster :).

CRect rotate(const CRect & r) const;
CPoint rotate(const CPoint & p) const;
CSize rotate(const CSize & s) const;
void rotate(POINT *pPnts, UINT nCnt) const;

Transform destination 2D objects into rotated 2D objects.
Example: If you pass a rectangle "r" to Create(), GetRotRect() would return rotate(r).

CRect rotateBack(const CRect & r) const;
CPoint rotateBack(const CPoint & p) const;
CSize rotateBack(const CSize & s) const;
void rotateBack(POINT *pPnts, UINT nCnt) const;

Transform rotated 2D objects into destination 2D objects.

CRect operator()(const CRect & r, bool bFwd = true) const;
CPoint operator()(const CPoint & p, bool bFwd = true) const;
CSize operator()(const CSize & sz, bool bFwd = true) const;
void operator()(POINT *pPnts, UINT nCnt, bool bFwd = true) const;

Short-cut operators for the above function groups.
If bFwd is true, rotate() is used, rotateBack() otherwise.

static CRect rotate(const CRect & r, int iAngle);
static CPoint rotate(const CPoint & p, int iAngle);
static CSize rotate(const CSize & s, int iAngle);
static void rotate(POINT *pPnts, UINT nCnt, int iAngle);

Any transformation from a destination 2D object into a rotated 2D object that has been rotated using the even angle iAngle. If you want to transform an object from a rotated to a destination DC, use rotate(...,-iAngle).
You can use these functions to perform calculations without creating a device context for this task.

operator cdxCRot90DC * ();
cdxCRot90DC *operator->();
const cdxCRot90DC *operator->() const;

Use an object of class cdxCRot90DC as a pointer.
This might be useful for CDC member functions that expect a pointer to a CDC.

Some Tips & Tricks

  • Drawing 3D text in the rotated device context may need you to know in which direction in the rotated device context the lower right corner of the destination lays.
    For example, if you want to draw some disabled text (as seen in a disabled CStatic), you normally would use:
    ...
    dc.SetBkMode(TRANSPARENT);
    dc.SetTextColor(::GetSysColor(COLOR_3DHILIGHT));
    dc.TextOut(pnt + CPoint(1,1),strText);
    dc.SetTextColor(::GetSysColor(COLOR_3DSHADOW));
    dc.TextOut(pnt,strText);
    ...

    But what happens if you do so in a rotated DC ?
    Previously, your "shadow" text would be up-left. But if the device context rotates your output by 180°, it would be down-right ... ugly !
    The solution is to use rotate() to transform the vector (1,1) into the corresponding vector in the rotated device context:

    ...
    rotDC.SetBkMode(TRANSPARENT);
    rotDC.SetTextColor(::GetSysColor(COLOR_3DHILIGHT));
    rotDC.TextOut(pnt + rotDC.rotate( CPoint(1,1) ),strText);
    rotDC.SetTextColor(::GetSysColor(COLOR_3DSHADOW));
    rotDC.TextOut(pnt,strText);
    ...

    ... and everything is fine :)

    Note that there's a member function DrawControl() (which is not described here) that may kill most of your problems :)

  • CDC::DrawEdge() and CDC::Draw3dRect() may have the same problem !
    I recommend to use some code as initially shown as example 2 to set up objects that need these functions (although one could easily write a short-cut for them. :)

To Do

  1. Support printing
  2. Post the following new classes to codeguru:
    • cdxCRotButton: A CButton with rotated text & icon.
    • cdxCRotStatic: A CStatic with rotated text & icon.
    • cdxCRotBevelLine: A bevelline with rotated text.

These classes are already finished but I don't have the time to write a proper text for codeguru ... I hope to find some time soon.

  • Add cdxCRotDC that allows rotation by any angle :)

Latest

Llew Goodstadt of the University Laboratory of Physiology, Oxford, UK has provided an updated project that addresses some of the problems of the original code in that it moves away from using PlgBlt and instead uses DIBs to perform the rotation. Llew did not have time to write up his solution, nor complete the code to his satisfaction, and so the demo is provided here "as is" in case others wish to extend this further.

License

This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.

A list of licenses authors might use can be found here.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here