桌面性能监视器






4.93/5 (11投票s)
2002年11月1日
6分钟阅读

231000

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;
只要 bContinue
为 TRUE
,线程就会循环。在线程循环期间,它执行以下功能
- 从性能数据帮助器 API 检索新的性能值
- 将新的性能值发布到主窗口
- 休眠预定的超时时间
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=桌面性能监视器
也请随时提交建议和评论。祝您使用愉快!