简体   繁体   中英

How to add an invisible line in PyQt5

在此处输入图像描述

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:

  1. The following will only work if applied to the topmost widget on which the "invisible line" must be applied.
  2. 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM