“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()来更新相应的“外观”。一些好处...
如果设计得当,您可以为您的应用程序设计平行的“主题”;基本上,使用不同的图像集覆盖您的应用程序和其中的控件,并使用配置文件轻松切换。


