MFC模型树控件TreeCtrl实现按下Ctrl键多选,按下Shift键连选
MFC的List Box只需要将控件属性中的Selection项设置为Extended,即可实现Ctrl键多选和Shift键连选;然而MFC的Tree Contrl(TreeCtrl)没有像List Box那样设置某个属性就实现Ctrl键多选和Shift键连选的功能,网上大多数MFC的TreeCtrl多选功能是用复选框实现的,也有前辈想通过继承类CTreeCtrl来重写多选功能,其中实现该功能较好的示例为以下链接:http://www.pudn.com/Download/item/id/640887.html,经过试验,发现该示例按住Ctrl只能选中两个项目。于是在此示例的基础上,我重新写了按下Ctrl键的代码,实现了真正的多选。
上述链接中包含一个基于对话框的MFC应用程序示例,该示例演示了CTreeCtrEx的用法,要将CTreeCtrlEx合并到自己的项目中,请按照以下步骤操作:
(1)将TreeCtrlEx.cpp和TreeCtrlEx.h两个文件复制到项目目录;
(2)将它们添加到项目中;
(3)在需要的地方包含TreeCtrlEx.h(包含在StdAfx.h中非常方便);
(4)在需要多选的地方,将对象的类型由CTreeCtrl更改为CTreeCtrlEx。
TreeCtrlEx.cpp和TreeCtrlEx.h中只有以下6个函数是多选所需要的,其他的可根据自己的需要保留:
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
BOOL SelectItems(HTREEITEM hFromItem, HTREEITEM hToItem);
void ClearSelection(BOOL bMultiOnly = FALSE);
void SelectMultiple(HTREEITEM hClickedItem, UINT nFlags, CPoint point);
下面直接上我修改后TreeCtrlEx.cpp和TreeCtrlEx.h中的代码,有什么不对的地方,还请读者留言,大家一起学习。代码仅供参考,转载请标明出处。
TreeCtrlEx.h
#pragma once
#include "afxcmn.h"
#include "vector"
using namespace std;
class CTreeCtrlEx :
public CTreeCtrl
{
public:
CTreeCtrlEx();
~CTreeCtrlEx();
DECLARE_MESSAGE_MAP()
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
BOOL SelectItems(HTREEITEM hFromItem, HTREEITEM hToItem);
void ClearSelection(BOOL bMultiOnly = FALSE);
protected:
void SelectMultiple(HTREEITEM hClickedItem, UINT nFlags, CPoint point);
public:
BOOL m_bEditLabelPending;
UINT m_idTimer;
HTREEITEM m_hClickedItem;
BOOL m_bSelectPending;
CPoint m_ptClick;
HTREEITEM m_hFirstSelectedItem;
BOOL m_bSelectionComplete;
bool bIsSelectedItem[100];//存放每一个选项的状态
};
TreeCtrlEx.cpp
#include "stdafx.h"
#include "TreeCtrlEx.h"
#define TCEX_EDITLABEL 1 // Edit label timer event
CTreeCtrlEx::CTreeCtrlEx()
{
m_bSelectPending = FALSE;
m_hClickedItem = NULL;
m_hFirstSelectedItem = NULL;
m_bSelectionComplete = TRUE;
m_bEditLabelPending = FALSE;
for (int i = 0; i < 100; i++)
{
bIsSelectedItem[i] = false;//初始化
}
}
CTreeCtrlEx::~CTreeCtrlEx()
{
}
BEGIN_MESSAGE_MAP(CTreeCtrlEx, CTreeCtrl)
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_KEYDOWN()
END_MESSAGE_MAP()
void CTreeCtrlEx::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
UINT nHitFlags = 0;
HTREEITEM hClickedItem = HitTest(point, &nHitFlags);
//必须显式调用标签编辑,OnLButtonDown通常会这样做,但我们不能在这里调用它,因为有多个选择...
if (!(nFlags&(MK_CONTROL | MK_SHIFT)) && (GetStyle() & TVS_EDITLABELS) && (nHitFlags & TVHT_ONITEMLABEL))
if (hClickedItem == GetSelectedItem())
{
//在编辑标签之前清除多重选择
ClearSelection();
SelectItem(hClickedItem);
//调用标签编辑
m_bEditLabelPending = TRUE;
m_idTimer = SetTimer(TCEX_EDITLABEL, GetDoubleClickTime(), NULL);
return;
}
m_bEditLabelPending = FALSE;
if (nHitFlags & TVHT_ONITEM)
{
SetFocus();
m_hClickedItem = hClickedItem;
//点击的项目是否已被选中
BOOL bIsClickedItemSelected = GetItemState(hClickedItem, TVIS_SELECTED) & TVIS_SELECTED;
if (bIsClickedItemSelected)
{
//可能用户想拖放多个项目!所以,等到OnLButtonUp()才进行选择。
m_bSelectPending = TRUE;
}
else
{
SelectMultiple(hClickedItem, nFlags, point);
m_bSelectPending = FALSE;
}
m_ptClick = point;
}
else
CTreeCtrl::OnLButtonDown(nFlags, point);
}
void CTreeCtrlEx::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
if (m_bSelectPending)
{
//一个项目一直在等待执行
SelectMultiple(m_hClickedItem, nFlags, point);
m_bSelectPending = FALSE;
}
m_hClickedItem = NULL;
CTreeCtrl::OnLButtonUp(nFlags, point);
}
void CTreeCtrlEx::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CWnd* pWnd = GetParent();
if (nChar == VK_NEXT || nChar == VK_PRIOR)
{
if (!(GetKeyState(VK_SHIFT) & 0x8000))
{
//用户在没有按住Shift键的情况下按PG键:清除多选(如果有多选),让基类做正常的选择工作!
if (GetSelectedCount()>1)
ClearSelection(TRUE);
CTreeCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
m_hFirstSelectedItem = GetSelectedItem();
return;
}
//表示选择过程未完成的标志(将禁止TVN_SELCHANGED发送给父类)
m_bSelectionComplete = FALSE;
//让基类选择项
CTreeCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
HTREEITEM hSelectedItem = GetSelectedItem();
//然后选择介于两者之间的项目
SelectItems(m_hFirstSelectedItem, hSelectedItem);
//选择过程现已完成。由于我们已经销毁了Windows的treectrl提供的TVN_SELCHANGED通知,
//现在我们必须自己生成一个通知,这样我们的父类才能知道选择的更改。
m_bSelectionComplete = TRUE;
if (pWnd)
{
NM_TREEVIEW tv;
memset(&tv.itemOld, 0, sizeof(tv.itemOld));
tv.hdr.hwndFrom = GetSafeHwnd();
tv.hdr.idFrom = GetWindowLong(GetSafeHwnd(), GWL_ID);
tv.hdr.code = TVN_SELCHANGED;
tv.itemNew.hItem = hSelectedItem;
tv.itemNew.state = GetItemState(hSelectedItem, 0xffffffff);
tv.itemNew.lParam = GetItemData(hSelectedItem);
tv.itemNew.mask = TVIF_HANDLE | TVIF_STATE | TVIF_PARAM;
tv.action = TVC_UNKNOWN;
pWnd->SendMessage(WM_NOTIFY, tv.hdr.idFrom, (LPARAM)&tv);
}
}
else if (nChar == VK_UP || nChar == VK_DOWN)
{
//获取当前选择的项目
HTREEITEM hSelectedItem = GetSelectedItem();
HTREEITEM hNextItem;
if (nChar == VK_UP)
hNextItem = GetPrevVisibleItem(hSelectedItem);
else
hNextItem = GetNextVisibleItem(hSelectedItem);
if (!(GetKeyState(VK_SHIFT) & 0x8000))
{
//用户在没有按住Shift键的情况下按下箭头键:清除多项选择(如果多项选择),
//并让基类执行正常的选择工作!
if (GetSelectedCount()>1)
ClearSelection(TRUE);
if (hNextItem)
CTreeCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
m_hFirstSelectedItem = GetSelectedItem();
return;
}
if (hNextItem)
{
//表示选择过程未完成的标志。
//(将禁止TVN_SELCHANGED发送给父类)
m_bSelectionComplete = FALSE;
//如果已经选择了下一项,我们假设用户在选择中“向后移动”,因此我们应该清除对上一项的选择
BOOL bSelect = !(GetItemState(hNextItem, TVIS_SELECTED) & TVIS_SELECTED);
//选择下一项(这也会取消选择前一项!)
SelectItem(hNextItem);
//现在,重新选择之前选择的项目
if (bSelect || (!(GetItemState(hSelectedItem, TVIS_SELECTED) & TVIS_SELECTED)))
SelectItems(m_hFirstSelectedItem, hNextItem);
//选择过程现已完成。由于我们已经销毁了Windows的treectrl提供的TVN_SELCHANGED通知,
//现在我们必须自己生成一个通知,这样我们的父类才能知道选择的更改。
m_bSelectionComplete = TRUE;
if (pWnd)
{
NM_TREEVIEW tv;
memset(&tv.itemOld, 0, sizeof(tv.itemOld));
tv.hdr.hwndFrom = GetSafeHwnd();
tv.hdr.idFrom = GetWindowLong(GetSafeHwnd(), GWL_ID);
tv.hdr.code = TVN_SELCHANGED;
tv.itemNew.hItem = hNextItem;
tv.itemNew.state = GetItemState(hNextItem, 0xffffffff);
tv.itemNew.lParam = GetItemData(hNextItem);
tv.itemNew.mask = TVIF_HANDLE | TVIF_STATE | TVIF_PARAM;
tv.action = TVC_UNKNOWN;
pWnd->SendMessage(WM_NOTIFY, tv.hdr.idFrom, (LPARAM)&tv);
}
}
//由于本例中未调用基类的OnKeyDown(),
//我们必须向父类提供我们自己的TVN_KEYDOWN通知
CWnd* pWnd = GetParent();
if (pWnd)
{
NMTVKEYDOWN tvk;
tvk.hdr.hwndFrom = GetSafeHwnd();
tvk.hdr.idFrom = GetWindowLong(GetSafeHwnd(), GWL_ID);
tvk.hdr.code = TVN_KEYDOWN;
tvk.wVKey = nChar;
tvk.flags = 0;
pWnd->SendMessage(WM_NOTIFY, tvk.hdr.idFrom, (LPARAM)&tvk);
}
}
else
//行为正常
CTreeCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
}
void CTreeCtrlEx::SelectMultiple(HTREEITEM hClickedItem, UINT nFlags, CPoint point)
{
//开始准备NM_TreeView结构,以便在选择完成后发送通知
NM_TREEVIEW tv;
memset(&tv.itemOld, 0, sizeof(tv.itemOld));
CWnd* pWnd = GetParent();
HTREEITEM hOldItem = GetSelectedItem();
if (hOldItem)
{
tv.itemOld.hItem = hOldItem;
tv.itemOld.state = GetItemState(hOldItem, 0xffffffff);
tv.itemOld.lParam = GetItemData(hOldItem);
tv.itemOld.mask = TVIF_HANDLE | TVIF_STATE | TVIF_PARAM;
}
//表示选择过程未完成的标志。
//(将禁止TVN_SELCHANGED发送给父类)
m_bSelectionComplete = FALSE;
//操作取决于用户是按住Shift键还是Ctrl键
if (nFlags & MK_SHIFT)
{
//从第一个选定项目选择到单击的项目
if (!m_hFirstSelectedItem)
m_hFirstSelectedItem = GetSelectedItem();
SelectItems(m_hFirstSelectedItem, hClickedItem);
}
else if (nFlags & MK_CONTROL)
{
//只有一个根节点时,存储该根节点下的所有子节点;
//当有多个根节点时,还需其他方法存储所有节点,
//可以参考《MFC之TreeCtrl遍历所有节点》https://www.cnblogs.com/HelloQLQ/p/12678753.html
vector<HTREEITEM>vecItem;
HTREEITEM root = GetRootItem();//若存在多个根节点,获取的是第一个根节点
HTREEITEM child = GetChildItem(root);
while (child != NULL)
{
vecItem.push_back(child);
child = GetNextItem(child, TVGN_NEXT);
}
for (int i = 0; i < vecItem.size(); i++)
{
bIsSelectedItem[i] = GetItemState(vecItem[i], TVIS_SELECTED) & TVIS_SELECTED;
}
//CString cStr;//查看根节点下的子节点个数
//cStr.Format(_T("%d"), vecItem.size());
//MessageBox(cStr);
BOOL bIsClickedItemSelected = GetItemState(hClickedItem, TVIS_SELECTED) & TVIS_SELECTED;
//必须合成TVN_SELCHANGING通知
if (pWnd)
{
tv.hdr.hwndFrom = GetSafeHwnd();
tv.hdr.idFrom = GetWindowLong(GetSafeHwnd(), GWL_ID);
tv.hdr.code = TVN_SELCHANGING;
tv.itemNew.hItem = hClickedItem;
tv.itemNew.state = GetItemState(hClickedItem, 0xffffffff);
tv.itemNew.lParam = GetItemData(hClickedItem);
tv.itemOld.hItem = NULL;
tv.itemOld.mask = 0;
tv.action = TVC_BYMOUSE;
tv.ptDrag.x = point.x;
tv.ptDrag.y = point.y;
pWnd->SendMessage(WM_NOTIFY, tv.hdr.idFrom, (LPARAM)&tv);
}
for (int i = 0; i < vecItem.size(); i++)
{
if (bIsSelectedItem[i] && vecItem[i] == hClickedItem)
SetItemState(vecItem[i], 0, TVIS_SELECTED);
else if (bIsSelectedItem[i] && vecItem[i] != hClickedItem)
SetItemState(vecItem[i], TVIS_SELECTED, TVIS_SELECTED);
else if (!bIsSelectedItem[i] && vecItem[i] == hClickedItem)
SetItemState(vecItem[i], TVIS_SELECTED, TVIS_SELECTED);
else if (!bIsSelectedItem[i] && vecItem[i] != hClickedItem)
SetItemState(vecItem[i], 0, TVIS_SELECTED);
}
}
else
{
//首先清除所有“多选”项目的选择
ClearSelection();
//然后选择单击的项目
SelectItem(hClickedItem);
SetItemState(hClickedItem, TVIS_SELECTED, TVIS_SELECTED);
//存储为第一个选定项目
m_hFirstSelectedItem = hClickedItem;
}
//选择流程现已完成。由于我们已经销毁了Windows的treectrl提供的TVN_SELCHANGED通知,
//现在我们必须自己生成一个通知。
//这样我们的父类就可以知道选择的更改。
m_bSelectionComplete = TRUE;
if (pWnd)
{
tv.hdr.hwndFrom = GetSafeHwnd();
tv.hdr.idFrom = GetWindowLong(GetSafeHwnd(), GWL_ID);
tv.hdr.code = TVN_SELCHANGED;
tv.itemNew.hItem = m_hClickedItem;
tv.itemNew.state = GetItemState(m_hClickedItem, 0xffffffff);
tv.itemNew.lParam = GetItemData(m_hClickedItem);
tv.itemNew.mask = TVIF_HANDLE | TVIF_STATE | TVIF_PARAM;
tv.action = TVC_UNKNOWN;
pWnd->SendMessage(WM_NOTIFY, tv.hdr.idFrom, (LPARAM)&tv);
}
}
BOOL CTreeCtrlEx::SelectItems(HTREEITEM hFromItem, HTREEITEM hToItem)
{
//确定选择方向
//(查看树中排在第一位的项目)
HTREEITEM hItem = GetRootItem();
while (hItem && hItem != hFromItem && hItem != hToItem)
hItem = GetNextVisibleItem(hItem);
if (!hItem)
return FALSE; //树中不可见的项目
BOOL bReverse = hItem == hToItem;
// "Really" select the 'to' item 这将取消选择之前选择的项
SelectItem(hToItem);
//再次浏览所有可见项目,然后选择/取消选择
hItem = GetRootItem();
BOOL bSelect = FALSE;
while (hItem)
{
if (hItem == (bReverse ? hToItem : hFromItem))
bSelect = TRUE;
if (bSelect)
{
if (!(GetItemState(hItem, TVIS_SELECTED) & TVIS_SELECTED))
SetItemState(hItem, TVIS_SELECTED, TVIS_SELECTED);
}
else
{
if (GetItemState(hItem, TVIS_SELECTED) & TVIS_SELECTED)
SetItemState(hItem, 0, TVIS_SELECTED);
}
if (hItem == (bReverse ? hFromItem : hToItem))
bSelect = FALSE;
hItem = GetNextVisibleItem(hItem);
}
return TRUE;
}
void CTreeCtrlEx::ClearSelection(BOOL bMultiOnly/*=FALSE*/)
{
for (HTREEITEM hItem = GetRootItem(); hItem != NULL; hItem = GetNextVisibleItem(hItem))
if (GetItemState(hItem, TVIS_SELECTED) & TVIS_SELECTED)
SetItemState(hItem, 0, TVIS_SELECTED);
}