Mouse position in mouseMoveEvent and mousePressEvent are different: QGraphicsObject - python

Mouse position in mouseMoveEvent and mousePressEvent are different in the following example. This is happening due to the added scaling. Without scaling the positions are same.
Do I have to update the boundingRect according to the changed scaling? How?
#!/usr/bin/env python
from PyQt5.QtCore import (QRectF)
from PyQt5.QtGui import (QPainter, QPixmap)
from PyQt5.QtWidgets import (QMainWindow, QApplication, QGraphicsObject, QGraphicsView, QGraphicsScene)
class TicTacToe(QGraphicsObject):
def __init__(self, helper):
super(TicTacToe, self).__init__()
self.mypixmap = QPixmap("exit1.png")
def paint(self, painter, option, widget):
painter.setOpacity(1)
painter.drawPixmap(0,0, 512, 512, self.mypixmap)
painter.drawLine(2,2,20,20)
def boundingRect(self):
return QRectF(0,0,512, 512)
def keyPressEvent(self, event):
print "aaaaaaaaaa"
def mouseMoveEvent(self, event):
print "ccccccccccc ", event.pos()
def mousePressEvent(self, event):
print "bbbbbbbbbbbb", event.pos()
class MyGraphicsView(QGraphicsView):
def __init__(self):
super(MyGraphicsView, self).__init__()
self.scene = QGraphicsScene(self)
self.tic_tac_toe = TicTacToe(self)
self.myScale = 2
self.tic_tac_toe.setScale(self.myScale)
self.setScene(self.scene)
self.scene.addItem(self.tic_tac_toe)
self.setMouseTracking(True)
def keyPressEvent(self, event):
self.tic_tac_toe.keyPressEvent(event)
def mouseMoveEvent(self, event):
print "mouse"
self.tic_tac_toe.mouseMoveEvent(event)
class Example(QMainWindow):
def __init__(self):
super(Example, self).__init__()
self.y = MyGraphicsView()
self.setCentralWidget(self.y)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = Example()
w.show()
sys.exit(app.exec_())

The problem is caused because you are sending the QGraphicsView events to a QGraphicsObject. In the case of QGraphicsView the event is of the QMouseEvent type but in the case of QGraphicsObject it is of the QGraphicsSceneMouseEvent type. In conclusion, you should not pass the QGraphicsView events to the QGraphicsObject since they refer to different events with different information.
The mousePressEvent event is enabled by default but in the case of the mouseMoveEvent event it can not be handled by QGraphicsObject, instead you must use hoverMoveEvent but these will only work inside the boundingRect of the QGraphicsObject.
#!/usr/bin/env python
from PyQt5.QtCore import (QRectF)
from PyQt5.QtGui import (QPainter, QPixmap)
from PyQt5.QtWidgets import (QMainWindow, QApplication, QGraphicsObject, QGraphicsView, QGraphicsScene)
class TicTacToe(QGraphicsObject):
def __init__(self, helper):
super(TicTacToe, self).__init__()
self.mypixmap = QPixmap("exit1.png")
self.setAcceptHoverEvents(True)
def paint(self, painter, option, widget):
painter.setOpacity(1)
painter.drawPixmap(0,0, 512, 512, self.mypixmap)
painter.drawLine(2,2,20,20)
def boundingRect(self):
return QRectF(0,0,512, 512)
def hoverMoveEvent(self, event):
print("ccccccccccc ", event.pos())
def mousePressEvent(self, event):
print("bbbbbbbbbbbb", event.pos())
class MyGraphicsView(QGraphicsView):
def __init__(self):
super(MyGraphicsView, self).__init__()
self.scene = QGraphicsScene(self)
self.tic_tac_toe = TicTacToe(self)
self.myScale = 2
self.tic_tac_toe.setScale(self.myScale)
self.setScene(self.scene)
self.scene.addItem(self.tic_tac_toe)
class Example(QMainWindow):
def __init__(self):
super(Example, self).__init__()
self.y = MyGraphicsView()
self.setCentralWidget(self.y)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = Example()
w.show()
sys.exit(app.exec_())
On the other hand, these points do not coincide with the position in the scene since those coordinates are relative to the item.
For you to understand better we could use the following analogy, let's say you are recording a scene with a camera, the camera's screen is like the QGraphicsView, the scene is the QGraphicsScene and the actors are the QGraphicsItems and QGraphicsObjects. Each of these elements has a different coordinate system.
In the case of QGraphicsView your QMouseEvent returns coordinates in units of pixels, if you want to convert it to coordinates of the scene you must use mapToScene().
In the case of the QGraphicsItem/QGraphicsObject have different coordinates to those of the scene, those are not affected by the transformations such as scale, rotation, etc. That is what is printing in the previous example. If you want to convert it to units of the scene you must use mapToScene().
In the following example I show all the impressions in units of the scene.
#!/usr/bin/env python
from PyQt5.QtCore import (QRectF)
from PyQt5.QtGui import (QPainter, QPixmap)
from PyQt5.QtWidgets import (QMainWindow, QApplication, QGraphicsObject, QGraphicsView, QGraphicsScene)
class TicTacToe(QGraphicsObject):
def __init__(self, helper):
super(TicTacToe, self).__init__()
self.mypixmap = QPixmap("exit1.png")
self.setAcceptHoverEvents(True)
def paint(self, painter, option, widget):
painter.setOpacity(1)
painter.drawPixmap(0,0, 512, 512, self.mypixmap)
painter.drawLine(2,2,20,20)
def boundingRect(self):
return QRectF(0,0,512, 512)
def hoverMoveEvent(self, event):
#print("hoverMoveEvent ", event.pos())
print("hoverMoveEvent", self.mapToScene(event.pos()))
def mousePressEvent(self, event):
#print("mousePressEvent", event.pos())
print("mousePressEvent", self.mapToScene(event.pos()))
class MyGraphicsView(QGraphicsView):
def __init__(self):
super(MyGraphicsView, self).__init__()
self.scene = QGraphicsScene(self)
self.setMouseTracking(True)
self.tic_tac_toe = TicTacToe(self)
self.myScale = 2
self.tic_tac_toe.setScale(self.myScale)
self.setScene(self.scene)
self.scene.addItem(self.tic_tac_toe)
def mouseMoveEvent(self, event):
print("mouseMoveEvent", self.mapToScene(event.pos()))
class Example(QMainWindow):
def __init__(self):
super(Example, self).__init__()
self.y = MyGraphicsView()
self.setCentralWidget(self.y)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = Example()
w.show()
sys.exit(app.exec_())
If you want more information check the following links:
http://doc.qt.io/qt-5/graphicsview.html
http://blog.qt.io/blog/2017/01/19/should-you-be-using-qgraphicsview/

Related

How to print the file path when the mouse hovers over an item in this QTreeView with QFileSystemModel

I'm making a custom QTreeView with QFileSystem model, and I have a MouseMoveEvent set up to print the path of the item that is hovered over.
I'm way down the rabbit hole and doing all kinds of weird things to make this work.
Here is the latest minimal reproducible code:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
class MainWindow(QWidget):
def __init__(self):
QWidget.__init__(self)
self.resize(500, 300)
self.layout = QVBoxLayout()
self.setLayout(self.layout)
self.myList = CustomTreeWidget()
self.myList.model.setRootPath("/Volumes/Home/User/Desktop/testsrc")
self.myList.setObjectName("/Volumes/Home/User/Desktop/testsrc")
self.layout.addWidget(self.myList)
class CustomTreeWidget(QTreeView):
def __init__(self):
super().__init__()
self.model = QFileSystemModel()
self.model.setFilter(QDir.NoDotAndDotDot | QDir.Files)
self.setModel(self.model)
self.setAlternatingRowColors(True)
self.setDragDropMode(QAbstractItemView.DragDrop)
self.setIndentation(0)
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.setMouseTracking(True)
self.model.directoryLoaded.connect(self._runwhenloaded)
def _runwhenloaded(self):
self.setRootIndex(self.model.index(self.objectName()))
self.model.setRootPath(self.objectName())
def mouseMoveEvent(self, event):
prev = ""
if self.selectedIndexes():
prev = self.selectedIndexes()[0]
x = event.x()
y = event.y()
self.setSelection(QRect(x, y, 1, 1), QItemSelectionModel.ClearAndSelect)
self.setCurrentIndex(self.selectedIndexes()[0])
print(self.model.filePath(self.currentIndex()))
if prev:
self.setCurrentIndex(prev)
# pos = QCursor.pos()
# indexat = self.indexAt(pos).row() # why -1?
# print(indexat) # why -1?
# print(self.indexAt(pos).row())
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Obviously this example is not proper at all, as it destroys multiple selections and scrolls to the previously selected item whenever the mouse moves, and just a hack in general.
I've gone through many iterations and read everything I could put my hands on but I can't figure it out.
The closest answer seems to be HERE, but it's in C and I don't understand it.
So the question is: How to print the file path when the mouse hovers over an item in this QTreeView?
A possible solution is to create an event filter that tracks the hover events and, according to that information, emits a signal that has the QModelIndex:
import sys
from PyQt5.QtCore import (
pyqtSignal,
pyqtSlot,
Qt,
QDir,
QEvent,
QModelIndex,
QObject,
QPersistentModelIndex,
QStandardPaths,
)
from PyQt5.QtWidgets import (
QAbstractItemView,
QApplication,
QFileSystemModel,
QMainWindow,
QTreeView,
)
from PyQt5 import sip
class HoverViewHelper(QObject):
hovered = pyqtSignal(QModelIndex)
def __init__(self, view):
super().__init__(view)
self._current_index = QPersistentModelIndex()
if not isinstance(view, QAbstractItemView):
raise TypeError(f"The {view} must be of type QAbstractItemView")
self._view = view
self.view.viewport().setAttribute(Qt.WA_Hover)
self.view.viewport().installEventFilter(self)
#property
def view(self):
return self._view
def eventFilter(self, obj, event):
if sip.isdeleted(self.view):
return True
if self.view.viewport() is obj:
if event.type() in (QEvent.HoverMove, QEvent.HoverEnter):
p = event.pos()
index = self.view.indexAt(p)
self._update_index(index)
elif event.type() == QEvent.HoverLeave:
if self._current_index.isValid():
self._update_index(QModelIndex())
return super().eventFilter(obj, event)
def _update_index(self, index):
pindex = QPersistentModelIndex(index)
if pindex != self._current_index:
self._current_index = pindex
self.hovered.emit(QModelIndex(self._current_index))
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.model = QFileSystemModel()
self.model.setRootPath(QDir.rootPath())
self.view = QTreeView()
self.view.setModel(self.model)
path = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation)
self.view.setRootIndex(self.model.index(path))
self.setCentralWidget(self.view)
helper = HoverViewHelper(self.view)
helper.hovered.connect(self.handle_hovered)
#pyqtSlot(QModelIndex)
def handle_hovered(self, index):
if not index.isValid():
return
path = self.model.filePath(index)
print(f"path: {path}")
def main():
app = QApplication(sys.argv)
w = MainWindow()
w.resize(640, 480)
w.show()
app.exec_()
if __name__ == "__main__":
main()

Pyqt5 draw a line between two widgets

I am trying to use QPainter to draw a line between two widgets. If I use a simple function inside the first class it works. But, I want to create a separate class of a QPainter event, that I can call at the first class whenever I want. But, it is not working as expected. Can you help me to figure out why the QPainter class is not adding a line.
import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.okButton = QPushButton("OK")
self.cancelButton = QPushButton("Cancel")
l1 = self.okButton.pos()
l2 = self.cancelButton.pos()
# This is to call the class to draw a line between those two widgets
a = QPaint(l1.x(), l1.y(), l2.x(), l2.y(),parent=self)
vbox = QVBoxLayout()
vbox.addWidget(self.okButton)
vbox.addWidget(self.cancelButton)
self.setLayout(vbox)
self.setGeometry(300, 300, 300, 150)
self.setWindowTitle('Buttons')
self.show()
class QPaint(QPainter):
def __init__(self, x1, y1, x2, y2, parent=None):
super().__init__()
def paintEvent(self, event):
self.setPen(Qt.red)
self.drawLine(x1,y1,x2,y2)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Widgets can only be painted in the widget's paintEvent method, so if you don't want to paint it in the same class then you can use multiple inheritance. On the other hand, the initial positions you use to paint will be the positions before showing that they are 0 making no line is painted but a point so it is better to track the positions using an event filter.
import sys
from PyQt5.QtCore import QEvent
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget
class Drawer:
def paintEvent(self, event):
painter = QPainter(self)
painter.drawLine(self.p1, self.p2)
class Example(QWidget, Drawer):
def __init__(self, parent=None):
super().__init__(parent)
self.initUI()
def initUI(self):
self.okButton = QPushButton("OK")
self.cancelButton = QPushButton("Cancel")
vbox = QVBoxLayout(self)
vbox.addWidget(self.okButton)
vbox.addWidget(self.cancelButton)
self.setGeometry(300, 300, 300, 150)
self.setWindowTitle("Buttons")
self.p1, self.p2 = self.okButton.pos(), self.cancelButton.pos()
self.okButton.installEventFilter(self)
self.cancelButton.installEventFilter(self)
def eventFilter(self, o, e):
if e.type() == QEvent.Move:
if o is self.okButton:
self.p1 = self.okButton.pos()
elif o is self.cancelButton:
self.p2 = self.cancelButton.pos()
self.update()
return super().eventFilter(o, e)
if __name__ == "__main__":
app = QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())

QPropertyAnimation not working with Window Opacity

I'm setting up a new desktop widget to make my life easier at work and using QPropertyAnimation to make it pretty. Fading the app in and out doesn't seem to want to work and in typical coder fashion, it's brought my progress to a standstill.
I'm implementing QPropertyAnimation in a personalised class to make my life easier, but since it's not intially worked I've taken it back to the class code and it's still being pretty stubborn. So far I've tried.
class widget(QWidget):
def init(self):
self.setSize(QSize(300, 300))
self.setWindowOpacity(1)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground)
def paintEvent(self, event):
s = self.size()
qp = QPainter()
qp.begin(self)
qp.setRenderHint(QPainter.Antialiasing, True)
qp.setBrush(QColor().fromRgb(2,106,194))
qp.setPen(QColor().fromRgb(2,106,194))
qp.drawRoundRect(QRect(0,0, 300, 300), 16, 8)
qp.end()
def show(self):
self.superShow()
a = QPropertyAnimation(self, "windowOpacity")
a.setDuration(500)
a.setStartValue(1)
a.setEndValue(0)
a.start()
def hide(self):
a = QPropertyAnimation(self, "windowOpacity")
a.setDuration(500)
a.setStartValue(0)
a.setEndValue(1)
a.finished.connect(self.superHide)
a.start()
def superShow(self):
super(widget, self).show()
def superHide(self):
super(widget, self).hide()
No error messages at all it just hides and shows after the animation duration is over. No idea where to look or what to do to get it working. I've only been coding for like 3 months or so.
Your code has many errors, for example:
I don't see where you call init().
Animations are local variables that will be removed when the show and hide methods are finished, which is almost instantaneous.
etc.
Instead of changing the opacity directly I will use QGraphicsOpacityEffect, instead of using the show and close method, I will use the showEvent, hideEvent and closeEvent methods.
import sys
from PySide2.QtCore import QEasingCurve, QEventLoop, QPropertyAnimation, QRect, QSize, Qt
from PySide2.QtGui import QColor, QPainter
from PySide2.QtWidgets import QAction, QApplication, QGraphicsOpacityEffect, QWidget
class Widget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.resize(QSize(300, 300))
# self.setWindowOpacity(1)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setContextMenuPolicy(Qt.ActionsContextMenu)
quit_action = QAction(self.tr("E&xit"), self)
quit_action.setShortcut(self.tr("Ctrl+Q"))
quit_action.triggered.connect(self.close)
self.addAction(quit_action)
effect = QGraphicsOpacityEffect(self, opacity=1.0)
self.setGraphicsEffect(effect)
self._animation = QPropertyAnimation(
self,
propertyName=b"opacity",
targetObject=effect,
duration=500,
startValue=0.0,
endValue=1.0,
)
def paintEvent(self, event):
qp = QPainter(self)
qp.setRenderHint(QPainter.Antialiasing, True)
qp.setBrush(QColor().fromRgb(2, 106, 194))
qp.setPen(QColor().fromRgb(2, 106, 194))
qp.drawRoundedRect(QRect(0, 0, 300, 300), 16, 8)
def fade_in(self):
self._animation.setDirection(QPropertyAnimation.Forward)
self._animation.start()
def fade_out(self):
loop = QEventLoop()
self._animation.finished.connect(loop.quit)
self._animation.setDirection(QPropertyAnimation.Backward)
self._animation.start()
loop.exec_()
def showEvent(self, event):
super().showEvent(event)
self.fade_in()
def closeEvent(self, event):
# fade out
self.fade_out()
super().closeEvent(event)
def hideEvent(self, event):
# fade out
self.fade_out()
super().hideEvent(event)
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())

Cannot save the state of Qt drawing

Maybe it is insufficient knowledge of Qt or of Python, of maybe even both, but I have a problem with Qt5 in Python.
I have a script that draws a line from a point where the mouse pointer is depressed to the point where it is released. That works fine.
Then when I want to draw a second line, I want to keep the first line on the QDialog as well, but because the way paintEvent works that is not possible straight away.
There for I've created a helper class (at first it was in the MyDialog class itself) to store all the points in a list and then use this class to redraw all the lines when paintEvent redraws the QDialog.
However, this is not working because for some reason all the points stored in the helper class get overridden by the last point. So if I draw ten lines, the helper class has 10 times the last (10th) point in its list.
Below you can see the code, can somebody shine a light on this? Thank!
import sys
from PyQt5.QtWidgets import QDialog, QApplication
from PyQt5.QtGui import QPainter, QPicture
from demoDrawLine import *
class ContextTest:
test = []
class MyForm(QDialog):
picture = []
def __init__(self):
super().__init__()
self.drawing = []
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.pos1 = [0,0]
self.pos2 = [0,0]
self.show()
def paintEvent(self, event):
super().paintEvent(event)
qp = QPainter()
qp.begin(self)
qp.drawLine(self.pos1[0], self.pos1[1], self.pos2[0], self.pos2[1])
qp.end()
def mousePressEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
self.pos1[0], self.pos1[1] = event.pos().x(), event.pos().y()
def mouseReleaseEvent(self, event):
self.pos2[0], self.pos2[1] = event.pos().x(), event.pos().y()
ContextTest.test.append((self.pos1, self.pos2))
self.update()
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyForm()
w.show()
sys.exit(app.exec_())
Here some results I copied from the debugger:
1st line drawn: ContextTest.test : [([150, 335], [452, 618])]
2nd line drawn: ContextTest.test : [([311, 695], [340, 666]), ([311, 695], [340, 666])]
3rd line drawn: ContextTest.test : [([1444, 249], [1043, 712]), ([1444, 249], [1043, 712]), ([1444, 249], [1043, 712])]
etc...
You are saving the points in test but you are not using it to paint, as you realized paintEvent has no notion of the past so you will have to save those points but better than saving points would be to keep instructions that know how to paint, for example it could add other figures without needing to write a lot of code in it.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Instruction:
def paint(self, painter):
raise NotImplementedError()
class LineInstruction(Instruction):
def __init__(self, line):
self._line = line
def paint(self, painter):
painter.drawLine(self._line)
class ContextTest:
instructions = []
class MyForm(QtWidgets.QDialog):
def __init__(self):
super().__init__()
self.drawing = []
self.show()
def paintEvent(self, event):
super().paintEvent(event)
qp = QtGui.QPainter(self)
for instruction in ContextTest.instructions:
instruction.paint(qp)
def mousePressEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
self.start = event.pos()
def mouseReleaseEvent(self, event):
l = QtCore.QLine(self.start, event.pos())
instruction = LineInstruction(l)
ContextTest.instructions.append(instruction)
self.update()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = MyForm()
w.show()
sys.exit(app.exec_())

PyQt Irregularly Shaped Windows (e.g. A circular without a border/decorations)

How do I create an irregularly shaped window in PyQt?
I found this C++ solution, however I am unsure of how to do that in Python.
Here you go:
from PyQt4 import QtGui, QtWebKit
from PyQt4.QtCore import Qt, QSize
class RoundWindow(QtWebKit.QWebView):
def __init__(self):
super(RoundWindow, self).__init__()
self.initUI()
def initUI(self):
self.setWindowFlags(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
def sizeHint(self):
return QSize(300,300)
def paintEvent(self, event):
qp = QtGui.QPainter()
qp.begin(self)
qp.setRenderHint(QtGui.QPainter.Antialiasing);
qp.setPen(Qt.NoPen);
qp.setBrush(QtGui.QColor(255, 0, 0, 127));
qp.drawEllipse(0, 0, 300, 300);
qp.end()
a = QtGui.QApplication([])
rw = RoundWindow()
rw.show()
a.exec_()
Screenshot
I've never written C++ in my life, but reading that code example was not that hard. You'll find that most Qt documentation online is in C++, so it's useful to at least be able to read.
Here is a PyQT5 example, which creates frameless, movable QWidget, with transparent png mask to generate irregularly shaped Window:
from PyQt5 import QtGui, QtWidgets
from PyQt5.QtCore import Qt, QPoint
class IrregularWindow(QtWidgets.QWidget):
def __init__(self):
super(IrregularWindow, self).__init__()
self.initUI()
def initUI(self):
self.setWindowFlags(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
def sizeHint(self):
return QSize(107, 41) # Set this to the exact image resolution
def paintEvent(self, event):
qp = QtGui.QPainter()
qp.begin(self)
pixmap = QtGui.QPixmap()
pixmap.load('image_with_transparency.png')
qp.drawPixmap(QPoint(0, 0), pixmap)
qp.end()
def mousePressEvent(self, event):
self.oldPos = event.globalPos()
def mouseMoveEvent(self, event):
delta = QPoint(event.globalPos() - self.oldPos)
self.move(self.x() + delta.x(), self.y() + delta.y())
self.oldPos = event.globalPos()
a = QtWidgets.QApplication([])
rw = IrregularWindow()
rw.show()
a.exec_()

Categories