可选择的树控件状态改变时的通知消息处理
昨天要实现一个功能:在一棵树上列出根据定义的规则从文件系统中过滤出来的一些文件,然后让用户从列出的文件中进行选择哪些文件需要处理,要求目录的状态发生变化时其下所有的子目录和文件的状态跟随发生相同的变化;
首先将树控件的属性中增加:TVS_CHECKBOXES 属性,这样树就可以在每一个项目前面显示一个选择框,然后是使用系统的 ImageList 并对每一个文件设置为相应的系统图标,代码如下:
// 获取系统的共享图标列表
inline HIMAGELIST WINAPI GetSystemImageList( UINT uFlags = SHGFI_SMALLICON )
{
SHFILEINFO sfi = { 0 }; TCHAR szDriver[] = { TEXT("C:\\") };
for( TCHAR chrDriver = _T('C') ; chrDriver <= _T('Z') ; chrDriver ++ )
{
szDriver[ 0 ] = chrDriver; CHK_EXP_RUN( GetDriveType( szDriver ) != DRIVE_FIXED , continue );
return (HIMAGELIST)::SHGetFileInfo( szDriver , 0, &sfi, sizeof(sfi), SHGFI_SYSICONINDEX | uFlags );
}
return NULL;
}
VOID CPageCollect::SetItemIcon( CTreeViewCtrl hTree , HTREEITEM hItem )
{
DWORD dwData = hTree.GetItemData( hItem );
CHK_EXP_END( dwData < FILE_BASE_INDEX );
SetupFileInfo xFile = { GUID_NULL , GUID_NULL };
UINT nIndex = dwData - FILE_BASE_INDEX;
Manager.GetFileInfo( nIndex , xFile );
StringT szPath = Manager.szProgram;
szPath += xFile.szPath;
SHFILEINFO sfi = { 0 } , sfo = { 0 };
UINT uFlags = SHGFI_SYSICONINDEX | SHGFI_SMALLICON ;
SHGetFileInfo( szPath , 0, &sfi, sizeof(sfi), uFlags );
uFlags = uFlags | SHGFI_OPENICON | SHGFI_SMALLICON ;
SHGetFileInfo( szPath , 0, &sfo, sizeof(sfo), uFlags );
hTree.SetItemImage( hItem , sfi.iIcon , sfo.iIcon );
}
inline HIMAGELIST WINAPI GetSystemImageList( UINT uFlags = SHGFI_SMALLICON )
{
SHFILEINFO sfi = { 0 }; TCHAR szDriver[] = { TEXT("C:\\") };
for( TCHAR chrDriver = _T('C') ; chrDriver <= _T('Z') ; chrDriver ++ )
{
szDriver[ 0 ] = chrDriver; CHK_EXP_RUN( GetDriveType( szDriver ) != DRIVE_FIXED , continue );
return (HIMAGELIST)::SHGetFileInfo( szDriver , 0, &sfi, sizeof(sfi), SHGFI_SYSICONINDEX | uFlags );
}
return NULL;
}
VOID CPageCollect::SetItemIcon( CTreeViewCtrl hTree , HTREEITEM hItem )
{
DWORD dwData = hTree.GetItemData( hItem );
CHK_EXP_END( dwData < FILE_BASE_INDEX );
SetupFileInfo xFile = { GUID_NULL , GUID_NULL };
UINT nIndex = dwData - FILE_BASE_INDEX;
Manager.GetFileInfo( nIndex , xFile );
StringT szPath = Manager.szProgram;
szPath += xFile.szPath;
SHFILEINFO sfi = { 0 } , sfo = { 0 };
UINT uFlags = SHGFI_SYSICONINDEX | SHGFI_SMALLICON ;
SHGetFileInfo( szPath , 0, &sfi, sizeof(sfi), uFlags );
uFlags = uFlags | SHGFI_OPENICON | SHGFI_SMALLICON ;
SHGetFileInfo( szPath , 0, &sfo, sizeof(sfo), uFlags );
hTree.SetItemImage( hItem , sfi.iIcon , sfo.iIcon );
}
在上面的代码中使用了一些检测执行宏,其定义如下:
//===========================================================================
// 辅助的检测宏:简化代码的编写
#define CHK_EXP_ACT( exp , one , two ) if( exp ){ one ; } else { two ; }
#define CHK_EXP_RET( exp , ret ) CHK_EXP_ACT( exp , return ret , ; )
#define CHK_EXP_RUN( exp , run ) CHK_EXP_ACT( exp , run , ; )
#define CHK_EXP_END( exp ) CHK_EXP_RUN( exp , return )
// 辅助的检测宏:简化代码的编写
#define CHK_EXP_ACT( exp , one , two ) if( exp ){ one ; } else { two ; }
#define CHK_EXP_RET( exp , ret ) CHK_EXP_ACT( exp , return ret , ; )
#define CHK_EXP_RUN( exp , run ) CHK_EXP_ACT( exp , run , ; )
#define CHK_EXP_END( exp ) CHK_EXP_RUN( exp , return )
在初始化该树的时候,设置 IMAGELIST 为 GetSystemImageList 函数的返回值,在构造树节点时,对每一个节点调用一次 SetItemIcon 函数来让树中的每一个节点的图标和系统保持一致;(注意:在我的实现中每个节点代表的文件的全路径是通过节点 TAG 数据指示的索引在一个列表中获得的相对路径,该相对路径的根目录有管理器的变量设定)
在树构造完成之后就是响应用户的消息,当用户点击在一个树节点上时,处理该消息并将子节点的选择状态同步的做变化,代码如下:
LRESULT CPageCollect::OnTreeClick( LPNMHDR lpNMHDR )
{
// 测试是否鼠标点击在了选择框上
POINT ptCursor = { 0 , 0 }; GetCursorPos( & ptCursor );
CTreeViewCtrl hReport = GetDlgItem( IDC_CHK_REPORTOR );
hReport.ScreenToClient( & ptCursor ); UINT uFlags = 0 ;
HTREEITEM hItem = hReport.HitTest( ptCursor , & uFlags );
CHK_EXP_RET( hItem == NULL || uFlags == NO_ERROR , FALSE );
CHK_EXP_RET( ( uFlags & TVHT_ONITEMSTATEICON ) == 0 , FALSE );
// 对一个节点的所有子节点进行状态管理
BOOL bChecked = hReport.GetCheckState( hItem ) ? FALSE : TRUE ;
SyncTreeItemState( hReport , hItem , bChecked );
return NO_ERROR ;
}
{
// 测试是否鼠标点击在了选择框上
POINT ptCursor = { 0 , 0 }; GetCursorPos( & ptCursor );
CTreeViewCtrl hReport = GetDlgItem( IDC_CHK_REPORTOR );
hReport.ScreenToClient( & ptCursor ); UINT uFlags = 0 ;
HTREEITEM hItem = hReport.HitTest( ptCursor , & uFlags );
CHK_EXP_RET( hItem == NULL || uFlags == NO_ERROR , FALSE );
CHK_EXP_RET( ( uFlags & TVHT_ONITEMSTATEICON ) == 0 , FALSE );
// 对一个节点的所有子节点进行状态管理
BOOL bChecked = hReport.GetCheckState( hItem ) ? FALSE : TRUE ;
SyncTreeItemState( hReport , hItem , bChecked );
return NO_ERROR ;
}
SyncTreeItemState 是一个递归处理函数,函数代码如下:
LRESULT CPageCollect::SyncItemCheckState( CTreeViewCtrl & hReport , HTREEITEM hItem , BOOL bChecked )
{
for( HTREEITEM hChild = hReport.GetChildItem( hItem ) ; hChild != NULL ; )
{
hReport.SetCheckState( hChild , bChecked );
SyncItemCheckState( hReport , hChild , bChecked );
hChild = hReport.GetNextSiblingItem( hChild );
}
return ERROR_SUCCESS ;
}
{
for( HTREEITEM hChild = hReport.GetChildItem( hItem ) ; hChild != NULL ; )
{
hReport.SetCheckState( hChild , bChecked );
SyncItemCheckState( hReport , hChild , bChecked );
hChild = hReport.GetNextSiblingItem( hChild );
}
return ERROR_SUCCESS ;
}
请注意:在上面的代码中检测了鼠标是否点击在了选择框上,如果不是不进行后续处理,否则将鼠标下面的项目的状态取出,然后取反(这是因为该项目的状态在该消息之后系统继续处理中会立刻改变)并对所有的子项目进行状态同步;该响应函数的消息处理声明如下:
NOTIFY_CODE_HANDLER_EX( NM_CLICK , OnTreeClick )
备注:因为在该对话框上只有一个树控件和几个按钮,所以无需指定通知消息来源控件的 ID
这样就大功告成了,后面就是实现根据状态获取哪些文件是用户选中需要处理的了,不过有意思的是在 SDK 的头文件中发现了消息:TVN_ITEMCHANGED,可选择的列表控件也有相似的消息:LVN_ITEMCHANGED,可以通过该消息可以在一个列表项目的选择状态发生改变后处理,但是树控件的这个消息虽然是被 _WIN32_IE 宏给控制起来的,但实际上应该是 WINVER,因为即使安装了 IE6 或其以上版本,只要系统版本低于 VISTA 树控件是不会发出该消息的,参考下面的代码:
NOTIFY_CODE_HANDLER_EX( TVN_ITEMCHANGED , OnTreeItemChange )
LRESULT CPageCollect::OnTreeItemChange( LPNMHDR lpNMHDR )
{
NMTVITEMCHANGE * lpItemChange = (NMTVITEMCHANGE*)lpNMHDR;
CHK_EXP_RET( lpItemChange->uChanged != TVIF_STATE , FALSE );
BOOL bChecked = m_hReport.GetCheckState( lpItemChange->hItem );
SyncTreeItemState( m_hReport , lpItemChange->hItem , bChecked );
return NO_ERROR ;
}
LRESULT CPageCollect::OnTreeItemChange( LPNMHDR lpNMHDR )
{
NMTVITEMCHANGE * lpItemChange = (NMTVITEMCHANGE*)lpNMHDR;
CHK_EXP_RET( lpItemChange->uChanged != TVIF_STATE , FALSE );
BOOL bChecked = m_hReport.GetCheckState( lpItemChange->hItem );
SyncTreeItemState( m_hReport , lpItemChange->hItem , bChecked );
return NO_ERROR ;
}
在 Windows Server 2008 上响应正常,但是在 Windows Server 2003 上毫无反映,通过加入跟踪语句可以更加清楚的明白这一点;