65.9K
CodeProject 正在变化。 阅读更多。
Home

桌面性能监视器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (11投票s)

2002年11月1日

6分钟阅读

viewsIcon

231000

downloadIcon

4816

如何实现一个 Explorer 桌面带,它使用 Microsoft 的性能数据帮助器界面来显示当前性能数据,例如内存、磁盘和处理器使用情况。

引言

Windows 性能监视器是一个用于确定计算机性能状况的绝佳工具。我一直认为监控计算机各个方面的能力非常有用和强大,但希望 Microsoft 的性能监视器能停靠到任务栏,这样我就可以持续监控我的系统,而不必将窗口置于前台。有一天,我决定自己写一个。本文将演示如何实现一个 Explorer 桌面带,它使用 Microsoft 的性能数据帮助器界面来显示当前性能数据,例如内存、磁盘和处理器使用情况。

支持的平台

需要 Internet Explorer 4.0 或更高版本以及以下操作系统之一

Windows NT 4.0
Windows 2000
Windows XP(家庭版和专业版)

安装

要安装桌面性能监视器带,您需要在 IETools.dll 上运行 regsvr32.exe。IETools.dll 可以从本文提供的源代码下载或构建。参见上面的链接。

Regsvr32.exe IETools.dll

成功注册带对象后,您可以通过右键单击任务栏来将工具栏添加到任务栏。转到“工具栏”子菜单并选择“性能监视器”选项。

要删除性能监视器,请取消选择工具栏子菜单上的“性能监视器”选项,并在 IETools.dll 上运行以下命令。

Regsvr32 /U IETools.dll

重启计算机,然后可以删除 IETools.dll。

创建带对象

桌面带只是一个 COM 对象,它实现了某些接口并注册自己属于特定的组件类别。每个桌面带都需要实现三个接口

IDesktopBand 接口向桌面带容器提供有关带对象的信息。IObjectWiteSite 接口使桌面和容器之间能够进行通信。IPersistStream 用于存储状态,以便可以保存和加载对象。

此外,桌面带还必须将自己注册为属于 CATID_DeskBand 组件类别。这让 explorer 知道您的对象可以托管在任务栏中,并将其添加到任务栏上下文菜单中可用的工具栏列表中。

通过运行 ATL COM 应用程序向导创建新的 COM 服务器 DLL 来创建 COM 对象。完成此操作后,您可以从上面提到的三个接口派生新的带对象。

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>

接下来,将接口添加到 ATL 的 COM 映射中

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() 

然后我们需要通过在组件类别中注册我们的带对象来让 explorer 知道它。

BEGIN_CATEGORY_MAP( CPerfBar )
    IMPLEMENTED_CATEGORY(CATID_DeskBand)
END_CATEGORY_MAP()    

绘制性能仪表

要绘制我们的性能仪表,我们首先需要创建一个窗口来绘制。ATL 有一个名为 CWindowImpl 的出色 HWND 包装器类,它使对象可以非常容易地处理和响应 Windows 消息。要使用它,我们首先从 CWindowImpl 派生我们的带对象。

class ATL_NO_VTABLE CPerfBar : 
    …
    public CWindowImpl<CPerfBar>

要处理窗口消息,我们必须为每个将要处理的消息创建消息映射和消息处理程序。这些都添加到我们的桌面带类中。

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 );

现在我们可以实现我们的绘图例程。性能数据以 0 到 100 的百分比形式存储在 STL deque 中。选择 deque 而不是 STL vector 是因为它提供了更快的头部插入。

typedef deque<FLOAT>           PerfValueQ;
typedef PerfValueQ::iterator   PerfValueQIterator ;
 
PerfValueQ  m_qPerfValues;  

OnPaint 处理程序创建一个内存设备上下文,以避免在绘制过程中闪烁。然后将仪表绘制到内存设备上下文,最后将内存设备上下文 BitBlt 到屏幕上。

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 );
   }

带对象持久性

带对象中的持久性不仅仅用于在关闭 Windows 时保存带的状态并在重新启动时重新加载状态。每次带对象从任务栏或桌面取消停靠时,它都会持久化到流中并销毁。一旦它被重新定位到新位置,就会创建一个全新的带对象实例,并从持久化流中加载其状态。

带对象中的持久性通过 IPersistStream 接口实现。IPersistStream 接口提供了在流中保存和加载对象的方法。最初,流请求流保存对象所需的最大字节大小。调用 GetSizeMax 方法来请求此信息。

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;
   }

然后通过调用保存方法来持久化对象。

 
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;
   }

当用户选择新的停靠位置时,带对象将被销毁。一旦选择了新位置,就会创建一个新的带对象并重新加载状态。

 
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;
   }

实现 IContextMenu

Explorer 的一个不错的功能是允许带对象向其上下文菜单添加菜单项。这允许性能监视器带对象添加一个用于打开选项对话框的命令,以便用户可以更改带对象的属性。

这是通过 IContextMenu 接口实现的,带对象必须实现该接口才能将项目添加到上下文菜单中。当显示上下文菜单时,explorer 将一个 HMENU 传递给带对象以添加菜单项。

 
#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;
   }

QueryContextMenu 函数的返回值是一个成功的 HRESULT,并在状态代码部分中包含添加到上下文菜单中的项目数。

然后向用户显示上下文菜单。如果用户通过 InvokeCommand 方法选择自定义菜单项,则会通知带对象。性能监视器带会启动一个对话框,允许用户更改带的属性。

 
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;
   }

性能监控

性能数据帮助器 (PDH) 库是 Microsoft 提供的用于检索实时性能信息的 API。此库仅在 NT 平台上受支持,因此性能监视器带也仅在 NT 平台上运行。用于监控源的 API 相当简单,Microsoft 提供了一种添加您自己的源的方法。要开始监控源,您需要打开一个查询并创建一个计数器。

 
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;
   }

计数器需要一个源路径,它看起来像“\Processor(_Total)\% Processor Time”。

成功创建性能计数器后,可以通过调用 PdhGetFormattedCounterValue 方法来检索当前值。

 
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;      
   }

清理分配的计数器和查询很简单,可以通过删除计数器和关闭查询来完成。

 
VOID CPerfMon::Stop()
   {
   if ( m_hCounter )   
      PdhRemoveCounter( m_hCounter );
 
   if ( m_hQuery )
      PdhCloseQuery   ( m_hQuery );
 
   m_hQuery   = NULL;
   m_hCounter = NULL;
   }

带对象的性能监控由一个单独的线程处理,该线程向主窗口发布自定义 Windows 消息。单独的线程用于将系统监控与 Windows 消息循环分开。主线程过程接受一个结构,该结构包含线程监控系统所需的所有信息,以及一个用于在线程和主窗口之间执行同步的额外布尔值。

 
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;

只要 bContinueTRUE,线程就会循环。在线程循环期间,它执行以下功能

  1. 从性能数据帮助器 API 检索新的性能值
  2. 将新的性能值发布到主窗口
  3. 休眠预定的超时时间


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();
   }

主窗口只是等待来自监控线程的消息,并像处理任何其他窗口消息一样处理它们。新的性能值被填充到 deque 中,并且主窗口无效。

LRESULT CPerfBar::OnNewPerformanceValue( UINT uMsg, WPARAM wParam, 
                                         LPARAM lParam, BOOL& bHandled )
   {
   m_qPerfValues.push_front( ((FLOAT)lParam / 100.0f ) );
   Invalidate();
   return 0;
   }

进一步增强

  • 添加不同的仪表类型(折线图、条形图、饼图等)。
  • 允许同时显示多个仪表。
  • 将历史记录保存到日志文件。
  • 能够从上下文菜单中终止进程。

报告缺陷和建议

请通过电子邮件向我报告所有缺陷:mailto:chad_busche@hotmail.com?subject=桌面性能监视器

也请随时提交建议和评论。祝您使用愉快!

© . All rights reserved.