1. Objective
My purpose is to build a rightmouse-click-menu like this:
When the user clicks on Grab and move, the button should disappear from the QScrollArea()
and move quickly towards the mouse. When it arrives at the mouse pointer, the button should fade out and the drag-and-drop operation can start.
2. Minimal, Reproducible Example
I got something working, but it isn't perfect yet. Please copy-paste the code below and run it with Python 3.x (I use Python 3.7) and PyQt5.
Note: To make the line
pixmap = QPixmap("my_pixmap.png")
work properly, let it refer to an existing png-image on your computer.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
class MyButton(QPushButton):
'''
A special push button.
'''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedWidth(300)
self.setFixedHeight(30)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.showMenu)
return
def showMenu(self, pos):
'''
Show this popup menu when the user clicks with the right mouse button.
'''
menu = QMenu()
menuAction_01 = menu.addAction("action 01")
menuAction_02 = menu.addAction("action 02")
menuAction_03 = menu.addAction("action 03")
menuAction_04 = menu.addAction("action 04")
menuAction_grab = menu.addAction("grab")
action = menu.exec_(self.mapToGlobal(pos))
if action == menuAction_01:
print("clicked on action 01")
elif action == menuAction_02:
print("clicked on action 02")
elif action == menuAction_03:
print("clicked on action 03")
elif action == menuAction_04:
print("clicked on action 04")
elif action == menuAction_grab:
print("clicked on grab")
# 1. Start animation
# -> button moves to mouse pointer
self.animate()
# 2. After animation finishes (about 1 sec)
# -> start drag operation
QTimer.singleShot(1000, self.start_drag)
return
def animate(self):
'''
The button removes itself from the QScrollArea() and flies to the mouse cursor.
For more details, see the anser of @eyllanesc at
https://stackoverflow.com/questions/56216698/how-display-a-qpropertyanimation-on-top-of-the-qscrollarea
'''
startpoint = self.window().mapFromGlobal(self.mapToGlobal(QPoint()))
endpoint = self.window().mapFromGlobal(QCursor.pos())
self.setParent(self.window())
anim = QPropertyAnimation(
self,
b"pos",
self,
duration=1000,
startValue=startpoint,
endValue=endpoint,
finished=self.hide,
)
anim.start()
self.show()
return
def start_drag(self):
'''
Start the drag operation.
'''
drag = QDrag(self)
pixmap = QPixmap("my_pixmap.png")
pixmap = pixmap.scaledToWidth(100, Qt.SmoothTransformation)
drag.setPixmap(pixmap)
mimeData = QMimeData()
mimeData.setText("Foobar")
drag.setMimeData(mimeData)
dropAction = drag.exec(Qt.CopyAction | Qt.MoveAction)
return
class CustomMainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(100, 100, 600, 300)
self.setWindowTitle("ANIMATION TEST")
# OUTER FRAME
# ============
self.frm = QFrame()
self.frm.setStyleSheet("""
QFrame {
background: #d3d7cf;
border: none;
}
""")
self.lyt = QHBoxLayout()
self.frm.setLayout(self.lyt)
self.setCentralWidget(self.frm)
# BUTTON FRAME
# =============
self.btn_frm = QFrame()
self.btn_frm.setStyleSheet("""
QFrame {
background: #ffffff;
border: none;
}
""")
self.btn_frm.setFixedWidth(400)
self.btn_frm.setFixedHeight(200)
self.btn_lyt = QVBoxLayout()
self.btn_lyt.setAlignment(Qt.AlignTop)
self.btn_lyt.setSpacing(5)
self.btn_frm.setLayout(self.btn_lyt)
# SCROLL AREA
# ============
self.scrollArea = QScrollArea()
self.scrollArea.setStyleSheet("""
QScrollArea {
border-style: solid;
border-width: 1px;
}
""")
self.scrollArea.setWidget(self.btn_frm)
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setFixedWidth(400)
self.scrollArea.setFixedHeight(150)
self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.lyt.addWidget(self.scrollArea)
# ADD BUTTONS TO BTN_LAYOUT
# ==========================
self.btn_lyt.addWidget(MyButton("Foo"))
self.btn_lyt.addWidget(MyButton("Bar"))
self.btn_lyt.addWidget(MyButton("Baz"))
self.btn_lyt.addWidget(MyButton("Qux"))
self.show()
self.setAcceptDrops(True)
return
def dropEvent(self, event):
event.acceptProposedAction()
print("dropEvent at {0!s}".format(event))
return
def dragLeaveEvent(self, event):
event.accept()
return
def dragEnterEvent(self, event):
event.acceptProposedAction()
return
if __name__== '__main__':
app = QApplication(sys.argv)
QApplication.setStyle(QStyleFactory.create('Plastique'))
myGUI = CustomMainWindow()
sys.exit(app.exec_())
Run the script and you will see a small window with a few buttons in a QScrollArea()
:
STEP 1: Click on one of the buttons with your right mouse button. You should see a popup menu. Click on "grab".
STEP 2: The button moves to your mouse pointer. Don't move the mouse pointer.
STEP 3: As soon as your mouse pointer is over the button (don't move the mouse, wait for the button to arrive), click and hold the mouse button down.
STEP 4: Now move the mouse (while holding the mouse button down). You should be in a drag-and-drop operation, with the pixmap locked to your mouse!
Okay, it works, but there are a few downsides.
3. Problem
At the end of the animation, the flying button is under your mouse pointer. But if you move your mouse pointer a tiny bit, the button disappears and you miss the drag-and-drop operation.
In other words, what I got now is not very robust. The user can easily miss the drag-and-drop operation.
NOTE: Apparently the problem I describe here only appears on Windows (not on Linux). But I got to make this thing work on Windows...
4. Potential solution
I believe the following approach would be better, and still intuitive to the user:
As soon as the button arrives under the mouse pointer (the end of the animation), the button fades away. The drag-and-drop operation starts automatically, without the need to click and hold down the mouse button. The drag continues while you move the mouse pointer, until you click somewhere. That mouse press is the dropEvent()
.
Do you know how to implement this? Or perhaps you have another approach in mind?
5. Notes
My question is actually the sequel of this one:
How display a QPropertyAnimation() on top of the QScrollArea()?
Thank you @eyllanesc for solving that one ^_^