拖放是在一个应用程序内或者多个应用程序之间传递信息的一种直观的现代操作方式。除了为剪贴板提供支持外,通常它还提供数据移动和复制的功能。
拖放操作包括两个截然不同的动作:拖动和放下。Qt窗口部件可以作为拖动点(darg site)、放下点(drop site)或者同时作为拖动点和放下点。
我们经常将文本文件推拽到notepate++等类型文本编辑器软件中。那么如何让Qt程序也能够支持这种操作呢?
我们需要在主窗口重新实现了来自父类的dragEnterEvent()
和 dropEvent()
函数。
protected:
virtual void dragEnterEvent(QDragEnterEvent* event) override;
virtual void dropEvent(QDropEvent* event) override;
在构造函数中,创建了一个QTextEdit
并且把它设置为中央窗口部件。默认情况下,QTextEdit
可以接受来自其他应用程序文本的拖动,并且如果用户在它上面放下一个文件,它将会把这个文件的内容填充到QTextEdit部件中。
由于拖放事件是从子窗口部件传递给父窗口部件的,所以通过禁用QTextEdit上的放下操作以及启用主窗口上的放下操作,就可以在整个MainWindow窗口中获得放下事件。
#include
#include
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
textEdit = new QTextEdit(this);
setCentralWidget(textEdit);
textEdit->setAcceptDrops(false);
setAcceptDrops(true);
setWindowTitle("Text Editor");
}
当用户把一个对象拖动到这个窗口部件上时,就会调用dragEnterEvent()
。如果对这个事件调用acceptProposedAction()
,就表明用户可以在这个窗口部件上拖放对象。默认情况下,窗口部件是不接受拖动的。Qt会自动改变光标来向用户说明这个窗口部件是不是有效的放下点。
这里,我们希望用户拖动的只能是文件,而非其他类型的东西。为了实现这一点,我们可以检查拖动的MIME
类型。
MIME
类型中的text/uri-list
用于存储一系列的统一资源标识符(Universal Re-source Identifier ,URI),它们可以是文件名、统―资源定位器(Uniform Resource Locator , URL,如 HTTP或者FTP路径),或者其他全局资源标识符。标准的MIME类型是由国际因特网地址分配委员会(Internet Assigned Numbers Authority , IANA)定义的,它们由类型、子类型信息以及分隔两者的斜线组·成。MME类通常由剪贴板和拖放系统使用,以识别不同类型的数据。可以从下面的网站得到正式的 MIME类型列表: http://www.iana.org/assignments/media-types/
void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
if(event->mimeData()->hasFormat("text/uri-list"))
{
event->acceptProposedAction();
}
}
当用户在窗口部件上放下一个对象时,就会调用dropEvent()
。.我们调用函数QMimeData::urls()
来获得QUrl
列表。通常,用户一次只拖动一个文件,但是通过拖动一个选择区域来同时拖动多个文件也是可能的。如果要拖放的URL不止一个,或者要拖放的URL,不是一个本地文件名,则会立即返回到原调用处。
QWidget也提供 dragMoveEvent()和 dragLeaveEvent()函数,但是在绝大多数应用程序中并不需要重新实现它们。
void MainWindow::dropEvent(QDropEvent *event)
{
QList<QUrl> urls = event->mimeData()->urls();
if(urls.empty())
return;
QString fileName = urls.first().toLocalFile();
if(fileName.isEmpty())
return;
if(ReadFile(fileName))
{
setWindowTitle(QString("%1-%2").arg(fileName).arg("Drag File"));
}
}
bool MainWindow::ReadFile(const QString &filename)
{
QFile file(filename);
file.open(QIODevice::ReadOnly | QIODevice::Text | QIODevice::Truncate);
if(false == file.isOpen())
{
return false;
}
QTextStream stream(&file);
textEdit->insertPlainText(stream.readAll());
file.flush();
file.close();
return true;
}
看看效果吧
我们将实现类似于一个下图的效果,但不需要向左向右的按键,通过拖拽目标实现移动。
思路:创建一个支持拖拽的QListWidget
子类ProjectListWidget
,并且作为该界面的一个部件。
在ProjectListWidget类中需要重写父类的五个事件,和一个私有方法以及一个坐标记录
protected:
virtual void mousePressEvent(QMouseEvent* event) override;
virtual void mouseMoveEvent(QMouseEvent* event) override;
virtual void dragEnterEvent(QDragEnterEvent* event) override;
virtual void dragMoveEvent(QDragMoveEvent* event) override;
virtual void dropEvent(QDropEvent* event) override;
private:
void performDrag();
private:
QPoint startPos;
在构造函数中,我们使列表框上的放下生效。
#include
#include
#include
ProjectListWidget::ProjectListWidget(QWidget* parent)
:QListWidget{parent}
{
setAcceptDrops(true);
}
当用户按下鼠标左键,就把鼠标位置保存到statPos
私有变量中。然后我们正常调用QListWidget 中mousePressEvent()
。
void ProjectListWidget::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
startPos = event->pos();
}
QListWidget::mousePressEvent(event);
}
当用户按住鼠标左键并移动鼠标光标时,就认为这是一个拖动的开始。我们计算当前鼠标位置和原来鼠标左键按下的点之间的距离-—这个“曼哈顿长度”(Manhattan Length)其实是从坐标原点到该矢量长度快速计算的近似值。如果这个距离大于或等于QApplication推荐的拖动起始距离值(通常是4个像素),那么就调用私有函数performDrag()
以启动拖动操作。这可以避免用户因为手握鼠标抖动而产生拖动。
void ProjectListWidget::mouseMoveEvent(QMouseEvent *event)
{
if(event->buttons() & Qt::LeftButton)
{
if(int distance = (event->pos() - startPos).manhattanLength();
distance >= QApplication::startDragDistance())
{
performDrag();
}
}
QListWidget::mouseMoveEvent(event);
}
在perfomDrag()
中,创建了一个类型为QDrag的对象,并且把this作为它的父对象。这个QDrag对象将数据存储在QMimeData
对象中。在这个实例中,我们利用QMineData::setText()
提供了作为text/plain
字符串的数据。QMimeData 提供了一些可用于处理最常用拖放类型(诸如图像、URL、颜色,等等)的函数,同时也可以处理任意由QByteArrays表宗的MiME类型。QDrag::setPiximap()
调用则可以在拖放发生时使图标随光标移动。
QDrag::exec()
调用启动并执行拖动操作;直到用户放下或取消此次拖动操作才会停止。它把所有支持的“拖放动作"(如 Qi: : CopyAction, Qt : : MoveAction和 Qt: : LinkAction)的组合作为其参数,并且返回被执行的拖放动作(如果没有执行任何动作,则返回Qt: IgnoreAction)。至于执行的是哪`个动作,取决于放下发生时源窗口部件是否允许、目标是否支持及按下了哪些组合键。在 exec()调用后,Qt拥有拖动对象的所有权并且可以在不需要它的时候删除它。
void ProjectListWidget::performDrag()
{
if(QListWidgetItem* item = currentItem();
nullptr != item)
{
QMimeData* mineData = new QMimeData();
mineData->setText(item->text());
QDrag* drag = new QDrag(this);
drag->setMimeData(mineData);
drag->setPixmap(QPixmap(":/icon.jpg"));
if(drag->exec(Qt::MoveAction) == Qt::MoveAction)
{
delete item;
item = nullptr;
}
}
}
ProjectListWidget窗口部件不仅能发起拖动,还可以接收同一个应用程序中来自另外一个ProjectListWidget部件的拖动。如果窗口部件是同个应用程序的一部分,QDragEnterEvent::source()
返回一个启动这个拖动的窗口部件的指针;否则,返回一个空指针值。
void ProjectListWidget::dragEnterEvent(QDragEnterEvent *event)
{
ProjectListWidget* source = reinterpret_cast<ProjectListWidget*>(event->source());
if(nullptr != source && source != this)
{
event->setDropAction(Qt::MoveAction);
event->accept();
}
}
dragMoveEvent()中的代码与dragEnterEvent()中编写的代码基本相同。因为需要重写QListWidget的函数实现(实际上是 QAbstractItemView的函数实现)。
void ProjectListWidget::dragMoveEvent(QDragMoveEvent *event)
{
ProjectListWidget* source = reinterpret_cast<ProjectListWidget*>(event->source());
if(nullptr != source && source != this)
{
event->setDropAction(Qt::MoveAction);
event->accept();
}
}
在dropEvent()中,我们使用QMimeData::text()
重新找回拖动的文本并随文本创建一个拖动项。还需要将事件作为“移动动作”来接受,从而告诉源窗口部件现在可以删除原来的拖动项了。
void ProjectListWidget::dropEvent(QDropEvent *event)
{
ProjectListWidget* source = reinterpret_cast<ProjectListWidget*>(event->source());
if(nullptr != source && source != this)
{
addItem(event->mimeData()->text());
event->setDropAction(Qt::MoveAction);
event->accept();
}
}
效果如下
拖放是在应用程序之间传递数据的有力机制。但是在某些情况下;,有可能在执行拖放时并未使用Qt的拖放工具。如果只是想在一个应用程序的窗口部件中移动数据,通常只要重新实现mousePressEvent()和 mouseReleaseEvent()函数就可以了。