I have a function button_max_slot that use showMaximized to maximize the window, and another function button_restore_slot to restore the window's size and place. the first time I call the button_max_slot it works great, then I use button_restore_function to restore window's size and place. But the second time I call the button_max_slot to maximize the window, it didn't work. I call self.isMaximized() and it returns true, but actually the window doesn't maximized.What should I do to fix this problem?Here is a minimal reproducible example:
from PyQt5 import QtWidgets
import sys
class MyWindow(QtWidgets.QMainWindow):
last_geometry = None
def __init__(self):
super().__init__()
self.resize(960, 540)
self.ButtonMax = QtWidgets.QPushButton(self)
self.ButtonMax.setObjectName("ButtonMax")
self.ButtonMax.setText("Max")
self.ButtonMax.move(100, 100)
self.ButtonRestore = QtWidgets.QPushButton(self)
self.ButtonRestore.setText("Restore")
self.ButtonRestore.setObjectName("ButtonRestore")
self.ButtonRestore.move(300, 100)
self.ButtonRestore.setEnabled(False)
self.ButtonMax.clicked.connect(self.button_max_slot)
self.ButtonRestore.clicked.connect(self.button_restore_slot)
def button_max_slot(self):
self.last_geometry = self.geometry()
self.showMaximized()
self.ButtonRestore.setEnabled(True)
self.ButtonMax.setEnabled(False)
def button_restore_slot(self):
self.setGeometry(self.last_geometry)
self.ButtonRestore.setEnabled(False)
self.ButtonMax.setEnabled(True)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
myshow = MyWindow()
myshow.show()
sys.exit(app.exec_())
tl;dr
Use showNormal() instead of setGeometry() to restore the window state.
Explanation
While not intuitive, it's still possible to set a geometry of a window even if its state should not allow it, and that's because setting the state of a window is not the same as setting its geometry.
Simply put, setting the window state tells the underlying platform/window manager to "choose" the geometry of the window based on the specified state, while setting the geometry asks the system to explicitly set the position and size of the window. Whether the system allows it, is another story.
An important thing to consider is that a QWidget (even a top level one, including a QDialog or a QMainWindow) is not the actual window shown on the screen. What you see is the QWindow (an abstract representation of the system's window for that widget), which is what actually contains the QWidget (or any of its inherited classes). Setting the state acts on the QWindow, while setting the geometry normally acts on the contained widget, excluding the possible window frame.
For instance, I'm able to reproduce your issue on my Linux system, but ekhumoro cannot, even though we both are using Linux (we're both using similar window managers, but they're still different: he's on OpenBox, I'm on FluxBox). Furthermore, I get inconsistent behavior after pressing the "Max" button, even if using the system features.
The fact that you got a maximized window state even if it doesn't look like it is, is exactly related to that: the state is maximized, the geometry isn't (because you changed it).
Consider it the other way around: you can manually resize a window in order to precisely occupy the whole available screen size, but that doesn't make it maximized: you can still probably see the "maximize" button in it's title bar (not the "normalize" one), and maybe even the window borders that are normally hidden when the window is actually maximized.
Note that the inconsistent behavior shown on different OS or window managers relies on two sides: the OS/wm implementation and Qt attempts to use a "standardized" behavior across all systems.
The solution is simple: just restore the state using showNormal() instead of setGeometry().
It usually works on all systems, with the exception of some very specific window managers on linux (and maybe some "non standard" behavior in recent Windows/MacOS versions), but it's the accepted approach.
For those cases, you might consider storing and restoring the geometry by overriding the top level window's changeEvent(), checking if the event.type() is a WindowStateChange and eventually decide the behavior based on the current windowState() and the oldState() of that event.
Remember that window states are flags, so they can be an OR combination of Qt.WindowState enums.
Related
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.
I'm implementing a custom widget to use it as a title bar on a dockable window. My problem arises only on Windows, namely, the window border disappears when the dock window is afloat.
Seems the problem is that, on Windows only, the window flags are changed. I.e. when I do this:
print dock_window.windowFlags()
dock_window.setTitleBarWidget(title_bar)
print dock_window.windowFlags()
it prints out different setting for the flags before and after.
However, it stays the same on linux and the borders remain unchanged.
My question is, how to restore the window border?
UPDATE: Since the custom title bar overrides the flags for the border when the dock window is floating, how can I edit the dock window so it has some kind of border?
(It is crucial for the dock window to have a custom title bar when floating.)
According to this answer this is expected behavior.
From the documentation of setTitleBarWidget:
If a title bar widget is set, QDockWidget will not use native window
decorations when it is floated.
So Linux does it the wrong way then?
Anyway as a workaround for Windows I implemented the idea (unsetting the title bar widget before floating) from the answer in PySide/PyQt.
from PySide import QtGui, QtCore
class MyDockWidget(QtGui.QDockWidget):
def __init__(self, title_widget):
super().__init__()
self.title_widget = title_widget
self.toggle_title_widget(False)
self.topLevelChanged.connect(self.toggle_title_widget)
def toggle_title_widget(self, off):
if off:
self.setTitleBarWidget(None)
else:
self.setTitleBarWidget(self.title_widget)
app = QtGui.QApplication([])
w = QtGui.QMainWindow()
t = QtGui.QLabel('Title')
d = MyDockWidget(t)
w.addDockWidget(QtCore.Qt.LeftDockWidgetArea, d)
w.show()
app.exec_()
At least it keeps the standard decoration when floating.
I found this to be an unresolved bug in QT and I don't see this as expected behavior.
I found multiple cases of people stumbling on this issue eg1, eg2 and others.
Some recommend unsetting and setting the setTitleBarWidget as in Trilarion's answer.
This, however, removes the custom title bar and was not ok with me.
others recommend setting flags on topLevelChanged event: window.setWindowFlags(Qt::Window | Qt::FramelessWindowHint);. this adds the usual title bar to the dock widget, which again is not what I personally want.
the best solution I found is w->setWindowFlags(Qt::Tool|Qt::CustomizeWindowHint);. This uses Qt.CustomizeWindowHint instead of Qt.FramelessWindowHint and does not produce a huge title bar, merely a small bar.
Implementation
from PyQt5.QtCore import Qt
....
def dockfloatevent(isfloating):
if isfloating:
dock.setWindowFlags(Qt.Tool | Qt.CustomizeWindowHint)
dock.topLevelChanged.connect(dockfloatevent)
I am not using the most up to date Qt, but from what I can tell this is still an issue? If someone has a Qt account maybe post something to the above bug link? I have already wasted many hours on this and don't feel like pushing it further :|
I'm trying to take a screenshot of the current active window in PyQt5. I know the generic method to take an screenshot of any window is QScreen::grabWindow(winID), for which winID is an implementation-specific ID depending on the window system. Since I'm running X and KDE, I plan to eventual use CTypes to call Xlib, but for now, I simply execute "xdotool getactivewindow" to obtain the windowID in a shell.
For a minimum exmaple, I created a QMainWindow with a QTimer. When the timer is fired, I identify the active window ID by executing "xdotool getactivewindow", get its return value, call grabWindow() to capture the active window, and display the screetshot in a QLabel. On startup, I also set my window a fixed 500x500 size for observation, and activate Qt.WindowStaysOnTopHint flag, so that my window is still visible when it's not in focus. To put them together, the implementation is the following code.
from PyQt5 import QtCore, QtGui, QtWidgets
import subprocess
class ScreenCapture(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
self.setFixedHeight(500)
self.setFixedWidth(500)
self.label = QtWidgets.QLabel(self)
self.timer = QtCore.QTimer(self)
self.timer.setInterval(500)
self.timer.timeout.connect(self.timer_handler)
self.timer.start()
self.screen = QtWidgets.QApplication.primaryScreen()
#QtCore.pyqtSlot()
def timer_handler(self):
window = int(subprocess.check_output(["xdotool", "getactivewindow"]).decode("ascii"))
self.screenshot = self.screen.grabWindow(window)
self.label.setPixmap(self.screenshot)
self.label.setFixedSize(self.screenshot.size())
if __name__ == '__main__':
app = QtWidgets.QApplication([])
window = ScreenCapture()
window.show()
app.exec()
To test the implementation, I started the script and clicked another window. It appears to work without problems if there is no overlap between my application window and the active window. See the following screenshot, when Firefox (right) is selected, my application is able to capture the active window of Firefox and display it in the QLabel.
However, the screenshot doesn't work as expected if there is an overlap between the application window and the active window. The window of the application itself will be captured, and creates a positive feedback.
If there is an overlap between the application window and the active window. The window of the application itself will be captured, and creates a positive feedback.
I've already disabled the 3D composite in KDE's settings, but the problem remains. The examples above are taken with all composite effects disabled.
Question
Why isn't this implementation working correctly when the application window and the active window are overlapped? I suspect it's an issue caused by some forms of unwanted interaction between graphics systems (Qt toolkit, window manager, X, etc), but I'm not sure.
Is it even possible solve this problem? (Note: I know I can hide() before the screenshot and show() it again, but it doesn't really solve this problem, which is taking a screenshot even if an overlap exists.)
As pointed out by #eyllanesc, it appears that it is not possible to do it in Qt, at least not with QScreen::grabWindow, because grabWindow() doesn't actually grab the window itself, but merely the area occupied by the window. The documentation contains the following warning.
The grabWindow() function grabs pixels from the screen, not from the window, i.e. if there is another window partially or entirely over the one you grab, you get pixels from the overlying window, too. The mouse cursor is generally not grabbed.
The conclusion is that it's impossible do to it in pure Qt. It's only possible to implement such a functionality by writing a low-level X program. Since the question asks for a solution "in Qt", any answer that potentially involves deeper, low-level X solutions are out-of-scope. This question can be marked as resolved.
The lesson to learn here: Always check the documentation before using a function or method.
Update: I managed to solve the problem by reading the window directly from X via Xlib. Somewhat ironically, my solution uses GTK to grab the window and sends its result to Qt... Anyway, you can write the same program with Xlib directly if you don't want to use GTK, but I used GTK since the Xlib-related functions in GDK is pretty convenient to demonstrate the basic concept.
To get a screenshot, we first convert our window ID to an GdkWindow suitable for use within GDK, and we call Gdk.pixbuf_get_from_window() to grab the window and store it in a gdk_pixbuf. Finally, we call save_to_bufferv() to convert the raw pixbuf to a suitable image format and store it in a buffer. At this point, the image in the buffer is suitable to use in any program, including Qt.
The documentation contains the following warning:
If the window is off the screen, then there is no image data in the obscured/offscreen regions to be placed in the pixbuf. The contents of portions of the pixbuf corresponding to the offscreen region are undefined.
If the window you’re obtaining data from is partially obscured by other windows, then the contents of the pixbuf areas corresponding to the obscured regions are undefined.
If the window is not mapped (typically because it’s iconified/minimized or not on the current workspace), then NULL will be returned.
If memory can’t be allocated for the return value, NULL will be returned instead.
It also has some remarks about compositing,
gdk_display_supports_composite has been deprecated since version 3.16 and should not be used in newly-written code.
Compositing is an outdated technology that only ever worked on X11.
So basically, it's only possible to grab a partially obscured window under X11 (not possible in Wayland!), with a compositing window manager. I tested it without compositing, and found the window is blacked-out when compositing is disabled. But when composition is enabled, it seems to work without problem. It may or may not work for your application. But I think if you are using compositing under X11, it probably will work.
from PyQt5 import QtCore, QtGui, QtWidgets
import subprocess
class ScreenCapture(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
self.setFixedHeight(500)
self.setFixedWidth(500)
self.label = QtWidgets.QLabel(self)
self.screen = QtWidgets.QApplication.primaryScreen()
self.timer = QtCore.QTimer(self)
self.timer.setInterval(500)
self.timer.timeout.connect(self.timer_handler)
self.timer.start()
#staticmethod
def grab_screenshot():
from gi.repository import Gdk, GdkX11
window_id = int(subprocess.check_output(["xdotool", "getactivewindow"]).decode("ascii"))
display = GdkX11.X11Display.get_default()
window = GdkX11.X11Window.foreign_new_for_display(display, window_id)
x, y, width, height = window.get_geometry()
pb = Gdk.pixbuf_get_from_window(window, 0, 0, width, height)
if pb:
buf = pb.save_to_bufferv("bmp", (), ())
return buf[1]
else:
return
#QtCore.pyqtSlot()
def timer_handler(self):
screenshot = self.grab_screenshot()
self.pixmap = QtGui.QPixmap()
if not self.pixmap:
return
self.pixmap.loadFromData(screenshot)
self.label.setPixmap(self.pixmap)
self.label.setFixedSize(self.pixmap.size())
if __name__ == '__main__':
app = QtWidgets.QApplication([])
window = ScreenCapture()
window.show()
app.exec()
Now it captures an active window perfectly, even if there are overlapping windows on top of it.
I'm implementing a custom widget to use it as a title bar on a dockable window. My problem arises only on Windows, namely, the window border disappears when the dock window is afloat.
Seems the problem is that, on Windows only, the window flags are changed. I.e. when I do this:
print dock_window.windowFlags()
dock_window.setTitleBarWidget(title_bar)
print dock_window.windowFlags()
it prints out different setting for the flags before and after.
However, it stays the same on linux and the borders remain unchanged.
My question is, how to restore the window border?
UPDATE: Since the custom title bar overrides the flags for the border when the dock window is floating, how can I edit the dock window so it has some kind of border?
(It is crucial for the dock window to have a custom title bar when floating.)
According to this answer this is expected behavior.
From the documentation of setTitleBarWidget:
If a title bar widget is set, QDockWidget will not use native window
decorations when it is floated.
So Linux does it the wrong way then?
Anyway as a workaround for Windows I implemented the idea (unsetting the title bar widget before floating) from the answer in PySide/PyQt.
from PySide import QtGui, QtCore
class MyDockWidget(QtGui.QDockWidget):
def __init__(self, title_widget):
super().__init__()
self.title_widget = title_widget
self.toggle_title_widget(False)
self.topLevelChanged.connect(self.toggle_title_widget)
def toggle_title_widget(self, off):
if off:
self.setTitleBarWidget(None)
else:
self.setTitleBarWidget(self.title_widget)
app = QtGui.QApplication([])
w = QtGui.QMainWindow()
t = QtGui.QLabel('Title')
d = MyDockWidget(t)
w.addDockWidget(QtCore.Qt.LeftDockWidgetArea, d)
w.show()
app.exec_()
At least it keeps the standard decoration when floating.
I found this to be an unresolved bug in QT and I don't see this as expected behavior.
I found multiple cases of people stumbling on this issue eg1, eg2 and others.
Some recommend unsetting and setting the setTitleBarWidget as in Trilarion's answer.
This, however, removes the custom title bar and was not ok with me.
others recommend setting flags on topLevelChanged event: window.setWindowFlags(Qt::Window | Qt::FramelessWindowHint);. this adds the usual title bar to the dock widget, which again is not what I personally want.
the best solution I found is w->setWindowFlags(Qt::Tool|Qt::CustomizeWindowHint);. This uses Qt.CustomizeWindowHint instead of Qt.FramelessWindowHint and does not produce a huge title bar, merely a small bar.
Implementation
from PyQt5.QtCore import Qt
....
def dockfloatevent(isfloating):
if isfloating:
dock.setWindowFlags(Qt.Tool | Qt.CustomizeWindowHint)
dock.topLevelChanged.connect(dockfloatevent)
I am not using the most up to date Qt, but from what I can tell this is still an issue? If someone has a Qt account maybe post something to the above bug link? I have already wasted many hours on this and don't feel like pushing it further :|
I have a grid of items in PyQt, and when the user modifies the window size I need to increase/decrease the number of columns accordingly. The number of rows are handled by a scrollarea, so I don't need to worry about changes in the y direction (if that matters).
Inside my implementation of QMainWindow, I know it's possible to override the resizeEvent() function, which will be triggered for any and all window adjustments. However, using that to rebuild the grid everytime is horribly inefficient. Just to test the function to see how it worked, I had resizeEvent merely print a string, and that caused my window adjustments to be slightly laggy and visually imperfect (jittery rather than smooth). I'll probably run a simple division operation on the window size to see if it has gotten larger or smaller enough to change the number of columns, but even that, when run a hundred times per adjustment, might cause lag issues. Rebuilding the entire grid might even take a second to do, so it would be preferable not to need to do it as the user is manipulating the window.
Is there a more efficient way to do it, or is resizeEvent my only option? Ideally, I'd like an event that triggered only once the user finished adjusting the window and not an event that triggers for practically every pixel movement as they happen (which can be hundreds or thousands of times per adjustment in the span of 1 second).
I'm using PyQt5, but if you're more familiar with PyQt4, I can figure out your PyQt4 solution in the context of PyQt5. Same for a C++ Qt4/5 solution.
It looks like the only real problem is detecting when resizing has completed. So long as this is carefully controlled, the actual laying out can be done in any way you like.
Below is a solution that uses a timer to control when a resize-completed signal is emitted. It doesn't appear to be laggy, but I haven't tested it with any complex layouts (should be okay, though).
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
resizeCompleted = QtCore.pyqtSignal()
def __init__(self):
QtGui.QWidget.__init__(self)
self._resize_timer = None
self.resizeCompleted.connect(self.handleResizeCompleted)
def updateResizeTimer(self, interval=None):
if self._resize_timer is not None:
self.killTimer(self._resize_timer)
if interval is not None:
self._resize_timer = self.startTimer(interval)
else:
self._resize_timer = None
def resizeEvent(self, event):
self.updateResizeTimer(300)
def timerEvent(self, event):
if event.timerId() == self._resize_timer:
self.updateResizeTimer()
self.resizeCompleted.emit()
def handleResizeCompleted(self):
print('resize complete')
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 300, 300)
window.show()
sys.exit(app.exec_())
I think you need a FlowLayoutfot this purpose, which automatically adjusts the number of columns on resizing the widget containing it. Here is the documentation for FlowLayout and here is the PyQt version of the same layout.