Глава 9. Drag and Drop.

"Drag and Drop" (от англ. "Перетащил и бросил") -- современный интуитивно понятный способ перемещения информации внутри приложения или между приложениями. Он часто реализуется как дополнение к поддержке буфера обмена.

В этой главе мы покажем как добавить в приложение поддержку механизма "перетащил и бросил". Затем мы будем использовать код "drag and drop" для реализации поддержки буфера обмена. Это возможно по той простой причине, что в основе обоих механизмов лежит один абстрактный класс QMimeSource, который может хранить данные в различных форматах.

9.1. Реализация механизма 'drag and drop' в приложениях.

В основе механизма "перетащил и бросил" лежат две операции: операция перетаскивания и операция сброса. Виджет может быть как источником, откуда начинается перетаскивание, так и местом, куда может производиться сбрасывание.

Это очень мощный механизм, позволяющий перетаскивать данные из одного приложения в другое. Однако, в некоторых случаях, можно реализовать некоторое подобие механизма "перетащил и бросил", не прибегая к специальным возможностям Qt. Если все, что вам нужно -- это перетащить какие либо данные внутри одного виджета, то гораздо проще это выполняется перекрытием обработчиков событий от мыши. Подобный подход мы рассматривали в Главе 8, при разработке виджета DiagramView.

В нашем первом примере мы рассмотрим -- как заставить Qt приложение принимать данные, перетаскиваемые из других приложений. Приложение представлено окном, где в качестве центрального, используется виджет QTextEdit. Когда пользователь перемещает какой либо файл с рабочего стола или из программы-обозревателя, то наше приложение будет загружать его в QTextEdit.

Ниже приводится определение класса MainWindow:

class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0, const char *name = 0); protected: void dragEnterEvent(QDragEnterEvent *event); void dropEvent(QDropEvent *event); private: bool readFile(const QString &fileName); QString strippedName(const QString &fullFileName); QTextEdit *textEdit; }; Класс MainWindow перекрывает методы предка (QWidget) dragEnterEvent() и dropEvent(). Так как целью данного примера является демонстрация работы механизма "drag and drop", ту часть реализации класса MainWindow, которая не имеет отношения к этому механизму, мы приводить не будем. MainWindow::MainWindow(QWidget *parent, const char *name) : QMainWindow(parent, name) { setCaption(tr("Drag File")); textEdit = new QTextEdit(this); setCentralWidget(textEdit); textEdit->viewport()->setAcceptDrops(false); setAcceptDrops(true); } В конструкторе создается объект класса QTextEdit и назначается центральным виджетом приложения. Далее запрещается "сброс" в область QTextEdit и разрешается для главного окна приложения.

Запрет на сброс для QTextEdit накладывается из-за того, что обработка перетаскивания и сброса будет реализована в классе главного окна приложения. По-умолчанию QTextEdit может принимать "сбрасываемые" текстовые данные, перетянутые из другого приложения, так например, если пользователь перетащит файл в область QTextEdit, то в виджет будет вставлено имя файла. Но это не совсем то, что нам надо. Мы собираемся открыть файл и поместить его содержимое в центральный виджет, а не вставить его имя в текст. Поэтому мы не будем использовать возможности класса QTextEdit, а реализуем свои собственные методы в классе главного окна приложения.

Поскольку события, в случае отказа от обработки, переходят от подчиненного виджета -- виджету владельцу, то главное окно получит событие "сброса" даже в том случае, если сброс осуществлялся в области QTextEdit.

void MainWindow::dragEnterEvent(QDragEnterEvent *event) { event->accept(QUriDrag::canDecode(event)); } Функция dragEnterEvent() вызывается, когда пользователь перемещает некий объект в область виджета. Если вызывается accept(true), то это говорит о том, что пользователь может сбросить перетаскиваемый объект на виджет. Если вызывается accept(false) -- перетаскиваемый объект не может быть принят виджетом. Qt автоматически изменяет внешний вид указателя мыши, показывая пользователю -- может или не может быть сброшен данный объект в этом месте.

В нашем примере предполагается, что пользователь может сбросить в область приложения только имена файлов. Поэтому мы воспользовались услугами класса QUriDrag, который обслуживает перетаскивание файлов, для опознания перетаскиваемого объекта. Этот класс может использоваться для опознания Универсальных Идентификаторов Ресурсов (URI -- Universal Resource Identifier), таких как пути FTP или HTTP.

void MainWindow::dropEvent(QDropEvent *event) { QStringList fileNames; if (QUriDrag::decodeLocalFiles(event, fileNames)) { if (readFile(fileNames[0])) setCaption(tr("%1 - Drag File") .arg(strippedName(fileNames[0]))); } } Функция dropEvent() вызывается в момент сброса объекта на виджет. Функция QUriDrag::decodeLocalFiles() возвращает список имен файлов, которые перетаскивает пользователь. Из этого списка мы вынимаем первый файл. Обычно пользователь перетаскивает файлы по одному, но возможна ситуация, когда перетаскивается несколько выделенных файлов.

Кроме того, класс QWidget предоставляет методы dragMoveEvent() и dragLeaveEvent(), но в большинстве приложений эти методы не используются.

Второй пример показывает -- как начать перетаскивание и как принять сбрасываемый объект. С этой целью мы создадим подкласс от QListBox, и реализуем в нем поддержку механизма "перетащил и бросил". Этот класс мы будем использовать в приложении "Project Chooser", показанном на рисунке 9.1.

Рисунок 9.1. Внешний вид приложения "Project Chooser".


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

Вся реализация будет размещаться в единственном классе, потомке QListBox. Ниже приводится определение класса:

class ProjectView : public QListBox { Q_OBJECT public: ProjectView(QWidget *parent, const char *name = 0); protected: void contentsMousePressEvent(QMouseEvent *event); void contentsMouseMoveEvent(QMouseEvent *event); void contentsDragEnterEvent(QDragEnterEvent *event); void contentsDropEvent(QDropEvent *event); private: void startDrag(); QPoint dragPos; }; Класс ProjectView реализует четыре обработчика событий, объявленных в QScrollView (базовый класс для QListBox). ProjectView::ProjectView(QWidget *parent, const char *name) : QListBox(parent, name) { viewport()->setAcceptDrops(true); } В конструкторе мы разрешаем прием сбрасываемых объектов в область списка. void ProjectView::contentsMousePressEvent(QMouseEvent *event) { if (event->button() == LeftButton) dragPos = event->pos(); QListBox::contentsMousePressEvent(event); } Когда пользователь нажимает левую кнопку мыши, позиция указателя запоминается в приватной переменной dragPos и вызывается метод предка contentsMousePressEvent(), чтобы обработать нажатие кнопки в обычном порядке. void ProjectView::contentsMouseMoveEvent(QMouseEvent *event) { if (event->state() & LeftButton) { int distance = (event->pos() - dragPos).manhattanLength(); if (distance > QApplication::startDragDistance()) startDrag(); } QListBox::contentsMouseMoveEvent(event); } Когда пользователь перемещает указатель мыши, при удерживаемой левой кнопке, мы полагаем, что началось перетаскивание объекта. Далее вычисляется расстояние между текущим положением указателя мыши и точкой начала перетаскивания.

Если это расстояние больше, чем рекомендуемое классом QApplication (обычно 4 пикселя), после которого перемещение мыши действительно начинает рассматриваться как перетаскивание объекта, вызывается startDrag(), которая отмечает начало перетаскивания. Это дает возможность избежать ложного запуска процесса перетаскивания из-за дрожжания руки пользователя.

void ProjectView::startDrag() { QString person = currentText(); if (!person.isEmpty()) { QTextDrag *drag = new QTextDrag(person, this); drag->setSubtype("x-person"); drag->setPixmap(QPixmap::fromMimeSource("person.png")); drag->drag(); } } В startDrag() создается объект класса QTextDrag. Этот класс представляет перетаскиваемый объект, который содержит перемещаемый текст. Это один из нескольких предопределенных типов, которые предоставляет Qt для перетаскиваемых объектов. Кроме него можно еще назвать QImageDrag, QColorDrag и QUriDrag. Дополнительно, в соответствие перетаскиваемому объекту, мы ставим небольшую картинку, которая будет перемещаться вслед за указателем мыши, изображая перетаскиваемый объект.

Затем вызывается setSubtype(), которая устанавливает подтип объекта -- x-person. После этого полный тип объекта MIME приобретает значение text/x-person. Если этого не сделать, то перетаскиваемый объект будет иметь тип MIME -- text/plain.

Стандартные типы MIME определены IANA (Internet Assigned Numbers Authority). Полный MIME тип состоит из названия типа и подтипа, разделенных символом слэша. Когда создается нестандартный тип, рекомендуется предварять название подтипа префиксом x-. Типы MIME используются буфером обмена и механизмом "drag and drop" для идентификации различных типов данных.

Функция drag() отмечает начало операции перетаскивания. После этого Qt принимает на себя обязательства по владению перетаскиваемым объектом, пока перемещение не будет завершено. Она сама удалит объект, когда нужда в нем отпадет, даже если он так и не достигнет места назначения.

void ProjectView::contentsDragEnterEvent(QDragEnterEvent *event) { event->accept(event->provides("text/x-person")); } Виджет класса ProjectView может не только начать перетаскивание объекта, типа text/x-person, но так же может принимать сбрасываемые объекты этого типа. Когда перемещаемый объект попадает в область виджета, выполняется проверка на корректность типа MIME. void ProjectView::contentsDropEvent(QDropEvent *event) { QString person; if (QTextDrag::decode(event, person)) { QWidget *fromWidget = event->source(); if (fromWidget && fromWidget != this && fromWidget->inherits("ProjectView")) { ProjectView *fromProject = (ProjectView *)fromWidget; QListBoxItem *item = fromProject->findItem(person, ExactMatch); delete item; insertItem(person); } } } В функции contentsDropEvent(), с помощью QTextDrag::decode(), из перетаскиваемого объекта извлекается текстовая строка. Функция QDropEvent::source() возвращает указатель на виджет, в котором была начата операция перетаскивания, но только в том случае, если виджет принадлежит тому же самому приложению. Если виджет-приемник и виджет-источник -- это не одно и то же, и виджет-источник принадлежит классу ProjectView, то элемент списка удаляется из виджета-источника (вызовом delete) и вставляется в виджет-приемник.