I am having an issue with QGridLayout using PyQt5. In my UI, I have two pyqtgraph PlotWidget(). One of them contains 5 different plots and the other one has only one. I want the PlotWidget() with 5 plots to be 5 times that of the other one. The following code configures everything for this plot size proportion:
grid_layout.addLayout(vlayout_signals, 0, 0, 2, 1)
grid_layout.addWidget(self.p1, 0, 1, 5, 1) # self.p1 has 5 plots in it.
grid_layout.addWidget(self.p2, 5, 1, 1, 1) # self.p2 has only 1 plot in it.
The problem is python does exactly the opposite. The widget with one plot becomes 5 times bigger. I don't know why. I wanted self.p1 to be at (row=0, col=1) and occupy 5 rows and 1 column, and self.p2 to be at (row=5, col=1) and only occupy 1 row and 1 column. For some reason, Python is not doing it (I'm using python 3.11).
Here is a minimal working example:
NFRAME = 200 * 6
import sys
import numpy as np
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QGridLayout, QComboBox, QSlider
from PyQt5.QtGui import QColor
import pyqtgraph as pg
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
grid_layout = QGridLayout()
vlayout = QVBoxLayout()
self.p1 = pg.PlotWidget()
self.p2 = pg.PlotWidget()
self.p1.plotItem.addLegend()
self.p2.plotItem.addLegend()
x = np.linspace(0, 2 * np.pi * 2, NFRAME)
y1 = np.sin(2 * np.pi * 1 * x)
y2 = 10 * np.sin(2 * np.pi * 2 * x) + 50
y3 = 20 * np.sin(2 * np.pi * 3 * x) + 200
y4 = 30 * np.sin(2 * np.pi * 4 * x) + 300
y5 = 40 * np.sin(2 * np.pi * 5 * x) + 400
self.ph1_y1 = self.p1.plot(x, y1, pen=pg.mkPen(QColor(255, 0, 0)), name="y1")
self.ph1_y2 = self.p1.plot(x, y2, pen=pg.mkPen(QColor(255, 255, 0)), name="y2")
self.ph1_y3 = self.p1.plot(x, y3, pen=pg.mkPen(QColor(0, 255, 255)), name="y3")
self.ph1_y4 = self.p1.plot(x, y4, pen=pg.mkPen(QColor(255, 0, 255)), name="y4")
self.ph1_y5 = self.p1.plot(x, y5, pen=pg.mkPen(QColor(102, 102, 255)), name="y5")
self.ph2_y1 = self.p2.plot(x, 0 * x + 6)
signal_select = QComboBox(self)
signal_select.addItem("Item1")
signal_select.addItem("Item2")
signal_offset = QSlider(Qt.Orientation.Vertical)
signal_gain = QSlider(Qt.Orientation.Vertical)
hlayout_sliders = QHBoxLayout()
hlayout_sliders.addWidget(signal_offset)
hlayout_sliders.addWidget(signal_gain)
hlayout_sliders_labels = QHBoxLayout()
signal_offset_label = QLabel()
signal_gain_label = QLabel()
signal_offset_label.setText("Offset")
signal_offset_label.setAlignment(Qt.AlignCenter)
signal_gain_label.setText("Gain")
signal_gain_label.setAlignment(Qt.AlignCenter)
hlayout_sliders_labels.addWidget(signal_offset_label)
hlayout_sliders_labels.addWidget(signal_gain_label)
arduino_signal_reset = QPushButton("Reset")
vlayout_signals = QVBoxLayout()
vlayout_signals.addWidget(signal_select)
vlayout_signals.addLayout(hlayout_sliders)
vlayout_signals.addLayout(hlayout_sliders_labels)
vlayout_signals.addWidget(arduino_signal_reset)
grid_layout.addLayout(vlayout_signals, 0, 0, 2, 1)
grid_layout.addWidget(self.p1, 0, 1, 5, 1)
grid_layout.addWidget(self.p2, 5, 1, 1, 1)
widget = QWidget()
widget.setLayout(grid_layout)
vlayout.addWidget(widget)
self.setLayout(grid_layout)
self.setCentralWidget(widget)
self.setWindowTitle('My softwaree')
if __name__ == '__main__':
app = QApplication(sys.argv)
windowExample = MainWindow()
windowExample.show()
sys.exit(app.exec_())
TL;DR
Don't use spans to set the proportions between widgets, instead use setRowStretch(), or, better, nested layouts.
Explanation
A common misconception about QGridLayout is that using "gaps" in rows and columns would result in placing them at different positions, and, similarly, using bigger row/column spans would result in a bigger size available for the widget.
That is not how the grid layout works, since its rows and columns have no specific size, nor they are directly considered for size ratios.
...At least, in theory.
What happens in your case is related to the complex way both QGridLayout and PlotWidget behave and interact.
When a layout-managed widget is shown for the first time (and whenever it's resized), it queries its layout, which in turn makes lots of internal computations for the items it manages, including considering their size hints, constraints and policies.
Note that by "items" I mean widgets, spacers and even inner layouts (see QLayoutItem).
QGridLayout is quite complex, and its behavior is not always intuitive, especially when dealing with complex widgets and layouts.
Items in a grid layout are placed in "cells", and when computing geometries for those cells (and the items they occupy) the layout engine does, more or less, the following:
query each layout item (widget, spacer or nested layout) about their size hints, constraints and policies;
correlate those items with the cells they occupy;
compute each item final geometry, which is based on the item x and y, and width and height of the last row/column they occupy;
add the last row/height coordinates to the width/height of the item, minus the initial x/y;
compute the internal required geometry of the item, considering the contents of the area occupied by the item;
set the geometry of the items, and eventually do everything again in case those items react to their resizing by updating their hints (which happens for "size reactive" widgets like QLabel or those that implement heightForWidth());
While this generally works, it has an important drawback: if the chosen span doesn't include other items in the same rows or columns, the resulting widths and heights are possibly "nullified" (meaning that their possible size is the minimum), especially whenever any widget has an Expanding size policy, which is the case of PlotWidget, which inherits from QAbstractScrollArea (since it's based on QGraphicsView), that by default always tries to expand its contents whenever there is space left from other widgets that don't expand theirselves.
Note that things are actually even more complex, as other widgets that by default have Expanding size policies might not give the same result (ie. QScrollArea). This answer is already quite extended, and I won't go too much into the darkness of Qt size management... The problem is mainly caused by a wrong usage of spans: if you want to know more, I suggest you to take your time to patiently study the Qt sources.
Take this basic example:
from PyQt5.QtWidgets import *
class Test(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet('''
QLabel {
border: 1px solid red;
}
''')
layout = QGridLayout(self)
w1 = QLabel('Row span=2')
w1.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
w2 = QLabel('Row span=1')
w2.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
layout.addWidget(w1, 0, 0, 2, 1)
layout.addWidget(w2, 2, 0, 1, 1)
app = QApplication([])
example = Test()
example.show()
sys.exit(app.exec_())
Which will result conceptually like your case:
This behavior may seem invalid and certainly not perfect, but the point remains: as mentioned above, the purpose of spans is not to directly suggest a size for the item; using something in the wrong way often results in unexpected behavior.
This is one of the reasons for which I generally don't use (and often discourage) QGridLayout for complex UIs: most of the times that I decide for a QGridLayout, I end up with dismantling it and use nested layouts instead.
Solutions
If you want your widgets to have proportional sizes, then you have two choices. Either use QGridLayout properly (with item based rows/columns and proper stretch factors), or use nested layouts.
In your case, keeping a grid layout requires you to:
use two rows, with each plot widget taking just one row;
use setRowStretch() for both rows, with a value of 5 for the first, and 1 for the second;
use a row span of 2 for the left panel;
add a stretch to the bottom of the left panel layout, so that its contents are "pushed" to the top;
vlayout_signals.addStretch()
grid_layout.addLayout(vlayout_signals, 0, 0, 2, 1)
grid_layout.addWidget(self.p1, 0, 1)
grid_layout.addWidget(self.p2, 1, 1)
grid_layout.setRowStretch(0, 5)
grid_layout.setRowStretch(1, 1)
With a nested layout, instead:
use a HBoxLayout as main layout;
create a vertical layout for the plot widgets, and add them using the stretch argument;
add a stretch to the left panel as above;
main_layout = QHBoxLayout(self)
vlayout_signals.addStretch()
main_layout.addLayout(vlayout_signals)
plot_layout = QVBoxLayout()
main_layout.addLayout(plot_layout)
plot_layout.addWidget(self.p1, stretch=5)
plot_layout.addWidget(self.p2, stretch=1)
Note: all QLayouts have an optional QWidget argument that automatically installs the layout on the given widget, thus avoiding calling layout.setLayout(widget).
I would strongly suggest you this last solution, as it's more consistent with widgets that will probably have no size relations: the moment you need to add more plots, you don't have to care about the row span of the panel.
Interestingly enough, the only case for which a grid layout would have really made sense is the slider/label pair: instead of adding two unrelated horizontal layouts, you should have used a grid for them. You were "lucky" because those labels are relatively small and similar in widths, but the reality is that they are not properly aligned with their corresponding sliders. Try to add the following:
self.setStyleSheet('QSlider, QLabel {border: 1px solid red;}')
And that's the result:
If those label had very different lengths, their offset would have been very confusing.
Conclusions
While QGridLayout is certainly useful, it's important to consider that it can rarely be used and relied upon as "main layout" that directly manages items that are very different in their "visual" purpose; its row/column cells are just abstract references to item positions, not their resulting sizes, and spans must always be used only according to the other contents in the corresponding rows and columns.
Complex UIs almost always require nested layout managers that group their contents based on the hierarchy of their usage.
The rule of thumb is that a layout has to be considered for items that are grouped within the same hierarchy. You can certainly use QGridLayout as a main layout, but only as long as it manages items that can be considered siblings. For instance, you could consider a grid layout like this:
┌─────────────┬─────────────────────────┐
|┌───────────┐|┌───────────────────────┐|
|| combo ||| ||
||┌────┬────┐||| plot1 ||
||| | |||| ||
||| s1 | s2 |||└───────────────────────┘|
||| | |||┌───────────────────────┐|
||├────┼────┤||| ||
||| l1 | l2 |||| ||
||└─────────┘||| ||
|| btn ||| ||
|| ^ ||| plot2 ||
|| ║ ||| ||
|| stretch ||| ||
|| ║ ||| ||
|| v ||| ||
|└───────────┘|└───────────────────────┘|
├─────────────┴─────────────────────────┤
| something else |
└───────────────────────────────────────┘
Which would be written as follows:
main_layout = QGridLayout()
panel_layout = QVBoxLayout()
main_layout.addLayout(panel_layout)
panel_layout.addWidget(combo)
slider_layout = QGridLayout()
panel_layout.addLayout(slider_layout)
slider_layout.addWidget(s1)
slider_layout.addWidget(s2, 0, 1)
slider_layout.addWidget(l1, 1, 0)
slider_layout.addWidget(l2, 1, 0)
panel_layout.addWidget(btn)
panel_layout.addStretch()
plot_layout = QVBoxLayout()
main_layout.addLayout(plot_layout, 0, 1)
plot_layout.addWidget(plot1)
plot_layout.addWidget(plot2)
main_layout.addWidget(else, 1, 0, 1, 2)
I suggest you to take your time to experiment with Qt Designer, in order to get accustomed with layout managers and different widget types, policies or restraints.
You can try adjusting the stretch factors on the grid rows.
For example something like this might be what you are looking for.
grid_layout.addLayout(vlayout_signals, 0, 0, 2, 1)
grid_layout.addWidget(self.p1, 0, 1, 5, 1)
grid_layout.addWidget(self.p2, 5, 1, 1, 1)
for row in range(6):
grid_layout.setRowStretch(row, 1)
Note
The above solution will not work in all situations. For a better explanation see #musicmante's answer.
I want my vue Konva Text element to completely fill the given height, like i expect of a rectangle.
This is issue becomes obvious when pairing with text images, (converting svg text to canvas) that properly match the given dimension
<v-text :config={
x: 50,
y: 50,
width: 1000,
height: 60,
fontSize: 60,
fontStyle: 'bold',
fontFamily 'Campton Book'
text: 'WELT'
}
/>
<v-rect
:config="{ x: 50, y: 50, fill: 'black', height: 60, width: 200 }"
/>
Second Part, is there any way to always pixel perfectly align the left side with the border? the x coordinate matches the border
Is this due to font constraints? What am I missing?
I tried to get the height of the text node to fix this positioning but this is the given height passed down as props
Text is defined as having parts above and below the baseline. Above is termed 'ascenders' amd below is 'descenders', which are required for lower case letters like j y g.
Setting the text fontSize to 60 does not say 'whatever the string, make it fill a space 60px high'. Instead it says 'Make text in a 60px font', which makes space for the descenders because they will generally be required.
If you know for sure that the text will be all caps, then a solution is to measure the height used and increase the font size by a computed factor so that the font fills the line height.
To do this you'll need to get the glyph measurements as follows:
const lineHeight = 60; // following your code
// make your text shape here...left out for brevity
const metrics = ctx.measureText('YOUR CAPS TEXT');
capsHeight = Math.abs(metrics.actualBoundingBoxAscent)
fontSize = lineHeight * lineHeight / capsHeight;
If I got that right, your 60px case should give a value around 75. That's based on the convention that ascenders are 80% of the line height. Now you set the font size of your shape to this new value and you should be filling the entire line height.
Regarding the left-alignment, this relies on what the font gurus call the a-b-c widths. The left gap is the a-space, the b is the character width (where the ink falls) and the c-space is the same as the a-space but on the right hand side.
Sadly unless someone else can tell me I am wrong, you don't get a-b-c widths in the canvas TextMetric. There is a workaround which is rather convoluted but viable. You would draw the text in black on an off-screen canvas filled with a transparent background. Then get the canvas pixel data and walk horizontal lines from the left of the canvas inspecting pixels and looking for the first colored pixel. Once you find it you have the measurement to offset the text shape horizontally.
In help(plt.bar) of matplotlib version 2.0.0, it says that the bars are
[...] with rectangles bounded by:
`left`, `left` + `width`, `bottom`, `bottom` + `height`
(left, right, bottom and top edges)
Yet, calling plt.bar(0, 1) produces
which does not start at left == 0, but instead at left - width/2 == -0.4. How to fix this?
Short answer:
plt.bar(0, 1, align="center")
Why:
The documentation on github says to use align=edge:
The default is now
(x - width/2, x + width/2, bottom, bottom + height)
[...] The align keyword-only argument controls if x is interpreted
as the center or the left edge of the rectangle.[...]
align : {'center', 'edge'}, optional, default: 'center'
If 'center', interpret the *x* argument as the coordinates
of the centers of the bars. If 'edge', aligns bars by
their left edges
To align the bars on the right edge pass a negative
*width* and ``align='edge'``
I want to get some subarea of an image and make it a new image, and then execute further functions on it.
How can I use mouse to select a subarea of an image?
I know img[] could get the subarea of img, but I need some function by which I can interact with img. I mean, I want to get a WYSIWYG effect.
Is there any commands available, or is there any methods of ROI capable?
There different ways to do what you want in a script: You could ask the user to place an ROI and then use the [] to address this area. If you want to get a new image from this selection (CTRL + C and CTRL + SHIFT + V without scripting) you would write:
ShowImage( ImageClone( img[] ) )orimg[].ImageClone().ShowImage()
If you want to place a ROI for the user, you can used SetSelection() for a simple, rectangle, volatile ROI, or you use the full ROI commands like in this example:
image img := RealImage( "Test", 4, 256, 256 ) // create image
img.ShowImage() // show the image (so that it has a display)
imageDisplay disp = img.ImageGetImageDisplay(0) // Get the 'display' of an image
ROI myR = NewRoi() // create ROI
myR.ROISetRectangle( 110, 120, 130, 140 ) // Make it a rectangle of given area
myR.ROISetVolatile( 0 ) // make it non-volatile
myR.ROISetLabel( "Selection" ) // give it a label
myR.ROISetColor( 0, 1, 0 ) // make it green
disp.ImageDisplayAddROI( myR ) // add it to the display
A full list of ROI commands can be found in the following section of the F1 help documentation: