Перетаскивание и отпускание
Перетаскивание и отпускание обеспечивает простой визуальный механизм, который пользователи могут использовать для передачи информации между приложениями и внутри них. Перетаскивание и отпускание аналогично механизму «вырезать» и «вставить» буфера обмена.
В этом документе описывается базовый механизм перетаскивания и отпускания и описывается подход, используемый для его включения в пользовательские элементы управления. Операции перетаскивания и отпускания также поддерживаются многими элементами управления Qt, такими как виджеты списка элементов и фреймворк просмотра графики, а также элементами редактирования для Qt Widgets и Qt Quick. Более подробную информацию о виджетах списка элементов и фреймворке просмотра графики можно найти в Использование перетаскивания и отпускания с виджетами списка элементов и Фреймворк просмотра графики.
Классы перетаскивания и отпускания
Эти классы обрабатывают перетаскивание и отпускание, а также необходимое кодирование и декодирование MIME-типов.
Поддержка передачи данных перетаскивания и отпускания на основе MIME |
|
Событие, которое отправляется виджету, когда действие перетаскивания и отпускания входит в него |
|
Событие, которое отправляется виджету, когда действие перетаскивания и отпускания покидает его |
|
Событие, которое отправляется во время выполнения действия перетаскивания и отпускания |
|
Событие, которое отправляется, когда действие перетаскивания и отпускания завершается |
Настройка
Объект QStyleHints предоставляет некоторые свойства, относящиеся к операциям перетаскивания и отпускания:
- QStyleHints::startDragTime() описывает количество миллисекунд, которое пользователь должен удерживать кнопку мыши над объектом, прежде чем начнется перетаскивание.
- QStyleHints::startDragDistance() указывает, на какое расстояние пользователь должен переместить мышь, удерживая кнопку мыши, прежде чем движение будет интерпретироваться как перетаскивание.
-
QStyleHints::startDragVelocity() указывает, насколько быстро (в пикселях/секунду) пользователь должен перемещать мышь, чтобы начать перетаскивание. Значение
0означает, что такого ограничения нет.
Эти величины предоставляют разумные значения по умолчанию, которые соответствуют базовой системе окон, для использования, если вы предоставляете поддержку перетаскивания и отпускания в своих элементах управления.
Перетаскивание и отпускание в Qt Quick
Остальная часть документа в основном посвящена реализации перетаскивания и отпускания в C++. Для использования перетаскивания и отпускания внутри сцены Qt Quick прочитайте документацию для элементов Qt Quick Drag, DragEvent и DropArea, а также примеры Перетаскивание и отпускание в Qt Quick.
Перетаскивание
Чтобы начать перетаскивание, создайте объект QDrag и вызовите его функцию exec(). В большинстве приложений разумно начинать операцию перетаскивания и отпускания только после нажатия кнопки мыши и перемещения курсора на определенное расстояние. Однако самый простой способ включить перетаскивание из виджета — переопределить функцию mousePressEvent() виджета и запустить операцию перетаскивания и отпускания:
void MainWindow::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton
&& iconLabel->geometry().contains(event->pos())) {
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData;
mimeData->setText(commentEdit->toPlainText());
drag->setMimeData(mimeData);
drag->setPixmap(iconPixmap);
Qt::DropAction dropAction = drag->exec();
...
}
} Хотя пользователю может потребоваться некоторое время для завершения операции перетаскивания, с точки зрения приложения функция exec() является блокирующей функцией, которая возвращает одно из нескольких значений . Эти значения указывают, как завершилась операция, и подробно описаны ниже.
Обратите внимание, что функция exec() не блокирует основной цикл событий.
Для виджетов, которым необходимо различать щелчки мышью и перетаскивание, полезно переопределить функцию mousePressEvent() виджета, чтобы записать начальную позицию перетаскивания:
void DragWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
dragStartPosition = event->pos();
} Позже, в mouseMoveEvent(), мы можем определить, следует ли начинать перетаскивание, и создать объект перетаскивания для обработки операции:
void DragWidget::mouseMoveEvent(QMouseEvent *event)
{
if (!(event->buttons() & Qt::LeftButton))
return;
if ((event->pos() - dragStartPosition).manhattanLength()
< QApplication::startDragDistance())
return;
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData;
mimeData->setData(mimeType, data);
drag->setMimeData(mimeData);
Qt::DropAction dropAction = drag->exec(Qt::CopyAction | Qt::MoveAction);
...
} Этот конкретный подход использует функцию QPoint::manhattanLength(), чтобы получить приблизительную оценку расстояния между тем, где произошел щелчок мышью, и текущей позицией курсора. Эта функция жертвует точностью ради скорости и обычно подходит для этой цели.
Отпускание
Чтобы иметь возможность получать данные, сброшенные на виджет, вызовите setAcceptDrops(true) для виджета и переопределите обработчики событий dragEnterEvent() и dropEvent().
Например, следующий код включает события отпускания в конструкторе подкласса QWidget, позволяя полезно реализовывать обработчики событий отпускания:
Window::Window(QWidget *parent)
: QWidget(parent)
{
...
setAcceptDrops(true);
} Функция dragEnterEvent() обычно используется для информирования Qt о типах данных, которые виджет принимает. Вы должны переопределить эту функцию, если хотите получить QDragMoveEvent или QDropEvent в своих переопределениях dragMoveEvent() и dropEvent().
Следующий код показывает, как dragEnterEvent() может быть переопределён, чтобы сообщить системе перетаскивания и отпускания, что мы можем обрабатывать только обычный текст:
void Window::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("text/plain"))
event->acceptProposedAction();
} Функция dropEvent() используется для распаковки сброшенных данных и обработки их способом, подходящим для вашего приложения.
В следующем коде текст, предоставленный в событии, передаётся в QTextBrowser, а QComboBox заполняется списком MIME-типов, используемых для описания данных:
void Window::dropEvent(QDropEvent *event)
{
textBrowser->setPlainText(event->mimeData()->text());
mimeTypeCombo->clear();
mimeTypeCombo->addItems(event->mimeData()->formats());
event->acceptProposedAction();
} В этом случае мы принимаем предложенное действие без проверки того, что это такое. В реальном приложении может потребоваться вернуть из функции dropEvent() без принятия предложенного действия или обработки данных, если действие не имеет отношения. Например, мы можем выбрать игнорирование действий Qt::LinkAction, если мы не поддерживаем ссылки на внешние источники в нашем приложении.
Переопределение предложенных действий
Мы также можем проигнорировать предложенное действие и выполнить другое действие с данными. Для этого мы вызовем setDropAction() объекта события с предпочтительным действием из Qt::DropAction перед вызовом accept(). Это гарантирует, что используемое действие сброса заменит предложенное действие.
Для более сложных приложений переопределение dragMoveEvent() и dragLeaveEvent() позволит сделать определенные части ваших виджетов чувствительными к событиям отпускания и даст вам больший контроль над перетаскиванием и отпусканием в вашем приложении.
Наследование сложных виджетов
Определённые стандартные виджеты Qt предоставляют собственную поддержку перетаскивания и отпускания. При наследовании этих виджетов может потребоваться переопределить dragMoveEvent() в дополнение к dragEnterEvent() и dropEvent(), чтобы предотвратить предоставление базовым классом обработки перетаскивания и отпускания по умолчанию и обработать любые особые случаи, которые вас интересуют.
Действия перетаскивания и отпускания
В простейшем случае целевой объект действия перетаскивания и отпускания получает копию перетаскиваемых данных, а источник решает, следует ли удалить оригинал. Это описывается действием CopyAction. Целевой объект может также выбрать обработку других действий, в частности действий MoveAction и LinkAction. Если источник вызывает QDrag::exec() и она возвращает MoveAction, источник отвечает за удаление исходных данных, если он выберет это сделать. Объекты QMimeData и QDrag, созданные виджетом-источником, не должны удаляться - они будут уничтожены Qt. Целевой объект отвечает за получение прав собственности на данные, отправленные при операции перетаскивания и отпускания; это обычно делается путём сохранения ссылок на данные.
Если целевой объект понимает действие LinkAction, он должен сохранить собственную ссылку на исходную информацию; источнику не требуется выполнять дальнейшую обработку данных. Наиболее распространённое использование действий перетаскивания и отпускания — при выполнении перемещения внутри одного и того же виджета; см. раздел Действия сброса для получения дополнительной информации об этой функции.
Другое важное использование действий перетаскивания — при использовании типа ссылки, такого как text/uri-list, где перетаскиваемые данные фактически являются ссылками на файлы или объекты.
Добавление новых типов перетаскивания и отпускания
Перетаскивание и отпускание не ограничиваются текстом и изображениями. Любой тип информации может быть передан в операции перетаскивания и отпускания. Для перетаскивания информации между приложениями приложения должны уметь указывать друг другу, какие форматы данных они могут принимать и какие они могут генерировать. Это достигается с помощью MIME-типов. Объект QDrag, созданный источником, содержит список MIME-типов, которые он использует для представления данных (от наиболее подходящего к наименее подходящему), а целевой объект отпускания использует один из них для доступа к данным. Для распространённых типов данных функции удобства обрабатывают используемые MIME-типы прозрачно, но для пользовательских типов данных необходимо явно указать их.
Для реализации действий перетаскивания и отпускания для типа информации, не покрытого функциями удобства QDrag, первым и самым важным шагом является поиск подходящих существующих форматов: Агентство по распределению интернет-номеров (IANA) предоставляет иерархический список MIME-типов медиа в Институте информационных наук (ISI). Использование стандартных MIME-типов максимизирует межсетевое взаимодействие вашего приложения с другим программным обеспечением сейчас и в будущем.
Для поддержки дополнительного типа медиа просто установите данные в объекте QMimeData с помощью функции setData(), указав полный MIME-тип и QByteArray, содержащий данные в соответствующем формате. Следующий код берёт пиксель из метки и сохраняет его как файл Portable Network Graphics (PNG) в объекте QMimeData:
QByteArray output;
QBuffer outputBuffer(&output);
outputBuffer.open(QIODevice::WriteOnly);
imageLabel->pixmap().toImage().save(&outputBuffer, "PNG");
mimeData->setData("image/png", output);Конечно, в этом случае мы могли бы просто использовать setImageData() вместо этого, чтобы предоставить данные изображения в различных форматах:
mimeData->setImageData(QVariant(*imageLabel->pixmap()));
Подход с QByteArray по-прежнему полезен в этом случае, потому что он обеспечивает больший контроль над объемом данных, хранимых в объекте QMimeData.
Обратите внимание, что пользовательские типы данных, используемые в представлениях элементов, должны быть объявлены как метаобъекты, и что для них должны быть реализованы операторы потока.
Действия при падении
В модели буфера обмена пользователь может вырезать или скопировать исходную информацию, а затем вставить её позже. Аналогично, в модели перетаскивания пользователь может перетащить копию информации или перетащить саму информацию в новое место (переместив её). Модель перетаскивания имеет дополнительную сложность для программиста: программа не знает, хочет ли пользователь вырезать или скопировать информацию, пока операция не будет завершена. Это часто не имеет значения при перетаскивании информации между приложениями, но внутри приложения важно проверить, какое действие падения было использовано.
Мы можем переопределить mouseMoveEvent() для виджета и начать операцию перетаскивания с комбинацией возможных действий при падении. Например, мы можем захотеть убедиться, что перетаскивание всегда перемещает объекты в виджете:
void DragWidget::mouseMoveEvent(QMouseEvent *event)
{
if (!(event->buttons() & Qt::LeftButton))
return;
if ((event->pos() - dragStartPosition).manhattanLength()
< QApplication::startDragDistance())
return;
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData;
mimeData->setData(mimeType, data);
drag->setMimeData(mimeData);
Qt::DropAction dropAction = drag->exec(Qt::CopyAction | Qt::MoveAction);
...
} Действие, возвращаемое функцией exec(), может по умолчанию быть CopyAction если информация попадает в другое приложение, но если она попадает в другой виджет в том же приложении, мы можем получить другое действие падения.
Предложенные действия при падении могут быть отфильтрованы в функции dragMoveEvent() виджета. Однако, можно принять все предложенные действия в dragEnterEvent() и позволить пользователю решить, какие из них принять позже:
void DragWidget::dragEnterEvent(QDragEnterEvent *event)
{
event->acceptProposedAction();
} Когда происходит падение в виджете, вызывается обработчик dropEvent(), и мы можем обработать каждое возможное действие поочерёдно. Во-первых, мы обрабатываем операции перетаскивания и падения внутри одного и того же виджета:
void DragWidget::dropEvent(QDropEvent *event)
{
if (event->source() == this && event->possibleActions() & Qt::MoveAction)
return; В этом случае мы отказываемся обрабатывать операции перемещения. Каждый тип действия при падении, который мы принимаем, проверяется и обрабатывается соответствующим образом:
if (event->proposedAction() == Qt::MoveAction) {
event->acceptProposedAction();
// Process the data from the event.
} else if (event->proposedAction() == Qt::CopyAction) {
event->acceptProposedAction();
// Process the data from the event.
} else {
// Ignore the drop.
return;
}
...
} Обратите внимание, что мы проверили отдельные действия при падении в приведенном выше коде. Как упоминалось выше в разделе Переопределение предложенных действий, иногда необходимо переопределить предложенное действие при падении и выбрать другое из набора возможных действий при падении. Для этого необходимо проверить наличие каждого действия в значении, предоставленном функцией possibleActions() события, установить действие при падении с помощью setDropAction() и вызвать accept().
Прямоугольники падения
Функция dragMoveEvent() виджета может быть использована для ограничения падений определёнными частями виджета, принимая предложенные действия при падении только тогда, когда курсор находится в этих областях. Например, следующий код принимает любые предложенные действия при падении, когда курсор находится над дочерним виджетом (dropFrame):
void Window::dragMoveEvent(QDragMoveEvent *event)
{
if (event->mimeData()->hasFormat("text/plain")
&& event->answerRect().intersects(dropFrame->geometry()))
event->acceptProposedAction();
} Функция dragMoveEvent() также может быть использована, если вам нужно предоставить визуальную обратную связь во время операции перетаскивания, прокрутить окно или выполнить что-либо подходящее.
Буфер обмена
Приложения также могут обмениваться данными, помещая их в буфер обмена. Для доступа к нему вам необходимо получить объект QClipboard из объекта QApplication.
Класс QMimeData используется для представления данных, которые передаются в буфер обмена и из него. Для помещения данных в буфер обмена можно использовать удобные функции setText(), setImage() и setPixmap() для распространённых типов данных. Эти функции похожи на те, которые есть в классе QMimeData, за исключением того, что они также принимают дополнительный аргумент, который управляет тем, где хранятся данные: если указан Clipboard, данные помещаются в буфер обмена; если указана Selection, данные помещаются в выделение мыши (только для X11). По умолчанию данные помещаются в буфер обмена.
Например, мы можем скопировать содержимое QLineEdit в буфер обмена с помощью следующего кода:
QGuiApplication::clipboard()->setText(lineEdit->text(), QClipboard::Clipboard);
Данные с различными типами MIME также могут быть помещены в буфер обмена. Создайте объект QMimeData и задайте данные с помощью функции setData() так, как описано в предыдущем разделе; этот объект затем можно поместить в буфер обмена с помощью функции setMimeData().
Класс QClipboard может уведомлять приложение об изменениях в содержащихся в нём данных с помощью сигнала dataChanged(). Например, мы можем отслеживать буфер обмена, подключив этот сигнал к слоту в виджете:
connect(clipboard, &QClipboard::dataChanged,
this, &ClipWindow::updateClipboard); Слоту, подключенному к этому сигналу, можно прочитать данные в буфере обмена, используя один из типов MIME, который может быть использован для их представления:
void ClipWindow::updateClipboard()
{
QStringList formats = clipboard->mimeData()->formats();
for (const auto &format : formats) {
QByteArray data = clipboard->mimeData()->data(format);
// ...
} Сигнал selectionChanged() может быть использован на X11 для отслеживания выделения мыши.
Примеры
- Перетаскиваемые значки
- Перетаскиваемый текст
- Место падения
- Магниты холодильника
- Пазл с перетаскиванием
Взаимодействие с другими приложениями
В X11 используется общедоступный протокол XDND, в то время как в Windows Qt использует стандарт OLE, а Qt для macOS использует Cocoa Drag Manager. В X11 XDND использует MIME, поэтому никакого перевода не требуется. API Qt одинаков независимо от платформы. В Windows приложения, поддерживающие MIME, могут обмениваться данными, используя имена форматов буфера обмена, которые являются типами MIME. Уже некоторые приложения Windows используют соглашения об именовании MIME для своих форматов буфера обмена.
Пользовательские классы для перевода проприетарных форматов буфера обмена могут быть зарегистрированы путем переопределения QWinMime в Windows или QMacPasteboardMime в macOS.
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-6.0/dnd.html