Introduction
The Windows performance monitor is a great tool for determining what is
happening with the performance of a computer. I have always found the ability to
monitor every aspect of a computer extremely useful and very powerful but wished
that Microsoft�s performance monitor would dock to the task bar so I could
continuously monitor my system without having to bring the window to the front.
One day I decided to write it. This article will demonstrate how to implement an
Explorer Desktop Band that uses the Microsoft�s Performance Data Helper
interface to display current performance data about activity such as memory,
disk, and processor usage.
Supported Platforms
Requires Internet Explorer 4.0 or greater and one of the following operating
systems:
Windows NT 4.0
Windows 2000
Windows XP (Home and Pro)
Installation
To install the desktop performance monitor band you need to run regsvr32.exe
on the IETools.dll. The IETools.dll can be downloaded or built from the source
provided as part of this article. See links above.
Regsvr32.exe IETools.dll
Once the band object has been successfully registered you can add the toolbar
to the taskbar by right clicking the task bar. Go to the Toolbars sub-menu and
select the �Performance Monitor� option.
To remove the performance monitor unselect the �Performance Monitor� option
on the toolbar sub-menu and run the following command on the IETools.dll.
Regsvr32 /U IETools.dll
Reboot the computer and then the IETools.dll can be deleted.
Creating the Band Object
A Desktop band is simply a COM object that implements certain interfaces and
registers itself as belonging to specific component category. There are three
interfaces every desktop band needs to implement:
The IDesktopBand
interface provides the desktop bands container
with information about the band object. IObjectWiteSite
interface
enables the communication between Desktop and container.
IPersistStream
is uses to store state so an object can be saved and
loaded.
Additionally the desktop band must also register itself as belonging to the
CATID_DeskBand
component category. This lets explorer know that
your object can be hosted in the taskbar and to add it to the list of available
toolbars in the taskbar�s context menu.
The COM object is created by running the ATL COM application wizard creating
new COM server dll. Once that is done you derive you new band objects from the
three interfaces mentioned above.
class ATL_NO_VTABLE CPerfBar :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CPerfBar, &CLSID_PerfBar>,
public IDispatchImpl<IPerfBar, &IID_IPerfBar,&LIBID_IETOOLSLib>,
public IObjectWithSite,
public IPersistStream,
public IDeskBand,
public IContextMenu,
public CWindowImpl<CPerfBar>
Next add the interfaces to ATL�s COM map
BEGIN_COM_MAP(CPerfBar)
COM_INTERFACE_ENTRY ( IPerfBar )
COM_INTERFACE_ENTRY ( IDispatch )
COM_INTERFACE_ENTRY ( IObjectWithSite )
COM_INTERFACE_ENTRY ( IDeskBand )
COM_INTERFACE_ENTRY ( IPersist )
COM_INTERFACE_ENTRY ( IPersistStream )
COM_INTERFACE_ENTRY ( IDockingWindow )
COM_INTERFACE_ENTRY ( IOleWindow )
COM_INTERFACE_ENTRY ( IContextMenu )
END_COM_MAP()
Then we need to let explorer know about our band object by registering it in
the component category.
BEGIN_CATEGORY_MAP( CPerfBar )
IMPLEMENTED_CATEGORY(CATID_DeskBand)
END_CATEGORY_MAP()
Drawing the Performance Meter
To draw our performance meter the first thing we need to do is create a
window to draw on. ATL has a nice HWND
wrapper class named
CWindowImpl
that makes it very easy for an object to handle and
respond to Windows messages. To use it we first derive our band object from
CWindowImpl
.
class ATL_NO_VTABLE CPerfBar :
�
public CWindowImpl<CPerfBar>
To handle window message we must create a message map and message handlers
for every message that will be handled. These are added to our desktop band
class.
BEGIN_MSG_MAP( CPerfBar )
MESSAGE_HANDLER( WM_CREATE, OnCreate )
MESSAGE_HANDLER( WM_DESTROY, OnGoodBye )
MESSAGE_HANDLER( WM_PAINT, OnPaint )
MESSAGE_HANDLER( WM_ERASEBKGND, OnEraseBg )
END_MSG_MAP()
LRESULT OnPaint ( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled );
LRESULT OnEraseBg( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled );
LRESULT OnCreate ( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled );
LRESULT OnGoodBye( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled );
Now we can implement our drawing routine. The performance data is stored as a
percentage from 0 to 100 in a STL deque. The deque was chosen over a STL vector
because it provides much faster insertions in the front.
typedef deque<FLOAT> PerfValueQ;
typedef PerfValueQ::iterator PerfValueQIterator ;
PerfValueQ m_qPerfValues;
The OnPaint
handler creates a memory device context to avoid
flickering during the drawing process. The meter is then drawn on to the memory
device context and finally the memory device context is BitBlt on to the screen.
LRESULT CPerfBar::OnPaint( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL&
bHandled )
{
PAINTSTRUCT ps = {0};
RECT rect = {0};
HDC hdcMem = NULL;
HBITMAP hbmMem = NULL;
HBITMAP hbmOld = NULL;
BeginPaint( &ps );
GetClientRect( &rect );
hdcMem = CreateCompatibleDC( ps.hdc );
hbmMem = CreateCompatibleBitmap( ps.hdc, rect.right - rect.left,
rect.bottom- rect.top);
hbmOld = (HBITMAP)SelectObject(hdcMem, hbmMem);
DrawBarMeter( hdcMem );
BitBlt( ps.hdc, rect.left, rect.top, rect.right-rect.left,
rect.bottom-rect.top, hdcMem, 0, 0, SRCCOPY);
SelectObject( hdcMem, hbmOld );
DeleteObject( hbmMem );
DeleteDC( hdcMem );
EndPaint( &ps);
return 0;
}
VOID CPerfBar::DrawBarMeter( HDC hdc )
{
INT barHeight = 0;
RECT rect = {0};
HPEN hOldPen = NULL;
PerfValueQIterator QIterator = m_qPerfValues.begin();
GetClientRect( &rect );
FillRect( hdc, &rect, m_backBrush );
hOldPen = (HPEN)SelectObject( hdc, m_forePen );
for ( ; rect.right >= rect.left; rect.right-- )
{
if ( QIterator != m_qPerfValues.end() )
{
barHeight = (INT)( (*QIterator) * ( rect.bottom - rect.top ) );
QIterator++;
}
else
barHeight = 0;
barHeight = barHeight < 2 ? 2 : barHeight;
MoveToEx( hdc, rect.right, rect.bottom, NULL );
LineTo ( hdc, rect.right, rect.bottom - barHeight );
}
m_qPerfValues.erase( QIterator, m_qPerfValues.end() );
SelectObject( hdc, hOldPen );
}
Band Object Persistence
Persistence in band objects is used for more than just saving the state of a
band when shutting down Windows and reloading the state when you start back up.
Every time the band object is undocked from the taskbar or desktop it is
persisted to a stream and destroyed. Once it has been relocated to the new
location a brand new instance of the band object is created and its state is
loaded from the persisted stream.
Persistence in band objects is implemented through the
IPersistStream
interface. The IPersistStream
interface
provides methods for saving and loading objects in a stream. Initially the
stream requests the maximum size in bytes that is needed for the stream to save
the object. The GetSizeMax
method is called to request this
information.
STDMETHODIMP CPerfBar::GetSizeMax( ULARGE_INTEGER* pcbSize )
{
if ( pcbSize == NULL )
return E_INVALIDARG;
ULISet32( *pcbSize, sizeof( m_clrFore ) +
sizeof( m_clrBack ) +
sizeof( m_ThreadData.dwRefreshRate ) +
sizeof( m_ThreadData.szCounterPath ) );
return S_OK;
}
The object is then persisted by a call to the save method.
STDMETHODIMP CPerfBar::Save( LPSTREAM pStream,
BOOL bClearDirty )
{
HRESULT hr = S_OK;
if ( FAILED( pStream->Write( &m_clrFore,
sizeof(m_clrFore), NULL ) ) ||
FAILED( pStream->Write( &m_clrBack,
sizeof(m_clrBack), NULL ) ) ||
FAILED( pStream->Write( &m_ThreadData.dwRefreshRate,
sizeof(m_ThreadData.dwRefreshRate), NULL ) ) ||
FAILED( pStream->Write( m_ThreadData.szCounterPath,
sizeof(m_ThreadData.szCounterPath), NULL ) ) )
{
hr = STG_E_CANTSAVE;
}
else
{
if ( bClearDirty )
m_bDirty = FALSE;
}
return hr;
}
The band object will then be destroyed while the user picks a new location to
dock the band. Once the new location has been chosen a new band object is
created and the state is reloaded.
STDMETHODIMP CPerfBar::Load( LPSTREAM pStream )
{
HRESULT hr = S_OK;
if ( FAILED( pStream->Read( &m_clrFore,
sizeof(m_clrFore), NULL ) ) ||
FAILED( pStream->Read( &m_clrBack,
sizeof(m_clrBack), NULL ) ) ||
FAILED( pStream->Read( &m_ThreadData.dwRefreshRate,
sizeof(m_ThreadData.dwRefreshRate), NULL ) ) ||
FAILED( pStream->Read( m_ThreadData.szCounterPath,
sizeof(m_ThreadData.szCounterPath), NULL ) ) )
{
hr = E_FAIL;
}
return hr;
}
Implementing IContextMenu
A nice feature is explorer allows band objects to add menu items to its
context menu. This allows the performance monitor band object to add a command
for opening up the options dialog so the user can change the properties of the
band object.
This is implemented through the IContextMenu
interface which a band object must implement to add items to the context menus.
When the context menu is displayed explorer passes the band object a
HMENU
to add menu items to.
#define MENU_ITEMS_ADDED 2
STDMETHODIMP CPerfBar::QueryContextMenu( HMENU hMenu,
UINT indexMenu,
UINT idCmdFirst,
UINT idCmdLast,
UINT uFlags )
{
HRESULT hr = S_OK;
if( CMF_DEFAULTONLY & uFlags )
hr = MAKE_HRESULT( SEVERITY_SUCCESS, 0, 0 );
else
{
TCHAR lptstrMenuString[MAX_STRINGTABLE] = {0};
LoadString( _Module.m_hInstResource,
IDS_MI_CONFIGURE,
lptstrMenuString,
MAX_STRINGTABLE );
InsertMenu( hMenu,
indexMenu,
MF_SEPARATOR | MF_BYPOSITION,
idCmdFirst + IDM_SEPERATOR, 0 );
InsertMenu( hMenu,
indexMenu,
MF_STRING | MF_BYPOSITION,
idCmdFirst + IDM_CONFIGURE,
lptstrMenuString );
hr = MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, MENU_ITEMS_ADDED );
}
return hr;
}
The return value to the QueryContextMenu
function is a
successful HRESULT
with the number of items added to the context
menu place in the status code section.
The context menu is then displayed to the user. Band objects are notified if
the user selects a custom menu item by the InvokeCommand
method.
The performance monitor band launches a dialog to allow the user to change the
properties of the band.
STDMETHODIMP CPerfBar::InvokeCommand( LPCMINVOKECOMMANDINFO pici )
{
HRESULT hr = S_OK;
if ( HIWORD( pici->lpVerb ) != 0 )
hr = E_INVALIDARG;
else
{
switch ( LOWORD( pici->lpVerb ) )
{
case IDM_CONFIGURE:
if ( m_dlgOptions.IsWindow() == FALSE )
{
m_dlgOptions.SetCounter ( m_ThreadData.szCounterPath );
m_dlgOptions.SetBackgroundColor ( m_clrBack );
m_dlgOptions.SetForegroundColor ( m_clrFore );
m_dlgOptions.SetRefreshRate( m_ThreadData.dwRefreshRate );
m_dlgOptions.SetDialogParent( m_hWnd );
m_dlgOptions.Create ( m_hWnd );
m_dlgOptions.CenterWindow( GetDesktopWindow() );
m_dlgOptions.ShowWindow( SW_SHOW );
}
else
m_dlgOptions.SetFocus();
hr = S_OK;
break;
default:
hr = E_INVALIDARG;
}
}
return hr;
}
Performance Monitoring
The Performance
Data Helper (PDH) library is an API provided by Microsoft to retrieve
real-time performance information. This library is only supported on the NT
platforms so the performance monitor band will only function on the NT platform
as well. The API for monitoring a source is fairly simple and Microsoft provides
a way to add your own sources. To begin monitoring a source you need to open a
query and create a counter.
class CPerfMon
{
public:
CPerfMon( );
virtual ~CPerfMon();
BOOL Start( LPTSTR lpstrCounter );
VOID Stop();
LONG GetValue();
private:
HQUERY m_hQuery;
HCOUNTER m_hCounter;
};
BOOL CPerfMon::Start( LPTSTR lpstrCounter )
{
PDH_STATUS pdhStatus = ERROR_SUCCESS;
Stop();
pdhStatus = PdhOpenQuery( NULL, 0, &m_hQuery ) ;
if ( pdhStatus == ERROR_SUCCESS )
{
pdhStatus = PdhValidatePath( lpstrCounter );
if ( pdhStatus == ERROR_SUCCESS )
{
pdhStatus = PdhAddCounter( m_hQuery, lpstrCounter, 0, &m_hCounter ) ;
}
}
ASSERT( pdhStatus == ERROR_SUCCESS );
if ( pdhStatus != ERROR_SUCCESS )
Stop();
return pdhStatus == ERROR_SUCCESS;
}
The counter requires a path to a source which looks something like
�\Processor(_Total)\% Processor Time�.
Once a performance counter has been successfully created, the current value
can be retrieve by a call to the PdhGetFormattedCounterValue
method.
LONG CPerfMon::GetValue()
{
PDH_STATUS pdhStatus = ERROR_SUCCESS;
LONG nRetVal = 0;
PDH_FMT_COUNTERVALUE pdhCounterValue;
pdhStatus = PdhCollectQueryData( m_hQuery );
if ( pdhStatus == ERROR_SUCCESS )
{
pdhStatus = PdhGetFormattedCounterValue( m_hCounter,
PDH_FMT_LONG,
NULL,
&pdhCounterValue );
if ( pdhStatus == ERROR_SUCCESS )
nRetVal = pdhCounterValue.longValue;
}
return nRetVal;
}
Cleanup of the allocated counters and queries is simple and can be
accomplished by removing the counter and closing the query.
VOID CPerfMon::Stop()
{
if ( m_hCounter )
PdhRemoveCounter( m_hCounter );
if ( m_hQuery )
PdhCloseQuery ( m_hQuery );
m_hQuery = NULL;
m_hCounter = NULL;
}
The performance monitoring for the band object is handled by a separate
thread that posts custom windows messages to the main window. The separate
thread is used to break the monitoring of the system from the windows message
loop. The main thread procedure takes a structure that contains all the
information the thread needs to monitor the system and an extra Boolean value
for performing synchronization between the thread and the main window.
typedef struct
{
HWND hWndNotify;
LONG bContinue;
DWORD dwRefreshRate;
TCHAR szCounterPath[ MAX_COUNTER_PATH ];
} PerfMonThreadData, *LPPERFMONTHREADDATA;
The thread loops so long as bContinue
is TRUE
.
While the thread loops, it performs the following functions:
- Retrieves a new performance value from the performance data helper API
- Posts the new performance value to the main window
- Sleeps for a predetermined timeout period
VOID PerfMonThreadProc( LPVOID lParam )
{
LPPERFMONTHREADDATA lpData = (LPPERFMONTHREADDATA) lParam;
INT nTime = 0;
CPerfMon PerfMon;
if ( PerfMon.Start( lpData->szCounterPath ) &&
IsWindow( lpData->hWndNotify ) )
{
while ( InterlockedExchange( &lpData->bContinue, TRUE ) )
{
PostMessage(lpData->hWndNotify, WM_ADDPERFVALUE, 0,
PerfMon.GetValue());
nTime = ( nTime == 0 ) ? lpData->dwRefreshRate :
lpData->dwRefreshRate -
(GetTickCount() - nTime);
if ( nTime > 0 )
{
Sleep( nTime );
}
nTime = GetTickCount();
}
}
PerfMon.Stop();
}
The main window simply waits for a message from the monitoring thread and
handles them as it would any other window message. The new performance value is
stuffed in the dequeue and the main window is invalidated.
LRESULT CPerfBar::OnNewPerformanceValue( UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled )
{
m_qPerfValues.push_front( ((FLOAT)lParam / 100.0f ) );
Invalidate();
return 0;
}
Further Enhancements
- Add different meter types (Line, Bar, Pie chart, Etc.).
- Allow for multiple meters to be displayed at once.
- Save history to a log file.
- Ability to kill a process from the context menu.
Reporting Defects and Suggestions
Please report all defects to me by email: mailto:chad_busche@hotmail.com?subject=Desktop
Performance Monitor
Please also feel free to submit suggestions and comments. Enjoy!