Как выполнить перетаскивание, не удерживая кнопку мыши? - PullRequest
2 голосов
/ 22 мая 2019

1.Цель

Моя цель - создать меню щелчка правой кнопкой мыши, например:

enter image description here

Когда пользователь нажимает Хватайте и двигайте , кнопка должна исчезнуть с QScrollArea() и быстро двигаться к мышке.Когда он достигнет указателя мыши, кнопка должна исчезнуть, и операция drag-and-drop может начаться.


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
        /8530564/kak-otobrazit-qpropertyanimation-poverh-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: Теперь переместите мышь (удерживая кнопку мыши нажатой).Вы должны быть в состоянии drag-and-drop , с растровым изображением, привязанным к вашей мыши!

enter image description here

Хорошо, этоработает, но есть несколько минусов.


3.Задача

В конце анимации летающая кнопка находится под указателем мыши.Но если вы слегка передвинете указатель мыши, кнопка исчезнет, ​​и вы пропустите операцию drag-and-drop .
Другими словами, то, что я получил сейчас, не очень надежно.Пользователь может легко пропустить операцию drag-and-drop .

ПРИМЕЧАНИЕ. Очевидно, что описанная здесь проблема возникает только в Windows (не в Linux).Но я должен заставить эту штуку работать на Windows ...


4.Потенциальное решение

Я полагаю, что следующий подход будет лучше и все же интуитивно понятен для пользователя:

Как только кнопка появляется под указателем мыши (конец анимации), кнопкаисчезает.Операция drag-and-drop запускается автоматически, без необходимости нажимать и удерживать кнопку мыши.Перетаскивание продолжается, пока вы перемещаете указатель мыши, пока не щелкните где-нибудь.Это нажатие мыши - dropEvent().

Вы знаете, как это реализовать?Или, может быть, вы имеете в виду другой подход?


5.Примечания

Мой вопрос на самом деле является продолжением этого: Как отобразить QPropertyAnimation () поверх QScrollArea ()?
Спасибо @eyllanesc за решение этой проблемыодин ^ _ ^

1 Ответ

1 голос
/ 23 мая 2019

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
        /8530564/kak-otobrazit-qpropertyanimation-poverh-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. В конце анимации кнопка на короткое время мигает, чтобы привлечь внимание пользователя.Текст на кнопке изменится на «Хватай меня».Функция кнопки self.hide() НЕ вызывается в течение двух секунд.Таким образом, у пользователя есть две секунды для запуска операции перетаскивания.

  3. Запуск операции перетаскивания происходит обычным образом: удерживайте нажатой кнопку левой мыши и двигайтесьуказатель мыши.

  4. Если пользователь ничего не делает в течение двух секунд, кнопка просто исчезнет.В противном случае кнопка будет просто сидеть там бесконечно.


2.Результаты

Работает как шарм.Просто скопируйте код в файл .py и запустите его с Python 3.x (я получил Python 3.7 ) и PyQt5 :

enter image description here


3.Заключение

Я знаю, что это решение не совсем то, к чему я стремился с самого начала: выполнение перетаскивания без удерживания кнопки мыши.Тем не менее, я думаю, что новый подход лучше, потому что он ближе к общей интуиции, чем на самом деле является перетаскивание.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...