Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Desktop Performance Monitor

0.00/5 (No votes)
31 Oct 2002 1  
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.

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 );
 
      // Add a seperator

      InsertMenu( hMenu, 
                  indexMenu, 
                  MF_SEPARATOR | MF_BYPOSITION, 
                  idCmdFirst + IDM_SEPERATOR, 0 ); 
 
      // Add the new menu item

      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;        // Window to post messages to

   LONG   bContinue;         // Set to false to cause the thread to stop 

                             // monitoring

   DWORD  dwRefreshRate;     // How often we get a new performance value

   TCHAR  szCounterPath[ MAX_COUNTER_PATH ];   // what we are monitoring

   } PerfMonThreadData, *LPPERFMONTHREADDATA;

The thread loops so long as bContinue is TRUE. While the thread loops, it performs the following functions:

  1. Retrieves a new performance value from the performance data helper API
  2. Posts the new performance value to the main window
  3. 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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here