如何在不按住鼠标按钮的情况下执行拖放? [英] How to perform a drag-and-drop without holding down the mouse button?

查看:77
本文介绍了如何在不按住鼠标按钮的情况下执行拖放?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

1.目标

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

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

 

2.最小的、可重现的示例

我做了一些工作,但它还不完美.请复制粘贴下面的代码并使用 Python 3.x(我使用 Python 3.7)和 PyQt5 运行它.

<块引用>

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

from PyQt5.QtWidgets import *从 PyQt5.QtGui 导入 *从 PyQt5.QtCore 导入 *导入系统类 MyButton(QPushButton):'''一个特殊的按钮.'''def __init__(self, *args, **kwargs):super().__init__(*args, **kwargs)self.setFixedWidth(300)self.setFixedHeight(30)self.setContextMenuPolicy(Qt.CustomContextMenu)self.customContextMenuRequested.connect(self.showMenu)返回def showMenu(self, pos):'''当用户用鼠标右键单击时显示此弹出菜单.'''菜单 = QMenu()menuAction_01 = menu.addAction("动作01")menuAction_02 = menu.addAction("动作02")menuAction_03 = menu.addAction("动作03")menuAction_04 = menu.addAction("动作04")menuAction_grab = menu.addAction("grab")action = menu.exec_(self.mapToGlobal(pos))如果动作== menuAction_01:打印(点击动作01")elif 动作 == menuAction_02:打印(点击动作02")elif 动作 == menuAction_03:打印(点击动作03")elif 动作 == menuAction_04:打印(点击动作04")elif 动作 == menuAction_grab:打印(点击抓取")# 1. 开始动画# ->按钮移动到鼠标指针self.animate()# 2. 动画结束后(约1秒)# ->开始拖动操作QTimer.singleShot(1000, self.start_drag)返回定义动画(自我):'''该按钮将自身从 QScrollArea() 中移除并飞向鼠标光标.有关更多详细信息,请参阅 @eyllanesc 的 anser athttps://stackoverflow.com/questions/56216698/how-display-a-qpropertyanimation-on-top-of-the-qscrollarea'''起点 = self.window().mapFromGlobal(self.mapToGlobal(QPoint()))端点 = self.window().mapFromGlobal(QCursor.pos())self.setParent(self.window())动画 = QPropertyAnimation(自己,b"pos",自己,持续时间=1000,起始值=起点,结束值=端点,完成=self.hide,)动画开始()自我展示()返回def start_drag(self):'''开始拖动操作.'''拖动 = QDrag(self)pixmap = QPixmap("my_pixmap.png")pixmap = pixmap.scaledToWidth(100, Qt.SmoothTransformation)drag.setPixmap(像素图)mimeData = QMimeData()mimeData.setText("Foobar")drag.setMimeData(mimeData)dropAction = drag.exec(Qt.CopyAction | Qt.MoveAction)返回类 CustomMainWindow(QMainWindow):def __init__(self):super().__init__()self.setGeometry(100, 100, 600, 300)self.setWindowTitle("动画测试")# 外框# ============self.frm = QFrame()self.frm.setStyleSheet("""QFrame {背景:#d3d7cf;边界:无;}""")self.lyt = QHBoxLayout()self.frm.setLayout(self.lyt)self.setCentralWidget(self.frm)# 按钮框# ==============self.btn_frm = QFrame()self.btn_frm.setStyleSheet("""QFrame {背景:#ffffff;边界:无;}""")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)# 滚动区域# ============self.scrollArea = QScrollArea()self.scrollArea.setStyleSheet("""QScrollArea {边框样式:实心;边框宽度: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)# 添加按钮到 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.setAcceptDrops(True)返回def dropEvent(self, event):event.acceptProposedAction()print("dropEvent at {0!s}".format(event))返回def dragLeaveEvent(self, event):事件.接受()返回def dragEnterEvent(self, event):event.acceptProposedAction()返回如果 __name__== '__main__':app = QApplication(sys.argv)QApplication.setStyle(QStyleFactory.create('Plastique'))myGUI = CustomMainWindow()sys.exit(app.exec_())

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

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

步骤 2: 按钮移动到您的鼠标指针.不要移动鼠标指针.

第 3 步: 一旦鼠标指针位于按钮上方(不要移动鼠标,等待按钮到达),点击并按住鼠标按钮.

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

好的,它有效,但有一些缺点.

 

3.问题

在动画结束时,飞行按钮位于鼠标指针下方.但是,如果您稍微移动鼠标指针,按钮就会消失,您就会错过拖放操作.
换句话说,我现在得到的不是很健壮.用户很容易错过拖放操作.

注意:显然我在这里描述的问题只出现在 Windows 上(而不是在 Linux 上).但我必须让这个东西在 Windows 上运行......

 

4.可能的解决方案

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

只要按钮到达鼠标指针下方(动画结束),按钮就会消失.拖放操作会自动开始,无需单击并按住鼠标按钮.当您移动鼠标指针时,拖动会继续进行,直到您单击某处.鼠标按下是 dropEvent().

你知道如何实现吗?或者您可能有其他方法?

 

5.备注

我的问题实际上是这个问题的续集:

 

3.结论

我知道这个解决方案并不是我从一开始的目标:在不按住鼠标按钮的情况下执行拖放.尽管如此,我认为新方法更好,因为它更接近一般的直觉,拖放实际上是什么.

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 ^_^

解决方案

1. Solution

Before presenting a solution, I want to express my gratitude to Mr. @eyllanesc for helping me. Without his help, I wouldn't have a solution right now.

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

This is what I changed:

  1. I improved the animation. Not the top left corner but the middle of the button flies to your mouse pointer when you click "grab" in the rightmouse menu. This improvement makes it much easier to grab the button once the animation has finished.

  2. At the end of the animation, the button flashes for a short moment to catch the user's attention. The text on the button changes into "GRAB ME". The button's self.hide() function is NOT called for two seconds. So the user has two seconds time to initiate a drag-and-drop operation.

  3. Initiating the drag-and-drop operation happens in the usual way: hold down the leftmouse button and move the mouse pointer.

  4. If the user doesn't do anything for two seconds, the button will just disappear. Otherwise, the button would just sit there indefinitely.

 

2. Results

It works like a charm. Just copy past the code in a .py file and run it with Python 3.x (I got Python 3.7) and PyQt5:

 

3. Conclusion

I know that this solution is not exactly what I aimed at from the beginning: performing a drag-and-drop without holding down the mouse button. Nevertheless, I think the new approach is better because it's closer to the general intuition what a drag-and-drop actually is.

这篇关于如何在不按住鼠标按钮的情况下执行拖放?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆