2

I am trying to rebuild a screen record PyQt App, and the ScreenToGIF is a very good demo for me, it creates an interface which only has the border and record contents in the "Central Widgets", like this:

ScreenShot of ScreenToGif Software

with key functions of:

  1. The border exists and can be drag and resize by mouse
  2. the inner content is transparent
  3. the mouse click can penetrate through the app, and interact with other app beneath it.

However, it is implemented in C# (link:https://github.com/NickeManarin/ScreenToGif), I am wondering whether it possible to make a similar PyQt App without learning to be expertise about C#?

Changing the background image of QMainWidgets to the desktop area been overlayed doesn't make sense, because mouse operation on desktop (such as double click to open files) should be recorded. Mouse event can penetrate the app (like Qt.WindowTransparentForInput applied for inner contents?)

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Howcanoe Wang
  • 133
  • 1
  • 8
  • I think that you can find answer here https://stackoverflow.com/questions/33982167/pyqt5-create-semi-transparent-window-with-non-transparent-children – Grzegorz Bokota Aug 29 '19 at 21:26
  • Possible duplicate of [PyQt5: Create semi-transparent window with non-transparent children](https://stackoverflow.com/questions/33982167/pyqt5-create-semi-transparent-window-with-non-transparent-children) – Grzegorz Bokota Aug 29 '19 at 21:26
  • I update the problem to be more specific, It solves the transparent (Func2), however, it conflicts with Func1(transparency fail immediately without Qt.FramelessWindowHint but Frame is required for this function) and doesn't help with Func3 – Howcanoe Wang Aug 30 '19 at 02:27

2 Answers2

2

What you want to achieve requires setting a mask, allowing you to have a widget that has a specific "shape" that doesn't have to be a rectangle.

The main difficulty is to understand how window geometries work, which can be tricky.
You have to ensure that the window "frame" (which includes its margins and titlebar - if any) has been computed, then find out the inner rectangle and create a mask accordingly. Note that on Linux this happens "some time" after show() has been called; I think you're on Windows, but I've implemented it in a way that should work fine for both Linux, MacOS and Windows. There's a comment about that, if you're sure that your program will run on Windows only.

Finally, I've only been able to run this on Linux, Wine and a virtualized WinXP environment. It should work fine on any system, but, from my experience, there's a specific "cosmetic" bug: the title bar is not painted according to the current Windows theme. I think that this is due to the fact that whenever a mask is applied, the underlying windows system doesn't draw its "styled" window frame as it usually would. If this happens in newer systems also, there could be a workaround, but it's not easy, and I cannot guarantee that it would solve this issue.

NB: remember that this approach will never allow you to draw anything inside the "grab rectangle" (no shade, nor semi-transparent color mask); the reason for this is that you obviously need to achieve mouse interaction with what is "beneath" the widget, and painting over it would require altering the overlaying mask.

from PyQt5 import QtCore, QtGui, QtWidgets

class VLine(QtWidgets.QFrame):
    # a simple VLine, like the one you get from designer
    def __init__(self):
        super(VLine, self).__init__()
        self.setFrameShape(self.VLine|self.Sunken)


class Grabber(QtWidgets.QWidget):
    dirty = True
    def __init__(self):
        super(Grabber, self).__init__()
        self.setWindowTitle('Screen grabber')
        # ensure that the widget always stays on top, no matter what
        self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)

        layout = QtWidgets.QVBoxLayout()
        self.setLayout(layout)
        # limit widget AND layout margins
        layout.setContentsMargins(0, 0, 0, 0)
        self.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        # create a "placeholder" widget for the screen grab geometry
        self.grabWidget = QtWidgets.QWidget()
        self.grabWidget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        layout.addWidget(self.grabWidget)

        # let's add a configuration panel
        self.panel = QtWidgets.QWidget()
        layout.addWidget(self.panel)

        panelLayout = QtWidgets.QHBoxLayout()
        self.panel.setLayout(panelLayout)
        panelLayout.setContentsMargins(0, 0, 0, 0)
        self.setContentsMargins(1, 1, 1, 1)

        self.configButton = QtWidgets.QPushButton(self.style().standardIcon(QtWidgets.QStyle.SP_ComputerIcon), '')
        self.configButton.setFlat(True)
        panelLayout.addWidget(self.configButton)

        panelLayout.addWidget(VLine())

        self.fpsSpinBox = QtWidgets.QSpinBox()
        panelLayout.addWidget(self.fpsSpinBox)
        self.fpsSpinBox.setRange(1, 50)
        self.fpsSpinBox.setValue(15)
        panelLayout.addWidget(QtWidgets.QLabel('fps'))

        panelLayout.addWidget(VLine())

        self.widthLabel = QtWidgets.QLabel()
        panelLayout.addWidget(self.widthLabel)
        self.widthLabel.setFrameShape(QtWidgets.QLabel.StyledPanel|QtWidgets.QLabel.Sunken)

        panelLayout.addWidget(QtWidgets.QLabel('x'))

        self.heightLabel = QtWidgets.QLabel()
        panelLayout.addWidget(self.heightLabel)
        self.heightLabel.setFrameShape(QtWidgets.QLabel.StyledPanel|QtWidgets.QLabel.Sunken)

        panelLayout.addWidget(QtWidgets.QLabel('px'))

        panelLayout.addWidget(VLine())

        self.recButton = QtWidgets.QPushButton('rec')
        panelLayout.addWidget(self.recButton)

        self.playButton = QtWidgets.QPushButton('play')
        panelLayout.addWidget(self.playButton)

        panelLayout.addStretch(1000)

    def updateMask(self):
        # get the *whole* window geometry, including its titlebar and borders
        frameRect = self.frameGeometry()

        # get the grabWidget geometry and remap it to global coordinates
        grabGeometry = self.grabWidget.geometry()
        grabGeometry.moveTopLeft(self.grabWidget.mapToGlobal(QtCore.QPoint(0, 0)))

        # get the actual margins between the grabWidget and the window margins
        left = frameRect.left() - grabGeometry.left()
        top = frameRect.top() - grabGeometry.top()
        right = frameRect.right() - grabGeometry.right()
        bottom = frameRect.bottom() - grabGeometry.bottom()

        # reset the geometries to get "0-point" rectangles for the mask
        frameRect.moveTopLeft(QtCore.QPoint(0, 0))
        grabGeometry.moveTopLeft(QtCore.QPoint(0, 0))

        # create the base mask region, adjusted to the margins between the
        # grabWidget and the window as computed above
        region = QtGui.QRegion(frameRect.adjusted(left, top, right, bottom))
        # "subtract" the grabWidget rectangle to get a mask that only contains
        # the window titlebar, margins and panel
        region -= QtGui.QRegion(grabGeometry)
        self.setMask(region)

        # update the grab size according to grabWidget geometry
        self.widthLabel.setText(str(self.grabWidget.width()))
        self.heightLabel.setText(str(self.grabWidget.height()))

    def resizeEvent(self, event):
        super(Grabber, self).resizeEvent(event)
        # the first resizeEvent is called *before* any first-time showEvent and
        # paintEvent, there's no need to update the mask until then; see below
        if not self.dirty:
            self.updateMask()

    def paintEvent(self, event):
        super(Grabber, self).paintEvent(event)
        # on Linux the frameGeometry is actually updated "sometime" after show()
        # is called; on Windows and MacOS it *should* happen as soon as the first
        # non-spontaneous showEvent is called (programmatically called: showEvent
        # is also called whenever a window is restored after it has been
        # minimized); we can assume that all that has already happened as soon as
        # the first paintEvent is called; before then the window is flagged as
        # "dirty", meaning that there's no need to update its mask yet.
        # Once paintEvent has been called the first time, the geometries should
        # have been already updated, we can mark the geometries "clean" and then
        # actually apply the mask.
        if self.dirty:
            self.updateMask()
            self.dirty = False


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    grabber = Grabber()
    grabber.show()
    sys.exit(app.exec_())
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thank you very much! It does exactly what needed! – Howcanoe Wang Sep 01 '19 at 16:58
  • Sorry musicamante, could you check your excellent example for resizing a window by capturing the left border in the direction of the left groan. And resizing the window capturing the upper bound towards the top. And also see what happens when you click the WindowMaximizeButtonHint button. Thanks in advance. – S. Nick Sep 02 '19 at 20:32
  • @S.Nick I actually didn't create a full example that supports capturing (even if I found it interesting as was actually thinking about implementing it by myself). Btw, what do you mean by "left groan"? – musicamante Sep 02 '19 at 21:52
  • @S.Nick I just found out some strange behavior when maximizing indeed, as the down side of the window (the panel) doesn't seem to be updated correctly indeed, at least at the beginning: the *stranger thing* about the downside (nerd-pun intended) is that, after adding a QApplication.processEvents() just *once*, now it's always painted correctly (at least on Linux/Fluxbox), even after removing the processEvents and running the original code once again. I think it might depend on some sort of Xorg window state "caching", but I'm not sure, and I can't test on other platforms right now. – musicamante Sep 02 '19 at 22:30
  • @musicamante yes, I noticed that the maximize may cause the problem, so I block the maximize, otherwize I need to write complex mask calculation functions, LOL – Howcanoe Wang Sep 03 '19 at 01:17
  • @HowcanoeWang this was a very basic implementation. Obviously, if you really need full-featured screen capture, using standard window frame (including the titlebar) is not a good idea: the most important reason is that it won't allow you to get an actual *full* screen grab (as the titlebar+window frame hide underlaying contents), which is also a problem since you have to consider that on Windows and MacOS there's no easy way to move a window over the top of the desktop. I was evaluating the possibility of creating a custom grabber by myself, if I'll succeed I'll update the answer accordingly. – musicamante Sep 03 '19 at 04:27
1

please try this

from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtCore import Qt
import sys


class MainWindowExample(QMainWindow):
    def __init__(self, parent=None):
        try:
            QMainWindow.__init__(self, parent)
            self.setWindowFlags(Qt.CustomizeWindowHint | Qt.FramelessWindowHint)
            self.setStyleSheet("border: 1px solid rgba(0, 0, 0, 0.15);")
        except Exception as e:
            print(e)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    main_widow = MainWindowExample()
    main_widow.show()
    sys.exit(app.exec_())
oetzi
  • 1,002
  • 10
  • 21
  • Thanks for your suggestion, it does make a semitransparent frame, but mouse can't penetrate to control other apps beneath it. I also update the description with 3 key functions I expected – Howcanoe Wang Aug 30 '19 at 02:21