“Skin”UI 控件库 (VC++)






3.74/5 (8投票s)
本文演示了如何利用图像和继承的力量,为您的Windows应用程序实现“皮肤化”外观。
- 下载源文件 - 35.3 KB
- 下载演示项目图片 - 322 KB
- 下载所需文件 - 1.02 MB
- 下载演示应用程序 (EXE) (理想分辨率 1024 x 768) - 0.97 MB
- 下载演示应用程序项目 - 2.17 MB
引言
该库承诺通过利用图像、GDI、组合和多重继承的力量,实现非Windows UI的外观和感觉(通过少量修改即可使其非常通用),适合那些想要开发自定义UI(例如圆角等)的用户。
灵感来源...
几年前,当我第一次看到Winamp(MP3播放器)的酷炫皮肤时,我感到兴奋和挑战,想要编写一个库,用于我未来的所有开发中,让那些认为炫酷UI只存在于Web应用程序和Flash应用程序中的人们感到震惊!
库中包含什么?
该库由以下类组成
CSkinControl
- 所有控件的父类,包含通用功能CSkinnedStatic
- 自定义类,用作静态控件或标签- 继承自:
Cwnd
,CSkinControl
CSkinnedButton
- 自定义类,用作按钮控件- 继承自:
Cwnd
,CSkinControl
CSkinnedEdit
- 自定义类,用作编辑控件- 继承自:
Cwnd
,CSkinControl
CSkinnedComboBox
- 自定义类,用作组合框控件- 继承自:
Cwnd
,CSkinControl
- 由以下类组成:
CSkinnedEdit
,CSkinnedButton
, 和CSkinnedListBox
CSkinnedListBox
- 自定义类,用作列表框控件- 继承自:
Cwnd
,CSkinControl
- 由以下类组成:
CSkinnedButton
CSkinnedScrollBar
- 自定义类,用作滚动条控件- 继承自:
Cwnd
,CSkinControl
- 由以下类组成:
CSkinnedButton
CSkinnedSliderCtrl
- 自定义类,用作滑块控件- 继承自:
Cwnd
,CSkinControl
- 由以下类组成:
CSkinnedButton
架构细节...
我们的想法是将尽可能多的通用功能存储在一个类(CSkinControl
)中,然后通过继承在具体的控件类中使用。基类保存对四个不同图像(ID)的引用,一个用于正常状态,一个用于禁用状态,一个用于悬停状态,一个用于按下状态。存储此信息的函数是SetImageResources(normal, hover, pressed, disabled)
。基类还包含以下功能:
- 位置和大小
SetCoordinates(left, top)
SetDimensions(width, height)
GetLeft()
GetTop()
GetWidth()
GetHeight()
- 颜色和字体
GetCurrentBackgroundColor()
GetTextColor()
GetBackgroundColor(state)
SetBackgroundColor(state, color)
SetForegroundColor(color)
SetTextColor(color)
SetFontName(name)
SetFontStyle(style)
SetFontSize(size)
GetFontName()
GetFontStyle()
GetFontSize()
最重要的函数是UpdateMemoryDC()
,它负责在屏幕上绘制和更新每个控件的视觉效果,无论是默认状态还是由用户操作(鼠标事件)触发的状态。
// 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()));
#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;
}
具体类提供了标准对应控件所需的功能。例如,CSkinnedEdit
支持文本选择、插入、删除(未实现复制粘贴 - 抱歉!),以及其他自定义功能,如“只读”、“小数验证”等。同样,CSkinnedScrollBar
提供设置最小范围、最大范围、获取滚动条按钮位置等功能。代码和函数名称都相当直观。对于我没有提供太多行内代码注释,我深感抱歉,您可以通过联系我来获取更多信息。
所有控件都是动态创建的。它们每个都有一个函数CreateSkinControl(name, rect, parent, id, flags)
,该函数接受提到的参数。最后一个参数(flags
)是一个有趣的参数,它保存创建所需的任何“额外”信息(正如您在不同控件中看到的)。例如,下面是CSkinnedButton
控件的创建代码:
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;
// Assign a default font and defaut colors
m_csFontName = "Arial";
m_nFontSize = 16;
m_nFontStyle = FONT_NORMAL;
m_crBackgroundColorHover = RGB(255,255,255);
m_crBackgroundColorPressed = RGB(255,255,255);
m_crBackgroundColorUnPressed = RGB(255,255,255);
m_crForegroundColor = RGB(0,0,0);
// Store special button information
m_lButtonType = lFlags;
// If the control is already created, return false
if(m_hWnd != NULL)
{
return FALSE;
}
// Create the control using CWnd::Create() and bring it to the top
// Notice the flag WS_CLIPSIBLINGS; this is necessary
// for proper rendering of composite controls
if(CWnd::Create(NULL, m_csText, WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS,
*lpRect, pParentWnd, nControlID, NULL))
{
CWnd::BringWindowToTop();
return TRUE;
}
return FALSE;
}
实现步骤...
- 在一个您想使用控件(例如按钮)的窗口/对话框中,定义一个指向该控件的指针成员变量。
CSkinnedButton* m_pOkButton;
OnCreate()
或OnInitDialog()
中,在创建用于背景绘制的内存DC的初始步骤之后,插入按钮的创建逻辑。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
m_pOkButton.SetShapedFlag(TRUE);
// Other code
...
}
按钮创建和按钮渲染的自定义代码实现在CSkinnedButton
类中,如下所示:
int CSkinnedButton::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CWnd::OnCreate(lpCreateStruct) == -1)
return -1;
CClientDC dc(this);
CBitmap bmpTemp;
m_dcMemory.CreateCompatibleDC(&dc);
if(bmpTemp.CreateCompatibleBitmap(&dc, m_nWidth, m_nHeight) != 0)
{
m_dcMemory.SelectObject(&bmpTemp);
if(PrepareFont())
{
}
UpdateMemoryDC();
// Create region if irregular shaped
if(m_bShape)
{
m_hRgn = CreateRectRgn(0,0,0,0);
if(m_hRgn != NULL)
{
if(GetWindowRgn(m_hRgn) == ERROR)
{
m_hRgn = NULL;
return -1;
}
}
else
{
return -1;
}
}
}
return 0;
}
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;
cbmpTemp.CreateCompatibleBitmap(&dc, m_nWidth, m_nHeight);
if(m_dcMemory.SelectObject(&cbmpTemp) != NULL)
{
m_dcMemory.FillSolidRect(0, 0, m_nWidth, m_nHeight,
GetCurrentBackgroundColor());
}
}
// This is most important section of code for irregular shapes
if(m_bShape != -1 && m_bFindEdges)
{
m_bFindEdges = FALSE;
FindControlEdge(this, &m_dcMemory, COLOR_MAGENTA, m_hRgnWindow);
}
return 0;
}
FindControlEdge()
(不是一个非常直观的名称!)实现了透明度算法,使用洋红色作为颜色蒙版,遍历图像,并裁剪出某个区域。您可能会争辩说,为什么不使用GDI函数TransparentBlt()
来实现呢。说得好!但是,当我尝试使用TransparentBlt
来实现时,它在Windows 98 SE上运行失败(尽管微软声称那个版本的Windows支持!)。无论如何,也许当时我没有正确的Windows补丁或SDK。我决定自己写。您可以选择使用TransparentBlt
,它肯定会比我的技术提供更优化的性能;)
此外,我的技术对所有图像都需要有四像素洋红色背景的严格要求!!!例如:
那些可能面临类似TransparentBlt()
问题的人,可以自由使用这里展示的算法或您自己的算法。
// 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;
bStop = FALSE;
nCurrentX = 0;
nCurrentY = 0;
nDirection = SOUTHEAST;
nFirstX = 0;
nFirstY = 0;
while(!bStop)
{
if((dcControl->GetPixel(nCurrentX+1, nCurrentY+1)) != colToSkip)
{
bStop = TRUE;
if(nCurrentX == 0 && nCurrentY == 0)
{
return FALSE;
}
}
else
{
nCurrentX++;
nCurrentY++;
}
}
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
{
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;
}
else
{
nCurrentX--;
nCurrentY++;
}
break;
case SOUTH:
if((dcControl->GetPixel(nCurrentX, nCurrentY+1)) != colToSkip)
{
nDirection = SOUTHEAST;
continue;
}
else
{
nCurrentY++;
}
break;
}
nCurDirection = nDirection;
if((dcControl->GetPixel(nCurrentX+1, nCurrentY+1)) != colToSkip)
{
nDirection = SOUTHEAST;
}
if((dcControl->GetPixel(nCurrentX+1, nCurrentY)) != colToSkip)
{
nDirection = EAST;
}
if((dcControl->GetPixel(nCurrentX+1, nCurrentY-1)) != colToSkip)
{
nDirection = NORTHEAST;
}
if((dcControl->GetPixel(nCurrentX, nCurrentY-1)) != colToSkip)
{
nDirection = NORTH;
}
if((dcControl->GetPixel(nCurrentX-1, nCurrentY-1)) != colToSkip)
{
nDirection = NORTHWEST;
}
if((dcControl->GetPixel(nCurrentX-1, nCurrentY)) != colToSkip)
{
nDirection = WEST;
}
if((dcControl->GetPixel(nCurrentX-1, nCurrentY+1)) != colToSkip)
{
nDirection = SOUTHWEST;
}
if((dcControl->GetPixel(nCurrentX, nCurrentY+1)) != colToSkip)
{
nDirection = SOUTH;
}
POINT ptTemp;
if(ptCoord.GetCount() > 0)
{
ptTemp = ptCoord.GetTail();
}
else
{
ptTemp.x = 0;
ptTemp.y = 0;
}
if(nCurrentX != ptTemp.x || nCurrentY != ptTemp.y)
{
nTempX = nCurrentX;
nTempY = nCurrentY;
switch (nCurDirection)
{
case NORTH:
case NORTHWEST:
nTempX++;
break;
case NORTHEAST:
case EAST:
nTempY++;
break;
}
ptTempCoord.x = nTempX;
ptTempCoord.y = nTempY;
ptCoord.AddTail(ptTempCoord);
}
if(nFirstX == 0 && nFirstY == 0)
{
nFirstX = nCurrentX;
nFirstY = nCurrentY;
}
else if(nCurrentX == nFirstX && nCurrentY == nFirstY)
{
break;
}
}
POINT *ptAll;
ptAll = new POINT[ptCoord.GetCount()];
int nLen = ptCoord.GetCount();
for(int idx=0; idx<nLen; idx++)
{
ptAll[idx] = ptCoord.GetHead();
ptCoord.RemoveHead();
}
hRgn = CreatePolygonRgn(ptAll, nLen, ALTERNATE);
delete []ptAll;
if(hRgn != NULL)
{
if(pWnd->SetWindowRgn(hRgn, TRUE) != 0)
{
return TRUE;
}
}
return FALSE;
}
LButtonDown/Up
,编辑框的OnChar
,等等),并相应地处理不同的控件状态(正常、悬停、禁用等),通过调用UpdateMemoryDC()
来更新相应的“外观”。一些好处...
如果设计得当,您可以为您的应用程序设计平行的“主题”;基本上,使用不同的图像集覆盖您的应用程序和其中的控件,并使用配置文件轻松切换。