Introduction
In a project I'm working on I needed the ability to show various image formats in a preview window. I wanted to be able
to handle any of the common image formats, such as Bitmaps, JPEGs, GIFs and PNGs. After a bit of searching around and
not finding anything that did precisely what I wanted I decided to roll my own.
Basics
The task breaks down into two sub-tasks. The first is to load the image from some source such as a file and to decode
it into a format that Windows can handle. Then comes the easy part, rendering the image onto the display.
Task 1 made easy
The usual approach to loading an image from a file is to find some library (commercial or open source) that handles the
image format you're interested in and stitch it into your program. I've done this before and I'm sure most of us have at some
time or other. The frequency with which such a task arises is attested to by the popularity of
this[
^]
article (
CxImage by Davide Pizzolato).
Whilst browsing through CodeProject and mulling over the prospect of yet again trying to use an external library to handle the
images I found
this[^]
(Starting with GDI+ by Christian Graus). After reading this article and then moving on to the MSDN documentation for GDI+ I realised this
was the perfect solution to task 1. (A mention in Mike Dunn's C++ FAQ didn't hurt either).
Task 2
This is much easier. The best way I've found is to derive a class from the
CStatic
class, make it
ownerdraw
and render the bitmap into
the
static
control.
Putting it all together
class CImagePreviewStatic : public CStatic
{
DECLARE_DYNAMIC(CImagePreviewStatic)
public:
CImagePreviewStatic();
virtual ~CImagePreviewStatic();
virtual BOOL Create();
virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
void SetFilename(LPCTSTR szFilename);
protected:
WCHAR m_wsFilename[_MAX_PATH];
Image *m_img;
Graphics *m_graphics;
DECLARE_MESSAGE_MAP()
};
The constructor
NULL
s the
Image
and
Graphics
pointers (both these objects are GDI+ objects). The destructor deletes those pointers.
The GDI+ objects get created in the
Create()
function:
BOOL CImagePreviewStatic::Create()
{
if (GetSafeHwnd() != HWND(NULL))
{
m_img = new Image(m_wsFilename);
m_graphics = new Graphics(GetSafeHwnd());
return TRUE;
}
return FALSE;
}
Pretty simple code. The
m_img
pointer is initialised with an
Image
object created using the filename of the image you want to load. The
m_graphics
pointer is initialised with a
Graphics
object which is
associated with the
CStatic
s underlying window.
Note that the Image
constructor requires a Unicode string. In ANSI builds the SetFilename()
function converts the ANSI filename into a
Unicode string using some helper macros.
void CImagePreviewStatic::SetFilename(LPCTSTR szFilename)
{
#ifndef _UNICODE
USES_CONVERSION;
#endif
ASSERT(szFilename);
ASSERT(AfxIsValidString(szFilename));
TRACE("%s\n", szFilename);
#ifndef _UNICODE
wcscpy(m_wsFilename, A2W(szFilename));
#else
wcscpy(m_wsFilename, szFilename);
#endif
delete m_img;
m_img = new Image(m_wsFilename, FALSE);
Invalidate();
}
Once we've done the string conversion (if required) we delete the existing
Image
pointer and create a new one using the new filename. Then we
Invalidate()
the window and let Windows send us a paint message some time in the future.
When Windows gets around to asking us to redraw the ownerdraw
logic kicks in.
void CImagePreviewStatic::DrawItem(LPDRAWITEMSTRUCT )
{
Unit units;
CRect rect;
if (m_img != NULL)
{
GetClientRect(&rect);
RectF destRect(REAL(rect.left),
REAL(rect.top),
REAL(rect.Width()),
REAL(rect.Height())),
srcRect;
m_img->GetBounds(&srcRect, &units);
m_graphics->DrawImage(m_img, destRect, srcRect.X, srcRect.Y, srcRect.Width,
srcRect.Height, UnitPixel, NULL);
}
}
What this code does is get the bounding rectangle of the underlying Window and creates a
RectF
object specifying the same coordinates. (The appended
F
means that each element is a
REAL
rather than an
int
). Then we get the bounds of the image itself and call the
DrawImage()
function to draw the image on the Window. The specific
DrawImage()
overload I used scales the image into the drawing
rectangle. Pretty simple code!
Using the code
You probably want to include these three lines at the end of your
stdafx.h
file.
#include <gdiplus.h>
using namespace Gdiplus;
#pragma comment(lib, "gdiplus.lib")
These lines include the header file for GDI+ and set the
Gdiplus
namespace. This will save some typing since you don't then have to prefix every GDI+
reference with
Gdiplus::
. The third line inserts a reference to the
gdiplus.lib
library into whichever object file contains it; this
saves you having to explicitly add the library to your project workspace.
Next, your application must initialise the GDI+ library before using any other GDI+ functions, and shut it down before exiting. That's done by this code somewhere
in your application. CMyApp::InitInstance()
is a good place.
.
.
.
GdiplusStartupInput gdiplusStartupInput;
GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
.
.
.
m_gdiplusToken
is an
unsigned long
used by GDI+ later when you want to shut it down. Your
CMyApp::ExitInstance()
would contain:
GdiplusShutdown(m_gdiplusToken);
Once you've initialised GDI+ you're good to go with all the rest of the GDI+ functionality.
To use CImagePreviewStatic
you need to add a static
window to your dialogs or views. Bind the window to a CImagePreviewStatic
object. Make sure the static
window has the SS_OWNERDRAW
style set. Once you've bound the window to the CImagePreviewStatic
object
call the Create()
function and then set the image filename you want to preview. For example:
class CImageDlg : public CDialog
{
DECLARE_DYNAMIC(CImageDlg)
enum adviseMessages
{
adviseUpdatePreview,
};
public:
CImageDlg();
virtual ~CImageDlg();
virtual void DoDataExchange(CDataExchange *pDX);
protected:
CImagePreviewStatic m_preview;
DECLARE_MESSAGE_MAP()
virtual BOOL OnInitDialog();
public:
};
In the
DoDataExchange()
function we use
DDX
to bind the
m_preview
member to a
static
control on our dialog template.
void CImageDlg::DoDataExchange(CDataExchange *pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Control(pDX, IDC_IMAGEPREVIEW, m_preview);
}
Then, in our
OnInitDialog()
we do:
BOOL CImageDlg::OnInitDialog()
{
CDialog::OnInitDialog();
m_preview.Create();
return TRUE;
}
Once this has been done our
CImagePreviewStatic
control is initialised and ready to display images. All we have to do is call the
SetFilename()
function and the
ownerdraw
plumbing in Windows does all the rest.
The demo program is the minimal implementation necessary to demonstrate the control. It has a hardwired image name and it expects to find this image in the programs
current directory.
History
March 6, 2004 - Initial Version.