Введение
Перетаскивание элементов мышью между разными частями интерфейса (или даже между разными приложениями) — привычный паттерн взаимодействия, который пользователи интуитивно ожидают для списков, файлов, элементов канбан-доски. Qt Widgets поддерживает drag-and-drop через события QDragEnterEvent, QDropEvent и класс QMimeData для передачи произвольных данных в процессе перетаскивания. В статье реализуем перетаскивание элементов между двумя списками.
Концепция
Источник перетаскивания (виджет, с которого начинается drag) инициирует операцию через создание QDrag, заполняя его QMimeData нужными данными (текст, список URL, или произвольные кастомные данные с собственным MIME-типом) и вызывая exec(). Виджет-приёмник должен явно разрешить приём через setAcceptDrops(true) и обработать последовательность событий: dragEnterEvent (решить, принимать ли данный тип данных, вызвав event->acceptProposedAction()), опционально dragMoveEvent, и финальный dropEvent (фактически обработать сброшенные данные).
Пример кода: Drag-and-Drop в Qt Widgets
// draggablelistwidget.h
#pragma once
#include <QListWidget>
#include <QDrag>
#include <QMimeData>
#include <QMouseEvent>
class DraggableListWidget : public QListWidget
{
Q_OBJECT
public:
explicit DraggableListWidget(QWidget *parent = nullptr) : QListWidget(parent)
{
setAcceptDrops(true);
setDragEnabled(true);
}
protected:
void dragEnterEvent(QDragEnterEvent *event) override
{
if (event->mimeData()->hasText()) {
event->acceptProposedAction();
}
}
void dragMoveEvent(QDragMoveEvent *event) override
{
if (event->mimeData()->hasText()) {
event->acceptProposedAction();
}
}
void dropEvent(QDropEvent *event) override
{
if (event->mimeData()->hasText()) {
const QString text = event->mimeData()->text();
addItem(text);
// Если перетаскивание из другого виджета этого же типа,
// удаляем элемент из источника при перемещении (а не копировании)
if (event->source() != this) {
if (auto *sourceList = qobject_cast<DraggableListWidget *>(event->source())) {
delete sourceList->currentItem();
}
}
event->acceptProposedAction();
}
}
void startDrag(Qt::DropActions supportedActions) override
{
QListWidgetItem *item = currentItem();
if (!item) return;
auto *mimeData = new QMimeData;
mimeData->setText(item->text());
auto *drag = new QDrag(this);
drag->setMimeData(mimeData);
drag->exec(supportedActions);
}
};
// Использование: две связанные доски с задачами
#include <QHBoxLayout>
#include <QWidget>
#include "draggablelistwidget.h"
class TaskBoard : public QWidget
{
Q_OBJECT
public:
TaskBoard(QWidget *parent = nullptr) : QWidget(parent)
{
auto *todoList = new DraggableListWidget(this);
todoList->addItems({"Изучить Qt", "Написать статью", "Провести код-ревью"});
auto *doneList = new DraggableListWidget(this);
auto *layout = new QHBoxLayout(this);
layout->addWidget(todoList);
layout->addWidget(doneList);
}
};
Пояснения к коду
Переопределение startDrag() определяет, что происходит при начале перетаскивания элемента из списка: создаётся QMimeData с текстом текущего элемента, оборачивается в QDrag и запускается через exec() — этот вызов блокирует выполнение до завершения операции перетаскивания (отпускания кнопки мыши над валидной или невалидной целью).
В dropEvent обрабатывается сброс данных: новый элемент добавляется в список-приёмник, а если перетаскивание происходило из другого экземпляра DraggableListWidget (event->source() != this), исходный элемент удаляется из списка-источника — это реализует семантику «перемещения» элемента между списками, а не копирования. Если убрать эту проверку и удаление, поведение стало бы копированием вместо перемещения.
Подводные камни
- Отсутствие проверки
event->source()перед приведением типа черезqobject_castможет привести к попытке вызвать методы на объекте неожиданного типа, если перетаскивание происходит из виджета другого класса, поддерживающего тот же MIME-тип —qobject_castвозвращаетnullptrпри несовпадении типа, что в примере корректно обрабатывается условиемif (auto *sourceList = ...), но об этой проверке легко забыть. - Удаление
currentItem()источника без проверки, что это действительно тот элемент, который был перетащен. Если пользователь успел изменить выделение в списке-источнике между началом перетаскивания и завершением сброса (что технически возможно в сложных сценариях с несколькими источниками ввода),currentItem()может вернуть не тот элемент — для надёжности стоит хранить явную ссылку на перетаскиваемый элемент вQMimeData(например, его индекс), а не полагаться на текущее выделение в моментdropEvent. - Перетаскивание между приложениями (а не только внутри одного) требует, чтобы MIME-тип данных был достаточно стандартным и предсказуемым для приложения-приёмника — кастомные MIME-типы данных хорошо работают для внутреннего drag-and-drop, но не гарантируют совместимость с другими приложениями.
- Игнорирование
Qt::DropActions(Copy/Move/Link) и визуальной индикации текущего действия. Без явной настройки поддерживаемых действий и соответствующего курсора пользователь не получает визуальной обратной связи о том, копируется элемент или перемещается — это особенно важно при перетаскивании с зажатыми модификаторами (Ctrl для копирования вместо перемещения, как принято в большинстве файловых менеджеров).