65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.74/5 (8投票s)

2007年1月21日

CPOL

4分钟阅读

viewsIcon

93957

downloadIcon

15313

本文演示了如何利用图像和继承的力量,为您的Windows应用程序实现“皮肤化”外观。

引言

该库承诺通过利用图像、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()来更新相应的“外观”。

一些好处...

如果设计得当,您可以为您的应用程序设计平行的“主题”;基本上,使用不同的图像集覆盖您的应用程序和其中的控件,并使用配置文件轻松切换。

© . All rights reserved.