skip to Main Content

I am developing an app for memorizing text using PyQt4. I want to show all the words in bubbles so that you see how long the word is. But when I have all the bubbles in my QScrollArea, they are aligned one under the other. I would like to have them aligned side-by-side, but with word-wrap.

I got the bubbles to work using a QLabel with rounded borders. But now that I have the words in QLabel's, PyQt doesn’t consider them as words – but as widgets. So PyQt puts one widget under the other. I would like the widgets to be aligned side-by-side until they reach the end of the line, and then they should wrap around to the next line – meaning the QLabel's should act like words in a text document.

Here is my code so far:

f = open(r'myFile.txt')

class Bubble(QtGui.QLabel):
    def __init__(self, text):
        super(Bubble, self).__init__(text)
        self.word = text
        self.setContentsMargins(5, 5, 5, 5)

    def paintEvent(self, e):
        p = QtGui.QPainter(self)
        p.setRenderHint(QtGui.QPainter.Antialiasing,True)
        p.drawRoundedRect(0,0,self.width()-1,self.height()-1,5,5)
        super(Bubble, self).paintEvent(e)


class MainWindow(QtGui.QMainWindow):
    def __init__(self, text, parent=None):
        QtGui.QMainWindow.__init__(self, parent)
        self.setupUi(self)
        self.MainArea = QtGui.QScrollArea
        self.widget = QtGui.QWidget()
        vbox = QtGui.QVBoxLayout()
        self.words = []
        for t in re.findall(r'bw+b', text):
            label = Bubble(t)
            label.setFont(QtGui.QFont('SblHebrew', 18))
            label.setFixedWidth(label.sizeHint().width())
            self.words.append(label)
            vbox.addWidget(label)
        self.widget.setLayout(vbox)
        self.MainArea.setWidget(self.widget)

if __name__ == '__main__':
    import sys
    app = QtGui.QApplication(sys.argv)
    myWindow = MainWindow(f.read(), None)
    myWindow.show()
    sys.exit(app.exec_())

When I run this I get:

picture of my example that I do not like

But I would like the words (the Qlabel's containing the words) to be next to each other, not under each other, like this (photoshopped):

enter image description here

I’ve been doing a lot of research, but no answers help me align the widgets next to each other.

2

Answers


  1. I thought it might be possible to use html in a QTextBrowser widget for this, but Qt’s rich-text engine doesn’t support the border-radius CSS property which would be needed for the bubble labels.

    So it looks like you need a PyQt port of the Flow Layout example. This can “word-wrap” a collection of widgets inside a container, and also allows the margins and horizontal/vertical spacing to be adjusted.

    Here is a demo script that implements the FlowLayout class and shows how to use it with your example:

    import sys
    from PyQt4 import QtCore, QtGui
    
    class FlowLayout(QtGui.QLayout):
        def __init__(self, parent=None, margin=-1, hspacing=-1, vspacing=-1):
            super(FlowLayout, self).__init__(parent)
            self._hspacing = hspacing
            self._vspacing = vspacing
            self._items = []
            self.setContentsMargins(margin, margin, margin, margin)
    
        def __del__(self):
            del self._items[:]
    
        def addItem(self, item):
            self._items.append(item)
    
        def horizontalSpacing(self):
            if self._hspacing >= 0:
                return self._hspacing
            else:
                return self.smartSpacing(
                    QtGui.QStyle.PM_LayoutHorizontalSpacing)
    
        def verticalSpacing(self):
            if self._vspacing >= 0:
                return self._vspacing
            else:
                return self.smartSpacing(
                    QtGui.QStyle.PM_LayoutVerticalSpacing)
    
        def count(self):
            return len(self._items)
    
        def itemAt(self, index):
            if 0 <= index < len(self._items):
                return self._items[index]
    
        def takeAt(self, index):
            if 0 <= index < len(self._items):
                return self._items.pop(index)
    
        def expandingDirections(self):
            return QtCore.Qt.Orientations(0)
    
        def hasHeightForWidth(self):
            return True
    
        def heightForWidth(self, width):
            return self.doLayout(QtCore.QRect(0, 0, width, 0), True)
    
        def setGeometry(self, rect):
            super(FlowLayout, self).setGeometry(rect)
            self.doLayout(rect, False)
    
        def sizeHint(self):
            return self.minimumSize()
    
        def minimumSize(self):
            size = QtCore.QSize()
            for item in self._items:
                size = size.expandedTo(item.minimumSize())
            left, top, right, bottom = self.getContentsMargins()
            size += QtCore.QSize(left + right, top + bottom)
            return size
    
        def doLayout(self, rect, testonly):
            left, top, right, bottom = self.getContentsMargins()
            effective = rect.adjusted(+left, +top, -right, -bottom)
            x = effective.x()
            y = effective.y()
            lineheight = 0
            for item in self._items:
                widget = item.widget()
                hspace = self.horizontalSpacing()
                if hspace == -1:
                    hspace = widget.style().layoutSpacing(
                        QtGui.QSizePolicy.PushButton,
                        QtGui.QSizePolicy.PushButton, QtCore.Qt.Horizontal)
                vspace = self.verticalSpacing()
                if vspace == -1:
                    vspace = widget.style().layoutSpacing(
                        QtGui.QSizePolicy.PushButton,
                        QtGui.QSizePolicy.PushButton, QtCore.Qt.Vertical)
                nextX = x + item.sizeHint().width() + hspace
                if nextX - hspace > effective.right() and lineheight > 0:
                    x = effective.x()
                    y = y + lineheight + vspace
                    nextX = x + item.sizeHint().width() + hspace
                    lineheight = 0
                if not testonly:
                    item.setGeometry(
                        QtCore.QRect(QtCore.QPoint(x, y), item.sizeHint()))
                x = nextX
                lineheight = max(lineheight, item.sizeHint().height())
            return y + lineheight - rect.y() + bottom
    
        def smartSpacing(self, pm):
            parent = self.parent()
            if parent is None:
                return -1
            elif parent.isWidgetType():
                return parent.style().pixelMetric(pm, None, parent)
            else:
                return parent.spacing()
    
    class Bubble(QtGui.QLabel):
        def __init__(self, text):
            super(Bubble, self).__init__(text)
            self.word = text
            self.setContentsMargins(5, 5, 5, 5)
    
        def paintEvent(self, event):
            painter = QtGui.QPainter(self)
            painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
            painter.drawRoundedRect(
                0, 0, self.width() - 1, self.height() - 1, 5, 5)
            super(Bubble, self).paintEvent(event)
    
    class MainWindow(QtGui.QMainWindow):
        def __init__(self, text, parent=None):
            super(MainWindow, self).__init__(parent)
            self.mainArea = QtGui.QScrollArea(self)
            self.mainArea.setWidgetResizable(True)
            widget = QtGui.QWidget(self.mainArea)
            widget.setMinimumWidth(50)
            layout = FlowLayout(widget)
            self.words = []
            for word in text.split():
                label = Bubble(word)
                label.setFont(QtGui.QFont('SblHebrew', 18))
                label.setFixedWidth(label.sizeHint().width())
                self.words.append(label)
                layout.addWidget(label)
            self.mainArea.setWidget(widget)
            self.setCentralWidget(self.mainArea)
    
    if __name__ == '__main__':
    
        app = QtGui.QApplication(sys.argv)
        window = MainWindow('Harry Potter is a series of fantasy literature')
        window.show()
        sys.exit(app.exec_())
    
    Login or Signup to reply.
  2. Here’s a PyQt5 version of the Flow Layout demo script:

    import sys
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    class FlowLayout(QtWidgets.QLayout):
        def __init__(self, parent=None, margin=-1, hspacing=-1, vspacing=-1):
            super(FlowLayout, self).__init__(parent)
            self._hspacing = hspacing
            self._vspacing = vspacing
            self._items = []
            self.setContentsMargins(margin, margin, margin, margin)
    
        def __del__(self):
            del self._items[:]
    
        def addItem(self, item):
            self._items.append(item)
    
        def horizontalSpacing(self):
            if self._hspacing >= 0:
                return self._hspacing
            else:
                return self.smartSpacing(
                    QtWidgets.QStyle.PM_LayoutHorizontalSpacing)
    
        def verticalSpacing(self):
            if self._vspacing >= 0:
                return self._vspacing
            else:
                return self.smartSpacing(
                    QtWidgets.QStyle.PM_LayoutVerticalSpacing)
    
        def count(self):
            return len(self._items)
    
        def itemAt(self, index):
            if 0 <= index < len(self._items):
                return self._items[index]
    
        def takeAt(self, index):
            if 0 <= index < len(self._items):
                return self._items.pop(index)
    
        def expandingDirections(self):
            return QtCore.Qt.Orientations(0)
    
        def hasHeightForWidth(self):
            return True
    
        def heightForWidth(self, width):
            return self.doLayout(QtCore.QRect(0, 0, width, 0), True)
    
        def setGeometry(self, rect):
            super(FlowLayout, self).setGeometry(rect)
            self.doLayout(rect, False)
    
        def sizeHint(self):
            return self.minimumSize()
    
        def minimumSize(self):
            size = QtCore.QSize()
            for item in self._items:
                size = size.expandedTo(item.minimumSize())
            left, top, right, bottom = self.getContentsMargins()
            size += QtCore.QSize(left + right, top + bottom)
            return size
    
        def doLayout(self, rect, testonly):
            left, top, right, bottom = self.getContentsMargins()
            effective = rect.adjusted(+left, +top, -right, -bottom)
            x = effective.x()
            y = effective.y()
            lineheight = 0
            for item in self._items:
                widget = item.widget()
                hspace = self.horizontalSpacing()
                if hspace == -1:
                    hspace = widget.style().layoutSpacing(
                        QtWidgets.QSizePolicy.PushButton,
                        QtWidgets.QSizePolicy.PushButton, QtCore.Qt.Horizontal)
                vspace = self.verticalSpacing()
                if vspace == -1:
                    vspace = widget.style().layoutSpacing(
                        QtWidgets.QSizePolicy.PushButton,
                        QtWidgets.QSizePolicy.PushButton, QtCore.Qt.Vertical)
                nextX = x + item.sizeHint().width() + hspace
                if nextX - hspace > effective.right() and lineheight > 0:
                    x = effective.x()
                    y = y + lineheight + vspace
                    nextX = x + item.sizeHint().width() + hspace
                    lineheight = 0
                if not testonly:
                    item.setGeometry(
                        QtCore.QRect(QtCore.QPoint(x, y), item.sizeHint()))
                x = nextX
                lineheight = max(lineheight, item.sizeHint().height())
            return y + lineheight - rect.y() + bottom
    
        def smartSpacing(self, pm):
            parent = self.parent()
            if parent is None:
                return -1
            elif parent.isWidgetType():
                return parent.style().pixelMetric(pm, None, parent)
            else:
                return parent.spacing()
    
    class Bubble(QtWidgets.QLabel):
        def __init__(self, text):
            super(Bubble, self).__init__(text)
            self.word = text
            self.setContentsMargins(5, 5, 5, 5)
    
        def paintEvent(self, event):
            painter = QtGui.QPainter(self)
            painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
            painter.drawRoundedRect(
                0, 0, self.width() - 1, self.height() - 1, 5, 5)
            super(Bubble, self).paintEvent(event)
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self, text, parent=None):
            super(MainWindow, self).__init__(parent)
            self.mainArea = QtWidgets.QScrollArea(self)
            self.mainArea.setWidgetResizable(True)
            widget = QtWidgets.QWidget(self.mainArea)
            widget.setMinimumWidth(50)
            layout = FlowLayout(widget)
            self.words = []
            for word in text.split():
                label = Bubble(word)
                label.setFont(QtGui.QFont('SblHebrew', 18))
                label.setFixedWidth(label.sizeHint().width())
                self.words.append(label)
                layout.addWidget(label)
            self.mainArea.setWidget(widget)
            self.setCentralWidget(self.mainArea)
    
    if __name__ == '__main__':
    
        app = QtWidgets.QApplication(sys.argv)
        window = MainWindow('Harry Potter is a series of fantasy literature')
        window.show()
        sys.exit(app.exec_())
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search