目录
1、Qt拖放:拖放基本原理(QDrag类)
拖放操作包括两个动作:拖动(drag)和放下(drop或称为放置)。当被拖动时拖动的数据会被存储为MIME类型(见第6章文件对话框)的对象,MIME类型使用QMimeData类来描述。MIME类型通常由剪贴板和拖放系统使用,以识别不同类型的数据。
拖动点(drag site):拖动的起始位置。
放下点(drop site):被拖动的对象放下的位置,若部件不能接受拖动的对象,Qt会改变光标的形状(一个禁用形状)来向用户进行说明。
1、拖放的启动和结束
(1)、启动拖放:拖放通过调用QDrag::exec()函数而启动,该函数是一个阻塞函数(但不会阻塞主事件循环),这意味着在拖放操作结束之前,不会返回该函数,调用QDrag::exe()函数后,Qt拥有对拖动对象的所有权,并会在必要时将其删除。
2)、结束拖放:当用户放下拖动或取消拖动操作时结束拖放。
2、拖放产生的过程及事件
(1)、启动拖放后,会使数据被拖动,这时需要按住鼠标按键才能拖动需要拖动的数据,松开鼠标按键时意味着拖动结束。在这期间会产生如下事件
(2)、默认情况下,部件不接受放下事件。使用QWidget::setAcceptDrops()函数可设置部件是否接受放下事件(即,拖放完成时发送的事件)。只有在部件接受放下事件的情形下,才会产生以下事件。
QDragEnterEvent:拖动进入事件。当拖动操作进入部件时,该事件被发送到部件,忽略该事件,将会导至后续的拖放事件不能被发送,此时在该部件上光标通常会在外观上显示为禁用的图形。
QDragMoveEvnet:拖动移动事件。当拖动操作正在进行时,以及当具有焦点时按下键盘的修饰键(比如Ctrl)时,发送该事件,要使部件能接收到该事件,则该部件必须接受QDragEnterEvent事件。
QDropEvent:放下事件。在完成拖放操作时发送该事件,即当用户在部件上放下一个对象时,发送此事件。要使部件能接收到该事件,则该部件必须接受QDragEnterEvent事件,且不能忽略QDragMoveEvnt事件。
QDragLeaveEvent:当拖放操作离开部件时发送该事件,注意:要使部件能接收到该事件,必须要使拖动先进入该部件(即产生QDragEnterEvent事件),然后再离开该部件,才会产生QDragLeaveEvent事件。因很少使用该事件,因此本文不做重点介绍。
注:必须接受是指必须重新实现该事件的处理函数并接受该事件,不能忽略是指在处件事理函数中不明确调用ignore()函数忽略该事件。
2、自定义控件拖拽
2.1 Item类属性设置
我们拖拽在某一窗体之中,要根据其位置大小来进行位置的更换和添加,就需要我们对所有Item进行一个管理,这里我们使用索引index将每个不同的item位置进行一次定位,方便后续的修改位置、添加操作。
void Item::setVerticalIndex(int index)
{
current_index = index;
}
后续涉及到位置之间的移动我们需要将旧位置的item删除后在移动后的位置新建item并重新进行排序,因此在这里我们使用一个信号
signals:
void signalDroppedAndMoved(Item*);
在我们定义好自定义控件的各种属性之后,首先需要去重写一下鼠标移动事件,我们通过QMimeData来利用拖拽事件发送控件信息以及通过它来确定对应发送其信号的控件
void Item::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
{
press_pos = event->pos();
return event->accept();
}
QWidget::mousePressEvent(event);
}
void Item::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton) // 左键拖拽
{
if ((event->pos() - press_pos).manhattanLength() >= QApplication::startDragDistance())
{
QMimeData* mime = new QMimeData();
mime->setData(ITEM_MIME_KEY,QString::number(reinterpret_cast<qint64>(this)).toUtf8());
mime->setText(type);
QDrag *drag = new QDrag(this);
drag->setMimeData(mime);
drag->exec(Qt::MoveAction);
}
}
}
重新鼠标事件之后,我们需要拖拽的时候的一些判断,现在我们去重写dragMoveEvent和dragEnterEvent事件。
dragMoveEvent和dragEnterEvent如下
void Item::dragMoveEvent(QDragMoveEvent *event)
{
if (canDropMimeData(event))
{
event->accept();
}
QWidget::dragMoveEvent(event);
}
void Item::dragEnterEvent(QDragEnterEvent *event)
{
if (canDropMimeData(event))
{
event->accept();
}
QWidget::dragEnterEvent(event);
}
其中canDropMimeData函数如下
bool Item::canDropMimeData(QDropEvent *event)
{
const QMimeData *mime = event->mimeData();
if (mime->hasFormat(ITEM_MIME_KEY)) //整行拖拽
{
Item *item = reinterpret_cast<Item*>(mime->data(ITEM_MIME_KEY).toInt());
if (item)
{
return true;
}
}
return false;
}
2.2 container类属性设置
我们对于被拖动的自定义容器的操作在我们的容器中进行设置。
首先在构造函数中我们调用setAcceptDrops并给参数为true使其允许接受其它控件放置在其上。
然后我们定义一个QList列表将构造的Item放入其中,后续排序、移动位置会使用到。之后写添加和插入的函数。
void Container::addItem(QString type)
{
insertItem(type,-1);
}
Item *Container::insertItem(QString type, int index)
{
Item *item = new Item(type,this);
if (index < 0 || index >= items.count()) //添加到末尾
{
if (items.count() >= 1)
{
item->move(items.last()->pos().x() , items.last()->pos().y() + item->height());
}
items.append(item);
item->setVerticalIndex(items.count() - 1); //下标索引从0开始
if (items.count() > 0)
{
item->move(items.last()->geometry().topLeft());
}
}
else //插到中间的情况
{
items.insert(index,item);
for (int i=index;i<items.count();i++) //重新设置位置
{
items.at(i)->setVerticalIndex(i);
}
if (index + 1 < items.count())
{
item->move(items.at(index + 1)->geometry().topLeft());
}
else if (index > 0 )
{
item->move(items.at(index - 1)->geometry().topLeft());
}
}
item->show();
connect(item,&Item::signalDroppedAndMoved,this,[=](Item* from_item){
slotDroppedAndMove(from_item,item);
});
return item;
}
有了添加和插入函数后,我们还需要构造一个移动item的函数
void Container::moveItem(int from_index, int to_index)
{
if (from_index == to_index) // 很可能发生的自己和自己交换
{
return ;
}
if (from_index < 0 || to_index < 0)
{
return ;
}
// 交换item
Item* item = items.at(from_index);
items.removeAt(from_index);
if (from_index < to_index) // 下移
{
items.insert(to_index, item);
for (int i = from_index; i <= to_index; i++)
{
items.at(i)->setVerticalIndex(i);
}
}
else // 上移
{
items.insert(to_index, item);
for (int i = from_index; i >= to_index; i--)
{
items.at(i)->setVerticalIndex(i);
}
}
adjustBucketsPositionsWithAnimation(qMin(from_index, to_index));
}
在移动过程中,我们会利用之前QList中存储的位置以及Item信息,将列表里面的信息进行添加或是更新,并且在其中加入了过渡动画
void Container::adjustBucketsPositionsWithAnimation(int start, int end)
{
if (end == -1)
{
end = items.count();
}
else
{
end++;
}
int top = (start-1) >= 0 ? items.at(start-1)->geometry().bottom() : 0;
int max_width = 0;
if (start > 0)
{
max_width = this->width();
}
for (int i = start; i < end; i++)
{
Item* item = items.at(i);
if (max_width < item->width())
{
max_width = item->width();
}
if (top != item->pos().y())
{
QPropertyAnimation* ani = new QPropertyAnimation(item, "pos");
ani->setStartValue(item->pos());
ani->setEndValue(QPoint(item->pos().x(), top));
ani->setDuration(500);
ani->setEasingCurve(QEasingCurve::OutQuart);
connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater()));
ani->start();
}
top += item->height();
}
int height = 0;
if (items.size())
{
height = top + items.last()->height();
}
else
{
height = 50;
}
this->resize(max_width, height);
}
在之前我们在item控件中,定义了一个信号名为signalDroppedAndMoved(Item*)的信号,现在我们需要对其进行处理,信号槽的连接在插入函数里面。连接处使用了Lambda表达式调用Container类的成员函数进行移动,其函数实现如下
void Container::slotDroppedAndMove(Item *from, Item *to)
{
int from_index = items.indexOf(from);
int to_index = items.indexOf(to);
moveItem(from_index,to_index);
}
核心代码如上,接下来是演示demo
3、程序示例
示例代码连接:
https://download.youkuaiyun.com/download/Mmcom9426/91070489?spm=1001.2014.3001.5503