Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PySide Qt: Auto vertical growth for TextEdit Widget, and spacing between widgets in a vertical layout

enter image description here

I need to Solve two problems With my widget above.

  1. I'd like to be able to define the amount of space put between the post widgets shown in the image (they look fine as is, but I wanna know it's done).
  2. I'd like to grow the text edits vertically based on the amount of text they contain without growing horizontally.

For 1 the code that populates the widgets is as follows :

self._body_frame = QWidget()
self._body_frame.setMinimumWidth(750)
self._body_layout = QVBoxLayout()
self._body_layout.setSpacing(0)
self._post_widgets = []
for i in range(self._posts_per_page):
    pw = PostWidget()
    self._post_widgets.append(pw)
    self._body_layout.addWidget(pw)

    self._body_frame.setLayout(self._body_layout)

SetSpacing(0) doesn't bring things any closer, however SetSpacing(100) does increase it.

edit

(for Question 2) I haven't mentioned this, but I want the parent widget to have a vertical scrollbar.

I have answered my own question, but its wordy, and cause and affect based. A proper well written tutorial style answer to address both points gets the bounty :D

edit 2

Using my own answer below I have solved the problem. I'll be accepting my own answer now.

enter image description here

like image 967
Hassan Syed Avatar asked Jul 26 '12 20:07

Hassan Syed


2 Answers

1) Layouts

The other answer on here is very unclear and possibly off about how layout margins work. Its actually very straightforward.

  1. Layouts have content margins
  2. Widgets have content margins

Both of these define a padding around what they contain. A margin setting of 2 on a layout means 2 pixels of padding on all sides. If you have parent-child widgets and layouts, which is always the case when you compose your UI, each object can specific margins which take effect individually. That is... a parent layout specifying a margin of 2, with a child layout specifying a margin of 2, will effectively have 4 pixels of margin being displayed (obviously with some frame drawing in between if the widget has a frame.

A simple layout example illustrates this:

w = QtGui.QWidget()
w.resize(600,400)

layout = QtGui.QVBoxLayout(w)
layout.setMargin(10)
frame = QtGui.QFrame()
frame.setFrameShape(frame.Box)
layout.addWidget(frame)

layout2 = QtGui.QVBoxLayout(frame)
layout2.setMargin(20)
frame2 = QtGui.QFrame()
frame2.setFrameShape(frame2.Box)
layout2.addWidget(frame2)

Layout Image Example

You can see that the top level margin is 10 on each side, and the child layout is 20 on each side. Nothing really complicated in terms of math.

Margin can also be specified on a per-side basis:

# left: 20, top: 0, right: 20, bottom: 0
layout.setContentsMargins(20,0,20,0)

There is also the option of setting spacing on a layout. Spacing is the pixel amount that is placed between each child of the layout. Setting it to 0 means they are right up against each other. Spacing is a feature of the layout, while margin is a feature of the entire object. A layout can have margin around it, and also spacing between its children. And, the children of the widget can have their own margins which are part of their individual displays.

layout.setSpacing(10) # 10 pixels between each layout item

2) Auto-Resizing QTextEdit

Now for the second part of your question. There are a few ways to create a auto-resizing QTextEdit I am sure. But one way to approach it is to watch for content changes in the document, and then adjust the widget based on the document height:

class Window(QtGui.QDialog):

    def __init__(self):
        super(Window, self).__init__()
        self.resize(600,400)

        self.mainLayout = QtGui.QVBoxLayout(self)
        self.mainLayout.setMargin(10)

        self.scroll = QtGui.QScrollArea()
        self.scroll.setWidgetResizable(True)
        self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
        self.mainLayout.addWidget(self.scroll)

        scrollContents = QtGui.QWidget()
        self.scroll.setWidget(scrollContents)

        self.textLayout = QtGui.QVBoxLayout(scrollContents)
        self.textLayout.setMargin(10)

        for _ in xrange(5):
            text = GrowingTextEdit()
            text.setMinimumHeight(50)
            self.textLayout.addWidget(text)


class GrowingTextEdit(QtGui.QTextEdit):

    def __init__(self, *args, **kwargs):
        super(GrowingTextEdit, self).__init__(*args, **kwargs)  
        self.document().contentsChanged.connect(self.sizeChange)

        self.heightMin = 0
        self.heightMax = 65000

    def sizeChange(self):
        docHeight = self.document().size().height()
        if self.heightMin <= docHeight <= self.heightMax:
            self.setMinimumHeight(docHeight)

I subclassed QTextEdit -> GrowingTextEdit, and connected the signal emitted from its document to a slot sizeChange that checks the document height. I also included a heightMin and heightMax attribute to let you specify how large or small its allowed to autogrow. If you try this out, you will see that as you type into the box, the widget will start to resize itself, and also shrink back when you remove lines. You can also turn off the scrollbars if you want. Right now each text edit has its own bars, in addition to the parent scroll area. Also, I think you could add a small pad value to the docHeight so that it expands just enough to not show scrollbars for the content.

This approach is not really low level. It uses the commonly exposed signals and child members of the widget for you to receive notifications of state changes. Its pretty common to make use of the signals for extending functionality of existing widgets.

Auto-Growing widget example picture

like image 122
jdi Avatar answered Oct 15 '22 13:10

jdi


To Address Question 1:

Parent Widgets and Layouts both have margins, in addition to the spacing parameter of the layout itself. From some cause and affect testing It is apprent that margins apply both to the outer region of a parent as well as an internal region.

So, for example if a 2 pixel margin is specified to a parent the vertical border has <--2 pixel | 2 pixel --> margin in addition to the margins of the layout (A HBoxLayout in this case). If the layout has a 2 pixel margin as well the area around horizontal line would look like:

<-- 2 pixel | 2 pixel --> <-- 2 pixel (L) 2 pixel--> (W)

edit Perhaps its more like this: | 2 pixel --> (L) 2 pixel --> <-- 2 pixel (W)

Where | is the vertical line of the parent (L) is the vertical line of the Layout and (W) is the border of the embedded widget in the horizontal layout.

The spacing of the layout is an additional parameter that controls how much space is inserted between widgets of the layout in addition to any layout margins.

The description above might not be accurate( so feel free to edit it where it is inaccurate), but setting the margins of the parent and the layout to zero as well as the layouts spacing to zero produces the result you are after.

For point 2:

I do not think there is a straight forward way to address this issue (you probably have to resort to hooking in at a lower level, which requires a deeper understanding of the API). I think you should use the QLabel Widget instead of the QTextEdit widget. Labels do not have a view and thus expand as needed, at least thats how they work by default, as long as the parent isn't constrained in it's movement.

So, change the QTextEdit to Qlabel and add a scrolling view to the parent and everything should work as you want. I have a feeling you chose QTextEdit because of it's background. Research the way HTML works in QT widgets and you might be able to alter the background via HTML.

edit

enter image description here

This widget (excuse the size) is created by the following code on OS X with PyQT:

import sys
from PyQt4 import Qt

class PostMeta(Qt.QWidget):
    posted_at_base_text = "<b> Posted At:</b>"
    posted_by_base_text = "<b> Posted By:</b>"

    def __init__(self):
        Qt.QWidget.__init__(self)
        self._posted_by_label = Qt.QLabel()
        self._posted_at_label = Qt.QLabel()
        layout = Qt.QVBoxLayout()
        layout.setMargin(0)
        layout.setSpacing(5)
        layout.addWidget(self._posted_by_label)
        layout.addWidget(self._posted_at_label)
        layout.addStretch()
        self.setLayout(layout)
        self._posted_by_label.setText(PostMeta.posted_by_base_text)
        self._posted_at_label.setText(PostMeta.posted_at_base_text)


class FramePost(Qt.QFrame):
    def __init__(self):
        Qt.QFrame.__init__(self)
        layout = Qt.QHBoxLayout()
        layout.setMargin(10)
        self.te = Qt.QLabel()
        self.te.setStyleSheet("QLabel { background : rgb(245,245,245) }")
        self.te.setFrameStyle( Qt.QFrame.Panel |  Qt.QFrame.Sunken)
        self.te.setLineWidth(1)
        self._post_meta = PostMeta()
        layout.addWidget(self._post_meta)
        vline = Qt.QFrame()
        vline.setFrameShape(Qt.QFrame.VLine)
        layout.addWidget(vline)
        layout.addWidget(self.te)
        self.te.setText(
            """            line one
            line two
            line three
            line four
            line five
            line six
            line seven
            line eight
            line nine
            line ten
            line eleven
            line twelve
            line thirteen""")
        self.setLayout(layout)

        self.setFrameStyle(Qt.QFrame.Box)
        self.setLineWidth(2)

app = Qt.QApplication(sys.argv)
w = Qt.QWidget()
layout = Qt.QHBoxLayout()
fp = FramePost()
layout.addWidget(fp)
w.setLayout(layout)
w.show()
app.exec_()

The labels in the left widget show the spacer and margin tweaking done, and I've used a QLabel for the post text. Notice I've tweaked the label to look a bit more like a default QTextEdit

like image 37
Hassan Syed Avatar answered Oct 15 '22 14:10

Hassan Syed