// MltiTree.cpp : implementation file // Copyright (c) 1999 Richard Hazlewood // This code is provided as-is. Use at your own peril. // // Multi-selection tree. Based, for the most part, on the // selection behaviour of the listview control. // TVN_SELCHANGING/TVN_SELCHANGED notifications are used // throughout: itemOld For de-selection, itemNew for selection. // Note: TVN_SELCHANGING/TVN_SELCHANGED are still sent by default // tree processing for focus changes, i.e. a SetItemState passed // TVIS_FOCUSED without TVIS_SELECTED will still cause notification // (if not already focused) //Decoding in TVN_SELCHANGED: //B = IsEmulatedNotify //O = itemOld.hItem != 0 //N = itemNew.hItem != 0 // //B O N //~~~~~~~ //0 1 0 A focus loss on itemOld //0 0 1 A focus/selection gain on itemNew //0 1 1 A focus loss on itemOld, a focus/selection gain on itemNew //1 1 0 A selection loss on itemOld //1 0 1 A selection gain on itemNew //else undefined #include "stdafx.h" #include #include "MltiTree.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// #ifndef MST_TIMER_PERIOD #define MST_TIMER_PERIOD 75 //ms #endif ///////////////////////////////////////////////////////////////////////////// // CMultiTree IMPLEMENT_DYNAMIC(CMultiTree, CMultiTree_BASE) CMultiTree::CMultiTree() { m_bMulti = TRUE; m_hSelect = NULL; m_bBandLabel = FALSE; m_bEmulated = FALSE; } CMultiTree::~CMultiTree() { } #define CTreeCtrl CMultiTree_BASE BEGIN_MESSAGE_MAP(CMultiTree, CTreeCtrl) #undef CTreeCtrl //{{AFX_MSG_MAP(CMultiTree) ON_WM_LBUTTONDOWN() ON_WM_SETFOCUS() ON_WM_KILLFOCUS() ON_WM_RBUTTONDOWN() ON_NOTIFY_REFLECT_EX(TVN_ITEMEXPANDING, OnItemExpanding) ON_WM_KEYDOWN() //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CMultiTree message handlers ///////////////////////////////////////////////////////////////////////////// // GetSelectedCount // - returns number of selection tree items UINT CMultiTree::GetSelectedCount() const { UINT nCount = 0; HTREEITEM hItem = GetFirstSelectedItem(); while (hItem) { nCount++; hItem = GetNextSelectedItem(hItem); } return nCount; } ///////////////////////////////////////////////////////////////////////////// // SetMultiSelect // - allow mode to be turned off BOOL CMultiTree::SetMultiSelect(BOOL bMulti) { BOOL b = m_bMulti; m_bMulti = bMulti; if (!m_bMulti) { HTREEITEM hItem = GetSelectedItem(); if (hItem && !IsSelected(hItem)) hItem = NULL; SelectAllIgnore(FALSE, hItem); if (hItem) SelectItem(hItem); } return b; } ///////////////////////////////////////////////////////////////////////////// // SetItemState // - replacement to handle TVIS_FOCUSED BOOL CMultiTree::SetItemState(HTREEITEM hItem, UINT nState, UINT nStateMask) { ASSERT(hItem); if (!m_bMulti) return CMultiTree_BASE::SetItemState(hItem, nState, nStateMask); HTREEITEM hFocus = GetSelectedItem(); //current focus BOOL bWasFocus = (hFocus == hItem); BOOL bFocusWasSel = hFocus && IsSelected(hFocus); //selection state of current focus BOOL bWasSel = IsSelected(hItem); //select state of acting item UINT nS = nState & ~TVIS_FOCUSED; UINT nSM = nStateMask & ~TVIS_FOCUSED; if (nStateMask & TVIS_FOCUSED) { //wanted to affect focus if (nState & TVIS_FOCUSED) { //wanted to set focus if (!bWasFocus && bFocusWasSel) { //because SelectItem would de-select the current 'real' selection // (the one with focus), need to make the tree ctrl think there is // no 'real' selection but still keep the the old item selected //it has to be done before the SelectItem call because // otherwise the TVN_SELCHANGING/ED notification handlers // wouldn't be able to get the proper list of selected items CMultiTree_BASE::SelectItem(NULL); //will cause notify, but can be taken as focus change CMultiTree_BASE::SetItemState(hFocus, TVIS_SELECTED, TVIS_SELECTED); UpdateWindow(); } if (!CMultiTree_BASE::SelectItem(hItem)) //set focus (will consequently select, if not already focused) return FALSE; if (nStateMask & TVIS_SELECTED) { //wanted to affect select state if (nState & TVIS_SELECTED) { //wanted to select, already done if wasn't focused if (!bWasFocus || bFocusWasSel) { nS &= ~TVIS_SELECTED; nSM &= ~TVIS_SELECTED; } } //else wanted to clear, base call will do that } else { //didn't want to affect select state if (!bWasSel) { //it wasn't previously selected, let base clear (correct) nS &= ~TVIS_SELECTED; nSM |= TVIS_SELECTED; } //else was already selected, no harm done } } else { //wanted to clear focus if (bWasFocus) { //it had the focus CMultiTree_BASE::SelectItem(NULL); //clear focus if (!(nStateMask & TVIS_SELECTED)) { //didn't want to affect select state if (bWasSel) { //it was selected, so restore ASSERT( !(nS & TVIS_SELECTED) ); ASSERT( !(nSM & TVIS_SELECTED) ); //set state here, to avoid double-notify CMultiTree_BASE::SetItemState(hItem, TVIS_SELECTED, TVIS_SELECTED); //let base do other states } } else if (nState & TVIS_SELECTED) { //wanted to select (but clear focus) if (bWasSel) { //if was selected, restore CMultiTree_BASE::SetItemState(hItem, TVIS_SELECTED, TVIS_SELECTED); } //don't want to notify, default did it nS &= ~TVIS_SELECTED; nSM &= ~TVIS_SELECTED; } } } } if (!nSM) return TRUE; //no other states to alter if (nSM & TVIS_SELECTED) { //still need to alter selection state NMTREEVIEW nmtv; nmtv.hdr.hwndFrom = m_hWnd; nmtv.hdr.idFrom = ::GetDlgCtrlID(m_hWnd); nmtv.hdr.code = TVN_SELCHANGING; nmtv.itemOld.mask = nmtv.itemNew.mask = 0; nmtv.itemOld.hItem = nmtv.itemNew.hItem = NULL; TVITEM& item = (nS & TVIS_SELECTED) ? nmtv.itemNew : nmtv.itemOld; item.mask = TVIF_HANDLE|TVIF_PARAM; item.hItem = hItem; item.lParam = GetItemData(hItem); if (_SendNotify(&nmtv.hdr)) return FALSE; //sel-changing stopped VERIFY( CMultiTree_BASE::SetItemState(hItem, nS, nSM) ); nmtv.hdr.code = TVN_SELCHANGED; _SendNotify(&nmtv.hdr); nS &= ~TVIS_SELECTED; nSM &= ~TVIS_SELECTED; } if (!nSM) return TRUE; return CMultiTree_BASE::SetItemState(hItem, nS, nSM); } UINT CMultiTree::GetItemState(HTREEITEM hItem, UINT nStateMask) const { UINT n = CMultiTree_BASE::GetItemState(hItem, nStateMask & ~TVIS_FOCUSED); if (nStateMask & TVIS_FOCUSED) if (GetSelectedItem() == hItem) n |= TVIS_FOCUSED; return n; } BOOL CMultiTree::SelectItem(HTREEITEM hItem) { if (m_bMulti) { //TRACE(_T("Use SetItemState or FocusItem when in multi-select mode\n")); ASSERT(FALSE); } return CMultiTree_BASE::SelectItem(hItem); } BOOL CMultiTree::FocusItem(HTREEITEM hItem) { ASSERT(m_bMulti); BOOL bRet = FALSE; if (hItem) bRet = SetItemState(hItem, TVIS_FOCUSED, TVIS_FOCUSED); else { hItem = CMultiTree_BASE::GetSelectedItem(); if (hItem) bRet = SetItemState(hItem, 0, TVIS_FOCUSED); } return bRet; } ///////////////////////////////////////////////////////////////////////////// // _SendNotify // - helper to distinguish between default control generated notifications // and this classes emulated ones (so can tell if focus or select notify) BOOL CMultiTree::_SendNotify(LPNMHDR pNMHDR) { ASSERT(::GetParent(m_hWnd)); //never expected this BOOL b = m_bEmulated; m_bEmulated = TRUE; BOOL bRes = ::SendMessage(::GetParent(m_hWnd), WM_NOTIFY, (WPARAM)pNMHDR->idFrom, (LPARAM)pNMHDR); m_bEmulated = b; return bRes; } ///////////////////////////////////////////////////////////////////////////// // Selection Parsing HTREEITEM CMultiTree::GetFirstSelectedItem() const { HTREEITEM hItem = GetRootItem(); while (hItem) { if (IsSelected(hItem)) break; hItem = GetNextVisibleItem(hItem); } return hItem; } HTREEITEM CMultiTree::GetNextSelectedItem(HTREEITEM hItem) const { hItem = GetNextVisibleItem(hItem); while (hItem) { if (IsSelected(hItem)) break; hItem = GetNextVisibleItem(hItem); } return hItem; } void CMultiTree::SelectAll(BOOL bSelect /*= TRUE*/) { bSelect = !!bSelect; //ensure 0 or 1 UINT nState = bSelect ? TVIS_SELECTED : 0; HTREEITEM hItem = GetRootItem(); while (hItem) { if (IsSelected(hItem) != bSelect) SetItemState(hItem, nState, TVIS_SELECTED); hItem = GetNextVisibleItem(hItem); } } void CMultiTree::SelectAllIgnore(BOOL bSelect, HTREEITEM hIgnore) { //special case to avoid multiple notifications for // the same item bSelect = !!bSelect; //ensure 0 or 1 UINT nState = bSelect ? TVIS_SELECTED : 0; HTREEITEM hItem = GetRootItem(); while (hItem) { if (hItem != hIgnore) if (IsSelected(hItem) != bSelect) SetItemState(hItem, nState, TVIS_SELECTED); hItem = GetNextVisibleItem(hItem); } } void CMultiTree::SelectRange(HTREEITEM hFirst, HTREEITEM hLast, BOOL bOnly /*= TRUE*/) { //locate (and select) either first or last // (so order is arbitrary) HTREEITEM hItem = GetRootItem(); while (hItem) { if ((hItem == hFirst) || (hItem == hLast)) { if (hFirst != hLast) { if (!IsSelected(hItem)) SetItemState(hItem, TVIS_SELECTED, TVIS_SELECTED); hItem = GetNextVisibleItem(hItem); } break; } if (bOnly && IsSelected(hItem)) SetItemState(hItem, 0, TVIS_SELECTED); hItem = GetNextVisibleItem(hItem); } //select rest of range while (hItem) { if (!IsSelected(hItem)) SetItemState(hItem, TVIS_SELECTED, TVIS_SELECTED); if ((hItem == hFirst) || (hItem == hLast)) { hItem = GetNextVisibleItem(hItem); break; } hItem = GetNextVisibleItem(hItem); } if (!bOnly) return; while (hItem) { if (IsSelected(hItem)) SetItemState(hItem, 0, TVIS_SELECTED); hItem = GetNextVisibleItem(hItem); } } ///////////////////////////////////////////////////////////////////////////// // OnButtonDown #define _bShift (nFlags & MK_SHIFT) #define _bCtrl (nFlags & MK_CONTROL) void CMultiTree::OnLButtonDown(UINT nFlags, CPoint point) { OnButtonDown(TRUE, nFlags, point); } void CMultiTree::OnRButtonDown(UINT nFlags, CPoint point) { OnButtonDown(FALSE, nFlags, point); } void CMultiTree::OnButtonDown(BOOL bLeft, UINT nFlags, CPoint point) { UINT nHF; HTREEITEM hItem; //if (::GetFocus() != m_hWnd) //::SetFocus(m_hWnd); BOOL bBase = !m_bMulti; if (!bBase) { hItem = HitTest(point, &nHF); if (hItem) { //base always handles expanding items //(doesn't really mean much to right button, but pass anyway) bBase = (nHF & (TVHT_ONITEMBUTTON/*|TVHT_ONITEMINDENT*/)); if (!bBase && bLeft && (GetStyle() & TVS_CHECKBOXES)) { //when the tree has check-boxes, the default handler makes // a quick selection of the clicked item, then re-selects // the previously selected item - to cause a sel-changed // notification. Fortunately it doesn't affect the multi- // selection, so can pass on. bBase = (nHF & TVHT_ONITEMSTATEICON); #ifdef _MST_MULTI_CHECK //Use the above define if you want all selected items to // be checked the same when any one of them is checked // - Interestingly this doesn't happen in the listview control // (LVS_EX_CHECKBOXES) if (bBase) { //the default selection notification would mess // the multi-selection up, so generate the notification // manually // (anyway, this is smoother than the selection flicker // the default gives) NMTREEVIEW nmtv; #ifdef TVN_CHKCHANGE nmtv.hdr.code = TVN_CHKCHANGE; #else nmtv.hdr.code = TVN_SELCHANGED; #endif nmtv.hdr.hwndFrom = m_hWnd; nmtv.hdr.idFrom = ::GetDlgCtrlID(m_hWnd); nmtv.itemOld.hItem = NULL; nmtv.itemNew.mask = TVIF_HANDLE|TVIF_PARAM; BOOL bChk = !GetCheck(hItem); if (IsSelected(hItem)) { HTREEITEM h = GetFirstSelectedItem(); while (h) { if (!GetCheck(h) == bChk) { //! to ensure 0 or 1 SetCheck(h, bChk); #ifdef TVN_CHKCHANGE //only send multiple check-change // notifications (not sel-changed) if (h != hItem) { //clicked item will be done last nmtv.itemNew.hItem = h; nmtv.itemNew.lParam = GetItemData(h); _SendNotify(&nmtv.hdr); } #endif } h = GetNextSelectedItem(h); } } else SetCheck(hItem, bChk); //notify clicked item nmtv.itemNew.hItem = hItem; nmtv.itemNew.lParam = GetItemData(hItem); _SendNotify(&nmtv.hdr); return; } #endif } } } if (bBase) { if (bLeft) CMultiTree_BASE::OnLButtonDown(nFlags, point); else CMultiTree_BASE::OnRButtonDown(nFlags, point); return; } if (!hItem || (nHF & (TVHT_ONITEMRIGHT|TVHT_NOWHERE|TVHT_ONITEMINDENT))) { //clicked in space, do rubber-banding DoBanding(bLeft, nFlags, point); return; } ASSERT(nHF & (TVHT_ONITEM|TVHT_ONITEMSTATEICON) ); //nothing else left DoPreSelection(hItem, bLeft, nFlags); DoAction(hItem, bLeft, nFlags, point); } void CMultiTree::DoPreSelection(HTREEITEM hItem, BOOL bLeft, UINT nFlags) { if (bLeft) { //if shift key down, select immediately if (_bShift) { if (!m_hSelect) m_hSelect = GetSelectedItem(); //focus SelectRange(m_hSelect, hItem, !_bCtrl); SetItemState(hItem, TVIS_FOCUSED, TVIS_FOCUSED); //focus changes to last clicked } else { if (!_bCtrl) { //if ctrl was down, then the selection is delayed until // mouse up, otherwise select the one item if (!IsSelected(hItem)) SelectAllIgnore(FALSE, hItem); SetItemState(hItem, TVIS_SELECTED|TVIS_FOCUSED, TVIS_SELECTED|TVIS_FOCUSED); } m_hSelect = NULL; //reset when a non-shift operation occurs } return; } //right mouse if (nFlags & (MK_CONTROL|MK_SHIFT)) { if (!_bShift) m_hSelect = hItem; return; //do nothing if shift or ctrl } if (!IsSelected(hItem)) SelectAllIgnore(FALSE, hItem); SetItemState(hItem, TVIS_SELECTED|TVIS_FOCUSED, TVIS_SELECTED|TVIS_FOCUSED); } void CMultiTree::DoAction(HTREEITEM hItem, BOOL bLeft, UINT nFlags, CPoint point) { ::SetCapture(m_hWnd); ASSERT(::GetCapture() == m_hWnd); MSG msg; UINT nDone = 0; CPoint pt; CSize sizeDrag(::GetSystemMetrics(SM_CXDRAG), ::GetSystemMetrics(SM_CYDRAG)); while (!nDone && ::GetMessage(&msg, NULL, 0, 0)) { if (::GetCapture() != m_hWnd) break; switch (msg.message) { case WM_MOUSEMOVE: pt.x = GET_X_LPARAM(msg.lParam); pt.y = GET_Y_LPARAM(msg.lParam); if ((abs(pt.x - point.x) > sizeDrag.cx) || ((abs(pt.y - point.y) > sizeDrag.cy)) ) nDone = 2; //because we exit loop, button up will still be dispatched // which means WM_CONTEXTMENU will be sent after TVN_BEGINRDRAG // - this is the same behaviour as original tree break; case WM_LBUTTONUP: case WM_RBUTTONUP: nDone = 1; break; default: ::DispatchMessage(&msg); break; } } ::ReleaseCapture(); ASSERT(::GetCapture() != m_hWnd); //construct tree notification info NMTREEVIEW nmtv; nmtv.hdr.hwndFrom = m_hWnd; nmtv.hdr.idFrom = ::GetDlgCtrlID(m_hWnd); nmtv.itemNew.mask = TVIF_HANDLE|TVIF_PARAM; nmtv.itemNew.hItem = hItem; nmtv.itemNew.lParam = GetItemData(hItem); DWORD dwStyle = GetStyle(); if (nDone == 1) { //click if (!_bShift && bLeft) { UINT nState = TVIS_SELECTED; if (_bCtrl) nState ^= (GetItemState(hItem, TVIS_SELECTED) & TVIS_SELECTED); else SelectAllIgnore(FALSE, hItem); SetItemState(hItem, TVIS_FOCUSED|nState, TVIS_FOCUSED|TVIS_SELECTED); } if (::GetFocus() != m_hWnd) ::SetFocus(m_hWnd); nmtv.hdr.code = bLeft ? NM_CLICK : NM_RCLICK; _SendNotify(&nmtv.hdr); } else if (nDone == 2) { //drag SetItemState(hItem, TVIS_FOCUSED|TVIS_SELECTED, TVIS_FOCUSED|TVIS_SELECTED); if (!(dwStyle & TVS_DISABLEDRAGDROP)) { nmtv.hdr.code = bLeft ? TVN_BEGINDRAG : TVN_BEGINRDRAG; nmtv.ptDrag = point; _SendNotify(&nmtv.hdr); } } } void CMultiTree::DoBanding(BOOL bLeft, UINT nFlags, CPoint point) { if (::GetFocus() != m_hWnd) ::SetFocus(m_hWnd); ::SetCapture(m_hWnd); CTreeItemList list; if (nFlags & (MK_SHIFT|MK_CONTROL)) GetSelectedList(list); CClientDC dc(this); CRect rectCli; GetClientRect(&rectCli); MSG msg; BOOL bDone = FALSE; CPoint pt; CSize sizeDrag(::GetSystemMetrics(SM_CXDRAG), ::GetSystemMetrics(SM_CYDRAG)); BOOL bDrag = FALSE; CSize sizeEdge(1, 1); UINT nTimer = SetTimer(1, MST_TIMER_PERIOD, NULL); //for scroll CPoint ptScr(GetScrollPos(SB_HORZ), GetScrollPos(SB_VERT)); CRect rect(0, 0, 0, 0); UINT h = 0; HTREEITEM hItem = GetRootItem(); if (hItem) { GetItemRect(hItem, &rect, FALSE); ptScr.y *= (h = rect.Height()); //this assumes equal height items } point += ptScr; while (!bDone && ::GetMessage(&msg, NULL, 0, 0)) { if (::GetCapture() != m_hWnd) break; switch (msg.message) { case WM_TIMER: pt = msg.pt; ScreenToClient(&pt); if (rectCli.PtInRect(pt)) { ::DispatchMessage(&msg); break; } msg.lParam = MAKELPARAM(pt.x, pt.y); //fall through to mousemove case WM_MOUSEMOVE: pt.x = GET_X_LPARAM(msg.lParam); pt.y = GET_Y_LPARAM(msg.lParam); if (!bDrag) { if ((abs(pt.x - point.x) <= sizeDrag.cx) && ((abs(pt.y - point.y) <= sizeDrag.cy)) ) break; bDrag = TRUE; if (!(nFlags & (MK_CONTROL|MK_SHIFT))) SelectAll(FALSE); UpdateWindow(); rect.SetRect(point, point); dc.DrawDragRect(rect, sizeEdge, NULL, sizeEdge); } dc.DrawDragRect(rect, sizeEdge, NULL, sizeEdge); //delete if (pt.y < rectCli.top) ::SendMessage(m_hWnd, WM_VSCROLL, SB_LINEUP, 0); else if (pt.y >= rectCli.bottom) ::SendMessage(m_hWnd, WM_VSCROLL, SB_LINEDOWN, 0); if (pt.x < rectCli.left) ::SendMessage(m_hWnd, WM_HSCROLL, SB_LINELEFT, 0); else if (pt.x >= rectCli.right) ::SendMessage(m_hWnd, WM_HSCROLL, SB_LINERIGHT, 0); ptScr = point; ptScr.x -= GetScrollPos(SB_HORZ); ptScr.y -= GetScrollPos(SB_VERT) * h; rect.SetRect(ptScr, pt); rect.NormalizeRect(); UpdateSelectionForRect(rect, nFlags, list); dc.DrawDragRect(rect, sizeEdge, NULL, sizeEdge); //draw break; case WM_LBUTTONUP: case WM_RBUTTONUP: bDone = TRUE; break; case WM_KEYDOWN: if (LOWORD(msg.wParam) == VK_ESCAPE) { SelectAll(FALSE); bDone = TRUE; break; } //dispatch default: ::DispatchMessage(&msg); break; } } KillTimer(nTimer); ::ReleaseCapture(); if (bDrag) dc.DrawDragRect(rect, sizeEdge, NULL, sizeEdge); else if (!(nFlags & (MK_CONTROL|MK_SHIFT))) SelectAll(FALSE); //construct notification info NMHDR hdr; hdr.hwndFrom = m_hWnd; hdr.idFrom = ::GetDlgCtrlID(m_hWnd); hdr.code = bLeft ? NM_CLICK : NM_RCLICK; _SendNotify(&hdr); //when banding, make sure we receive WM_CONTEXTMENU // every time - which is what the listview ctrl does ::DispatchMessage(&msg); } void CMultiTree::UpdateSelectionForRect(LPCRECT pRect, UINT nFlags, CTreeItemList& list) { CRect rect; BOOL bSel; POSITION pos; HTREEITEM hItem = GetRootItem(); while (hItem) { bSel = IsSelected(hItem); GetItemRect(hItem, &rect, m_bBandLabel); if (rect.IntersectRect(rect, pRect)) { //item in rect pos = list.Find(hItem); if (!bSel && !pos) SetItemState(hItem, TVIS_SELECTED, TVIS_SELECTED); else if (_bCtrl && pos) SetItemState(hItem, 0, TVIS_SELECTED); else if (_bShift && pos) list.RemoveAt(pos); //if shift and in rect, don't lock anymore } else { //item not in rect pos = list.Find(hItem); if (bSel && !pos) SetItemState(hItem, 0, TVIS_SELECTED); else if (pos) SetItemState(hItem, TVIS_SELECTED, TVIS_SELECTED); } hItem = GetNextVisibleItem(hItem); } UpdateWindow(); } void CMultiTree::OnSetFocus(CWnd* pOldWnd) { CMultiTree_BASE::OnSetFocus(pOldWnd); if (m_bMulti) { //'emulated' selected items will remain greyed // if application gets covered HTREEITEM hItem = GetFirstSelectedItem(); RECT rect; while (hItem) { GetItemRect(hItem, &rect, TRUE); InvalidateRect(&rect); hItem = GetNextSelectedItem(hItem); } } } void CMultiTree::OnKillFocus(CWnd* pNewWnd) { CMultiTree_BASE::OnKillFocus(pNewWnd); if (m_bMulti) { //'emulated' selected items may not get // refreshed to grey HTREEITEM hItem = GetFirstSelectedItem(); RECT rect; while (hItem) { GetItemRect(hItem, &rect, TRUE); InvalidateRect(&rect); hItem = GetNextSelectedItem(hItem); } } } BOOL CMultiTree::OnItemExpanding(NMHDR* pNMHDR, LRESULT* pResult) { if (!m_bMulti) return FALSE; LPNMTREEVIEW pNMTreeView = (LPNMTREEVIEW)pNMHDR; *pResult = 0; if ((pNMTreeView->action == TVE_COLLAPSE) || (pNMTreeView->action == TVE_COLLAPSERESET)) { //clear selection of children, it would be confusing otherwise // - the notifications can be over-ridden to stop the de-selection // if required //unfortunately, the parent window can't over-ride this functionality // because MFC gives this class first crack. So if changes are required // a derived class will have to be used.. ASSERT(pNMTreeView->itemNew.hItem); //if a descendent item has focus the parent will get selected as a // consequence of collapsing the tree, so preserve // (if the parent was already selected it will gain focus, but // that's acceptable) BOOL bWasSel = IsSelected(pNMTreeView->itemNew.hItem); BOOL bWasFocus = SelectChildren(pNMTreeView->itemNew.hItem, FALSE, TRUE); if (bWasFocus && !bWasSel) CMultiTree_BASE::SelectItem(NULL); //stop parent from gaining selection } return FALSE; //pass to parent } BOOL CMultiTree::SelectChildren(HTREEITEM hParent, BOOL bSelect /*= TRUE*/, BOOL bRecurse /*= TRUE*/) { UINT nS = bSelect ? TVIS_SELECTED : 0; BOOL bFocusWasInHere = FALSE; HTREEITEM hItem = GetNextItem(hParent, TVGN_CHILD); while (hItem) { UINT nState = GetItemState(hItem, TVIS_SELECTED|TVIS_EXPANDED|TVIS_FOCUSED); if ((nState & TVIS_SELECTED) != nS) SetItemState(hItem, nS, TVIS_SELECTED); bFocusWasInHere |= (nState & TVIS_FOCUSED); if (bRecurse && (nState & TVIS_EXPANDED)) bFocusWasInHere |= SelectChildren(hItem, bSelect, bRecurse); hItem = GetNextSiblingItem(hItem); } return bFocusWasInHere; } void CMultiTree::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { if (!m_bMulti) { CMultiTree_BASE::OnKeyDown(nChar, nRepCnt, nFlags); return; } BOOL bCtrl = (GetKeyState(VK_CONTROL) & 0x8000); BOOL bShift = (GetKeyState(VK_SHIFT) & 0x8000); BOOL bDir = FALSE; HTREEITEM hSel = NULL; switch (nChar) { case VK_UP: bDir = TRUE; case VK_DOWN: //common hSel = GetSelectedItem(); if (!m_hSelect) m_hSelect = hSel; if (!bCtrl && !bShift) { m_hSelect = NULL; //reset SelectAll(FALSE); } break; // case VK_SPACE: // hSel = GetSelectedItem(); // if (hSel) { // BOOL b = IsSelected(hSel); // if (bCtrl) // SetItemState(hSel, b ? 0 : TVIS_SELECTED, TVIS_SELECTED); // else if (!b) // SetItemState(hSel, TVIS_SELECTED, TVIS_SELECTED); // } // return; //don't call base class (it would start a search on the char) //TODO: the text search isn't stopped by this ^. It may be done in the TranslateMessage, // so would have to trap PreTranslateMessage to avoid it. If 'space' selection is // required then need to implement - I'm not bothered. } CMultiTree_BASE::OnKeyDown(nChar, nRepCnt, nFlags); if (!hSel || (!bCtrl && !bShift) ) return; HTREEITEM hNext = bDir ? GetPrevVisibleItem(hSel) : GetNextVisibleItem(hSel); if (!hNext) hNext = hSel; if (bShift) SelectRange(m_hSelect, hNext, TRUE); else if (bCtrl) SetItemState(hNext, TVIS_FOCUSED, TVIS_FOCUSED); } void CMultiTree::GetSelectedList(CTreeItemList& list) const { list.RemoveAll(); HTREEITEM hItem = GetFirstSelectedItem(); while (hItem) { list.AddTail(hItem); hItem = GetNextSelectedItem(hItem); } }