一半君的总结纸

听话只听一半君

QTreeWidget研究2 如何拖拽带itemWidget的item

用TreeWidget的时候有个问题是,如果item有itemWidget,拖拽后就没了,edit:其实本文的方法完全是错的,拖拽widget应该用QMimeData

用于在StackOverflow或是QQ群或是#pyqt的发问模板2号: 代码在此

#!/usr/bin/env python2
import os
import sys
import re
import textwrap

from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt, QString


class CommandWidget(QtGui.QDialog):

    def __init__(self, parent=None, val=None):
        super(CommandWidget, self).__init__()
        self.layout = QtGui.QHBoxLayout(self)
        browseBtn = ElideButton(parent)
        browseBtn.setMinimumSize(QtCore.QSize(0, 25))
        browseBtn.setText(QString(val))
        browseBtn.setStyleSheet("text-align: left")
        self.layout.addWidget(browseBtn)
        self.browseBtn = browseBtn
        self.browseBtn.clicked.connect(self.browseCommandScript)
        self.browseBtn.setIconSize(QtCore.QSize(64, 64))

    def browseCommandScript(self):
        script = QtGui.QFileDialog.getOpenFileName(
            self, 'Select Script file', '/tmp/crap', "Executable Files (*)")
        if script:
            self._script = script
            old_text = str(self.browseBtn.text()).strip()
            old_text = re.search('^script [\d-]*', old_text).group()
            self.browseBtn.setText(('%s %s' % (old_text, script)))


class ElideButton(QtGui.QPushButton):

    def __init__(self, parent=None):

        super(ElideButton, self).__init__(parent)
        font = self.font()
        font.setPointSize(10)
        self.setFont(font)

    def paintEvent(self, event):
        painter = QtGui.QStylePainter(self)

        metrics = QtGui.QFontMetrics(self.font())
        elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width())

        option = QtGui.QStyleOptionButton()
        self.initStyleOption(option)
        option.text = ''
        painter.drawControl(QtGui.QStyle.CE_PushButton, option)
        painter.drawText(self.rect(), Qt.AlignLeft | Qt.AlignVCenter, elided)


class MyTreeView(QtGui.QTreeView):

    def __init__(self, parent=None):
        super(MyTreeView, self).__init__(parent)
        self.dropIndicatorRect = QtCore.QRect()

    def paintEvent(self, event):
        painter = QtGui.QPainter(self.viewport())
        self.drawTree(painter, event.region())
        # in original implementation, it calls an inline function paintDropIndicator here
        self.paintDropIndicator(painter)

    def paintDropIndicator(self, painter):

        if self.state() == QtGui.QAbstractItemView.DraggingState:
            opt = QtGui.QStyleOption()
            opt.init(self)
            opt.rect = self.dropIndicatorRect
            rect = opt.rect

            if rect.height() == 0:
                pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine)
                painter.setPen(pen)
                painter.drawLine(rect.topLeft(), rect.topRight())
            else:
                pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine)
                painter.setPen(pen)
                painter.drawRect(rect)


class MyLineEdit(QtGui.QWidget):

    def __init__(self, value=None, parent=None):
        super(MyLineEdit, self).__init__(parent)
        self.layout = QtGui.QHBoxLayout(self)
        self.layout.setSpacing(0)
        self.layout.setMargin(3)

        self.lineEdit = QtGui.QLineEdit(value)
        spacer1 = QtGui.QSpacerItem(20, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
        spacer2 = QtGui.QSpacerItem(20, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)

        self.lineEdit.setContentsMargins(2, 2, 2, 2)
        self.lineEdit.setAlignment(Qt.AlignHCenter)

        self.layout.addItem(spacer1)
        self.layout.addWidget(self.lineEdit)
        self.layout.addItem(spacer2)

        self.lineEdit.setMaximumSize(QtCore.QSize(70, 25))

    def text(self):
        return self.lineEdit.text()

    def setText(self, text):
        return self.lineEdit.setText(text)


class TheUI(QtGui.QDialog):

    def __init__(self, args=None, parent=None):
        super(TheUI, self).__init__(parent)

        splitter = QtGui.QSplitter()
        splitter.setOrientation(Qt.Vertical)

        # dummy widget for top part
        widget_top = QtGui.QWidget(splitter)

        # vertial layout  for top
        layout_top = QtGui.QVBoxLayout(widget_top)

        treeWidget = QtGui.QTreeWidget()
        treeWidget.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
        layout_top.addWidget(treeWidget)

        button1 = QtGui.QPushButton('Add')
        button2 = QtGui.QPushButton('Add Child')

        # horizontal layout for top buttons
        layout = QtGui.QHBoxLayout()
        layout.addWidget(button1)
        layout.addWidget(button2)

        layout_top.addLayout(layout)

        splitter.addWidget(widget_top)

        # dummy widget for bottom part
        widget_bottom = QtGui.QWidget(splitter)
        layout_bottom = QtGui.QVBoxLayout(widget_bottom)
        plainTextEdit = QtGui.QPlainTextEdit()
        layout_bottom.addWidget(plainTextEdit)

        # horizontal layout for bottom  checkbox and slider
        layout = QtGui.QHBoxLayout(widget_bottom)
        rootDecorationCB = QtGui.QCheckBox('RootIsDecorated')
        layout.addWidget(rootDecorationCB)
        layout_bottom.addLayout(layout)

        indentationSlider = QtGui.QSlider()
        indentationSlider.setOrientation(Qt.Horizontal)
        indentationSlider.setRange(0, 100)
        indentationSlider.setValue(20)

        layout.addWidget(indentationSlider)

        mainLayout = QtGui.QVBoxLayout(self)
        mainLayout.addWidget(splitter)

        # hide bottom part at the beginning
        splitter.setSizes([1, 0])
        rootDecorationCB.setCheckState(Qt.Checked)

        # connect signals
        rootDecorationCB.stateChanged.connect(self._update_root_decorated)
        indentationSlider.valueChanged.connect(self._alter_indentation)
        plainTextEdit.textChanged.connect(self._update_css)

        # hide tree header
        treeWidget.setHeaderHidden(True)

        self.treeWidget = treeWidget

        self.button1 = button1
        self.button2 = button2
        self.plainTextEdit = plainTextEdit

        self.button1.clicked.connect(lambda *x: self.addCmd())
        self.button2.clicked.connect(lambda *x: self.addChildCmd())

        HEADERS = ("script", "chunksize", "mem")
        self.treeWidget.setHeaderLabels(HEADERS)
        self.treeWidget.setColumnCount(len(HEADERS))

        self.treeWidget.setColumnWidth(0, 160)
        self.treeWidget.header().show()

        self.treeWidget.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
        self.treeWidget.setStyleSheet('''
                                         QTreeView {
                                             show-decoration-selected: 1;
                                         }

                                         QTreeView::item:hover {
                                             background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
                                         }

                                         QTreeView::item:selected:active{
                                             background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6ea1f1, stop: 1 #567dbc);
                                         }

                                         QTreeView::item:selected:!active {
                                             background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6b9be8, stop: 1 #577fbf);
                                         }
                                         ''')

        self.resize(500, 500)
        for i in xrange(6):
            item = self.addCmd(i)
            if i in (3, 4):
                self.addChildCmd()
                if i == 4:
                    self.addCmd('%s-2' % i, parent=item)

        self.treeWidget.expandAll()
        self.setStyleSheet("QTreeWidget::item{ height: 30px;  }")

        # populate textedit with existing stylesheet
        existing_style_sheet = textwrap.dedent(str(self.treeWidget.styleSheet()))

        existing_style_sheet += '/* Above this line are the pre-existing css styles */'
        self.plainTextEdit.setPlainText(existing_style_sheet)

    def addChildCmd(self):
        parent = self.treeWidget.currentItem()
        self.addCmd(parent=parent)
        self.treeWidget.setCurrentItem(parent)

    def addCmd(self, i=None, parent=None):
        'add a level to tree widget'

        root = self.treeWidget.invisibleRootItem()
        if not parent:
            parent = root

        if i is None:
            if parent == root:
                i = self.treeWidget.topLevelItemCount()
            else:
                i = str(parent.text(0)).strip()[7:]
                i = '%s-%s' % (i, parent.childCount() + 1)

        # item = QtGui.QTreeWidgetItem(parent, ['script %s' % i, '1', '150'])

        script = '   script %s' % i
        # item = QtGui.QTreeWidgetItem(parent, [script, '1', '150'])
        item = QtGui.QTreeWidgetItem(parent, [script, '', ''])

        self.treeWidget.setItemWidget(item, 0, CommandWidget(val=script))

        self.treeWidget.setItemWidget(item, 1, MyLineEdit('1'))
        self.treeWidget.setItemWidget(item, 2, MyLineEdit('150'))

        self.treeWidget.setCurrentItem(item)
        self.treeWidget.expandAll()
        return item

    def _update_css(self):
        self.treeWidget.setStyleSheet(self.plainTextEdit.toPlainText())

    def _update_root_decorated(self, state):
        if state == Qt.Checked:
            self.treeWidget.setRootIsDecorated(True)
        else:
            self.treeWidget.setRootIsDecorated(False)
        self.treeWidget.updateGeometries()

    def _alter_indentation(self, value):
        self.treeWidget.setIndentation(value)
        self.treeWidget.updateGeometries()

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    gui = TheUI()
    gui.show()
    app.exec_()

simple_treeWidget_2.py

正如Qt官方文档所说:

  • This function should only be used to display static content in the place of a tree widget item. If you want to display custom dynamic content or implement a custom editor widget, use QTreeView and subclass QItemDelegate instead.

所以本来就不应该把widget放在item上,同时又希望能被用户操作. 但是lz现在就是有这个一个奇葩的要求,lz也知道这种情况下应该使用 QTreeView,并且使用model/view模式,但是要求是 editor 始终可见,而不是默认的 双击后才出现 editor(就是图中的客制过的按钮和lineedit),所以此时你只好使用openPersistentEditor,但同样,这又是不推荐的,一则每次插入或者删除item后都要刷新整个Tree/Table widget(关掉全部的persistent editor然后再次打开),这样行多了以后速度会很慢,二则当用户”edit”某个editor里的数据以后,如果影响到周围其他的列,又得自己去更新数据或者刷新editor,感觉这似乎是滥用model/view模式(如果我自己去刷新数据了,那我还用model/view干嘛,既然我用这个模式,就当然得按照这个模式的写法来写 – 更新model里的数据,view负责刷新,delegate负责自定义显示效果)

现在的问题是拖放item之后,item widget就消失了,要如何才能保有item widget,并且保持上面的数据呢,比如第一列的按钮可以按下选择一个文件,希望达到的效果是拖放之后,按钮和lineedit都还在,同时按钮上显示的路径也还是拖放之前“选择的”路径

问题如下所示
simple_treeWidget_2.py

lz目前采取的办法是在itemwidget 创建之初就给他加个.treeIWidgetItem 属性,把item的reference存在里面,同时widget上的“值”存在treeWidgetItem的data里,然后reimplement dropEvent, 在drop发生之后,根据treeWidgetItem的data的值,重新创建一个新的item widget(当然当用户操作widget的时候,是更新了treeWidgetItem的data的),但是这样操作非常麻烦,在删除行的时候也许要做这个操作,如果拖放的item有子item,同样得递归的对所有子item做这个重新创建item widget的操作,感觉这是错误的做法。如果item widget决定换一下的话,tree widget class的dropEvent也得对应更改,感觉这是把一件事分到了多个不同的文件里,显然是错误的。

跑题,有时候拖拽以后发现某行只显示了一部分,如下图所示,据高手说是因为没有给某行的item sizeHint, 他说只要给treeWidget用.setUniformRowHeights(True)就可以了。
without_uniform_rowHeight

上面用的方法在这里
drop_with_widgets_method_0

    def dropEvent(self, event):
        pos = event.pos()
        item = self.itemAt(pos)
        dragItem = self.currentItem()
        if item:
            index = self.indexFromItem(item)
            self.model().setData(index, 0, Qt.UserRole)

        if item is self.currentItem():
            QtGui.QTreeWidget.dropEvent(self, event)
            event.accept()
            return

        self.recreate_widget(dragItem, event, for_drop=True)

        # recursively recreate widgets because dragged item may have children
        def recreate_widget_for_item(item):
            # if item has children, call the function on each child
            for i in range(item.childCount()):
                self.recreate_widget(item.child(i), event)
                # if current child has its own children, call function on each grandchild
                if item.child(i).childCount():
                    recreate_widget_for_item(item.child(i))

        # call the recursive funtion on current dragged item, so that all of its descendants are handled ( widgets recreated  )
        recreate_widget_for_item(dragItem)

        # self.expandAll()
        self.updateGeometry()


    def recreate_widget(self, dragItem, event=None, for_drop=False):
        # recreate command widget
        data = dragItem.data(0, Qt.UserRole + 1).toPyObject()
        print 'in recreate_widget:', data
        commandWidget = CommandWidget(self, val=data)
        widgets = [commandWidget]
        if for_drop:
            # drop happens here by calling QTreeWidget class dropEvent
            QtGui.QTreeWidget.dropEvent(self, event)
        self.setItemWidget(dragItem, 0, commandWidget)
        dragItem.setExpanded(True)

这样的缺陷是,对其他列也要这么做(如果有itemWidgets的话,而且把其他列的custom widget的代码和treeWidget的混在了一起,如果想改custom widget,还得去treeWidget里改…)

下面是同样的拖放方法,但是整合了QTreeWidget 自定义拖拽之乱搞 – by The Tree Widget Guy的效果,代码在这里

custom drop indicator and recreating item widgets

本来希望达到的是拖之前和拖之后用同一个QWidget,经过观察qt的QTreeWidget源码后发现,他是拖之前先takeItem,存在数组里,然后再insert到放手的地方,但不幸的是,放了之后就没了,我重新把这段改成了python版的,试验过drop之前把itemWidget存在list里,drop之后再setItemWidget回去,但是这样还是不显示,而且如果点collapse的+号直接segfault … orz.

观察item widgets的父子关系是,setItemWidget之后, QTreeWidget->QWidget->设置的itemWidget

后经搜索QObject Clone后得知,Qt本身设计就不允许你复制QObject

“This is by design. The usual way to solve it is to implement a method (typically called clone()) that allows you to specify the exact semantics that should apply when copying instances of your class. This approach also prevents unintentional copies from being made implicitly, e.g by container classes.”

所以想要放了以后还有,只能在某处自己重新建一个,为了把TreeWidget和不同列的custom widget的代码分开,又想了个一下脑残法(还是通过复制):
我给每个custom widget都来一个clone method, 返回一个和自己当前状态完全一样的新的自身,然后我在TreeWidget的dropEvent里调用这个clone,这样就可以把他们分开了,代码在这里

这里有几个不用递归的高逼格的用iterator找Tree结构里的所有Children的方法,没看懂…orz

下面这个是在这里抄的


    def iterativeChildren(self, nodes):
        results = []
        while True:
            newNodes = []
            if not nodes:
                break
            for node in nodes:
                results.append(node)
                for i in range(node.childCount()):
                    print 'newNodes:', newNodes
                    newNodes += [node.child(i)]
            nodes = newNodes
        results = nodes + results
        return results
    def dropEvent(self, event):
        pos = event.pos()
        item = self.itemAt(pos)
        if item:
            index = self.indexFromItem(item)
            self.model().setData(index, 0, Qt.UserRole)

        if item is self.currentItem():
            QtGui.QTreeWidget.dropEvent(self, event)
            event.accept()
            return

        if event.source == self and event.dropAction() == Qt.MoveAction or self.dragDropMode() == QtGui.QAbstractItemView.InternalMove:

            topIndex = QtCore.QModelIndex()
            col = -1
            row = -1

            l = [event, row, col, topIndex]

            if self.dropOn(l):

                event, row, col, topIndex = l

                idxs = self.selectedIndexes()
                indexes = []
                existing_rows = set()
                for i in idxs:
                    if i.row() not in existing_rows:
                        indexes.append(i)
                        existing_rows.add(i.row())

                if topIndex in indexes:
                    return

                # try storing the itemWidgets first
                # we should iterate through all child items,and store itemWidgets for them
                widgets = []

                dropRow = self.model().index(row, col, topIndex)
                taken = []

                indexes_reverse = indexes[:]
                indexes_reverse.reverse()
                # i = 0
                for index in indexes_reverse:
                    parent = self.itemFromIndex(index)
                    item_widget = self.itemWidget(parent, 0)

                    print 'item_widget:', item_widget, item_widget.parent()

                    # item_widget.setParent(self)
                    print 'dragging item has child:', parent.childCount()

                    # print 'before dragging, child 0 ',self.itemWidget( parent.child(0),0).browseBtn.text()

                    # in case it has children , we get all of them
                    all_child = []

                    all_items = self.iterativeChildren([parent])

                    print 'all items:', len(all_items), all_items

                    # store cloned widgets in a list
                    widgets = [self.itemWidget(i, 0).clone() for i in all_items]

                    # widgets.append(item_widget.clone())

                    if not parent or not parent.parent():
                    # if not parent or not isinstance(parent.parent(),QtGui.QTreeWidgetItem):
                        taken.append(self.takeTopLevelItem(index.row()))
                    else:
                        taken.append(parent.parent().takeChild(index.row()))

                    # i += 1
                    # break

                taken.reverse()

                print 'itemWidgets:', widgets

                for index in indexes:
                    print 'inserting: topIndex:', topIndex.isValid(), row
                    if row == -1:  # means index=root
                        if topIndex.isValid():  # Returns the model index of the model's root item. The root item is the parent item to the view's toplevel items. The root can be invalid.
                            parent = self.itemFromIndex(topIndex)
                            parent.insertChild(parent.childCount(), taken[0])

                            # after insert the itemwidget is gone
                            # print 'after dragging, child 0 ',self.itemWidget( taken[0],0).browseBtn.text()

                            # self.setItemWidget(taken[0],0,QtGui.QLineEdit())
                            # self.setItemWidget(taken[0],0,new_widget)
                            print 'row==-1,if',  # self.itemWidget(taken[0],0),self.itemWidget(taken[0],0).parent()
                            # taken = taken[1:]

                        else:
                            self.insertTopLevelItem(self.topLevelItemCount(), taken[0])
                            # taken = taken[1:]
                            print 'row==-1,else'
                    else:
                        r = dropRow.row() if dropRow.row() >= 0 else row
                        if topIndex.isValid():
                            parent = self.itemFromIndex(topIndex)
                            parent.insertChild(min(r, parent.childCount()), taken[0])
                            # taken = taken[1:]
                            print 'row!=-1,if'
                        else:
                            self.insertTopLevelItem(min(r, self.topLevelItemCount()), taken[0])
                            # taken = taken[1:]
                            print 'row!=-1,else'

                    all_items = self.iterativeChildren([taken[0]])

                    for i, w in zip(all_items, widgets):
                        self.setItemWidget(i, 0, w)

                    taken = taken[1:]
                event.accept()

        QtGui.QTreeWidget.dropEvent(self, event)
        self.expandAll()
        self.updateGeometry()

未完待续 …

Advertisements

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s

%d 博主赞过: