Click here to Skip to main content
12,760,377 members (32,995 online)
Click here to Skip to main content
Add your own
alternative version

Stats

226.5K views
7.3K downloads
106 bookmarked
Posted 29 Sep 2000

EZSkin - A Primitive Framework for building skinnable apps

, 29 Jan 2001
Rate this:
Please Sign up or sign in to vote.
A mini library to build Bitmap based skinnable apps.

Introduction

This is a framework for building Skinnable UIs for MFC apps. This is by no means complete and presently only supports Dialog based apps. But it is highly extensible. Well, a Screen Capture speaks more than a thousand lines of code and two of them should do better.

EZSkin1.gif (44507 bytes)

EZSkin2.gif (34070 bytes)

I have categorized the whole thing into three subjects.

The source code is not that well commented. But it is wordy enough to understand and follows standard coding rules for MFC. I hope this has been an effort worth making.

Important - Instructions for running the demo

Initially you will only get the <default> item in the list box when you run it first. Close the application, then find the HKEY_CURRENT_USER\Software\EZSuite\EZSkinDemo\Skins key in your registry and enter the path where you have extracted the skins as the value for the Dir key.

Interface

Introduction

This is just a neat & extensible architecture to build Winamp style skinnable apps rather than a full featured library. The protocol can be classed into four layers!

Manager->Skin->Component->Reader

Manager

In the sample code, CEZSkinManager is the class which performs the role of the manager. It is a simple class which is responsible for certain trivial tasks like loading the user preferences/settings from the registry or wherever you have them. There are four simple functions that basically help us manage the skins.

This is a non trivial class and all the lower layers are independent of this. So, you can implement it wherever and in whatever way you want. You can even have your app class exhibit this functionality.

void LoadSkin(CString strSkin);//Loads the skin by name
//For displaying a Skin browser kind of dialog
int EnumerateSkins(CStringArray* pstrar);
virtual void Save();
virtual void Read();//Registry, Ini or ur own save system

And additionally there are two helpers which do exactly what they suggest.

//Makes a path out of a name
CString GetSkinPath(CString strName,BOOL bValidate =TRUE);
CString GetCurrentSkinPath() const;

One more example that says it hard & clear "Managers do the least!" :-)

Well, there is a not-so-difficult question of where this object should reside. It loads the preferences/settings so it should be a member of the App class with the read & save methods called during InitInstance & ExitInstance respectively. Right? I just took another route and derived my app class from this along with CWinApp.

Skin

The Backbone! This is, as the name suggests *The Skin*. CEZSkin represents this layer.

It is a Singleton. It sure does make sense as I cannot imagine n-skin objects hanging around taking a heavy toll on the resources with their bitmaps, fonts, icons & what not. Moreover it holds together all components and needs to be accessible from every skinned UI element and hence it is better to have a singleton with a static function returning its JIT instance rather than a global pointer polluting the ::.

CEZSkin& CEZSkin::Instance()
{
    static CEZSkin  Instance;//The one and only.
    return Instance;
}

Component

This is a skinlet. It is the skin of a particular UI element or a class of UI elements. The interface IEZComponent represents this.

class IEZSkinComponent : public CObject
{
DECLARE_SERIAL(IEZSkinComponent)
public:
    virtual BOOL Load(IEZSkinIni* pIni,BOOL bLoadDefaultOnFailure  = TRUE) 
    {ASSERT(FALSE); return FALSE;}
    virtual BOOL LoadDefault() 
    {ASSERT(FALSE); return FALSE;}
    virtual void Destroy() 
    {ASSERT(FALSE);}
    virtual BOOL IsLoaded()
    {ASSERT(FALSE); return FALSE;}
    virtual BOOL IsDefault()
    {ASSERT(FALSE); return TRUE;}
};

Hey, why is it a stupid assert always virtual function rather than a pure VF? Well now at last comes some spicy implementation.

The reason for not having an abstract class in place of this pseudo is to make it creatable at runtime using the class name. See DECLARE_SERIAL(IEZSkinComponent).

The reason why I want it this way is to write code like this.

CEZSkin& ezs = CEZSkin::Instance();
ezs.AddComponent(_T("CEZDialogSkin"));
//class CEZDialogSkin:public IEZSkinComponent

Though it is perfectly possible to do this using the RUNTIME_CLASS way, I just thought it would be cool if I could have the class name as a part of the skin definition in an INI file/registry etc...

The CEZSkin class holds the components by using a CTypedPtrMap.

CTypedPtrMap<CMapStringToOb,CString,IEZSkinComponent*> m_mapComponents;

All the functions of the interface would be called by CEZSkin which does JIT instantiation of the component during CEZSkin::GetComponent. The code reads like this,

IEZSkinComponent* pComponent = NULL;
if(!m_mapComponents.Lookup(strComponent,pComponent))
    return NULL;//Not registered

if(!pComponent)//Not yet created -do JIT Instantiation
{
    pComponent = 
      (IEZSkinComponent*)CEZRuntimeClass::CreateObject(strComponent);
    ASSERT(pComponent);
    m_mapComponents.SetAt(strComponent,pComponent);
}
if(m_bDefault)//Is the default skin loaded
{
  if(!pComponent->IsDefault()) //Make the component default
  {
   pComponent->Destroy();
   pComponent->LoadDefault();
  }
}
else if(!pComponent->IsLoaded())// new?
     pComponent->Load(m_pIni);
return pComponent;//Ok have it!

Reader

This is again a pseudo abstract class for providing certain trivial *read from skin definition* functions.

class IEZSkinIni :public CObject 
{
DECLARE_SERIAL(IEZSkinIni)
public:
  virtual BOOL GetValue(CString strSection,CString strKey,COLORREF& clrValue)
  {ASSERT(FALSE); return FALSE;}//Read Triplet Value
  virtual BOOL GetValue(CString strSection, CString strKey, int& nValue)
  {ASSERT(FALSE); return FALSE;}//Read Integer Value
  virtual BOOL GetValue(CString strSection,CString strKey, CString& strValue)
  {ASSERT(FALSE); return FALSE;}//Read String Value
  virtual BOOL GetValue(CString strSection, CString strKey, CPoint& ptValue)
  {ASSERT(FALSE); return FALSE;}//Read Twin Value
  virtual BOOL Read(CString strCurrentSkinPath)
  {ASSERT(FALSE);return FALSE;}//Init
};

Working

Step 1: Manager loads the settings during its read function.

Call CEZSkinManager::Read() during InitInstance.

Step 2: Manager introduces the Reader to the Skin with code like:

CEZSkin::Instance().SetIni(_T("CEZSkinIni"));
//class CEZSkinIni:public IEZSkinIni

Step 3: Manager loads the current skin or sets the skin to default.

void CEZSkinManager::Read()
{
    m_strSkins = AfxGetApp()->GetProfileString(HKEY_SKINS,HKEY_DIR,_T(""));
    CEZSkin::Instance().SetIni(_T("CEZSkinIni"));
    CFileFind ff;
    BOOL bLoaded = ff.FindFile(m_strSkins);
    if(bLoaded)
    {
      CEZSkin::Instance().SetSkinsDir(m_strSkins);
      m_strCurrentSkin = AfxGetApp()->GetProfileString(HKEY_SKINS,HKEY_SKIN);
      ff.Close();
    }
    LoadSkin(m_strCurrentSkin);
}
void CEZSkinManager::LoadSkin(CString strSkin)
{
    CFileFind ff;
    BOOL bLoaded = ff.FindFile(GetSkinPath(strSkin));
    if(bLoaded)
    {
      m_strCurrentSkin = strSkin;
      bLoaded = CEZSkin::Instance().LoadSkin(m_strCurrentSkin);
    }
    ff.Close();
}

Step 4: The Skinned objects communicate with the CEZSkin to initialize and get the components.

Now let us see some CEZSkin functions that relate to the above task.

virtual void SetIni(CString strClassName);
virtual void AddComponent(CString strClassName);
virtual IEZSkinComponent* GetComponent(CString strComponent);
virtual void LoadDefault();
virtual BOOL LoadSkin(CString strSkin);

The first function is called by the Manager in the above manner. The skinned UI element (Window) calls the next two functions like this.

void CSkinnedWindow::Init()
{
   //class CMySkin:public IEZSkinComponent
   CEZSkin::Instance().AddComponent(_T("CMySkin"));
   ....
}
void CSkinnedWindow::OnPaint()
{
    CPaintDC dc(this);
    CEZSkin& skin = CEZSkin::Instance();
    CMySkin* pSkin = skin.GetComponent(_T("CMySkin"));
    //////Do Painting by getting the attributes of the component
    //say..
    COLORREF clrBack = pSkin->GetBackgroundColor();
    dc.FillSolidRect(CEZClientRect(this),clrBack);
    .....
}

Step 5: and finally Manager writes the current settings to the storage.

Call CEZSkinManager::Save during ExitInstance.

AfxGetApp()->WriteProfileString(HKEY_SKINS,HKEY_DIR,m_strSkins);
AfxGetApp()->WriteProfileString(HKEY_SKINS,HKEY_SKIN,m_strCurrentSkin);

Implementation

Introduction

In the demo, I have implemented the EZSkin interface to create a skinned dialog. Strictly speaking, CEZSkinManager should have been discussed here but it would have been difficult for me to explain the interface without this class.

The following classes form the basis of this implementation.

CEZSkinIni

This gives a default implementation of IEZSkinIni. It implements the Reader layer as an INI file. I have used the CIni class by Iuri Apollonio and modified it to fit into the framework.

It uses a CStdioFile to read the INI file and stores every line in a CStringArray, then parses every line to get the required value. I have used a ; as the comment starter and , as the value separator.

It uses AfxExtractSubString to parse comma separated values.

A sample Skin INI;

[Skin]
Name = Black;
Author = V.Lakshmi Narasimhan;
Comment = Black Beauty;

[Main]
Bmp = back.bmp;
Draw = Tile;

[Caption]
Bmp = Caption.bmp;
DRAW = Tile;
TextFont = ARial Black,B,25;
TextColor = 200,200,200;
BtnsNormal = btns.bmp;
BtnsHilight  = btnsh.bmp;
TransColor = 192,224,64;
BtnPos = 7,27,47;
BtnWidth   = 20;

CEZGenericSkin

This gives a default implementation of IEZSkinComponent and is still pseudo abstract having a few assert always functions.

This class provides the interface for a window which requires the following skin attributes:

  • Background bitmap,
  • Background Color,
  • Text Color &
  • Text Font

This class holds the data using the following members:

BOOL m_bDefault;
BOOL m_bLoaded;
CEZDib m_Dib;//See the helpers section
CFont m_font;
COLORREF m_clrTxt;
COLORREF m_clrBk;

To use this class, one should derive from this and override the following functions:

//{Pseudo Pure virtual functions
virtual CString GetSection()
{ASSERT(FALSE);return _T("");}
virtual void LoadDefaultBmp(){ASSERT(FALSE);}
virtual void LoadDefaultFont(){ASSERT(FALSE);}
virtual void LoadDefaultBackColor(){ASSERT(FALSE);}
virtual void LoadDefaultTextColor(){ASSERT(FALSE);}
//}

It provides default implementation for all the functions exposed by the IEZSkinComponent interface. The reason why a derived class must override the above functions is this:

BOOL CEZGenericSkin::LoadDefault()
{
    LoadDefaultBmp();
    LoadDefaultBackColor();
    LoadDefaultTextColor();
    LoadDefaultFont();

    m_bDefault = TRUE;
    m_bLoaded = TRUE;
    return TRUE;
}

It also has a cool helper which loads a font into the m_font member given the Font face name style and width.

BOOL CEZGenericSkin::LoadFont(CString strFont, CString strStyle, int nHeight)

E.g. usage:

LoadFont(_T("Times New Roman"),_T("BI"),20);

To see how easy it is to implement IEZSkinComponent using CEZGenericSkin, take a look at the definition of CEZDialogSkin.

CEZDialogSkin

IMPLEMENT_SERIAL(CEZDialogSkin,IEZSkinComponent,(UINT)-1)

CString CEZDialogSkin::GetSection()
{return _T("Main");}

void CEZDialogSkin::LoadDefaultBackColor()
{m_clrBk= RGB(0,0,255);}

void CEZDialogSkin::LoadDefaultBmp()
{
    m_Dib.Load(IDB_BACK);
    m_Dib.SetType(CEZDib::BMP_TILE);
}

void CEZDialogSkin::LoadDefaultFont()
{LoadFont(_T("Times New Roman"),_T("B"),20);}

void CEZDialogSkin::LoadDefaultTextColor()
{m_clrTxt= RGB(255,0,0);}

CEZCaptionSkin

This is not quite as small as CEZDialogSkin.

It has additional members for Caption buttons - Rects, Highlighted & Normal bitmaps and transparent color of the bitmaps.

CEZDib m_DibBtnNormal;
CEZDib m_DibBtnHilight;
CRect m_rectBtns[3];
COLORREF m_clrTransparent;

Helpers

Introduction

Here we just take a look at the various helper classes that are used in the demo.

Rects

These are classes derived from CRect that encapsulate CWnd::GetxxxRect functions and CDC::GetClipBox so that it is possible to write code like:

CPaintDC dc(this);

//CEZDib dib;
dib.Draw(&dc,CEZClientRect(this));

//instead of 
//CRect rect;
//GetClientRect(&rect);
//dib.Draw(&dc,rect);

DCs

CEZMemDC, is CMemDC with additional bCopyOnDestruct parameter which prevents the DC from transferring its contents to the destination. CEZBmpDC selects a bitmap or portions of it to a compatible DC and can be used as a scratch pad.

The coolest one is the CEZMonoDC which takes in a DC and creates a DC with a monochrome bitmap of the source DC in place.

CEZMonoDC(CDC* pDCSrc,LPRECT pRect=NULL):CDC()
{
    ASSERT(pDCSrc != NULL);
    CreateCompatibleDC(pDCSrc);
    m_rect = pRect?*pRect:CEZClipRect(pDCSrc);
    m_bitmap.CreateBitmap(m_rect.Width(),m_rect.Height(),1,1,NULL);
    pDCSrc->SetBkColor(pDCSrc->GetPixel(  0, 0 ) ) ;
    m_pOldBitmap =(CBitmap*)SelectObject(&m_bitmap);
    SetWindowOrg(m_rect.left, m_rect.top);
}

CEZDib

This is built on Jörg König's CDIBitmap class. I have included certain goodies from other DIB classes I found. The important change I have done to CDIBitmap is to make it passable as a CBitmap as suggested by Paul DiLascia in periodicals 97. I have also added four drawing functions that draw a normal bitmap, stretched bitmap, tiled bitmap and one that draws transparently.

BOOL CEZDib::DrawTransparent(CDC* pDC,COLORREF clrTrans, 
  const CRect& rcDest,const CRect& rcSrc) const
{
    CRect rcDC(rcDest),rcBmp(rcSrc);

    if(rcDC.IsRectNull()) rcDC =CEZClipRect(pDC);
    if(rcBmp.IsRectNull()) rcBmp = CRect(0,0,GetWidth(),GetHeight());


    CEZMemDC memDC(pDC,&rcDC,TRUE,TRUE ),imageDC(pDC,&rcDC,FALSE);
    CEZMonoDC backDC(pDC,&rcDC),maskDC(pDC,&rcDC);

    DrawNormal(&imageDC,rcDC,rcBmp);

    COLORREF clrImageOld = imageDC.SetBkColor(clrTrans);
    maskDC.BitBlt(rcDC.left,rcDC.top,rcDC.Width(),
      rcDC.Height(),&imageDC,rcDC.left,rcDC.top,SRCCOPY);
    imageDC.SetBkColor(clrImageOld);

    backDC.BitBlt(rcDC.left,rcDC.top,rcDC.Width(),
      rcDC.Height(),&maskDC,rcDC.left,rcDC.top,NOTSRCCOPY);

    memDC.BitBlt(rcDC.left,rcDC.top,rcDC.Width(),rcDC.Height(),
      &maskDC,rcDC.left,rcDC.top,SRCAND);

    imageDC.BitBlt(rcDC.left,rcDC.top,rcDC.Width(),
      rcDC.Height(),&backDC,rcDC.left,rcDC.top,SRCAND);

    memDC.BitBlt(rcDC.left,rcDC.top,rcDC.Width(),rcDC.Height(),
      &imageDC,rcDC.left,rcDC.top,SRCPAINT);

    return TRUE;
}

CEZWindowNC

A class that encapsulates non client area functions of CWnd.

BOOL HasBorder();
BOOL HasSysMenu();
BOOL HasCaption();
CRect GetCaptionRect();
CRect GetLeftBorderRect();
CRect GetRightBorderRect();
CRect GetTopBorderRect();
CRect GetBottomBorderRect();

CEZDialog

This is the sample skinned UI element.

BOOL CEZDialog::OnEraseBkgnd(CDC* pDC) 
{
    CEZSkin& ezs = CEZSkin::Instance();
    CEZDialogSkin* pSkin  = 
      DYNAMIC_DOWNCAST(CEZDialogSkin,
      ezs.GetComponent(_T("CEZDialogSkin")));
    ASSERT(pSkin);
    const CEZDib& bmp = pSkin->GetBackgroundBitmap();
    CEZClientRect rcClient(this);
    bmp.Draw(pDC,rcClient);
    return TRUE; 
}
void CEZDialog::Init()
{
    CEZSkin& ezs = CEZSkin::Instance();
    ezs.AddComponent(_T("CEZDialogSkin"));
    VERIFY(m_brushHollow.CreateStockObject(HOLLOW_BRUSH));
}

Wow, doesn't the code look too small for a dialog with a skinned bitmap background?

CEZCaption

I have based this class on Dave Lorde's CCaption code. I have modified the original code to use CEZSkin and have also added code to paint and handle caption buttons. It uses CEZDib and CEZWindowNC extensively. I have also made changes to make it work for a dialog.

Though the caption does paint and handle the buttons well, I experienced problems with mouse tracking. I have simplified the tracking of the class at the cost of reducing the functionality. It would be nice if someone posts an article on how to do it.

Updates

Jan 30, 2001

  • Fixed static library crash.
  • Inconsistencies in mouse tracking in caption.
  • Adds a CEZBorder class to paint the border.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Lakshmi Vyas
Web Developer
United States United States
No Biography provided

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionVery Old - Needs Updating Pin
Michael B Pliam13-Aug-11 8:51
memberMichael B Pliam13-Aug-11 8:51 
GeneralConverting Existing GUIs to Skinnable GUIs Pin
Seetha Raman Thimmavajjula27-Aug-03 20:09
memberSeetha Raman Thimmavajjula27-Aug-03 20:09 
GeneralSkinMagic SDK Library:The better solution for Skinnable application Pin
jedyking22-Jan-03 7:24
memberjedyking22-Jan-03 7:24 
GeneralSDI Pin
Anonymous5-Aug-02 10:41
sussAnonymous5-Aug-02 10:41 
GeneralRe: SDI Pin
jedy1-Jan-03 20:31
memberjedy1-Jan-03 20:31 
GeneralCompile error Pin
CosmoS2k18-Feb-02 16:03
memberCosmoS2k18-Feb-02 16:03 
GeneralRe: Compile error Pin
geo_mm8-Sep-05 0:27
membergeo_mm8-Sep-05 0:27 
GeneralModal Dialogs usage - BUG Pin
Eugene Novojilov24-Jan-02 6:22
memberEugene Novojilov24-Jan-02 6:22 
GeneralRe: Modal Dialogs usage - BUG Pin
Anonymous8-Sep-02 3:58
sussAnonymous8-Sep-02 3:58 
GeneralRe: Modal Dialogs usage - BUG Pin
Anonymous8-Sep-02 4:07
sussAnonymous8-Sep-02 4:07 
GeneralWell done Pin
dicker20-Jan-02 20:56
memberdicker20-Jan-02 20:56 
GeneralGood Work Pin
Surend18-Nov-01 2:09
memberSurend18-Nov-01 2:09 
GeneralButtons, sliders, progress bars, etc. Pin
Ilya Kheifets25-Apr-01 12:46
memberIlya Kheifets25-Apr-01 12:46 
GeneralControl Panel Applet Pin
chrisw17-Apr-01 8:47
memberchrisw17-Apr-01 8:47 
GeneralRe: Control Panel Applet Pin
V Lakshmi Narasimhan17-Apr-01 18:10
memberV Lakshmi Narasimhan17-Apr-01 18:10 
Generalcool but Pin
Bulent Ozkir6-Feb-01 4:08
memberBulent Ozkir6-Feb-01 4:08 
GeneralThanks aLOT Pin
Matt Philmon31-Jan-01 16:50
memberMatt Philmon31-Jan-01 16:50 
GeneralA problem I need your help. Pin
Anonymous31-Jan-01 11:37
memberAnonymous31-Jan-01 11:37 
GeneralRe: A problem I need your help. Pin
Matt Philmon31-Jan-01 16:50
memberMatt Philmon31-Jan-01 16:50 
GeneralResize behavior Pin
Colorado Wolf20-Dec-00 13:08
memberColorado Wolf20-Dec-00 13:08 
GeneralRe: Resize behavior Pin
V Lakshmi Narasimhan20-Dec-00 16:55
memberV Lakshmi Narasimhan20-Dec-00 16:55 
Generalwindow border Pin
Anonymous11-Dec-00 1:54
memberAnonymous11-Dec-00 1:54 
GeneralRe: window border Pin
ezlux11-Dec-00 5:26
memberezlux11-Dec-00 5:26 
GeneralRe: window border Pin
Anonymous11-Dec-00 20:06
memberAnonymous11-Dec-00 20:06 
GeneralRe: How can i use this for only changing the skin for the dialogbox? Pin
Anonymous26-Nov-00 17:57
memberAnonymous26-Nov-00 17:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.170217.1 | Last Updated 30 Jan 2001
Article Copyright 2000 by Lakshmi Vyas
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid