拖放

拖放是在一个应用程序内或多个应用程序间传递信息的一种直观的现代操作方式。除了为剪贴板提供支持外,通常它还提供数据移动和复制的功能。在实现的过程中,可以复用拖动代码来实现对剪贴板的支持,之所以能够复用,是因为拖动与剪贴板的功能机理都是以QMimeData类为基础的,而QMimeData是一个可以提供不同格式数据的类。

 使拖动生效

拖动操作包括两个动作:拖动和放下。Qt窗口部件可以作为拖动点、放下点或者同时作为拖动点和放下点。

例如以Qt程序是以QTextEdit作为中央窗口部件,当用户从桌面上或文件资源管理器中拖动一个文本文件并且在这个应用程序上放下时,该应用程序就会将文本文件载入到QTextEdit中:

class MainWindow:public QMainWindow
{
    Q_OBJECT
public:
    MainWindow();
protected:
    void dragEnterEvent(QDragEnterEvent* event);
    void dropEvent(QDropEvent * event);
private:
    bool readFile(const QString& fileName);
    QTextEdit * textEdit;
};

在构造函数中,创建了一个QTextEdit并且把它设置为中央窗口部件。默认情况下,QTextEdit可以接受来自其他应用程序文本的拖动,并且如果用户在它上面放下一个文件,它将会把这个文件的名称插入到文本中。

MainWindow::MainWindow()
{
    textEdit = new QTextEdit;
    setCentralWidget(textEdit);
    textEdit->setAcceptDrops(false);
    setAcceptDrops(true);
    
    setWindowTitle(tr("Text Editor"));
}
 由于拖放时间是从子窗口部件传递给父窗口部件的,所以通过禁用QTextEdit上的放下操作以及启用父窗口MainWindow上的放下操作,就可以在整个MainWindow窗口中获得放下事件。 

void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
    if (event->mimeData()->hasFormat("text/uri-list"))
        event->acceptProposedAction();
}
 当用户把一个对象拖动到这个窗口部件上时,就会调用dragEnterEvent()。如果对这个事件调用acceptProposedAction()就表明用户可以在这个窗口部件上拖放对象。默认情况下,窗口部件是不接受拖动的。Qt会自动改变光标来向用户说明这个窗口部件是不是有效的放下点。 

MIME类型就是设定某种扩展名文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义文件名,以及一些媒体文件打开方式。

这里我们希望用户拖动的只能是文件,而非其他类型的东西。为了实现这一点,可以检查拖动的MIME类型。MIME类型中的text/uri-list用于存储一系列的同一资源标识符,他们可以是文件名、同一资源定位器或者是其他的全局资源标识符。标准的MIME类型是由国际因特网地址分配委员会定义的,他们由类型、子类型信息以及两者的斜线构成。MIME类型通常由剪贴板和拖放系统使用,以识别不同类型的数据。

void MainWindow::dropEvent(QDropEvent *event)
{
    QList<QUrl> urls = event->mimeData()->urls();
    if (url.isEmpty())
        return;
    
    QString fileName = urls.first().toLocalFile();
    if (fileName.isEmpty())
        return;
    
    if (readFile(fileName))
        setWindowTitle(tr("%1 - %2").arg(fileName).arg(tr("Drag File")));
}
 当用户在窗口部件上放下一个对象时,就会调用dropEvent()。通过调用函数QMimeData::urls()获得QUrl列表。通常,用户一次只拖动一个文件,但是通过拖动一个选择区域来同时拖动文件也是可能的。如果要拖动的Url不止一个,或者要拖放的URL不是一个本地文件名,则会立即返回原调用处。 

QWidget也提供了dragMoveEvent()和dropLeaveEvent()函数,但是绝大多数应用程序中并不需要重新实现他们。

下例实现,在左右两个ProjectListWidget中拖动文件。

#ifndef PROJECTLISTWIDGET_H
#define PROJECTLISTWIDGET_H

#include <QListWidget>

class ProjectListWidget : public QListWidget
{
    Q_OBJECT

public:
    ProjectListWidget(QWidget *parent = 0);

protected:
    void mousePressEvent(QMouseEvent *event);
    void mouseMoveEvent(QMouseEvent *event);
    void dragEnterEvent(QDragEnterEvent *event);
    void dragMoveEvent(QDragMoveEvent *event);
    void dropEvent(QDropEvent *event);

private:
    void performDrag();

    QPoint startPos;
};

#endif
实现文件:

void ProjectListWidget::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton)
        startPos = event->pos();
    QListWidget::mousePressEvent(event);
}

void ProjectListWidget::mouseMoveEvent(QMouseEvent *event)
{
    if (event->buttons() & Qt::LeftButton) {
        int distance = (event->pos() - startPos).manhattanLength();
        if (distance >= QApplication::startDragDistance()) //计算当前鼠标位置和起始位置的哈密顿长度,如果该长度大于等于QApplication推荐的拖动起始距离值,则调用私有函数performDrag()以启动拖动操作。
            performDrag();
    }
    QListWidget::mouseMoveEvent(event);
}


void ProjectListWidget::dragEnterEvent(QDragEnterEvent *event)
{
    ProjectListWidget *source =
            qobject_cast<ProjectListWidget *>(event->source());
    if (source && source != this) {
        event->setDropAction(Qt::MoveAction);
        event->accept();
    }
}
//ProjectListWidget窗口部件不仅能发起拖动,还可以接受同一个应用程序中来自另外一个ProjectListWidget部件的拖动。如果窗口部件是同一个应用程序的一部分,则QDragEvent::source()返回一个启动这个拖动的窗口部件的指针,否则返回一个空指针。使用qobject_cast<T>(),确保这个拖动来自ProjectListWidget,如果一切无误,则告诉Qt预备将该动作视为一个移动操作。



void ProjectListWidget::dragMoveEvent(QDragMoveEvent *event)
{
    ProjectListWidget *source =
            qobject_cast<ProjectListWidget *>(event->source());
    if (source && source != this) {
        event->setDropAction(Qt::MoveAction);
        event->accept();
    }
}
//dragMoveEvent()代码和dragEnterEvent()中编写的代码基本相同,这样是必要的,因为需要重写QListWidget的函数实现(实际上是QAbstractItemView的函数实现)。


void ProjectListWidget::dropEvent(QDropEvent *event)
{
    ProjectListWidget *source =
            qobject_cast<ProjectListWidget *>(event->source());
    if (source && source != this) {
        addItem(event->mimeData()->text());
        event->setDropAction(Qt::MoveAction);
        event->accept();
    }
}
//使用QMimeData::text()重新找回拖动的文本并随文本创建一个拖动项,还需要把该事件作为“移动动作”来接受,从而告诉源窗口部件现在可以删除原来的拖动项了。


void ProjectListWidget::performDrag()
{
    QListWidgetItem *item = currentItem();
    if (item) {
        QMimeData *mimeData = new QMimeData;
        mimeData->setText(item->text());

        QDrag *drag = new QDrag(this);//在拖动函数中创建了 QDrag对象,并把this作为他的父对象。
        drag->setMimeData(mimeData);
        drag->setPixmap(QPixmap(":/images/person.png")); //可设置在拖放时,图标随光标移动
        if (drag->exec(Qt::MoveAction) == Qt::MoveAction) //QDrag::exec()调用启动并执行拖动操作,直到用户放下或取消此次拖动操作才会停止。它把所支持的“拖	动动作”(如Qt::CopyAction, Qt::MoveAction和Qt::LinkAction)组合作为其参,并且返回被执行的拖放动作(如果没有执行任何动作,则返回Qt::IgnoreAction)。	至于执行的是哪个动作,取决于放下发生时源窗口部件是否允许、目标是否支持以及按下了那些组合键。在exec()调用后,Qt拥有拖动对象的所有权并且可以在不需要	他的时候删除它。

            delete item;
    }
}

拖放是在应用程序之间传递数据的有力机制,但是在某些情况下,有可能在执行拖放时并未使用Qt的拖放工具。如果只是想在一个应用程序的窗口部件中移动数据,通常只需要实现mousePressEvent()和mouseReleaseEvent()即可。


支持自定义的拖动类型:

可以调用QMimeData::setText()来创建一个文本拖动,并且使用QMimeData:urls()来重新找回text/uri-list拖动的内容。如果想拖动纯文本、超文本、图像、URL甚至颜色,则可以只使用QMimeData类而不必要采用其他手段,但如果拖动的是自定义数据,则必须选择如下三种方式之一:

(1)使用QMimeData::setData(),可以提供任意数据作为QByteArray的内容,并且在随后利用QMimeData::data()提取这些数据

(2)可以通过子类化QMimeData并且重新实现formats()和retrieveData()来处理自定义数据类型

(3)对于在简单应用程序中的拖放操作,可以子类化QMimeData并且利用我们所需要的任意数据结构来存储数据


剪贴板处理技术

多数应用程序都通过某一种或几种方式来使用Qt的内置剪贴板处理技术。例如,QTextEdit类除了提供快捷键支持外,还提供cut()、copy()、paste()槽,所以几乎不需要其他额外的代码。

当编写自己的类时,可以通过QApplication::clipboard()来访问剪贴板,它会返回一个指向应用程序QClipboard对象的指针。处理系统剪贴板也是很容易的:调用setText()、setImage()、setPixmap()把数据放到剪贴板中,并且调用text()、image()、pixmap()来重新获得数据即可。

对某些应用程序,需要剪贴操作某些自定义数据类型,则需要子类化QMimeData,并且重新实现一些虚函数。

如果应用程序通过一个自定义QMimeData子类支持拖放,则可以只复用QMimeData子类并且利用setMimeData()函数把它放到剪贴板中。可以对剪贴按使用mimeData()来获得数据。

如果想在剪贴板中的内容发生变化时立即得到通报,可以通过建立QClipboard::dataChanged()信号和自定义槽的连接来实现。

例如以下是如何在文本编辑器中重新执行mouseReleaseEvent()以支持鼠标中键粘贴功能的代码:

void MyTextEditor::mouseReleaseEvent(QMouseEvent* event)
{
    QClipboard* clipboard = QApplication::clipboard();
    if (event->button() == Qt::MidButton && clipboard->supportsSelection())
    {
        QString text = clipboard->text(QClipboard::Selection);
        pasteText(text);
    }
}




Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐