pythonpysidepyside6qt6

How to make transparent but clickable window?


I am making a screenshot snipping tool with PySide6 and I want to make an invisible frameless window to let user drag and capture screenshot. But every method I know will let the mouse click behind the window instead of on the window.

Is there any way to make the window transparent but still receives mouse input? (I made the snipping tool with Tkinter successfully, but I don't want to use tkinter)

This is my code using PySide6 (working on mac):

from PySide6 import QtWidgets, QtCore, QtGui
from PySide6.QtWidgets import QApplication, QWidget
from PySide6.QtGui import QScreen, QPixmap
import sys

using_debug_mode = None

class DraggingPanel(QWidget):
    def __init__(self, callback=None, cancel_callback=None):
        super().__init__()
        self.callback = callback
        self.cancel_callback = cancel_callback
        self.start_x = None
        self.start_y = None
        self.rect:QtCore.QRect = None
        
        window_tools.set_frameless(self)

        ###👇👇👇 just look at here is enough 👇👇👇###

        # method 1: cannot click on the window
        '''
        widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
        '''

        # method 2: cannot click on the window
        '''
        self.setStyleSheet('background-color: #00000000;')
        '''

        # method 3: when opacity <= 0.5, same as method 1, it will become totally transparent and not receiving mouse input.
        '''
        self.setWindowOpacity(0.01)
        '''

        height = screen.height()
        width = screen.width()
        self.setGeometry(0, 0, width, height)
        self.setMouseTracking(True)

    def paintEvent(self, event):
        if self.rect:
            painter = QtGui.QPainter(self)
            painter.setPen(QtGui.QPen(QtGui.Qt.white, 2))
            painter.drawRect(self.rect)

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            self.start_x = event.globalPosition().x()
            self.start_y = event.globalPosition().y()
            self.rect = QtCore.QRect(self.start_x, self.start_y, 0, 0)

    def mouseMoveEvent(self, event):
        if event.buttons() & QtCore.Qt.LeftButton:
            self.rect.setBottomRight(event.globalPosition().toPoint())
            self.update()

    def mouseReleaseEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            self.capture_screen()

    def capture_screen(self):
        xs = [self.rect.x(), self.rect.x() + self.rect.width()]
        ys = [self.rect.y(), self.rect.y() + self.rect.height()]
        xs.sort()
        ys.sort()
        x1, x2 = xs
        y1, y2 = ys
        w = x2-x1
        h = y2-y1
        self.hide()

        if self.rect and w > 0 and h > 0:
            def do_capture():
                screen_cap = QScreen.grabWindow(QApplication.primaryScreen(), 0, 0, 
                                                screen.width(),
                                                screen.height())
                to_img_scale_x = lambda val: round(val * screen_cap.width()/screen.width())
                to_img_scale_y = lambda val: round(val * screen_cap.height()/screen.height())
                screen_cap = screen_cap.copy(to_img_scale_x(x1),
                                                to_img_scale_y(y1),
                                                to_img_scale_x(w),
                                                to_img_scale_y(h)) if True else screen_cap
                if using_debug_mode: print("Screenshot taken!")
                QtCore.QTimer.singleShot(1, lambda: self.callback(screen_cap) and self.deleteLater())
            QtCore.QTimer.singleShot(1, do_capture)
        else:
            if using_debug_mode: print("Screenshot canceled!")
            QtCore.QTimer.singleShot(1, lambda: self.cancel_callback() and self.deleteLater())

        





### Tools ###

class window_tools:
    @staticmethod
    def set_flag(widget:QWidget, flag: QtCore.Qt, on=True):
        # Get current flags and remove the FramelessWindowHint flag
        widget.setWindowFlag(flag, on=on)
    @staticmethod
    def set_no_entity_mode(widget:QWidget, transparent_no_enitity=True, frameless = True):
        "the mouse can click behind the window."
        window_tools.set_flag(widget, QtCore.Qt.FramelessWindowHint, on=frameless)
        widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, on=transparent_no_enitity)
    def set_bg_to_almost_transparent(widget):
        "Opacity: 1/255, Clickable\n\n`widget.setStyleSheet('background-color: #01000000;')`"
        widget.setStyleSheet('background-color: #01000000;')
    @staticmethod
    def set_frameless(widget, on = True):
        window_tools.set_flag(widget, QtCore.Qt.FramelessWindowHint, on=on)
    @staticmethod
    def set_always_on_top(widget, on = True):
        window_tools.set_flag(widget, QtCore.Qt.WindowStaysOnTopHint, on=on)
    @staticmethod
    def create_window(title="", borderless=False, always_on_top=False, width=300, height=300):
        # Create a new QWidget (or QMainWindow)
        new_window = QWidget()
        new_window.setWindowTitle(title)
        
        # Set window flags based on parameters
        new_window.setWindowFlag(QtCore.Qt.FramelessWindowHint, borderless)
        new_window.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, always_on_top)
        
        # Set the size of the window
        new_window.resize(width, height)
        
        return new_window

class screen:
    @staticmethod
    def size():
        return app_using.primaryScreen().size()
    @staticmethod
    def width():
        return screen.size().width()
    @staticmethod
    def height():
        return screen.size().height()
    

### main function ###

app_using: QApplication = None
def launch_screenshot_panel(Qapp: QApplication, on_screenshot=None, on_cancel=None, debug_logging=False):
    "Please STORE the returned widget to prevent garbage collection"
    global using_debug_mode, app_using
    using_debug_mode = debug_logging
    app_using = Qapp
    panel = DraggingPanel(on_screenshot, on_cancel)
    panel.show()
    return panel
    


if __name__ == "__main__":
    if not QtWidgets.QApplication.instance():
        app = QtWidgets.QApplication(sys.argv)
    else:
        app = QtWidgets.QApplication.instance()
    
    def on_screenshot(img: QPixmap):
        img.save("a.png")
        quit()
    def on_screenshot_cancelled():
        print("cancelled callback!")
    panel = launch_screenshot_panel(app, on_screenshot=on_screenshot, on_cancel=on_screenshot_cancelled, debug_logging=True)

    sys.exit(app_using.exec())

Solution

  • I figured out a solution, although not the most elegant.

    Just use style sheet to set the backgound color opacity to '01':

    window.setStyleSheet('background-color: #01000000;')
    

    Hide the window during the capturing process.