Skinned UI control library (VCpp)
Skinned UI control library (VCpp)
com/Articles/17271/Skinned-UI-control-library-VC
bigb_602
27 Mar 2007 1
This article demonstrates how to leverage the power of images and inheritance to achieve a skinned look for
your Windows applications.
Download source files - 35.3 KB
Download demo project images - 322 KB
Download required files - 1.02 MB
Download demo application (EXE) (ideal resolution 1024 x 768) - 0.97 MB
Download demo application project - 2.17 MB
Introduction
The library promises to achieve (with minor modifications - to make it very generic) a non-Windows UI look-n-
feel for those who want to develop a customized UI (curves et al) by leveraging the power of images, GDI,
composition, and multiple inheritance.
Inspired by...
A few years back, when I first saw the cool skins on Winamp (MP3 player), I felt excited and challenged to
write a library to use in all my future development that would shock people who believe that slick UI is only
feasible in web applications and Flash applications!
Architectural details...
The idea was to store as much common functionalities as possible in one class (CSkinControl) and then
consume it, through inheritance, in concrete control classes. The base class holds references to four different
images (IDs), one for normal state, one for disabled state, one for hover state, and one for pressed state. The
function that stores this is SetImageResources(normal, hover, pressed, disabled). The base class also
contains functionality for:
The most important function is UpdateMemoryDC() which takes care of drawing and updating the visual of
each control on screen, whether in default state or triggered by some user-action (mouse events).
C++
Shrink ▲
// This function attempts to load image
// resources from a DLL and renders the same on the screen
int CSkinControl::UpdateMemoryDC()
{
HBITMAP hBitmap = NULL;
BITMAP bmpTemp;
// If gifs are the preferred resources, use conversion
#ifdef USE_GIF_IMAGES
hBitmap = LoadGIF(GetDllInstance((LPCTSTR)m_csDLLFileName),
MAKEINTRESOURCE(GetID()));
#else
hBitmap = LoadBitmap(GetDllInstance((LPTSTR)(LPCTSTR)m_csDLLFileName),
MAKEINTRESOURCE(GetID()));
2
#endif
if(hBitmap != NULL)
{
::GetObject(hBitmap, sizeof(BITMAP), &bmpTemp);
m_lImageWidth = bmpTemp.bmWidth;
m_lImageHeight = bmpTemp.bmHeight;
::SelectObject(m_dcMemory.GetSafeHdc(),hBitmap);
}
// If the object is of text type (edit)
else if(m_nPressedID == -1 && m_nUnPressedID == -1 && m_nHoverID == -1)
{
m_dcMemory.SetTextColor(m_crTextColor);
m_dcMemory.DrawText(m_csText, CRect(0, 0, m_nWidth, m_nHeight), DT_CENTER);
}
return 0;
}
Concrete classes provide functionalities that are required by their standard counterparts. For
example, CSkinnedEdit supports text selection, insertion, deletion (no copy-paste implemented - sorry!!), and
other customized features like "read-only", "decimal point validation", etc.
Similarly, CSkinnedScrollBar provides functionality to set minimum range, maximum range, retrieve position of
scrollbar button, and so on. The code and function names are quite self-explanatory. I apologize for not
providing many inline code comments, for which you can always contact me.
All the controls are created dynamically. Each one of them has a function CreateSkinControl(name, rect,
parent, id, flags) that takes parameters as mentioned. The last one (flags) is an interesting parameter that
holds any "extra" information required (as you'll see in different controls) for creation. As an example, shown
below is the creation code for CSkinnedButton control:
C++
Shrink ▲
BOOL CSkinnedButton::CreateSkinControl(LPCTSTR lpszWindowName, LPRECT lpRect,
CWnd *pParentWnd, UINT nControlID, long lFlags)
{
// Set windows name, location, size, parent, and control id
m_csText = lpszWindowName;
m_nLeft = lpRect->left;
m_nTop = lpRect->top;
m_nWidth = lpRect->right - lpRect->left;
m_nHeight = lpRect->bottom - lpRect->top;
m_pParentWnd = pParentWnd;
m_nControlID = nControlID;
3
m_lButtonType = lFlags;
return TRUE;
}
return FALSE;
}
Steps to implement...
In a window/dialog that you want to use a control (button, for example), define a member variable that is a
pointer to the control.
C++
CSkinnedButton* m_pOkButton;
Inside the creation logic of the dialog OnCreate() or OnInitDialog(), insert the creation logic of the button, after
a few initializations of creating a memory DC for background painting.
C++
Shrink ▲
int CMyDialog::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if(CDialog::OnCreate(lpCreateStruct) == -1)
{
return -1;
}
CClientDC dc(this);
m_memDC.CreateCompatibleDC(&dc);
m_memBmp.CreateCompatibleBitmap(&dc, 1024, 768);
m_memDC.SelectObject(&m_memBmp);
// Other code
...
// Create button
m_pOkButton = new CSkinnedButton;
// Assign 4 image ids
m_pOkButton.SetImageResource(ID_NORMAL, ID_HOVER, ID_PRESSED, ID_DISABLED);
// This flag (true) suggests that the button
// is an irregular shaped, which will be drawn using
// a transparency algorithm to achieve the desired result
4
m_pOkButton.SetShapedFlag(TRUE);
// Other code
...
}
The custom code for button creation and button rendering is implemented in the CSkinnedButton class as
shown:
C++
Shrink ▲
int CSkinnedButton::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CWnd::OnCreate(lpCreateStruct) == -1)
return -1;
CClientDC dc(this);
CBitmap bmpTemp;
m_dcMemory.CreateCompatibleDC(&dc);
if(PrepareFont())
{
}
UpdateMemoryDC();
return 0;
}
5
int CSkinnedButton::UpdateMemoryDC()
{
BITMAP bmpTemp;
memset(&bmpTemp, 0, sizeof(BITMAP));
if(m_dcMemory == NULL)
{
return -1;
}
#ifdef USE_GIF_IMAGES
if(m_hBitmap != NULL && m_hBitmap == GetCurrentStateBitmap())
{
return -1;
}
m_hBitmap = GetCurrentStateBitmap();
#else
hBitmap = GetCurrentStateBitmap();
#endif
if(m_hBitmap != NULL)
{
::GetObject(m_hBitmap, sizeof(BITMAP), &bmpTemp);
m_lImageWidth = bmpTemp.bmWidth;
m_lImageHeight = bmpTemp.bmHeight;
::SelectObject(m_dcMemory.GetSafeHdc(),m_hBitmap);
}
else if(m_nPressedID == -1 && m_nUnPressedID == -1 && m_nHoverID == -1)
{
CClientDC dc(this);
m_dcMemory.SetMapMode(dc.GetMapMode());
m_dcMemory.SetWindowExt(dc.GetWindowExt());
m_dcMemory.SetViewportExt(dc.GetViewportExt());
m_dcMemory.SetWindowOrg(0, 0);
CBitmap cbmpTemp;
if(m_dcMemory.SelectObject(&cbmpTemp) != NULL)
{
m_dcMemory.FillSolidRect(0, 0, m_nWidth, m_nHeight,
GetCurrentBackgroundColor());
}
}
FindControlEdge() (not a very intuitive name!) implements the transparency algorithm, using a Magenta color
mask, traversing through the image, and cutting out a region. You might argue that why not use the GDI
function TransparentBlt() to achieve the same. Good point! However, when I tried to implement
using TransparentBlt, it failed to run in Windows 98 SE (although MS claims to have support in that version of
Windows!). Anyways, may be I didn't have the correct patch of Windows or SDK at the time. I decided to write
my own. You have a choice of using TransparentBlt which would promise an optimized performance over my
technique for sure ;)
Also, my technique introduces a strict requirement of having all images bound by a four pixel magenta
background!!! Example:
Those who might be facing a similar problem of TransparentBlt() are free to use the algorithm shown here or
one of your own.
C++
Shrink ▲
// This function traverses through an image and creates
// a region eliminating "magenta" pixels and sets it to the window handle
BOOL FindControlEdge(CWnd* pWnd, CDC *dcControl,
COLORREF colToSkip, HRGN &hRgn)
{
int nCurrentX = 0;
int nCurrentY = 0;
int nTempX = 0;
int nTempY = 0;
BOOL bStop = FALSE;
int nDirection = 0;
int nCurDirection = 0;
int nFirstX = 0;
int nFirstY = 0;
int nXMap = 0;
int nYMap = 0;
int nIterate = 0;
POINT ptTempCoord;
CList<point,> ptCoord;
CRect rcWindow(0,0,0,0);
CRect rcClient(0,0,0,0);
pWnd->GetWindowRect(&rcWindow);
pWnd->GetClientRect(&rcClient);
pWnd->ClientToScreen(&rcClient);
nXMap = rcClient.left - rcWindow.left;
nYMap = rcClient.top - rcWindow.top;
nIterate = 0;
7
bStop = FALSE;
nCurrentX = 0;
nCurrentY = 0;
nDirection = SOUTHEAST;
nFirstX = 0;
nFirstY = 0;
while(!bStop)
{
if((dcControl->GetPixel(nCurrentX+1, nCurrentY+1)) != colToSkip)
{
bStop = TRUE;
bStop = FALSE;
while(!bStop)
{
nIterate++;
switch(nDirection)
{
case SOUTHEAST:
if((dcControl->GetPixel(nCurrentX+1, nCurrentY+1)) != colToSkip)
{
nDirection = EAST;
continue;
}
else
{
nCurrentX++;
nCurrentY++;
}
break;
case EAST:
if((dcControl->GetPixel(nCurrentX+1, nCurrentY)) != colToSkip)
{
nDirection = NORTHEAST;
continue;
}
else
8
{
nCurrentX++;
}
break;
case NORTHEAST:
if((dcControl->GetPixel(nCurrentX+1, nCurrentY-1)) != colToSkip)
{
nDirection = NORTH;
continue;
}
else
{
nCurrentX++;
nCurrentY--;
}
break;
case NORTH:
if((dcControl->GetPixel(nCurrentX, nCurrentY-1)) != colToSkip)
{
nDirection = NORTHWEST;
continue;
}
else
{
nCurrentY--;
}
break;
case NORTHWEST:
if((dcControl->GetPixel(nCurrentX-1, nCurrentY-1)) != colToSkip)
{
nDirection = WEST;
continue;
}
else
{
nCurrentX--;
nCurrentY--;
}
break;
case WEST:
if((dcControl->GetPixel(nCurrentX-1, nCurrentY)) != colToSkip)
{
nDirection = SOUTHWEST;
continue;
}
else
{
nCurrentX--;
}
break;
case SOUTHWEST:
if((dcControl->GetPixel(nCurrentX-1, nCurrentY+1)) != colToSkip)
{
nDirection = SOUTH;
continue;
9
}
else
{
nCurrentX--;
nCurrentY++;
}
break;
case SOUTH:
if((dcControl->GetPixel(nCurrentX, nCurrentY+1)) != colToSkip)
{
nDirection = SOUTHEAST;
continue;
}
else
{
nCurrentY++;
}
break;
}
nCurDirection = nDirection;
10
POINT ptTemp;
if(ptCoord.GetCount() > 0)
{
ptTemp = ptCoord.GetTail();
}
else
{
ptTemp.x = 0;
ptTemp.y = 0;
}
switch (nCurDirection)
{
case NORTH:
case NORTHWEST:
nTempX++;
break;
case NORTHEAST:
case EAST:
nTempY++;
break;
}
ptTempCoord.x = nTempX;
ptTempCoord.y = nTempY;
ptCoord.AddTail(ptTempCoord);
}
POINT *ptAll;
delete []ptAll;
if(hRgn != NULL)
{
if(pWnd->SetWindowRgn(hRgn, TRUE) != 0)
{
return TRUE;
}
}
return FALSE;
}
Finally, you implement message handlers to react to control events and messages (LButtonDown/Up for
Button, OnChar for Edit, and so on), and appropriately play with different control states (normal, hover,
disabled, etc.) and updating of corresponding "look" by calling UpdateMemoryDC().
Some benefits...
If you design properly, you can come up with parallel "themes" for your application; basically, different sets of
images to super-impose on your application and controls within, and switch easily, using configuration files.
12