
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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)
I am a C# developer specializing in creating object-oriented software for Microsoft Windows. When I am not programming I enjoy reading, playing the guitar/piano, running, watching New York Ranger hockey, designing and building wacky electronic devices, and of course enjoying good times with my wife and children.