how to use QGraphicsView::RubberBandDrag? - python

Can somebody please provide an explanation, or better yet a short example, of how to use the RubberBandDrag enum value in QGraphicsView? PyQt5 would be great, but I can translate from the C++ version if that is preferred for whomever can provide a helpful exmaple.
NoDrag and ScrollHandDrag are relatively easy to understand (NoDrag makes the mouse a pointer and you can capture clicks at certain locations, ScrollHandDrag makes the mouse a hand and you can implement click and drag to scroll around), but I'm unclear on what RubberBandDrag can be used for.
Before somebody says "go read the docs", here is the information provided
https://doc.qt.io/qt-5/qgraphicsview.html
enum QGraphicsView::DragMode
QGraphicsView::RubberBandDrag
A rubber band will appear. Dragging the mouse will set the rubber band
geometry, and all items covered by the rubber band are selected. This
mode is disabled for non-interactive views.
This is clear but I'm not sure how I could actually use RubberBandDrag. Is there a way to use this to drag points of a QPolygon around after initial placement? That would be really useful.

The QGraphicsView::RubberBandDrag flag only serves to activate the internal QRubberBand:
And the QRubberBand in general only aims to visualize a selected area and in the case of QGraphicsView select the items below that area if they are selectable(enable QGraphicsItem::ItemIsSelectable flag).
According to your last question: Is there a way to use this to drag points of a QPolygon around after initial placement? That would be really useful, it seems to me that you have an XY problem since it seems that the use of drag in the terminology makes you think that it serves to drag elements, because no, that drag refers to the way of creating the rubber band.
In a few moments I will show how to implement the drag of the vertices to modify the QPolygon.
The following shows how to modify the position of the vertices by dragging the mouse:
import math
from PyQt5 import QtCore, QtGui, QtWidgets
class GripItem(QtWidgets.QGraphicsPathItem):
circle = QtGui.QPainterPath()
circle.addEllipse(QtCore.QRectF(-10, -10, 20, 20))
square = QtGui.QPainterPath()
square.addRect(QtCore.QRectF(-15, -15, 30, 30))
def __init__(self, annotation_item, index):
super(GripItem, self).__init__()
self.m_annotation_item = annotation_item
self.m_index = index
self.setPath(GripItem.circle)
self.setBrush(QtGui.QColor("green"))
self.setPen(QtGui.QPen(QtGui.QColor("green"), 2))
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
self.setAcceptHoverEvents(True)
self.setZValue(11)
self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
def hoverEnterEvent(self, event):
self.setPath(GripItem.square)
self.setBrush(QtGui.QColor("red"))
super(GripItem, self).hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self.setPath(GripItem.circle)
self.setBrush(QtGui.QColor("green"))
super(GripItem, self).hoverLeaveEvent(event)
def mouseReleaseEvent(self, event):
self.setSelected(False)
super(GripItem, self).mouseReleaseEvent(event)
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isEnabled():
self.m_annotation_item.movePoint(self.m_index, value)
return super(GripItem, self).itemChange(change, value)
class PolygonAnnotation(QtWidgets.QGraphicsPolygonItem):
def __init__(self, parent=None):
super(PolygonAnnotation, self).__init__(parent)
self.m_points = []
self.setZValue(10)
self.setPen(QtGui.QPen(QtGui.QColor("green"), 2))
self.setAcceptHoverEvents(True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
self.m_items = []
def addPoint(self, p):
self.m_points.append(p)
self.setPolygon(QtGui.QPolygonF(self.m_points))
item = GripItem(self, len(self.m_points) - 1)
self.scene().addItem(item)
self.m_items.append(item)
item.setPos(p)
def movePoint(self, i, p):
if 0 <= i < len(self.m_points):
self.m_points[i] = self.mapFromScene(p)
self.setPolygon(QtGui.QPolygonF(self.m_points))
def move_item(self, index, pos):
if 0 <= index < len(self.m_items):
item = self.m_items[index]
item.setEnabled(False)
item.setPos(pos)
item.setEnabled(True)
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
for i, point in enumerate(self.m_points):
self.move_item(i, self.mapToScene(point))
return super(PolygonAnnotation, self).itemChange(change, value)
def hoverEnterEvent(self, event):
self.setBrush(QtGui.QColor(255, 0, 0, 100))
super(PolygonAnnotation, self).hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
super(PolygonAnnotation, self).hoverLeaveEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
scene = QtWidgets.QGraphicsScene()
w = QtWidgets.QGraphicsView(scene)
polygon_item = PolygonAnnotation()
scene.addItem(polygon_item)
r = 100
sides = 10
for i in range(sides):
angle = 2 * math.pi * i / sides
x = r * math.cos(angle)
y = r * math.sin(angle)
p = QtCore.QPointF(x, y) + QtCore.QPointF(200, 200)
polygon_item.addPoint(p)
w.resize(640, 480)
w.show()
sys.exit(app.exec_())

Related

Avoiding collisions of QGraphicsItem shapes moved by the mouse

An interesting discussion was raised here about preventing collisions of circles, made of QGraphicsEllipseItems, in a QGraphicsScene. The question narrowed the scope to 2 colliding items but the larger goal still remained, what about for any number of collisions?
This is the desired behavior:
When one item is dragged over other items they should not overlap, instead it should move around those items as close as possible to the mouse.
It should not “teleport” if it gets blocked in by other items.
It should be a smooth and predictable movement.
As this becomes increasingly complex to find the best “safe” position for the circle while it’s moving I wanted to present another way to implement this using a physics simulator.
Given the behavior described above it’s a good candidate for 2D rigid body physics, maybe it can be done without but it would be difficult to get it perfect. I am using pymunk in this example because I’m familiar with it but the same concepts will work with other libraries.
The scene has a kinematic body to represent the mouse and the circles are represented by static bodies initially. While a circle is selected it switches to a dynamic body and is constrained to the mouse by a damped spring. Its position is updated as the space is updated by a given time step on each timeout interval.
The item is not actually moved in the same way as the ItemIsMovable flag is not enabled, which means it no longer moves instantly with the mouse. It’s very close but there’s a small delay, although you may prefer this to better see how it reacts to collisions. (Even so, you can fine-tune the parameters to have it move faster/closer to the mouse than I did**).
On the other hand, the collisions are handled perfectly and will already support other kinds of shapes.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import pymunk
class Circle(QGraphicsEllipseItem):
def __init__(self, r, **kwargs):
super().__init__(-r, -r, r * 2, r * 2, **kwargs)
self.setFlag(QGraphicsItem.ItemIsSelectable)
self.static = pymunk.Body(body_type=pymunk.Body.STATIC)
self.circle = pymunk.Circle(self.static, r)
self.circle.friction = 0
mass = 10
self.dynamic = pymunk.Body(mass, pymunk.moment_for_circle(mass, 0, r))
self.updatePos = lambda: self.setPos(*self.dynamic.position, dset=False)
def setPos(self, *pos, dset=True):
super().setPos(*pos)
if len(pos) == 1:
pos = pos[0].x(), pos[0].y()
self.static.position = pos
if dset:
self.dynamic.position = pos
def itemChange(self, change, value):
if change == QGraphicsItem.ItemSelectedChange:
space = self.circle.space
space.remove(self.circle.body, self.circle)
self.circle.body = self.dynamic if value else self.static
space.add(self.circle.body, self.circle)
return super().itemChange(change, value)
def paint(self, painter, option, widget):
option.state &= ~QStyle.State_Selected
super().paint(painter, option, widget)
class Scene(QGraphicsScene):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.space = pymunk.Space()
self.space.damping = 0.02
self.body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
self.space.add(self.body)
self.timer = QTimer(self, timerType=Qt.PreciseTimer, timeout=self.step)
self.selectionChanged.connect(self.setConstraint)
def setConstraint(self):
selected = self.selectedItems()
if selected:
shape = selected[0].circle
if not shape.body.constraints:
self.space.remove(*self.space.constraints)
spring = pymunk.DampedSpring(
self.body, shape.body, (0, 0), (0, 0),
rest_length=0, stiffness=100, damping=10)
spring.collide_bodies = False
self.space.add(spring)
def step(self):
for i in range(10):
self.space.step(1 / 30)
self.selectedItems()[0].updatePos()
def mousePressEvent(self, event):
super().mousePressEvent(event)
if self.selectedItems():
self.body.position = event.scenePos().x(), event.scenePos().y()
self.timer.start(1000 / 30)
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
if self.selectedItems():
self.body.position = event.scenePos().x(), event.scenePos().y()
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
self.timer.stop()
def addCircle(self, x, y, radius):
item = Circle(radius)
item.setPos(x, y)
self.addItem(item)
self.space.add(item.circle.body, item.circle)
return item
if __name__ == '__main__':
app = QApplication(sys.argv)
scene = Scene(0, 0, 1000, 800)
for i in range(7, 13):
item = scene.addCircle(150 * (i - 6), 400, i * 5)
item.setBrush(Qt.GlobalColor(i))
view = QGraphicsView(scene, renderHints=QPainter.Antialiasing)
view.show()
sys.exit(app.exec_())
**Can adjust the following:
Spring stiffness and damping
Body mass and moment of inertia
Space damping
Space.step time step / how many calls per QTimer timeout
QTimer interval

Adding a subwindow into the Mainwindow

i added a draggable red circle into my main window. The red circle can be moved to anywhere you want on the main window. But i want to set a border where the red circle is allowed be move. Probably it should be done with a sub window? Anyone that has an idea how to do this? I come this far:
import sys
from PyQt5.QtWidgets import QApplication, QGraphicsView, QWidget, QGraphicsEllipseItem, QMainWindow, QGroupBox, QGraphicsScene, QHBoxLayout
from PyQt5.QtCore import Qt, QPointF, QRect
class MovingObject(QGraphicsEllipseItem):
def __init__(self, x, y, r):
#de meegegeven waardes gebruiken om beginpositie en grootte ellips te bepalen
super().__init__(0, 0, r, r)
self.setPos(x, y)
self.setBrush(Qt.red)
##mousePressEvent checkt of er wel of niet wordt geklikt
def mousePressEvent(self, event):
pass
##mouseMoveEvent is om de item te kunnen draggen
def mouseMoveEvent(self, event):
orig_cursor_position = event.lastScenePos()
updated_cursor_position = event.scenePos()
orig_position = self.scenePos()
updated_cursor_x = updated_cursor_position.x() - orig_cursor_position.x() + orig_position.x()
updated_cursor_y = updated_cursor_position.y() - orig_cursor_position.y() + orig_position.y()
self.setPos(QPointF(updated_cursor_x, updated_cursor_y))
class GraphicView(QGraphicsView):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene()
self.setScene(self.scene)
self.setSceneRect(0, 0, 60, 60)
#waardes x, y, r waarvan x en y beginpositie van ellips is en r is straal van ellips
self.scene.addItem(MovingObject(0, 0, 40))
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(800, 500, 400, 400)
self.setWindowTitle("MainWindow")
#set GraphicView in Window
self.graphicView = GraphicView()
self.setCentralWidget(self.graphicView)
app = QApplication(sys.argv)
GUI = Window()
GUI.show()
sys.exit(app.exec_())
First of all, there's no need to reimplement mouse events to allow movements of a QGraphicsItem using the mouse, as setting the ItemIsMovable flag is enough.
Once the flag is set, what is needed is to filter geometry changes and react to them.
This is achieved by setting the ItemSendsGeometryChanges flag and reimplementing the itemChange() function. Using the ItemPositionChange change, you can receive the "future" position before it's applied and eventually change (and return) the received value; the returned value is the actual final position that will be applied during the movement.
What remains is to give a reference to check it.
In the following example I'm setting the scene rect (not the view rect as you did) with bigger margins, and set those margins for the item; you obviously can set any QRectF for that.
I also implemented the drawBackground() function in order to show the scene rect that are used as limits.
class MovingObject(QGraphicsEllipseItem):
def __init__(self, x, y, r):
super().__init__(0, 0, r, r)
self.setPos(x, y)
self.setBrush(Qt.red)
self.setFlag(self.ItemIsMovable, True)
self.setFlag(self.ItemSendsGeometryChanges, True)
self.margins = None
def setMargins(self, margins):
self.margins = margins
def itemChange(self, change, value):
if change == self.ItemPositionChange and self.margins:
newRect = self.boundingRect().translated(value)
if newRect.x() < self.margins.x():
# beyond left margin, reset
value.setX(self.margins.x())
elif newRect.right() > self.margins.right():
# beyond right margin
value.setX(self.margins.right() - newRect.width())
if newRect.y() < self.margins.y():
# beyond top margin
value.setY(self.margins.y())
elif newRect.bottom() > self.margins.bottom():
# beyond bottom margin
value.setY(self.margins.bottom() - newRect.height())
return super().itemChange(change, value)
class GraphicView(QGraphicsView):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene()
self.setScene(self.scene)
self.scene.setSceneRect(0, 0, 120, 120)
self.movingObject = MovingObject(0, 0, 40)
self.scene.addItem(self.movingObject)
self.movingObject.setMargins(self.scene.sceneRect())
def drawBackground(self, painter, rect):
painter.drawRect(self.sceneRect())
Do note that the graphics view framework is as powerful as it is hard to really know and understand. Given the question you're asking ("Probably it should be done with a sub window?") it's clear that you still need to understand how it works, as using sub windows is a completely different thing.
I strongly suggest you to carefully its documentation and everything (functions and properties) related to the QGraphicsItem, which is the base class for all graphics items.
Existing properties should never be overwritten; scene() is a basic property of QGraphicsView, so you either choose another name as an instance attribute (self.myScene = QGraphicsScene()), or you just use a local variable (scene = QGraphicsScene()) and always use self.scene() outside the __init__.

Qt - Show widget or label above all widget

I want to display a loading screen every time a user presses a button (a process that takes a few seconds runs).
I want something like this
QSplashScreen does not help me because that is only used before opening the application and a QDialog is not useful for me because I want that by dragging the window the application will move along with the message Loading...
What do I have to use?
The only (safe) way to achieve this is to add a child widget without adding it to any layout manager.
The only things you have to care about is that the widget is always raised as soon as it's shown, and that the geometry is always updated to the parent widget (or, better, the top level window).
This is a slightly more advanced example, but it has the benefit that you can just subclass any widget adding the LoadingWidget class to the base classes in order to implement a loading mechanism.
from random import randrange
from PyQt5 import QtCore, QtGui, QtWidgets
class Loader(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
self.gradient = QtGui.QConicalGradient(.5, .5, 0)
self.gradient.setCoordinateMode(self.gradient.ObjectBoundingMode)
self.gradient.setColorAt(.25, QtCore.Qt.transparent)
self.gradient.setColorAt(.75, QtCore.Qt.transparent)
self.animation = QtCore.QVariantAnimation(
startValue=0., endValue=1.,
duration=1000, loopCount=-1,
valueChanged=self.updateGradient
)
self.stopTimer = QtCore.QTimer(singleShot=True, timeout=self.stop)
self.focusWidget = None
self.hide()
parent.installEventFilter(self)
def start(self, timeout=None):
self.show()
self.raise_()
self.focusWidget = QtWidgets.QApplication.focusWidget()
self.setFocus()
if timeout:
self.stopTimer.start(timeout)
else:
self.stopTimer.setInterval(0)
def stop(self):
self.hide()
self.stopTimer.stop()
if self.focusWidget:
self.focusWidget.setFocus()
self.focusWidget = None
def updateGradient(self, value):
self.gradient.setAngle(-value * 360)
self.update()
def eventFilter(self, source, event):
# ensure that we always cover the whole parent area
if event.type() == QtCore.QEvent.Resize:
self.setGeometry(source.rect())
return super().eventFilter(source, event)
def showEvent(self, event):
self.setGeometry(self.parent().rect())
self.animation.start()
def hideEvent(self, event):
# stop the animation when hidden, just for performance
self.animation.stop()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setRenderHints(qp.Antialiasing)
color = self.palette().window().color()
color.setAlpha(max(color.alpha() * .5, 128))
qp.fillRect(self.rect(), color)
text = 'Loading...'
interval = self.stopTimer.interval()
if interval:
remaining = int(max(0, interval - self.stopTimer.remainingTime()) / interval * 100)
textWidth = self.fontMetrics().width(text + ' 000%')
text += ' {}%'.format(remaining)
else:
textWidth = self.fontMetrics().width(text)
textHeight = self.fontMetrics().height()
# ensure that there's enough space for the text
if textWidth > self.width() or textHeight * 3 > self.height():
drawText = False
size = max(0, min(self.width(), self.height()) - textHeight * 2)
else:
size = size = min(self.height() / 3, max(textWidth, textHeight))
drawText = True
circleRect = QtCore.QRect(0, 0, size, size)
circleRect.moveCenter(self.rect().center())
if drawText:
# text is going to be drawn, move the circle rect higher
circleRect.moveTop(circleRect.top() - textHeight)
middle = circleRect.center().x()
qp.drawText(
middle - textWidth / 2, circleRect.bottom() + textHeight,
textWidth, textHeight,
QtCore.Qt.AlignCenter, text)
self.gradient.setColorAt(.5, self.palette().windowText().color())
qp.setPen(QtGui.QPen(self.gradient, textHeight))
qp.drawEllipse(circleRect)
class LoadingExtension(object):
# a base class to extend any QWidget subclass's top level window with a loader
def startLoading(self, timeout=0):
window = self.window()
if not hasattr(window, '_loader'):
window._loader = Loader(window)
window._loader.start(timeout)
# this is just for testing purposes
if not timeout:
QtCore.QTimer.singleShot(randrange(1000, 5000), window._loader.stop)
def loadingFinished(self):
if hasattr(self.window(), '_loader'):
self.window()._loader.stop()
class Test(QtWidgets.QWidget, LoadingExtension):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
# just a test widget
textEdit = QtWidgets.QTextEdit()
layout.addWidget(textEdit, 0, 0, 1, 2)
textEdit.setMinimumHeight(20)
layout.addWidget(QtWidgets.QLabel('Timeout:'))
self.timeoutSpin = QtWidgets.QSpinBox(maximum=5000, singleStep=250, specialValueText='Random')
layout.addWidget(self.timeoutSpin, 1, 1)
self.timeoutSpin.setValue(2000)
btn = QtWidgets.QPushButton('Start loading...')
layout.addWidget(btn, 2, 0, 1, 2)
btn.clicked.connect(lambda: self.startLoading(self.timeoutSpin.value()))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(app.exec_())
Please check Qt::WindowFlags. The Qt::SplashScreen flag will give you splash screen experience without usage QSplashScreen (you can use it with all widget as show) or, better, use QDialog with this flag.
For moving, probably fine solution is not available but you can just use parent moveEvent to emmit signal. For example:
Main window:
moveEvent -> signal moved
Dialog:
signal move -> re-center window.
Its look as not hard.
By the way, I think block all GUI during application run is not the best solution. You you think use QProgressBar?
You can use this slot: void QWidget::raise().
Raises this widget to the top of the parent widget's stack.
After this call the widget will be visually in front of any overlapping sibling widgets.

How to force screen-snip size ratio. PyQt5

I want to modify Screen-Snip code from GitHub/harupy/snipping-tool so that every screen-snip has a ratio of 3 x 2. (I will save as 600 x 400 px image later)
I'm not sure how to modify self.end dynamically so that the user clicks and drags with a 3 x 2 ratio. The mouse position will define the x coordinate, and the y coordinate will be int(x * 2/3)
Any suggestions on how to do this? I promise I've been researching this, and I just can't seem to "crack the code" of modifying only the y coordinate of self.end
Here is the code:
import sys
import PyQt5
from PyQt5 import QtWidgets, QtCore, QtGui
import tkinter as tk
from PIL import ImageGrab
import numpy as np
import cv2 # package is officially called opencv-python
class MyWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
root = tk.Tk()
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
self.setGeometry(0, 0, screen_width, screen_height)
self.setWindowTitle(' ')
self.begin = QtCore.QPoint()
self.end = QtCore.QPoint()
self.setWindowOpacity(0.3)
QtWidgets.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.CrossCursor)
)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
print('Capture the screen...')
self.show()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setPen(QtGui.QPen(QtGui.QColor('black'), 3))
qp.setBrush(QtGui.QColor(128, 128, 255, 128))
qp.drawRect(QtCore.QRect(self.begin, self.end)) ##### This seems like the place I should modify. #########
def mousePressEvent(self, event):
self.begin = event.pos()
self.end = self.begin
self.update()
def mouseMoveEvent(self, event):
self.end = event.pos()
self.update()
def mouseReleaseEvent(self, event):
self.close()
x1 = min(self.begin.x(), self.end.x())
y1 = min(self.begin.y(), self.end.y())
x2 = max(self.begin.x(), self.end.x())
y2 = max(self.begin.y(), self.end.y())
img = ImageGrab.grab(bbox=(x1, y1, x2, y2))
img.save('capture.png')
img = cv2.cvtColor(np.array(img), cv2.COLOR_BGR2RGB)
cv2.imshow('Captured Image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MyWidget()
window.show()
app.aboutToQuit.connect(app.deleteLater)
sys.exit(app.exec_())
You don't need to "change the y coordinate", you just need to use the correct arguments to create the rectangle.
There are various ways to initialize a QRect, you are using the two points, another one (and more common) is to use the coordinates of the origin and the size of the rectangle.
Once you know the width, you can compute the height, and make it negative if the y of the end point is above the begin.
Note that in this way you could get a "negative" rectangle (negative width, with the "right" edge actually at the left, the same for the height/bottom), so it's usually better to use normalized, which also allows you to get the correct coordinates of the rectangle for screen grabbing.
class MyWidget(QtWidgets.QWidget):
# ...
def getRect(self):
# a commodity function that always return a correctly sized
# rectangle, with normalized coordinates
width = self.end.x() - self.begin.x()
height = abs(width * 2 / 3)
if self.end.y() < self.begin.y():
height *= -1
return QtCore.QRect(self.begin.x(), self.begin.y(),
width, height).normalized()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setPen(QtGui.QPen(QtGui.QColor('black'), 3))
qp.setBrush(QtGui.QColor(128, 128, 255, 128))
qp.drawRect(self.getRect())
def mouseReleaseEvent(self, event):
self.close()
rect = self.getRect()
img = ImageGrab.grab(bbox=(
rect.topLeft().x(),
rect.topLeft().y(),
rect.bottomRight().x(),
rect.bottomRight().y()
))
# ...
I suggest you to use a delayed setGeometry as in some systems (specifically Linux), the "final" geometry is actually applied only as soon as the window is correctly mapped from the window manager, especially if the window manager tends to apply a geometry on its own when the window is shown the first time. For example, I have two screens, and your window got "centered" on my main screen, making it shifted by half width of the other screen.
Also consider that importing Tk just for the screen size doesn't make much sense, since Qt already provides all necessary tools.
You can use something like that:
class MyWidget(QtWidgets.QWidget):
# ...
def showEvent(self, event):
if not event.spontaneous():
# delay the geometry on the "next" cycle of the Qt event loop;
# this should take care of positioning issues for systems that
# try to move newly created windows on their own
QtCore.QTimer.singleShot(0, self.resetPos)
def resetPos(self):
rect = QtCore.QRect()
# create a rectangle that is the sum of the geometries of all available
# screens; the |= operator acts as `rect = rect.united(screen.geometry())`
for screen in QtWidgets.QApplication.screens():
rect |= screen.geometry()
self.setGeometry(rect)

Moving QSlider to Mouse Click Position

I have a QSlider that I want to move to the position of the mouse cursor when the user presses the left mouse button. I've been hunting around and couldn't find anything that was recent and solved my problem.
This is the slider I have. I want to be able to click to have the slider jump to the position where the mouse clicks. I can drag the slider, but I want to be able to click. I tested out clicking on the slider in the Dolphin file manager. It incremented rather than jumping to the exact position of the mouse.
Looking at the Qt5 documentation
QSlider has very few of its own functions [...]
This would indicate that there is no built-in way to do this. Is there no way to get where the mouse clicked and move the slider to that point?
The solution is to make a calculation of the position and set it in the mousePressEvent, the calculation is not easy as an arithmetic calculation since it depends on the style of each OS and the stylesheet so we must use QStyle as shown below:
from PyQt5 import QtCore, QtWidgets
class Slider(QtWidgets.QSlider):
def mousePressEvent(self, event):
super(Slider, self).mousePressEvent(event)
if event.button() == QtCore.Qt.LeftButton:
val = self.pixelPosToRangeValue(event.pos())
self.setValue(val)
def pixelPosToRangeValue(self, pos):
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
gr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderGroove, self)
sr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderHandle, self)
if self.orientation() == QtCore.Qt.Horizontal:
sliderLength = sr.width()
sliderMin = gr.x()
sliderMax = gr.right() - sliderLength + 1
else:
sliderLength = sr.height()
sliderMin = gr.y()
sliderMax = gr.bottom() - sliderLength + 1;
pr = pos - sr.center() + sr.topLeft()
p = pr.x() if self.orientation() == QtCore.Qt.Horizontal else pr.y()
return QtWidgets.QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), p - sliderMin,
sliderMax - sliderMin, opt.upsideDown)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QWidget()
flay = QtWidgets.QFormLayout(w)
w1 = QtWidgets.QSlider(QtCore.Qt.Horizontal)
w2 = Slider(QtCore.Qt.Horizontal)
flay.addRow("default: ", w1)
flay.addRow("modified: ", w2)
w.show()
sys.exit(app.exec_())
QSlider doesn't have such feature so the only way to implepent this is write custom widget and override mouse click in it:
class Slider(QSlider):
def mousePressEvent(self, e):
if e.button() == Qt.LeftButton:
e.accept()
x = e.pos().x()
value = (self.maximum() - self.minimum()) * x / self.width() + self.minimum()
self.setValue(value)
else:
return super().mousePressEvent(self, e)
Note that this code will work for horizontal slider only.
I believe I have a much less involved solution:
from PyQt5.QtWidgets import QSlider
class ClickSlider(QSlider):
"""A slider with a signal that emits its position when it is pressed. Created to get around the slider only updating when the handle is dragged, but not when a new position is clicked"""
sliderPressedWithValue = QSignal(int)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sliderPressed.connect(self.on_slider_pressed)
def on_slider_pressed(self):
"""emits a more descriptive signal when pressed (with slider value during the press event)"""
self.sliderPressedWithValue.emit(self.value())
And then just make sure to connect to whatever you're updating like this:
# example if you're updating a QMediaPlayer object
from PyQt5.QtMultimedia import QMediaPlayer
player = QMediaPlayer()
slider = ClickSlider()
slider.sliderPressedWithValue.connect(player.setPosition) # updates on click
slider.sliderMoved.connect(player.setPosition) # updates on drag
Try this code.
class MySliderStyle : public QProxyStyle
{
public:
virtual int styleHint(StyleHint hint, const QStyleOption * option = 0, const QWidget * widget = 0, QStyleHintReturn * returnData = 0) const{
if (hint == QStyle::SH_Slider_AbsoluteSetButtons)
{
return Qt::LeftButton;
}
else
{
return QProxyStyle::styleHint(hint, option, widget, returnData);
}
}
};
ui->mySlider->setStyle(new MySliderStyle);

Categories