Nigel Thompson
Microsoft Developer Network Technology Group
Created: July 19, 1994
Click here to open or copy the files in the MFCCLIP sample application for this technical article.
This article shows a technique for transferring Microsoft® Foundation Class (MFC) C++ objects through the Windows™ Clipboard in 32-bit applications. The MFCCLIP sample application, which accompanies this article, implements an extended-selection, owner-drawn list box in its view. Multiple items in the list box may be selected and copied to the Clipboard in a private format. If the private format data is present in the Clipboard, the data may be pasted into the list box. The sample code shows how a list of objects, rather than simply a single object, can be copied and pasted. The owner-drawn list box includes a bitmap at the start of each line (for fun—really). The bitmaps are drawn using a new class, CTransBmp, which makes drawing a bitmap image with a transparent border trivial.
The key tasks illustrated by the sample code are:
While writing a small application, I needed to create several list boxes to show some data. I wanted to be able to select items from one list and copy them to the Clipboard for later pasting into a different list. Each item in the list had several fields, and consequently, I used an owner-drawn list box so that I could format each line of the list box in the most suitable way. Part of the formatting included drawing a small bitmap with an irregular shape at the start of some of the lines to indicate a particular status.
The design required two major tasks: (1) creating an owner-drawn list box in a Microsoft® Foundation Class (MFC) application's view class, and (2) copying MFC objects to the Windows™ Clipboard. Another minor task was drawing the bitmap for each line, which I had done before, although not in an MFC-based application.
The MFCCLIP sample application shows all the code required to implement an owner-drawn list box with copy and paste support. The implementation of the paste function is a bit trivial, but it shows the technique. Figure 1 shows a screen shot of the MFCCLIP application. As you can see, each line of the list box has a bitmap at the beginning, followed by some text.
Figure 1. A screen shot of the MFCCLIP application
Before we look at manipulating any data, let's see what's involved in creating a list box in the application's view window. We want the list box to fill the entire view, and if an item is double-clicked, we want to show the edit dialog box for that item.
Creating the list box as a child window is fairly simple. Use ClassWizard to add a WM_CREATE message handler to the view class, and in the handler, create the list box as a visible child window the same size as the client area of the view window.
int CClipView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
CRect rc;
GetClientRect(&rc);
// Adjust the client area to make the list box look better.
rc.bottom -= 2;
m_wndList.Create(WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL
| LBS_DISABLENOSCROLL | LBS_OWNERDRAWFIXED
| LBS_EXTENDEDSEL | LBS_NOINTEGRALHEIGHT
| LBS_NOTIFY,
rc,
this,
IDC_MYLIST);
return 0;
}
Note that I used App Studio to create the IDC_MYLIST symbol (the value isn't important). The LBS_NOTIFY flag tells the list box to notify its parent of events such as mouse clicks.
Unfortunately, ClassWizard can't be used to add handlers to the view class for notification messages from the list box. We have to do this by hand. We want to add a handler for the LBN_DBLCLK event, which is sent to the parent window in a WM_COMMAND message. The first step is to declare the handler function in the view's header file:
class CClipView : public CView
{
...
// Generated message map functions
protected:
//{{AFX_MSG(CClipView)
...
afx_msg void OnListboxDblClick();
...
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
It doesn't matter what the function is called. I called mine OnListBoxDblClick just to be obscure <grin>.
The next step is to add an entry in the message map in the CPP file for the view:
BEGIN_MESSAGE_MAP(CClipView, CView)
//{{AFX_MSG_MAP(CClipView)
...
ON_CONTROL(LBN_DBLCLK, IDC_MYLIST, OnListBoxDblClick)
...
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
The ON_CONTROL macro is used to manage control notification messages. The three parameters are the notification message, the ID of the control sending the message, and the function to be called to handle the event. Having made the entry in the message map, we can write the handler function itself:
void CClipView::OnListBoxDblClick()
{
// Find what was clicked.
int i = m_wndList.GetSelCount();
// Get the first selected item.
int iSel = LB_ERR;
m_wndList.GetSelItems(1, &iSel);
ASSERT(iSel != LB_ERR);
CMyObj* pObj = (CMyObj*) m_wndList.GetItemData(iSel);
ASSERT(pObj);
ASSERT(pObj->IsKindOf(RUNTIME_CLASS(CMyObj)));
if (pObj->DoEditDialog() == IDOK) {
CClipDoc* pDoc = GetDocument();
pDoc->SetModifiedFlag();
pDoc->UpdateAllViews(NULL);
}
}
Because this is an extended-selection list box, we can't use GetCurSel to find the selected item. Instead, we must assume that multiple items may be selected. I chose to implement the code such that, if more than one item is selected, we take the first one.
GetSelItems is called to get the first item number of the currently selected set of items. The GetItemData function is used to retrieve the item pointer that was set when the item was originally added to the list box by calling the AddString function. (Remember, this is an owner-drawn list box, so the item data is any 32-bit quantity that we want to store there.) The item data is cast to be a pointer to a CMyObj object, and some ASSERT statements confirm that all is well. The object's edit dialog box is then called into being (my editor refuses to let me use the word invoked). If the user closes the edit dialog box by clicking the OK button, the document is marked as having been modified, and all views of the data are redrawn to reflect the change.
If the view changes size, we need to resize the list box, so ClassWizard is used to add a handler for WM_SIZE messages. The list box is resized accordingly.
void CClipView::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
// Resize the list box to fit in the entire client area.
m_wndList.SetWindowPos(NULL,
0, 0,
cx, cy-2,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOZORDER);
}
So that the list box can be controlled by keyboard commands, we need to ensure that the list box gets the focus if the user activates the view window. Use ClassWizard again to add a handler for WM_SETFOCUS messages, and simply set the focus to the list box window:
void CClipView::OnSetFocus(CWnd* pOldWnd)
{
// Set focus to the list box.
m_wndList.SetFocus();
}
When the view window is destroyed, it's important that we destroy any child window belonging to the view. The destructor for the view class will destroy only the MFC objects—not the actual Windows window objects. So again, use ClassWizard (what a handy thing it is!) to add a handler for WM_DESTROY messages:
void CClipView::OnDestroy()
{
CView::OnDestroy();
// Be sure to destroy the window we created.
m_wndList.DestroyWindow();
}
Now that we have a list box, it would be nice if we could see the data it contains. All that is required to do that is for the view to handle WM_DRAWITEM and WM_MEASUREITEM messages. If the list box is to be sorted, you will also need to handle WM_COMPAREITEM messages. For each message, we add a handler. Let's look at the WM_MEASUREITEM handler first:
void CClipView::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT
lpMeasureItemStruct)
{
// Return the height of the font or the bitmap,
// whichever is greater.
lpMeasureItemStruct->itemHeight = max(m_iFontHeight,
m_bmSmile.GetHeight());
}
In order for you to understand this simple piece of code, I need to explain a couple of points: First, this list box has items that are all the same height (it was created with the LBS_OWNERDRAWFIXED flag), so the height of each line is a constant. Because I wanted each line to contain a bitmap and some text, I chose to report the line item height as the height of the bitmap or the text—whichever is greater. The height of the text is determined in the view class constructor when the font is created. The constructor also loads the bitmap.
CClipView::CClipView()
{
// Load the font we want to use.
m_font.CreateStockObject(ANSI_FIXED_FONT);
// Get the metrics of the font.
CDC dc;
dc.CreateCompatibleDC(NULL);
CFont* pfntOld = (CFont*) dc.SelectObject(&m_font);
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
dc.SelectObject(pfntOld);
m_iFontHeight = tm.tmHeight;
m_iFontWidth = tm.tmMaxCharWidth;
// Load the bitmap we want.
m_bmSmile.LoadBitmap(IDB_SMILE);
}
Note that the bitmap used here is of the CTransBmp class, which we'll come to a little later on. The regular MFC CBitmap object doesn't have a GetHeight or GetWidth member.
Now let's look at the code for drawing an item in the list box in response to a WM_DRAWITEM message:
void CClipView::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT pDI)
{
CMyObj* pObj;
HFONT hfntOld;
CRect rcText;
switch (pDI->itemAction) {
case ODA_DRAWENTIRE:
// Draw the whole line of information.
// Get a pointer to the object.
pObj = (CMyObj*) pDI->itemData;
ASSERT(pObj);
ASSERT(pObj->IsKindOf(RUNTIME_CLASS(CMyObj)));
// Set up the font we want to use.
hfntOld = (HFONT) ::SelectObject(pDI->hDC, m_font.m_hObject);
rcText = pDI->rcItem;
// Erase the entire area.
::ExtTextOut(pDI->hDC,
rcText.left, rcText.top,
ETO_OPAQUE,
&rcText,
"", 0,
NULL);
// Draw the bitmap in place.
m_bmSmile.DrawTrans(pDI->hDC, rcText.left, rcText.top);
// Move the text over to just beyond the bitmap.
rcText.left = pDI->rcItem.left + m_bmSmile.GetWidth() + 2;
::DrawText(pDI->hDC,
pObj->GetText(),
-1,
&rcText,
DT_LEFT | DT_VCENTER);
// Check if we need to show selection state.
if (pDI->itemState & ODS_SELECTED) {
::InvertRect(pDI->hDC, &(pDI->rcItem));
}
// Check if we need to show focus state.
if (pDI->itemState & ODS_FOCUS) {
::DrawFocusRect(pDI->hDC, &(pDI->rcItem));
}
::SelectObject(pDI->hDC, hfntOld);
break;
case ODA_FOCUS:
// Toggle the focus state.
::DrawFocusRect(pDI->hDC, &(pDI->rcItem));
break;
case ODA_SELECT:
// Toggle the selection state.
::InvertRect(pDI->hDC, &(pDI->rcItem));
break;
default:
break;
}
}
There is no way you can arrive at this piece of code by studying the Microsoft Win32® Software Development Kit (SDK) for Windows NT™ documentation of the WM_DRAWITEM message and DRAWITEMSTRUCT structure. This is right. Trust me! Let's go through it step by step.
The WM_DRAWITEM message is sent for one of three possible tasks: to draw the entire line of data afresh, to change the selection state, or to change the state of the focus indicator. Let's start with drawing the entire line of data.
A pointer to the CMyObj object is extracted from the DRAWITEMSTRUCT and validated. The font we want to use is then selected into the device context (DC). Before we draw anything, we must erase the entire area; the easiest way to do this is to call ExtTextOut. I know that seems bizarre, but it's easier than using FillRect because it doesn't require us to create a brush, and will by default use the correct text background color for the fill. ExtTextOut is also implemented very efficiently in device drivers, so it takes very little time to execute.
Having erased the area, we can draw the bitmap. We'll look at how the DrawTrans function works later. For now, just accept that it draws the bitmap with a transparent border (so it can have an irregular shape as many icons do). The text rectangle is then adjusted so that its left edge is just past the area occupied by the bitmap, and the text from the object is drawn by calling DrawText. (I could have used ExtTextOut here instead.)
Finally, we must check to see if the item is currently selected or has the input focus. If it's selected, we invert the entire area. If it has the focus, we call DrawFocusRect to show the dotted-line rectangle that indicates focus.
The WM_DRAWITEM message is also sent to change the state of the focus or selection of an item, and we need to provide handlers for these cases, too. It turns out that these messages are always sent in such a way that you don't need to know if the focus (or selection) state is being set or reset; it's enough to just toggle the current state. So implementing these two tasks becomes trivial: We simply call InvertRect to change the selection state or DrawFocusRect to change the focus state.
In order to show what is required to copy MFC objects to the Clipboard, I created two classes: CMyObj and CMyObList.
The CMyObj class is derived from CObject and provides a simple object that contains a string of text. Here's the header file:
class CMyObj : public CObject
{
public:
DECLARE_SERIAL(CMyObj)
CMyObj();
CMyObj(CMyObj& rOb);
~CMyObj();
virtual void Serialize(CArchive& ar);
const CString& GetText()
{return m_strText;}
void SetText(CString& str)
{m_strText = str;}
int DoEditDialog();
private:
CString m_strText;
};
The SetText and GetText member functions allow the text to be set or retrieved. The DoEditDialog function allows the user to edit the contents of the object, using the dialog box shown in Figure 2.
Figure 2. The dialog box used to edit the contents of a CMyObj string
The implementation of CMyObj is very simple. Here are the constructors and the destructor from MYOBJ.CPP:
CMyObj::CMyObj()
{
m_strText = "Some text.";
}
CMyObj::~CMyObj()
{
}
Calling the DoEditDialog function opens the dialog box for editing the text string:
int CMyObj::DoEditDialog()
{
CMyObjDlg dlg;
dlg.m_strText = m_strText;
int iRes;
if ((iRes = dlg.DoModal()) == IDOK) {
m_strText = dlg.m_strText;
}
return iRes;
}
The dialog box code itself is trivial, so I won't reproduce it here. You can look at the source in MYOBJDLG.CPP if you want to see the details.
The CMyObList class is derived from the MFC class CObList. CMyObList is simply a list of CMyObj objects. Here's the header file from MYOBLIST.H:
class CMyObList : public CObList
{
DECLARE_SERIAL(CMyObList)
public:
CMyObList();
~CMyObList();
void DeleteAll();
CMyObj* RemoveHead()
{return (CMyObj*) CObList::RemoveHead();}
CMyObj* GetNext(POSITION& rPos)
{return (CMyObj*) CObList::GetNext(rPos);}
void Append(CMyObj* pMyObj);
BOOL Remove(CMyObj* pMyObj);
virtual void Serialize(CArchive& ar);
};
The RemoveHead and GetNext functions perform the same function as those in the base class. Let's look at the others, starting with the constructor and destructor:
CMyObList::CMyObList()
{
}
CMyObList::~CMyObList()
{
DeleteAll();
}
Note that the destructor calls the DeleteAll function to destroy any objects still in the list.
void CMyObList::DeleteAll()
{
while(!IsEmpty()) {
CMyObj* ptr = RemoveHead();
ASSERT(ptr);
delete ptr;
}
}
The DeleteAll function walks the list, removing and destroying the top item each time. Items are added to the end of the list with the Append function.
void CMyObList::Append(CMyObj* pMyObj)
{
ASSERT(pMyObj);
ASSERT(pMyObj->IsKindOf(RUNTIME_CLASS(CMyObj)));
CObList::AddTail(pMyObj);
}
And finally, the Remove function is used to unlink an object from the list without destroying the object:
BOOL CMyObList::Remove(CMyObj* pMyObj)
{
POSITION pos = Find(pMyObj);
if (!pos) return FALSE;
RemoveAt(pos);
return TRUE;
}
That's all we need to look at for now. Let's move on to see how a CMyObList object containing a series of CMyObj items can be copied to the Clipboard.
The list box is an extended-selection list, which means an arbitrary number of discontiguous items can be selected. When the current set of selected items is copied to the Clipboard, we want to create a list object containing copies of all the selected objects and copy the list itself to the Clipboard.
The Windows Clipboard uses global memory to share the data it's holding, so to copy something to the Clipboard, we need to create a piece of shared global memory and write the data to that. With MFC objects, the easiest way to save the state of the object is to use its Serialize function to write it to a file. If we can use a piece of global memory as a file, we can serialize the objects to the memory and the task is done. The MFC library provides the CMemFile class, which allows you to write to a memory-based file, but because it doesn't allow you direct access to the memory, the only way to use this class is to serialize the objects to a CMemFile, get the length of the file, create a global memory block, and read the CMemFile back into the global memory. This works, but is wasteful of memory.
The MFC library includes an undocumented class called CSharedFile (documented in AFXPRIV.H), which allows you to essentially create a CMemFile object around a piece of memory you can access. The Clipboard copy and paste code below makes use of the CSharedFile class.
I used App Studio to create an Edit menu with only Copy and Paste items initially. I used ClassWizard to add handlers for both of these and also to add handlers for their OnUpdate. . . functions. The OnUpdate. . . handlers control the enabling or disabling of the individual menu items, so that we can disable the Edit Copy command if nothing is selected and the Edit Paste command if there is nothing in the Clipboard we can paste.
Because the data we want to copy to the Clipboard is of our own design, we need to register the name of the format with Windows and get a unique ID for the format. I like to do this when the application first starts up, so I added this to the initialization of my application:
BOOL CMfcClipApp::InitInstance()
{
...
// Register our Clipboard format names.
m_uiMyListClipFormat = ::RegisterClipboardFormat("My Object List");
...
}
The m_uiMyListClipFormat variable is defined in the application main header file as a UINT and the application object instance (theApp) is made global:
class CMfcClipApp : public CWinApp
{
public:
CMfcClipApp();
UINT m_uiMyListClipFormat;
// Overrides
virtual BOOL InitInstance();
// Implementation
//{{AFX_MSG(CMfcClipApp)
afx_msg void OnAppAbout();
// NOTE: ClassWizard will add and remove member functions here.
// DO NOT EDIT what you see in these blocks of generated code !
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
extern CMfcClipApp theApp;
Having registered a Clipboard format, we can look at copying data to the Clipboard. Let's first look at the OnUpdate. . . handler for the Edit Copy command:
void CClipView::OnUpdateEditCopy(CCmdUI* pCmdUI)
{
int i = m_wndList.GetSelCount();
pCmdUI->Enable(i > 0 ? TRUE : FALSE);
}
If no items are selected, the command is disabled. This does two things: It shows the user the command won't do anything (it's grayed out), and it prevents us from getting Copy commands when there is nothing to copy. Let's look at the code for doing the actual copy operation:
void CClipView::OnEditCopy()
{
// Get the number of selected items.
int iCount = m_wndList.GetSelCount();
ASSERT(iCount > 0);
// Get the list of selection IDs.
int* pItems = new int [iCount];
m_wndList.GetSelItems(iCount, pItems);
// Create a list.
CMyObList ObList;
// Add all the items to the list.
int i;
CMyObj* pObj;
for (i=0; i<iCount; i++) {
pObj = (CMyObj*) m_wndList.GetItemData(pItems[i]);
ObList.Append(pObj);
}
// Done with the item list.
delete pItems;
// Create a memory-file-based archive.
CSharedFile mf (GMEM_MOVEABLE|GMEM_DDESHARE|GMEM_ZEROINIT);
CArchive ar(&mf, CArchive::store);
ObList.Serialize(ar);
ar.Close(); // Flush and close.
HGLOBAL hMem = mf.Detach();
if (!hMem) return;
// Nuke the list but not the objects.
ObList.RemoveAll();
// Send the Clipboard the data.
OpenClipboard();
EmptyClipboard();
SetClipboardData(theApp.m_uiMyListClipFormat, hMem);
CloseClipboard();
}
The number of selected items is used to create a temporary array of selection IDs. The IDs are set into the array by calling GetSelItems. A new CMyObList is created and the pointer to each object is added to the list. Note that we are adding a reference to an object that is in use elsewhere, so it's important that we don't delete these objects by mistake when we're done here.
Once the new CMyObList object has been built, a CSharedFile is created and an archive is created on top of the shared file. We can now serialize the list to the archive and, hence, to the shared file in memory. Once that's done, the memory of the shared file is detached, ready for sending to the Clipboard. The temporary CMyList object has all its items removed, not destroyed. The list object itself is, of course, destroyed when the function exits. The Clipboard is opened, emptied, and set with the data from the shared file. The memory block now becomes the property of the Clipboard. Finally, the Clipboard is closed.
If you use the Clipboard viewer after copying a list to the Clipboard, you'll see an item from your private format. You can't view the data, of course, because the Clipboard has no way of understanding it. You can add that capability for owner-drawn Clipboard items if you want the user to be able to see the data in the Clipboard viewer.
The first thing to take care of is enabling the EditPaste command if our data type is in the Clipboard:
void CClipView::OnUpdateEditPaste(CCmdUI* pCmdUI)
{
// See if there is a list available.
OpenClipboard();
UINT uiFmt = 0;
while (uiFmt = EnumClipboardFormats(uiFmt)) {
if (uiFmt == theApp.m_uiMyListClipFormat) {
CloseClipboard();
pCmdUI->Enable(TRUE);
return;
}
}
pCmdUI->Enable(FALSE);
CloseClipboard();
}
Having done that, we can go on to implement the actual paste code. This is very much the reverse of the process used to copy the data to the Clipboard earlier.
void CClipView::OnEditPaste()
{
OpenClipboard();
HGLOBAL hMem = ::GetClipboardData(theApp.m_uiMyListClipFormat);
if (!hMem) {
CloseClipboard();
return;
}
// Create a mem file.
CSharedFile mf;
mf.SetHandle(hMem);
// Create the archive and get the data.
CArchive ar(&mf, CArchive::load);
CMyObList PasteList;
PasteList.Serialize(ar);
ar.Close();
mf.Detach();
CloseClipboard();
// Add all the objects to the doc.
CClipDoc* pDoc = GetDocument();
ASSERT(pDoc);
CMyObList* pObList = pDoc->GetObList();
ASSERT(pObList);
ASSERT(pObList->IsKindOf(RUNTIME_CLASS(CMyObList)));
POSITION pos = NULL;
// Remove each of the CMyObj objects from the paste list
// and append them to the list.
while (! PasteList.IsEmpty()) {
CMyObj* pObj = PasteList.RemoveHead();
ASSERT(pObj);
ASSERT(pObj->IsKindOf(RUNTIME_CLASS(CMyObj)));
pObList->Append(pObj);
}
pDoc->SetModifiedFlag();
pDoc->UpdateAllViews(NULL);
}
The memory handle from the Clipboard is used to create a shared file that is, in turn, used to create an archive. An object list is created and its Serialize function is used to fill the list. Once that's done, the Clipboard can be closed. Each item in the list is then removed from the list and added to the document. Finally, the views of the document are repainted to reflect the change.
OK, so you're offended. Don't read any further.
Ha! So you're still reading, eh? Well, I hope it's worth it. This is the gratuitous part of the article, which really hasn't got anything to do with Clipboards or owner-drawn list boxes at all. It's just an easy way to draw a bitmap both as a complete rectangle and as an irregular shape, rather like one might make an icon. The color shown in the top-left pixel of the bitmap is used to define which areas of the image will be transparent. Figure 3 shows the bitmap used for the smiley face used in the list box example above.
Figure 3. The smiley face bitmap being edited in App Studio
As you can see from Figure 1, the green areas are not drawn when the DrawTrans function is used to draw the bitmap. Therefore, you can create a bitmap of any irregular shape, so long as you reserve just one color for the transparent areas. The top-left pixel of the image must be painted with the transparent color.
Let's look at the CTransBmp class, which makes drawing bitmaps so simple. Here's the header from TRANSBMP.H:
class CTransBmp : public CBitmap
{
public:
CTransBmp();
~CTransBmp();
void Draw(HDC hDC, int x, int y);
void Draw(CDC* pDC, int x, int y);
void DrawTrans(HDC hDC, int x, int y);
void DrawTrans(CDC* pDC, int x, int y);
int GetWidth();
int GetHeight();
private:
int m_iWidth;
int m_iHeight;
HBITMAP m_hbmMask; // Handle to mask bitmap
void GetMetrics();
void CreateMask(HDC hDC);
};
CTransBmp is derived from CBitmap, so it inherits all the properties of the CBitmap class. I added the GetWidth and GetHeight members because they are tremendously useful in dealing with bitmap objects. The Draw and DrawTrans functions draw the image to either a Windows DC handle (HDC) or an MFC CDC object for flexibility. Draw draws the bitmap as a solid image; DrawTrans draws it with transparent areas defined by the top-left pixel color.
Let's start with the constructor and destructor and go through all the functions:
CTransBmp::CTransBmp()
{
m_iWidth = 0;
m_iHeight = 0;
m_hbmMask = NULL;
}
CTransBmp::~CTransBmp()
{
}
The constructor sets the internal height and width variables to zero. It also sets the mask handle to NULL. (We'll see what the mask does later.) Note that when a CBitmap object is created, it does not contain a Windows graphics device interface (GDI) bitmap object. This is added later, so the width and height can't be determined when the CTransBmp object is constructed.
Once a bitmap is loaded, the height and width can be determined by a helper function:
void CTransBmp::GetMetrics()
{
// Get the width and height.
BITMAP bm;
GetObject(sizeof(bm), &bm);
m_iWidth = bm.bmWidth;
m_iHeight = bm.bmHeight;
}
Now we can see how the GetWidth and GetHeight functions work:
int CTransBmp::GetWidth()
{
if ((m_iWidth == 0) || (m_iHeight == 0)){
GetMetrics();
}
return m_iWidth;
}
int CTransBmp::GetHeight()
{
if ((m_iWidth == 0) || (m_iHeight == 0)){
GetMetrics();
}
return m_iHeight;
}
If the width or height are currently zero, the internal GetMetrics function attempts to retrieve them and cache them.
Now let's see how the bitmap is drawn as a solid block:
void CTransBmp::Draw(HDC hDC, int x, int y)
{
ASSERT(hDC);
// Create a memory DC.
HDC hdcMem = ::CreateCompatibleDC(hDC);
// Select the bitmap into the mem DC.
HBITMAP hbmold =
(HBITMAP)::SelectObject(hdcMem,
(HBITMAP)(m_hObject));
// Blt the bits.
::BitBlt(hDC,
x, y,
GetWidth(), GetHeight(),
hdcMem,
0, 0,
SRCCOPY);
::SelectObject(hdcMem, hbmold);
::DeleteDC(hdcMem);
}
A memory DC is created, and the bitmap selected into it. Note that m_hObject is a member of the CBitmap class and holds the handle to the GDI bitmap object. Once the bitmap is selected into the memory DC, we can copy the bitmap image by calling BitBlt. The memory DC is then tidied up and deleted.
OK so far. Now for the interesting bit—drawing the bitmap with transparent areas. We do this by creating a mask that determines which bits to copy and which bits to leave out. The mask will be made from a monochrome bitmap, using another helper function.
void CTransBmp::CreateMask(HDC hDC)
{
// Nuke any existing mask.
if (m_hbmMask) {
::DeleteObject(m_hbmMask);
}
// Create memory DCs to work with.
HDC hdcMask = ::CreateCompatibleDC(hDC);
HDC hdcImage = ::CreateCompatibleDC(hDC);
// Create a monochrome bitmap for the mask.
m_hbmMask = ::CreateBitmap(GetWidth(),
GetHeight(),
1,
1,
NULL);
// Select the mono bitmap into its DC.
HBITMAP hbmOldMask = (HBITMAP)::SelectObject(hdcMask, m_hbmMask);
// Select the image bitmap into its DC.
HBITMAP hbmOldImage = (HBITMAP)::SelectObject(hdcImage, m_hObject);
// Set the transparency color to be the top-left pixel.
::SetBkColor(hdcImage, ::GetPixel(hdcImage, 0, 0));
// Make the mask.
::BitBlt(hdcMask,
0, 0,
GetWidth(), GetHeight(),
hdcImage,
0, 0,
SRCCOPY);
// Tidy up.
::SelectObject(hdcMask, hbmOldMask);
::SelectObject(hdcImage, hbmOldImage);
::DeleteDC(hdcMask);
::DeleteDC(hdcImage);
}
Memory DCs are created for both the image and the mask. A monochrome bitmap for the mask is created the same size as the image bitmap. The image and mask bitmaps are selected into their respective DCs. Now comes the clever bit. The background color of the image DC is set to be the pixel color of the top-left pixel of the image, and BitBlt is called. The current background color controls what happens when a color DC is bltted to a monochrome DC. The bits from the color DC that match the background color are copied to the monochrome DC as white pixels (1). The color DC pixels that don't match the background color are copied to the monochrome DC as black pixels (0). So now we have a monochrome mask that defines the transparent areas of the image.
Let's see now how the image is drawn transparently using the mask:
void CTransBmp::DrawTrans(HDC hDC, int x, int y)
{
ASSERT(hDC);
if (!m_hbmMask) CreateMask(hDC);
ASSERT(m_hbmMask);
int dx = GetWidth();
int dy = GetHeight();
// Create a memory DC to which to draw.
HDC hdcOffScr = ::CreateCompatibleDC(hDC);
// Create a bitmap for the off-screen DC that is really
// color-compatible with the destination DC.
HBITMAP hbmOffScr = ::CreateBitmap(dx, dy,
(BYTE)GetDeviceCaps(hDC, PLANES),
(BYTE)GetDeviceCaps(hDC, BITSPIXEL),
NULL);
// Select the buffer bitmap into the off-screen DC.
HBITMAP hbmOldOffScr = (HBITMAP)::SelectObject(hdcOffScr, hbmOffScr);
// Copy the image of the destination rectangle to the
// off-screen buffer DC, so we can play with it.
::BitBlt(hdcOffScr, 0, 0, dx, dy, hDC, x, y, SRCCOPY);
// Create a memory DC for the source image.
HDC hdcImage = ::CreateCompatibleDC(hDC);
HBITMAP hbmOldImage = (HBITMAP)::SelectObject(hdcImage, m_hObject);
// Create a memory DC for the mask.
HDC hdcMask = ::CreateCompatibleDC(hDC);
HBITMAP hbmOldMask = (HBITMAP)::SelectObject(hdcMask, m_hbmMask);
// XOR the image with the destination.
::SetBkColor(hdcOffScr,rgbWhite);
::BitBlt(hdcOffScr, 0, 0, dx, dy ,hdcImage, 0, 0, DSx);
// AND the destination with the mask.
::BitBlt(hdcOffScr, 0, 0, dx, dy, hdcMask, 0,0, DSa);
// XOR the destination with the image again.
::BitBlt(hdcOffScr, 0, 0, dx, dy, hdcImage, 0, 0, DSx);
// Copy the resultant image back to the screen DC.
::BitBlt(hDC, x, y, dx, dy, hdcOffScr, 0, 0, SRCCOPY);
// Tidy up.
::SelectObject(hdcOffScr, hbmOldOffScr);
::SelectObject(hdcImage, hbmOldImage);
::SelectObject(hdcMask, hbmOldMask);
::DeleteObject(hbmOffScr);
::DeleteDC(hdcOffScr);
::DeleteDC(hdcImage);
::DeleteDC(hdcMask);
}
There are lots of DCs and lots of bitmaps here. These are the essential steps:
Create a memory DC with a bitmap the same size as the image. We can use construct the final image in this memory DC. (Without this buffer, the screen would flicker as we drew the bitmap.)
Copy the current state of the screen DC to the buffer.
XOR the image of the bitmap with the buffer.
Use the mask to make a black area in the buffer where we want the nontransparent bits of the image to be by ANDing it with the buffer.
XOR the image with the buffer again. Note that 2 XORs together do nothing, so the areas of the buffer that were unmodified by Step 4 are now set back to the way they were before Step 3. The areas altered to black by the masking operation in Step 4 are now converted to the colors of the image.
Copy the final image from the buffer back to the screen DC to make it visible.
The key point to this process is that the main drawing operations (Steps 3, 4, and 5) are done to an off-screen buffer, so the screen image doesn't flicker as the bitmap is drawn.
So now you've seen how to add an owner-drawn list box to your application's view, get mouse events from the list box, draw data in the list box, copy data to and from the Clipboard, and draw pretty bitmaps, too. How's that for a set of tips?