PyQt: diseño envolvente de widgets dentro de QScrollArea

Estoy desarrollando una aplicación para memorizar texto usando PyQt4. Quiero mostrar todas las palabras en burbujas para que vea cuánto tiempo es la palabra. Pero cuando tengo todas las burbujas en mi QScrollArea , están alineadas una debajo de la otra. Me gustaría tenerlos alineados lado a lado, pero con el ajuste de palabras.

Tengo las burbujas para trabajar usando una QLabel con bordes redondeados. Pero ahora que tengo las palabras en QLabel's , PyQt no las considera como palabras, sino como widgets. Así que PyQt pone un widget debajo del otro. Me gustaría que los widgets estuvieran alineados lado a lado hasta que lleguen al final de la línea, y luego deberían ajustarse a la siguiente línea, lo que significa que la QLabel's debería actuar como palabras en un documento de texto.

Aquí está mi código hasta ahora:

 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'\b\w+\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_()) 

Cuando corro esto me sale:

Imagen de mi ejemplo que no me gusta.

    Pero me gustaría que las palabras (las Qlabel's contienen las palabras) estuvieran una al lado de la otra, no debajo de la otra, de esta manera (photoshopped):

    introduzca la descripción de la imagen aquí

    He estado investigando mucho, pero ninguna respuesta me ayuda a alinear los widgets uno al lado del otro.

    Pensé que podría ser posible usar html en un widget QTextBrowser para esto, pero el motor de texto enriquecido de Qt no es compatible con la propiedad CSS de border-radius que sería necesaria para las tags de burbuja.

    Así que parece que necesitas un puerto PyQt del ejemplo de diseño de flujo . Esto puede “ajustar” una colección de widgets dentro de un contenedor, y también permite ajustar los márgenes y el espaciado horizontal / vertical.

    Aquí hay un script de demostración que implementa la clase FlowLayout y muestra cómo usarlo con su ejemplo:

     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_()) 

    Aquí hay una versión PyQt5 de la secuencia de comandos de demostración de diseño de flujo:

     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_())