Глава 9. Drag and Drop

Предыдущая глава | Содержание | Следующая глава

Глава 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) и вставляется в виджет-приемник.


9.2. Поддержка нестандартных типов данных при перетаскивании.

До сих пор мы имели дело с предопределенными типами перетаскиваемых объектов. Например, мы использовали QUriDrag, для перетаскивания файлов, и QTextDrag -- для текста. Оба этих класса являются наследниками QDragObject, который служит базой для всех перемещаемых объектов. В свою очредь, класс QDragObject наследует свойства абстрактного класса QMimeSource, предназначенного для хранения данных различных типов.

Если вы пожелаете перемещать объекты с текстовой информацией, с изображениями, с именами файлов или с информацией о цвете, то можно использовать предопределенные классы Qt: QTextDrag, QImageDrag, QUriDrag и QColorDrag. Но если вам необходимо перемещать нестандартные типы данных, то у вас есть два пути:

  • Сохранить информацию, в двоичном представлении, в объекте класса QStoredDrag.

  • Создать свой собственный класс перетаскиваемых объектов, породив его от QDragObject и перекрыв соответствующие виртуальные методы.

Класс QStoredDrag может хранить любые двоичные данные, что позволяет использовать его для любых типов MIME. Например, если вам потребуется перетащить некоторые данные, хранящиеся в файле формата (фиктивного) ASDF, то можно рекомендовать примерно такой код:
void MyWidget::startDrag() 
{ 
  QByteArray data = toAsdf(); 
  if (!data.isEmpty()) { 
    QStoredDrag *drag = new QStoredDrag("octet-stream/x-asdf", this); 
    drag->setEncodedData(data); 
    drag->setPixmap(QPixmap::fromMimeSource("asdf.png")); 
    drag->drag(); 
  } 
}
      
Однако, QStoredDrag имеет ряд неудобств. Одно из них заключается в том, что он может хранить только один MIME тип. Если мы предполагаем использовать механизм "drag and drop" только в пределах одного приложения, то это не является большой проблемой. Но когда необходимо реализовать взаимодействие между различными приложениями, то одного MIME типа, как правило бывает недостаточно.

Другое неудобство состоит в необходимости преобразования данных в QByteArray, даже если приемник не может принимать данные этого типа. При достаточно большом объеме данных, это может привести к неоправданной потере производительности. Было бы намного удобнее, если бы преобразование выполнялось в момент сброса перетаскиваемого объекта.

Решение этих двух проблем заключается в создании дочернего класса от QDragObject и реализации двух виртуальных методов format() и encodedData(), используемых Qt для получения сведений о перетаскиваемых объектах. Чтобы показать -- как это можно сделать, мы создадим класс CellDrag, который будет хранить данные из одной или нескольких ячеек таблицы QTable.

class CellDrag : public QDragObject 
{ 
public: 
  CellDrag(const QString &text, QWidget *parent = 0, 
           const char *name = 0); 
  const char *format(int index) const; 
  QByteArray encodedData(const char *format) const; 
  
  static bool canDecode(const QMimeSource *source); 
  static bool decode(const QMimeSource *source, QString &str); 
  
private: 
  QString toCsv() const; 
  QString toHtml() const; 
  QString plainText; 
};
      
Класс CellDrag порожден от класса QDragObject. В нем только две функции имеют прямое отношение к механизму "drag and drop" -- это format() и encodedData(). Дополнительно, только лишь для удобства, он предоставляет в распоряжение программиста статические функции canDecode() и decode(), которые извлекают данные в момент сброса.
CellDrag::CellDrag(const QString &text, QWidget *parent, 
                   const char *name) 
    : QDragObject(parent, name) 
{ 
  plainText = text; 
}
      
Конструктору передается строка в текстовом виде, которая будет перемещаться. Это обычный текст, который может содержать символы табуляции и перевода строки. Этот текстовый тип мы использовали в Главе 4, когда добавляли в приложение Spreadsheet поддержку буфера обмена (см. раздел Реализация меню Edit).
const char *CellDrag::format(int index) const 
{ 
  switch (index) { 
    case 0: 
      return "text/csv"; 
    case 1: 
      return "text/html"; 
    case 2:
      return "text/plain"; 
    default: 
      return 0; 
  } 
}
      
Функция format() перекрывает метод родительского класса QMimeSource и возвращает различные MIME типы, поддерживаемые объектом при перетаскивании. В нашем примере поддерживаются три типа данных: CSV (от англ. Comma-Separated Values -- Данные, Разделенные Запятыми), HTML и простой текст.

Когда Qt пытается определить -- какой MIME тип поддерживается перетаскиваемым объектом, она вызывает format() с аргументом index, равным 0, 1, 2... и так до тех пор, пока format() не вернет пустой указатель. Типы MIME для CSV и HTML были взяты из официального списка, который вы найдете по адресу: http://www.iana.org/assignments/media-types/ .

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

QByteArray CellDrag::encodedData(const char *format) const 
{ 
  QByteArray data; 
  QTextOStream out(data); 
  
  if (qstrcmp(format, "text/csv") == 0) { 
    out << toCsv(); 
  } else if (qstrcmp(format, "text/html") == 0) { 
    out << toHtml(); 
  } else if (qstrcmp(format, "text/plain") == 0) { 
    out << plainText; 
  } 
  return data; 
}
      
Функция encodedData() возвращает данные в заказанном формате. Аргумент format, обычно содержит одну из строк, которую возвращает функция format(), но мы не можем безоговорочно утверждать это, поскольку не все приложения проверяют тип MIME вызовом format(). В приложениях Qt такая проверка обычно выполняется вызовом provides() внутри QDragEnterEvent и QDragMoveEvent (как мы это видели ранее).

Для преобразования QString в QByteArray, лучше использовать QTextStream.

QString CellDrag::toCsv() const 
{ 
  QString out = plainText; 
  out.replace("\\", "\\\\"); 
  out.replace("\"", "\\\""); 
  out.replace("\t", "\", \"");
  out.replace("\n", "\"\n\""); 
  out.prepend("\""); 
  out.append("\""); 
  return out; 
}
   
QString CellDrag::toHtml() const 
{ 
  QString out = QStyleSheet::escape(plainText); 
  out.replace("\t", "<td>"); 
  out.replace("\n", "\n<tr><td>"); 
  out.prepend("<table>\n<tr><td>"); 
  out.append("\n</table>"); 
  return out; 
}
      
Функции toCsv() и toHtml() выполняют преобразование символов табуляции и перевода строки в соответствующие элементы формата CSV и HTML. Например, данные
      Red    Green    Blue 
      Cyan   Yellow   Magenta
      
будут преобразованы в
      "Red",   "Green",   "Blue" 
      "Cyan",  "Yellow",  "Magenta"     
      
или в
      <table> 
      <tr><td>Red<td>Green<td>Blue 
      <tr><td>Cyan<td>Yellow<td>Magenta 
      </table>
      
Преобразование выполняется простой заменой одних символов другими, с помощью QString::replace(). Для экранирования специальных символов HTML используется статическая функция QStyleSheet::escape().
bool CellDrag::canDecode(const QMimeSource *source) 
{ 
  return source->provides("text/plain"); 
}
      
Функция canDecode() возвращает true, если перетаскиваемые данные могут быть декодированы, в противном случае возвращается false.

Хотя мы и предусматриваем поддержку трех форматов для перетаскиваемых данных, мы будем принимать только данные в простом текстовом виде, поскольку для наших нужд этого будет более чем достаточно. Если пользователь попытается переместить ячейки из QTable в HTML-редактор, то данные будут преобразованы в HTML-таблицу. Но если пользователь попробует переместить произвольную HTML-таблицу (например, из браузера) в QTable, то эти данные не будут восприняты приложением.

bool CellDrag::decode(const QMimeSource *source, QString &str) 
{ 
  QByteArray data = source->encodedData("text/plain");
  str = QString::fromLocal8Bit((const char *)data, data.size()); 
  return !str.isEmpty(); 
}
      
И, наконец, функция decode() преобразует text/plain данные в QString. Здесь мы предполагаем, что используется 8-ми битная кодировка символов.

Если вы пожелаете точно указывать кодировку символов, для перемещаемых данных, вы можете задать параметр charset формата text/plain, напимер:

      text/plain;charset=US-ASCII 
      text/plain;charset=ISO-8859-1 
      text/plain;charset=Shift_JIS
      
Итак. Мы закончили описание реализации класса CellDrag. Нам осталось только интегрировать его с QTable. Оказывается, класс QTable уже выполняет почти все, что нам нужно. Единственное, что нам остается сделать -- это вызвать setDragEnabled(true) в конструкторе и перекрыть метод QTable::dragObject(), который будет возвращать CellDrag:
QDragObject *MyTable::dragObject() 
{ 
  return new CellDrag(selectionAsString(), this); 
}
      
Мы не приводим текст функции selectionAsString(), поскольку он почти полностью совпадает с текстом функции Spreadsheet::copy().

Чтобы добавить поддержку приема данных, сбрасываемых на таблицу, необходимо перекрыть методы contentsDragEnterEvent() и contentsDropEvent() точно так же, как мы это делали в приложении "Project Chooser".


9.3. Расширенные возможности буфера обмена.

Большинство приложений используют внутренние механизмы Qt, при работе с буфером обмена. Например, класс QTextEdit включает в себя поддержку комбинаций клавиш Ctrl+X, Ctrl+C и Ctrl+V, которые соответствуют слотам cut(), copy() и paste(). В результате этого, от программиста не требуется написания специального кода, отвечающего за работу с буфером обмена.

При написании собственных классов, вы можете получить доступ к буферу обмена с помощью функции QApplication::clipboard(), которая возвращает указатель на объект класса QClipboard. Работа с буфером обмена на удивление проста и незатейлива! Чтобы поместить в него данные нужно лишь вызвать метод setText(), setImage() или setPixmap(). Чтобы получить данные из буфера -- text(), image() или pixmap(). В Главе 4 мы уже пробовали работать с буфером обмена, при разработке приложения Spreadsheet.

Однако, в некоторых случаях, встроенной поддержки буфера обмена может оказаться недостаточно. Например, может потребоваться обеспечить поддержку данных, которые не являются ни текстом, ни рисунком. Или, с целью повышения совместимости с другими приложениями, необходимо будет организовать обмен данными в нескольких форматах. Проблема очень напоминает то, с чем мы уже столкнулись чуть выше, поэтому и решение ее практически аналогичное: необходимо создать дочерний класс от QMimeSource и перекрыть методы родительского класса format() и encodedData()..

Если в приложение включена поддержка механизма "drag and drop", то вы можете просто использовать уже существующий потомок класса QDragObject, помещая объекты этого типа в буфер обмена, вызовом setData(). Поскольку QDragObject ведет свою родословную от QMimeSource, а буфер обмена умеет взаимодействовать с классом QMimeSource, то все будет работать без особых проблем.

Рассмотрим на примере, как можно реализовать функцию copy() для потомка класса QTable:

void MyTable::copy() 
{ 
  QApplication::clipboard()->setData(dragObject()); 
}
      
В конце предыдущего раздела мы реализовали функцию dragObject(), которая возвращает CellDrag, предназначенный для хранения содержимого выделенных ячеек.

Чтобы извлечь данные из буфера обмена, необходимо обратиться к методу data(). Ниже приводится текст функции paste() для потомка класса QTable:

void MyTable::paste() 
{ 
  QMimeSource *source = QApplication::clipboard()->data(); 
  if (CellDrag::canDecode(source)) { 
    QString str; 
    CellDrag::decode(source, str); 
    performPaste(str); 
  } 
}
      
Функция performPaste() -- практически полный аналог функции Spreadsheet::paste() из Главы 4.

Это практически все, что необходимо для расширения возможностей при работе с буфером обмена.

Буфер обмена X11, предоставляет дополнительные возможности, которые недоступны в операционных системах Windows и Mac OS X. В X11, обычно имеется возможность вставки выделенной области, щелчком средней кнопки трехкнопочной мыши, благодаря наличию отдельного буфера "выделения". Если вы желаете добавить поддержку этого буфера обмена в свои виджеты, вам придется добавить дополнительный аргумент QClipboard::Selection во все вызовы, обращающиеся к буферу обмена. Например, вот как можно реализовать обработчик события mouseReleaseEvent() в текстовом редакторе, который должен поддерживать вставку блоков текста по щелчку средней кнопки мыши:

void MyTextEditor::mouseReleaseEvent(QMouseEvent *event) 
{ 
  QClipboard *clipboard = QApplication::clipboard(); 
  if (event->button() == MidButton 
          && clipboard->supportsSelection()) { 
    QString text = clipboard->text(QClipboard::Selection); pasteText(text); 
  } 
}
      
На платформе X11 функция supportsSelection() возвращает true, на других -- false.

Предыдущая глава | Содержание | Следующая глава

[ опубликовано 04/11/2005 ]

Ж. Бланшетт, М. Саммерфильд - Глава 9. Drag and Drop   Версия для печати