By0ute / pyqt-collapsible-widget

PyQt collapsible widget in the spirit of the Maya frameLayout command
MIT License
56 stars 16 forks source link

PyQt5 Version #2

Open blabut opened 5 years ago

blabut commented 5 years ago

Hi Caroline,

Thank you very much for this code, helped me a lot ! I actually cloned it and modify it to make it PyQt5 compatible, since this is the version I am using. If you are interested, feel free to add me as a contributor and I'd be glad to share !

Best, Paul

wernerlievens commented 4 years ago

Hi Paul, I would be interested in your code. Would you consider providing it?

Regards, Werner

blabut commented 4 years ago

Hi Werner, of course! There you go:

# from PyQt5 import QtGui, QtCore
from PyQt5.QtWidgets import QFrame, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QPushButton
from PyQt5.QtCore import QPoint, QPointF
from PyQt5.QtGui import QColor
from PyQt5.QtGui import QPainter

class FrameLayout(QWidget):
    def __init__(self, parent=None, title=None):
        QFrame.__init__(self, parent=parent)
        self._is_collasped = True
        self._title_frame = None
        self._content, self._content_layout = (None, None)

        self._main_v_layout = QVBoxLayout(self)
        self._main_v_layout.addWidget(self.initTitleFrame(title, self._is_collasped))
        self._main_v_layout.addWidget(self.initContent(self._is_collasped))
        self.initCollapsable()

    def initTitleFrame(self, title, collapsed):
        self._title_frame = self.TitleFrame(title=title, collapsed=collapsed)

        return self._title_frame

    def initContent(self, collapsed):
        self._content = QWidget()
        self._content_layout = QVBoxLayout()

        self._content.setLayout(self._content_layout)
        self._content.setVisible(not collapsed)

        return self._content

    def addWidget(self, widget):
        self._content_layout.addWidget(widget)

    def initCollapsable(self):
        self._title_frame.clicked.connect(self.toggleCollapsed)

    def toggleCollapsed(self):
        self._content.setVisible(self._is_collasped)
        self._title_frame.setMinimumHeight([50,25][int(self._is_collasped)])
        self._is_collasped = not self._is_collasped
        self._title_frame._arrow.setArrow(int(self._is_collasped))

    ############################
    #           TITLE          #
    ############################
    class TitleFrame(QPushButton):
        def __init__(self, parent=None, title="", collapsed=False):
            QFrame.__init__(self, parent=parent)

            self.setMinimumHeight(50) # height of the button
            self.move(QPoint(24, 0))
            self.setStyleSheet("border:2px solid rgb(41, 41, 41); ")

            self._hlayout = QHBoxLayout(self)
            self._hlayout.setContentsMargins(0, 0, 0, 0)
            self._hlayout.setSpacing(0)

            self._arrow = None
            self._title = None

            self._hlayout.addWidget(self.initArrow(collapsed))
            self._hlayout.addWidget(self.initTitle(title))

        def initArrow(self, collapsed):
            self._arrow = FrameLayout.Arrow(collapsed=collapsed)
            self._arrow.setStyleSheet("border:0px")

            return self._arrow

        def initTitle(self, title=None):
            self._title = QLabel(title)
            self._title.setMinimumHeight(17)
            self._title.move(QPoint(24, 0))
            self._title.setStyleSheet("border:0px")

            return self._title

        # def mousePressEvent(self, event):
        #     self.emit(QtCore.SIGNAL('clicked()'))

            # return super(FrameLayout.TitleFrame, self).mousePressEvent(event)

    #############################
    #           ARROW           #
    #############################
    class Arrow(QFrame):
        def __init__(self, parent=None, collapsed=False):
            QFrame.__init__(self, parent=parent)

            self.setMaximumSize(24, 24)

            # horizontal == 0
            self._arrow_horizontal = (QPointF(7.0, 8.0), QPointF(17.0, 8.0), QPointF(12.0, 13.0))
            # vertical == 1
            self._arrow_vertical = (QPointF(8.0, 7.0), QPointF(13.0, 12.0), QPointF(8.0, 17.0))
            # arrow
            self._arrow = None
            self.setArrow(int(collapsed))

        def setArrow(self, arrow_dir):
            if arrow_dir:
                self._arrow = self._arrow_vertical
            else:
                self._arrow = self._arrow_horizontal

        def paintEvent(self, event):
            painter = QPainter()
            painter.begin(self)
            painter.setBrush(QColor(192, 192, 192))
            painter.setPen(QColor(64, 64, 64))
            painter.drawPolygon(*self._arrow)
            painter.end()

Cheers,

Paul

wernerlievens commented 4 years ago

Thanks for the fast reply Paul,

Greetings, Werner

wernerlievens commented 4 years ago

Hi Paul, Sorry to trouble you again. Do you have any sample code? Something like Caroline Beyne provides in her 'main.py' file? There seems to have changed a lot and i'm not an expert in QT5 stuff :)

Regards, Werner

blabut commented 4 years ago

Hi Werner,

Unfortunately the only project I have involving it is a professional one I cannot share in its integrity.. But basically, if you get familiar with the main concepts of PyQt5 (see here for instance), then you should be fine.

The main idea is that the FrameLayout behaves as an QWidget (a graphical element in PyQt5), so once instanciated, they can be for instance added to any layout object: layout_object.addWidget(your_frame_layout_instance)

To instanciate it, just provide the titleargument to the constructor: your_frame_layout_instance = FrameLayout(title = "your_title_here")

Then use the addWidget method to add the inner element(s) of your collapsible frame: your_frame_layout_instance.addWidget(any_widget_like_object)

I hope that helps! I am quite busy at the moment but I'll try to create a clean repo with the code and some examples soon.

Best, Paul

excalamus commented 3 years ago

@wernerlievens Here's a rough mashup of @paupaulaz and @By0ute's code. Basically, some components in QtGui were moved to QtWidgets. Replace the appropriate modules (by running and correcting errors) in __main__.py and update to use Python3 (print functions instead of expressions):

import sys
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtWidgets import QFrame, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QPushButton
from PyQt5.QtCore import QPoint, QPointF
from PyQt5.QtGui import QColor
from PyQt5.QtGui import QPainter

class FrameLayout(QWidget):
    def __init__(self, parent=None, title=None):
        QFrame.__init__(self, parent=parent)
        self._is_collasped = True
        self._title_frame = None
        self._content, self._content_layout = (None, None)

        self._main_v_layout = QVBoxLayout(self)
        self._main_v_layout.addWidget(self.initTitleFrame(title, self._is_collasped))
        self._main_v_layout.addWidget(self.initContent(self._is_collasped))
        self.initCollapsable()

    def initTitleFrame(self, title, collapsed):
        self._title_frame = self.TitleFrame(title=title, collapsed=collapsed)

        return self._title_frame

    def initContent(self, collapsed):
        self._content = QWidget()
        self._content_layout = QVBoxLayout()

        self._content.setLayout(self._content_layout)
        self._content.setVisible(not collapsed)

        return self._content

    def addWidget(self, widget):
        self._content_layout.addWidget(widget)

    def initCollapsable(self):
        self._title_frame.clicked.connect(self.toggleCollapsed)

    def toggleCollapsed(self):
        self._content.setVisible(self._is_collasped)
        self._title_frame.setMinimumHeight([50,25][int(self._is_collasped)])
        self._is_collasped = not self._is_collasped
        self._title_frame._arrow.setArrow(int(self._is_collasped))

    ############################
    #           TITLE          #
    ############################
    class TitleFrame(QPushButton):
        def __init__(self, parent=None, title="", collapsed=False):
            QFrame.__init__(self, parent=parent)

            self.setMinimumHeight(50) # height of the button
            self.move(QPoint(24, 0))
            self.setStyleSheet("border:2px solid rgb(41, 41, 41); ")

            self._hlayout = QHBoxLayout(self)
            self._hlayout.setContentsMargins(0, 0, 0, 0)
            self._hlayout.setSpacing(0)

            self._arrow = None
            self._title = None

            self._hlayout.addWidget(self.initArrow(collapsed))
            self._hlayout.addWidget(self.initTitle(title))

        def initArrow(self, collapsed):
            self._arrow = FrameLayout.Arrow(collapsed=collapsed)
            self._arrow.setStyleSheet("border:0px")

            return self._arrow

        def initTitle(self, title=None):
            self._title = QLabel(title)
            self._title.setMinimumHeight(17)
            self._title.move(QPoint(24, 0))
            self._title.setStyleSheet("border:0px")

            return self._title

        # def mousePressEvent(self, event):
        #     self.emit(QtCore.SIGNAL('clicked()'))

            # return super(FrameLayout.TitleFrame, self).mousePressEvent(event)

    #############################
    #           ARROW           #
    #############################
    class Arrow(QFrame):
        def __init__(self, parent=None, collapsed=False):
            QFrame.__init__(self, parent=parent)

            self.setMaximumSize(24, 24)

            # horizontal == 0
            self._arrow_horizontal = (QPointF(7.0, 8.0), QPointF(17.0, 8.0), QPointF(12.0, 13.0))
            # vertical == 1
            self._arrow_vertical = (QPointF(8.0, 7.0), QPointF(13.0, 12.0), QPointF(8.0, 17.0))
            # arrow
            self._arrow = None
            self.setArrow(int(collapsed))

        def setArrow(self, arrow_dir):
            if arrow_dir:
                self._arrow = self._arrow_vertical
            else:
                self._arrow = self._arrow_horizontal

        def paintEvent(self, event):
            painter = QPainter()
            painter.begin(self)
            painter.setBrush(QColor(192, 192, 192))
            painter.setPen(QColor(64, 64, 64))
            painter.drawPolygon(*self._arrow)
            painter.end()

if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)

    win = QtWidgets.QMainWindow()
    w = QtWidgets.QWidget()
    w.setMinimumWidth(350)
    win.setCentralWidget(w)
    l = QtWidgets.QVBoxLayout()
    l.setSpacing(0)
    l.setAlignment(QtCore.Qt.AlignTop)
    w.setLayout(l)

    t = FrameLayout(title="Buttons")
    t.addWidget(QtWidgets.QPushButton('a'))
    t.addWidget(QtWidgets.QPushButton('b'))
    t.addWidget(QtWidgets.QPushButton('c'))

    f = FrameLayout(title="TableWidget")
    rows, cols = (6, 3)
    data = {'col1': ['1', '2', '3', '4', '5', '6'],
            'col2': ['7', '8', '9', '10', '11', '12'],
            'col3': ['13', '14', '15', '16', '17', '18']}
    table = QtWidgets.QTableWidget(rows, cols)
    headers = []
    for n, key in enumerate(sorted(data.keys())):
        headers.append(key)
        for m, item in enumerate(data[key]):
            newitem = QtWidgets.QTableWidgetItem(item)
            table.setItem(m, n, newitem)
    table.setHorizontalHeaderLabels(headers)
    f.addWidget(table)

    l.addWidget(t)
    l.addWidget(f)

    win.show()
    win.raise_()
    print("\2")
    sys.exit(app.exec_())
Patitotective commented 2 years ago

Does the code should't be updated to PyQt5 and Python 3? I mean, update the repository not having the updated code on an issue

Patitotective commented 2 years ago

I've created my own collapsible widget based on this https://gist.github.com/Patitotective/0a9cd57dfeb448fd77c148bd9b7b2ed8