PyQt5 QGridLayout sizing incorrect - python

I'm having some issues with QGridLayout in pyqt5. I'm trying to make a GUI that has a stack of buttons on one side, a table on the other side, and a plot that occupies the entire bottom of the window. This is the first program I've ever made, so I might have more issues than I know.
I've arranged the buttons within a QTableWidget, and the main QTableWidget contains several fields where users can enter data. I'd like the data entry table to be larger in size than the button table, but resizing it as in this answer doesn't seem to do anything. The button table is larger no matter the columnSpan entry I put in. What am I doing wrong?
Here are the relevant bits of code:
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setGeometry(50, 50, 700, 1000)
self.home()
def home(self):
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.button_table = QTableWidget(self)
self.layer_add = QPushButton("Add layer", self)
self.plotter = QPushButton("plot transmission", self)
self.layer_table = QTableWidget(self)
self.graphWidget = pg.PlotWidget(self)
self.grid = QGridLayout()
self.grid.setSpacing(10)
self.grid.addWidget(self.button_table, 0, 0, 1, 1)
self.grid.addWidget(self.layer_table, 0, 1, 1, 3)
self.grid.addWidget(self.graphWidget, 1, 0, 1, 4)
self.centralWidget().setLayout(self.grid)
self.show()
I doodled in what I'd ideally like to have happen... here's a picture of what it looks like with the above code
and in red what I'd like to have happen.
Edit: I don't understand why, if I set the QGridLayout columnSpan to be 1 for the table on the left and 3 for the table on the right, the left-hand table is still significantly wider. I am open to either learning how to fix that, understanding how to make the left-hand table auto-shrink to the size of the buttons within it, or an alternative layout suggestion. Thanks for any help!

While you answered your own question, it seems that you changed the behavior by removing the first table (moreover, changing the resize mode of the first column stretch doesn't have much to do with your issue). So I'm answering to your [edited] question, even if it's missing the part in which you added the buttons to the first table.
The main problem was that you were setting a column span too big for the second table:
self.grid.addWidget(self.button_table, 0, 0, 1, 1)
self.grid.addWidget(self.layer_table, 0, 1, 1, 3) # <- 3 columns!
self.grid.addWidget(self.graphWidget, 1, 0, 1, 4)
In the code above, you're telling the layout that the layer_table will have a column span of 3 columns. Even if you are not actually using three columns, by doing this the layout thinks that the second table will (probably) occupy more space than the first.
Normally, a QGridLayout will use the columnStretch property for that, but since by default the stretch is 0 for all columns and rows, it will use the span as a reference.
In fact, using the following:
self.grid.addWidget(self.button_table, 0, 0, 1, 1)
self.grid.addWidget(self.layer_table, 0, 1, 1, 1) # <- 1 column!
self.grid.addWidget(self.graphWidget, 1, 0, 1, 2)
is the same as this:
self.grid.addWidget(self.button_table, 0, 0, 1, 1)
self.grid.addWidget(self.layer_table, 0, 1, 1, 3) # <- 3 columns!
self.grid.addWidget(self.graphWidget, 1, 0, 1, 4)
self.grid.setColumnStretch(0, 1)
self.grid.setColumnStretch(1, 1)
In the first case, the column span is 1 (one widget, one column), and, since the two widgets are of the same type, they will use half of the available horizontal space. In the second, the column span of the right table is 3 (as in your code), but the stretch is 1 for both the first and second column, and 0 for the third and fourth, meaning that a widget that occupies the second, third and fourth column will have the same available space than a widget that occupies the first, thus obtaining the horizontal space equally divided between those two widgets.
col1 | col2 | col3 | col4
1 | 1 | 0 | 0
Since the second table occupies columns 2 to 4, it will have a stretch of 1 (1 + 0 + 0). Stretches are used by layouts to equally divide the space between widgets (considering their size hints, their minimum size hints, or their minimum/maximum size whenever they're set): the stretches are summed integer values, and then the layout uses the proportions between the sum and those values to resize widgets.
To ensure that the first table uses only the minimum space required to show its contents, you need to do the following:
set the sizeAdjustPolicy (which is a property of every QAbstractScrollArea descendant, including every item view) to AdjustToContents; this will make the table "tell" the layout that its size hint is based on its minimum contents;
set the resize mode of the horizontal header to adjust all of its sections (as in columns) to their contents;
set the horizontal size policy of the table to Maximum; the term "maximum" might be counterintuitive, but it means that the widget's size cannot be larger than its size hint; still, it could be shrunk if any other widget requires space (but not less than the minimumSizeHint) so, alternatively, you could use Fixed (meaning that it cannot even shrink), but it's usually better to allow widgets to be shrunk anyway if the layout is too crowded and the user requires to make the window smaller than it is;
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setGeometry(50, 50, 700, 1000)
self.home()
def home(self):
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.button_table = QTableWidget(self)
self.button_table.setRowCount(3)
self.button_table.setColumnCount(1)
# set the sizeHint of the table view (actually, its ancestor class,
# QAbstractScrollArea) to the minimum size required to show its contents
self.button_table.setSizeAdjustPolicy(self.button_table.AdjustToContents)
# set all sections of the horizontal headers to adjust themselves to
# their contents
self.button_table.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeToContents)
# get the current sizePolicy and set it to Maximum, meaning that it will
# use its sizeHint as "maximum": it can expand, but there's no need for
# that, so if any other sibling widget requires more space, it can use it
policy = self.button_table.sizePolicy()
policy.setHorizontalPolicy(policy.Maximum)
# apply the changed policy
self.button_table.setSizePolicy(policy)
self.layer_add = QPushButton("Add layer", self)
self.plotter = QPushButton("plot transmission", self)
# I restored the following lines, which were missing in your edit
self.button_table.setCellWidget(0, 0, self.layer_add)
self.button_table.setCellWidget(1, 0, self.plotter)
self.layer_table = QTableWidget(self)
self.graphWidget = pg.PlotWidget(self)
self.grid = QGridLayout()
self.grid.setSpacing(10)
self.grid.addWidget(self.button_table, 0, 0, 1, 1)
self.grid.addWidget(self.layer_table, 0, 1, 1, 1)
self.grid.addWidget(self.graphWidget, 1, 0, 1, 2)
self.centralWidget().setLayout(self.grid)
As you can see, now the left table only uses the minimum required width, based on the horizontal header width (plus the vertical header width), which in turn is based on the sum of the maximum width of each column.

Ahh turns out that using setColumnStretch on column 1 fixed this problem. I also changed the left table to a QVBoxLayout and put it in using QGridLayout.addLayout, so everything looks better now
I still don't quite understand why the two tables were unequal widths on QGridLayout though, regardless of the number of columns selected.

Related

QFontMetrics leave extra space between lines

I'm trying to draw multiple paragraphs of text with PySide6's QPainter and QFontMetrics. I want to draw them with the same spacing as they would have if I drew them all in a single block of text, but the line spacing isn't quite right.
In the following example, the font metrics say that the font's line spacing is 17. When I measure a single line of text, the bounding rectangle is indeed 17 pixels high. However, when I measure two lines of text, the bounding rectangle is 35 pixels high, not 34. Where does the extra pixel come from, and can I see it on some property of the font or the font metrics?
from PySide6.QtGui import QFont, QFontMetrics
from PySide6.QtWidgets import QApplication
app = QApplication()
font = QFont()
metrics = QFontMetrics(font)
print(metrics.lineSpacing()) # 17
print(metrics.boundingRect(0, 0, 100, 100, 0, 'A').height()) # 17
print(metrics.boundingRect(0, 0, 100, 100, 0, 'A\nB').height()) # 35 != 17 * 2
print(metrics.leading()) # 0
print(metrics.ascent()) # 14
print(metrics.descent()) # 3
By the way, it isn't always one extra pixel. If I make the font bigger, the extra space increases.
Update
I thought I had figured this out with musicamante's suggestion of switching from QFontMetrics to QFontMetricsF, but there's still a difference.
from PySide6.QtCore import QRectF
from PySide6.QtGui import QFont, QFontMetricsF
from PySide6.QtWidgets import QApplication
app = QApplication()
font = QFont()
metrics = QFontMetricsF(font)
print(metrics.height()) # 16.8125
print(metrics.boundingRect(QRectF(0, 0, 100, 100),
0,
'A').getCoords()) # (0.0, 0.0, 9.9375, 16.8125)
print(metrics.boundingRect(QRectF(0, 0, 100, 100),
0,
'A\nB').getCoords()) # (0.0, 0.0, 9.9375, 34.8125)
# Note the height of that rect doesn't match the next calculation.
print(metrics.height() + metrics.lineSpacing()) # 34.046875
# I can't see any combination of these numbers that makes 34.8125
print(metrics.lineSpacing()) # 17.234375
print(metrics.leading()) # 0.421875
print(metrics.ascent()) # 13.984375
print(metrics.descent()) # 2.828125
First of all, the height of a bounding rect of a font metrics doesn't depend on the characters used, but on the font specifications.
Two lines of text don't have the double of the height() of the bounding rect of a single line: instead, you have to consider the lineSpacing().
In practice, the height of a bounding rect is normally the sum of:
the height() multiplied the number of lines;
the leading() multiplied by the number of spaces between the lines (aka: number of lines - 1);
Or, similarly, the sum of:
the ascent();
the lineSpacing() multiplied by the number of spaces between lines;
the descent();
Note that, obviously, the number of lines depends on the input text and the given options, for instance, if word wrapping was enabled and any of the source lines didn't fit the given source rectangle.
Also consider that most fonts are vectorial, meaning that their coordinates and metrics are proportional and in floating point values. QFontMetrics, instead, works with integer values for simplicity and optimization reasons, so you might get inconsistent results caused by rounding in cases for which the point size doesn't give rounded values: non integer numbers are generally "floored" (like int() in python).
In your case, the leading is probably more than 0 (but still less than 1), so you don't get a proper sum of the aforementioned heights.
Specifically, QFontMetrics.boundingRect() returns a QRect resulting by the QRectF.toAlignedRect() of the computed formatted text, which is always "the smallest possible integer rectangle that completely contains this rectangle".
If you need to get precise coordinates, you need to use QFontMetricsF, which is the floating point counterpart of the default basic QFontMetrics.
That said, if you plan on drawing formatted text with QPainter, then consider using QTextDocument or, at least, QTextLayout, which is consistent with the standard Qt text drawing and is generally faster, more reliable and "simpler" (well, once you get to know it). While it might seem a bit too complex than required, it's actually what Qt does when calling boundingRect(), so if you need custom painting, the QTextLayout option is actually better, especially if you can combine it with some smart caching (see QPicture) to avoid the common python bottleneck.

wx.grid.Grid using too much space within wx.BoxSizer

I'm trying to display a table of items in wxpython using a wx.grid.Grid() inside a wx.BoxSizer. The table is large but I didn't want it to take up all the screen space so I put it in the boxsizer as shown in the code below.
def setOutputPanel(self):
self.outputBox = wx.grid.Grid(parent=self.pnl)
self.outputBox.CreateGrid(100, 100)
self.outputBox.DisableCellEditControl()
self.outputPanel.Add(self.outputBox, 1, wx.EXPAND)
self.graphControl = wx.Button(parent=self.pnl, label="close")
self.outputPanel.Add(self.graphControl, 0, wx.ALIGN_RIGHT)
self.outputText = wx.TextCtrl(parent=self.pnl, value="text control", style=wx.EXPAND)
self.outputPanel.Add(self.outputText, 1, wx.EXPAND)
The problem is that when I put the size for the outputText to 1
self.outputPanel.Add(self.outputText, 1, wx.EXPAND)
It doesn't show at all
link to TextCtrl#size1
I can get it to show if I set the size to 0.
self.outputPanel.Add(self.outputText, 0, wx.EXPAND)
But, it's far too small. I'd like it to be the same height at the table. link to TextCtrl#size0
So, after reading some more of the wxPython documentation I have found the solution.
If you go to this page, where they discuss some of the wxPython sizer features (the super class for wx.BoxSizer). On the page it says "Normally wx.Sizers will use wx.Window.GetEffectiveMinSize to determine what the minimal size of window items should be, and will use that size to calculate the layout." so I added the wx.FIXED_MINSIZE and now the grid doesn't have a minimum size.
This might seem like a bad idea as the grid can now shrink to a size that's too small. But, I've set the frame min size pretty large and the grid uses half the space, so I don't think It's going to be an issue.

Issues with position manipulation using QGridLayout addWidget(QWidget,x,y)

This is a really simple question I'm just new to PyQt5 and am a bit confused on how QGridLayout works...
def init_main_page(self):
layout = QGridLayout()
b1 = buttons.QPushButton("0",self.main_page)
b2 = buttons.QPushButton("1",self.main_page)
b3 = buttons.QPushButton("2",self.main_page)
layout.addWidget(b1,0,0)
layout.addWidget(b2,5,0)
layout.addWidget(b3,1,0)
self.main_page.setLayout(layout)
The problem I am having is that no matter how high I make the x and y arguments in addwidget(QWidget,x,y), it b1 b2 and b3 always remain equidistant from each other. I'm trying figure out how to manipulate the position of the buttons whilst maintaining a proportional setup (so avoiding QPushButton.move()) and from what I've seen, QGridLayout is the best way to do this.
Setting the coordinates of widgets in a grid layout by "skipping" rows or columns is almost useless, as those are grid coordinates that only tell the layout manager in which "slots" the widgets will be: since there is nothing in the rows 2 to 4, that space will not be used.
To achieve what you want you need to set stretch factors and, possibly, use minimum heights for the specified rows.
layout.addWidget(b1, 0, 0)
layout.addWidget(b3, 1, 0)
layout.addWidget(b2, 2, 0)
layout.setRowStretch(2, 1)
layout.setRowMinimumHeight(2, 50)
But this might not be what you want, since it will place the third button in the vertical center of the row grid (leaving empty space at the bottom).
To avoid that there are two possible solutions:
add the widget by setting the alignment to the bottom:
layout.addWidget(b2, 2, 0, alignment=QtCore.Qt.AlignBottom)
add the last widget to the fourth row, and set stretch and minimum height for the third empty row.
layout.addWidget(b2, 3, 0)
layout.setRowStretch(2, 1)
layout.setRowMinimumHeight(2, 50)

Tkinter: Extend labels to the edge of the column

I want to set up a three-column table with Python and Tkinter. For this purpose, I implement each cell both as a canvas and as a label inside the canvas because I need the tags option of the canvas widget.
I want to spread each cell over the whole width of the column from left to right (so that click events can be recorded not only on the text but on the whole row). However, the following code does not do this:
The text is centered but the labels and canvases only include the text, not the whole width of the cell. I tried adding
sticky = Tkinter.N+Tkinter.S+Tkinter.E+Tkinter.W
on the labels and/or the canvases; however, with some combinations, the text is aligned left and each label and canvas only includes the text instead of the whole cell, with others the sticky attribute the text is again centered but the labels and canvases are still narrow.
This is my code:
import Tkinter
class Application(Tkinter.Frame):
def __init__(self, master=None):
Tkinter.Frame.__init__(self, master)
self.master.geometry("800x600")
self.grid()
list = [[1, 2, 3], [4, 5, 6], [7777, 8888, 9999]]
self.cells_canvas = []
self.cells_label = []
i = 0
for entry in list:
self.cells_canvas.append([Tkinter.Canvas(self), Tkinter.Canvas(self), Tkinter.Canvas(self)])
self.cells_label.append([None, None, None])
self.cells_label[i][0] = Tkinter.Label(self.cells_canvas[i][0])
self.cells_label[i][0]["text"] = entry[0]
self.cells_label[i][0].config(bg = "#A00")
self.cells_label[i][0].grid(row = 0, column = 0, columnspan = 1) # reference A
self.cells_canvas[i][0].grid(row = i, column = 0, columnspan = 1) # reference B
self.cells_label[i][1] = Tkinter.Label(self.cells_canvas[i][1])
self.cells_label[i][1]["text"] = entry[1]
self.cells_label[i][1].grid(row = 0, column = 0, columnspan = 1)
self.cells_canvas[i][1].grid(row = i, column = 1, columnspan = 1)
self.cells_label[i][2] = Tkinter.Label(self.cells_canvas[i][2])
self.cells_label[i][2]["text"] = entry[2]
self.cells_label[i][2].grid(row = 0, column = 0, columnspan = 1)
self.cells_canvas[i][2].grid(row = i, column = 2, columnspan = 1)
i = i+1
root = Tkinter.Tk()
app = Application()
app.mainloop()
With this code, only the text in column 0 but not the whole columns appears with red background.
With adding sticky = Tkinter.N+Tkinter.S+Tkinter.E+Tkinter.W at reference A nothing changes.
With adding that at code B, the text appears aligned left. Again, only the text has red beackground.
With that code at references A and B, the same.
When using grid, the sticky option is indeed how you get a label to fill its cell. The label widget has an anchor option which controls where the text appears within the label.
In addition, you have to make sure that in a given parent, the column(s) expand to fill any extra space in the parent. In your case, column 0 in the canvas is only as wide as its contents, so it doesn't fill the full width of the canvas.
You need to give column 0 in each canvas a positive weight so the column will expand to fill the space given to it.
self.cells_canvas[i][0].columnconfigure(0, weight=1)
self.cells_canvas[i][1].columnconfigure(1, weight=1)
self.cells_canvas[i][2].columnconfigure(2, weight=1)
You'll probably want to do a similar thing with each row. A very good rule of thumb when using a canvas is to always make sure at least one row and at least one column have a positive weight.
If you only have one widget in each canvas, pack is probably the better choice since you don't have to worry about color and rows.

Can't change _NET_WM_STRUT_PARTIAL property

I want to reserve some space on the screen for my Gtk application written in Python. I've wrote this function:
import xcb, xcb.xproto
import struct
def reserve_space(xid, data):
connection = xcb.connect()
atom_cookie = connection.core.InternAtom(True, len("_NET_WM_STRUT_PARTIAL"),
"_NET_WM_STRUT_PARTIAL")
type_cookie = connection.core.InternAtom(True, len("CARDINAL"), "CARDINAL")
atom = atom_cookie.reply().atom
atom_type = type_cookie.reply().atom
data_p = struct.pack("I I I I I I I I I I I I", *data)
strat_cookie = connection.core.ChangeProperty(xcb.xproto.PropMode.Replace, xid,
atom, xcb.xproto.Atom.CARDINAL, 32, len(data_p), data_p)
connection.flush()
It's call looks like this:
utils.reserve_space(xid, [0, 60, 0, 0, 0, 0, 24, 767, 0, 0, 0, 0])
Unfortunately, it doesn't work. Where is an error in my code?
UPD:
Here is my xprop output. My WM is Compiz.
I have uploaded a gist that demonstrates how to specify a strut across the top of the current monitor for what might be a task-bar. It may help explain some of this.
The gist of my gist is below:
window = gtk.Window()
window.show_all()
topw = window.get_toplevel().window
topw.property_change("_NET_WM_STRUT","CARDINAL",32,gtk.gdk.PROP_MODE_REPLACE,
[0, 0, bar_size, 0])
topw.property_change("_NET_WM_STRUT_PARTIAL","CARDINAL",32,gtk.gdk.PROP_MODE_REPLACE,
[0, 0, bar_size, 0, 0, 0, 0, 0, x, x+width, 0, 0])
I found the strut arguments confusing at first, so here is an explanation that I hope is clearer:
we set _NET_WM_STRUT, the older mechanism as well as _NET_WM_STRUT_PARTIAL but window managers ignore the former if they support the latter. The numbers in the array are as follows:
0, 0, bar_size, 0 are the number of pixels to reserve along each edge of the screen given in the order left, right, top, bottom. Here the size of the bar is reserved at the top of the screen and the other edges are left alone.
_NET_WM_STRUT_PARTIAL also supplies a further four pairs, each being a start and end position for the strut (they don't need to occupy the entire edge).
In the example, we set the top start to the current monitor's x co-ordinate and the top-end to the same value plus that monitor's width. The net result is that space is reserved only on the current monitor.
Note that co-ordinates are specified relative to the screen (i.e. all monitors together).
(see the referenced gist for the full context)
Changing to using ChangePropertyChecked(), and then checking the result gives a BadLength exception.
I think the bug here is that the ChangeProperty() parameter data_len is the number of elements of the size given by format , not the number of bytes, in the property data data.
Slightly modified code which works for me:
def reserve_space(xid, data):
connection = xcb.connect()
atom_cookie = connection.core.InternAtom(False, len("_NET_WM_STRUT_PARTIAL"),
"_NET_WM_STRUT_PARTIAL")
atom = atom_cookie.reply().atom
data_p = struct.pack("12I", *data)
strat_cookie = connection.core.ChangePropertyChecked(xcb.xproto.PropMode.Replace, xid,
atom, xcb.xproto.Atom.CARDINAL, 32, len(data_p)/4, data_p)
strat_cookie.check()
connection.flush()

Categories