├── images ├── pyqt_collapsable_widget.gif └── pyqt_collapsable_widget_maya.gif ├── .gitignore ├── README.md ├── LICENSE └── code ├── main.py └── FrameLayout.py /images/pyqt_collapsable_widget.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-caro/pyqt-collapsible-widget/HEAD/images/pyqt_collapsable_widget.gif -------------------------------------------------------------------------------- /images/pyqt_collapsable_widget_maya.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-caro/pyqt-collapsible-widget/HEAD/images/pyqt_collapsable_widget_maya.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyQt Collapsable Widget 2 | *A PyQt collapsable widget in the spirit of the Maya* **frameLayout** *command* 3 | 4 | ## Why ? 5 | Because if there is no widget collapsable in PyQt (until Qt5). But the Maya cmds has a UI command called **frameLayout** that makes its content collapsable. Inside a PyQt design, for consistency, I made this code to create a collapsable widget. With the help from [this post](https://groups.google.com/d/msg/python_inside_maya/vO1pvA4YhF0/WpXMlkpgl54J), especially [this chunk](http://pastebin.com/qYgDDYsB). 6 | 7 | ## How does it look? 8 | **Here you can see the result in a standard Python application** 9 | 10 | ![PyQt Collapsable Widget in Python standalone](https://github.com/By0ute/pyqt-collapsable-widget/blob/master/images/pyqt_collapsable_widget.gif) 11 | 12 | **Here you can see the result from a Maya window** 13 | 14 | ![PyQt Collapsable Widget in Maya](https://github.com/By0ute/pyqt-collapsable-widget/blob/master/images/pyqt_collapsable_widget_maya.gif) 15 | 16 | ###### *Hope you'll enjoy it!*, :kissing_cat: 17 | [**By0ute**](https://github.com/By0ute) :princess: -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Caroline Beyne 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 | -------------------------------------------------------------------------------- /code/main.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Caroline Beyne' 2 | 3 | from PyQt4 import QtGui, QtCore 4 | import sys 5 | from FrameLayout import FrameLayout 6 | 7 | if __name__ == '__main__': 8 | 9 | app = QtGui.QApplication(sys.argv) 10 | 11 | win = QtGui.QMainWindow() 12 | w = QtGui.QWidget() 13 | w.setMinimumWidth(350) 14 | win.setCentralWidget(w) 15 | l = QtGui.QVBoxLayout() 16 | l.setSpacing(0) 17 | l.setAlignment(QtCore.Qt.AlignTop) 18 | w.setLayout(l) 19 | 20 | t = FrameLayout(title="Buttons") 21 | t.addWidget(QtGui.QPushButton('a')) 22 | t.addWidget(QtGui.QPushButton('b')) 23 | t.addWidget(QtGui.QPushButton('c')) 24 | 25 | f = FrameLayout(title="TableWidget") 26 | rows, cols = (6, 3) 27 | data = {'col1': ['1', '2', '3', '4', '5', '6'], 28 | 'col2': ['7', '8', '9', '10', '11', '12'], 29 | 'col3': ['13', '14', '15', '16', '17', '18']} 30 | table = QtGui.QTableWidget(rows, cols) 31 | headers = [] 32 | for n, key in enumerate(sorted(data.keys())): 33 | headers.append(key) 34 | for m, item in enumerate(data[key]): 35 | newitem = QtGui.QTableWidgetItem(item) 36 | table.setItem(m, n, newitem) 37 | table.setHorizontalHeaderLabels(headers) 38 | f.addWidget(table) 39 | 40 | l.addWidget(t) 41 | l.addWidget(f) 42 | 43 | win.show() 44 | win.raise_() 45 | print "Finish" 46 | sys.exit(app.exec_()) 47 | 48 | -------------------------------------------------------------------------------- /code/FrameLayout.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Caroline Beyne' 2 | 3 | from PyQt4 import QtGui, QtCore 4 | 5 | class FrameLayout(QtGui.QWidget): 6 | def __init__(self, parent=None, title=None): 7 | QtGui.QFrame.__init__(self, parent=parent) 8 | 9 | self._is_collasped = True 10 | self._title_frame = None 11 | self._content, self._content_layout = (None, None) 12 | 13 | self._main_v_layout = QtGui.QVBoxLayout(self) 14 | self._main_v_layout.addWidget(self.initTitleFrame(title, self._is_collasped)) 15 | self._main_v_layout.addWidget(self.initContent(self._is_collasped)) 16 | 17 | self.initCollapsable() 18 | 19 | def initTitleFrame(self, title, collapsed): 20 | self._title_frame = self.TitleFrame(title=title, collapsed=collapsed) 21 | 22 | return self._title_frame 23 | 24 | def initContent(self, collapsed): 25 | self._content = QtGui.QWidget() 26 | self._content_layout = QtGui.QVBoxLayout() 27 | 28 | self._content.setLayout(self._content_layout) 29 | self._content.setVisible(not collapsed) 30 | 31 | return self._content 32 | 33 | def addWidget(self, widget): 34 | self._content_layout.addWidget(widget) 35 | 36 | def initCollapsable(self): 37 | QtCore.QObject.connect(self._title_frame, QtCore.SIGNAL('clicked()'), self.toggleCollapsed) 38 | 39 | def toggleCollapsed(self): 40 | self._content.setVisible(self._is_collasped) 41 | self._is_collasped = not self._is_collasped 42 | self._title_frame._arrow.setArrow(int(self._is_collasped)) 43 | 44 | ############################ 45 | # TITLE # 46 | ############################ 47 | class TitleFrame(QtGui.QFrame): 48 | def __init__(self, parent=None, title="", collapsed=False): 49 | QtGui.QFrame.__init__(self, parent=parent) 50 | 51 | self.setMinimumHeight(24) 52 | self.move(QtCore.QPoint(24, 0)) 53 | self.setStyleSheet("border:1px solid rgb(41, 41, 41); ") 54 | 55 | self._hlayout = QtGui.QHBoxLayout(self) 56 | self._hlayout.setContentsMargins(0, 0, 0, 0) 57 | self._hlayout.setSpacing(0) 58 | 59 | self._arrow = None 60 | self._title = None 61 | 62 | self._hlayout.addWidget(self.initArrow(collapsed)) 63 | self._hlayout.addWidget(self.initTitle(title)) 64 | 65 | def initArrow(self, collapsed): 66 | self._arrow = FrameLayout.Arrow(collapsed=collapsed) 67 | self._arrow.setStyleSheet("border:0px") 68 | 69 | return self._arrow 70 | 71 | def initTitle(self, title=None): 72 | self._title = QtGui.QLabel(title) 73 | self._title.setMinimumHeight(24) 74 | self._title.move(QtCore.QPoint(24, 0)) 75 | self._title.setStyleSheet("border:0px") 76 | 77 | return self._title 78 | 79 | def mousePressEvent(self, event): 80 | self.emit(QtCore.SIGNAL('clicked()')) 81 | 82 | return super(FrameLayout.TitleFrame, self).mousePressEvent(event) 83 | 84 | 85 | ############################# 86 | # ARROW # 87 | ############################# 88 | class Arrow(QtGui.QFrame): 89 | def __init__(self, parent=None, collapsed=False): 90 | QtGui.QFrame.__init__(self, parent=parent) 91 | 92 | self.setMaximumSize(24, 24) 93 | 94 | # horizontal == 0 95 | self._arrow_horizontal = (QtCore.QPointF(7.0, 8.0), QtCore.QPointF(17.0, 8.0), QtCore.QPointF(12.0, 13.0)) 96 | # vertical == 1 97 | self._arrow_vertical = (QtCore.QPointF(8.0, 7.0), QtCore.QPointF(13.0, 12.0), QtCore.QPointF(8.0, 17.0)) 98 | # arrow 99 | self._arrow = None 100 | self.setArrow(int(collapsed)) 101 | 102 | def setArrow(self, arrow_dir): 103 | if arrow_dir: 104 | self._arrow = self._arrow_vertical 105 | else: 106 | self._arrow = self._arrow_horizontal 107 | 108 | def paintEvent(self, event): 109 | painter = QtGui.QPainter() 110 | painter.begin(self) 111 | painter.setBrush(QtGui.QColor(192, 192, 192)) 112 | painter.setPen(QtGui.QColor(64, 64, 64)) 113 | painter.drawPolygon(*self._arrow) 114 | painter.end() 115 | --------------------------------------------------------------------------------