Перетаскивание с помощью мыши (Drag&Drop) — интуитивно понятный жест, используемый во многих приложениях. Нельзя сказать, что он является наилучшим вариантом организации взаимодействия с программой. Но уметь встраивать подобное поведение в Qt-проекты не помешает.
Постановка задачи
Обычно в качестве элементов для перетаскивания рассматриваются текст и изображения. Мы создадим приложение, которое умеет перемещать и текст, и изображение одновременно. Оно будет состоять из двух виджетов.
Первый виджет позволит проводить загрузку изображнеия из файловой системы. Он же станет виджетом-источником, поддерживающим событие Drag.
Второй виджет мы сделаем виджетом-приемником Drop. Он сможет принимать то, что пользователь перетащит из первого виджета.
Базовый виджет
Начнем с создания виджета, который умеет отображать изображение и подпись под ним.
Заголовочный файл imagetextwidget.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#ifndef IMAGETEXTWIDGET_H #define IMAGETEXTWIDGET_H #include <QWidget> class QLabel; class ImageTextWidget : public QWidget { Q_OBJECT public: ImageTextWidget( QWidget *parent = 0 ); ~ImageTextWidget(); void setPixmap( const QPixmap& pix ); QPixmap getPixmap() const; void setText( const QString& text ); QString getText() const; private: QLabel* m_imageLabel; QLabel* m_textLabel; }; #endif // IMAGETEXTWIDGET_H |
Реализация imagetextwidget.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include "imagetextwidget.h" #include <QLabel> #include <QVBoxLayout> ImageTextWidget::ImageTextWidget( QWidget *parent ) : QWidget( parent ) { QVBoxLayout* layout = new QVBoxLayout; layout->addWidget( m_imageLabel = new QLabel( trUtf8( "<Место для изображения>" ) ), 1 ); layout->addWidget( m_textLabel = new QLabel( trUtf8( "<Место для подписи>" ) ) ); setLayout( layout ); m_imageLabel->setAlignment( Qt::AlignVCenter | Qt::AlignHCenter ); m_textLabel->setAlignment( Qt::AlignHCenter ); resize( 300, 300 ); } ImageTextWidget::~ImageTextWidget() { } void ImageTextWidget::setPixmap( const QPixmap& pix ) { m_imageLabel->setPixmap( pix ); } QPixmap ImageTextWidget::getPixmap() const { return m_imageLabel->pixmap() ? *m_imageLabel->pixmap() : QPixmap(); } void ImageTextWidget::setText( const QString& text ) { m_textLabel->setText( text ); } QString ImageTextWidget::getText() const { return m_textLabel->text(); } |
В этом коде нет ничего особенного, поэтому останавливаться на нем мы не будем.
Drag-виджет
Виджет-источник, из которого можно перетаскивать текст и изображения, уже поинтереснее. Посмотрим на его заголовочный файл dragwidget.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#ifndef DRAGWIDGET_H #define DRAGWIDGET_H #include <imagetextwidget.h> class DragWidget : public ImageTextWidget { Q_OBJECT public: explicit DragWidget( QWidget* parent = 0 ); protected: void mousePressEvent( QMouseEvent* event ); void mouseMoveEvent( QMouseEvent* event ); private slots: void onLoadImage(); private: QPoint m_dragStart; }; #endif // DRAGWIDGET_H |
Вся суть заключается в отлавливании событий нажатия кнопки мыши mousePressEvent() и перемещения курсора mouseMoveEvent(). Также обратите внимание на поле типа QPoint. В нем мы будем хранить позицию начала движения.
А вот и реализация dragwidget.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
#include "dragwidget.h" #include <QDebug> #include <QMouseEvent> #include <QDrag> #include <QMimeData> #include <QPushButton> #include <QBoxLayout> #include <QFileDialog> #include <QFileInfo> #include <QPainter> #include <QApplication> DragWidget::DragWidget( QWidget* parent ) : ImageTextWidget( parent ) { QPushButton* btn = new QPushButton( trUtf8( "Загрузить изображение" ) ); layout()->addWidget( btn ); connect( btn, SIGNAL( clicked( bool ) ), SLOT( onLoadImage() ) ); } void DragWidget::mousePressEvent( QMouseEvent* event ) { m_dragStart = event->pos(); } void DragWidget::mouseMoveEvent( QMouseEvent *event ) { if( ( event->buttons() & Qt::LeftButton ) && !getPixmap().isNull() && QApplication::startDragDistance() <= ( event->pos() - m_dragStart ).manhattanLength() ) { QDrag* drag = new QDrag( this ); QMimeData* mimeData = new QMimeData; mimeData->setImageData( getPixmap().toImage() ); mimeData->setText( getText() ); drag->setMimeData( mimeData ); drag->setPixmap( getPixmap() ); Qt::DropAction result = drag->exec( Qt::MoveAction ); qDebug() << "Drop action result: " << result; if( result == Qt::MoveAction ) { setPixmap( QPixmap() ); setText( "" ); } } } void DragWidget::onLoadImage() { QString filePath = QFileDialog::getOpenFileName( this, trUtf8( "Загрузка изображения" ), ".", trUtf8( "Изображения (*.jpg *.png *.gif)" ) ); if( !filePath.isEmpty() ) { QPixmap pix( filePath ); setPixmap( pix ); QFileInfo info( filePath ); setText( info.fileName() ); } } |
В функции-члене mousePressEvent() мы всего лишь сохраняем позицию курсора, в которой произошел щелчок. Главное действие происходит в mouseMoveEvent(), которое соответствует перемещению мыши. Сначала мы проверяем, что перетаскивание имеет смысл, т.е. нажата левая кнопка мыши, имеется загруженное изображение и пройденное расстояние от начальной позиции превышает некий минимум, который мы узнаем с помощью QApplication::startDragDistance().
Если все хорошо, то можно создать экземпляр QDrag. При этом информация для перетаскивания помещается в QMimeData. Мы кладем в него изображение и текст (как и планировали). На самом деле, в QMimeData можно передавать вообще все, что угодно. Но об этом в другой раз.
Следующим шагом мы связываем mimeData с drag. Кроме того, для drag устанавливаем изображение, которое будет отображаться при перемещении.
Ключевой момент: QDrag работает по принципу, похожему на модальные диалоговые окна, поэтому для его использования вызываем drag->exec(). В качестве параметра передаем Qt::MoveAction (есть и другие варианты, но их в этот раз трогать не будем).
Если виджет-приемник (которым мы сейчас займемся) успешно принял данные, то drag->exec() вернет Qt::MoveAction. Это означает, что перемещение прошло успешно, поэтому просто очищаем изображение и текст, которые здесь больше не нужны.
Drop-виджет
Реализация виджета-приемника проще, чем для виджета-источника. Убедимся в этом сами. И начнем, как обычно, с заголовочного файла dropwidget.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#ifndef DROPWIDGET_H #define DROPWIDGET_H #include <imagetextwidget.h> class DropWidget : public ImageTextWidget { Q_OBJECT public: explicit DropWidget( QWidget* parent = 0 ); protected: void dragEnterEvent( QDragEnterEvent* event ); void dropEvent( QDropEvent* event ); }; #endif // DROPWIDGET_H |
Обработка вновь основана на событиях. Для этого мы переопределяем функции-члены dragEnterEvent() и dropEvent(). Посмотрим, что в них происходит (файл dropwidget.cpp):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include "dropwidget.h" #include <QDragEnterEvent> #include <QDropEvent> #include <QMimeData> #include <QDebug> DropWidget::DropWidget( QWidget* parent ) : ImageTextWidget( parent ) { setAcceptDrops( true ); } void DropWidget::dragEnterEvent( QDragEnterEvent* event ) { QStringList formats = event->mimeData()->formats(); qDebug() << formats; if( formats.contains( "application/x-qt-image") && formats.contains( "text/plain" ) ) { event->acceptProposedAction(); } } void DropWidget::dropEvent( QDropEvent* event ) { setPixmap( QPixmap::fromImage( event->mimeData()->imageData().value< QImage >() ) ); setText( event->mimeData()->text() ); event->acceptProposedAction(); } |
Обработчик события dragEnterEvent() срабатывает, когда курсор мыши, «заряженный» данными виджета-источника, оказывается над виджетом-приемником. Сначала мы выводим на консоль список форматов данных, которые содержит в себе событие.
Замечание: Когда запустите получившуюся программу, можете провести эксперимент и поперетаскивать что-нибудь из других приложений. Хоть наш виджет вряд ли сможет принять их содержимое, но зато выведет на консоль то, что они пытаются передать.
Однако главная задача dragEnterEvent() заключается в том, чтобы решить, нужно ли принимать перетаскиваемое содержимое или нет. Проверка осуществляется с помощью функций event->mimeData()->hasImage() и event->mimeData->hasText(). Если проверка пройдена, то для подтверждения достаточно вызвать event->acceptProposedAction().
Обработчик события dropEvent() вызывается в самый ответственный момент, т.е. когда перемещаемое вложение «приземляется» на виджет (в момент отпускания левой кнопки мыши над ним). В этом случае мы просто устанавливаем изображение и текст для отображения в виджете-приемнике. Кроме того, не забудьте про вызов event->acceptProposedAction(). Таким образом мы сообщаем виджету-источнику, что успешно приняли его содержимое.
Собираем приложение
А теперь организуем запуск нашей программы. Файл main.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include "dragwidget.h" #include "dropwidget.h" #include <QApplication> int main( int argc, char* argv[] ) { QApplication a( argc, argv ); DragWidget dragWgt; dragWgt.show(); DropWidget dropWgt; dropWgt.show(); return a.exec(); } |
Выводы
Мы рассмотрели лишь самый минимум возможностей, которые предоставляет Qt для реализации обработчиков Drag&Drop. Но этого уже хватит для решения большинства стандартных задач.