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())
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.