Animation Control derived from MFC CStatic class using GDI Plus

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.