QListWidget 拖拽和多选

目标是

  1.  需要一个能拖拽的list widget
  2. 拖到上面的item有checkbox
  3. 不能有重复的item
  4. 能够空格键toggle多个选择的items
  5. delete键删除选择的items

代码在这里

drag_between_two_list_widgets_check_2_multi.py

主要是 通过 reimplement keyPressEvent 来实现空格键和delete键

    def keyPressEvent(self, event):

        if event.key() == Qt.Key_Space:
            print 'yeah space'
            if self.selectedItems():
                new_state = Qt.Unchecked if self.selectedItems()[0].checkState() else Qt.Checked
                for item in self.selectedItems():
                    if item.flags() & Qt.ItemIsUserCheckable:
                        item.setCheckState(new_state)

            self.reset()
        elif event.key() == Qt.Key_Delete:
            for item in self.selectedItems():
                self.takeItem(self.row(item))

通过 reimplement dataChanged 来实现拖拽之后,如果某些items之前已经存在于下方list widget里,那就删掉因为拖拽而产生的新items

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Space:
            print 'yeah space'
            if self.selectedItems():
                new_state = Qt.Unchecked if self.selectedItems()[0].checkState() else Qt.Checked
                for item in self.selectedItems():
                    if item.flags() & Qt.ItemIsUserCheckable:
                        item.setCheckState(new_state)

            self.reset()
        elif event.key() == Qt.Key_Delete:
            for item in self.selectedItems():
                self.takeItem(self.row(item))

通过 reimplement rowsInserted 来实现当有items被拖到下方list widget的时候,打开item的 Qt.ItemIsUserCheckable item flag, 从而使checkbox出现

    def rowsInserted(self, parent, start, end):
        if self._dropping:
            self.emit(QtCore.SIGNAL("dropped"), (start, end))
        super(ThumbListWidget, self).rowsInserted(parent, start, end)

    def items_dropped(self, arg):
        start, end = arg
        print range(start, end + 1)
        for row in range(start, end + 1):
            item = self.item(row)
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Checked)

但是这里遇到一个问题是,没法用鼠标框选,如果你从list widget下侧空白区域开始框选,是可以的,如何才能在右侧空白区域也可以框选呢?如下图所示
failed to select by dragging

希望达到的效果如下图所示

dragging select in action

虽然可以自己在mousePressEvent里画一个QRubberBand,然后在mouseMoveEvent里选中框中的item,但是这样比较麻烦。经试验,发现只要在空白区域开始拖拽时,把state设置成QAbstractItemView.DragSelectingState即可(这是通过看qabstractitemview.cpp的mouseMoveEvent发现的)

加上下面的mousePressEvent就可以在空白处框选了,代码在这里

    def mousePressEvent(self,event):
        QtGui.QListWidget.mousePressEvent(self,event)

        item = self.itemAt(event.pos())
        rect = self.visualItemRect(item)
        if rect.translated(100,0).contains(event.pos()):
            self.setState(QtGui.QAbstractItemView.DragSelectingState)

drag_between_two_list_widgets_check_2_multi_sel.py

上面的版本依然有两个问题

  1. 首先是如果已经框选了一堆,这时候如果你想选其中的一个(在右侧空白处点),此时会没有反应,因为你已经让他只要在右侧空白处点,就算是框选,而此时你并没有作出“框”的动作
  2. 把rect右移100只是为了测试,万一第一列的字符很宽呢,所以这里的右移距离应该通过计算得出

下面的代码修正了前两个问题,代码在这里

想法是

  1. 首先当然是在右侧空白区域按下鼠标才认为是开始框选
  2. 其次必须鼠标点下去之后并移动了一段距离,才认为是框选
  3. 计算第一列字符的宽度,用来估计右侧空白区域开始的位置
    def mousePressEvent(self, event):
        QtGui.QListWidget.mousePressEvent(self, event)

        self.mousePressPos = (event.pos())

    def mouseMoveEvent(self, event):
        if (event.pos() - self.mousePressPos).manhattanLength() > QtGui.QApplication.startDragDistance():
            item = self.itemAt(event.pos())
            if item:
                rect = self.visualItemRect(item)
                # offset = 100
                char_width = QtGui.QFontMetricsF(QtGui.QFont(self.font())).width('a')
                offset = len(item.data(0).toString()) * char_width
                if rect.translated(offset, 0).contains(event.pos()):
                    self.setState(QtGui.QAbstractItemView.DragSelectingState)

        QtGui.QListWidget.mouseMoveEvent(self, event)

当然上面的mouseMoveEvent里的rect.translated(offset, 0).contains(event.pos())处,似乎不大对劲,这意思是说如果空白区域包含鼠标当前位置的话,就如何如何,但是就算鼠标移到了左侧,依然是框选状态,也许这是因为没有else的缘故,不管怎样,实际效果是对的

drag_between_two_list_widgets_check_2_multi_sel_1.py

最后一个问题是希望能在框选了多个item的时候,能够通过点一个checkbox,就把所有的其他已选择的items们,全都给勾选或者去勾,上面的版本只能通过空格键实现
can't toggle all selected items

这里使用了一个弱智方法,检测点击的位置,如果点到了checkbox上,就toggle所有但前选择的item,缺陷是有延迟,代码在这里

    def mousePressEvent(self, event):
        item = self.selectedCheckStateItem(event.pos())
        if item:
            new_state = Qt.Unchecked if item.checkState()==Qt.Checked else Qt.Checked
            QtGui.QApplication.processEvents()
            self.setSelectedCheckStates(new_state,item)
            QtGui.QApplication.processEvents()
            self.viewport().update()
        else:
            QtGui.QListWidget.mousePressEvent(self, event)

        self.mousePressPos = (event.pos())

    def setSelectedCheckStates(self,state,click_item):
        for item in self.selectedItems():
            if item is not click_item:
                item.setCheckState(state)

    def selectedCheckStateItem(self,pos):
        item = self.itemAt(pos)
        if item:
            opt = QtGui.QStyleOptionButton()
            opt.rect = self.visualItemRect(item)
            rect = self.style().subElementRect(QtGui.QStyle.SE_ViewItemCheckIndicator, opt)
            if item in self.selectedItems() and rect.contains(pos):
                return item
        return 0

drag_between_two_list_widgets_check_2_multi_sel_toggle.py

上面的版本依然存在问题

  • 拖拽的操作很奇怪,普通常见软件都可以在任意位置开始框选,然后点中蓝色区域即意味着要开始拖拽了,而不是上面的强迫用户去点item的文字部分才能拖
  • 选中多个items以后,用鼠标toggle checkbox状态时的效果有延迟,经观测是当按下鼠标没放手前 setSelectedCheckStates 就已经执行了,他应该在放手后执行

通过把toogle checkbox的部分挪到mouseReleaseEvent里,以及在mouseReleaseEvent里判断是不是一开时就点在之前已经框选到的item范围内,可以达到上述要求,代码在这里

    def mousePressEvent(self, event):

        # check if clicked within previously selected area
        self._drag = False
        selectedItems = self.selectedItems()
        if selectedItems:
            for sel_item in selectedItems:
                if self.visualItemRect(sel_item).contains(event.pos()):
                    self._drag = True
                    break

        self.mousePressPos = (event.pos())
        QtGui.QListWidget.mousePressEvent(self, event)


    def mouseMoveEvent(self, event):
        if (event.pos() - self.mousePressPos).manhattanLength() > QtGui.QApplication.startDragDistance():
            item = self.itemAt(event.pos())

            if item and not self._drag:
                rect = self.visualItemRect(item)
                self.setState(QtGui.QAbstractItemView.DragSelectingState)

        QtGui.QListWidget.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        item = self.selectedCheckStateItem(event.pos())
        if item:
            selectedItems = self.selectedItems()
            new_state = Qt.Unchecked if item.checkState() == Qt.Checked else Qt.Checked
            self.setSelectedCheckStates(new_state, item)
            # QtGui.QApplication.processEvents()
            self.viewport().update()

        QtGui.QListWidget.mouseReleaseEvent(self, event)
        if item:
            for sel_item in selectedItems:
                sel_item.setSelected(True)

drag_between_two_list_widgets_check_2_multi_sel_toggle_1.py

未完待续…

参考:
QListWidgetItem check/uncheck

留下评论