EZSkin - A Primitive Framework for building skinnable apps






2.91/5 (8 votes)
Sep 30, 2000

250055

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