关于Qt的model-view部分就告一段落,今天我们开始新的部分。或许有些朋友觉得前面的部分说得很简单。对此我也没有办法,毕竟,Qt是一个很庞大的库,一时半会根本不可能穷尽所有内容,并且我也有很多东西不知道,有时候也必须去查找资料才能明白。

  今天开始的部分是关于Qt提供的一些通用算法。这部分内容来自C++ GUI Programming with Qt 4, 2nd Edition。

  <QtAlgorithms>提供了一系列通用的模板函数,用于实现容器上面的基本算法。这部分算法很多依赖于STL风格的遍历器(还记得前面曾经说过的Java风格的遍历器和STL风格的遍历器吗?)。实际上,C++ STL也提供了很多通用算法,包含在<algorithm> 头文件内。这部分算法对于Qt容器同样也是适用的。因此,如果你想使用的算法在Qt的<QtAlgorithms>头文件中没有包含,那么就可以使用STL的算法代替,这并不会产生什么冲突。这里我们来说几个Qt中的通用算法。虽然这些算法都是很简单的,但是,库函数往往会比自己编写的更有效率,因此还是推荐使用系统提供的函数的。

  首先是qFind()函数。qFind()函数会在容器中查找一个特定的值。它的参数中有一个起始位置和终止位置,如果被查找的元素存在,函数返回第一个匹配项的位置,否则则返回终止位置。注意,我们这里说的“位置”,实际上是STL风格的遍历器。我们知道,使用STL风格遍历器是可以反映一个位置的。例如下面的例子,i的值将是list.begin() + 1,而j会是list.end():

QStringList list;
list << "Emma" << "Karl" << "James" << "Mariette";
QStringList::iterator i = qFind(list.begin(), list.end(), "Karl");
QStringList::iterator j = qFind(list.begin(), list.end(), "Petra");
qBinaryFind()的行为很像qFind(),所不同的是,qBinaryFind()是二分查找算法,它只适用于查找排序之后的集合,而qFind()则是标准的线性查找。通常,二分查找法使用条件更为苛刻,但是效率也会更高。

  qFill()会使用给定值对容器进行填充。例如:

QLinkedList<int> list(10);
qFill(list.begin(), list.end(), 1009);

  正如其他基于遍历器的算法一样,qFill()也可以针对容器的一部分进行操作,例如下面的代码将会把vector的前5位设置成1009,而最后5位设置为2013:

QVector<int> vect(10);
qFill(vect.begin(), vect.begin() + 5, 1009);
qFill(vect.end() - 5, vect.end(), 2013);

  qCopy()算法可以实现将一个容器中的元素复制到另一个容器,例如:

QVector<int> vect(list.count());
qCopy(list.begin(), list.end(), vect.begin());

  qCopy()也可以用于同一容器中的元素的复制。qCopy()操作成功的关键是源容器和目的容器的范围不会发生溢出。例如如下代码,我们将把一个列表的最后两个元素复制给前两个元素:

qCopy(list.begin(), list.begin() + 2, list.end() - 2);

  qSort()实现了容器元素的递增排序,使用起来也很简单:

qSort(list.begin(), list.end());

  默认情况下,qSort()将使用 < 运算符进行元素的比较。这暗示如果需要的话,你必须定义 < 运算符。如果需要按照递减排序,需要将qGreater<T>()当作第三个参数传给qSort()函数。例如:

qSort(list.begin(), list.end(), qGreater<int>());
注意,这里的T实际上是容器的泛型类型。实际上,我们可以利用第三个参数对排序进行定义。例如,我们自定义的数据类型中有一个大小写不敏感的QString的小于比较函数:

bool insensitiveLessThan(const QString &str1, const QString &str2)
{
        return str1.toLower() < str2.toLower();
}

  那么,我们可以这样使用qSort()从而可以利用这个函数:

QStringList list;
// ...
qSort(list.begin(), list.end(), insensitiveLessThan);

  qStableSort()函数类似与qSort(),所不同之处在于它是稳定排序。稳定排序是算法设计上的一个名词,意思是,在排序过程中,如果有两个元素相等,那么在排序结果中这两个元素的先后顺序同排序前的原始顺序是一致的。举个例子,对于一个序列:a1, a5, a32, a31, a4,它们的大小顺序是a1 < a31 = a32 < a4 < a5,那么稳定排序之后的结果应该是 a1, a32, a31, a4, a5,也就是相等的元素在排序结果中出现的顺序和原始顺序是一致的。稳定排序在某些场合是很有用的,比如,现在有一份按照学号排序的学生成绩单。你想按照成绩高低重新进行排序,对于成绩一样的学生,还是遵循原来的学号顺序。这时候就要稳定排序了。

  qDeleteAll()函数将对容器中存储的所有指针进行delete操作。这个函数仅在容器元素是指针的情形下才适用。执行过这个函数之后,容器中的指针均被执行了delete运算,但是这些指针依然被存储在容器中,成为野指针,你需要调用容器的 clear()函数来避免这些指针的误用:

qDeleteAll(list);
list.clear();

  qSwap()函数可以交换两个元素的位置。例如:

 

int x1 = line.x1();
int x2 = line.x2();
if (x1 > x2)
        qSwap(x1, x2);

  最后,在<QtGlobal>头文件中,也定义了几个有用的函数。这个头文件被其他所有的头文件include了,因此你不需要显式的include这个头文件了。

  在这个头文件中有这么几个函数:qAbs()返回参数的绝对值,qMin()和qMax()则返回两个值的最大值和最小值。
前面我们在介绍QString的最后部分曾经提到了QByteArray这个类。现在我们就首先对这个类进行介绍。

  QByteArray具有类似与QString的API。它也有相应的函数,比如left(), right(), mid()等。这些函数不仅名字和QString一样,而且也具有几乎相同的功能。QByteArray可以存储原生的二进制数据和8位编码的文本数据。这句话怎么理解呢?我们知道,计算机内部所有的数据都是以0和1的形式存储的。这种形式就是二进制。比如一串0、1代码:1000,计算机并不知道它代表的是什么,这需要由上下文决定:它可以是整数8,也可以是一个ARGB的颜色(准确的说,整数8的编码并不是这么简单,但我们姑且这个理解吧)。对于文件,即便是一个文本文件,读出时也可以按照二进制的形式读出,这就是二进制格式。如果把这些二进制的0、1串按照编码解释成一个个字符,就是文本形式了。因此,QByteArray实际上是原生的二进制,但是也可以当作是文本,因此拥有文本的一些操作。但是,我们还是建议使用QString表示文本,重要的原因是,QString支持Unicode。

  为了方便期间,QByteArray自动的保证“最后一个字节之后的那个位”是‘\0‘。这就使得 QByteArray可以很容易的转换成const char *,也就是上一章节中我们提到的那两个函数。同样,作为原生二进制存储,QByteArray中间也可以存储‘\0‘,而不必须是‘\0‘在最后一位。

  在有些情况下,我们希望把数据存储在一个变量中。例如,我有一个数组,既希望存整数,又希望存浮点数,还希望存 string。对于Java来说,很简单,只要把这个数组声明成Object[]类型的。这是什么意思呢?实际上,这里用到的是继承。在Java 中,int和float虽然是原生数据类型,但是它们都有分别对应一个包装类Integer和Float。所有这些Integer、Float和 String都是继承于Object,也就是说,Integer、Float和String都是一个(也就是is-a的关系)Object,这样,Object的数组就可以存储不同的类型。但是,C++中没有这样一个Object类,原因在于,Java是单根的,而C++不是。在Java中,所有类都可以上溯到Object类,但是C++中没有这么一个根。那么,怎么实现这么的操作呢?一种办法是,我们都存成string类,比如int i=10,我就存"10"字符串。简单的数据类型固然可以,可复杂一些的呢?比如一个颜色?难道要把ARGB所有的值都转化成string?这种做法很复杂,而且失去了C++的类型检查等好处。于是我们想另外的办法:创建一个Object类,这是一个“很大很大的”类,里面存储了几乎所有的数据类型,比如下面的代码:
 

class Object  
{  
public:  
    int intValue;  
    float floatValue;  
    string stringValue;  
}; 

  这个类怎么样?它就足以存储int、float和string了。嗯,这就是我们的思路,也是Qt的思路。在Qt中,这样的类就是QVariant。

  QVariant可以保存很多Qt的数据类型,包括QBrush、QColor、QCursor、 QDateTime、QFont、QKeySequence、QPalette、QPen、QPixmap、QPoint、QRect、QRegion、 QSize和QString,并且还有C++基本类型,如int、float等。QVariant还能保存很多集合类型,如 QMap<QString, QVariant>, QStringList和QList<QVariant>。item view classes,数据库模块和QSettings都大量使用了QVariant类,,以方便我们读写数据。

  QVariant也可以进行嵌套存储,例如

QMap<QString, QVariant> pearMap;  
pearMap["Standard"] = 1.95;  
pearMap["Organic"] = 2.25;  
 
QMap<QString, QVariant> fruitMap;  
fruitMap["Orange"] = 2.10;  
fruitMap["Pineapple"] = 3.85;  
fruitMap["Pear"] = pearMap; 

  QVariant被用于构建Qt Meta-Object,因此是QtCore的一部分。当然,我们也可以在GUI模块中使用,例如

QIcon icon("open.png");  
QVariant variant = icon;  
// other function  
QIcon icon = variant.value<QIcon>(); 

  我们使用了value<T>()模版函数,获取存储在QVariant中的数据。这种函数在非GUI数据中同样适用,但是,在非GUI模块中,我们通常使用toInt()这样的一系列to...()函数,如toString()等。

 

  如果你觉得QVariant提供的存储数据类型太少,也可以自定义QVariant的存储类型。被 QVariant存储的数据类型需要有一个默认的构造函数和一个拷贝构造函数。为了实现这个功能,首先必须使用Q_DECLARE_METATYPE() 宏。通常会将这个宏放在类的声明所在头文件的下面:

Q_DECLARE_METATYPE(BusinessCard) 

  然后我们就可以使用:

BusinessCard businessCard;  
QVariant variant = QVariant::fromValue(businessCard);  
// ...  
if (variant.canConvert<BusinessCard>()) {  
    BusinessCard card = variant.value<BusinessCard>();  
    // ...  
} 

  由于VC 6的编译器限制,这些模板函数不能使用,如果你使用这个编译器,需要使用qVariantFromValue(), qVariantValue<T>()和qVariantCanConvert<T>()这三个宏。

  如果自定义数据类型重写了<<和>>运算符,那么就可以直接在QDataStream中使用。不过首先需要使用qRegisterMetaTypeStreamOperators<T>().宏进行注册。这就能够让QSettings使用操作符对数据进行操作,例如

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

  DnD 其实是由两部分组成的:Drag 和 Drop。Drag 是将被拖放对象“拖动”,Drop 是将被拖放对象“放下”,前者一般是一个按下鼠标的过程,而后者则是一个松开鼠标的过程,这两者之间鼠标一直是被按下的。当然,这只是一种通常的情况,其他情况还是要看应用程序的具体实现。对于 Qt 而言,widget既可以作为 drag 对象,也可以作为 drop 对象,或者二者都是。

  下面的一个例子来自 C++ GUI Programming with Qt 4, 2nd Edition。在这个例子中,我们创建一个程序,可以将系统中的文本文件拖放过来,然后在窗口中读取内容。

  mainwindow.h

#ifndef MAINWINDOW_H  
#define MAINWINDOW_H  
 
#include <QtGui>  
 
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;  
};  
 
#endif // MAINWINDOW_H 
mainwindow.cpp

#include "mainwindow.h"  
 
MainWindow::MainWindow(QWidget *parent)  
    : QMainWindow(parent)  
{  
    textEdit = new QTextEdit;  
    setCentralWidget(textEdit);  
 
    textEdit->setAcceptDrops(false);  
    setAcceptDrops(true);  
 
    setWindowTitle(tr("Text Editor"));  
}  
 
MainWindow::~MainWindow()  
{  
}  
 
void MainWindow::dragEnterEvent(QDragEnterEvent *event)  
{  
    if (event->mimeData()->hasFormat("text/uri-list")) {  
        event->acceptProposedAction();  
    }  
}  
 
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);  
    QTextStream in(&file);  
    QString content;  
    if(file.open(QIODevice::ReadOnly)) {  
        in >> content;  
        r = true;  
    }  
    textEdit->setText(content);  
    return r;  
} 
main.cpp

#include <QtGui/QApplication>  
#include "mainwindow.h"  
 
int main(int argc, char *argv[])  
{  
    QApplication a(argc, argv);  
    MainWindow w;  
    w.show();  
    return a.exec();  
} 

  这里的代码并不是很复杂。在MainWindow中,一个QTextEdit作为窗口中间的widget。这个类中有两个protected的函数:dragEnterEvent() 和 dropEvent(),这两个函数都是继承自 QWidget,看它们的名字就知道这是两个事件,而不仅仅是signal。

  在构造函数中,我们创建了 QTextEdit 的对象。默认情况下,QTextEdit 可以接受从其他的应用程序拖放过来的文本类型的信息。如果用户把一个文件拖到这里面,那么就会把文件名插入到文本的当前位置。但是我们希望让 MainWindow 读取文件内容,而不仅仅是插入文件名,所以我们在MainWindow中对 drop 事件进行了处理,因此要把QTextEdit的setAcceptDrops()函数置为false,并且把MainWindow的 setAcceptDrops()置为true,以便让MainWindow对 drop 事件进行处理。

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

 

  当用户将对象释放到组件上面时,dropEvent() 函数会被回调。我们使用 QMimeData::urls()来或者 QUrl 的一个list。通常,这种拖动应该只用一个文件,但是也不排除多个文件一起拖动。因此我们需要检查这个list是否为空,如果不为空,则取出第一个。如果不成立,则立即返回。最后我们调用 readFile() 函数读取文件内容。关于读取操作我们会在以后的章节中详细说明,这里不再赘述。

  好了,至此我们的小程序就解释完毕了,运行一下看看效果吧!

  对于拖动和脱离,Qt 也提供了类似的函数:dragMoveEvent() 和 dragLeaveEvent(),不过对于大部分应用而言,这两个函数的使用率要小得多。
很长时间没有来写博客了,前段时间一直在帮同学弄一个 spring-mvc 的项目,今天终于做完了,不过公司里面又要开始做 flex 4,估计还会忙一段时间吧!

  接着上次的说,上次说到了拖放技术,今天依然是一个例子,同样是来自《C++ GUI Programming with Qt 4, 2nd Edition》的。

  这次的 demo 还算是比较实用:实现的是两个 list 之间的数据互拖。在很多项目中,这一需求还是比较常见的吧!下面也就算是抛砖引玉了啊!

  projectlistwidget.h

#ifndef PROJECTLISTWIDGET_H  
#define PROJECTLISTWIDGET_H  
 
#include <QtGui>  
 
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 // PROJECTLISTWIDGET_H 

  projectlistwidget.cpp

#include "projectlistwidget.h"  
 
ProjectListWidget::ProjectListWidget(QWidget *parent)  
    : QListWidget(parent)  
{  
    setAcceptDrops(true);  
}  
 
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;  
    }  
}  
 
void ProjectListWidget::dragEnterEvent(QDragEnterEvent *event)  
{  
    ProjectListWidget *source =  
            qobject_cast<ProjectListWidget *>(event->source());  
    if (source && source != this) {  
        event->setDropAction(Qt::MoveAction);  
        event->accept();  
    }  
}  
 
void ProjectListWidget::dragMoveEvent(QDragMoveEvent *event)  
{  
    ProjectListWidget *source =  
            qobject_cast<ProjectListWidget *>(event->source());  
    if (source && source != this) {  
        event->setDropAction(Qt::MoveAction);  
        event->accept();  
    }  
}  
 
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();  
    }  
}  
我们从构造函数开始看起。Qt 中很多组件是可以接受拖放的,但是默认动作都是不允许的,因此在构造函数中,我们调用 setAcceptDrops(true); 函数,让组件能够接受拖放事件。

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

  然后我们重写了 mouseMoveEvent() 事件。下面还是先来看看代码:

void ProjectListWidget::mouseMoveEvent(QMouseEvent *event)  
{  
    if (event->buttons() & Qt::LeftButton) {  
        int distance = (event->pos() - startPos).manhattanLength();  
        if (distance >= QApplication::startDragDistance())  
            performDrag();  
    }  
    QListWidget::mouseMoveEvent(event);  
} 

  在这里判断了如果鼠标拖动的时候一直按住左键(也就是 if 里面的内容),那么就计算一个 manhattanLength() 值。从字面上翻译,这是个“曼哈顿长度”。这是什么意思呢?我们看一下 event.pos() - startPos 是什么。还记得在 mousePressEvent() 函数中,我们将鼠标按下的坐标记录为 startPos,而 event.pos() 则是鼠标当前的坐标:一个点减去另外一个点,没错,这就是向量!其实,所谓曼哈顿距离就是两点之间的距离(至于为什么叫这个奇怪的名字,大家查查百科就知道啦!),也就是这个向量的长度。下面又是一个判断,如果大于 QApplication::startDragDistance(),我们才进行 drag 的操作。当然,最后还是要调用系统默认的鼠标拖动函数。这一判断的意义在于,防止用户因为手的抖动等因素造成的鼠标拖动。用户必须将鼠标拖动一段距离之后,我们才认为他是希望进行拖动操作,而这一距离就是 QApplication::startDragDistance() 提供的,这个值通常是 4px。
performDrag() 开始处理拖放过程。我们创建了一个 QDrag 对象,将 this 作为 parent。QDrag 使用 QMimeData 存储数据。例如我们使用 QMimeData::setText() 函数将一个字符串存储为 text/plain 类型的数据。QMimeData 提供了很多函数,用于存储诸如 URL、颜色等类型的数据。使用 QDrag::setPixmap() 则可以设置拖动发生时鼠标的样式。QDrag::exec() 会阻塞拖动的操作,直到用户完成操作或者取消操作。它接受不同类型的动作作为参数,返回值是真正执行的动作。这些动作的类型为 Qt::CopyAction,Qt::MoveAction 和 Qt::LinkAction。返回值会有这三种动作,同时增加一个 Qt::IgnoreAction 用于表示用户取消了拖放。这些动作取决于拖放源对象允许的类型,目的对象接受的类型以及拖放时按下的键盘按键。在 exec() 调用之后,Qt 会在拖放对象不需要的时候 delete 掉它。

  ProjectListWidget 不仅能够发出拖动事件,而且能够接受同一应用程序中的不同 ProjectListWidget 对象的数据。在 dragEnterEvent() 中,我们使用 event->source() 获取这样的对象:如果拖放数据来自同一类型的对象,并且来自同一应用程序则返回其指针,否则返回 NULL。我们使用 qobject_cast 宏将指针转换成 ProjectListWidget* 类型,然后设置接受 Qt::MoveAction 类型的拖动。dragMoveEvent() 则和这个函数具有相同的代码,因为我们需要重写拖动移动的代码。

  最后在 dropEvent() 函数中,我们取出 QDrag 中的 mimeData 数据,调用 addItem() 添加到当前的列表中。这样,一个相对完整的拖放的代码就完成了。

  拖放技术是 Qt 中功能强大的一个技术,但是对于不涉及数据的同一组件中拖动,或许仅仅简单的实现 mouse event 就足够了,具体还是要自己斟酌啦!
 前面的例子都是使用的系统提供的拖放对象 QMimeData 进行拖放数据的存储,比如使用 QMimeData::setText() 创建文本,使用 QMimeData::urls() 创建 URL 对象。但是,如果你希望使用一些自定义的对象作为拖放数据,比如自定义类等等,单纯使用 QMimeData 可能就没有那么容易了。为了实现这种操作,我们可以从下面三种实现方式中选择一个:

  将自定义数据作为 QByteArray 对象,使用 QMimeData::setData() 函数作为二进制数据存储到 QMimeData 中,然后使用 QMimeData::Data() 读取;

  继承 QMimeData,重写其中的 formats() 和 retrieveData() 函数操作自定义数据;

  如果拖放操作仅仅发生在同一个应用程序,可以直接继承 QMimeData,然后使用任意合适的数据结构进行存储。

  第一种方法不需要继承任何类,但是有一些局限:即是拖放不会发生,我们也必须将自定义的数据对象转换成 QByteArray 对象;如果你希望支持很多种拖放的数据,那么每种类型的数据都必须使用一个 QMimeData 类,这可能会导致类爆炸;如果数据很大的话,这种方式可能会降低系统的可维护性。然而,后两种实现方式就不会有这些问题,或者说是能够减小这种问题,并且能够让我们有完全控制权。

  我们先来看一个应用,使用 QTableWidget 来进行拖放操作,拖放的类型包括 plain/text,plain/html 和 plain/csv。如果使用第一种实现方法,我们的代码将会如下所示:

void MyTableWidget::mouseMoveEvent(QMouseEvent *event)  
{  
    if (event->buttons() & Qt::LeftButton) {  
        int distance = (event->pos() - startPos).manhattanLength();  
        if (distance >= QApplication::startDragDistance())  
            performDrag();  
    }  
    QTableWidget::mouseMoveEvent(event);  
}  
 
void MyTableWidget::performDrag()  
{  
    QString plainText = selectionAsPlainText();  
    if (plainText.isEmpty())  
        return;  
 
    QMimeData *mimeData = new QMimeData;  
    mimeData->setText(plainText);  
    mimeData->setHtml(toHtml(plainText));  
    mimeData->setData("text/csv", toCsv(plainText).toUtf8());  
 
    QDrag *drag = new QDrag(this);  
    drag->setMimeData(mimeData);  
    if (drag->exec(Qt::CopyAction | Qt::MoveAction) == Qt::MoveAction)  
        deleteSelection();  
}  
对于这段代码,我们应该已经很容易的理解:在 performDrag() 函数中,我们调用 QMimeData 的 setText() 和 setHTML() 函数存储 plain/text 和 plain/html 数据,使用 setData() 将 text/csv 类型的数据作为二进制 QByteArray 类型存储。

QString MyTableWidget::toCsv(const QString &plainText)  
{  
    QString result = plainText;  
    result.replace("\\", "\\\\");  
    result.replace("\"", "\\\"");  
    result.replace("\t", "\", \"");  
    result.replace("\n", "\"\n\"");  
    result.prepend("\"");  
    result.append("\"");  
    return result;  
}  
 
QString MyTableWidget::toHtml(const QString &plainText)  
{  
    QString result = Qt::escape(plainText);  
    result.replace("\t", "<td>");  
    result.replace("\n", "\n<tr><td>");  
    result.prepend("<table>\n<tr><td>");  
    result.append("\n</table>");  
    return result;  
}  

  toCsv() 和 toHtml() 函数将数据取出并转换成我们需要的 csv 和 html类型的数据。例如,下面的数据

Red  Green  Blue 
Cyan Yellow Magenta

  转换成 csv 格式为:

"Red", "Green", "Blue" 
"Cyan", "Yellow", "Magenta"

  转换成 html 格式为:
<table>

  <tr><td>Red<td>Green<td>Blue

  <tr><td>Cyan<td>Yellow<td>Magenta

  </table>

  在放置的函数中我们像以前一样使用:

void MyTableWidget::dropEvent(QDropEvent *event)  
{  
    if (event->mimeData()->hasFormat("text/csv")) {  
        QByteArray csvData = event->mimeData()->data("text/csv");  
        QString csvText = QString::fromUtf8(csvData);  
        // ...  
        event->acceptProposedAction();  
    } else if (event->mimeData()->hasFormat("text/plain")) {  
        QString plainText = event->mimeData()->text();  
        // ...  
        event->acceptProposedAction();  
    }  
}  

  虽然我们接受三种数据类型,但是在这个函数中我们只接受两种类型。至于 html 类型,我们希望如果用户将 QTableWidget 的数据拖到一个 HTML 编辑器,那么它就会自动转换成 html 代码,但是我们不计划支持将外部的 html 代码拖放到 QTableWidget 上。为了让这段代码能够工作,我们需要在构造函数中设置 setAcceptDrops(true) 和 setSelectionMode(ContiguousSelection)。

  好了,上面就是我们所说的第一种方式的实现。这里并没有给出完整的实现代码,大家可以根据需要自己实现一下试试。下面我们将按照第二种方法重新实现这个需求。
 

class TableMimeData : public QMimeData  
{  
    Q_OBJECT  
 
public:  
    TableMimeData(const QTableWidget *tableWidget,  
                  const QTableWidgetSelectionRange &range);  
 
    const QTableWidget *tableWidget() const { return myTableWidget; }  
    QTableWidgetSelectionRange range() const { return myRange; }  
    QStringList formats() const;  
 
protected:  
    QVariant retrieveData(const QString &format,  
                          QVariant::Type preferredType) const;  
 
private:  
    static QString toHtml(const QString &plainText);  
    static QString toCsv(const QString &plainText);  
 
    QString text(int row, int column) const;  
    QString rangeAsPlainText() const;  
 
    const QTableWidget *myTableWidget;  
    QTableWidgetSelectionRange myRange;  
    QStringList myFormats;  
};  

  为了避免存储具体的数据,我们存储 table 和选择区域的坐标的指针。

TableMimeData::TableMimeData(const QTableWidget *tableWidget,  
                             const QTableWidgetSelectionRange &range)  
{  
    myTableWidget = tableWidget;  
    myRange = range;  
    myFormats << "text/csv" << "text/html" << "text/plain";  
}  
 
QStringList TableMimeData::formats() const 
{  
    return myFormats;  
} 
构造函数中,我们对私有变量进行初始化。formats() 函数返回的是被 MIME 数据对象支持的数据类型列表。这个列表是没有先后顺序的,但是最佳实践是将“最适合”的类型放在第一位。对于支持多种类型的应用程序而言,有时候会直接选用第一个符合的类型存储。

QVariant TableMimeData::retrieveData(const QString &format,  
                                     QVariant::Type preferredType) const 
{  
    if (format == "text/plain") {  
        return rangeAsPlainText();  
    } else if (format == "text/csv") {  
        return toCsv(rangeAsPlainText());  
    } else if (format == "text/html") {  
        return toHtml(rangeAsPlainText());  
    } else {  
        return QMimeData::retrieveData(format, preferredType);  
    }  
} 

  函数 retrieveData() 将给出的 MIME 类型作为 QVariant 返回。参数 format 的值通常是 formats() 函数返回值之一,但是我们并不能假定一定是这个值之一,因为并不是所有的应用程序都会通过 formats() 函数检查 MIME 类型。一些返回函数,比如 text(), html(), urls(), imageData(), colorData() 和 data() 实际上都是在 QMimeData 的 retrieveData() 函数中实现的。第二个参数 preferredType 给出我们应该在 QVariant 中存储哪种类型的数据。在这里,我们简单的将其忽略了,并且在 else 语句中,我们假定 QMimeData 会自动将其转换成所需要的类型。

void MyTableWidget::dropEvent(QDropEvent *event)  
{  
    const TableMimeData *tableData =  
            qobject_cast<const TableMimeData *>(event->mimeData());  
 
    if (tableData) {  
        const QTableWidget *otherTable = tableData->tableWidget();  
        QTableWidgetSelectionRange otherRange = tableData->range();  
        // ...  
        event->acceptProposedAction();  
    } else if (event->mimeData()->hasFormat("text/csv")) {  
        QByteArray csvData = event->mimeData()->data("text/csv");  
        QString csvText = QString::fromUtf8(csvData);  
        // ...  
        event->acceptProposedAction();  
    } else if (event->mimeData()->hasFormat("text/plain")) {  
        QString plainText = event->mimeData()->text();  
        // ...  
        event->acceptProposedAction();  
    }  
    QTableWidget::mouseMoveEvent(event);  
} 

  在放置的函数中,我们需要按照我们自己定义的数据类型进行选择。我们使用 qobject_cast 宏进行类型转换。如果成功,说明数据来自同一应用程序,因此我们直接设置 QTableWidget 相关 数据,如果转换失败,我们则使用一般的处理方式。
 剪贴板的操作经常和前面所说的拖放技术在一起使用,因此我们现在先来说说剪贴板的相关操作。

  大家对剪贴板都很熟悉。我们可以简单的把它理解成一个数据的存储池,可以把外面的数据放置进去,也可以把里面的数据取出来。剪贴板是由操作系统维护的,所以这提供了跨应用程序数据交互的一种方式。Qt 已经为我们封装好很多关于剪贴板的操作,因此我们可以在自己的应用中很容易的实现。下面还是从代码开始:

  clipboarddemo.h

#ifndef CLIPBOARDDEMO_H  
#define CLIPBOARDDEMO_H  
 
#include <QtGui/QWidget>  
 
class ClipboardDemo : public QWidget  
{  
    Q_OBJECT  
 
public:  
    ClipboardDemo(QWidget *parent = 0);  
 
private slots:  
    void setClipboard();  
    void getClipboard();  
};  
 
#endif // CLIPBOARDDEMO_H 

  clipboarddemo.cpp

#include <QtGui>  
#include "clipboarddemo.h"  
 
ClipboardDemo::ClipboardDemo(QWidget *parent)  
    : QWidget(parent)  
{  
    QVBoxLayout *mainLayout = new QVBoxLayout(this);  
    QHBoxLayout *northLayout = new QHBoxLayout;  
    QHBoxLayout *southLayout = new QHBoxLayout;  
 
    QTextEdit *editor = new QTextEdit;  
    QLabel *label = new QLabel;  
    label->setText("Text Input: ");  
    label->setBuddy(editor);  
    QPushButton *copyButton = new QPushButton;  
    copyButton->setText("Set Clipboard");  
    QPushButton *pasteButton = new QPushButton;  
    pasteButton->setText("Get Clipboard");  
 
    northLayout->addWidget(label);  
    northLayout->addWidget(editor);  
    southLayout->addWidget(copyButton);  
    southLayout->addWidget(pasteButton);  
    mainLayout->addLayout(northLayout);  
    mainLayout->addLayout(southLayout);  
 
    connect(copyButton, SIGNAL(clicked()), this, SLOT(setClipboard()));  
    connect(pasteButton, SIGNAL(clicked()), this, SLOT(getClipboard()));  
}  
 
void ClipboardDemo::setClipboard()  
{  
    QClipboard *board = QApplication::clipboard();  
    board->setText("Text from Qt Application");  
}  
 
void ClipboardDemo::getClipboard()  
{  
    QClipboard *board = QApplication::clipboard();  
    QString str = board->text();  
    QMessageBox::information(NULL, "From clipboard", str);  
}  

#include "clipboarddemo.h"  
 
#include <QtGui>  
#include <QApplication>  
 
int main(int argc, char *argv[])  
{  
    QApplication a(argc, argv);  
    ClipboardDemo w;  
    w.show();  
    return a.exec();  
}  

  main() 函数很简单,就是把我们的 ClipboardDemo 类显示了出来。我们重点来看 ClipboardDemo 中的代码。

  构造函数同样没什么复杂的内容,我们把一个label。一个 textedit 和两个 button摆放到窗口中。这些代码已经能够很轻易的写出来了;然后进行了信号槽的连接。

void ClipboardDemo::setClipboard()  
{  
    QClipboard *board = QApplication::clipboard();  
    board->setText("Text from Qt Application");  
}  
 
void ClipboardDemo::getClipboard()  
{  
    QClipboard *board = QApplication::clipboard();  
    QString str = board->text();  
    QMessageBox::information(NULL, "From clipboard", str);  
} 

  在 slot 函数中,我们使用 QApplication::clipboard() 函数访问到系统剪贴板。这个函数的返回值是 QClipboard 的指针。我们可以从这个类的 API 中看到,通过 setText(),setImage() 或者 setPixmap() 函数可以将数据放置到剪贴板内,也就是通常所说的剪贴或者复制的操作;使用 text(),image() 或者 pixmap() 函数则可以从剪贴板获得数据,也就是粘贴。

 

  另外值得说的是,通过上面的例子可以看出,QTextEdit 默认就是支持 Ctrl+C, Ctrl+V 等快捷键操作的。不仅如此,很多 Qt 的组件都提供了很方便的操作,因此我们需要从文档中获取具体的信息,从而避免自己重新去发明轮子。

  QClipboard 提供的数据类型很少,如果需要,我们可以继承 QMimeData 类,通过调用 setMimeData() 函数让剪贴板能够支持我们自己的数据类型。

  在 X11 系统中,鼠标中键(一般就是滚轮)可以支持剪贴操作的。为了实现这一功能,我们需要向 QClipboard::text() 函数传递 QClipboard::Selection 参数。例如,我们在鼠标按键释放的事件中进行如下处理:

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

  这里的 supportsSelection() 在 X11 平台返回 true,其余平台都是返回 false 的。

  另外,QClipboard 提供了 dataChanged() 信号,以便监听剪贴板数据变化。
今天开始进入 Qt 的另一个部分:文件读写,也就是 IO。文件读写在很多应用程序中都是需要的。Qt 通过 QIODevice 提供了IO的抽象,这种设备(device)具有读写字节块的能力。常用的IO读写的类包括以下几个:

QFlie访问本地文件系统或者嵌入资源
QTemporaryFile创建和访问本地文件系统的临时文件
QBuffer读写 QByteArray
QProcess运行外部程序,处理进程间通讯
QTcpSocketTCP 协议网络数据传输
QUdpSocket传输 UDP 报文
QSslSocket使用 SSL/TLS 传输数据

  QProcess、QTcpSocket、QUdpSoctet 和 QSslSocket 是顺序访问设备,它们的数据只能访问一遍,也就是说,你只能从第一个字节开始访问,直到最后一个字节。QFile、QTemporaryFile 和 QBuffer 是随机访问设备,你可以从任何位置访问任意次数,还可以使用 QIODevice::seek() 函数来重新定位文件指针。

  在访问方式上,Qt 提供了两个更高级别的抽象:使用 QDataStream 进行二进制方式的访问和使用 QTextStream 进行文本方式的访问。这些类可以帮助我们控制字节顺序和文本编码,使程序员从这种问题中解脱出来。

  QFile 对于访问独立的文件是非常方便的,无论是在文件系统中还是在应用程序的资源文件中。Qt 同样也提供了 QDir 和 QFileInfo 两个类,用于处理文件夹相关事务以及查看文件信息等。

  这次我们先从二进制文件的读写说起。

  以二进制格式访问数据的最简单的方式是实例化一个 QFile 对象,打开文件,然后使用 QDataStream 进行访问。QDataStream 提供了平台独立的访问数据格式的方法,这些数据格式包括标准的 C++ 类型,如 int、double等;多种 Qt 类型,如QByteArray、QFont、QImage、QPixmap、QString 和 QVariant,以及 Qt 的容器类,如 QList<T> 和 QMap<K, T>。先看如下的代码:

QImage image("philip.png");  
 
QMap<QString, QColor> map;  
map.insert("red", Qt::red);  
map.insert("green", Qt::green);  
map.insert("blue", Qt::blue);  
 
QFile file("facts.dat");  
if (!file.open(QIODevice::WriteOnly)) {  
    std::cerr << "Cannot open file for writing: " 
              << qPrintable(file.errorString()) << std::endl;  
    return;  
}  
 
QDataStream out(&file);  
out.setVersion(QDataStream::Qt_4_3);  
 
out << quint32(0x12345678) << image << map; 

  这里,我们首先创建了一个 QImage 对象,一个 QMap<QString, QColor>,然后使用 QFile 创建了一个名为 "facts.dat" 的文件,然后以只写方式打开。如果打开失败,直接 return;否则我们使用 QFile 的指针创建一个 QDataStream 对象,然后设置 version,这个我们以后再详细说明,最后就像 std 的 cout 一样,使用 << 运算符输出结果。

  0x12345678 成为“魔术数字”,这是二进制文件输出中经常使用的一种技术。我们定义的二进制格式通常具有一个这样的“魔术数字”,用于标志文件格式。例如,我们在文件最开始写入 0x12345678,在读取的时候首先检查这个数字是不是 0x12345678,如果不是的话,这就不是可识别格式,因此根本不需要去读取。一般二进制格式都会有这么一个魔术数字,例如 Java 的 class 文件的魔术数字就是 0xCAFE BABE(很 Java 的名字),使用二进制查看器就可以查看。魔术数字是一个 32 位的无符号整数,因此我们使用 quint32 宏来得到一个平台无关的 32 位无符号整数。

  在这段代码中我们使用了一个 qPrintable() 宏,这个宏实际上是把 QString 对象转换成 const char *。注意到我们使用的是 C++ 标准错误输出 cerr,因此必须使用这个转换。当然,QString::toStdString() 函数也能够完成同样的操作。

  读取的过程就很简单了,需要注意的是读取必须同写入的过程一一对应,即第一个写入 quint32 型的魔术数字,那么第一个读出的也必须是一个 quint32 格式的数据,如

quint32 n;  
QImage image;  
QMap<QString, QColor> map;  
 
QFile file("facts.dat");  
if (!file.open(QIODevice::ReadOnly)) {  
    std::cerr << "Cannot open file for reading: " 
              << qPrintable(file.errorString()) << std::endl;  
    return;  
}  
 
QDataStream in(&file);  
in.setVersion(QDataStream::Qt_4_3);  
 
in >> n >> image >> map; 

  好了,数据读出了,拿着到处去用吧!

  这个 version 是干什么用的呢?对于二进制的读写,随着 Qt 的版本升级,可能相同的内容有了不同的读写方式,比如可能由大端写入变成了小端写入等,这样的话旧版本 Qt 写入的内容就不能正确的读出,因此需要设定一个版本号。比如这里我们使用 QDataStream::Qt_4_3,意思是,我们使用 Qt 4.3 的方式写入数据。实际上,现在的最高版本号已经是 QDataStream::Qt_4_6。如果这么写,就是说,4.3 版本之前的 Qt 是不能保证正确读写文件内容的。那么,问题就来了:我们以硬编码的方式写入这个 version,岂不是不能使用最新版的 Qt 的读写了?

  解决方法之一是,我们不仅仅写入一个魔术数字,同时写入这个文件的版本。例如:

QFile file("file.xxx");  
file.open(QIODevice::WriteOnly);  
QDataStream out(&file);  
 
// Write a header with a "magic number" and a version  
out << (quint32)0xA0B0C0D0;  
out << (qint32)123;  
 
out.setVersion(QDataStream::Qt_4_0);  
 
// Write the data  
out << lots_of_interesting_data; 

  这个 file.xxx 文件的版本号是 123。我们认为,如果版本号是123的话,则可以使用 Qt_4_0 版本读取。所以我们的读取代码就需要判断一下:

QFile file("file.xxx");  
 file.open(QIODevice::ReadOnly);  
 QDataStream in(&file);  
 
 // Read and check the header  
 quint32 magic;  
 in >> magic;  
 if (magic != 0xA0B0C0D0)  
     return XXX_BAD_FILE_FORMAT;  
 
 // Read the version  
 qint32 version;  
 in >> version;  
 if (version < 100)  
     return XXX_BAD_FILE_TOO_OLD;  
 if (version > 123)  
     return XXX_BAD_FILE_TOO_NEW;  
 
 if (version <= 110)  
     in.setVersion(QDataStream::Qt_3_2);  
 else 
     in.setVersion(QDataStream::Qt_4_0);  
 
 // Read the data  
 in >> lots_of_interesting_data;  
 if (version >= 120)  
     in >> data_new_in_XXX_version_1_2;  
 in >> other_interesting_data; 

  这样,我们就可以比较完美的处理二进制格式的数据读写了。
二进制文件比较小巧,但是不是人可读的格式。文本文件是一种人可读的格式的文件,为了操作这种文件,我们需要使用QTextStream类。 QTextStream和QDataStream的使用类似,只不过它是操作纯文本文件的。还有一些文本格式,比如XML、HTML,虽然可以由 QTextStream生成,但Qt提供了更方便的XML操作类,这里就不包括这部分内容了。

  QTextStream会自动将 Unicode 编码同操作系统的编码进行转换,这一操作对程序员是透明的。它也会将换行符进行转换,同样不需要你自己去处理。QTextStream使用16位的 QChar作为基础的数据存储单位,同样,它也支持C++标准类型,如int等。实际上,这是将这种标准类型与字符串进行了相互转换。

  QTextStream同QDataStream使用基本一致,例如下面的代码将把“Thomas M. Disch: 334/n”写入到 tmp.txt 文件中:

QFile file("sf-book.txt");  
if (!file.open(QIODevice::WriteOnly)) {  
    std::cerr << "Cannot open file for writing: " 
              << qPrintable(file.errorString()) << std::endl;  
    return;  
}  
 
QTextStream out(&file);  
out << "Thomas M. Disch: " << 334 << endl; 

  可以看到,这段代码同前面的 QDataStream 相关代码基本雷同。文本文件的写入比较容易,但是读出就不那么简单了。例如,

out << "Denmark" << "Norway"; 

  是我们写入的代码。我们分别写入两个单词,然后试图以与二进制文件读出的格式相同的形式读出:

in >> str1 >> str2; 

  上面两段代码的 out 和 in 都是 QTextStream 类型的。虽然我们可以正常写入,但读出的时候,str1里面将是 DenmarkNorway,str2 是空的。以文本形式写入数据,是不能区分数据的截断位置的。因为使用 QDataStream写入的时候,实际上是要在字符串前面写如长度信息的。因此,对于文本文件,更多的是一种全局性质的操作,比如使用 QTextStream::readLine() 读取一行,使用 QTextStream::readAll() 读取所有文本,之后再对获得的QString对象进行处理。

  默认情况下,QTextStream 使用操作系统的本地编码进行读写。不过你可以使用 setCodec() 函数进行设置,比如

stream.setCodec("UTF-8"); 

  同 <iostream> 类似,QTextStream 也提供了一些用于格式化输出的描述符,称为stream manipulators。这些描述符放置在输出内容之前,或者是使用相应的函数,用于对后面的输出内容做格式化。具体的描述符如下

setIntegerBase(int)
0读出时自动检测数字前缀
2二进制
8八进制
10十进制
16十六进制
setNumberFlags(NumberFlags)
ShowBase显示前缀,二进制显示0b,八进制显示0,十六进制显示0x
ForceSign在实数前面显示符号
ForcePoint在数字中显示点分隔符
UppercaseBase使用大写的前缀,如0B, 0X
UppercaseDigits使用大写字母做十六进制数字
setRealNumberNotation(RealNumberNotation)
FixedNotation定点计数表示,如0.000123
ScientificNotation科学计数法表示,如1.23e-4
SmartNotation定点或科学计数法表示,自动选择简洁的一种表示法
setRealNumberPrecision(int)
设置生成的最大的小数位数,默认是6
setFieldWidth(int)
设置一个字段的最小值,默认是0
setFieldAlignment(FieldAlignment)
AlignLeft左对齐
AlignRight右对齐
AlignCenter中间对齐
AlignAccountingStyle符号和数字之间对齐
setPadChar(QChar)
设置对齐时填充的字符,默认是空格

  比如,下面的代码

out << showbase << uppercasedigits << hex << 12345678; 

  将输出0xBC614E。或者我们可以这样去写:

out.setNumberFlags(QTextStream::ShowBase | QTextStream::UppercaseDigits);  
out.setIntegerBase(16);  
out << 12345678; 

  QTextStream 不仅仅可以输出到 QIODevice 上,也可以输出到 QString 上面,例如

QString str;  
QTextStream(&str) << oct << 31 << " " << dec << 25 << endl; 
所谓 IO 其实不过是与其他设备之间的数据交互。在 
Linux 上这个概念或许会更加清楚一些。Linux 把所有设备都看作是一种文件,因此所有的 IO 都归结到对文件的数据交互。同样,与其他进程之间也存在着数据交互,这就是进程间交互。

  为什么需要进程间交互呢?Qt 虽然是一个很庞大的库,但是也不能面面俱到。每个需求都提供一种解决方案是不现实的。比如操作系统提供了查看当前文件夹下所有文件的命令(Windows 下是 dir, Linux 下是 ls),那么 Qt 就可以通过调用这个命令获取其中的信息。当然这不是一个很恰当的例子,因为 Qt 同样提供了相同的操作。不过,如果你使用版本控制系统,比如 SVN,然后你希望通过 SVN 的版本号生成自己系统的 build number,那么就不得不调用 svn 命令获取当前仓库的版本号。这些操作都涉及到进程间交互。

  Qt 使用 QProcess 类完成进程间交互。我们从例子开始看起。由于比较简单,所以没有把 main() 函数贴上来,大家顺手写下就好的啦!

  mainwindow.h

#ifndef MAINWINDOW_H  
#define MAINWINDOW_H  
 
#include <QtGui>  
 
class MainWindow : public QMainWindow  
{  
    Q_OBJECT  
 
public:  
    MainWindow(QWidget *parent = 0);  
    ~MainWindow();  
 
private slots:  
    void openProcess();  
 
private:  
    QProcess *p;  
};  
 
#endif // MAINWINDOW_H 
mainwindow.cpp

#include "mainwindow.h"  
 
MainWindow::MainWindow(QWidget *parent)  
    : QMainWindow(parent)  
{  
    p = new QProcess(this);  
    QPushButton *bt = new QPushButton("execute notepad", this);  
    connect(bt, SIGNAL(clicked()), this, SLOT(openProcess()));  
}  
 
MainWindow::~MainWindow()  
{  
 
}  
 
void MainWindow::openProcess()  
{  
    p->start("notepad.exe");  
} 

  这个窗口很简单,只有一个按钮,当你点击按钮之后,程序会调用 Windows 的记事本。这里我们使用的是

p->start("notepad.exe"); 

  语句。QProcess::start() 接受两个参数,第一个是要执行的命令或者程序,这里就是 notepad.exe;第二个是一个 QStringList 类型的数据,也就是需要传递给这个程序的运行参数。注意,这个程序是需要能够由系统找到的,一般是完全路径。但是这里为什么只有 notepad.exe 呢?因为这个程序实际是放置在 Windows 系统文件夹下,是已经添加到了系统路径之中,因此不需要再添加本身的路径。

  下面我们再看一个更复杂的例子,调用一个系统命令,这里我使用的是 Windows,因此需要调用 dir;如果你是在 Linux 进行编译,就需要改成 ls 了。

  mainwindow.h

#ifndef MAINWINDOW_H  
#define MAINWINDOW_H  
 
#include <QtGui>  
 
class MainWindow : public QMainWindow  
{  
    Q_OBJECT  
 
public:  
    MainWindow(QWidget *parent = 0);  
    ~MainWindow();  
 
private slots:  
    void openProcess();  
    void readResult(int exitCode);  
 
private:  
    QProcess *p;  
};  
 
#endif // MAINWINDOW_H 
mainwindow.cpp

#include "mainwindow.h"  
 
MainWindow::MainWindow(QWidget *parent)  
    : QMainWindow(parent)  
{  
    p = new QProcess(this);  
    QPushButton *bt = new QPushButton("execute notepad", this);  
    connect(bt, SIGNAL(clicked()), this, SLOT(openProcess()));  
}  
 
MainWindow::~MainWindow()  
{  
 
}  
 
void MainWindow::openProcess()  
{  
    p->start("cmd.exe", QStringList() << "/c" << "dir");  
    connect(p, SIGNAL(finished(int)), this, SLOT(readResult(int)));  
}  
 
void MainWindow::readResult(int exitCode)  
{  
    if(exitCode == 0) {  
        QTextCodec* gbkCodec = QTextCodec::codecForName("GBK");  
        QString result = gbkCodec->toUnicode(p->readAll());  
        QMessageBox::information(this, "dir", result);  
    }  
}  

  我们仅增加了一个 slot 函数。在按钮点击的 slot 中,我们通过 QProcess::start() 函数运行了指令

  cmd.exe /c dir

  这里是说,打开系统的 cmd 程序,然后运行 dir 指令。如果有对参数 /c 有疑问,只好去查阅 Windows 的相关手册了哦,这已经不是 Qt 的问题了。然后我们 process 的 finished() 信号连接到新增加的 slot 上面。这个 signal 有一个参数 int。我们知道,对于 C/C++ 程序而言,main() 函数总是返回一个 int,也就是退出代码,用于指示程序是否正常退出。这里的 int 参数就是这个退出代码。在 slot 中,我们检查退出代码是否是0,一般而言,如果退出代码为0,说明是正常退出。然后把结果显示在 QMessageBox 中。怎么做到的呢?原来,QProcess::readAll() 函数可以读出程序输出内容。我们使用这个函数将所有的输出获取之后,由于它的返回结果是 QByteArray 类型,所以再转换成 QString 显示出来。另外注意一点,中文本 Windows 使用的是 GBK 编码,而 Qt 使用的是 Unicode 编码,因此需要做一下转换,否则是会出现乱码的,大家可以尝试一下。

 

  好了,进程间交互就说这么说,通过查看文档你可以找到如何用 QProcess 实现进程过程的监听,或者是令Qt 程序等待这个进程结束后再继续执行的函数。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐