WxPython wxSplitterWindow.SplitVertically always shows uneven split - python

I'm trying to understand how to use the wxSplitterWindow class but can't seem to get a vertical split to be even (i.e. left and right panes each take up the same amount of space). According to the wxPython 3.03 documentation, wxSplitterWindow.SplitVertically has the following signature:
SplitVertically(self, window1, window2, sashPosition=0)
The last parameter sashPosition has the following description:
sashPosition - The initial position of the sash. If this value is positive, it specifies the size of the left pane. If it is negative, it is absolute value gives the size of the right pane. Finally, specify 0 (default) to choose the default position (half of the total window width).
From the description, I gather that passing a 0 for sashPosition splits the window in half with the left and right panes taking up equal space. However, when I run the following example program, I get a window with an uneven split where almost all of the left panel is hidden.
import wx
class SplitterFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title='SplitterWindow example')
# Create the main splitter window (to be split vertically)
self.splitter = wx.SplitterWindow(self)
self.rightPanel = wx.Panel (self.splitter)
self.rightPanel.SetBackgroundColour(wx.BLUE)
self.leftPanel = wx.Panel (self.splitter)
self.leftPanel.SetBackgroundColour(wx.RED)
# Expecting an even split with this call
self.splitter.SplitVertically (self.leftPanel, self.rightPanel, 0)
if __name__ == '__main__':
app = wx.App(0)
frame = SplitterFrame()
frame.Show()
app.MainLoop()
I'm running Python v2.7.10 using wxPython v3.0 toolkit on a Windows 8.1 machine.

I had the same problem, by trial, and error I get that this modification solved the problem
self.splitter.SplitHorizontally(self.leftPanel, self.rightPanel, 100)
But this is dependent on the size of the frame.
I'm using wxpython version 3.0.0.0

I know this thread is ancient, but in case anyone else has this issue:
From the wiki: https://wxpython.org/Phoenix/docs/html/wx.SplitterWindow.html#wx.SplitterWindow.SetSashGravity
Notice that when sash gravity for a newly created splitter window, it is often necessary to explicitly set the splitter size using SetSize to ensure that is big enough for its initial sash position. Otherwise, i.e. if the window is created with the default tiny size and only resized to its correct size later, the initial sash position will be affected by the gravity and typically result in sash being at the rightmost position for the gravity of 1
Either set the size manually with splitter.SetInitialSize(...) (obviously frame size dependent)
or use splitter.SetSashGravity(.5) so they grow evenly.

Related

Remove empty space at bottom of QTableWidget

I'm using PySide6 6.4.1 to build a table widget that automatically resizes to the number of rows. Here's a minimal example:
from PySide6.QtWidgets import *
class MW(QWidget):
def __init__(self):
super().__init__()
self.button = QPushButton("Test")
self.table = QTableWidget(self)
self.table.setColumnCount(1)
self.table.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents)
self.setLayout(QVBoxLayout(self))
self.layout().addWidget(self.button)
self.layout().addWidget(self.table)
self.button.clicked.connect(self.test)
return
def test(self):
self.table.insertRow(0)
self.table.setItem(0, 0, QTableWidgetItem("new item"))
self.table.adjustSize()
self.adjustSize()
return
app = QApplication()
mw = MW()
mw.show()
app.exec()
Somehow this always leaves a bit of empty space at the bottom of the table. How do I get rid of this space without doing manual resizing?
(Nevermind the weird font size, it's a known bug when using UI scaling. I've adjusted the font size manually as well and it doesn't get rid of this problem.)
Qt item views inherit from QAbstractScrollArea, which has some peculiar size related aspects:
it has an Expanding size policy that tells the parent layout it can use as much space as possible, possibly increasing the available space at initialization;
it has a minimumSizeHint() that always includes a minimum reasonable size allowing showing the scroll bars (even if they are not visible);
if the sizeAdjustPolicy is AdjustToContents it's also based on the viewport size hint;
It's also mandatory to consider a fundamental aspect about scroll areas: size management is a tricky subject, and some level of compromise is necessary most of the times. This is the case whenever the scroll bars potentially change the available size of the viewport (the part of the widget that is able to scroll), which is the default behavior of Qt in most systems, unless the scroll bars are always hidden/visible or they are transient (they "overlay" above the viewport without affecting its available visible size).
To clarify this aspect, consider a scroll area with content that has a minimum size of 100x100 and scroll bars that have a default extent (width for the vertical one, height for the horizontal) of 20: if the height hint of the content is changed to 110, then you'd theoretically need an area of 100x110. But Qt needs to know the hints before laying out widgets and setting their geometries. This means that you cannot know if the scroll bars have to be shown before the widget is finally laid out, but that hint is required to lay out the widget itself. So, recursion.
Qt layout management is a system that is far from perfect, but I doubt that there is one, at least considering normal UI management (don't consider web layouts: their concept is based on different assumption, most importantly the fact that the whole "window" has potentially infinite dimensions). This is an aspect that must be always considered, especially if the shown contents are set to adapt their size based on the contents; it's the case of fitInView() of QGraphicsView or the known issues of QLayout with rich text based widgets.
Qt doesn't provide "foolproof" solutions for these aspects, because its layout management doesn't allow it as it has been implemented primarily considering performance and usability: the UI has to work and be responsive before being "fancy".
It's one of the reasons for which it's almost impossible to have real fixed-aspect-ratio widgets or windows. You can work around it, but at some point you'll have some inconsistencies, and you have to live with that. Also consider that this kind of behavior is generally not very UX-friendly. UI elements that resize themselves (and, consequentially, alter the whole layout) at anytime are usually annoying and very user-unfriendly, especially if they displace their or other contents: it's like having a car that constantly moves the driving controls depending on the amount of passengers.
That said, it's not impossible to have a partially working solution.
The requirements are to:
override minimumSizeHint(), so that a minimal reasonable size is always returned;
override sizeHint() that is used to adjust the widget (and parents) based on the contents of the view;
change the vertical size policy of the table to Preferred, which will tell the layout manager that the height of the size hint will be considered as default, still allowing it to expand in case other items in the layout don't use the remaining space, and eventually shrink it if required;
eventually do the same for the horizontal policy in order to adapt it to the actual horizontal header size, otherwise use self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch), but be aware that this might complicate things (see the note after the code);
class TableWidget(QTableWidget):
def sizeHint(self):
hHeader = self.horizontalHeader()
vHeader = self.verticalHeader()
f = self.frameWidth() * 2
# the simple solution is to get the length, but this might be a problem
# whenever *any* section of the header is set to Stretch
targetWidth = width = f + hHeader.length()
# a possible alternative (but still far from perfect):
width = f
for c in range(self.columnCount()):
if hHeader.isSectionHidden(c):
continue
width += self.sizeHintForColumn(c)
targetWidth = width
if not vHeader.isHidden():
width += vHeader.width()
hpol = self.horizontalScrollBarPolicy()
height = f + vHeader.length() + hHeader.height()
if (
hpol != Qt.ScrollBarAlwaysOff
and not self.horizontalScrollBar().isHidden()
and (
hpol == Qt.ScrollBarAlwaysOn
and hHeader.length() + f < targetWidth
)
):
height += self.horizontalScrollBar().sizeHint().height()
return QSize(width, height)
def minimumSizeHint(self):
hint = self.sizeHint()
minHint = super().minimumSizeHint()
return QSize(
min(minHint.width(), hint.width()),
min(super().minimumSizeHint().height(), hint.height())
)
class MW(QWidget):
def __init__(self):
# ...
pol = self.table.sizePolicy()
pol.setVerticalPolicy(QSizePolicy.Preferred)
self.table.setSizePolicy(pol)
Be aware that the above doesn't solve all problems. It might work fine for a QTableView having just one column or when using the default interactive (or fixed) section resize mode, but whenever you set different resize modes for each column the result may be wrong.
In order to provide a finer resize, you'll need to do much complex computations that take into account each section resize mode for the horizontal header, the default/minimum/maximum and eventually the hint based on the content.
Further notes: 1. calling adjustSize() on the parent is normally enough, it's not necessary to do it on the children; 2. self.setLayout(QVBoxLayout(self)) is pointless, the self argument already sets the layout; just use layout = QVBoxLayout(self) and use that as a local variable to add widgets; 3. in Python the return at the end of a function is always implicit, you shall not add it as it's useless, redundant and distracting.

Python GTK3 How to bring widgets to the front?

I have an application (actually a plugin for another application) that presents a GTK notebook. Each tab contains a technical drawing of an operation, with a set of SpinButtons that allow you to alter the dimensions of the operation.
If you need more context, it's here: https://forum.linuxcnc.org/41-guis/26550-lathe-macros?start=150#82743
As can be seen above, this all worked fine in GTK2. The widgets (first iteration in a GTK_Fixed, then moved to a GTK_Table) were pre-positioned and the image (a particular layer of a single SVG) was plonked in behind.
Then we updated to GTK3 (and Python 3) and it stopped working. The SVG image now appears on top of the input widgets, and they can no-longer be seen or operated.
I am perfectly happy to change the top level container[1], if that will help. But the code that used to work (and now doesn't) is:
def on_expose(self,nb,data=None):
tab_num = nb.get_current_page()
tab = nb.get_nth_page(tab_num)
cr = tab.get_property('window').cairo_create()
cr.set_operator(cairo.OPERATOR_OVER)
alloc = tab.get_allocation()
x, y, w, h = (alloc.x, alloc.y, alloc.width, alloc.height)
sw = self.svg.get_dimensions().width
sh = self.svg.get_dimensions().height
cr.translate(0, y)
cr.scale(1.0 *w / sw, 1.0*h/sh)
#TODO: gtk3 drawing works, but svg is drawn over the UI elements
self.svg.render_cairo_sub(cr = cr, id = '#layer%i' % tab_num)
[1] In fact I will probably go back to GTK_Fixed and move the elements about in the handler when the window resizes, scaled according to the original position. The GTK_Table (deprecated) version takes over 2 minutes to open in the Glade editor.
Unless there is a more elegant way to do this too?

Pyqt5 image coordinates

I display images with Qlabel.I need image coordinates/pixel coordinates but, I use mouseclickevent its show me only Qlabel coordinates.
for examples my image is 800*753 and my Qlabel geometry is (701,451).I reads coordinates in (701,451) but I need image coordinates in (800*753)
def resimac(self):
filename= QtWidgets.QFileDialog.getOpenFileName(None, 'Resim Yükle', '.', 'Image Files (*.png *.jpg *.jpeg *.bmp *.tif)')
self.image=QtGui.QImage(filename[0])
self.pixmap=QtGui.QPixmap.fromImage(self.image)
self.resim1.setPixmap(self.pixmap)
self.resim1.mousePressEvent=self.getPixel
def getPixel(self, event):
x = event.pos().x()
y = event.pos().y()
print("X=",x," y= ",y)
Since you didn't provide a minimal, reproducible example, I'm going to assume that you're probably setting the scaledContents property, but that could also be not true (in case you set a maximum or fixed size for the label).
There are some other serious issues about your answer, I'll address them at the end of this answer.
The point has to be mapped to the pixmap coordinates
When setting a pixmap to a QLabel, Qt automatically resizes the label to its contents.
Well, it does it unless the label has some size constrains: a maximum/fixed size that is smaller than the pixmap, and/or the QLabel has the scaledContents property set to True as written above. Note that this also happens if any of its ancestors has some size constraints (for example, the main window has a maximum size, or it's maximized to a screen smaller than the space the window needs).
In any of those cases, the mousePressEvent will obviously give you the coordinates based on the widget, not on the pixmap.
First of all, even if it doesn't seem to be that important, you'll have to consider that every widget can have some contents margins: the widget will still receive events that happen inside the area of those margins, even if they are outside its actual contents, so you'll have to consider that aspect, and ensure that the event happens within the real geometry of the widget contents (in this case, the pixmap). If that's true, you'll have to translate the event position to that rectangle to get its position according to the pixmap.
Then, if the scaledContents property is true, the image will be scaled to the current available size of the label (which also means that its aspect ratio will not be maintained), so you'll need to scale the position.
This is just a matter of math: compute the proportion between the image size and the (contents of the) label, then multiply the value using that proportion.
# click on the horizontal center of the widget
mouseX = 100
pixmapWidth = 400
widgetWidth = 200
xRatio = pixmapWidth / widgetWidth
# xRatio = 2.0
pixmapX = mouseX * xRatio
# the resulting "x" is the horizontal center of the pixmap
# pixmapX = 200
On the other hand, if the contents are not scaled you'll have to consider the QLabel alignment property; it is usually aligned on the left and vertically centered, but that depends on the OS, the style currently in use and the localization (consider right-to-left writing languages). This means that if the image is smaller than the available size, there will be some empty space within its margins, and you'll have to be aware of that.
In the following example I'm trying to take care about all of that (I'd have to be honest, I'm not 100% sure, as there might be some 1-pixel tolerance due to various reasons, most regarding integer-based coordinates and DPI awareness).
Note that instead of overwriting mousePressEvent as you did, I'm using an event filter, I'll explain the reason for it afterwards.
from PyQt5 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QGridLayout(self)
self.getImageButton = QtWidgets.QPushButton('Select')
layout.addWidget(self.getImageButton)
self.getImageButton.clicked.connect(self.resimac)
self.resim1 = QtWidgets.QLabel()
layout.addWidget(self.resim1)
self.resim1.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter)
# I'm assuming the following...
self.resim1.setScaledContents(True)
self.resim1.setFixedSize(701,451)
# install an event filter to "capture" mouse events (amongst others)
self.resim1.installEventFilter(self)
def resimac(self):
filename, filter = QtWidgets.QFileDialog.getOpenFileName(None, 'Resim Yükle', '.', 'Image Files (*.png *.jpg *.jpeg *.bmp *.tif)')
if not filename:
return
self.resim1.setPixmap(QtGui.QPixmap(filename))
def eventFilter(self, source, event):
# if the source is our QLabel, it has a valid pixmap, and the event is
# a left click, proceed in trying to get the event position
if (source == self.resim1 and source.pixmap() and not source.pixmap().isNull() and
event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton):
self.getClickedPosition(event.pos())
return super().eventFilter(source, event)
def getClickedPosition(self, pos):
# consider the widget contents margins
contentsRect = QtCore.QRectF(self.resim1.contentsRect())
if pos not in contentsRect:
# outside widget margins, ignore!
return
# adjust the position to the contents margins
pos -= contentsRect.topLeft()
pixmapRect = self.resim1.pixmap().rect()
if self.resim1.hasScaledContents():
x = pos.x() * pixmapRect.width() / contentsRect.width()
y = pos.y() * pixmapRect.height() / contentsRect.height()
pos = QtCore.QPoint(x, y)
else:
align = self.resim1.alignment()
# for historical reasons, QRect (which is based on integer values),
# returns right() as (left+width-1) and bottom as (top+height-1),
# and so their opposite functions set/moveRight and set/moveBottom
# take that into consideration; using a QRectF can prevent that; see:
# https://doc.qt.io/qt-5/qrect.html#right
# https://doc.qt.io/qt-5/qrect.html#bottom
pixmapRect = QtCore.QRectF(pixmapRect)
# the pixmap is not left aligned, align it correctly
if align & QtCore.Qt.AlignRight:
pixmapRect.moveRight(contentsRect.x() + contentsRect.width())
elif align & QtCore.Qt.AlignHCenter:
pixmapRect.moveLeft(contentsRect.center().x() - pixmapRect.width() / 2)
# the pixmap is not top aligned (note that the default for QLabel is
# Qt.AlignVCenter, the vertical center)
if align & QtCore.Qt.AlignBottom:
pixmapRect.moveBottom(contentsRect.y() + contentsRect.height())
elif align & QtCore.Qt.AlignVCenter:
pixmapRect.moveTop(contentsRect.center().y() - pixmapRect.height() / 2)
if not pos in pixmapRect:
# outside image margins, ignore!
return
# translate coordinates to the image position and convert it back to
# a QPoint, which is integer based
pos = (pos - pixmapRect.topLeft()).toPoint()
print('X={}, Y={}'.format(pos.x(), pos.y()))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
Now. A couple of suggestions.
Don't overwrite existing child object methods with [other] object's instance attributes
There are various reasons for which this is not a good idea, and, while dealing with Qt, the most important of them is that Qt uses function caching for virtual functions; this means that as soon as a virtual is called the first time, that function will always be called in the future. While your approach could work in simple cases (especially if the overwriting happens within the parent's __init__), it's usually prone to unexpected behavior that's difficult to debug if you're not very careful.
And that's exactly your case: I suppose that resimac is not called upon parent instantiation and until after some other event (possibly a clicked button) happens. But if the user, for some reason, clicks on the label before a new pixmap is loaded, your supposedly overwritten method will never get called: at that time, you've not overwritten it yet, so the user clicks the label, Qt calls the QLabel's base class mousePressEvent implementation, and then that method will always be called from that point on, no matter if you try to overwrite it.
To work around that, you have at least 3 options:
use an event filter (as the example above); an event filter is something that "captures" events of a widgets and allows you to observe (and interact) with it; you can also decide to propagate that event to the widget's parent or not (that's mostly the case of key/mouse events: if a widget isn't "interested" about one of those events, it "tells" its parent to care about it); this is the simplest method, but it can become hard to implement and debug for complex cases;
subclass the widget and manually add it to your GUI within your code;
subclass it and "promote" the widget if you're using Qt's Designer;
You don't need to use a QImage for a QLabel.
This is not that an issue, it's just a suggestion: QPixmap already uses (sort of) fromImage within its C++ code when constructing it with a path as an argument, so there's no need for that.
Always, always provide usable, Minimal Reproducible Example code.
See:
https://stackoverflow.com/help/how-to-ask
https://stackoverflow.com/help/minimal-reproducible-example
It could take time, even hours to get an "MRE", but it's worth it: there'll always somebody that could answer you, but doesn't want to or couldn't dig into your code for various reasons (mostly because it's incomplete, vague, inusable, lacking context, or even too expanded). If, for any reason, there'll be just that one user, you'll be losing your occasion to solve your problem. Be patient, carefully prepare your questions, and you'll probably get plenty of interactions and useful insight from it.

Set minimal reasonable width for a QScrollArea

I am relatively new to Qt, which I access through PySide.
I have a longish list of content that I want to make vertically scrollable.
The horizontal size is not an issue. I tried using QScrollArea for that. Here is a minimal example:
import sys
import PySide.QtGui as gui
application = gui.QApplication(sys.argv)
window = gui.QScrollArea()
list = gui.QWidget()
layout = gui.QVBoxLayout(list)
for i in range(100):
layout.addWidget(gui.QLabel(
"A something longish, slightly convoluted example text."))
window.setWidget(list)
window.show()
sys.exit(application.exec_())
What happens:
The scroll-area sets its horizontal size to the size needed for the labels
It notices that the vertical space is insufficient, so it adds a vertical scrollbar.
Due to the vertical scrollbar, the horizontal space is now insufficient as well, and so the horizontal scrollbar is also shown.
I can make the horizontal scrollbar go away with setHorizontalScrollBarPolicy, but the main problem persists: the vertical scrollbar obscures part of the labels.
How can I set the width of the scroll-area to the minimal value which does not need a horizontal scrollbar?
Your example is somewhat unrealistic, because the scroll-area is made the top-level window. More typically, it would be a child widget, and its initial size would be determined by a layout, and would be indirectly constrained by the sizes of other widgets and/or layouts. As a top-level window, the constraints are different, and partly under the influence of the window-manager (the exact behaviour of which can vary between platforms).
To ensure that the scroll-area has the correct size, you must set a minimum width for it, based on its contents, and also allowing for the vertical scrollbar and the frame. It is also probably best to set the widgetResizable property to True and add an expandable spacer to the end of the contents layout.
In the example below, I have changed the background colour of the labels to make it easier to see what is going on. I have also allowed resizing the window smaller than its initial size, by resetting the minimum width after it is shown - but that is optional.
import sys
import PySide.QtGui as gui
application = gui.QApplication(sys.argv)
window = gui.QScrollArea()
window.setWidgetResizable(True)
list = gui.QWidget()
layout = gui.QVBoxLayout(list)
# just for testing
window.setStyleSheet('QLabel {background-color: red}')
for i in range(30):
layout.addWidget(gui.QLabel(
"A something longish, slightly convoluted example text."))
layout.addStretch()
window.setWidget(list)
window.setMinimumWidth(
list.sizeHint().width() +
2 * window.frameWidth() +
window.verticalScrollBar().sizeHint().width())
window.show()
# allow resizing smaller
window.setMinimumWidth(1)
sys.exit(application.exec_())
Try to use sizeHint() in the widgets and use the result to set the minimum width of the QScrollArea.
Some extra info here: http://doc.qt.io/qt-5/layout.html

pyqt: Fixed Position

How can I give a widget a fixed position? Like so I can "attach"/put it at the bottom of the window and it will always be there; even when the window is expanded. I couldn't find anything useful on how to do it and, I suppose obviously, none of the obvious things work (resize(), setGeometry(), etc.). Any help?
I assume by "fixed position" you mean a position relative to one of the window edges. That's what your second sentence implies. So that's the question I will answer.
Use a layout manager with stretches and spacings. Here's a simple example to attach a widget "w" to the bottom of a window "win". This code typically gets called by (or goes inside) your window's constructor.
lay = QVBoxLayout(win)
lay.addStretch(1)
lay.addWidget(w)
The BoxLayout makes "w" stick to the bottom of the window and stay in that position as the window is resized.
you must reimplement the parent windows resizeEvent function, here is the code to make a widget "attached" to the bottom of the window:
def resizeEvent(self, event):
#widget.move(x, y)
self.bottom_widget.move(0, self.height() - self.bottom_widget.height())
#if you want the widgets width equal to window width:
self.bottom_widget.setWidth(self.width())
whenever the window is resized, this function will be called and it will move the widget to the bottom of the window. this is an absolute positioning approach, but you can always use QSpacerItem to push your widget to the bottom.

Categories