Qt 学习之路 2(52):使用拖放

拖放(Drag and Drop),通常会简称为 DnD,是现代软件开发中必不可少的一项技术。它提供了一种能够在应用程序内部甚至是应用程序之间进行信息交换的机制。操作系统与应用程序之间进行的剪贴板内容的交换,也可以被认为是拖放的一部分。

拖放其实是由两部分组成的:拖动和释放。拖动是将被拖放对象进行移动,释放是将被拖放对象放下。前者是一个按下鼠标按键并移动的过程,后者是一个松开鼠标按键的过程;通常这两个操作之间的鼠标按键是被一直按下的。当然,这只是一种普遍的情况,其它情况还是要看应用程序的具体实现。对于 Qt 而言,一个组件既可以作为被拖动对象进行拖动,也可以作为释放掉的目的地对象,或者二者都是。

在下面的例子中(来自 C++ GUI Programming with Qt4, 2nd Edition),我们将创建一个程序,将操作系统中的文本文件拖进来,然后在窗口中读取内容。

 
 
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
protected:
void dragEnterEvent(QDragEnterEvent *event);
void dropEvent(QDropEvent *event);
private:
bool readFile(const QString &fileName);
QTextEdit *textEdit;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = 0);
    ~MainWindow();
protected:
    void dragEnterEvent(QDragEnterEvent *event);
    void dropEvent(QDropEvent *event);
private:
    bool readFile(const QString &fileName);
    QTextEdit *textEdit;
};

注意到我们需要重写dragEnterEvent()dropEvent()两个函数。顾名思义,前者是拖放进入的事件,后者是释放鼠标的事件。

 
 
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
textEdit = new QTextEdit;
setCentralWidget(textEdit);

textEdit->setAcceptDrops(false);
setAcceptDrops(true);

setWindowTitle(tr("Text Editor"));
}

MainWindow::~MainWindow()
{
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    textEdit = new QTextEdit;
    setCentralWidget(textEdit);
 
    textEdit->setAcceptDrops(false);
    setAcceptDrops(true);
 
    setWindowTitle(tr("Text Editor"));
}
 
MainWindow::~MainWindow()
{
}

在构造函数中,我们创建了QTextEdit的对象。默认情况下,QTextEdit可以接受从其它应用程序拖放过来的文本类型的数据。如果用户把一个文件拖到这面,默认会把文件名插入到光标位置。但是我们希望让MainWindow读取文件内容,而不是仅仅插入文件名,所以我们在MainWindow中加入了拖放操作。首先要把QTextEditsetAcceptDrops()函数置为 false,并且把MainWindowsetAcceptDrops()置为 true,这样我们就能够让MainWindow截获拖放事件,而不是交给QTextEdit处理。

 
 
void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("text/uri-list")) {
event->acceptProposedAction();
}
}
1
2
3
4
5
6
void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
    if (event->mimeData()->hasFormat("text/uri-list")) {
        event->acceptProposedAction();
    }
}

当用户将对象拖动到组件上面时,系统会回调dragEnterEvent()函数。如果我们在事件处理代码中调用acceptProposeAction()函数,就可以向用户暗示,你可以将拖动的对象放在这个组件上。默认情况下,组件是不会接受拖放的。如果我们调用了这个函数,那么 Qt 会自动以光标样式的变化来提示用户是否可以将对象放在组件上。在这里,我们希望告诉用户,窗口可以接受拖放,但是我们仅接受某一种类型的文件,而不是全部文件。我们首先检查拖放文件的 MIME 类型信息。MIME 类型由 Internet Assigned Numbers Authority (IANA) 定义,Qt 的拖放事件使用 MIME 类型来判断拖放对象的类型。关于 MIME 类型的详细信息,请参考 http://www.iana.org/assignments/media-types/。MIME 类型为 text/uri-list 通常用来描述一个 URI 列表。这些 URI 可以是文件名,可以是 URL 或者其它的资源描述符。如果发现用户拖放的是一个 text/uri-list 数据(即文件名),我们便接受这个动作。

 
 
void MainWindow::dropEvent(QDropEvent *event)
{
QList<QUrl> urls = event->mimeData()->urls();
if (urls.isEmpty()) {
return;
}

QString fileName = urls.first().toLocalFile();
if (fileName.isEmpty()) {
return;
}

if (readFile(fileName)) {
setWindowTitle(tr("%1 - %2").arg(fileName, tr("Drag File")));
}
}

bool MainWindow::readFile(const QString &fileName)
{
bool r = false;
QFile file(fileName);
QString content;
if(file.open(QIODevice::ReadOnly)) {
content = file.readAll();
r = true;
}
textEdit->setText(content);
return r;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void MainWindow::dropEvent(QDropEvent *event)
{
    QList<QUrl> urls = event->mimeData()->urls();
    if (urls.isEmpty()) {
        return;
    }
 
    QString fileName = urls.first().toLocalFile();
    if (fileName.isEmpty()) {
        return;
    }
 
    if (readFile(fileName)) {
        setWindowTitle(tr("%1 - %2").arg(fileName, tr("Drag File")));
    }
}
 
bool MainWindow::readFile(const QString &fileName)
{
    bool r = false;
    QFile file(fileName);
    QString content;
    if(file.open(QIODevice::ReadOnly)) {
        content = file.readAll();
        r = true;
    }
    textEdit->setText(content);
    return r;
}

当用户将对象释放到组件上面时,系统回调dropEvent()函数。我们使用QMimeData::urls()来获得QUrl的一个列表。通常,这种拖动应该只有一个文件,但是也不排除多个文件一起拖动。因此我们需要检查这个列表是否为空,如果不为空,则取出第一个,否则立即返回。最后我们调用readFile()函数读取文件内容。这个函数的内容很简单,我们前面也讲解过有关文件的操作,这里不再赘述。现在可以运行下看看效果了。

----

接下来的例子也是来自 C++ GUI Programming with Qt4, 2nd Edition。在这个例子中,我们将创建左右两个并列的列表,可以实现二者之间数据的相互拖动。

 
 
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;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
};

ProjectListWidget是我们的列表的实现。这个类继承自QListWidget。在最终的程序中,将会是两个ProjectListWidget的并列。

 
 
ProjectListWidget::ProjectListWidget(QWidget *parent)
: QListWidget(parent)
{
setAcceptDrops(true);
}
1
2
3
4
5
ProjectListWidget::ProjectListWidget(QWidget *parent)
    : QListWidget(parent)
{
    setAcceptDrops(true);
}

构造函数我们设置了setAcceptDrops(),使ProjectListWidget能够支持拖动操作。

 
 
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())
performDrag();
}
QListWidget::mouseMoveEvent(event);
}

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

QDrag *drag = new QDrag(this);
drag->setMimeData(mimeData);
drag->setPixmap(QPixmap(":/images/person.png"));
if (drag->exec(Qt::MoveAction) == Qt::MoveAction)
delete item;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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())
            performDrag();
    }
    QListWidget::mouseMoveEvent(event);
}
 
void ProjectListWidget::performDrag()
{
    QListWidgetItem *item = currentItem();
    if (item) {
        QMimeData *mimeData = new QMimeData;
        mimeData->setText(item->text());
 
        QDrag *drag = new QDrag(this);
        drag->setMimeData(mimeData);
        drag->setPixmap(QPixmap(":/images/person.png"));
        if (drag->exec(Qt::MoveAction) == Qt::MoveAction)
            delete item;
    }
}

mousePressEvent()函数中,我们检测鼠标左键点击,如果是的话就记录下当前位置。需要注意的是,这个函数最后需要调用系统自带的处理函数,以便实现通常的那种操作。这在一些重写事件的函数中都是需要注意的,前面我们已经反复强调过这一点。

mouseMoveEvent()函数判断了,如果鼠标在移动的时候一直按住左键(也就是 if 里面的内容),那么就计算一个manhattanLength()值。从字面上翻译,这是个“曼哈顿长度”。首先来看看event.pos() - startPos是什么。在mousePressEvent()函数中,我们将鼠标按下的坐标记录为 startPos,而event.pos()则是鼠标当前的坐标:一个点减去另外一个点,这就是一个位移向量。所谓曼哈顿距离就是两点之间的距离(按照勾股定理进行计算而来),也就是这个向量的长度。然后继续判断,如果大于QApplication::startDragDistance(),我们才进行释放的操作。当然,最后还是要调用系统默认的鼠标拖动函数。这一判断的意义在于,防止用户因为手的抖动等因素造成的鼠标拖动。用户必须将鼠标拖动一段距离之后,我们才认为他是希望进行拖动操作,而这一距离就是QApplication::startDragDistance()提供的,这个值通常是 4px。

performDrag()开始处理拖放的过程。这里,我们要创建一个QDrag对象,将 this 作为 parent。QDrag使用QMimeData存储数据。例如我们使用QMimeData::setText()函数将一个字符串存储为 text/plain 类型的数据。QMimeData提供了很多函数,用于存储诸如 URL、颜色等类型的数据。使用QDrag::setPixmap()则可以设置拖动发生时鼠标的样式。QDrag::exec()会阻塞拖动的操作,直到用户完成操作或者取消操作。它接受不同类型的动作作为参数,返回值是真正执行的动作。这些动作的类型一般为Qt::CopyActionQt::MoveActionQt::LinkAction。返回值会有这几种动作,同时还会有一个Qt::IgnoreAction用于表示用户取消了拖放。这些动作取决于拖放源对象允许的类型,目的对象接受的类型以及拖放时按下的键盘按键。在exec()调用之后,Qt 会在拖放对象不需要的时候释放掉。

 
 
void ProjectListWidget::dragMoveEvent(QDragMoveEvent *event)
{
ProjectListWidget *source =
qobject_cast(event->source());
if (source && source != this) {
event->setDropAction(Qt::MoveAction);
event->accept();
}
}

void ProjectListWidget::dropEvent(QDropEvent *event)
{
ProjectListWidget *source =
qobject_cast(event->source());
if (source && source != this) {
addItem(event->mimeData()->text());
event->setDropAction(Qt::MoveAction);
event->accept();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void ProjectListWidget::dragMoveEvent(QDragMoveEvent *event)
{
    ProjectListWidget *source =
            qobject_cast(event->source());
    if (source && source != this) {
        event->setDropAction(Qt::MoveAction);
        event->accept();
    }
}
 
void ProjectListWidget::dropEvent(QDropEvent *event)
{
    ProjectListWidget *source =
            qobject_cast(event->source());
    if (source && source != this) {
        addItem(event->mimeData()->text());
        event->setDropAction(Qt::MoveAction);
        event->accept();
    }
}

dragMoveEvent()dropEvent()相似。首先判断事件的来源(source),由于我们是两个ProjectListWidget之间相互拖动,所以来源应该是ProjectListWidget类型的(当然,这个 source 不能是自己,所以我们还得判断source != this)。dragMoveEvent()中我们检查的是被拖动的对象;dropEvent()中我们检查的是释放的对象:这二者是不同的。

Qt 学习之路 2(52):使用拖放的更多相关文章

  1. Qt 学习之路 2(53):自定义拖放数据

    Qt 学习之路 2(53):自定义拖放数据 豆子  2013年5月26日  Qt 学习之路 2  13条评论上一章中,我们的例子使用系统提供的拖放对象QMimeData进行拖放数据的存储.比如使用QM ...

  2. Qt 学习之路 2(67):访问网络(3)

    Qt 学习之路 2(67):访问网络(3) 豆子 2013年11月5日 Qt 学习之路 2 16条评论 上一章我们了解了如何使用我们设计的NetWorker类实现我们所需要的网络操作.本章我们将继续完 ...

  3. Qt 学习之路 2(66):访问网络(2)

    Home / Qt 学习之路 2 / Qt 学习之路 2(66):访问网络(2) Qt 学习之路 2(66):访问网络(2)  豆子  2013年10月31日  Qt 学习之路 2  27条评论 上一 ...

  4. Qt 学习之路 2(54):剪贴板

    Qt 学习之路 2(54):剪贴板 豆子 2013年6月8日 Qt 学习之路 2 2条评论 剪贴板的操作经常和前面所说的拖放技术在一起使用.大家对剪贴板都很熟悉.我们可以简单地把它理解成一个数据存储池 ...

  5. Qt 学习之路 2(51):布尔表达式树模型

    Qt 学习之路 2(51):布尔表达式树模型 豆子 2013年5月15日 Qt 学习之路 2 17条评论 本章将会是自定义模型的最后一部分.原本打算结束这部分内容,不过实在不忍心放弃这个示例.来自于 ...

  6. Qt 学习之路 2(49):自定义只读模型

    Qt 学习之路 2(49):自定义只读模型 豆子 2013年5月5日 Qt 学习之路 2 18条评论 model/view 模型将数据与视图分割开来,也就是说,我们可以为不同的视图,QListView ...

  7. Qt 学习之路 2(46):视图和委托

    Home / Qt 学习之路 2 / Qt 学习之路 2(46):视图和委托 Qt 学习之路 2(46):视图和委托  豆子  2013年3月11日  Qt 学习之路 2  63条评论 前面我们介绍了 ...

  8. Qt 学习之路 2(38):存储容器

    Qt 学习之路 2(38):存储容器 豆子 2013年1月14日 Qt 学习之路 2 38条评论 存储容器(containers)有时候也被称为集合(collections),是能够在内存中存储其它特 ...

  9. Qt 学习之路 2(32):贪吃蛇游戏(2)

    Qt 学习之路 2(32):贪吃蛇游戏(2) 豆子 2012年12月27日 Qt 学习之路 2 55条评论 下面我们继续上一章的内容.在上一章中,我们已经完成了地图的设计,当然是相当简单的.在我们的游 ...

随机推荐

  1. C99标准新特性的说明

    C99标准新特性的说明   一.说明 ====== 这里的讨论的是C语言的国际标准,即国际标准化组织ISO,制定的C语言标准.历史上ISO制定过4个版本的C语言标准,他们分别是:C90(ISO/IEC ...

  2. 数据仓库-数据采集-ETL漫谈

    数据仓库之ETL漫谈ETL,Extraction-Transformation-Loading的缩写,中文名称为数据抽取.转换和加载.大多数据仓库的数据架构可以概括为:数据源-->ODS(操作型 ...

  3. [luogu3369/bzoj3224]普通平衡树(splay模板、平衡树初探)

    解题关键:splay模板题整理. 如何不加入极大极小值?(待思考) #include<cstdio> #include<cstring> #include<algorit ...

  4. 使用Java创建XML数据

    ------------siwuxie095                         工程名:TestCreateXML 包名:com.siwuxie095.xml 类名:CreateXML. ...

  5. 面试题:各大公司Java后端开发面试题总结 已看1 背1 有用 链接有必要看看

    ThreadLocal(线程变量副本)       --整理 Synchronized实现内存共享,ThreadLocal为每个线程维护一个本地变量. 采用空间换时间,它用于线程间的数据隔离,为每一个 ...

  6. sql平时小总结

    sql练习:举例子: 1.CREATE TABLE IF NOT EXISTS zz0 (number INT(11)); CREATE TABLE IF NOT EXISTS zz1 (number ...

  7. R dataframe 遗忘, which 矩阵搜索

    A data frame is used for storing data tables. It is a list of vectors of equal length. For example, ...

  8. 【原创】boost::recursive_mutex请小心使用

    recursive_mutex会减少死锁几率?恩看上去的确如此,但是事实上呢?我们从BOOST的官方逻辑去理解: recursive_mutex类可多次进入锁,这样在递归时可以避免一次死锁的几率,这是 ...

  9. java.lang.NoSuchMethodError: org.objectweb.asm.ClassVisitor.visit(IILjava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)V

    异常完整信息 严重: Servlet.service() for servlet RegServlet threw exception java.lang.NoSuchMethodError: org ...

  10. small cell 在安防领域的应用探讨

    在安防领域,最核心的问题是:如何有效区分“内部人员”与“外部人员”.所谓“有效”包含两点意思,一是安全,尽可能地过滤出“外部人员”.二是效率,即尽可能无干扰地或较小干扰地使“内部人员”通过.所有的安全 ...