4

1. Intro

I'm working in Python 3.7 on Windows 10 and use PyQt5 for the GUI. In my application, I got a QScrollArea() with an array of buttons inside. When clicked, a button has to move outside the area. I use a QPropertyAnimation() to show the movement.

 

2. Minimal, Reproducible Example

I've created a small application for testing. The application shows a small QScrollArea() with a bunch of buttons inside. When you click on a button, it will move to the right:

enter image description here

Here is the code:

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys

class MyButton(QPushButton):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setFixedWidth(300)
        self.setFixedHeight(30)
        self.clicked.connect(self.animate)
        return

    def animate(self):
        self.anim = QPropertyAnimation(self, b'position')
        self.anim.setDuration(3000)
        self.anim.setStartValue(QPointF(self.pos().x(), self.pos().y()))
        self.anim.setEndValue(QPointF(self.pos().x() + 200, self.pos().y() - 20))
        self.anim.start()
        return

    def _set_pos_(self, pos):
        self.move(pos.x(), pos.y())
        return

    position = pyqtProperty(QPointF, fset=_set_pos_)


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()
        return

if __name__== '__main__':
    app = QApplication(sys.argv)
    QApplication.setStyle(QStyleFactory.create('Plastique'))
    myGUI = CustomMainWindow()
    sys.exit(app.exec_())

 

3. The problem

When the button moves, it stays in the QScrollArea(). I would need to get it on top of everything:

enter image description here

 

4. Solution (almost)

Thank you @magrif for pointing me in the right direction. Thanks to your suggestions, I got something working.

So I changed the animate() function into this:

    def animate(self):
        self.anim = QPropertyAnimation(self, b'position')
        self.anim.setDuration(3000)
        startpoint = self.mapToGlobal(self.pos())
        endpoint = self.mapToGlobal(QPoint(self.pos().x() + 200, self.pos().y() - 20))
        self.setWindowFlags(Qt.Popup)
        self.show()
        self.anim.setStartValue(QPointF(startpoint.x(), startpoint.y()))
        self.anim.setEndValue(QPointF(endpoint.x(), endpoint.y()))
        self.anim.start()
        QTimer.singleShot(1000, self.hide)
        return

Note that I install a single-shot timer to hide() the button after one second. That's because the Qt eventloop is blocked as long as this button behaves as a "popup" (because of self.setWindowFlags(Qt.Popup)). Anyway, the one-shot timer works good enough for me.

Unfortunately I got one issue left. When I click on the first button Foo, it starts its animation (almost) from where it was sitting initially. If I click on one of the other buttons - like Baz - it suddenly jumps down about 100 pixels and starts its animation from there.

I think this has something to do with the startpoint = self.mapToGlobal(self.pos()) function and the fact that those buttons are sitting in a QScrollArea(). But I don't know how to fix this.

 

5. Objective

My purpose is to build a rightmouse-click-menu like this:

enter image description here

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.

Note: The following question related to this topic is this one:
Qt: How to perform a drag-and-drop without holding down the mouse button?

K.Mulier
  • 8,069
  • 15
  • 79
  • 141

2 Answers2

3

The position of a widget is relative to its parent, so you should not use startpoint = self.mapToGlobal(self.pos()), but startpoint = self.mapToGlobal(QPoint()), since for the widget the position of the topLeft is the QPoint(0, 0).

So if you want to use the @magrif solution you should change it to:

def animate(self):
    startpoint = self.mapToGlobal(QPoint())
    self.setWindowFlags(Qt.Popup)
    self.show()
    anim = QPropertyAnimation(
        self,
        b"pos",
        self,
        duration=3000,
        startValue=startpoint,
        endValue=startpoint + QPoint(200, -20),
        finished=self.deleteLater,
    )
    anim.start()

But the drawback is that while the animation is running you can not interact with the window.

Another solution is to change the parent of the QFrame to the window itself:

def animate(self):
    startpoint = self.window().mapFromGlobal(self.mapToGlobal(QPoint()))
    self.setParent(self.window())
    anim = QPropertyAnimation(
        self,
        b"pos",
        self,
        duration=3000,
        startValue=startpoint,
        endValue=startpoint + QPoint(200, -20),
        finished=self.deleteLater,
    )
    anim.start()
    self.show()

Note: it is not necessary to create the qproperty position since the qproperty pos already exists.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Amazing! This works like a charm. Thank you so much for your efforts. I've donated a couple of coffees ;-) – K.Mulier May 22 '19 at 18:33
  • Hi @eyllanesc, I got a related question here: https://stackoverflow.com/questions/56263595/qt-how-to-perform-a-drag-and-drop-without-holding-down-the-mouse-button Could you please have a look? Thank you so much :-) – K.Mulier May 22 '19 at 19:21
  • I had the exact same problem, and to clarify for anyone confused about this: self.mapToGlobal(P) means that if you're standing in the top left of your widget (0,0) and point to a position P (represented by a QPoint(x, y) widget), then by calling this function you will get that position in global coordinates (the coordinates of your actual screen). Calling "this->pos()" will get your widget position in your PARENT coordinate system. Calling self.mapToGlobal(this->pos()) will then return a position (in global coordinates) "this->pos()" off-set from the position of your widget. – Postermaestro Mar 15 '22 at 13:36
1
  1. Calculate global coordinates for button using mapToGlobal().

  2. Set flag Qt.Popup using setWindowFlags() and show()

  3. Init start and end values relate global coordinates from (1), and start animation

ps At C++ at works :)

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
magrif
  • 396
  • 4
  • 20
  • Hi @eyllanesc and magrif, thank you for pointing me in the right direction. I got something working, but there's still an issue. Please read the paragraph I just added to my question for more details. Thank you so much ^_^ – K.Mulier May 22 '19 at 16:07
  • @K.Mulier I have only edited the question, I have not contributed to the answer itself. – eyllanesc May 22 '19 at 16:19