如何在不按住鼠标键的情况下执行拖放操作?

时间:2019-05-22 19:19:58

标签: python python-3.x pyqt pyqt5

1。目标

我的目的是建立一个像这样的鼠标右键单击菜单:

enter image description here

当用户点击抓取并移动时,该按钮应从QScrollArea()中消失,并迅速移向鼠标。当它到达鼠标指针时,该按钮应淡出,并且拖放操作可以开始。


2。最小的,可重现的示例

我有一些工作要做,但还不是很完美。请复制粘贴以下代码,然后使用 Python 3.x (我使用Python 3.7)和 PyQt5 来运行它。

  

注意:要使pixmap = QPixmap("my_pixmap.png")行正常工作,请使其引用计算机上的现有png图像。

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

运行脚本,您将在QScrollArea()中看到一个带有几个按钮的小窗口:

步骤1:用鼠标右键单击其中一个按钮。您应该会看到一个弹出菜单。点击“抓取”。

步骤2:该按钮移至鼠标指针。不要移动鼠标指针。

步骤3:一旦鼠标指针悬停在按钮上方(不要移动鼠标,等待按钮到达),请单击并按住鼠标按钮。

步骤4:现在移动鼠标(按住鼠标按钮的同时)。您应该在拖放操作中,将像素图锁定在鼠标上!

enter image description here

好的,可以,但是有一些缺点。


3。问题

动画结束时,飞行按钮位于鼠标指针下方。但是,如果将鼠标指针稍微移动一点,该按钮就会消失,并且会错过拖放操作。
换句话说,我现在得到的不是很可靠。用户可以轻松地错过拖放操作。

注意:显然,我在此描述的问题仅出现在Windows(而不是Linux)上。但是我必须使这个东西在Windows上可以工作...


4。可能的解决方案

我相信以下方法会更好,并且对用户仍然直观:

一旦按钮到达鼠标指针下方(动画的结尾),按钮就会消失。 拖放操作将自动启动,而无需单击并按住鼠标按钮。在移动鼠标指针时,拖动将继续,直到单击某处。鼠标按下的是dropEvent()

您知道如何实现吗?也许您在想另一种方法?


5。笔记

我的问题实际上是这个问题的续集: How display a QPropertyAnimation() on top of the QScrollArea()?
谢谢@eyllanesc解决了一个^ _ ^

1 个答案:

答案 0 :(得分:1)

1。解决方案

在提出解决方案之前,我要感谢@eyllanesc先生为我提供的帮助。没有他的帮助,我现在将没有解决方案。

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

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)
        self.dragStartPosition = 0
        self.set_style(False)
        return

    def set_style(self, blink):
        if blink:
            background = "#d3d7cf"
        else:
            background = "#2e3436"
        self.setStyleSheet(f"""
            QPushButton {{
                /* white on red */
                background-color:{background};
                color:#ffffff;
                border-color:#888a85;
                border-style:solid;
                border-width:1px;
                border-radius: 6px;
                font-family:Courier;
                font-size:10pt;
                padding:2px 2px 2px 2px;
            }}
        """)
        self.update()
        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")
            # Start animation -> button moves to mouse pointer
            self.animate()
        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
        '''
        def start():
            startpoint = self.window().mapFromGlobal(self.mapToGlobal(QPoint()))
            endpoint = self.window().mapFromGlobal(QCursor.pos() - QPoint(int(self.width()/2), int(self.height()/2)))
            self.setParent(self.window())
            anim = QPropertyAnimation(
                self,
                b"pos",
                self,
                duration=500,
                startValue=startpoint,
                endValue=endpoint,
                finished=blink,
            )
            anim.start()
            self.show()
            return
        def blink():
            # Flash the button to catch attention
            self.setText("GRAB ME")
            QTimer.singleShot(10, functools.partial(self.set_style, True))
            QTimer.singleShot(100, functools.partial(self.set_style, False))
            QTimer.singleShot(200, functools.partial(self.set_style, True))
            QTimer.singleShot(300, functools.partial(self.set_style, False))
            QTimer.singleShot(400, functools.partial(self.set_style, True))
            QTimer.singleShot(500, functools.partial(self.set_style, False))
            finish()
            return
        def finish():
            # After two seconds, hide the button
            # (even if user did not grab it)
            QTimer.singleShot(2000, self.hide)
            return
        start()
        return

    def start_drag(self):
        '''
        Start the drag operation.
        '''
        # 1. Start of drag-and-drop operation
        #    => button must disappear
        self.hide()

        # 2. Initiate drag-and-drop
        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

    def mousePressEvent(self, event):
        '''
        Left or Right mouseclick
        '''
        def leftmouse():
            print("left mouse click")
            self.dragStartPosition = event.pos()
            return
        def rightmouse():
            print("right mouse click")
            return
        if event.button() == Qt.LeftButton:
            leftmouse()
            return
        if event.button() == Qt.RightButton:
            rightmouse()
            return
        return

    def mouseMoveEvent(self, event):
        '''
        Mouse move event
        '''
        event.accept()
        if event.buttons() == Qt.NoButton:
            return
        if self.dragStartPosition is None:
            return
        if (event.pos() - self.dragStartPosition).manhattanLength() < QApplication.startDragDistance():
            return
        self.start_drag()
        return

class CustomMainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setGeometry(100, 100, 600, 300)
        self.setWindowTitle("ANIMATION & DRAG AND DROP")

        # 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_())

这就是我所做的更改:

  1. 我改进了动画。当您单击鼠标右键菜单中的“抓取”时,按钮的中心不是左上角而是鼠标中点。这项改进使动画制作完成后更容易抓住按钮。

  2. 在动画结束时,按钮闪烁一小段时间以引起用户的注意。按钮上的文本变为“ GRAB ME”。两秒钟未调用按钮的self.hide()函数。因此,用户有两秒钟的时间来启动拖放操作。

  3. 开始拖放操作的方式通常是:按住鼠标左键并移动鼠标指针。

  4. 如果用户两秒钟不执行任何操作,则该按钮将消失。否则,按钮将无限期地坐在那里。


2。结果

它就像一种魅力。只需将代码复制到.py文件中,然后使用 Python 3.x (我得到了 Python 3.7 )和 PyQt5

enter image description here


3。结论

我知道此解决方案并非一开始就针对我的目标:无需按住鼠标键即可执行拖放操作。不过,我认为这种新方法更好,因为它实际上更接近拖放的实际感觉。