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;
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"));
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;
if(!pComponent)
{
pComponent =
(IEZSkinComponent*)CEZRuntimeClass::CreateObject(strComponent);
ASSERT(pComponent);
m_mapComponents.SetAt(strComponent,pComponent);
}
if(m_bDefault)
{
if(!pComponent->IsDefault())
{
pComponent->Destroy();
pComponent->LoadDefault();
}
}
else if(!pComponent->IsLoaded())
pComponent->Load(m_pIni);
return pComponent;
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;}
virtual BOOL GetValue(CString strSection, CString strKey, int& nValue)
{ASSERT(FALSE); return FALSE;}
virtual BOOL GetValue(CString strSection,CString strKey, CString& strValue)
{ASSERT(FALSE); return FALSE;}
virtual BOOL GetValue(CString strSection, CString strKey, CPoint& ptValue)
{ASSERT(FALSE); return FALSE;}
virtual BOOL Read(CString strCurrentSkinPath)
{ASSERT(FALSE);return FALSE;}
};
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"));
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 tucancode.net.
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()
{
CEZSkin::Instance().AddComponent(_T("CMySkin"));
....
}
void CSkinnedWindow::OnPaint()
{
CPaintDC dc(this);
CEZSkin& skin = CEZSkin::Instance();
CMySkin* pSkin = skin.GetComponent(_T("CMySkin"));
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;
CFont m_font;
COLORREF m_clrTxt;
COLORREF m_clrBk;
To use this class, one
should derive from this and override the following
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);
dib.Draw(&dc,CEZClientRect(this));
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.
Collapse
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),mucancode.netDC(pDC,&rcDC);
DrawNormal(&imageDC,rcDC,rcBmp);
COLORREF clrImageOld = imageDC.SetBkColor(clrTrans);
mucancode.netDC.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(),&mucancode.netDC,rcDC.left,rcDC.top,NOTSRCCOPY);
memDC.BitBlt(rcDC.left,rcDC.top,rcDC.Width(),rcDC.Height(),
&mucancode.netDC,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.