介绍 标准的Windows拖列表框(MFC中的CDragListBox)有一个众所周知的缺点,那就是它不支持多重选择。它还不支持从一个列表框拖拽到另一个列表框;只允许在列表框内重新排序。我试图解决这些缺点,结果创建了两个新类: 一个替换CDragListBox的工具,但是支持多重选择 背景 最终的目标是创建一个用于在列表视图中添加/删除列的对话框,类似于Outlook中的“自定义视图”对话框。我本可以完全避免拖拽,而使用“添加/删除”和“向上/向下移动”按钮来代替,但那样做会很蹩脚。 各种拖拉列表框解决方案在CodeProject的其他地方都可以找到,但是没有一个具有我想要的特性组合。具体来说,我想: 支持单个、多个和扩展的选择,支持从一个列表框拖拽到另一个,但不使用OLEDrag反馈类似于Outlook的“自定义视图”对话框,支持通过Escape键取消拖拽 为什么我不想用OLE?因为: 这是一个hassleI希望我的解决方案是简单和轻量级的,andOLE对于在两个列表框之间拖拽条目来说太过了 我看着源于CDragListBox但这并没有提供任何好处,因为实现MFC CDragListBox可用的来源,大多数真正的工作是通过DL_ *消息(DL_BEGINDRAG等等),和实现信息的隐藏在窗口列表框控件本身。经过一番折腾后,我放弃了,决定直接从CListBox派生。不过,我还是尽可能地保留了CDragListBox的界面(下面将详细介绍)。因此,应该可以用我的任何一个类替换CDragListBox的现有实例,只需对调用代码进行少量或不进行修改。 使用的代码 使用派生类很简单。如果您只需要在列表框内重新排序项目,请使用CDragListBoxEx。如果您还需要在列表框之间拖放项目,请使用CInterDragListBox。任何一个类都可以作为MFC的CDragListBox的临时替代。 CDragListBoxEx 由于CDragListBoxEx与CDragListBox具有相同的接口,所以MFC文档仍然适用。唯一的区别是: 支持多个和扩展选择,提供可覆盖的端拖;DrawInsert overridable现在带一个额外的布尔参数(Enable), true用于绘制插入标记,false用于擦除之前绘制的插入标记 列表框项可以有关联的数据,拖拽不仅可以移动项的文本,还可以移动它们的关联数据。SetItemData和SetItemDataPtr函数将用户定义的数据与列表框项关联起来。 注意,大多数可覆盖函数接收一个CPoint作为参数,这个点在屏幕坐标中,就像在MFC的CDragListBox中一样。我曾尝试过切换到客户端坐标,但这是一个错误,原因有两个:它会破坏现有的CDragListBox的使用,并且会使CInterDragListBox的实现更加复杂。经验法则是,如果一个控件将一个点暴露在该控件的上下文之外(例如在其他控件中),那么这个点应该在屏幕坐标中。 还要注意,不能对已排序列表框中的项重新排序。那是最不合逻辑的,上尉! CInterDragListBox CInterDragListBox派生自CDragListBoxEx,具有相同的接口,所以MFC文档仍然适用。区别只是在实现上:某些虚函数被覆盖,允许在列表框之间拖拽,以及在列表框内拖拽(重新排序)。 注意,唯一有效的拖放目标是同一个应用程序中的CInterDragListBox实例。如果您需要其他类型的控件作为拖放目标,您需要自己实现,通过修改拖动覆盖。如果您需要在其他应用程序中支持drop目标,您应该使用OLE,这个项目不适合您。 还要注意,只支持移动;没有实现复制。要实现复制,需要覆盖drop并使用更复杂的方法替换对MoveSelectedItems的调用。GetSelectedItems可能仍然有用,因为它能够剪切或复制。它的第三个参数(Delete)在默认情况下为真,但如果为假,则复制所选内容,而不是删除。PasteItems可能仍然有用。 的兴趣点 CDragListBoxEx中最重要的变量是拖拽状态(m_DragState),它的枚举如下: 隐藏,复制Code
enum { // drag states DTS_NONE, // not dragging DTS_TRACK, // left button down, but motion hasn't reached drag threshold DTS_PREDRAG, // motion reached drag threshold, but drag hasn't started yet DTS_DRAG, // drag is in progress };
当按下左键时,控件进入DTS_TRACK状态,导致它捕获光标,并通过OnMouseMove监视鼠标。一个问题是,在列表框扩展选择的默认实现中,一个没有Ctrl或Shift修饰键的左键向下启动一个新的选择。这使得不可能拖动多个项目,因为开始拖动将清除多个选择。大概这就是为什么CDragListBox不允许多重选择。在本例中,简单的解决方案是将清除选择延迟到鼠标向上处理程序。一个成员变量(m_DeferSelect)也被设置,因此鼠标处理程序不需要重复同样的测试。 隐藏,复制Code
void CDragListBoxEx::OnLButtonDown(UINT nFlags, CPoint point) { if (m_DragState == DTS_NONE) { m_DragState = DTS_TRACK; m_DragOrigin = point; SetCapture(); } // if extended selection mode and multiple items are selected, don't alter // selection on button down without modifier keys; could be start of drag m_DeferSelect = (GetStyle() & LBS_EXTENDEDSEL) && GetSelCount() > 1 && !(nFlags & (MK_SHIFT | MK_CONTROL)); if (!m_DeferSelect) CListBox::OnLButtonDown(nFlags, point); }
如果鼠标移动超过拖动阈值,则调用BeginDrag,如果返回TRUE,则状态更改为DTS_DRAG。这允许派生的BeginDrag以自己的任意理由不允许拖拽。拖动阈值是一个Windows系统指标,可从GetSystemMetrics (SM_CXDRAG和SM_CYDRAG)获得。 隐藏,复制Code
void CDragListBoxEx::OnMouseMove(UINT nFlags, CPoint point) { CPoint spt(point); ClientToScreen(&spt); if (m_DragState == DTS_TRACK) { // if tracking if (nFlags & (MK_SHIFT | MK_CONTROL)) // if modifier keys CListBox::OnMouseMove(nFlags, point);// delegate to base class else { // no modifier keys // if motion in either axis exceeds drag threshold if (abs(m_DragOrigin.x - point.x) > GetSystemMetrics(SM_CXDRAG) || abs(m_DragOrigin.y - point.y) > GetSystemMetrics(SM_CYDRAG)) { if (BeginDrag(spt)) m_DragState = DTS_DRAG; // we're dragging } } } if (m_DragState == DTS_DRAG) // if dragging Dragging(spt); }
BeginDrag实现大部分只是重置了一些成员,只是为了确定。唯一复杂的是,在扩展选择模式下,可以通过按住左键同时移动鼠标来选择项目。这可能会导致在拖动开始后选择其他项目。解决方案是通过发送WM_LBUTTONUP来完成基类的button up行为。然而,这也会导致我们的按钮向上处理程序(见下文)被调用,它通常会结束拖放操作,因此有必要引入中间状态DTS_PREDRAG,我们的按钮向上处理程序会忽略这个中间状态。发送WM_LBUTTONUP也释放捕获,所以我们必须在之后再次设置捕获。 隐藏,复制Code
BOOL CDragListBoxEx::BeginDrag(CPoint point) { UNREFERENCED_PARAMETER(point); m_PrevInsPos = -1; m_PrevTop = -1; m_DragState = DTS_PREDRAG; SendMessage(WM_LBUTTONUP, 0, 0); // avoids extending selection if (::GetCapture() != m_hWnd) // make sure we still have capture SetCapture(); return(TRUE); }
我们的按钮处理程序必须处理所有可能的拖放状态。对于DTS_NONE和DTS_PREDRAG,我们执行基类行为。如果状态是DTS_TRACK,则不会开始拖放,因为没有超过阈值。首先,我们检查m_DeferSelect(参见OnLButtonDown),如果它被设置,我们清除当前的选择,并选择光标下的单个项目。然后我们调用EndDrag来释放捕获并重置状态。如果状态是DTS_DRAG,我们将通过拖放结束拖放,因此除了调用EndDrag之外,我们还调用drop overridable,它负责将选定的项目移动到它们的新位置。 隐藏,复制Code
void CDragListBoxEx::OnLButtonUp(UINT nFlags, CPoint point) { CPoint spt(point); ClientToScreen(&spt); switch (m_DragState) { case DTS_NONE: case DTS_PREDRAG: CListBox::OnLButtonUp(nFlags, point); break; case DTS_TRACK: if (m_DeferSelect) { // if selection deferred in button down SetSel(-1, FALSE); // clear selection int pos = HitTest(spt); if (pos >= 0) SetSel(pos, TRUE); // select one item } EndDrag(); break; case DTS_DRAG: EndDrag(); Dropped(spt); break; } }
其余的复杂操作包括滚动、绘制插入点和设置适当的光标。这些都是通过拖动overridable完成的,它在每次鼠标移动时都被调用(参见OnMouseMove)。 隐藏,复制Code
UINT CDragListBoxEx::Dragging(CPoint point) { AutoScroll(point); UpdateInsert(point); if (::WindowFromPoint(point) == m_hWnd) SetCursor(AfxGetApp()->LoadCursor(IDC_DRAG_MOVE)); else // not in this list box SetCursor(LoadCursor(NULL, IDC_NO)); return(DL_CURSORSET); }
如果游标在列表框顶部或底部的一行内移动,则通过启用计时器来实现滚动。实际的滚动是由计时器消息处理程序(OnTimer)完成的,它从m_ScrollDelta成员变量中获得所需的滚动:-1(向上)、1(向下)或0(无)。出于效率考虑,滚动计时器只在需要时才创建。 隐藏,复制Code
void CDragListBoxEx::AutoScroll(CPoint point) { CRect cr, ir, hr; GetClientRect(cr); GetItemRect(0, ir); int margin = ir.Height(); CPoint cpt(point); ScreenToClient(&cpt); if (cpt.y < cr.top + margin) // if cursor is above top boundary m_ScrollDelta = -1; // start scrolling up else if (cpt.y >= cr.bottom - margin) // if cursor is below bottom boundary m_ScrollDelta = 1; // start scrolling down else m_ScrollDelta = 0; // stop scrolling if (m_ScrollDelta && !m_ScrollTimer) // if scrolling and timer not created yet m_ScrollTimer = SetTimer (SCROLL_TIMER, SCROLL_DELAY, NULL); // create it }
定时器处理程序。注意,除非滚动条位置实际改变,否则不会采取任何操作。这可以防止插入点在列表框滚动到底部或顶部并保持在那里时闪烁。 隐藏,复制Code
void CDragListBoxEx::OnTimer(UINT nIDEvent) { if (nIDEvent == SCROLL_TIMER) { if (m_ScrollDelta) { // if scrolling int NewTop = GetTopIndex() + m_ScrollDelta; if (NewTop != m_PrevTop) { // if scroll position changed EraseInsert(); // erase previous insert // before scrolling SetTopIndex(NewTop); // scroll to new position CPoint pt; GetCursorPos(&pt); UpdateInsert(pt); // draw new insert position m_PrevTop = NewTop; // update scroll position } } } else CListBox::OnTimer(nIDEvent); }
DrawInsert的任务是绘制/擦除插入标记。插入标记的外观当然是一个严格的偏好问题:我碰巧喜欢带有两个箭头的红色虚线,但如果您不喜欢,请随意修改或覆盖此函数。由两个区域组成的数组用于保存由DrawArrow函数创建的箭头。区域的使用允许箭头很容易被擦掉,方法是获得每个箭头的边框(通过GetRgnBox),然后重绘父窗口在该边框内的任何部分。显式擦除是健壮的,并给予更多的自由比使用异或模式。 注意,项目参数可以等于项目的数量;这不是错误,而是表示插入标记位于列表中的最后一项之后。还要注意,y被夹在列表的底部。这可以处理用户向下滚动时不小心超过列表框底部的情况;不夹紧,插入的标记会消失。 隐藏,收缩,复制Code
void CDragListBoxEx::DrawInsert(int Item, bool Enable) { ASSERT(Item >= 0 && Item <= GetCount()); CDC *pDC = GetDC(); CRect cr; GetClientRect(&cr); int items = GetCount(); int y; CRect r; if (Item < items) { GetItemRect(Item, &r); y = r.top; } else { // insert after last item GetItemRect(items - 1, &r); y = r.bottom; } if (y >= cr.bottom) // if below control y = cr.bottom - 1; // clamp to bottom edge static const int ARROWS = 2; CRgn arrow[ARROWS]; MakeArrow(CPoint(cr.left, y), TRUE, arrow[0]); MakeArrow(CPoint(cr.right, y), FALSE, arrow[1]); if (Enable) { COLORREF InsColor = RGB(255, 0, 0); CPen pen(PS_DOT, 1, InsColor); CBrush brush(InsColor); CPen *pPrevPen = pDC->SelectObject(&pen); pDC->SetBkMode(TRANSPARENT); pDC->MoveTo(cr.left, y); // draw line pDC->LineTo(cr.right, y); for (int i = 0; i < ARROWS; i++) // draw arrows pDC->FillRgn(&arrow[i], &brush); pDC->SelectObject(pPrevPen); } else { // erase marker CRect r(cr.left, y, cr.right, y + 1); RedrawWindow(&r, NULL); // erase line CWnd *pParent = GetParent(); for (int i = 0; i < ARROWS; i++) { arrow[i].GetRgnBox(r); // get arrow's bounding box ClientToScreen(r); pParent->ScreenToClient(r); pParent->RedrawWindow(&r, NULL); // erase arrow } } ReleaseDC(pDC); }
箭头存储为局部坐标空间中的点数组。这些点被映射到客户端坐标,然后通过CreatePolygonRgn转换为一个区域。因为左右箭头是彼此的镜像,所以都可以从单个模式创建。 隐藏,复制Code
void CDragListBoxEx::MakeArrow(CPoint point, bool left, CRgn& rgn) { static const POINT ArrowPt[] = { {0, 0}, {5, 5}, {5, 2}, {9, 2}, {9, -1}, {5, -1}, {5, -5} }; static const int pts = sizeof(ArrowPt) / sizeof(POINT); POINT pta[pts]; int dir = left ? -1 : 1; for (int i = 0; i < pts; i++) { pta[i].x = point.x + ArrowPt[i].x * dir; pta[i].y = point.y + ArrowPt[i].y; } rgn.CreatePolygonRgn(pta, pts, ALTERNATE); }
这就是关于CDragListBoxEx的内容。最后我们将简要介绍一下CInterDragListBox。 CInterDragListBox最有趣的地方是它非常简单。它几乎所有的行为都继承自CDragListBoxEx。将重写拖动成员以包含有关的决策无论我们是在源列表上还是在目标列表上,也就是说。,无论我们是在重新排序还是在列表间进行拖放。如果我们在一个源上,我们的基类处理它。如果我们超过了一个目标,我们调用目标的拖动成员,然后我们的基类处理它。 惟一棘手的事情是,当插入标记跳转到一个新实例时,它会在旧实例中留下一个陈旧的标记。我们不能期望基类清除除它自身之外的任何实例中的标记,因此派生类负责清除陈旧的标记。它通过在成员变量(m_TargetList)中存储当前目标列表的HWND来实现。当目标更改时,将调用EraseTarget以清除旧目标中的内容。注意,我们使用HWND而不是CWnd *,以避免存储临时CWnd *可能导致的问题。 隐藏,复制Code
UINT CInterDragListBox::Dragging(CPoint point) { UINT retc; HWND Target; CInterDragListBox *pList = ListFromPoint(point); if (pList == this) { // if we're in this list Target = m_hWnd; retc = CDragListBoxEx::Dragging(point); // do reordering } else { // not reordering if (pList != NULL) { // if we're in a target list Target = pList->m_hWnd; pList->Dragging(point); // do target behavior } else { // not in a list Target = NULL; SetCursor(LoadCursor(NULL, IDC_NO)); } retc = DL_CURSORSET; } if (Target != m_hTargetList) { // if target changed EraseTarget(); m_hTargetList = Target; // update target } return(retc); }
最后,drop被覆盖以处理列表间删除。如果我们要重新排序,基类会处理它。如果是列表间删除,我们调用MoveSelectedItems,它将实际工作委托给基类函数GetSelectedItems和pToList- PasteItems。 隐藏,复制Code
void CInterDragListBox::Dropped(CPoint point) { CInterDragListBox *pList = ListFromPoint(point); if (pList == this) { // if we're in this list CDragListBoxEx::Dropped(point); // do reordering behavior } else { // not reordering if (pList != NULL) { // if we're in a target list int InsPos = pList->GetInsertPos(point); MoveSelectedItems(pList, InsPos); } } } void CInterDragListBox::MoveSelectedItems(CInterDragListBox *pToList, int InsertPos) { int top = GetTopIndex(); // save scroll position CStringArray ItemText; CDWordArray ItemData; int ipos; // dummy arg; deletion doesn't affect insert position GetSelectedItems(ItemText, ItemData, ipos); // cut selected items pToList->PasteItems(ItemText, ItemData, InsertPos); // paste items SetTopIndex(top); // restore scroll position }
历史 2009年8月4日:首次发布 本文转载于:http://www.diyabc.com/frontweb/news327.html