How Can I Add Custom Background Wallpaper to my UI?

Improve the appeal of your MFC-based app by adding custom background wallpaper to dialogs and forms


August 21, 2002
URL:http://www.drdobbs.com/how-can-i-add-custom-background-wallpape/184416746

How Can I Add Custom Background Wallpaper to my UI?


Under MS-DOS, the freewheeling nature of application presentation meant that every developer had to be both a programmer and a UI designer. With Windows 3.0, many of the UI components became just that—components. Once the programmer API to the controls was learned, the knowledge could be leveraged across many application projects, and the developer would be assured that the end-user would already understand basic application navigation and interaction.

The downside to all of this uniformity was obvious—it made for some really boring user interfaces. I mean, after you’ve seen one gray dialog with tons of controls, you’ve them all. I recall seeing many applications consisting of banks of controls in scores of modal dialogs that forced the user into a unnatural data entry model where something more flexible (and less standard) like a spreadsheet interface might have done the job better. It seemed that many developers had learned the lessons of MS-DOS all too well.

However, in some isolated quarters of the development community, a few brave souls began experimenting with radical new ways to presenting the interface. Rather than standardize on a common look-and-feel across all applications, they created highly custom and graphical interfaces that mimicked what a real-world device might look like. Modern examples abound including many MP3 players, groupware such as Groove, instant messengers such as Odigo, and the like. Some of these interfaces have gone so far as to use nonrectangular windows, embedded interface gizmos that pop out when hot spot areas of the interface are clicked on and provision for a high level of end-user customization. This is often referred to as “skinning” and it’s a hot topic for developers looking for a new way to present their application in an appealing way. In a future article, we’ll look into how skinning can be accomplished in a modern MFC application.

This month, we’ll take a look at one kind of customization that can be easily and quickly done to many applications that have a dialog or form driven interface—adding custom background wallpaper to dialogs and forms.

First off, the code is in MFC. However, none of the techniques are restricted to MFC. They can be applied in a similar fashion if you want to use the raw SDK in C, WTL if you prefer the ATL way of doing things, Visual Basic, and so on.

Let’s start off with a class that derives from the MFC class CFormView:

class CSpicyFormView : public CFormView
{
public:
	CSpicyFormView(const int dlgID);

protected:
	// Generated message map functions
	//{{AFX_MSG(CSpicyFormView)

	// handler for WM_ERASEBKGND
	afx_msg BOOL OnEraseBkgnd(CDC* pDC);

	// handler for WM_CTLCOLOR
	afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);

	//}}AFX_MSG
	DECLARE_MESSAGE_MAP()

private:
	// the bitmap we will use for the background.
	CBitmap m_bmpBkgrd;

	// the foreground color that matches the colors of the bitmap
	COLORREF m_clrForeground;

	// the background color that matches the colors of the bitmap
	COLORREF m_clrBackground;

	// a brush that uses the background color.
	CBrush m_brushBackground;
};

We’ve specified handlers for two key window messages—WM_ERASEBKGND and WM_CTLCOLOR. The first message is sent to a window when it “invalidates” or otherwise becomes out-of-date and needs refreshing. The second message is sent by child windows (often controls) to determine what colors they should use for their foreground, background, and how they should erase themselves. Ordinarily, these messages are not handled in a CFormView derivation—they simply fall to the default CWnd::DefWindowProc method. In our case, we want to override the default handling and replace it with new code.

Next, we’ll look at the implementation of the first method OnEraseBkgnd():

BEGIN_MESSAGE_MAP(CSpicyFormView, CFormView)
	//{{AFX_MSG_MAP(CSpicyFormView)
	ON_WM_ERASEBKGND()
	ON_WM_CTLCOLOR()
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()

CSpicyFormView::CSpicyFormView(const int dlgID) : CFormView( dlgID )
{
	// load our bitmap from resource.  this could be passed in as a
// parameter or otherwise initialized.
	m_bmpBkgrd.LoadBitmap( IDB_FORM );

// assign the foreground & background colors.  Override these 
// colors with appropriate choices for your bitmap.
	m_clrForeground = RGB(255,0,0);
	m_clrBackground = RGB(0,255,0);

	// this brush will be used by child windows when they need to
// paint their backgrounds.  Call any of the CreateBrush methods 
// on CBrush with the appropriate colors.
	m_brushBackground.CreateSolidBrush( m_clrBackground );
}

BOOL CSpicyFormView::OnEraseBkgnd(CDC* pDC)
{
	// Get background bitmap and dimensions.
	SIZE sz;
	BITMAP b;
	m_bmpBkgrd.GetBitmap( &b );
	sz.cx = b.bmWidth;
	sz.cy = b.bmHeight;

	// get the client area to paint.
  	CRect rect;
  	GetClientRect(&rect);    

	// get the bitmap and create a suitable memory device context for
// it.
	CDC dcBmp;
	dcBmp.CreateCompatibleDC( pDC );
	CBitmap* bmpCurrentBkgrd = dcBmp.SelectObject( &m_bmpBkgrd ); 

	// create a device context to write on.  this is faster than 
// writing to the screen directly.
	CDC dcMem;
	dcMem.CreateCompatibleDC( pDC );

	// create a bitmap for the memory device context large enough to 
// handle the client area of the window.
	CBitmap dcMemBmp;
	dcMemBmp.CreateCompatibleBitmap(pDC, rect.Width(),rect.Height());
	CBitmap* bmpOldMemDC = dcMem.SelectObject( &dcMemBmp );

	// determine where the invalidated rectangle is that we need to 
// update in comparison to the size of the bitmap.  We want to be
// sure to adjust to account for a rectangle that covers only a 
// partial amount of the bitmap.  This is important for bitmaps 
// with patterns that would appear incorrectly if not properly 
// aligned.
	int offsetwidth = rect.left - (rect.left % sz.cx);
	int offsetheight = rect.top - (rect.top % sz.cy );

	// blit (copy) the bitmap onto the memory device context by moving 
// across and the down the rectangle.  Notice how we adjust for 
// placement of the bitmap to account for any needed cropping.
	for( int x=offsetwidth;x(rect.right;x+=sz.cx )
		for( int y=offsetheight;y(rect.bottom;y+=sz.cy )
		  	dcMem.BitBlt(x, y, x+sz.cx > rect.right ? 
rect.right-x : sz.cx, 
			x+sz.cy > rect.bottom ?
rect.bottom-y : sz.cy,
						&dcBmp,
						0,0,
		  				SRCCOPY);

	// blit (copy) the memory dc onto the screen dc.
	pDC->BitBlt( offsetwidth, offsetheight, rect.right, rect.bottom,
&dcMem, 0, 0, SRCCOPY );

	// clean up the bitmaps from the dc's.
	dcMem.SelectObject( bmpOldMemDC );
	dcBmp.SelectObject( bmpCurrentBkgrd );

	return TRUE;
}

Much of this code is straightforward use of device contexts. However, the logic to properly crop the bitmap to account for invalidated rectangles that overlap individual bitmaps is the most critical part of this algorithm. For kicks, create a bitmap with a pattern. It doesn't have to be fancy. Then change the code that initializes the variables offsetwidth and offsetheight to be equal to rect.left and rect.top, respectively. Once the code is running, move other windows across the top of the window in question. You should see how the bitmap pattern gets corrupted over time as the algorithm no longer correctly adjusts for needed cropping.

You might be wondering how big to make the bitmap, particularly if it uses a regular pattern. There's no set rule on its size, however I would suggest a bitmap of at least 16 pixels by 16 pixels. A bitmap smaller than this just eats up processor time when painting because CDC::BitBlt has to be called more times. The downside to a very large bitmap is the amount of memory it takes up. Experiment with different size bitmaps to see the tradeoff between memory usage and painting speed If you have a bitmap (such as a photograph) that doesn't lend itself to a regular pattern, you might have to just make it as big as the window so it doesn't get tiled as the updating code executes. An alternative would be to look at the different options on CDC::BitBlt and possibly consider stretching the image if appropriate.

Next, we'll look at the implementation for OnCtlColor():

HBRUSH CSpicyFormView::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
    switch (nCtlColor) 
    	{
	case CTLCOLOR_EDIT:
		{
      	pDC->SetTextColor(m_clrForeground);
	       pDC->SetBkColor(m_clrBackground);
		return (HBRUSH)( m_brushBackground.GetSafeHandle());
		}

	case CTLCOLOR_BTN:
		{
        	pDC->SetTextColor(m_clrForeground);
pDC->SetBkColor(m_clrBackground);
		return (HBRUSH)( m_brushBackground.GetSafeHandle());
		}

	case CTLCOLOR_LISTBOX:
		{
        	pDC->SetTextColor(m_clrForeground);
		pDC->SetBkColor( m_clrBackground);
		return (HBRUSH)( m_brushBackground.GetSafeHandle());
		}

	case CTLCOLOR_STATIC:
		{
        	pDC->SetTextColor(m_clrForeground);
pDC->SetBkColor(m_clrBackground);
		return (HBRUSH)( m_brushBackground.GetSafeHandle());
		}

    	default:
           	return CFormView::OnCtlColor(pDC, pWnd, nCtlColor);
	}
}

For each type of child control, we specify what kind of colors it should use in painting itself. This logic only works for those controls that issue a WM_CTLCOLOR message to its parent. Not all child controls do this, particularly custom controls written by third parties. Often they have a specific API that the developer calls to set these colors. These controls would be handled on a dialog-by-dialog basis, but would still use the colors specified by the parent dialog. For example, a spreadsheet control would be initialized in the CFormView::OnInitialUpdate() of its parent with the required colors by sending appropriate messages to the spreadsheet control.

Lastly, some controls need to appear as though they have no background and instead are just painted directly on the parent window. An example is static text-although the text control has a background (since it's a window) it usually appears to the user as though the text itself was just drawn onto the parent window. The background color of the text is most often the same as the background color for the parent window and thus just disappears from view. However, in the case of a bitmap background for the parent, the text control would appear odd if it painted a flat color as its background-the text would have a rectangular border with a color that would appear visually distinct from the surrounding parent bitmap background. The solution here is one of two choices-either mark the control as being transparent (meaning that it does not erase its background in the first place), or subclass the control and override its handling of the WM_ERASEBKGND message. The former is easier but may not properly refresh an invalidated control in all cases while the latter is more work but guaranteed to work properly. You'll have to experiment to see which works better for you.


Mark M. Baker is the Chief of Research & Development at BNA Software located in Washington, D.C. Send your Windows development questions to [email protected].

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.