Search code examples
pythonpyqt5qtguiqtcoreqtwidgets

PyQt5 grabWindow captures black screen instead of the selected area


I have an application where I have a transparent window, I am capturing the screen underneath and then displaying the same once user release the left mouse button. But the problem is I see only black screen, I tried saving the selected screenshot but still same black screen.

Here is my code :

from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5 import QtGui as qtg
import sys

class MainWindow(qtw.QMainWindow): 
    
    def __init__(self, *arg, **kwargs):
        super().__init__()
  
        self.setWindowFlag(qtc.Qt.FramelessWindowHint)
        self.setAttribute(qtc.Qt.WA_TranslucentBackground)

        borderWidget = qtw.QWidget(objectName='borderWidget')
        self.setCentralWidget(borderWidget)
        
        bgd = self.palette().color(qtg.QPalette.Window)
        bgd.setAlphaF(.005)
        self.setStyleSheet('''
            #borderWidget {{
                border: 3px solid blue;
                background: {bgd};
            }}
        '''.format(bgd=bgd.name(bgd.HexArgb)))
        self.setGeometry(100, 100, 400, 300)
        self.showFullScreen()

        self.setCursor(qtc.Qt.CrossCursor)

        self.begin = None
        self.end = None

        self.show()

    def paintEvent(self, event):
        if self.begin:
            qpbox = qtg.QPainter(self)
            br = qtg.QBrush(qtg.QColor(100, 10, 10, 40))  
            qpbox.setBrush(br)   
            qpbox.drawRect(qtc.QRect(self.begin, self.end))  

    # close on right click
    def mouseReleaseEvent(self, QMouseEvent):
        if QMouseEvent.button() == qtc.Qt.RightButton:
            self.close()
        elif QMouseEvent.button() == qtc.Qt.LeftButton:
            screen = qtw.QApplication.primaryScreen()
            img = screen.grabWindow(self.winId(), self.begin.x(), self.end.y(), self.end.x() - self.begin.x() , self.end.y()-self.begin.y())
            img.save('screenshot.png', 'png')
            self.setStyleSheet("")
            self.central_widget = qtw.QWidget()
            label = qtw.QLabel(self)
            label.setPixmap(img)
            self.resize(img.width(), img.height())
            self.setCentralWidget(label)


    def mousePressEvent(self, QMouseEvent):
        if QMouseEvent.button() == qtc.Qt.LeftButton:
            self.begin = QMouseEvent.pos()
            self.end = QMouseEvent.pos()
            self.update()

    def mouseMoveEvent(self, QMouseEvent):
        self.end = QMouseEvent.pos()
        self.update()


if __name__ == '__main__':
    app = qtw.QApplication(sys.argv)
    w = MainWindow()
    sys.exit(app.exec_())

Solution

  • You're grabbing from the current window, not from the desktop. While what you see is the desktop (due to the transparency), specifying a window id results in grabbing only that window without considering the background composition or any other foreign window.

    If you want to grab from the screen, you need to use the root window's id, which is 0.

    Also note that:

    • the coordinates are wrong, as you used self.end for the y coordinate;
    • if the user selects a negative rectangle, the result is unexpected; you should use a normalized rectangle instead (which always have positive width and height);
    • you should hide the widget before properly taking the screenshot, otherwise you will also capture the darkened background of the capture area;
    • there's no need to always replace the central widget, just use an empty QLabel and change its pixmap;
    • an alpha value of 0.005 is practically pointless, just make it transparent;
    • the capture rectangle should be cleared after the screenshot has been taken;
    class MainWindow(qtw.QMainWindow):     
        def __init__(self, *arg, **kwargs):
            super().__init__()
      
            self.setWindowFlags(self.windowFlags() | qtc.Qt.FramelessWindowHint)
            self.setAttribute(qtc.Qt.WA_TranslucentBackground)
    
            # use a QLabel
            borderWidget = qtw.QLabel(objectName='borderWidget')
            self.setCentralWidget(borderWidget)
            
            self.setStyleSheet('''
                #borderWidget {{
                    border: 3px solid blue;
                    background: transparent;
                }}
            ''')
    
            # pointless, you're showing the window in full screen
            # self.setGeometry(100, 100, 400, 300)
    
            # variables that are required for painting must be declared *before* 
            # calling any show* function; while this is generally not an issue, 
            # as painting will actually happen "later", it's conceptually wrong
            # to declare a variable after it's (possibly) required by a function.
            self.captureRect = None
    
            self.showFullScreen()
    
            self.setCursor(qtc.Qt.CrossCursor)
    
            # unnecessary, you've already called showFullScreen
            # self.show()
    
        def paintEvent(self, event):
            if self.captureRect:
                qpbox = qtg.QPainter(self)
                br = qtg.QBrush(qtg.QColor(100, 10, 10, 40))  
                qpbox.setBrush(br)   
                qpbox.drawRect(self.captureRect)
    
        def mouseReleaseEvent(self, event):
            if event.button() == qtc.Qt.RightButton:
                self.close()
            elif event.button() == qtc.Qt.LeftButton:
                self.hide()
                screen = qtw.QApplication.primaryScreen()
                img = screen.grabWindow(0, *self.captureRect.getRect())
                self.show()
                img.save('screenshot.png', 'png')
                self.setStyleSheet('')
                self.centralWidget().setPixmap(img)
                self.captureRect = None
    
        def mousePressEvent(self, event):
            if event.button() == qtc.Qt.LeftButton:
                self.begin = event.pos()
                self.captureRect = qtc.QRect(self.begin, qtc.QSize())
    
        def mouseMoveEvent(self, event):
            self.captureRect = qtc.QRect(self.begin, event.pos()).normalized()
            self.update()
    

    Note that I changed the event handler argument: QMouseEvent is a class, and even though you're using the module (so the actual Qt class would be qtg.QMouseEvent), that might be confusing and risky if you eventually decide to directly import classes; besides, only class and constant names should have capitalized names, not variables or functions.