How to get cursor location within window in PyQt5/PySide2? - python

Questions and Briefing
Hello. I am trying to fix a problem with my PyQt5 window which has a custom Title Bar. If you maximize the window and you hover over the title bar with the cursor and drag the window to move it, the window will resize and you will be able to move it as normal.
However, when the window restores previous state before it is maximized using the cursor click and drag method, the cursor does not grip onto the location on the title bar to move the window, but, it stays in the same place where the click and drag method was started so the cursor is on the other side of the screen mysteriously moving the window.
How would I make it so the cursor would grip onto the title bar and when the window is restored using the click and drag method it would be locked on to that same location, the same as you would have for normal applications.
Sub-Questions
How would I get the location of the cursor inside the window? I have managed to get the cursor location using QCursor.pos() but that returns the global position on the actual screen but I think I need it on the window so I can try to make it so the cursor grips onto the title bar.
I have tried to...
Use QCursor.pos() before the click and drag restoration method is done and then I did QCursor.setPos() to set the previously captured position, it does work but then it causes the window to move with it and the cursor is also out of position from the intended grip location inside the window.
Code
class MainWindow(QMainWindow): # Main Window
def __init__(self): # Main initialisation
super(MainWindow, self).__init__() # Call superclass
# UI Setup
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# Move Window
def move_window(event):
# Restore Window (before movement)
if UIFunctions.return_status(self) == 1: # If maximized
UIFunctions.maximize_restore(self)
# QCursor.setPos(self.previous_cursor_pos)
if event.buttons() == Qt.LeftButton: # If Left Click (move window)
self.move(self.pos() + event.globalPos() - self.dragPos)
self.dragPos = event.globalPos()
event.accept()
# Set Title Bar
self.ui.title_bar.mouseMoveEvent = move_window
# Set UI Definitions
UIFunctions.ui_definitions(self)
self.show() # Show main window
def mousePressEvent(self, event): # Mouse Press Event
self.dragPos = event.globalPos()
(Main Class with Move Window method)
Other Info
Python Version: Python 3.8.6
Module Used: PySide2
Happy to provide more information.

musicmante's solution in the comments fixed my issue. I was trying to use something like self.pos.x() and after I switched it to self.mapFromGlobal(QCursor.pos()).x()

Related

QToolButton with popup QSlider

I'm trying to make volume button, on click it should mute/unmute and and on hover it should popup QSlider, so user can set whatever level he wants. Now I'm trying to achieve this by showing slider window in enterEvent and hiding it in leaveEvent:
class VolumeButton(QToolButton):
def __init__(self, parent=None):
super().__init__(parent)
self.setIcon(volumeicon)
self.slider = QSlider()
self.slider.setWindowFlags(Qt.FramelessWindowHint)
self.slider.setWindowModality(Qt.NonModal)
def enterEvent(self, event):
self.slider.move(self.mapToGlobal(self.rect().topLeft()))
self.slider.show()
def leaveEvent(self, event):
self.slider.hide()
The problem is that mapToGlobal seems to be connected in some way with enterEvent and it creates recursion, but without mapToGlobal I can't place slider at the right position.
I'm not sure that QToolButton and FramelessWindow are the right widgets to achieve wished result, so let me know if there a better ways to do that.
The problem is not from mapToGlobal, but from the fact that the leaveEvent is fired as soon as the slider is shown: since the slider is in the same coordinates of the mouse, Qt considers that the mouse has "left" the button (and "entered" the slider).
You cannot use the simple leaveEvent for this, as you need to check the cursor position against both the button and the slider.
A possible solution is to create a QRegion that contains the geometry of both widgets and check if the cursor is inside it. In order to process the mouse events of the slider, an event filter must be installed on it:
class VolumeButton(QToolButton):
def __init__(self, parent=None):
super().__init__(parent)
self.setIcon(volumeicon)
self.slider = QSlider()
self.slider.setWindowFlags(Qt.FramelessWindowHint)
self.slider.setWindowModality(Qt.NonModal)
self.slider.installEventFilter(self)
def isInside(self):
buttonRect = self.rect().translated(self.mapToGlobal(QPoint()))
if not self.slider.isVisible():
return QCursor.pos() in buttonRect
region = QRegion(buttonRect)
region |= QRegion(self.slider.geometry())
return region.contains(QCursor.pos())
def enterEvent(self, event):
if not self.slider.isVisible():
self.slider.move(self.mapToGlobal(QPoint()))
self.slider.show()
def leaveEvent(self, event):
if not self.isInside():
self.slider.hide()
def eventFilter(self, source, event):
if source == self.slider and event.type() == event.Leave:
if not self.isInside():
self.slider.hide()
return super().eventFilter(source, event)
Note that self.rect() is always at coordinates (0, 0), so you can just use self.mapToGlobal(QPoint()) to get the widget's global position. On the other hand, you might want to show the slider "outside" the button, so you could use self.mapToGlobal(self.rect().bottomLeft()).
Be aware that trying to place the slider "outside" the button geometry might result in a problem if the user moves the mouse in the gap between the button and the slider; in that case, you will need to create a valid region that covers both widgets and a reasonable space between them.

How to take action on clicking QPushButton [duplicate]

I'm having a problem updating the icon of a button set with QToolButton. The idea is to use the button for a movie player. To play, one presses the button and the icon changes to pause. When pressed again, play is paused and the icon reverts to play. I have some working code, but the problem is that the icon is not updating consistently. If I keep the Qt window in focus, it takes one or two button presses to change the icon to the intended image, by which time the actual image is not the intended image (swapped play/pause).
Here is some minimal example code:
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QWidget, QStyle, QToolButton
class Widget(QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent=parent)
self.play_button = QToolButton(clicked=self.update_button)
self.play_button.setIcon(self.style().standardIcon(QStyle.SP_MediaStop))
self.verticalLayout = QVBoxLayout(self)
self.verticalLayout.addWidget(self.play_button)
self.button_pressed = False
def update_button(self):
if self.button_pressed:
self.play_button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
self.button_pressed = False
print("Button should be set to PLAY. Press is", self.button_pressed)
else:
self.play_button.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
self.button_pressed = True
print("Button should be set to PAUSE. Press is", self.button_pressed)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
In the above I start with a stop icon just to be sure to observe a change (any click should always change the icon). Keeping the window focused, I get the following output:
1st click: "Button should be set to PAUSE. Press is True" (no change in icon)
2nd click: "Button should be set to PLAY. Press is False" (icon changes to pause)
3rd click: "Button should be set to PAUSE. Press is True" (icon changes to play)
(and so on, continues swapping as intended)
I've also noticed that if after each click, I click outside the Qt window, or resize the Qt window, the button icon updates to the correct one. What am I doing wrong? How do I force the icon to update?
This behaviour happens mostly with QToolButton, but QPushButton also give issues (works when focused, but misbehaves/loses track of correct status if I resize the Qt window). Using PyQt 5.12.3 and qt 5.12.5 on macOS.
Seems like this issue is a bug in the Qt implementation for macOS. I tested and it happens with both PyQt5 and PySide2, so it must come from Qt. Forcing a redraw with a call to .repaint() after .setIcon() seems to make the problem go away:
self.play_button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
self.play_button.repaint()

Is it possible to create QMainWindow with only outer border?

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:
with key functions of:
The border exists and can be drag and resize by mouse
the inner content is transparent
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?)
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_())
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_())

QGraphicsView behaving differently when mouse is held over a QGraphicsProxyWidget

I have a pyside2 GUI that uses QGraphicsView.
I use setDragMode(QGraphicsView.ScrollHandDrag) to make the view dragable, but i override the cursor with viewport().setCursor(Qt.ArrowCursor) on mouseReleaseEvent to avoid constantly having the open-hand in stead of the normal arrow cursor.
This is explained here: Changing the cursor in a QGraphicsView (in c++)
In the GUI there is also a QGraphicsProxyWidget with a QLabel. When the mouse is placed over the ProxyWidget, the viewport().setCursor(Qt.ArrowCursor) does not work (the moseReleaseEvent is called, so i know that setCursor is called), and when the mouse leaves the ProxyWidget, the open-hand cursor shows in stead of the arrow-cursor.
When the mouse is placed all other places in the QGraphicsView everything is working as expected.
Does anyone know why setCursor is behaving differently when the mouse is placed over a proxyWidget?
In QGraphicsView:
def mouseReleaseEvent(self, event):
QGraphicsView.mouseReleaseEvent(self, event)
self.viewport().setCursor(Qt.ArrowCursor)
def infoBoxShow(self, edge, mouse_pos):
if self.info_box is None:
self.info_box = VardeInfoBox_v2.InfoBox()
self.info_box.corresponding_edge = edge
self.info_box.setPos(mouse_pos)
self.info_box.setInfoText(edge)
self.main_scene.addItem(self.info_box)
InfoBox (As you can see i have tried to set some flags without success):
class InfoBox(QGraphicsItem):
Type = QGraphicsItem.UserType + 1
def __init__(self):
QGraphicsItem.__init__(self)
self.setFlag(QGraphicsItem.hover)
self.setZValue(4)
proxy = QGraphicsProxyWidget(self)
widget = QLabel("TEST!")
widget.setAttribute(Qt.WA_TransparentForMouseEvents)
widget.setWindowModality(Qt.NonModal)
proxy.setWidget(widget)
self.corresponding_edge = None

pyqtgraph Lock dock layout

Is there a way to lock docks in pyqtgraph so that the user cannot move them around?
I'm using a small touchscreen to display a pyqtgraph application with multiple docks. It is very easy for the user to accidentally move a dock. When that happens the screen becomes unusable because of the size. I would like to prevent the user from moving the docks.
However, the user must still be able to choose between docks (i.e. treat them like a tab widget).
Just to be clear, I want to prevent a dock from being detached and I want to prevent the dock from being drug to a new location.
Thanks,
Chris
I managed to disable the ability to detach and drag docks by overriding the Dock class' methods.
Dragging a dock moves it to another location. So I overrode all of the 'drag' event handlers with methods that do nothing (i.e. a no-op).
Double-clicking on a dock's label will cause the dock to detach. So, I overrode the dock's label's double-click event handler with a no-op.
Replace Dock with MyDock in your code. UPDATE: I added code to override the drag methods for the DockArea too because I was still able to move DockAreas around.
Here is the code:
##
# This class is used to eliminate a standard Dock class' ability to detach and
# move (i.e. dragging this Dock will have no effect)
#
class MyDock(Dock):
def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True):
# Initialize the baseclass
#
Dock.__init__(self, name, area, size, widget, hideTitle, autoOrientation)
# Override the label's double click event. Normally double clicking
# the dock's label will cause it to detach into it's own window.
#
self.label.mouseDoubleClickEvent=self.noopEvent
def dragEventEnter(self, ev):
pass
def dragMoveEvent(self, ev):
pass
def dragLeaveEvent(self, ev):
pass
def dragDropEvent(self, ev):
pass
def noopEvent(self,ev):
pass
class MyDockArea(DockArea):
def dragEventEnter(self, ev):
pass
def dragMoveEvent(self, ev):
pass
def dragLeaveEvent(self, ev):
pass
def dragDropEvent(self, ev):
pas

Categories

Resources