├── .gitignore ├── .tgitconfig ├── LICENSE ├── README.md ├── SUMMARY.md ├── book.json ├── cover.jpg ├── cover_small.jpg ├── designer ├── README.md ├── img │ ├── .directory │ ├── elide_button_designer.png │ └── elide_button_designer_promote.png └── promoted_widgets.md ├── graphics_view └── README.md ├── list ├── README.md ├── article.md ├── img │ └── checkbox_multi_toggle.gif ├── itemitem.md └── list_widget.md ├── reame.md ├── signal-and-slot ├── README.md ├── dynamical-create-a-series-of-widget-and-binding-signal-and-slot.md └── img │ ├── dynamical-create-checkboxes.png │ └── finder-preferences.png ├── stylesheet └── README.md ├── table └── README.md ├── tree ├── README.md ├── article.md ├── drop_indicator.md └── img │ ├── custom_drop_indicator.gif │ ├── itemWidget_dragging.gif │ └── transparent_dragging.gif └── widget ├── 31qmenu.md ├── README.md ├── css_overflowlabel.md └── img ├── elide_button.gif └── maya.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # book 56 | _book 57 | 58 | 59 | -------------------------------------------------------------------------------- /.tgitconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykuu/PyQt-PySide-Cookbook/fe9a65d47c80e3846149d48c9c9b90e46421e00a/.tgitconfig -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jimmy Kuu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyQt/PySide Cookbook 2 | ==================== 3 | 4 | 该项目主要用于收集PyQt/PySide开发中碰到的问题的解决方案,以及一些案例。 5 | 6 | 主要代码说话,解决问题为主,少讲或不讲原理。 7 | 8 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [简介](README.md) 4 | * [资源和翻译](reame.md) 5 | * [信号和槽](signal-and-slot/README.md) 6 | * [动态创建一系列组件并绑定信号和槽](signal-and-slot/dynamical-create-a-series-of-widget-and-binding-signal-and-slot.md) 7 | * [Widget](widget/README.md) 8 | * [带overflow效果的按钮](widget/css_overflowlabel.md) 9 | * [QAction](widget/31qmenu.md) 10 | * [List Widget](list/README.md) 11 | * [遍历List Widget](list/list_widget.md) 12 | * [同时勾选多个items](list/article.md) 13 | * [Tree](tree/README.md) 14 | * [半透明效果拖拽](tree/article.md) 15 | * [自定义drop indicator](tree/drop_indicator.md) 16 | * [拖拽带itemWidget的treeWidgetItem](tree/article.md) 17 | * [Table](table/README.md) 18 | * [对话框](README.md) 19 | * [打包发布](README.md) 20 | * [StyleSheet](stylesheet/README.md) 21 | * [Designer](designer/README.md) 22 | * [Promoted Widgets](designer/promoted_widgets.md) 23 | * [Graphics View](graphics_view/README.md) 24 | 25 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykuu/PyQt-PySide-Cookbook/fe9a65d47c80e3846149d48c9c9b90e46421e00a/cover.jpg -------------------------------------------------------------------------------- /cover_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykuu/PyQt-PySide-Cookbook/fe9a65d47c80e3846149d48c9c9b90e46421e00a/cover_small.jpg -------------------------------------------------------------------------------- /designer/README.md: -------------------------------------------------------------------------------- 1 | # Designer 2 | -------------------------------------------------------------------------------- /designer/img/.directory: -------------------------------------------------------------------------------- 1 | [Dolphin] 2 | HeaderColumnWidths=332,75,160 3 | SortRole=size 4 | Timestamp=2014,9,14,1,21,31 5 | Version=3 6 | ViewMode=1 7 | -------------------------------------------------------------------------------- /designer/img/elide_button_designer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykuu/PyQt-PySide-Cookbook/fe9a65d47c80e3846149d48c9c9b90e46421e00a/designer/img/elide_button_designer.png -------------------------------------------------------------------------------- /designer/img/elide_button_designer_promote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykuu/PyQt-PySide-Cookbook/fe9a65d47c80e3846149d48c9c9b90e46421e00a/designer/img/elide_button_designer_promote.png -------------------------------------------------------------------------------- /designer/promoted_widgets.md: -------------------------------------------------------------------------------- 1 | # Promoted Widgets 2 | 如果你有一些自定义的widgets,但是又希望用designer来画ui,这时有两种做法: 3 | 4 | - 把这些widgets做成插件,让他们出现在designer左侧的widget列表里。但这样难度比较高,如果不是需要经常反复使用的widget,没必要这么做 5 | - 使用promoted widgets,操作相对简单 6 | 7 | 以[3.1带overflow效果的按钮](../widget/css_overflowlabel.md)为例: 8 | 9 | ![elide_button_designer](img/elide_button_designer.png) 10 | 11 | 如果你定义了ElideButton这个Subclass,你可以把他保存成单独一个文件elide_button.py,然后在designer里依然用QPushButton,但是创建之后在上面右击,选择Promote to,然后照下图填入 12 | 13 | ![elide_button_designer_promote](img/elide_button_designer_promote.png) 14 | 15 | 其中ElideButton必须和你的subclass的名字一致,elide_button.h必须和你的.py文件名一致(请无视.h) 16 | 17 | 这样你就可以达到,虽然designer里画的是QPushButton,但是当运行的时候,他其实使用的是你的ElideButton。 18 | 19 | 代码如下: 20 | ```python 21 | #!/usr/bin/env python2 22 | import os 23 | import sys 24 | from PyQt4 import QtGui, QtCore 25 | from PyQt4.QtCore import Qt, QString 26 | from PyQt4 import uic 27 | 28 | 29 | class TheUI(QtGui.QDialog): 30 | 31 | def __init__(self, args=None, parent=None): 32 | super(TheUI, self).__init__(parent) 33 | self.ui = uic.loadUi('elide_button.ui', self) 34 | self.pushButton.setText('Oh Yeah This is a super long string') 35 | self.ui.show() 36 | self.setMinimumWidth(20) 37 | 38 | if __name__ == '__main__': 39 | app = QtGui.QApplication(sys.argv) 40 | gui = TheUI() 41 | gui.show() 42 | app.exec_() 43 | ``` 44 | 综上,你可以随意使用自己定义的widget,与此同时还可以使用designer来画ui,只要你在designer里把相应的widget promote成你自定义的widget即可。 45 | -------------------------------------------------------------------------------- /graphics_view/README.md: -------------------------------------------------------------------------------- 1 | # Graphics View 2 | -------------------------------------------------------------------------------- /list/README.md: -------------------------------------------------------------------------------- 1 | # List Widget 2 | -------------------------------------------------------------------------------- /list/article.md: -------------------------------------------------------------------------------- 1 | # 同时勾选多个items 2 | 框选多个item之后,用空格键可以勾选/去选多个item,效果如下图所示: 3 | 4 | ![img](img/checkbox_multi_toggle.gif) 5 | 6 | 方法是reimplement keyPressEvent,只要按键是空格键,就勾选/去选选择了的items,代码如下 7 | 8 | ``` 9 | ```python``` 10 | 11 | from PyQt4 import QtGui, QtCore 12 | from PyQt4.QtCore import Qt, QString 13 | import sys 14 | import os 15 | 16 | 17 | class ThumbListWidget(QtGui.QListWidget): 18 | 19 | def __init__(self, type, parent=None): 20 | super(ThumbListWidget, self).__init__(parent) 21 | self.setIconSize(QtCore.QSize(124, 124)) 22 | self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) 23 | self.setAcceptDrops(True) 24 | self.setSelectionRectVisible(True) 25 | 26 | def keyPressEvent(self, event): 27 | 28 | if event.key() == Qt.Key_Space: 29 | if self.selectedItems(): 30 | new_state = Qt.Unchecked if self.selectedItems()[0].checkState() else Qt.Checked 31 | for item in self.selectedItems(): 32 | if item.flags() & Qt.ItemIsUserCheckable: 33 | item.setCheckState(new_state) 34 | 35 | self.viewport().update() 36 | 37 | elif event.key() == Qt.Key_Delete: 38 | for item in self.selectedItems(): 39 | self.takeItem(self.row(item)) 40 | 41 | def iterAllItems(self): 42 | for i in range(self.count()): 43 | yield self.item(i) 44 | 45 | 46 | class Dialog(QtGui.QMainWindow): 47 | 48 | def __init__(self): 49 | super(QtGui.QMainWindow, self).__init__() 50 | self.listItems = {} 51 | 52 | myQWidget = QtGui.QWidget() 53 | myBoxLayout = QtGui.QVBoxLayout() 54 | myQWidget.setLayout(myBoxLayout) 55 | self.setCentralWidget(myQWidget) 56 | 57 | self.listWidgetA = ThumbListWidget(self) 58 | for i in range(5): 59 | QtGui.QListWidgetItem('Item ' + str(i + 1), self.listWidgetA) 60 | 61 | for item in self.listWidgetA.iterAllItems(): 62 | item.setFlags(item.flags() | Qt.ItemIsUserCheckable) 63 | item.setCheckState(Qt.Checked) 64 | 65 | myBoxLayout.addWidget(self.listWidgetA) 66 | self.listWidgetA.setAcceptDrops(False) 67 | self.listWidgetA.viewport().update() 68 | 69 | if __name__ == '__main__': 70 | app = QtGui.QApplication(sys.argv) 71 | dialog = Dialog() 72 | dialog.show() 73 | dialog.resize(400, 140) 74 | sys.exit(app.exec_()) 75 | ``` 76 | 但是还存在一个问题,希望能够在框选了多个item之后,通过单击任意一个item的checkbox,也能达到勾选/去选所有item的效果,此时可以给list widget添加如下几个method: 77 | ```python 78 | def mouseReleaseEvent(self, event): 79 | item = self.selectedCheckStateItem(event.pos()) 80 | if item: 81 | selectedItems = self.selectedItems() 82 | new_state = Qt.Unchecked if item.checkState() == Qt.Checked else Qt.Checked 83 | self.setSelectedCheckStates(new_state, item) 84 | # QtGui.QApplication.processEvents() 85 | self.viewport().update() 86 | 87 | QtGui.QListWidget.mouseReleaseEvent(self, event) 88 | if item: 89 | for sel_item in selectedItems: 90 | sel_item.setSelected(True) 91 | 92 | def setSelectedCheckStates(self, state, click_item): 93 | for item in self.selectedItems(): 94 | if item is not click_item: 95 | item.setCheckState(state) 96 | 97 | def selectedCheckStateItem(self, pos): 98 | item = self.itemAt(pos) 99 | if item: 100 | opt = QtGui.QStyleOptionButton() 101 | opt.rect = self.visualItemRect(item) 102 | rect = self.style().subElementRect(QtGui.QStyle.SE_ViewItemCheckIndicator, opt) 103 | if item in self.selectedItems() and rect.contains(pos): 104 | return item 105 | return None 106 | ``` 107 | 其原理是当鼠标按键释放时,通过在`selectedCheckStateItem`中判断释放位置是否刚好在某个item的左侧的checkbox上,如果是,则返回此item,否则返回None。 108 | 109 | 如果确实鼠标按键在某个item左侧的checkbox上释放了,那就拿到他现在的勾选状态,然后相应的勾选/去选当前所有选中的items。 110 | 111 | **Note:** 112 | 113 | - 解决问题的关键点是 114 | ```python 115 | opt = QtGui.QStyleOptionButton() 116 | opt.rect = self.visualItemRect(item) 117 | rect = self.style().subElementRect(QtGui.QStyle.SE_ViewItemCheckIndicator, opt) 118 | ``` 119 | 此3行代码得到的是某个item的左侧的checkbox所占据的rect。 120 | 121 | -------------------------------------------------------------------------------- /list/img/checkbox_multi_toggle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykuu/PyQt-PySide-Cookbook/fe9a65d47c80e3846149d48c9c9b90e46421e00a/list/img/checkbox_multi_toggle.gif -------------------------------------------------------------------------------- /list/itemitem.md: -------------------------------------------------------------------------------- 1 | # 显示可勾选的item并同时勾选多个item 2 | -------------------------------------------------------------------------------- /list/list_widget.md: -------------------------------------------------------------------------------- 1 | # 遍历List Widget 2 | 最直接的方法如下: 3 | 4 | ```python 5 | items = [] 6 | for index in xrange(self.listWidget.count()): 7 | items.append(self.listWidget.item(index)) 8 | ``` 9 | 但是这样很不pythonic,基于一般越短的代码就是越正确的代码的原理,可以像下面这样: 10 | 11 | ```python 12 | all_items = self.listWidgetA.findItems(QString('*'), Qt.MatchWrap | Qt.MatchWildcard) 13 | ``` 14 | 但是这相当于把所有的items全部存到一个list里面了,会费资源,还是不好。 15 | 16 | 可以考虑如下方式,在listWidget的subclass里定义一个iterAllItems method,其实他是一个generator。 17 | 18 | ```python 19 | def iterAllItems(self): 20 | for i in range(self.count()): 21 | yield self.item(i) 22 | ``` 23 | 24 | -------------------------------------------------------------------------------- /reame.md: -------------------------------------------------------------------------------- 1 | # 资源和翻译 2 | -------------------------------------------------------------------------------- /signal-and-slot/README.md: -------------------------------------------------------------------------------- 1 | # 信号和槽 2 | -------------------------------------------------------------------------------- /signal-and-slot/dynamical-create-a-series-of-widget-and-binding-signal-and-slot.md: -------------------------------------------------------------------------------- 1 | # 动态创建一系列组件并绑定信号和槽 2 | 3 | 4 | ![Finder Preferenes](img/finder-preferences.png) 5 | 6 | 如上图所示的需求,需要创建4个QCheckBox,一种做法是直接Designer中设计,这种做法适合知道选项的情况。还有种情况是这些选项是通过配置文件读出来的,或者是数据库中取出来,或者其他情况得到的,这时候就需要动态创建了。 7 | 8 | 解决思路是循环一个列表,创建对象,插入布局即可。这里稍微增加一些复杂性,对所创建的QCheckBox对象进行信号和槽的绑定,并处理信号,这时候我们可以通过`sender()`方法来获得是哪个对象发出的信号。 9 | 10 | 其他的控件类似,这里就用QCheckBox做例子了。 11 | 12 | 下面是完整代码(PyQt4/Python2.7): 13 | 14 | # -*- coding: utf-8 -*- 15 | from PyQt4 import QtGui, QtCore 16 | 17 | class Widget(QtGui.QWidget): 18 | def __init__(self, parent=None): 19 | QtGui.QWidget.__init__(self, parent) 20 | 21 | layout = QtGui.QVBoxLayout() 22 | 23 | items = [(0, 'Python'), (1, 'Golang'), (2, 'JavaScript'), (3, 'Ruby')] 24 | for id_, text in items: 25 | checkBox = QtGui.QCheckBox(text, self) 26 | checkBox.id_ = id_ 27 | checkBox.stateChanged.connect(self.checkLanguage) 28 | layout.addWidget(checkBox) 29 | 30 | self.lMessage = QtGui.QLabel(self) 31 | layout.addWidget(self.lMessage) 32 | 33 | self.setLayout(layout) 34 | 35 | def checkLanguage(self, state): 36 | checkBox = self.sender() # 获取发射信号的对象 37 | 38 | if state == QtCore.Qt.Unchecked: 39 | self.lMessage.setText(u'取消选择了{0}: {1}'.format(checkBox.id_, checkBox.text())) 40 | elif state == QtCore.Qt.Checked: 41 | self.lMessage.setText(u'选择了{0}: {1}'.format(checkBox.id_, checkBox.text())) 42 | 43 | 44 | if __name__ == '__main__': 45 | import sys 46 | 47 | app = QtGui.QApplication(sys.argv) 48 | widget = Widget() 49 | widget.show() 50 | sys.exit(app.exec_()) 51 | 52 | 53 | 效果截图: 54 | 55 | ![动态创建组件效果](img/dynamical-create-checkboxes.png) 56 | 57 | 58 | -------------------------------------------------------------------------------- /signal-and-slot/img/dynamical-create-checkboxes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykuu/PyQt-PySide-Cookbook/fe9a65d47c80e3846149d48c9c9b90e46421e00a/signal-and-slot/img/dynamical-create-checkboxes.png -------------------------------------------------------------------------------- /signal-and-slot/img/finder-preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykuu/PyQt-PySide-Cookbook/fe9a65d47c80e3846149d48c9c9b90e46421e00a/signal-and-slot/img/finder-preferences.png -------------------------------------------------------------------------------- /stylesheet/README.md: -------------------------------------------------------------------------------- 1 | # StyleSheet 2 | -------------------------------------------------------------------------------- /table/README.md: -------------------------------------------------------------------------------- 1 | # Table 2 | -------------------------------------------------------------------------------- /tree/README.md: -------------------------------------------------------------------------------- 1 | # Tree 2 | -------------------------------------------------------------------------------- /tree/article.md: -------------------------------------------------------------------------------- 1 | # 拖拽带itemWidget的treeWidgetItem 2 | 3 | 如果你在QTreeWidget的item上了itemWidget,你会发现拖放之后,itemWidget就消失了,这是“正常现象“,因为按照qt文档的[描述](http://qt-project.org/doc/qt-4.8/qtreewidget.html#setItemWidget),这个`setItemWidget`只能用来显示静态widet。 4 | 5 | 这里想了一个绕过的方法:给每个custom widget都写一个`clone` method,使其 返回一个和自己当前状态完全一样的新的instance,然后在TreeWidget的dropEvent里调用这个`clone` method,drop之前把"clone"出来的emWidget存在list里,drop之后再setItemWidget回去。 6 | 7 | 8 | ![itemWidget_dragging](img/itemWidget_dragging.gif) 9 | 10 | 11 | 下面的代码包含了前一节的[自定义drop indicator](drop_indicator.md)的效果 12 | 13 | ```python 14 | #!/usr/bin/env python2 15 | import os 16 | import sys 17 | import re 18 | 19 | from PyQt4 import QtGui, QtCore 20 | from PyQt4.QtCore import Qt, QString 21 | 22 | 23 | class MyWidget(QtGui.QDialog): 24 | 25 | def __init__(self, parent=None, val=None): 26 | super(MyWidget, self).__init__() 27 | self.layout = QtGui.QHBoxLayout(self) 28 | browseBtn = ElideButton(parent) 29 | browseBtn.setMinimumSize(QtCore.QSize(0, 25)) 30 | browseBtn.setText(QString(val)) 31 | browseBtn.setStyleSheet("text-align: left") 32 | self.layout.addWidget(browseBtn) 33 | self.browseBtn = browseBtn 34 | self.browseBtn.clicked.connect(self.browseCommandScript) 35 | self.browseBtn.setIconSize(QtCore.QSize(64, 64)) 36 | 37 | def browseCommandScript(self): 38 | script = QtGui.QFileDialog.getOpenFileName( 39 | self, 'Select Script file', '/tmp/crap', "Executable Files (*)") 40 | if script: 41 | self._script = script 42 | old_text = str(self.browseBtn.text()).strip() 43 | old_text = re.search('^script [\d-]*', old_text).group() 44 | self.browseBtn.setText(('%s %s' % (old_text, script))) 45 | 46 | def clone(self): 47 | clone = MyWidget(val=str(self.browseBtn.text())) 48 | return clone 49 | 50 | 51 | class ElideButton(QtGui.QPushButton): 52 | 53 | def __init__(self, parent=None): 54 | 55 | super(ElideButton, self).__init__(parent) 56 | font = self.font() 57 | font.setPointSize(10) 58 | self.setFont(font) 59 | 60 | def paintEvent(self, event): 61 | painter = QtGui.QStylePainter(self) 62 | 63 | metrics = QtGui.QFontMetrics(self.font()) 64 | elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width()) 65 | 66 | option = QtGui.QStyleOptionButton() 67 | self.initStyleOption(option) 68 | option.text = '' 69 | painter.drawControl(QtGui.QStyle.CE_PushButton, option) 70 | painter.drawText(self.rect(), Qt.AlignLeft | Qt.AlignVCenter, elided) 71 | 72 | 73 | class MyTreeView(QtGui.QTreeView): 74 | 75 | def __init__(self, parent=None): 76 | super(MyTreeView, self).__init__(parent) 77 | self.dropIndicatorRect = QtCore.QRect() 78 | 79 | def paintEvent(self, event): 80 | painter = QtGui.QPainter(self.viewport()) 81 | self.drawTree(painter, event.region()) 82 | # in original implementation, it calls an inline function paintDropIndicator here 83 | self.paintDropIndicator(painter) 84 | 85 | def paintDropIndicator(self, painter): 86 | 87 | if self.state() == QtGui.QAbstractItemView.DraggingState: 88 | opt = QtGui.QStyleOption() 89 | opt.init(self) 90 | opt.rect = self.dropIndicatorRect 91 | rect = opt.rect 92 | 93 | if rect.height() == 0: 94 | pen = QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.DashLine) 95 | painter.setPen(pen) 96 | painter.drawLine(rect.topLeft(), rect.topRight()) 97 | else: 98 | pen = QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.DashLine) 99 | painter.setPen(pen) 100 | painter.drawRect(rect) 101 | 102 | 103 | class MyTreeWidget(QtGui.QTreeWidget, MyTreeView): 104 | 105 | # def mouseMoveEvent(self, e): 106 | # if self.state()==QtGui.QAbstractItemView.DraggingState: 107 | # mimeData = self.model().mimeData(self.selectedIndexes()) 108 | # drag = QtGui.QDrag(self) 109 | # drag.setMimeData(mimeData) 110 | # drag.exec_(QtCore.Qt.MoveAction) 111 | 112 | def startDrag(self, supportedActions): 113 | listsQModelIndex = self.selectedIndexes() 114 | if listsQModelIndex: 115 | mimeData = QtCore.QMimeData() 116 | dataQMimeData = self.model().mimeData(listsQModelIndex) 117 | # if not dataQMimeData: 118 | # return None 119 | dragQDrag = QtGui.QDrag(self) 120 | # dragQDrag.setPixmap(QtGui.QPixmap('test.jpg')) # <- For put your custom image here 121 | dragQDrag.setMimeData(dataQMimeData) 122 | defaultDropAction = QtCore.Qt.IgnoreAction 123 | if ((supportedActions & QtCore.Qt.CopyAction) and (self.dragDropMode() != QtGui.QAbstractItemView.InternalMove)): 124 | defaultDropAction = QtCore.Qt.CopyAction 125 | dragQDrag.exec_(supportedActions, defaultDropAction) 126 | 127 | def dragMoveEvent(self, event): 128 | pos = event.pos() 129 | item = self.itemAt(pos) 130 | 131 | if item: 132 | index = self.indexFromItem(item) 133 | 134 | rect = self.visualRect(index) 135 | rect_left = self.visualRect(index.sibling(index.row(), 0)) 136 | rect_right = self.visualRect(index.sibling(index.row(), self.columnCount() - 1)) 137 | self.dropIndicatorPosition = self.position(event.pos(), rect, index) 138 | 139 | if self.dropIndicatorPosition == self.AboveItem: 140 | self.dropIndicatorRect = QtCore.QRect(rect_left.left(), rect_left.top(), rect_right.right() - rect_left.left(), 0) 141 | event.accept() 142 | elif self.dropIndicatorPosition == self.BelowItem: 143 | self.dropIndicatorRect = QtCore.QRect(rect_left.left(), rect_left.bottom(), rect_right.right() - rect_left.left(), 0) 144 | event.accept() 145 | 146 | elif self.dropIndicatorPosition == self.OnItem: 147 | self.dropIndicatorRect = QtCore.QRect(rect_left.left(), rect_left.top(), rect_right.right() - rect_left.left(), rect.height()) 148 | event.accept() 149 | else: 150 | self.dropIndicatorRect = QtCore.QRect() 151 | 152 | self.model().setData(index, self.dropIndicatorPosition, Qt.UserRole) 153 | 154 | # self.setState(QtGui.QAbstractItemView.DraggingState) 155 | # This is necessary or else the previously drawn rect won't be erased 156 | self.viewport().update() 157 | 158 | def iterativeChildren(self, nodes): 159 | results = [] 160 | while True: 161 | newNodes = [] 162 | if not nodes: 163 | break 164 | for node in nodes: 165 | results.append(node) 166 | for i in range(node.childCount()): 167 | print 'newNodes:', newNodes 168 | newNodes += [node.child(i)] 169 | nodes = newNodes 170 | results = nodes + results 171 | return results 172 | 173 | def keyPressEvent(self, event): 174 | 'delete currently selected item' 175 | QtGui.QTreeWidget.keyPressEvent(self, event) 176 | key = event.key() 177 | 178 | if self.currentItem(): 179 | 180 | root = self.invisibleRootItem() 181 | parent = self.currentItem().parent() or root 182 | 183 | if key == Qt.Key_Delete: 184 | parent.removeChild(self.currentItem()) 185 | 186 | def dropEvent(self, event): 187 | pos = event.pos() 188 | item = self.itemAt(pos) 189 | if item: 190 | index = self.indexFromItem(item) 191 | self.model().setData(index, 0, Qt.UserRole) 192 | 193 | if item is self.currentItem(): 194 | QtGui.QTreeWidget.dropEvent(self, event) 195 | event.accept() 196 | return 197 | 198 | if event.source == self and event.dropAction() == Qt.MoveAction or self.dragDropMode() == QtGui.QAbstractItemView.InternalMove: 199 | 200 | topIndex = QtCore.QModelIndex() 201 | col = -1 202 | row = -1 203 | 204 | l = [event, row, col, topIndex] 205 | 206 | if self.dropOn(l): 207 | 208 | event, row, col, topIndex = l 209 | 210 | idxs = self.selectedIndexes() 211 | indexes = [] 212 | existing_rows = set() 213 | for i in idxs: 214 | if i.row() not in existing_rows: 215 | indexes.append(i) 216 | existing_rows.add(i.row()) 217 | 218 | if topIndex in indexes: 219 | return 220 | 221 | # try storing the itemWidgets first 222 | # we should iterate through all child items,and store itemWidgets for them 223 | widgets = [] 224 | 225 | dropRow = self.model().index(row, col, topIndex) 226 | taken = [] 227 | 228 | indexes_reverse = indexes[:] 229 | indexes_reverse.reverse() 230 | # i = 0 231 | for index in indexes_reverse: 232 | parent = self.itemFromIndex(index) 233 | item_widget = self.itemWidget(parent, 0) 234 | 235 | print 'item_widget:', item_widget, item_widget.parent() 236 | 237 | # item_widget.setParent(self) 238 | print 'dragging item has child:', parent.childCount() 239 | 240 | # print 'before dragging, child 0 ',self.itemWidget( parent.child(0),0).browseBtn.text() 241 | 242 | # in case it has children , we get all of them 243 | all_child = [] 244 | 245 | all_items = self.iterativeChildren([parent]) 246 | 247 | print 'all items:', len(all_items), all_items 248 | 249 | # store cloned widgets in a list 250 | widgets = [self.itemWidget(i, 0).clone() for i in all_items] 251 | 252 | # widgets.append(item_widget.clone()) 253 | 254 | if not parent or not parent.parent(): 255 | # if not parent or not isinstance(parent.parent(),QtGui.QTreeWidgetItem): 256 | taken.append(self.takeTopLevelItem(index.row())) 257 | else: 258 | taken.append(parent.parent().takeChild(index.row())) 259 | 260 | # i += 1 261 | # break 262 | 263 | taken.reverse() 264 | 265 | print 'itemWidgets:', widgets 266 | 267 | for index in indexes: 268 | print 'inserting: topIndex:', topIndex.isValid(), row 269 | if row == -1: # means index=root 270 | 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. 271 | parent = self.itemFromIndex(topIndex) 272 | parent.insertChild(parent.childCount(), taken[0]) 273 | 274 | # after insert the itemwidget is gone 275 | # print 'after dragging, child 0 ',self.itemWidget( taken[0],0).browseBtn.text() 276 | 277 | # self.setItemWidget(taken[0],0,QtGui.QLineEdit()) 278 | # self.setItemWidget(taken[0],0,new_widget) 279 | print 'row==-1,if', # self.itemWidget(taken[0],0),self.itemWidget(taken[0],0).parent() 280 | # taken = taken[1:] 281 | 282 | else: 283 | self.insertTopLevelItem(self.topLevelItemCount(), taken[0]) 284 | # taken = taken[1:] 285 | print 'row==-1,else' 286 | else: 287 | r = dropRow.row() if dropRow.row() >= 0 else row 288 | if topIndex.isValid(): 289 | parent = self.itemFromIndex(topIndex) 290 | parent.insertChild(min(r, parent.childCount()), taken[0]) 291 | # taken = taken[1:] 292 | print 'row!=-1,if' 293 | else: 294 | self.insertTopLevelItem(min(r, self.topLevelItemCount()), taken[0]) 295 | # taken = taken[1:] 296 | print 'row!=-1,else' 297 | 298 | all_items = self.iterativeChildren([taken[0]]) 299 | 300 | for i, w in zip(all_items, widgets): 301 | self.setItemWidget(i, 0, w) 302 | 303 | taken = taken[1:] 304 | event.accept() 305 | 306 | QtGui.QTreeWidget.dropEvent(self, event) 307 | self.expandAll() 308 | self.updateGeometry() 309 | 310 | def position(self, pos, rect, index): 311 | r = QtGui.QAbstractItemView.OnViewport 312 | # margin*2 must be smaller than row height, or the drop onItem rect won't show 313 | margin = 10 314 | if pos.y() - rect.top() < margin: 315 | r = QtGui.QAbstractItemView.AboveItem 316 | elif rect.bottom() - pos.y() < margin: 317 | r = QtGui.QAbstractItemView.BelowItem 318 | # elif rect.contains(pos, True): 319 | elif pos.y() - rect.top() > margin and rect.bottom() - pos.y() > margin: 320 | r = QtGui.QAbstractItemView.OnItem 321 | 322 | return r 323 | 324 | def dropOn(self, l): 325 | 326 | event, row, col, index = l 327 | 328 | root = self.rootIndex() 329 | 330 | if self.viewport().rect().contains(event.pos()): 331 | index = self.indexAt(event.pos()) 332 | # if drop on nothing or drop out side of index zone 333 | print 'in drop on ', index, index.isValid(), self.visualRect(index).contains(event.pos()) 334 | if not index.isValid() or not self.visualRect(index).contains(event.pos()): 335 | index = root 336 | 337 | if index != root: 338 | 339 | dropIndicatorPosition = self.position(event.pos(), self.visualRect(index), index) 340 | if self.dropIndicatorPosition == self.AboveItem: 341 | print 'dropon above' 342 | row = index.row() 343 | col = index.column() 344 | index = index.parent() 345 | 346 | elif self.dropIndicatorPosition == self.BelowItem: 347 | print 'dropon below' 348 | row = index.row() + 1 349 | col = index.column() 350 | index = index.parent() 351 | 352 | elif self.dropIndicatorPosition == self.OnItem: 353 | print 'dropon onItem' 354 | pass 355 | elif self.dropIndicatorPosition == self.OnViewport: 356 | pass 357 | else: 358 | pass 359 | 360 | else: 361 | self.dropIndicatorPosition = self.OnViewport 362 | 363 | l[0], l[1], l[2], l[3] = event, row, col, index 364 | 365 | # if not self.droppingOnItself(event, index): 366 | return True 367 | 368 | 369 | class TheUI(QtGui.QDialog): 370 | 371 | def __init__(self, args=None, parent=None): 372 | super(TheUI, self).__init__(parent) 373 | self.layout1 = QtGui.QVBoxLayout(self) 374 | treeWidget = MyTreeWidget() 375 | 376 | # treeWidget.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) 377 | # treeWidget.setSelectionRectVisible(True) 378 | 379 | button1 = QtGui.QPushButton('Add') 380 | button2 = QtGui.QPushButton('Add Child') 381 | 382 | self.layout1.addWidget(treeWidget) 383 | 384 | self.layout2 = QtGui.QHBoxLayout() 385 | self.layout2.addWidget(button1) 386 | self.layout2.addWidget(button2) 387 | 388 | self.layout1.addLayout(self.layout2) 389 | 390 | treeWidget.setHeaderHidden(True) 391 | 392 | self.treeWidget = treeWidget 393 | self.button1 = button1 394 | self.button2 = button2 395 | self.button1.clicked.connect(lambda *x: self.addCmd()) 396 | self.button2.clicked.connect(lambda *x: self.addChildCmd()) 397 | 398 | HEADERS = ("script", "chunksize", "mem") 399 | self.treeWidget.setHeaderLabels(HEADERS) 400 | self.treeWidget.setColumnCount(len(HEADERS)) 401 | 402 | self.treeWidget.setColumnWidth(0, 160) 403 | self.treeWidget.header().show() 404 | 405 | self.treeWidget.setDragDropMode(QtGui.QAbstractItemView.InternalMove) 406 | self.treeWidget.setStyleSheet(''' 407 | QTreeView { 408 | show-decoration-selected: 1; 409 | } 410 | 411 | QTreeView::item:hover { 412 | background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1); 413 | } 414 | 415 | QTreeView::item:selected:active{ 416 | background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6ea1f1, stop: 1 #567dbc); 417 | } 418 | 419 | QTreeView::item:selected:!active { 420 | background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6b9be8, stop: 1 #577fbf); 421 | } 422 | ''') 423 | 424 | self.resize(500, 350) 425 | for i in xrange(6): 426 | item = self.addCmd(i) 427 | if i in (3, 4): 428 | self.addChildCmd() 429 | if i == 4: 430 | self.addCmd('%s-2' % i, parent=item) 431 | 432 | self.treeWidget.expandAll() 433 | self.setStyleSheet("QTreeWidget::item{ height: 30px; }") 434 | 435 | def addChildCmd(self): 436 | parent = self.treeWidget.currentItem() 437 | self.addCmd(parent=parent) 438 | self.treeWidget.setCurrentItem(parent) 439 | 440 | def addCmd(self, i=None, parent=None): 441 | 'add a level to tree widget' 442 | 443 | root = self.treeWidget.invisibleRootItem() 444 | if not parent: 445 | parent = root 446 | 447 | if i is None: 448 | if parent == root: 449 | i = self.treeWidget.topLevelItemCount() 450 | else: 451 | i = str(parent.text(0)).strip()[7:] 452 | i = '%s-%s' % (i, parent.childCount() + 1) 453 | 454 | # item = QtGui.QTreeWidgetItem(parent, ['script %s' % i, '1', '150']) 455 | 456 | script = ' script %s' % i 457 | item = QtGui.QTreeWidgetItem(parent, [script, '1', '150']) 458 | 459 | self.treeWidget.setItemWidget(item, 0, MyWidget(val=script)) 460 | 461 | self.treeWidget.setCurrentItem(item) 462 | self.treeWidget.expandAll() 463 | return item 464 | 465 | if __name__ == '__main__': 466 | app = QtGui.QApplication(sys.argv) 467 | gui = TheUI() 468 | gui.show() 469 | app.exec_() 470 | 471 | ``` 472 | -------------------------------------------------------------------------------- /tree/drop_indicator.md: -------------------------------------------------------------------------------- 1 | # 自定义drop indicator 2 | 想实现如下的效果: 3 | - 加粗的drop indicator 4 | - 修改插入的“判定”灵敏度(这一点很重要,因为默认判定是2px,很难轻松的把拖拽的item“插入”两行之间) 5 | 6 | ![custom_drop_indicator](img/custom_drop_indicator.gif) 7 | 8 | 其中 9 | - `MyTreeView`里的`paintDropIndicator`用来自定义paint drop indicator 10 | - `position` function用来修改默认的插入“判定”,原始默认值是2,显然`margin*2`必须小于行高,不然“恰好”放在item上的判定就没法发生了 11 | - 在`dragMoveEvent`里通过position返回的“判定”,来决定表示放手位置的dropIndicatorRect的坐标 12 | - `dropEvent`就是把c++版直接翻译了下,应该需要继续改进,很多地方不是python里的恰当写法 13 | 14 | 代码如下 15 | ```python 16 | #!/usr/bin/env python2 17 | 18 | import os 19 | import sys 20 | import re 21 | 22 | from PyQt4 import QtGui, QtCore 23 | from PyQt4.QtCore import Qt, QString 24 | 25 | 26 | class MyTreeView(QtGui.QTreeView): 27 | 28 | def __init__(self, parent=None): 29 | super(MyTreeView, self).__init__(parent) 30 | self.dropIndicatorRect = QtCore.QRect() 31 | 32 | def paintEvent(self, event): 33 | painter = QtGui.QPainter(self.viewport()) 34 | self.drawTree(painter, event.region()) 35 | # in original implementation, it calls an inline function paintDropIndicator here 36 | self.paintDropIndicator(painter) 37 | 38 | def paintDropIndicator(self, painter): 39 | 40 | if self.state() == QtGui.QAbstractItemView.DraggingState: 41 | opt = QtGui.QStyleOption() 42 | opt.init(self) 43 | opt.rect = self.dropIndicatorRect 44 | rect = opt.rect 45 | 46 | brush = QtGui.QBrush(QtGui.QColor(Qt.black)) 47 | 48 | if rect.height() == 0: 49 | pen = QtGui.QPen(brush, 2, QtCore.Qt.SolidLine) 50 | painter.setPen(pen) 51 | painter.drawLine(rect.topLeft(), rect.topRight()) 52 | else: 53 | pen = QtGui.QPen(brush, 2, QtCore.Qt.SolidLine) 54 | painter.setPen(pen) 55 | painter.drawRect(rect) 56 | 57 | 58 | class MyTreeWidget(QtGui.QTreeWidget, MyTreeView): 59 | 60 | def startDrag(self, supportedActions): 61 | listsQModelIndex = self.selectedIndexes() 62 | if listsQModelIndex: 63 | mimeData = QtCore.QMimeData() 64 | dataQMimeData = self.model().mimeData(listsQModelIndex) 65 | dragQDrag = QtGui.QDrag(self) 66 | # dragQDrag.setPixmap(QtGui.QPixmap('test.jpg')) # <- For put your custom image here 67 | dragQDrag.setMimeData(dataQMimeData) 68 | defaultDropAction = QtCore.Qt.IgnoreAction 69 | if ((supportedActions & QtCore.Qt.CopyAction) and (self.dragDropMode() != QtGui.QAbstractItemView.InternalMove)): 70 | defaultDropAction = QtCore.Qt.CopyAction 71 | dragQDrag.exec_(supportedActions, defaultDropAction) 72 | 73 | def dragMoveEvent(self, event): 74 | pos = event.pos() 75 | item = self.itemAt(pos) 76 | 77 | if item: 78 | index = self.indexFromItem(item) # this always get the default 0 column index 79 | 80 | rect = self.visualRect(index) 81 | rect_left = self.visualRect(index.sibling(index.row(), 0)) 82 | rect_right = self.visualRect(index.sibling(index.row(), self.header().logicalIndex(self.columnCount() - 1))) # in case section has been moved 83 | 84 | self.dropIndicatorPosition = self.position(event.pos(), rect, index) 85 | 86 | if self.dropIndicatorPosition == self.AboveItem: 87 | self.dropIndicatorRect = QtCore.QRect(rect_left.left(), rect_left.top(), rect_right.right() - rect_left.left(), 0) 88 | event.accept() 89 | elif self.dropIndicatorPosition == self.BelowItem: 90 | self.dropIndicatorRect = QtCore.QRect(rect_left.left(), rect_left.bottom(), rect_right.right() - rect_left.left(), 0) 91 | event.accept() 92 | elif self.dropIndicatorPosition == self.OnItem: 93 | self.dropIndicatorRect = QtCore.QRect(rect_left.left(), rect_left.top(), rect_right.right() - rect_left.left(), rect.height()) 94 | event.accept() 95 | else: 96 | self.dropIndicatorRect = QtCore.QRect() 97 | 98 | self.model().setData(index, self.dropIndicatorPosition, Qt.UserRole) 99 | 100 | # This is necessary or else the previously drawn rect won't be erased 101 | self.viewport().update() 102 | 103 | def dropEvent(self, event): 104 | pos = event.pos() 105 | item = self.itemAt(pos) 106 | 107 | if item is self.currentItem(): 108 | QtGui.QTreeWidget.dropEvent(self, event) 109 | event.accept() 110 | return 111 | 112 | if item: 113 | index = self.indexFromItem(item) 114 | self.model().setData(index, 0, Qt.UserRole) 115 | 116 | if event.source == self and event.dropAction() == Qt.MoveAction or self.dragDropMode() == QtGui.QAbstractItemView.InternalMove: 117 | 118 | topIndex = QtCore.QModelIndex() 119 | col = -1 120 | row = -1 121 | 122 | l = [event, row, col, topIndex] 123 | 124 | if self.dropOn(l): 125 | 126 | event, row, col, topIndex = l 127 | 128 | idxs = self.selectedIndexes() 129 | indexes = [] 130 | existing_rows = set() 131 | for i in idxs: 132 | if i.row() not in existing_rows: 133 | indexes.append(i) 134 | existing_rows.add(i.row()) 135 | 136 | if topIndex in indexes: 137 | return 138 | 139 | dropRow = self.model().index(row, col, topIndex) 140 | taken = [] 141 | 142 | indexes_reverse = indexes[:] 143 | indexes_reverse.reverse() 144 | i = 0 145 | for index in indexes_reverse: 146 | parent = self.itemFromIndex(index) 147 | if not parent or not parent.parent(): 148 | # if not parent or not isinstance(parent.parent(),QtGui.QTreeWidgetItem): 149 | taken.append(self.takeTopLevelItem(index.row())) 150 | else: 151 | taken.append(parent.parent().takeChild(index.row())) 152 | 153 | i += 1 154 | # break 155 | 156 | taken.reverse() 157 | 158 | for index in indexes: 159 | if row == -1: 160 | if topIndex.isValid(): 161 | parent = self.itemFromIndex(topIndex) 162 | parent.insertChild(parent.childCount(), taken[0]) 163 | taken = taken[1:] 164 | 165 | else: 166 | self.insertTopLevelItem(self.topLevelItemCount(), taken[0]) 167 | taken = taken[1:] 168 | else: 169 | r = dropRow.row() if dropRow.row() >= 0 else row 170 | if topIndex.isValid(): 171 | parent = self.itemFromIndex(topIndex) 172 | parent.insertChild(min(r, parent.childCount()), taken[0]) 173 | taken = taken[1:] 174 | else: 175 | self.insertTopLevelItem(min(r, self.topLevelItemCount()), taken[0]) 176 | taken = taken[1:] 177 | 178 | event.accept() 179 | 180 | QtGui.QTreeWidget.dropEvent(self, event) 181 | self.expandAll() 182 | 183 | def position(self, pos, rect, index): 184 | r = QtGui.QAbstractItemView.OnViewport 185 | # margin*2 must be smaller than row height, or the drop onItem rect won't show 186 | margin = 10 187 | if pos.y() - rect.top() < margin: 188 | r = QtGui.QAbstractItemView.AboveItem 189 | elif rect.bottom() - pos.y() < margin: 190 | r = QtGui.QAbstractItemView.BelowItem 191 | 192 | # this rect is always the first column rect 193 | # elif rect.contains(pos, True): 194 | elif pos.y() - rect.top() > margin and rect.bottom() - pos.y() > margin: 195 | r = QtGui.QAbstractItemView.OnItem 196 | 197 | return r 198 | 199 | def dropOn(self, l): 200 | 201 | event, row, col, index = l 202 | 203 | root = self.rootIndex() 204 | 205 | if self.viewport().rect().contains(event.pos()): 206 | index = self.indexAt(event.pos()) 207 | if not index.isValid() or not self.visualRect(index).contains(event.pos()): 208 | index = root 209 | 210 | if index != root: 211 | 212 | dropIndicatorPosition = self.position(event.pos(), self.visualRect(index), index) 213 | if self.dropIndicatorPosition == self.AboveItem: 214 | print 'dropon above' 215 | row = index.row() 216 | col = index.column() 217 | index = index.parent() 218 | 219 | elif self.dropIndicatorPosition == self.BelowItem: 220 | print 'dropon below' 221 | row = index.row() + 1 222 | col = index.column() 223 | index = index.parent() 224 | 225 | elif self.dropIndicatorPosition == self.OnItem: 226 | print 'dropon onItem' 227 | pass 228 | elif self.dropIndicatorPosition == self.OnViewport: 229 | pass 230 | else: 231 | pass 232 | 233 | else: 234 | self.dropIndicatorPosition = self.OnViewport 235 | 236 | l[0], l[1], l[2], l[3] = event, row, col, index 237 | 238 | # if not self.droppingOnItself(event, index): 239 | return True 240 | 241 | 242 | class TheUI(QtGui.QDialog): 243 | 244 | def __init__(self, args=None, parent=None): 245 | super(TheUI, self).__init__(parent) 246 | self.layout1 = QtGui.QVBoxLayout(self) 247 | treeWidget = MyTreeWidget() 248 | 249 | treeWidget.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) 250 | 251 | button1 = QtGui.QPushButton('Add') 252 | button2 = QtGui.QPushButton('Add Child') 253 | 254 | self.layout1.addWidget(treeWidget) 255 | 256 | self.layout2 = QtGui.QHBoxLayout() 257 | self.layout2.addWidget(button1) 258 | self.layout2.addWidget(button2) 259 | 260 | self.layout1.addLayout(self.layout2) 261 | 262 | treeWidget.setHeaderHidden(True) 263 | 264 | self.treeWidget = treeWidget 265 | self.button1 = button1 266 | self.button2 = button2 267 | self.button1.clicked.connect(lambda *x: self.addCmd()) 268 | self.button2.clicked.connect(lambda *x: self.addChildCmd()) 269 | 270 | HEADERS = ("script", "chunksize", "mem") 271 | self.treeWidget.setHeaderLabels(HEADERS) 272 | self.treeWidget.setColumnCount(len(HEADERS)) 273 | 274 | self.treeWidget.setColumnWidth(0, 160) 275 | self.treeWidget.header().show() 276 | 277 | self.treeWidget.setDragDropMode(QtGui.QAbstractItemView.InternalMove) 278 | self.treeWidget.setStyleSheet(''' 279 | QTreeView { 280 | show-decoration-selected: 1; 281 | } 282 | 283 | QTreeView::item:hover { 284 | background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1); 285 | } 286 | 287 | QTreeView::item:selected:active{ 288 | background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6ea1f1, stop: 1 #567dbc); 289 | } 290 | 291 | QTreeView::item:selected:!active { 292 | background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6b9be8, stop: 1 #577fbf); 293 | } 294 | ''') 295 | 296 | self.resize(500, 350) 297 | for i in xrange(6): 298 | item = self.addCmd(i) 299 | if i in (3, 4): 300 | self.addChildCmd() 301 | if i == 4: 302 | self.addCmd('%s-2' % i, parent=item) 303 | 304 | self.treeWidget.expandAll() 305 | self.setStyleSheet("QTreeWidget::item{ height: 30px; }") 306 | 307 | def addChildCmd(self): 308 | parent = self.treeWidget.currentItem() 309 | self.addCmd(parent=parent) 310 | self.treeWidget.setCurrentItem(parent) 311 | 312 | def addCmd(self, i=None, parent=None): 313 | 'add a level to tree widget' 314 | 315 | root = self.treeWidget.invisibleRootItem() 316 | if not parent: 317 | parent = root 318 | 319 | if i is None: 320 | if parent == root: 321 | i = self.treeWidget.topLevelItemCount() 322 | else: 323 | i = str(parent.text(0))[7:] 324 | i = '%s-%s' % (i, parent.childCount() + 1) 325 | 326 | item = QtGui.QTreeWidgetItem(parent, ['script %s' % i, '1', '150']) 327 | 328 | self.treeWidget.setCurrentItem(item) 329 | self.treeWidget.expandAll() 330 | return item 331 | 332 | if __name__ == '__main__': 333 | app = QtGui.QApplication(sys.argv) 334 | gui = TheUI() 335 | gui.show() 336 | app.exec_() 337 | ``` 338 | 339 | 340 | -------------------------------------------------------------------------------- /tree/img/custom_drop_indicator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykuu/PyQt-PySide-Cookbook/fe9a65d47c80e3846149d48c9c9b90e46421e00a/tree/img/custom_drop_indicator.gif -------------------------------------------------------------------------------- /tree/img/itemWidget_dragging.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykuu/PyQt-PySide-Cookbook/fe9a65d47c80e3846149d48c9c9b90e46421e00a/tree/img/itemWidget_dragging.gif -------------------------------------------------------------------------------- /tree/img/transparent_dragging.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykuu/PyQt-PySide-Cookbook/fe9a65d47c80e3846149d48c9c9b90e46421e00a/tree/img/transparent_dragging.gif -------------------------------------------------------------------------------- /widget/31qmenu.md: -------------------------------------------------------------------------------- 1 | # 3.1.QAction 2 | 类似maya里的下拉菜单 3 | 4 | ![img](img/maya.png) 5 | ```python 6 | import os 7 | import sys 8 | 9 | from PyQt4 import QtGui, QtCore 10 | 11 | MY_ICON = [ 12 | '16 16 4 1', 13 | ' c None', 14 | '. c #c6c4c4', 15 | 'l c #888888', 16 | ': c #909090', 17 | ' ', 18 | ' ', 19 | ' ', 20 | ' ', 21 | ' llllllll ', 22 | ' l l ', 23 | ' l l ', 24 | ' l l ', 25 | ' l l ', 26 | ' l l ', 27 | ' l l ', 28 | ' l l ', 29 | ' llllllll ', 30 | ' ', 31 | ' ', 32 | ' ', 33 | ' ', 34 | ' ', 35 | ] 36 | 37 | ## Custom Action used to provide an action and a clickable icon 38 | # 39 | class ExtendedQAction(QtGui.QWidgetAction): 40 | def __init__(self, label, mainAction, secondaryAction, *args, **kw): 41 | QtGui.QWidgetAction.__init__(self, *args, **kw) 42 | 43 | myWidget = QtGui.QWidget() 44 | myLayout = QtGui.QHBoxLayout() 45 | myLayout.setSpacing( 0 ) 46 | myLayout.setContentsMargins( 0, 0, 0, 0 ) 47 | myWidget.setLayout(myLayout) 48 | myLabel = ExtendedQLabel(label) 49 | myIcon = ExtendedQLabel() 50 | myIcon.setPixmap(QtGui.QPixmap(MY_ICON)) 51 | myLayout.addWidget(myLabel, stretch=1) 52 | myLayout.addWidget(myIcon, stretch=0) 53 | 54 | ## Hack in the hover colors to a style sheet 55 | # The global stylesheet is not controlling the highlight color. 56 | # It would be good to figure out how to avoid hardcoding styles here. 57 | defaultHLBackground = "#%02x%02x%02x" % myWidget.palette().highlight().color().getRgb()[:3] 58 | defaultHLText = "#%02x%02x%02x" % myWidget.palette().highlightedText().color().getRgb()[:3] 59 | myLabel.setStyleSheet('padding-left:14px') 60 | myWidget.setStyleSheet("QWidget:hover { background:%s; color: %s;} QWidget { padding: 4px; margin:0px}" % (defaultHLBackground,defaultHLText)) 61 | 62 | 63 | myIcon.setToolTip("Secondary Action Toolttip" ) 64 | 65 | self.connect(myLabel, QtCore.SIGNAL('clicked()'), mainAction) 66 | self.connect(myIcon, QtCore.SIGNAL('clicked()'), secondaryAction) 67 | 68 | self.setDefaultWidget(myWidget) 69 | 70 | 71 | ## Clickable QLabel, this was the path of least resistance for making a 72 | # clickable image/label. 73 | # 74 | class ExtendedQLabel(QtGui.QLabel): 75 | 76 | def __init(self, parent): 77 | QtGui.QLabel.__init__(self, parent) 78 | 79 | def mouseReleaseEvent(self, ev): 80 | self.emit(QtCore.SIGNAL('clicked()')) 81 | 82 | 83 | ################################################################################ 84 | # Usage of the dual action menu items. 85 | ################################################################################ 86 | class MyMainWindow(QtGui.QMainWindow): 87 | def __init__(self, *args, **kwargs): 88 | QtGui.QMainWindow.__init__(self, *args, **kwargs) 89 | 90 | regularAction = QtGui.QAction('Foo', self) 91 | extendedAction = ExtendedQAction('Bar', self.mainAction, self.secondaryAction, self) 92 | regularAction2 = QtGui.QAction('Hello', self) 93 | extendedAction2 = ExtendedQAction('World', self.mainAction, self.secondaryAction, self) 94 | 95 | menubar = self.menuBar() 96 | myMenu = menubar.addMenu('&Menu') 97 | myMenu.addAction(regularAction) 98 | myMenu.addAction(extendedAction) 99 | myMenu.addAction(regularAction2) 100 | myMenu.addAction(extendedAction2) 101 | 102 | self.setWindowTitle('Fun and Games') 103 | 104 | def mainAction(self): 105 | print "performing main action" 106 | 107 | def secondaryAction(self): 108 | print "performing sectiondary action" 109 | 110 | def main(): 111 | app = QtGui.QApplication(sys.argv) 112 | funAndGames = MyMainWindow() 113 | funAndGames.show() 114 | sys.exit(app.exec_()) 115 | 116 | if __name__ == '__main__': 117 | main() 118 | ``` 119 | 120 | -------------------------------------------------------------------------------- /widget/README.md: -------------------------------------------------------------------------------- 1 | # Widget 2 | -------------------------------------------------------------------------------- /widget/css_overflowlabel.md: -------------------------------------------------------------------------------- 1 | # 带overflow效果的按钮 2 | 想实现的效果是,当QPushButton上的label过长的时候,自动把过长的部分...类似css里的overflow效果 3 | 4 | ![elide_button](img/elide_button.gif) 5 | 6 | 代码如下: 7 | ```python 8 | #!/usr/bin/env python2 9 | import os 10 | import sys 11 | from PyQt4 import QtGui, QtCore 12 | from PyQt4.QtCore import Qt, QString 13 | 14 | 15 | class ElideButton(QtGui.QPushButton): 16 | 17 | def __init__(self, parent=None): 18 | 19 | super(ElideButton, self).__init__(parent) 20 | font = self.font() 21 | font.setPointSize(10) 22 | self.setFont(font) 23 | 24 | def paintEvent(self, event): 25 | painter = QtGui.QStylePainter(self) 26 | 27 | metrics = QtGui.QFontMetrics(self.font()) 28 | elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width()) 29 | 30 | option = QtGui.QStyleOptionButton() 31 | self.initStyleOption(option) 32 | option.text = '' 33 | painter.drawControl(QtGui.QStyle.CE_PushButton, option) 34 | painter.drawText(self.rect(), Qt.AlignLeft | Qt.AlignVCenter, elided) 35 | 36 | 37 | class TheUI(QtGui.QDialog): 38 | 39 | def __init__(self, args=None, parent=None): 40 | super(TheUI, self).__init__(parent) 41 | self.layout = QtGui.QVBoxLayout(self) 42 | self.button = ElideButton('Oh Yeah This is a super long string') 43 | self.layout.addWidget(self.button) 44 | self.setMinimumWidth(20) 45 | 46 | if __name__ == '__main__': 47 | app = QtGui.QApplication(sys.argv) 48 | gui = TheUI() 49 | gui.show() 50 | app.exec_() 51 | ``` 52 | 其实这个例子有个小缺陷,label左侧应该有些空隙(由style决定的)但是此处没有继承到。 53 | -------------------------------------------------------------------------------- /widget/img/elide_button.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykuu/PyQt-PySide-Cookbook/fe9a65d47c80e3846149d48c9c9b90e46421e00a/widget/img/elide_button.gif -------------------------------------------------------------------------------- /widget/img/maya.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmykuu/PyQt-PySide-Cookbook/fe9a65d47c80e3846149d48c9c9b90e46421e00a/widget/img/maya.png --------------------------------------------------------------------------------