目标是
- 需要一个能拖拽的list widget
- 拖到上面的item有checkbox
- 不能有重复的item
- 能够空格键toggle多个选择的items
- delete键删除选择的items
代码在这里
主要是 通过 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下侧空白区域开始框选,是可以的,如何才能在右侧空白区域也可以框选呢?如下图所示
希望达到的效果如下图所示
虽然可以自己在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)
上面的版本依然有两个问题
- 首先是如果已经框选了一堆,这时候如果你想选其中的一个(在右侧空白处点),此时会没有反应,因为你已经让他只要在右侧空白处点,就算是框选,而此时你并没有作出“框”的动作
- 把rect右移100只是为了测试,万一第一列的字符很宽呢,所以这里的右移距离应该通过计算得出
下面的代码修正了前两个问题,代码在这里
想法是
- 首先当然是在右侧空白区域按下鼠标才认为是开始框选
- 其次必须鼠标点下去之后并移动了一段距离,才认为是框选
- 计算第一列字符的宽度,用来估计右侧空白区域开始的位置
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的缘故,不管怎样,实际效果是对的
最后一个问题是希望能在框选了多个item的时候,能够通过点一个checkbox,就把所有的其他已选择的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
上面的版本依然存在问题
- 拖拽的操作很奇怪,普通常见软件都可以在任意位置开始框选,然后点中蓝色区域即意味着要开始拖拽了,而不是上面的强迫用户去点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)
未完待续…