Pysides6 columnSpan backwards from setting - python

I'm trying to create an app with a file browser that is 1/5 the width of a tab pane.
Why is the layout columnSpan backwards from what I set it?
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("My App")
layout = QGridLayout()
layout.addWidget(FileTree(), 0, 0, 1, 1)
layout.addWidget(Tabs(), 0, 1, 1, 5)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)

The row and column span refer to the grid cells of the layout, not the ratio of each row or column.
Setting a span means that the widget (or layout) will occupy that amount of cells, not that it will be 5 times bigger, as the size of each cell depends on the size (and hint) of its content. Since you have nothing else in the other 4 columns, those cells are actually empty.
It works exactly like a table: when you span a cell to make it occupy 2 columns, but the second column has no width, the cell will keep the same size.
Note that even if you had widgets in another row and in any of those columns, this would still not mean that the tab widget would occupy 5 times the space of the first, especially if those widgets were small: QTreeView inherits QAbstractItemView, which by default has an expanding size policy in both directions.
To achieve the wanted ratio, you must use setColumnStretch(), and not set any span unless you actually need it for widgets in other rows:
layout.addWidget(FileTree())
layout.addWidget(Tabs(), 0, 1)
layout.setColumnStretch(0, 1)
layout.setColumnStretch(1, 5)

Related

Tkinter Grid Spacing Issue

So I'm trying to create a basic text-entry thing to understand the basics of Tkinter, but when I try and grid something, if the column or row is greater than 1, it will act as if it is 1
Here is my code:
from tkinter import *
window = Tk()
window.minsize(width=500, height=300)
# label
my_label = Label(text="Text", font=("Consolas", 24, "bold"))
my_label["text"] = "New Text"
my_label.config(text="New Text")
# my_label.pack() # adding keyword parameters such as "expand=True" or "side='left'" can affect where it is positioned
# my_label.place(x=100, y=40)
my_label.grid(column=10, row=15)
# Button
def button_clicked():
my_label["text"] = input.get()
button = Button(text="Click Me", command=button_clicked)
button.grid(row=1, column=1)
# Entry
input = Entry(width=10)
window.mainloop()
Now I want the label to be about 3/4 of the way across the screen, and as you see, I use (for the label) column 10 and row 15. Still, when I try and run this, the label will only go diagonally down from the button. Is this how Tkinter is supposed to work, or am I just doing it wrong? I would appreciate any help.
yes, that's basically how tkinter works, it is at column 10, it's just that columns 2-9 all have zero width as they contain nothing, so you end up seeing only 2 columns.
to make something "centered" you need to have its "bounding box" scale with the containing "frame" using grid_columnconfigure from the Tk official grid tutorial
my_label.grid(column=10, row=15)
window.grid_columnconfigure(10,weight=1) # column 10 will grow
window.grid_rowconfigure(15,weight=1) # row 15 will grow
this allows the bounding box to grow and the label will remain centered as it is not set to stick to any corner, but it is offset by the button.
but if you really wanted it to be in the exact center then you can make the label span more columns than the button.
my_label.grid(column=1,columnspan=2, row=2) # label spans columns 1 and 2
window.grid_columnconfigure(2,weight=1) # column 2 will grow
window.grid_rowconfigure(2,weight=1) # row 2 will grow.
you get the required result.
but now we want both the button and the label to be centered, we can set them both in the same column that is set to grow with the size of the window, and make both rows grow to fit the entire window.
my_label.grid(column=1,columnspan=2, row=2, sticky="N")
window.grid_columnconfigure(1,weight=1)
window.grid_rowconfigure([1,2],weight=1)
simply playing with grid_columnconfigure and column and columnspan and sticky and their row counterparts is usually enough to get any shape you want on any screen scale if you add some padding.

PyQt QGridLayout() misbehaviour

I try to get a GUI application to work but unfortunately the spacing of the single canvas objects is not as expected. I want to have the following layout:
For the Grid Layout I use the following code:
# Adapt Widget StyleSheet
self.button_classification.setStyleSheet("font: bold 10pt Consolas; color: #009682") # #009682 is the KIT-Green
self.button_start.setStyleSheet('font: bold 10pt Consolas; color: #009682; border-style: outset; border-width: 2px; border-radius: 10px; border-color: #646873; padding: 6px')
self.layout4_image_widget.setStyleSheet('background-image: url(Python_logo.png);')
# Define Layouts
self.window = QWidget()
self.layout = QGridLayout()
# Add Widgets to layouts
self.layout.addWidget(self.canvas_left, 0, 0, 5, 5)
self.layout.addWidget(self.canvas_right, 0, 5, 5, 5)
self.layout.addWidget(self.canvas_right_buttom, 8, 7, 3, 3)
self.layout.addWidget(self.canvas_right_top, 5, 7, 3, 3)
self.layout.addWidget(self.canvas_middle_buttom, 8, 3, 3, 4)
self.layout.addWidget(self.canvas_middle_top, 5, 3, 3, 4)
self.layout.addWidget(self.layout4_image_widget, 5, 0, 3, 3)
self.layout.addWidget(self.button_start, 8, 0, 1, 3)
self.layout.addWidget(self.button_classification, 9, 0, 1, 3)
self.layout.addWidget(self.LCD_Number, 10, 0, 1, 3)
self.window.setLayout(self.layout)
But unfortunately what I receive as result is:
So the rows in the middle 5-7 are to wide.
Using column and row spans for widgets doesn't work as you seem to believe. Each "cell" in a grid layout might have different sizes, and it all depends on each widget sizeHint and sizePolicy.
All widget have a default size policy, which is a property that a widget uses to tell its container how it behaves when it is resized (should it expand, have a fixed height/width, can it shrink, etc).
By default, buttons and checkboxes have a fixed vertical policy, which means that they can never grow or shrink.
Every layout has a stretch value that can be assigned for each of its "slots". The stretch always defaults to 0, which leaves to each widget the possibility to grow or shrink to fit its needs, but when that value is set the available spaces are computed by the sum of each stretch in the layout and then divided equally. For example, if you have a simple layout with 2 widgets and a stretch=1 for the first and 2 for the second, the available space will be divided by 3 (1 + 2), with the first occupying a third of it and the second 2 thirds.
For grid layouts you can then use setRowStretch() and setColumnStretch() to achieve something like you proposed, but this would work only for widget that have compatible policies (preferred or expanding), and since in your case those buttons have fixed height it wouldn't be enough.
There are two possibilities, but in both cases I'll use a simpler layout, since there's no need to use a high count of rows and columns for the reasons explained above and setRowStretch/setColumnStretch can be used instead.
The layout uses just 4 columns:
+----+---+---+----+
| | |
+----+---+---+----+
| | | |
+----+---+---+----+
| | | |
+----+---+---+----+
The first row has two "cells" with a 2 column span, and the following rows have the 2 central slots united as a single cell with a similar span.
1. Manually set the policy for those widgets
In this case there are actually 5 rows, the first has a rowStretch=5, the second 3, and the remaining 1. By manually setting the vertical size policy to the widgets, they can grow to the height required by the stretch of the row they are occupying.
# first row, notice the spans
self.layout.addWidget(self.canvas_left, 0, 0, 1, 2)
self.layout.addWidget(self.canvas_right, 0, 2, 1, 2)
# second row, if the span is not specified defaults to (1, 1)
self.layout.addWidget(self.layout4_image_widget, 1, 0)
self.layout.addWidget(self.canvas_middle_top, 1, 1, 1, 2)
self.layout.addWidget(self.canvas_right_top, 1, 3)
# left widgets for third, fourth and fifth rows
self.layout.addWidget(self.button_start, 2, 0)
self.layout.addWidget(self.button_classification, 3, 0)
self.layout.addWidget(self.LCD_Number, 4, 0)
# remaining widgets, notice the vertical and horizontal spans
self.layout.addWidget(self.canvas_right_buttom, 2, 1, 3, 2)
self.layout.addWidget(self.canvas_middle_buttom, 2, 3, 3, 1)
# the column stretches set based on the grid given by you
self.layout.setColumnStretch(0, 3)
self.layout.setColumnStretch(1, 2)
self.layout.setColumnStretch(2, 2)
self.layout.setColumnStretch(3, 3)
# the row stretches, with the final 3 rows set to 1
self.layout.setRowStretch(0, 5)
self.layout.setRowStretch(1, 3)
self.layout.setRowStretch(2, 1)
self.layout.setRowStretch(3, 1)
self.layout.setRowStretch(4, 1)
# the default policy for both buttons and checkboxes is QSizePolicy.Minimum
# horizontally, and QSizePolicy.Fixed vertically, let's just change the latter
self.button_start.setSizePolicy(
QSizePolicy.Minimum, QSizePolicy.Preferred)
self.button_classification.setSizePolicy(
QSizePolicy.Minimum, QSizePolicy.Preferred)
Unfortunately this isn't perfect: as you can see the button might become too big and the checkbox has a lot of unnecessary margins that could be used by the LCD widget instead.
2. Use a container widget
In this case there only 3 rows, and an extra QWidget is used as container for the widgets on the bottom left.
self.layout.addWidget(self.canvas_left, 0, 0, 1, 2)
self.layout.addWidget(self.canvas_right, 0, 2, 1, 2)
self.layout.addWidget(self.layout4_image_widget, 1, 0)
self.layout.addWidget(self.canvas_middle_top, 1, 1, 1, 2)
self.layout.addWidget(self.canvas_right_top, 1, 3)
# create a container widget and add it to the main layout
self.container = QWidget()
self.layout.addWidget(self.container, 2, 0)
containerLayout = QVBoxLayout(self.container)
# add the widgets to the container
containerLayout.addWidget(self.button_start)
containerLayout.addWidget(self.button_classification)
containerLayout.addWidget(self.LCD_Number)
# the remaining widgets on the third row
self.layout.addWidget(self.canvas_right_buttom, 2, 1, 1, 2)
self.layout.addWidget(self.canvas_middle_buttom, 2, 3)
self.layout.setColumnStretch(0, 3)
self.layout.setColumnStretch(1, 2)
self.layout.setColumnStretch(2, 2)
self.layout.setColumnStretch(3, 3)
# there are only 3 rows, the second and third have the same row span
self.layout.setRowStretch(0, 5)
self.layout.setRowStretch(1, 3)
self.layout.setRowStretch(2, 3)
In this case the space usage is more optimized, leaving more space to those widgets that need it (the LCD).
https://doc.qt.io/qtforpython/PySide2/QtWidgets/QGridLayout.html#PySide2.QtWidgets.PySide2.QtWidgets.QGridLayout.setRowStretch
maybe there is something to do with this function... as they say the stretch factor has something to do with the row's relative space occupation and its default value is 0. You could maybe try setting them at 1 for each row.

How to add text (QLabel) outside (on top) grid layout?

I'm using Pyqt5 to build a very simple GUI.
In this window I want to place some text with info on top of a grid layout. The grid is made of 2 columns and I want the text to go full width (like HTML attribute colspan). I can't find a way to place the entirety of the text.
Text is: "Return a list of same-distance points couples from a file of 2D points"
I tried setting the Qlabel containing the text as the 1x1 element of the grid and give it a width of 2 columns, I tried place i manually with the move function; either of these solutions does not show the text properly.
class MatchDistance(QWidget):
def initUI(self):
super().initUI()
self.setWindowTitle('Match distance')
info_label = QLabel("Return a list of same-distance points couples from a file of 2D points", self)
info_label.move(10, 10)
# QPushButton and QLineEdit setup [...]
self.grid.addWidget(self.input_file_1, 1, 1)
self.grid.addWidget(self.output_file, 2, 1)
self.grid.addWidget(self.btn_input_1, 1, 2)
self.grid.addWidget(self.btn_output, 2, 2)
self.grid.addWidget(self.btn_run, 3, 2)
self.grid.addWidget(self.btn_mainwindow, 4, 2)
self.setWindowTitle("script#1: Match distance")
self.show()
Your description is confusing so I will not refer to your implementation, but to respond you must take into account the following:
The indices that set the row and column start at 0.
If you use the layouts then you can no longer use move since the position is handled by the layout.
Considering the above, the solution is:
self.grid.addWidget(info_label, 0, 0, 1, 2)
self.grid.addWidget(self.input_file_1, 1, 0)
self.grid.addWidget(self.output_file, 2, 0)
self.grid.addWidget(self.btn_input_1, 1, 1)
self.grid.addWidget(self.btn_output, 2, 1)
self.grid.addWidget(self.btn_run, 3, 1)
self.grid.addWidget(self.btn_mainwindow, 4, 1)

PyQtGraph Graphics Layout Widget issue

I'm trying to create a plot layout using PyQtGraph within a PyQt application.
I need a single row with two plots the first two columns wide and the second a single column wide.
Reading the docs I presumed something like this would work:
# Create the PyQtGraph Plot area
self.view = pg.GraphicsLayoutWidget()
self.w1 = self.view.addPlot(row=1, col=1, colspan=2, title = 'Data1')
self.w2 = self.view.addPlot(row=1, col=3, colspan=1, title = 'Data2')
But in this case I get two plot areas each taking 50% of the window width.
What am I doing wrong?
Best Regards,
Ben
colspan allows you to let a cell in the grid layout span over multiple columns. I a way it merges multiple grid cells. In your example you end up with a grid of 1 row by 3 columns. The first two columns apparently each have a width of 25% of the total (or one has 0% and the other 50%), and the third column takes the other 50%. In short: colspan does not allow you to control the width of the columns.
So, how to set the width of the columns or their contents? That was surprisingly hard to find. There seem to be no PyQtGraph methods that handle this directly, you must use the underlying Qt classes.
A pg.GraphicsLayoutWidget has as its central item a pg.GraphicsLayout. This in turn has a layout member that contains a Qt QGraphicsGridLayout. This allows you to manipulate the column widths with: setColumnFixedWidth, setColumnMaximimumWidth, setColumnStretchFactor, etc. Something like this may be what you need:
self.view = pg.GraphicsLayoutWidget()
self.w1 = self.view.addPlot(row=0, col=0, title = 'Data1')
self.w2 = self.view.addPlot(row=0, col=1, title = 'Data2')
qGraphicsGridLayout = self.view.ci.layout
qGraphicsGridLayout.setColumnStretchFactor(0, 2)
qGraphicsGridLayout.setColumnStretchFactor(1, 1)
Take a look in the documentation of QGraphicsGridLayout and experiment a bit.

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

I need to Solve two problems With my widget above.
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).
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.
1) Layouts
The other answer on here is very unclear and possibly off about how layout margins work. Its actually very straightforward.
Layouts have content margins
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)
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.
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
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

Categories

Resources