This
project gives a sample of a control that animates a group of pictures on a
dialog box. The control uses the Microsoft GDIPlus library. It displays
pictures given either by handle or by a path to a file. If you don?t have
experience with GDIPlus please read the MFC GDIPlus
Common Issues article.
The control is implemented in the class CAnimate_Ctrl which is derived
from the CStatic class from MFC. The animate control can be created either
statically (from a resource - as it is shown in the project) or dynamically
(with the Create method of the parent class). When created the following flag
must be set: SS_SIMPLE.
To start and stop the animation use the methods startAnim and stopAnim.
To
add a picture to the animation series use the addFrame method. It receives a
parameter that is either HBITMAP or CString and an integer parameter which
shows where in the list the new frame should be put. This integer is either
the index or one of these constants:
LIST_TAIL
? adds the frame as last in the list
LIST_HEAD
? adds the frame as first in the list
To remove a frame from the animation series use removeFrame.
When the pictures from the animation are shown on the control there are 3
styles for how trey are displayed. To change these styles call the
setPictureStyle method with one of these constants:
PSTYLE_NORMAL
? the picture is displayed at real size in the top left corner of the
control
PSTYLE_CENTER
? the picture is centered in the control
PSTYLE_STRETCH
? the picture size is changed so it would fit the control (Warning: you
might lose the aspect ratio of the picture if you display it this way)
Again use Invalidate to make the change take effect.
To change the background of the control use the setBkColor method. The default
value is white (RGB(255,255,255)).
To control the speed of the animation use the setFMS method. It sets the
time span between the changing of two fames in milliseconds. The default value
is 300 which is approximately 3 frames per second.
In order for any change to take effect you must stop the animation and
then start it again.
Implementation
of the CStatic derived GDI Plus animation class:
The simplest way for the realization of this class it to create a thread which
draws the new picture every X milliseconds where X is the number given to
setFMS. But first we have to create the pictures and put them a list. A list
not an array for we don?t know the actual number of pictures we?re going
to use. A list is good for dealing with sets with unknown size.
Creating
pictures for GDI Plus in MFC:
In GDIPlus there is a class called Image. This is the class that
actually holds the images for the control described.
An Image can be created in many ways, one of which is with its static member
FromFile which gets one parameter ? the path to the picture. FromFile will
create a new Image instance containing the data from the picture file. The
file might be any of the formats supported by GDIPlus. These formats vary in
different versions of GDIPlus but the ones that are always supported are BMP,
JPEG, GIF and TIFF. FromFile returns a new instance of Image so after it
isn?t needed anymore the instance must be deleted to avoid memory leaks. If
a problem appears during the picture creation FromFile returns either NULL or
an Image with status different from Ok.
For
example if it returns NULL the problem most likely is that GDIPlus hasn?t
been started (check out ?GDIPlus Common Issues? to see about starting and
shutting down GDIplus). GDIPlus is a Unicode library that can only deal with
Unicode strings but there is no need to make Unicode projects to use it. To
make a Unicode string out of an ASCII string we use the method ascii2unicode
which does this conversion. So after getting the ASCII string as a parameter
we convert it to Unicode and call Image::FromFile to get the picture in the
memory. Then we simply add the new Image to the list. So the fist
implementation of addFrame looks like this:
void
CAnimate_Ctrl::addFrame(int nPos, const CString& strFileName)
{
ASSERT(nPos
>= -2 || nPos < m_iList.GetCount());
wchar_t*
wstrFName = new wchar_t[strFileName.GetLength()+1];
ascii2unicode(strFileName,
wstrFName);
ASSERT(wstrFName!=NULL);
Image*
pImage = Image::FromFile(wstrFName);
delete
[] wstrFName; wstrFName = NULL;
if(nPos==LIST_HEAD)
m_iList.AddHead(pImage);
else
if(nPos == LIST_TAIL)
m_iList.AddTail(pImage);
else {
POSITION pos = m_iList.GetHeadPosition();
for (int i=0;i<m_iList.GetCount();i++, m_iList.GetNext(pos))
if(i==nPos)
break;
m_iList.InsertBefore(pos, pImage);
}
}
To get an Image from a handle to bitmap (HBITMAP) we must use the Bitmap
class from GDIPlus. It has a static method FromHBITMAP which is much like
FromFile, only it takes HBITMAP istead of string to create the picture.
Bitmap is a class derived from Image. So we can give the member pointer to
Image the value return from FromHBITMAP which is a pointer to Bitmap. Except
for the HBITMAP parameter the function takes a parameter of type HPALETTE.
This must be the palette of the surface that we?re going to draw the
picture on. So we must give the palette of the device context of the
control. So the other version of addFrame looks like this:
void
CAnimate_Ctrl::addFrame(int nPos, HBITMAP hbm)
{
ASSERT(nPos >= -2 || nPos < m_iList.GetCount());
CPalette
pal;
pal.CreateHalftonePalette(GetDC());
Image*
pImage = Bitmap::FromHBITMAP(hbm, (HPALETTE)pal);
if(nPos==LIST_HEAD)
m_iList.AddHead(pImage);
else
if(nPos == LIST_TAIL)
m_iList.AddTail(pImage);
else
{
POSITION pos = m_iList.GetHeadPosition();
for (int i=0;i<m_iList.GetCount();i++, m_iList.GetNext(pos))
if(i==nPos)
break;
m_iList.InsertBefore(pos, pImage);
}
}
Now we have to make the thread which will draw the images when needed. A
thread is most quickly created with AfxCreateThread. It takes two
parameters. One is a function pointer to a function of type UINT proc(
LPVOID pParam ). And the second will be set as a parameter to the proc when
it?s called. Id would be really easy if we could pass the current instance
of the CAnimate_Ctrl class with this but unfortunately MFC CWnd derived
objects cannot be passed between threads. So we create a wrapper structure
which holds the data we need.
struct transfer {
CGDIPImageList* piList; //the list of pictures
int pstyle; //draw style
UINT FMS; //millisexons between frames
bool* bAnim; //should it animate
HWND hwnd; //handle to the control
COLORREF backcol; //the background color
} m_transfer;
Now all we need to do in startAnim is fill the structure with the most
recent data and start the thread.
void
CAnimate_Ctrl::startAnim()
{
if(m_bAnimate)
return;
m_bAnimate = true;
m_transfer.bAnim = &m_bAnimate;
m_transfer.FMS
= m_nFMS;
m_transfer.piList
= &m_iList;
m_transfer.pstyle
= m_pictureStyle;
m_transfer.hwnd
= m_hWnd;
m_transfer.backcol
= m_backCol;
AfxBeginThread(AnimateThreadProc, &m_transfer);
}
Notice that bAnim is a pointer. So when we change the value of m_bAnimate
the thread will ?know? to stop animating.
AnimateThreadProc is the function which we used to draw the frames.
We don?t use the class itself as a parameter but a wrapper function. That
is why you have to stop and start the animation for changes to take effect.
A GDIPlus object can only be drawn on a GDIPlus surface. Luckily the
Graphics class has a constructor which attaches it to an existing device
context (HDC). Also the Graphics class has a method called DrawImage which
draws an object from type Image on the surface. The DrawImage function has a
lot of ways to be called but the most common are DrawImage(Image*, int, int)
which draws the image in real size with a given top-left corner and
DrawImage(Image*, int, int, int, int) which draws the image with a given
top-left corner with given height and width. So we attach a Graphics object
to the control?s DC and then draw the image on it, depending on the style
that is selected. Here is the sample code:
switch(pTrans->pstyle)
{
case PSTYLE_NORMAL:
Graphics(dc.GetSafeHdc()).DrawImage(pImage,left,top);
break;
case PSTYLE_CENTER:
top = rect.Height()/2 - (pImage->GetHeight())/2;
left = rect.Width()/2 - (pImage->GetWidth())/2;
Graphics(dc.GetSafeHdc()).DrawImage(pImage,left,top);
break;
case PSTYLE_STRETCH:
Graphics(dc.GetSafeHdc()).DrawImage(pImage,top,left,rect.Width(),rect.Height());
break;
default:
ASSERT(false);
}
We know when to draw, we know how to draw, we know where to draw, so the
code of AnimateThreadProc is clear:
UINT
AnimateThreadProc( LPVOID pParam )
{
CAnimate_Ctrl::transfer*
pTrans = (CAnimate_Ctrl::transfer*)pParam;
CWnd
wnd;
wnd.Attach(pTrans->hwnd);
POSITION posCurrentFrame = pTrans->piList->GetHeadPosition();
UINT
nTicks = GetTickCount();
CPaintDC dc(&wnd); // device context for painting
CRect
rect;
wnd.GetWindowRect(rect);
int
top, left;
top
= left = 0;
CRect rectClient;
wnd.GetClientRect(rectClient);
while(*(pTrans->bAnim)) {
if(GetTickCount() - nTicks > pTrans->FMS) {
Image* pImage = pTrans->piList->GetNext(posCurrentFrame);
//we reached the end of the list so go the beginning
if(posCurrentFrame == pTrans->piList->GetTailPosition())
posCurrentFrame = pTrans->piList->GetHeadPosition();
//invalid image ? skip it
if(pImage == NULL || pImage->GetLastStatus() != Ok)
continue;
dc.FillSolidRect(rectClient, pTrans->backcol);
//draw with the code shown above
wnd.Invalidate();
nTicks = GetTickCount();
}
}
wnd.Detach();
return 0;
}
In this particular example project an animation is shown which can be
started and stopped from the user.
The Sample Project can be downloaded from here.