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.
How can I make QSlider's paintEvent's rect a little smaller? I want to have a really thin slider but I want it to be able to receive clicks easily so it's actual size has to be a bit bigger than what's painted. If the slider is really thin, 2 pixels for example, it's so annoying to try and catch the handle or click on it. I tried creating custom QPaintEvent within paintEvent method and making it's rect height equal to 1-2 and then passing it to super().paintEvent(my_new_paint_event) but it didn't work. How can this be done?
Besides using a QSS (which requires to style all properties), the only reliable solution is to use a proxy style.
While overriding the paintEvent() of the slider is possible, there are two main issues:
changing the event region is completely pointless, as it will only change the exposed region that the function will draw upon (and that region might also be completely ignored);
the default paintEvent() of QSlider updates its option based on the current state and active/hovered subcontrols before calling the style functions, and unless you're willing to override a bunch of other functions (enter/leave, mouse press/move/release, keyboard events, etc), you'll only get a partially working result that won't reflect the actual widget state;
A QSlider draws itself using the drawComplexControl() function of QStyle, and this is achieved by providing a set of flags used for the subControls and activeSubControls of QStyleOptionSlider.
Since one of those controls is SC_SliderGroove, which is the part of the slider on which the handle moves, the solution is to remove that from the subControls, and do the painting on your own. Remember that painting happens from bottom to top, so the custom groove must be drawn before calling the base implementation.
class Style(QtWidgets.QProxyStyle):
def drawComplexControl(self, control, opt, qp, widget=None):
if control == self.CC_Slider:
# get the default rectangle of the groove
groove = self.subControlRect(
control, opt, self.SC_SliderGroove, widget)
# create a small one
if opt.orientation == QtCore.Qt.Horizontal:
rect = QtCore.QRectF(
groove.x(), groove.center().y() - .5,
groove.width(), 2)
else:
rect = QtCore.QRectF(
groove.center().x() - .5, groove.y(),
2, groove.height())
qp.save()
qp.setBrush(opt.palette.mid())
qp.setPen(opt.palette.dark().color())
qp.setRenderHints(qp.Antialiasing)
qp.drawRoundedRect(rect, 1, 1)
qp.restore()
# remove the groove flag from the subcontrol list
opt.subControls &= ~self.SC_SliderGroove
super().drawComplexControl(control, opt, qp, widget)
The above is generic for PyQt5 and PySide, for PyQt6 you need to change the flag names using their type, like Qt.Orientation.Horizontal, etc.
I am basically building a GUI with pyqt5 supposed to incorporate two videos. To do this, I use QMediaPlayer in combination with QVideoWidget, one for each class. The point is: while the first video plays as expected, the second one refuses to play. It uses exactly the same framework as the first one (one pushbuton for play/pause and one slidebar), and the same structure of code, but the screen remains desperately black when trying to play.
Worse, if I comment the code for the first video, the second now plays normally. Could that mean there is some conflict between the two QMedialPlayers? I can't make sense of that.
Any help would be greatly appreciated.
Here is my code (the GUI looks weird because I have removed most of it for clarity):
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QLabel, QPushButton, QLineEdit, QFrame, QHBoxLayout, QCheckBox, QRadioButton, QButtonGroup, QStyle, QSlider, QStackedLayout
import sys
from tkinter import Tk
from PyQt5.QtCore import pyqtSlot, QRect, Qt, QRunnable, QThreadPool, QThread, QObject, QUrl, QSize
import time
from PyQt5 import QtMultimedia
from PyQt5.QtMultimedia import QMediaContent, QMediaPlayer
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5.QtGui import QFont
from PyQt5.QtGui import QImage, QPalette, QBrush, QIcon, QPixmap
class DNN_Viewer(QWidget):
def __init__(self, n_filters=2):
super(DNN_Viewer, self).__init__()
# initialise GUI
self.init_gui()
# initialise videos to display images
self.mp1.play()
self.mp1.pause()
self.mp2.play()
self.mp2.pause()
def init_gui(self):
# main window
root = Tk()
screen_width = root.winfo_screenwidth() # screen width
screen_height = root.winfo_screenheight() # screen heigth
self.width = 1900 # interface width
self.heigth = 1000 # interface height
self.left = (screen_width - self.width) / 2 # left-center interface
self.top = (screen_height - self.heigth) / 2 # top-center interface
self.setFixedSize(self.width, self.heigth)
self.move(self.left, self.top)
self.setStyleSheet("background: white"); # interface background color
# bottom left frame
self.fm2 = QFrame(self) # creation
self.fm2.setGeometry(30, 550, 850, 430) # left, top, width, height
self.fm2.setFrameShape(QFrame.Panel); # use panel style for frame
self.fm2.setLineWidth(1) # frame line width
# video for weights and gradients
self.vw1 = QVideoWidget(self) # declare video widget
self.vw1.move(50,555) # left, top
self.vw1.resize(542,380) # width, height
self.vw1.setStyleSheet("background-color:black;"); # set black background
# wrapper for the video
self.mp1 = QMediaPlayer(self) # declare QMediaPlayer
self.mp1.setVideoOutput(self.vw1) # use video widget vw1 as output
fileName = "path_to_video_1" # local path to video
self.mp1.setMedia(QMediaContent(QUrl.fromLocalFile(fileName))) # path to video
self.mp1.stateChanged.connect(self.cb_mp1_1) # callback on change state (play, pause, stop)
self.mp1.positionChanged.connect(self.cb_mp1_2) # callback to move slider cursor
self.mp1.durationChanged.connect(self.cb_mp1_3) # callback to update slider range
# play button for video
self.pb2 = QPushButton(self) # creation
self.pb2.move(50,940) # left, top
self.pb2.resize(40,30) # width, height
self.pb2.setIconSize(QSize(18,18)) # button text
self.pb2.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) # standard triangle icon for play
self.pb2.clicked.connect(self.cb_pb2) # callback on click (play/pause)
# position slider for video
self.sld1 = QSlider(Qt.Horizontal,self) # creation
self.sld1.setGeometry( 110, 940, 482, 30) # left, top, width, height
self.sld1.sliderMoved.connect(self.cb_sld1) # callback on move
# title label
self.lb23 = QLabel(self) # creation
self.lb23.setText("Loss and accuracy") # label text
self.lb23.move(980,10) # left, top
self.lb23.setStyleSheet("font-size: 30px; font-family: \
FreeSans; font-weight: bold") # set font and size
# top right frame
self.fm3 = QFrame(self) # creation
self.fm3.setGeometry(980, 50, 850, 430) # left, top, width, height
self.fm3.setFrameShape(QFrame.Panel); # use panel style for frame
self.fm3.setLineWidth(1) # frame line width
# video for loss and accuracy
self.vw2 = QVideoWidget(self) # declare video widget
self.vw2.move(1000,55) # left, top
self.vw2.resize(542,380) # width, height
self.vw2.setStyleSheet("background-color:black;"); # set black background
# wrapper for the video
self.mp2 = QMediaPlayer(self) # declare QMediaPlayer
self.mp2.setVideoOutput(self.vw2) # use video widget vw1 as output
fileName2 = "path_to_video_2" # local path to video
self.mp2.setMedia(QMediaContent(QUrl.fromLocalFile(fileName2))) # path to video
self.mp2.stateChanged.connect(self.cb_mp2_1) # callback on change state (play, pause, stop)
self.mp2.positionChanged.connect(self.cb_mp2_2) # callback to move slider cursor
self.mp2.durationChanged.connect(self.cb_mp2_3) # callback to update slider range
# play button for video
self.pb3 = QPushButton(self) # creation
self.pb3.move(1000,440) # left, top
self.pb3.resize(40,30) # width, height
self.pb3.setIconSize(QSize(18,18)) # button text
self.pb3.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) # standard triangle icon for play
self.pb3.clicked.connect(self.cb_pb3) # callback on click (play/pause)
# position slider for video
self.sld2 = QSlider(Qt.Horizontal,self) # creation
self.sld2.setGeometry(1060, 440, 482, 30) # left, top, width, height
self.sld2.sliderMoved.connect(self.cb_sld2) # callback on move
def cb_mp1_1(self, state):
if self.mp1.state() == QMediaPlayer.PlayingState: # if playing, switch button icon to pause
self.pb2.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
elif self.mp1.state() == QMediaPlayer.StoppedState: # if stopped, rewind to first image
self.mp1.play()
self.mp1.pause()
else:
self.pb2.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) # if paused, switch button icon to play
def cb_mp1_2(self, position):
self.sld1.setValue(position) # set slider position to video position
def cb_mp1_3(self, duration):
self.sld1.setRange(0, duration) # set slider range to video position
def cb_pb2(self):
if self.mp1.state() == QMediaPlayer.PlayingState: # set to pause if playing
self.mp1.pause()
else:
self.mp1.play() # set to play if in pause
def cb_sld1(self, position):
self.mp1.setPosition(position) # set video position to slider position
def cb_mp2_1(self, state):
if self.mp2.state() == QMediaPlayer.PlayingState: # if playing, switch button icon to pause
self.pb3.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
elif self.mp2.state() == QMediaPlayer.StoppedState: # if stopped, rewind to first image
self.mp2.play()
self.mp2.pause()
else:
self.pb3.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) # if paused, switch button icon to play
def cb_mp2_2(self, position):
self.sld2.setValue(position) # set slider position to video position
def cb_mp2_3(self, duration):
self.sld2.setRange(0, duration) # set slider range to video position
def cb_pb3(self):
if self.mp2.state() == QMediaPlayer.PlayingState: # set to pause if playing
self.mp2.pause()
else:
self.mp2.play() # set to play if in pause
def cb_sld2(self, position):
self.mp2.setPosition(position) # set video position to slider position
# run GUI
def dnn_viewer():
app = QApplication(sys.argv) # initiate app; sys.argv argument is only for OS-specific settings
viewer = DNN_Viewer() # create instance of Fil_Rouge_Dashboard class
viewer.show() # display dashboard
sys.exit(app.exec_()) # allow exit of the figure by clicking on the top right cross
# call window function
dnn_viewer()
tl:dr;
Use layout managers.
Explanation
Well, it seems you accidentally found a (possible) bug by doing something really wrong.
QVideoWidget is a widget that is more complex than it seems, since it interfaces itself with the underlying graphics system of the OS, and, in order to correctly show its contents (the video), it has to be actively notified of its geometry.
Simply speaking, QVideoWidget does not directly show the "pictures" of the video QMediaPlayer shows, but tells the Operating System to do so (well, not exactly, but we won't discuss it here). This is because video displaying might take advantage of some hardware acceleration, or require some processing (for example, for HDR videos), similarly to what 3D/OpenGL graphics does.
When a program is going to display some (system managed) video, it has to tell the OS about the available geometry for that video, so that the OS is able to show it at the correct coordinates, and possibly apply resizing, some form of "clipping" (if another window is overlayed, for example) or any other level of [post]processing.
The "something really wrong" I was talking about before is based on the fact that you are using fixed geometries (sizes and positions) for both video widgets, and I think that Qt is not able to notify the system about those geometries for more than a widget at once if that happens before the video window is actually mapped (as in "shown").
Why is it really wrong, besides the issue at hand?
Each one of our devices is mostly unique: what you see on your device will be shown in a (possibly radically) different way on other's.
There are many reasons for that, including:
Operating System (and versions) and its behavior;
screen size and DPI (for example, I wasn't able to view the complete window of your code, since I've a smaller screen);
default/customized system font size; most importantly, if the default font is very big, the widgets might overlap;
further customization (for example, default margins and spacing);
if the interface is "adaptive", the user should be able to resize the interface:
if the user has a smaller screen, the ui should be resizable, so that everything is visible instead of having the need to move the window beyond the screen margins (something that is sometimes impossible: for example on Windows you can't move a window above the top margin of the screen);
if the user has a bigger screen (or uses a very high DPI setting), the interface would be too small and some elements might be hard to read or interact with;
That's the reason for which almost any nowadays website uses "responsive" layouts, which adapt the contents according to the screen of the device they're going to be displayed into.
The solution is very simple, and will also solve the big issue about your GUI: avoid any fixed geometry for your GUI and use layout managers instead.
Note that you can still use fixed sizes (not positions, sizes!): that's not that big of an issue, but using layout managers will help you a lot with it, by repositioning all elements according to the available space.
The reason for it is that layout managers ensure that any resizing operation (something that also happens many times as soon as a window is shown the first time) is also notified to the system, whenever it's required (like, for instance, adapting the QVideoWidget output).
If you want to keep the "bottom-right/top-left" layout, you can still do that:
set a main QGridLayout for the widget (DNN_Viewer), create another grid layout for each player and add those layout to the main one.
The structure will be something like this:
+------------------------- DNN_Viewer -------------------------+
| | +------ player2Layout ------+ |
| | | | |
| | | vw2 | |
| | | | |
| | +-------+-------------------+ |
| | | pb2 | sld1 | |
| | +-------+-------------------+ |
+------------------------------+-------------------------------+
| +------ player1Layout------+ | |
| | | | |
| | vw1 | | |
| | | | |
| +-------+------------------+ | |
| | pb1 | sld2 | | |
| +-------+------------------+ | |
+------------------------------+-------------------------------+
class DNN_Viewer(QWidget):
# ...
def init_gui(self):
# create a grid layout for the widget and automatically set it for it
layout = QtWidgets.QGridLayout(self)
player1Layout = QtWidgets.QGridLayout()
# add the layout to the second row, at the first column
layout.addLayout(player1Layout, 1, 0)
# video for weights and gradients
self.vw1 = QVideoWidget(self)
# add the video widget at the first row and column, but set its column
# span to 2: we'll need to add two widgets in the second row, the play
# button and the slider
player1Layout.addWidget(self.vw1, 0, 0, 1, 2)
# ...
self.pb2 = QPushButton(self)
# add the button to the layout; if you don't specify rows and columns it
# normally means that the widget is added to a new grid row
player1Layout.addWidget(self.pb2)
# ...
self.sld1 = QSlider(Qt.Horizontal,self)
# add the slider to the second row, besides the button
player1Layout.addWidget(self.sld1, 1, 1)
# ...
player2Layout = QtWidgets.QGridLayout()
# add the second player layout to the first row, second column
layout.addLayout(player2Layout, 0, 1)
self.vw2 = QVideoWidget(self)
# same column span as before
player2Layout.addWidget(self.vw2, 0, 0, 1, 2)
# ...
self.pb3 = QPushButton(self)
player2Layout.addWidget(self.pb3, 1, 0)
# ...
self.sld2 = QSlider(Qt.Horizontal,self)
player2Layout.addWidget(self.sld2, 1, 1)
This will solve your main issue (and lots of others you didn't consider).
Some further suggestions:
use more descriptive variable names; things like pb2 or lb23 seem easier to use and you might be led to think that short variables equals less time spent typing. Actually, there's no final benefit in that: while it may be true that shorter variable names might improve compiling speed (especially for interpreted languages like Python), at the end there's almost no advantage; on the contrary, you'll have to remember what "sld2" means, while something like "player2Slider" is way more descriptive and easier to read (which means you'll read and debug faster, and people reading your code will understand it and help you much more easily)
for the same reason above, use more descriptive function names: names like cb_mp1_3 mean literally nothing; naming is really important, and the startup speed improvement reported above is almost dismissible with todays computers; it also helps you to get help from others: it took more time to understand what was your actual issue, than to understand what your code does, since all those names were almost meaningless to me; read more on the official Style Guide for Python Code (aka, PEP 8);
use comments wisely:
avoid over-commenting, it makes comments distracting while losing much of their purpose (that said, while "Let the code be the documentation" is a good concept, don't overextimate it)
avoid "fancy" formatted comments: they might seem cool, but at the end they are just annoying to deal with; if you want to comment a function to better describe what it does, use the triple quotes feature Python already provides; also consider that many code sharing services have column limits (and StackOverflow is amongst them): people would need to scroll each line to read the corresponding comment;
if you need a description for a single line function, it's possible that the function is not descriptive as it could or should be, as explained above;
be more consistent with blank lines separations between functions or classes: Python was created with readability in mind, and it's a good thing to follow that principle;
don't overwrite existing attribute names: self.width() and self.height() are base properties of all QWidgets, and you might need to access them often;
be more consistent with the imports you're using, especially with complex modules like Qt: you should either import the submodules (from PyQt5 import QtWidgets, ...) or the single classes (from PyQt5.QtWidgets import QApplication, ...); note that, while the latter could be considered more "pythonic", it's usually tricky with Qt, since it has hundreds of classes (and you might need tens of them in each script), then you always have to remember to add every class each time you need it, and you might end up importing unnecessary classes you're not using anymore; there's not much performance improvement using this approach, at least with Qt, especially if you forget to remove unnecessary imports (in your case, the possible benefit of importing single classes is completely canceled by the fact that there are at least 10 imported classes that are never actually used);
avoid unnecessary imports from other frameworks if they are not absolutely necessary: if you need to know the screen geometry, use QApplication.screens(), don't import Tk just for that;
So, I started again trying to solve the issue with musicamant's very exhaustive answer. It indeed solved the issue, but I was not happy with having a solution that would work only with adaptative GUIs. So I investigated again the issue, starting with a minimal GUI where only the two videos would be present. And, to my greatest amazement, the two videos played fine, even with a fixed size GUI.
So I started inflating the GUI again, adding all the elements till I recovered my initial GUI. And at some point, I experienced the bug again, which made it possible to identify the actual cause.
So the culprit is called... QFrame. Yes, for real. The Qframe caused all that mess. At first I was using a QFrame with setFrameShape(QFrame.Panel), so that a rectangular frame is created at once. Then I installed the video widget inside the frame. It turns out that with certain videos, the QFrame adopts a strange behaviours and kind of "covers" the video output, making the video viewer screen vanish. The sound remains unaffected. It only happens for certain videos and not for others, which does not make any real sense. Still, removing the frame instantaneously solves the issue, so that really is a bug.
It seems that with musicamante's solution, the frame does not adopt this strange behaviour, hence a working solution. Another possible solution with fixed size GUIs is to use frames that don't cover the video. Concretely, rather than using a single QFrame with setFrameShape(QFrame.Panel) which creates a rectangle in one frame, a set of four frames must be used, two of them being QFrame with setFrameShape(QFrame.Hline), and the other two being QFrame with setFrameShape(QFrame.Vline), organised to form a rectangle. I tested it and it works. The frames only cover the horizontal/vertical surfaces they go through, and so the "inside" of the rectangle is not part of any frame, which avoids the bug.
This attached image is the screenshot of an application developed using PyQt5.
The image clearly has an invisible line running in the middle of the boxes enclosing the contents.
What code should I add in my program to draw an invisible line overlaying all other objects created earlier. I couldn't find any documentation regarding this but as the image suggests, it has somehow been implemented.
A code snippet is not needed to be provided by me since this is a question about adding/developing a feature rather than debugging or changing any existing code.
Premise: what you provided as an example doesn't seem a very good thing to do. It also seems more a glich than a "feature", and adding "invisible" lines like that might result in an annoying GUI for the user. The only scenario in which I'd use it would be a purely graphical/fancy one, for which you actually want to create a "glitch" for some reason. Also, note that the following solutions are not easy, and their usage requires you an advanced skill level and experience with Qt, because if you don't really understand what's happening, you'll most certainly encounter bugs or unexpected results that will be very difficult to fix.
Now. You can't actually "paint an invisible line", but there are certain work arounds that can get you a similar result, depending on the situation.
The main problem is that painting (at least on Qt) happens from the "bottom" of each widget, and each child widget is painted over the previous painting process, in reverse stacking order: if you have widgets that overlap, the topmost one will paint over the other. This is more clear if you have a container widget (such as a QFrame or a QGroupBox) with a background color and its children use another one: the background of the children will be painted over the parent's.
The (theoretically) most simple solution is to have a child widget that is not added to the main widget layout manager.
Two important notes:
The following will only work if applied to the topmost widget on which the "invisible line" must be applied.
If the widget on which you apply this is not the top level window, the line will probably not be really invisible.
class TestWithChildLine(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
for row in range(3):
for col in range(6):
layout.addWidget(QtWidgets.QDial(), row, col)
# create a widget child of this one, but *do not add* it to the layout
self.invisibleWidget = QtWidgets.QWidget(self)
# ensure that the widget background is painted
self.invisibleWidget.setAutoFillBackground(True)
# and that it doesn't receive mouse events
self.invisibleWidget.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
def resizeEvent(self, event):
super().resizeEvent(event)
# create a rectangle that will be used for the "invisible" line, wide
# as the main widget but with 10 pixel height, then center it
rect = QtCore.QRect(0, 0, self.width(), 10)
rect.moveCenter(self.rect().center())
# set the geometry of the "invisible" widget to that rectangle
self.invisibleWidget.setGeometry(rect)
Unfortunately, this approach has a big issue: if the background color has an alpha component or uses a pixmap (like many styles do, and you have NO control nor access to it), the result will not be an invisible line.
Here is a screenshot taken using the "Oxygen" style (I set a 20 pixel spacing for the layout); as you can see, the Oxygen style draws a custom gradient for window backgrounds, which will result in a "not invisible line":
The only easy workaround for that is to set the background using stylesheets (changing the palette is not enough, as the style will still use its own way of painting using a gradient derived from the QPalette.Window role):
self.invisibleWidget = QtWidgets.QWidget(self)
self.invisibleWidget.setObjectName('InvisibleLine')
self.invisibleWidget.setAutoFillBackground(True)
self.invisibleWidget.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
self.setStyleSheet('''
TestWithChildFull, #InvisibleLine {
background: lightGray;
}
''')
The selectors are required to avoid stylesheet propagation to child widgets; I used the '#' selector to identify the object name of the "invisible" widget.
As you can see, now we've lost the gradient, but the result works as expected:
Now. There's another, more complicated solution, but that should work with any situation, assuming that you're still using it on a top level window.
This approach still uses the child widget technique, but uses QWidget.render() to paint the current background of the top level window on a QPixmap, and then set that pixmap to the child widget (which now is a QLabel).
The trick is to use the DrawWindowBackground render flag, which allows us to paint the widget without any children. Note that in this case I used a black background, which shows a "lighter" gradient on the borders that better demonstrate the effect:
class TestWithChildLabel(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
layout.setSpacing(40)
for row in range(3):
for col in range(6):
layout.addWidget(QtWidgets.QDial(), row, col)
self.invisibleWidget = QtWidgets.QLabel(self)
self.invisibleWidget.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
palette = self.palette()
palette.setColor(palette.Window, QtGui.QColor('black'))
self.setPalette(palette)
def resizeEvent(self, event):
super().resizeEvent(event)
pm = QtGui.QPixmap(self.size())
pm.fill(QtCore.Qt.transparent)
qp = QtGui.QPainter(pm)
maskRect = QtCore.QRect(0, 0, self.width(), 50)
maskRect.moveTop(50)
region = QtGui.QRegion(maskRect)
self.render(qp, maskRect.topLeft(), flags=self.DrawWindowBackground,
sourceRegion=region)
qp.end()
self.invisibleWidget.setPixmap(pm)
self.invisibleWidget.setGeometry(self.rect())
And here is the result:
Finally, an further alternative would be to manually apply a mask to each child widget, according to their position. But that could become really difficult (and possibly hard to manage/debug) if you have complex layouts or a high child count, since you'd need to set (or unset) the mask for all direct children each time a resize event occurs. I won't demonstrate this scenario, as I believe it's too complex and unnecessary.
I’m trying to make an application consisting of a QMainWindow, the central widget of which is a QToolBar (it may not be usual, but for my purpose the toolbar’s well suited). Docks are allowed below only. I added a QDockWidget to it, and a QAction on the QToolBar toggles the QDockWidget on and off with removeDockWidget() and restoreDockWidget().
The default size of the QMainWindow is 800 by 24, QToolBar’s maximumHeight is set to 24 too. Right after the removeDockWidget() is called, QMainWindow’s geometry is set back to (0,0,800,24) with setGeometry().
What I want to achieve is to resize the QMainWindow’s height to 24 when the DockWidget’s removed. The setGeometry() seems to work since width and position change accordingly, but funnily enough, the height doesn’t budge. And that’s my problem really :)
What’s the matter you think?
Here is a screen-cast illustrating the issue at hand.
NB: if i create the same scenario using a QWidget rather than QMainWindow, and using a show() or hide() on the child widget, then I can resize the parent with adjustSize() without problem: it seems the problem here above is QMainWindow specific.
Options
a) You can overload sizeHint() a virtual function. Let it return the size you want for your main window.
b) In the main window's constructor you can call setMinimumSize() and setMaximumSize() one after another, both with the the desired main window size. If you keep both same you get a fixed size.
c) Take a look at layout()->setResizeMode(Fixed).
It looks like you misunderstood the meaning of the QMainWindow.sizeHint() method.
According to QWidget.sizeHint() documentation (from which QMainWindow inherits):
This property holds the recommended size for the widget.
If the value of this property is an invalid size, no size is recommended.
The default implementation of sizeHint() returns an invalid size if there is no layout for this widget, and returns the layout's preferred size otherwise.
To get the actual size of your window, you should use the QMainWindow.geometry() method instead, which gives all the informations about the widget size and position:
win_geo = self.geometry()
win_top = win_geo.top()
win_bottom = win_geo.bottom()
win_left = win_geo.left()
win_right = win_geo.right()
win_width = win_geo.width()
win_height = win_geo.height()