Click here to Skip to main content
6,596,602 members and growing! (19,382 online)
Email Password   helpLost your password?
Desktop Development » Miscellaneous » General     Intermediate License: The Code Project Open License (CPOL)

"Skinned" UI control library (VC++)

By bigb_602

This article demonstrates how to leverage the power of images and inheritance to achieve a "skinned" look for your Windows applications
VC6Win2K, WinXP, MFC, GDI, Dev
Posted:21 Jan 2007
Updated:27 Mar 2007
Views:35,789
Bookmarked:41 times
Unedited contribution
Announcements
Loading...
 
Search    
Advanced Search
Add to IE Search
printPrint   add Share
      Discuss Discuss   Broken Article?Report  
7 votes for this article.
Popularity: 3.15 Rating: 3.73 out of 5
1 vote, 14.3%
1

2
1 vote, 14.3%
3
1 vote, 14.3%
4
4 votes, 57.1%
5

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...

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 developments that would shock people with belief that slick UI is only feasible in web applications and Flash applications!

What's inside the library?

The library is composed of following classes:

  • CSkinControl (Parent class of all controls, containing common functionality)
  • CSkinnedStatic - Custom class to act as a static control or label
    • Inherits from: Cwnd, CSkinControl
  • CSkinnedButton - Custom class to act as a button control
    • Inherits from: Cwnd, CSkinControl
  • CSkinnedEdit - Custom class to act as an edit control
    • Inherits from: Cwnd, CSkinControl
  • CSkinnedComboBox - Custom class to act as a combo box control
    • Inherits from: Cwnd, CSkinControl
    • Composed of: CSkinnedEdit, CSkinnedButton, and CSkinnedListBox
  • CSkinnedListBox - Custom class to act as a list box control
    • Inherits from: Cwnd, CSkinControl
    • Composed of: CSkinnedButton
  • CSkinnedScrollBar - Custom class to act as a scroll bar control
    • Inherits from: Cwnd, CSkinControl
    • Composed of: CSkinnedButton
  • CSkinnedSliderCtrl - Custom class to act as a slider control
    • Inherits from: Cwnd, CSkinControl
    • Composed of: CSkinnedButton

  • Architectural details...

    The idea was to store as much common functionalities as possible in one class (CSkinControl) and then consume the same, through inheritance, in concrete control classes. The base class holds references to 4 different images (ids), one for normal state, one for disabled state, one for hover state, and one for pressed state. The function that stores the same is SetImageResources(normal, hover, pressed, disabled). The base class also contains functionality for:

  • Location and Size:
    • SetCoordinates(left, top)
    • SetDimensions(width, height)
    • GetLeft()
    • GetTop()
    • GetWidth()
    • GetHeight()
  • Colors and Fonts:
    • GetCurrentBackgroundColor()
    • GetTextColor()
    • GetBackgroundColor(state)
    • SetBackgroundColor(state, color)
    • SetForegroundColor(color)
    • SetTextColor(color)
    • SetFontName(name)
    • SetFontStyle(style)
    • SetFontSize(size)
    • GetFontName()
    • GetFontStyle()
    • GetFontSize()
  • Most important 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).

    // 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;
    }
    

    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 scroll bar 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, mentioned below is the creation code for CSkinnedButton control.

    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;
    }
    

    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.
    CSkinnedButton* m_pOkButton;
    
  • Inside creation logic of the dialog OnCreate() or OnInitDialog(), insert the creation logic of the button, after few initializations of creating a memory DC for background painting.
    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
    
        ...
    }
    
    Custom code for button creation and button rendering is implemented in the CSkinnedButton class as shown:
    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() (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 supported in that version of Windows!). Anyways, may be I didnt 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 strick requirement of having all images bounded by a 4 pixel magenta background!!!!!!! Example:

    For those who might face a similar problem of TransparentBlt() are free to use the algorithm placed here or of your own.

    // 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 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;
    }
    
  • 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 updation of corresponding "look" by calling UpdateMemoryDC()

    Some benefits...

    If you design properly, you can come up with parallel "themes" for your application; basically different set of images to super-impose on your application and controls within, and switch easily, using configuration files.

  • License

    This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

    About the Author

    bigb_602


    Member
    My company: http://www.idyeah.com
    My blog: http://insidemeseeplusplus.blogspot.com
    Occupation: CEO
    Company: IDYeah Creations
    Location: India India

    Other popular Miscellaneous articles:

    Article Top
    You must Sign In to use this message board.
    FAQ FAQ 
     
    Noise Tolerance  Layout  Per page   
     Msgs 1 to 25 of 27 (Total in Forum: 27) (Refresh)FirstPrevNext
    Generalnice Pinmemberyctsai23:56 29 Nov '07  
    GeneralRe: nice Pinmemberbigb_6020:15 30 Nov '07  
    GeneralWow... Pinmemberthund3rstruck9:57 28 Mar '07  
    GeneralRe: Wow... Pinmemberbigb_60218:31 28 Mar '07  
    GeneralScreams for a screenshot! PinmemberJerry Evans8:48 28 Mar '07  
    GeneralRe: Screams for a screenshot! Pinmemberbryce14:05 28 Mar '07  
    GeneralRe: Screams for a screenshot! Pinmemberbigb_60218:57 28 Mar '07  
    GeneralRe: Missing DLL's in demoproject Pinmemberbigb_60218:55 28 Mar '07  
    QuestionLoading PNG Images Pinmember22:02 27 Mar '07  
    AnswerRe: Loading PNG Images Pinmemberbigb_60222:51 27 Mar '07  
    QuestionRe: Loading PNG Images PinmemberReji Dasan2:48 28 Mar '07  
    AnswerRe: Loading PNG Images PinmemberScope5:41 28 Mar '07  
    QuestionNice Distibution! but where are some header files? PinmemberTaeyoung Jin18:40 27 Mar '07  
    AnswerRe: Nice Distibution! but where are some header files? Pinmemberbigb_60218:46 27 Mar '07  
    GeneralRe: Nice Distibution! but where are some header files? PinmemberTaeyoung Jin20:00 27 Mar '07  
    GeneralRe: Nice Distibution! but where are some header files? PinmemberTaeyoung Jin20:20 27 Mar '07  
    GeneralRe: Nice Distibution! but where are some header files? PinmemberTaeyoung Jin20:38 27 Mar '07  
    GeneralRe: Nice Distibution! but where are some header files? Pinmemberbigb_60222:54 27 Mar '07  
    GeneralRe: Nice Distibution! but where are some header files? PinmemberTaeyoung Jin1:15 28 Mar '07  
    GeneralRe: Nice Distibution! but where are some header files? Pinmemberbigb_60218:59 28 Mar '07  
    GeneralRe: Nice Distibution! but where are some header files? PinmemberTaeyoung Jin20:17 28 Mar '07  
    GeneralGood work ! PinmemberAbinThomas21:27 28 Feb '07  
    GeneralRe: Good work ! Pinmemberbigb_60223:05 27 Mar '07  
    GeneralRe: Good work ! PinmemberTaeyoung Jin1:18 28 Mar '07  
    GeneralRe: Good work ! Pinmemberbigb_60218:59 28 Mar '07  

    General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

    PermaLink | Privacy | Terms of Use
    Last Updated: 27 Mar 2007
    Editor:
    Copyright 2007 by bigb_602
    Everything else Copyright © CodeProject, 1999-2009
    Web16 | Advertise on the Code Project