Introduction
Many, many moons ago I
extended the
MFC
status bar by
creating a version capable of
housing almost any type of
control.
I seem to recall that my code
even supported VBX controls.
(VBX? yuk!) Anyway, this article
presents a simpler version that
targets
ActiveX controls.
The demo program illustrates
things by hosting two
ActiveX controls on
the status bar of an MFC
application.
The inside scoop
I�ll start by presenting my
hosting technique, beginning
with the
CStatusBarChildWnd
class.
I derive
CStatusBarChildWnd
from
CWnd
as shown here:
Collapse
Copy Code
class CStatusBarChildWnd : public CWnd
{
public:
CStatusBarChildWnd( int nMaxWidth )
{
m_nMaxWidth = nMaxWidth;
}
inline int GetMaxWidth( void ) const
{
return m_nMaxWidth;
}
private:
int m_nMaxWidth;
};
In addition to being a
placeholder for ActiveX controls
this class also exposes the
GetMaxWidth
property, which is used by the
status bar to determine the
maximum width of a control at
runtime. The status bar class
itself is named
CStatusBarEx
, and is
defined as follows:
Collapse
Copy Code
class CStatusBarEx : public CStatusBar
{
public:
CStatusBarEx( void );
virtual ~CStatusBarEx( void );
BOOL AddChildWindow( LPCTSTR lpszClass, UINT nID, int nWidth,
DWORD dwStyle = 0 );
BOOL RemoveChildWindow( UINT nID );
CWnd* GetChildWindow( UINT nID ) const;
protected:
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnDestroy();
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
DECLARE_MESSAGE_MAP()
private:
CMap<UINT, UINT, CStatusBarChildWnd*,
CStatusBarChildWnd*> m_mapChildren;
BOOL _RebuildPanes( void );
BOOL _ResizeChildWindows( void );
};
The first public function
declared by the
CStatusBarEx
class is
AddChildWindow
,
which is used to add new
controls to the status bar. This
function is implemented as shown
here:
Collapse
Copy Code
BOOL CStatusBarEx::AddChildWindow( LPCTSTR lpszClass, UINT nID, int nWidth,
DWORD dwStyle )
{
CStatusBarChildWnd* pWnd = NULL;
if ( TRUE == m_mapChildren.Lookup( nID, pWnd ) )
return FALSE;
try
{
pWnd = ::new CStatusBarChildWnd( nWidth );
if ( FALSE == pWnd->CreateControl( lpszClass, NULL,
dwStyle | (WS_CHILD | WS_VISIBLE), CRect( 0, 0, 0, nWidth ),
this, nID ) )
AfxThrowUserException();
m_mapChildren[ nID ] = pWnd;
return _RebuildPanes();
}
catch ( CException* e )
{
e->Delete();
::delete pWnd;
return FALSE;
} }
This code
creates
an
ActiveX control as a
child window of the status bar
and then saves a pointer to that
control in a map for later
access. As a final step, a call
is made to _RebuildPanes
,
which begins a process designed
to manage the layout of all the
controls on the status bar. The
_RebuildPanes
function is implemented as shown
below:
Collapse
Copy Code
BOOL CStatusBarEx::_RebuildPanes( void )
{
UINT* pIndicators = NULL;
try
{
int nCount = m_mapChildren.GetCount() + 1;
pIndicators = ::new UINT[ nCount ];
::memset( pIndicators, ID_SEPARATOR, sizeof( UINT ) * nCount );
if ( FALSE == SetIndicators( pIndicators, nCount ) )
AfxThrowUserException();
::delete[] pIndicators;
pIndicators = NULL;
POSITION pos = m_mapChildren.GetStartPosition();
UINT nIndex = 1;
UINT nID = 0;
CStatusBarChildWnd* pWnd = NULL;
while ( NULL != pos )
{
m_mapChildren.GetNextAssoc( pos, nID, pWnd );
ASSERT_VALID( pWnd );
SetPaneInfo( nIndex, nID, SBPS_NOBORDERS,
pWnd->GetMaxWidth() );
nIndex++;
}
return _ResizeChildWindows();
}
catch ( CException* e )
{
e->Delete();
::delete[] pIndicators;
return FALSE;
} }
This function takes advantage
of the fact that
MFC
internally manages the layout of
indicator panes. What I do is
create an indicator for each
control, then I position the
associated control over
indicator pane. With help from
MFC,
I get layout logic for almost
free. This function looks
complicated because I was forced
to perform some workarounds in
order to live in harmony with
the framework. The first problem
is that
MFC stores
indicator pane information in a
fixed array, making it difficult
to add or remove individual
panes at runtime. I solved this
by rebuilding all the indicators
every time a control is added or
removed. The next problem has to
do with the SetIndicators
function, which causes an
assertion if a string resource
isn�t available for each
indicator in the array. To solve
this I temporarily set each
identifier to ID_SEPARATOR
(since separators don�t use
string resources) before calling
SetIndicators
.
Afterwards, I loop through and
plug all the actual properties
into each indicator as a
separate step. I admit that this
approach is a little ugly, but
it works, and it makes layout
management much easier.
The last step of the
_RebuildPanes
function is
a call to
_ResizeChildWindows
,
which simply iterates through
the map and repositions and
resizes each control to match
the footprint of its
corresponding indicator pane.
This function is implemented as
follows:
Collapse
Copy Code
BOOL CStatusBarEx::_ResizeChildWindows( void )
{
int nCount = m_mapChildren.GetCount();
if ( 0 == nCount )
return TRUE;
POSITION pos = m_mapChildren.GetStartPosition();
UINT nIndex = 1;
UINT nID = 0;
CStatusBarChildWnd* pWnd = NULL;
HDWP hDwp = ::BeginDeferWindowPos( nCount );
while ( NULL != pos )
{
m_mapChildren.GetNextAssoc( pos, nID, pWnd );
CRect rc;
GetItemRect( nIndex, &rc );
::DeferWindowPos( hDwp, pWnd->GetSafeHwnd(),
GetSafeHwnd(), rc.left, rc.top, rc.Width(), rc.Height(),
SWP_NOZORDER );
nIndex++;
}
::EndDeferWindowPos( hDwp );
return TRUE;
}
The use of deferred window
positioning in this loop may
seem a bit obscure to some
readers, but this mechanism
allows multiple sibling windows
to be resized/repositioned
within a single update
operation. Using these functions
greatly increases the efficiency
of the entire layout process,
and reduces the possibility of
screen flicker.
Removing controls from the
status bar is accomplished
through the
RemoveChildWindow
function, which is implemented
like this:
Collapse
Copy Code
BOOL CStatusBarEx::RemoveChildWindow( UINT nID )
{
CStatusBarChildWnd* pWnd = NULL;
if ( FALSE == m_mapChildren.Lookup( nID, pWnd ) )
return FALSE;
pWnd->DestroyWindow();
::delete pWnd;
VERIFY( TRUE == m_mapChildren.RemoveKey( nID ) );
return _RebuildPanes();
}
All that is happening here is
that a pointer is being pulled
from the map and used to destroy
the associated
ActiveX control.
Afterwards, a call to
_RebuildPanes
performs
layout management exactly as was
previously described, except
that this time a pane will be
removed rather than added.
The final public function on
CStatusBarEx
is
GetChildWindow
,
which may be used to obtain a
pointer to any embedded control.
This function is implemented
like this:
Collapse
Copy Code
CWnd* CStatusBarEx::GetChildWindow( UINT nID ) const
{
CStatusBarChildWnd* pWnd = NULL;
m_mapChildren.Lookup( nID, pWnd );
return pWnd;
}
The sample application
The sample uses a class named
CDemoStatusBar
,
derived from CStatusBarEx
.
As an application, it doesn't do
much more than demonstrate
adding controls and processing
event notifications. The
CDemoStatusBar
is defined
like this:
Collapse
Copy Code
class CDemoStatusBar : public CStatusBarEx
{
public:
CDemoStatusBar( void );
virtual ~CDemoStatusBar( void );
protected:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnChangeDtpicker();
DECLARE_MESSAGE_MAP()
DECLARE_EVENTSINK_MAP()
};
The OnCreate
handler is called from MFC when
the status bar is created, and
is implemented like this:
Collapse
Copy Code
int CDemoStatusBar::OnCreate( LPCREATESTRUCT lpCreateStruct )
{
if ( -1 == CStatusBarEx::OnCreate( lpCreateStruct ) )
return -1;
SendMessage( SB_SETMINHEIGHT, 22, 0 );
AddChildWindow( _T("MSComCtl2.DTPicker"), IDC_DTPICKER, 100 );
AddChildWindow( _T("COMCTL.Slider"), IDC_SLIDER, 150 );
return 0;
}
Notice that the first
parameter to each
AddChildWindow
call is in
fact the program identifier of
an
ActiveX control.
These strings can be located on
your system by selecting the
�Project/Add To
Project/Components and
Controls� menu
choice, and selecting the
�Registered
ActiveX
Controls� item in the
resulting dialog. This action
produces a list of
ActiveX
controls, where each
control is shown using a program
identifier. Simply copy the
string into your own code and
you�re in business!
Intercepting and handling the
onchange
event from
the date/time picker involves
adding the following handler to
the event map on the
CDemoStatusBar
class:
Collapse
Copy Code
BEGIN_EVENTSINK_MAP(CDemoStatusBar, CStatusBarEx)
ON_EVENT(CDemoStatusBar, IDC_DTPICKER,
2 , OnChangeDtpicker, VTS_NONE)
END_EVENTSINK_MAP()
This causes
MFC
to route all onchange
notification events from the
date/time picker to the
OnChangeDtPicker
handler.
In the sample we have written
OnChangeDtPicker
to
simply display the selected
date/time value on the status
bar, as shown here:
Collapse
Copy Code
void CDemoStatusBar::OnChangeDtpicker( void )
{
CDTPicker* pWnd = (CDTPicker*)GetChildWindow( IDC_DTPICKER );
ASSERT_VALID( pWnd );
CString strDate;
strDate.Format( _T("Date Selected: %d/%d/%d"), pWnd->GetMonth().iVal,
pWnd->GetDay().iVal, pWnd->GetYear().iVal );
GetParentFrame()->SetMessageText( strDate );
}
Final thoughts
Keep in mind that you will
not need to derive from
CStatusBarEx
in your
project unless you intend to
process control notifications.
In a simpler scenario,
CStatusBarEx
could be
used as a drop-in replacement
for the
CStatusBar
class when events are not an
issue.
I�m sure you will come up
with many creative ways to use
this tool in your projects.
Have fun! :o)