IT Notes

Drag&Drop в Qt: Перемещаем изображения и текст

Перетаскивание с помощью мыши (Drag&Drop) - интуитивно понятный жест, используемый во многих приложениях. Нельзя сказать, что он является наилучшим вариантом организации взаимодействия с программой. Но уметь встраивать подобное поведение в Qt-проекты не помешает.

Постановка задачи

Обычно в качестве элементов для перетаскивания рассматриваются текст и изображения. Мы создадим приложение, которое умеет перемещать и текст, и изображение одновременно. Оно будет состоять из двух виджетов.

Первый виджет позволит проводить загрузку изображнеия из файловой системы. Он же станет виджетом-источником, поддерживающим событие Drag.

Второй виджет мы сделаем виджетом-приемником Drop. Он сможет принимать то, что пользователь перетащит из первого виджета.

Вот как будет выглядеть наша программа:

qt-drag-n-drop-demo-sample-thumbnail

Базовый виджет

Начнем с создания виджета, который умеет отображать изображение и подпись под ним.

Заголовочный файл imagetextwidget.h:

#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:

#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:

#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:

#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:

#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):

#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:

#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. Но этого уже хватит для решения большинства стандартных задач.

Исходники

 Скачать исходники с примером использования Drag&Drop в Qt

Похожие публикации

Комментарии

Супер! Здорово помогло!

Спасибо:)

Пожалуйста =)