11.4 通用对话框:QErrorMessage、QFileDialog、QProgressDialog

本节介绍通用对话框:QErrorMessage、QFileDialog、QProgressDialog,并通过示例程序展示这三种对话框的使 用。

11.4.1 QErrorMessage

QErrorMessage 提供用于显示错误提示消息的对话框。QErrorMessage 对话框由一个文本显示框和一个复选框组成:
errmsg
文本显示框展示错误消息字符串,复选框选中时,下次调用 showMessage() 就继续显示相同的错误消息;如果复选框处于非选中状态,那么下次调用 showMessage() ,就不显示相同的错误消息。
QErrorMessage 构造函数比较简单,只有指定父窗口指针的参数:
QErrorMessage(QWidget * parent = 0)
使用 QErrorMessage 对话框,可以直接调用 showMessage() 槽函数,或者将带 QString 参数的信号关联到该槽函数,都可以弹窗显示错误消息。
void    showMessage(const QString & message)  //公有槽函数,按消息内容区分
void    showMessage(const QString & message, const QString & type)  //公有槽函数,按 type 类型区分
第二个 showMessage() 槽函数参数就是消息字符串,如果弹窗后,用户取消复选框选中状态,那么相同字符串的消息不在弹窗显示。
第二个 showMessage() 槽函数带有消息字符串和消息类型两个参数,消息类型是自己定义的任意字符串,方便作区分。在指定 type 类型后,用户如果取消复选框选中状态,那么会判定该类型消息都不需要在显示,隐藏匹配 type 类型的所有消息。
对于 showMessage() 函数,正常情况消息会立即显示,但是如果 showMessage() 连续调用多次,排队的消息太多,就会按照先后顺序依次弹窗显示。

QErrorMessage 除了 showMessage() 函数显示消息,还有另一种用途,它的静态函数 qtHandler() 可以接管 qDebug(), qWarning() 和 qFatal() 消息显示:
QErrorMessage *    qtHandler()   //静态函数
qtHandler() 函数内部调用 qInstallMessageHandler() 函数,专门创建一个 QErrorMessage 对话框,用于显示 qDebug(), qWarning() 和 qFatal() 输出的消息。这对于图形化程序很有用,如果没有命令行终端,调用 qtHandler() 保存对话框指针,那么所有 qDebug(), qWarning() 和 qFatal() 输出的消息都会自动通过对话框显示。

QErrorMessage 默认都是非模态的对话框,对于连续多次的 showMessage() 或者是接管了多次 qDebug(), qWarning() 和 qFatal() 输出的消息,这些消息都会按顺序排队显示。对于取消复选框选中状态的消息,下次就会自动忽略重复的消息,不同的消息(不同 type 类型)总会至少显示一次。

11.4.2 QFileDialog

QFileDialog 用于获取打开或保存文件、文件夹、URL路径,之前章节多次使用过  QFileDialog::getOpenFileName() 静态函数获取打开的文件名。本小节详细介绍文件对话框,QFileDialog 对话框外观如下图所示:
filedlg
文件对话框可以设置文件名 filter 过滤器(或叫筛选器),上图设置了过滤器,仅显示 *.h 和 *.cpp 文件,用户点选文件,然后点击“打开”按 钮,对话框就会关闭,并可以通过函数获取用户选择的文件名。
QFileDialog 也有两种使用方式,第一种是使用自定义对象方式,第二种是使用静态函数弹窗获取文件名。下面我们先介绍第一种自定义对象使用的 QFileDialog 普通成员函数,然后再介绍静态函数。
(1)QFileDialog 普通成员函数
QFileDialog 构造函数如下:
QFileDialog(QWidget * parent, Qt::WindowFlags flags)
QFileDialog(QWidget * parent = 0, const QString & caption = QString(), const QString & directory = QString(), const QString & filter = QString())
第一个构造函数参数比较简化,只有父指针和对话框标志位。
第二个构造函数除了父指针,还可以指定窗口标题文本 caption 、默认打开路径 directory 和文件名过滤器 filter 。
定义对话框对象之后,需要设置对话框的文件模式、文件名过滤器、视图模式等属性,然后再用 exec() 或 show() 显示对话框,文件对话框通常用模态显示。
QFileDialog 有五个主要的属性,控制对话框的工作和显示:
①接受模式 acceptMode
接受模式是指接受文件打开或文件保存两种模式,默认为 AcceptOpen,读写函数如下:
AcceptMode    acceptMode() const
void    setAcceptMode(AcceptMode mode)
文件打开模式总是打开已经存在的文件,而文件保存模式可以设置不存在文件名作为新文件写入。
AcceptMode 枚举值如下所示:
QFileDialog::​ AcceptMode 枚举常量 数值 描述
QFileDialog::AcceptOpen  0 接受文件打开,获取已存在文件名。
QFileDialog::AcceptSave  1 接受文件保存,可以设置不存在的文件名(新建文件),如果是已存在的文件名,就是覆盖写入。

②文件模式 fileMode
文件模式是允许用户选择文件或文件夹的范围,就是对话框返回的文件名限定,默认为 AnyFile,读写函数如下:
FileMode    fileMode() const
void    setFileMode(FileMode mode)
FileMode 枚举值如下:
QFileDialog::FileMode 枚举常量 数值 描述
QFileDialog::AnyFile  0 一个任意的文件名,无论该文件否存在。
QFileDialog::ExistingFile  1 一个已存在的文件名。
QFileDialog::Directory  2 一个文件夹名称,这时文件和文件夹都会显示。
QFileDialog::ExistingFiles  3 多个已存在文件名。

③文件名默认后缀 defaultSuffix 以及后缀名筛选
defaultSuffix 是获取文件名的默认后缀,如果用户输入文件名时没有写后缀, 那么对话框给返回的文件名自动添加该默认后缀,读写函数为:
QString    defaultSuffix() const
void    setDefaultSuffix(const QString & suffix)
setDefaultSuffix() 参数的 suffix 字符串不需要以点号 . 打头,如果以点号打头,会自动删除点号,只存字母等后缀名,例如 "txt" 。
在文件对话框浏览时,可以设置后缀名过滤器,帮助筛选文件类型:
void    setNameFilter(const QString & filter)
void    setNameFilters(const QStringList & filters)
QStringList    nameFilters() const   //获取后缀名过滤器列表
第一个设置后缀名过滤器的函数,参数是一个字符串,字符串可以是多种类型多种后缀名,例如:
"Images (*.png *.xpm *.jpg);;Text files (*.txt);;XML files (*.xml)"
两个英文分号区分文件类型,英文括号前面是文件类型描述,英文括号里面是空格分隔的多种后缀名。
如果希望匹配所有文件,设置过滤器为 "All files (*)"  。
第二个设置后缀名过滤器的函数,其参数为 QStringList  字符串列表,不需要双分号分隔文件类型,例如:
QStringList filters;
filters << "Image files (*.png *.xpm *.jpg)"
        << "Text files (*.txt)"
        << "Any files (*)";

QFileDialog dialog(this);
dialog.setNameFilters(filters);
dialog.exec();
对于多种类型过滤器,可以选择一个作为当前显示的过滤器:
void    selectNameFilter(const QString & filter)  //设置参数里的过滤器为当前显示的过滤器
QString    selectedNameFilter() const  //获取当前选中的后缀名过滤器

④显示模式 viewMode
文件对话框浏览文件目录时主要有列表显示和详情显示,默认是详情模式,读写函数为:
ViewMode    viewMode() const
void    setViewMode(ViewMode mode)
ViewMode 枚举值如下:
QFileDialog::ViewMode 枚举常量 数值 描述
QFileDialog::Detail  0 详情模式,显示文件或文件夹条目的图标、名称和详情。
QFileDialog::List  1 列表模式,只显示文件夹或文件夹的图标和名称。

⑤对话框选项 options
options 选项控制对话框的多种外观和文件筛选特性,默认不设置任何选项,读写函数如下:
Options    options() const  //获取多个选项
void    setOptions(Options options)  //设置多个选项
bool    testOption(Option option) const  //测试单个选项
void    setOption(Option option, bool on = true)  //设置单个选项
Options 枚举标志位如下:
QFileDialog::Options 标志位 数值 描述
QFileDialog::ShowDirsOnly  0x00000001 该标志位为 1,对话框只显示目录(仅在文件模式为 Directory 时生效)。默认标志位 0 显示所有文件和目录。
QFileDialog::DontResolveSymlinks  0x00000002 标志位为 1 时解析符号链接(快捷方式)。默认标志位 0 解析符号链接。
QFileDialog::DontConfirmOverwrite  0x00000004 标志位为 1 时不提示覆盖文件。默认标志位 0 会提示用户将要覆盖文件。
QFileDialog::DontUseNativeDialog  0x00000010 不使用本地文件对话框。默认都是使用操作系统本地的文件对话框。如果要用非本土对话框,需要子类化QFileDialog并包含 Q_OBJECT 宏。
QFileDialog::ReadOnly  0x00000020 指示显示模型为只读。
QFileDialog::HideNameFilterDetails  0x00000040 指示后缀名详情隐藏或显示。
QFileDialog::DontUseSheet  0x00000008 不使用工作表,兼容老版本Qt代码,在 Qt 4.5 之后已经没有用处了。
QFileDialog::DontUseCustomDirectoryIcons  0x00000080 不使用定制文件夹图标,总是使用默认的文件夹图标。有些系统默认可以显示定制的文件夹图标,这个选项自从 Qt 5.2 开始生效。

这些选项需要在对话框显示之前设置完毕,如果对话框已经显示了,设置选项只能在对话框关闭后下次显示才能生效。

除了属性读写函数,文件对话框还有其他功能函数。
文件对话框界面的按钮和标签文本支持自定义内容,其读写函数为:
QString    labelText(DialogLabel label) const
void    setLabelText(DialogLabel label, const QString & text)
DialogLabel 指示读写的标签对象类型,如下列表所示:
QFileDialog::DialogLabel 枚举常量 数值 描述
QFileDialog::LookIn  0 LookIn 标签。
QFileDialog::FileName  1 文件名标签。
QFileDialog::FileType  2 文件类型标签。
QFileDialog::Accept  3 接受按钮标签,就是打开或保存按钮文本。
QFileDialog::Reject  4 拒绝按钮标签。

Windows 7 系统实际测试时 QFileDialog::FileName、QFileDialog::Accept 设置有效,修改的是“文件名”标签,“打开”按钮的文本,其他的未发现效果,可能其他操作系统有效果。

文件对话框默认工作路径可以通过下面函数设置:
void    setDirectory(const QString & directory)  //设置当前路径
void    setDirectory(const QDir & directory)  //设置当前路径
QDir    directory() const  //获取当前路径
文件对话框支持模态显示和非模态显示,都是从基类继承,如 exec() 、show() 函数。
文件对话框也有一次性关联接收信号对象的打开函数:
void QFileDialog::​open(QObject * receiver, const char * member)
QFileDialog 根据文件模式决定关联到槽函数的信号:
文件模式为 ExistingFiles 时,将  filesSelected() 信号关联到槽函数;
文件模式为其他数值时,将 fileSelected() 信号关联到槽函数。
这个关联是一次性的,当对话框关闭时,解除信号和槽函数关联。

文件可以通过函数提前设置默认选中的文件,帮助用户简化点选默认文件的操作:
void    selectFile(const QString & filename)  //通过代码选定默认的文件名
正常情况下,在对话框关闭后,获取用户最终选定的文件列表,使用下面函数:
QStringList    selectedFiles() const
注意 selectedFiles() 返回的总是 QStringList 列表,如果打开或保存一个文件,那么文件名就是列表的第 0 号字符串。

对于非模态文件对话框,可以接收文件对话框的信号实时监测用户在对话框的点选动态:
void    currentChanged(const QString & path)  //当前选中文件变化
void    directoryEntered(const QString & directory)  //进入新的文件夹
void    fileSelected(const QString & file)  //选中的单 个文件
void    filesSelected(const QStringList & selected)  //选中的多个文件,参数是列表
void    filterSelected(const QString & filter)  //选中的后缀名过滤器

以上内容都是常规的获取系统文件路径形式,比如
"D:/QtProjects/ch10/stackedwidget.ui"
文件对话框还有一套针对 URL 形式路径的函数和信号,函数和信号名全都带 Url 字样,例如:
QList<QUrl>    selectedUrls() const  //选择的多个URL
void    urlsSelected(const QList<QUrl> & urls)  //信号,选择的多个URL
URL 路径格式举例:
"file:///D:/QtProjects/ch10/stackedwidget.ui"
就是加了前缀字符串 "file:///" ,URL 通常在网页浏览器里使用,在网页浏览器输入 URL 可以访问对应的系统文件。

系统文件路径与 URL 路径可以互相转换:
QString QUrl::​toLocalFile() const   //普通成员函数,将 QUrl 对象内容转为本地系统文件路径
QUrl QUrl::​fromLocalFile(const QString & localFile)   //静态函数,根据本地系统文件路径转为 QUrl 对象

文件对话框的普通成员函数运用例子代码如下,针对保存一个文件,获取文件名:
    QFileDialog dlg;
    dlg.setAcceptMode(QFileDialog::AcceptSave);  //保存模式
    dlg.setFileMode(QFileDialog::AnyFile); //选择任意一个文件
    dlg.setNameFilter("Source files(*.h *.cpp);;All files(*)"); //后缀名过滤器
    dlg.setViewMode(QFileDialog::Detail); //详情显示
    QString oneFileName;
    if( dlg.exec() )
    {
        oneFileName = dlg.selectedFiles()[0]; //确定只有一个文件名
        qDebug()<<oneFileName;
    }

针对打开多个文件名,示例代码:
    QFileDialog dlg;
    dlg.setAcceptMode(QFileDialog::AcceptOpen);  //打开模式
    dlg.setFileMode(QFileDialog::ExistingFiles); //选择多个文件
    dlg.setNameFilter("Source files(*.h *.cpp);;All files(*)"); //后缀名过滤器
    dlg.setViewMode(QFileDialog::Detail); //详情显示
    QStringList listFileName;
    if( dlg.exec() )
    {
        listFileName = dlg.selectedFiles();
        qDebug()<<listFileName;
    }

(2)QFileDialog 静态成员函数
静态函数使用比较方便快捷,上面自定义对话框代码需要使用多个函数,逐一设置各种参数,而静态函数只需要一个函数搞定。
①获取一个打开的文件名
QString    getOpenFileName(QWidget * parent = 0, const QString & caption = QString(), const QString & dir = QString(), const QString & filter = QString(), QString * selectedFilter = 0, Options options = 0)

QUrl    getOpenFileUrl(QWidget * parent = 0, const QString & caption = QString(), const QUrl & dir = QUrl(), const QString & filter = QString(), QString * selectedFilter = 0, Options options = 0, const QStringList & supportedSchemes = QStringList())
参数 parent  是父窗口指针,caption 是对话框标题文本,dir 是当前路径,filter 是后缀名过滤器,selectedFilter 是指向选中过滤器的指针,默认为 0,options 是对话框选项。
第一个函数获取的是系统文件名形式,第二个是 URL 路径形式。URL 函数最后的参数 supportedSchemes 用于限定用户允许选择的 URL 类型,文件对话框中一般默认就是选择本地系统文件。

②获取多个打开的文件名
QStringList    getOpenFileNames(QWidget * parent = 0, const QString & caption = QString(), const QString & dir = QString(), const QString & filter = QString(), QString * selectedFilter = 0, Options options = 0)

QList<QUrl>    getOpenFileUrls(QWidget * parent = 0, const QString & caption = QString(), const QUrl & dir = QUrl(), const QString & filter = QString(), QString * selectedFilter = 0, Options options = 0, const QStringList & supportedSchemes = QStringList())
这两个函数参数与上面 ① 中两个函数一样,只是返回值为文件名列表、URL列表。

③获取一个保存的文件名
QString    getSaveFileName(QWidget * parent = 0, const QString & caption = QString(), const QString & dir = QString(), const QString & filter = QString(), QString * selectedFilter = 0, Options options = 0)

QUrl    getSaveFileUrl(QWidget * parent = 0, const QString & caption = QString(), const QUrl & dir = QUrl(), const QString & filter = QString(), QString * selectedFilter = 0, Options options = 0, const QStringList & supportedSchemes = QStringList())
这两个函数参数与上面 ① 中两个函数一样,返回的文件名如果不存在就是新建文件保存,如果文件名已存在就是覆盖文件保存。

④获取一个存在的文件夹路径
QString    getExistingDirectory(QWidget * parent = 0, const QString & caption = QString(), const QString & dir = QString(), Options options = ShowDirsOnly)

QUrl    getExistingDirectoryUrl(QWidget * parent = 0, const QString & caption = QString(), const QUrl & dir = QUrl(), Options options = ShowDirsOnly, const QStringList & supportedSchemes = QStringList())
这两个函数获取的是文件夹路径,参数 parent 是父窗口指针,caption 是窗口标题文本,dir 是当前路径,options 默认只显示文件夹,不显示文件,supportedSchemes 一般不用设置,默认是选择系统里的文件夹路径。

使用静态函数获取文件名就非常简单,例如:
fileName = QFileDialog::getOpenFileName(this,
    tr("Open Image"), "/home/jana", tr("Image Files (*.png *.jpg *.bmp)"));
一般填写前几个常用的参数即可,后面的参数和选项不用设置。

11.4.3 QProgressDialog

QProgressDialog 进度对话框用于程序长时间执行某个操作时,弹出显示长时间任务的进度,例如复制大文件,或者计算大文件的 MD5 校验值,这些操作耗时长,可以使用进度条 QProgressBar 或者进度对话框 QProgressDialog 显示这些长时间任务进度。QProgressDialog 外观如下图所示:
progress
进度对话框主要由描述标签、进度条、中断按钮(或叫取消按钮)等构成,任务执行时,点击中断按钮可以中断任务。
QProgressDialog 构造函数如下:
QProgressDialog(QWidget * parent = 0, Qt::WindowFlags f = 0)
QProgressDialog(const QString & labelText, const QString & cancelButtonText, int minimum, int maximum, QWidget * parent = 0, Qt::WindowFlags f = 0)
第一个是简化的构造函数,参数只有父窗口指针和窗口标志位。
第二个构造函数参数 labelText 是对话框上面的描述标签文本,cancelButtonText 是中断任务按钮的文本,
minimum 是进度 0% 对应的初始值,maximum 是进度 100% 完成时对应的结束值,parent 是父窗口指针,f 是窗口标志位。

QProgressDialog 有 8 个属性,分别介绍如下:
① labelText 描述标签文本以及中止按钮设置
就是进度对话框上面的描述文本,读写函数为:
QString    labelText() const  //获取描述标签文本
void    setLabelText(const QString & text)  //槽函数,设置描述标签文本
对话框不仅能使用默认的标签对象,还可以自定义描述标签对象:
void    setLabel(QLabel * label)  //设置自定义的标签对象
对话框取消任务的按钮文本可以修改,并可以自定义取消按钮和进度条控件:
void    setCancelButtonText(const QString & cancelButtonText) //槽函数,设置取消按钮文本
void    setCancelButton(QPushButton * cancelButton) //自定义取消按钮
void    setBar(QProgressBar * bar)  //自定义进度条控件

②minimum 最小值
最小值就是进度 0% 对应的值,默认值就是 0,读写函数为:
int    minimum() const
void    setMinimum(int minimum)  //槽函数
③maximum 最大值
最大值就是进度 100% 对应的值,默认是100,读写函数为:
int    maximum() const
void    setMaximum(int maximum) //槽函数
可以同时设置最小值和最大值的区间:
void    setRange(int minimum, int maximum) //槽函数,设置区间

④minimumDuration 持续时间下限(单位毫秒)
持续时间下限是指当任务预期的持续时间低于该值时,不显示进度对话框;
只有任务预期的持续时间达到 minimumDuration 以上,才会显示进度对话框。这是针对时间较短任务做的优化,短时间任务没必要弹出进度条。默认的 minimumDuration 数值为 4000 毫秒,即 4 秒。
如果把 minimumDuration 修改为 0 毫秒,那么进度对话框就总是显示。minimumDuration 读写函数为:
int    minimumDuration() const  //单位毫秒
void    setMinimumDuration(int ms)  //槽函数,单位毫秒

⑤value 当前进度数值
value 就是位于 minimum 和 maximum 之前的数值,进度对话框自动把 value 计算为百分比的形式显示,例如 [0,100] 的区间,value 为 100 就是 100% 进度,但如果区间为 [0,1000] ,那么 100 的数值就只有 10% 进度。
value 读写函数为:
int    value() const
void    setValue(int progress)   //槽函数
 setValue() 槽函数会自动显示进度对话框,而不需要调用 show() 函数。

⑥ wasCanceled 是否取消任务
当用户点击取消按钮时,wasCanceled 被设置为 true,这个属性只有读取函数:
bool    wasCanceled() const
在长时间任务执行过程中,每隔一段时间检查 wasCanceled(),如果为 true ,那么说明用户希望中止操作,不愿意继续等待,就可以执行中止任务的 代码,实现用户的意图。

⑦ autoClose 进度完成时自动关闭
autoClose 属性为 true 时, reset() 函数会自动关闭对话框。autoClose 默认值就是 true 。读写函数为:
bool    autoClose() const
void    setAutoClose(bool close)
⑧ autoReset 进度完成后自动重置
autoReset 是指当进度值达到 maximum 之后,自动调用 reset() 函数,隐藏对话框,并重置进度数值。autoReset 默认值也是 true。读写函数为:
bool    autoReset() const
void    setAutoReset(bool reset)

进度对话框还有两个槽函数和一个信号:
void    cancel()  //槽函数,取消任务
void    reset()  //槽函数,重置进度对话框
void    canceled()  //信号,用户点击取消按钮触发,该信号总是触发cancel() 槽函数
cancel() 槽函数是通过代码设置 wasCanceled() 为 true,进度对话框会隐藏,表示取消任务,并重置对话框。
reset() 槽函数重置进度对话框,如果 autoClose() 为 true 就自动隐藏对话框。
canceled() 信号由用户点击取消按钮触发,并且该信号默认关联到 cancel() 槽函数。
默认情况下,autoClose 和 autoReset 都是 true,当进度值设置为 maximum 后,自动重置进度对话框,并关闭该进度对话框。

进度对话框也可以使用 open() 打开对话框函数实现一次性的信号和槽关联:
void    open(QObject * receiver, const char * member)
注意进度对话框是将 canceled() 信号关联到接收对象的 member 槽函数。这个信号和槽函数关联是一次性的,触发一次后自动解除关联。

QProgressDialog 有两种使用方式,模态使用和非模态使用。模态使用的过程比较简单:
    QProgressDialog progress("Copying files...", "Abort Copy", 0, numFiles, this);
    progress.setWindowModality(Qt::WindowModal);

    for (int i = 0; i < numFiles; i++) {
        progress.setValue(i);

        if (progress.wasCanceled())
            break;
        //... copy one file
    }
    progress.setValue(numFiles);
定义 progress 对话框,描述标签文本为 "Copying files..." ,取消按钮文本为 "Abort Copy" ,区间就是 0 到 numFiles;
进度对话框使用 progress.setWindowModality(Qt::WindowModal) 模态设置,这样使得用户能够与进度对话框继续交互,方 便用户点击取消按钮,中止任务执行。
循环体用于处理长时间任务,在每轮循环里调用 progress.setValue(i) 更新进度,并检查用户是否取消任务;
循环结束后,调用 progress.setValue(numFiles) ,进度完成 100%,自动重置并关闭进度对话框。

QProgressDialog 非模态使用稍微复杂一些,常用于后台任务的处理,用户持续与前台窗口交互,后台进度用非模态进度对话框显示。后台操作常基于定时器  QTimer(或定时事件 QObject::timerEvent())、套接字通知 QSocketNotifier 、 QUrlOperator ,或者使用单独线程处理后台数据。
对于这些后台应用场景,在主窗口状态栏设置 QProgressBar 也能起到相同效果。
对于非模态进度对话框,需要有事件循环(event loop)持续运行,将进度对话框的 canceled() 关联到取消任务操作的槽函数上,并定期调用 setValue() 更新进度。例如:
// Operation constructor
Operation::Operation(QObject *parent)
    : QObject(parent), steps(0)
{
    pd = new QProgressDialog("Operation in progress.", "Cancel", 0, 100);
    connect(pd, SIGNAL(canceled()), this, SLOT(cancel()));
    t = new QTimer(this);
    connect(t, SIGNAL(timeout()), this, SLOT(perform()));
    t->start(0);
}

void Operation::perform()
{
    pd->setValue(steps);
    //... perform one percent of the operation
    steps++;
    if (steps > pd->maximum())
        t->stop();
}

void Operation::cancel()
{
    t->stop();
    //... cleanup
}
Operation 是持续任务的操作类,构造函数定义了进度对话框,并将进度对话框的 canceled() 信号关联到操作类对象的 cancel() 槽函数;然后新建了定时器 QTimer 对象,将定时器周期触发的信号 timeout() 关联到槽函数 perform() ,然后启动了定时器。
定时器的周期信号触发 perform() 槽函数,在该函数里面进行分批次的任务工作,并调用 pd->setValue(steps) 更新进度;当进度达到最大值 pd->maximum(),完成进度,停止定时器。
如果用户点击了进度对话框取消按钮,那么 canceled() 信号触发 Operation::cancel() 槽函数,在该函数里执行中止任务的代码。
进度对话框的内容介绍到这,下面我们通过一个示例展示本节三种对话框的使用。

11.4.4 CalcMD5 示例

我们学习一个计算文件 MD5 校验值的示例,展示 QErrorMessage、QFileDialog、QProgressDialog 三种对话框的使用。
我们打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 calcmd5,创建路径 D:\QtProjects\ch11,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,注意修改主窗口类名为 WidgetCalcMD5,然后点击下一步;
④项目管理不修改,点击完成。
由于使用 Qt 通用对话框,就不再需要额外新建自己的子窗口类和UI文件。
我们打开 widgetcalcmd5.ui 界面文件,拖入控件:
ui1
第一行控件是“文件名”标签,单行编辑器 lineEditFileName,“浏览”按钮 pushButtonBrowser,“计算MD5”按钮 pushButtonCalcMD5。
第二行是文本浏览框 textBrowserInfo 。
第一行控件按照水平布局器排列,然后在右边对象树选中根节点 WidgetCalcMD5,点击垂直布局按钮,窗口整体按照垂直布局排列。窗口尺寸为默认的 400 * 300 。

完成布局后我们依次右击两个按钮,为按钮添加 clicked() 信号的槽函数。
添加两个槽函数之后,我们保存并关闭 ui 文件,回到代码编辑界面。
我们首先编辑头文件 widgetcalcmd5.h 内容:
#ifndef WIDGETCALCMD5_H
#define WIDGETCALCMD5_H

#include <QWidget>
#include <QErrorMessage>
#include <QFileDialog>
#include <QProgressDialog>
#include <QFile>
#include <QCryptographicHash>  //计算MD5

namespace Ui {
class WidgetCalcMD5;
}

class WidgetCalcMD5 : public QWidget
{
    Q_OBJECT

public:
    explicit WidgetCalcMD5(QWidget *parent = 0);
    ~WidgetCalcMD5();

private slots:
    void on_pushButtonBrowser_clicked();

    void on_pushButtonCalcMD5_clicked();

private:
    Ui::WidgetCalcMD5 *ui;
    //错误消息框
    QErrorMessage m_dlgErrorMsg;
    //文件名
    QString m_strFileName;
    //计算文件对象 MD5 
    QByteArray CalcFileMD5( QFile &fileIn, qint64 nFileSize );

};

#endif // WIDGETCALCMD5_H
我们添加了 本节三个对话框的头文件包含,以及 QFile 和 QCryptographicHash 头文件包含。
QFile 、QCryptographicHash 用于读取文件内容和计算文件的 MD5 校验和。
WidgetCalcMD5 类内部自动添加了两个按钮槽函数声明,然后我们自定义了成员变量,
m_dlgErrorMsg 对话框用于显示错误消息,m_strFileName 保存文件名,
CalcFileMD5() 函数根据打开后的文件对象计算 MD5 校验和。

接下来我们分段编辑源文件 widgetcalcmd5.cpp 内容,首先是头文件包含和构造函数部分:
#include "widgetcalcmd5.h"
#include "ui_widgetcalcmd5.h"

WidgetCalcMD5::WidgetCalcMD5(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::WidgetCalcMD5)
{
    ui->setupUi(this);
}

WidgetCalcMD5::~WidgetCalcMD5()
{
    delete ui;
}
头文件包含和构造函数、析构函数都是自动生成的内容,没有修改。

接下来我们编辑“浏览”按钮对应的槽函数:
//浏览要打开的文件
void WidgetCalcMD5::on_pushButtonBrowser_clicked()
{
    QString strFileName = QFileDialog::getOpenFileName(
                this, tr("选择文件"), "", "All files (*)");
    if( strFileName.isEmpty() )
    {
        m_dlgErrorMsg.showMessage( tr("文件名为空,未选择文件!") );
        return;
    }
    //文件名非空
    m_strFileName = strFileName;
    ui->lineEditFileName->setText( m_strFileName );
    //清空旧的文本框
    ui->textBrowserInfo->clear();
}
我们调用 QFileDialog::getOpenFileName() 静态函数获取要打开的文件名,文件对话框标题为 "选择文件",后缀名过滤器是所有文件,
然后判断文件名 strFileName 是否为空,空字符串就弹出消息框说明文件名为空,没有选择文件,直接返回;
strFileName 非空时,将文件名存到成员变量 m_strFileName,并将文件名字符串显示到 单行编辑器 ui->lineEditFileName ,最后将信息显示文本框清空,以便后续显示新打开文件的 MD5 信息。

接下来我们编辑“计算MD5”按钮槽函数内容:
//槽函数,计算文件 MD5
void WidgetCalcMD5::on_pushButtonCalcMD5_clicked()
{
    //先从编辑框获取文件名
    QString strFileName = ui->lineEditFileName->text().trimmed();
    if( strFileName.isEmpty() )
    {
        m_dlgErrorMsg.showMessage( tr("文件名编辑框内容为空!") );
        return;
    }
    //文件名非空
    m_strFileName = strFileName;
    //文件对象
    QFile fileIn(m_strFileName);
    //尝试打开
    if( ! fileIn.open(QIODevice::ReadOnly) )
    {
        m_dlgErrorMsg.showMessage( tr("打开指定文件失败!") );
        return;
    }
    //正常打开文件,检查文件大小
    qint64 nFileSize = fileIn.size();
    if( nFileSize < 1 )
    {
        m_dlgErrorMsg.showMessage( tr("文件大小为 0,没有数据!") );
        fileIn.close();
        return;
    }
    //计算MD5值
    QByteArray baMD5 = CalcFileMD5( fileIn, nFileSize );
    //构造信息字符串
    QString strInfo = tr("文件名:") + m_strFileName;
    strInfo += tr("\n文件大小:%1 字节").arg(nFileSize);
    strInfo += tr("\nMD5校验值:\n");
    strInfo += baMD5.toHex().toUpper();
    //设置给文本框
    ui->textBrowserInfo->setText(strInfo);
    //关闭文件
    fileIn.close();
}
我们先从单行编辑器获取文件名 strFileName,判断文件名是否为空,如果为空就弹窗显示错误消息,返回;
如果文件名非空,存到成员变量 m_strFileName,根据该文件名定义文件对象 fileIn,
以只读模式打开该文件,判断是否打开失败,如果失败就弹出错误消息框并返回,文件打开成功就继续后面的代码。
获取 fileIn 文件的大小,存到 nFileSize,如果文件大小 nFileSize 小于 1,说明是空的文件,
空文件就弹出显示没有数据的消息框,并关闭文件,返回。
对于正常有内容的文件,调用函数 CalcFileMD5() 计算得到 MD5 字节数组,存到  baMD5。
然后构造信息字符串 strInfo,将文件名、文件大小、文件 MD5 的十六进制字符串等内容都添加到信息字符串,
将 strInfo 显示到 ui->textBrowserInfo 文本框,最后关闭文件。

最后我们编辑 CalcFileMD5() 函数的内容:
//计算文件对象 MD5
QByteArray WidgetCalcMD5::CalcFileMD5( QFile &fileIn, qint64 nFileSize )
{
    //返回的MD5字节数组
    QByteArray baRet;
    //定义算法对象
    QCryptographicHash algMD5(QCryptographicHash::Md5);
    //数据块
    QByteArray baCurData;
    //判断
    if( nFileSize < 100*1000 ) //小于 100K
    {
        //一次读取全部数据计算MD5
        baCurData = fileIn.readAll();
        algMD5.addData(baCurData);
        baRet = algMD5.result();
        return baRet;
    }
    //如果是大于 100K,分块读取数据
    qint64 oneBlockSize = nFileSize / 100; //可能有余数
    int nBlocksCount = 100;
    if( nFileSize % oneBlockSize != 0 )
    {
        //存在余数除不尽
        nBlocksCount += 1;
    }

    //先设置进度对话框,然后开始循环读取文件内容
    QProgressDialog dlgProgress(tr("正在计算MD5 ..."), tr("取消计算"),
                    0, nBlocksCount, this);
    dlgProgress.setWindowModality( Qt::WindowModal );
    dlgProgress.setMinimumDuration( 0 ); //总是显示对话框
    //循环处理
    for(int i=0; i<nBlocksCount; i++)
    {
        //设置进度,自动显示对话框
        dlgProgress.setValue(i);
        //判断是否取消
        if( dlgProgress.wasCanceled() )
        {
            break;
        }
        //正常读取数据块,计算MD5
        baCurData = fileIn.read(oneBlockSize);
        algMD5.addData(baCurData);
    }
    //进度没有取消,获取正常MD5结果
    if( ! dlgProgress.wasCanceled() )
    {
        baRet = algMD5.result();
    }
    //结束进度
    dlgProgress.setValue( nBlocksCount );
    //返回MD5字节数组
    return baRet;
}
函数开头定义字节数组 baRet,用于保存 MD5 数据,定义算法对象 algMD5,用于计算文件内容的 MD5 数据,
定义 baCurData 保存读取的文件内容。
然后判断文件大小 nFileSize,如果小于 100K,那么一次性读取文件所有内容,添加到算法对象 algMD5,
从算法对象 algMD5 获取计算的 MD5 结果存到 baRet,返回该数组。

如果文件达到了 100K 和以上,我们分块读取文件内容,并用进度对话框展示实时的进度。
我们计划分 100 次(文件大小是100的倍数)或 101 次(非100整除的情况)完成文件读取。
计算 nFileSize / 100,得到一个块的大小 oneBlockSize ,
计算 nFileSize 模 oneBlockSize 的余数,如果为 0 就是整除,nBlocksCount 数值为 100 块,
如果余数非零,那么 nBlocksCount 数值为 101 块。
然后定义进度对话框 dlgProgress,对话框标签为 "正在计算MD5 ..." ,取消按钮文本 "取消计算",取值区间 0 到 nBlocksCount 。
设置 dlgProgress 为模态显示,处理过程中占据前台显示,并设置显示的最短持续时间为 0 毫秒,就是一直显示进度对话框。

然后开始循环处理文件分块,在循环体内部先调用 dlgProgress.setValue(i) 更新进度,并显示进度对话框,
调用 dlgProgress.wasCanceled(),判断是否取消计算任务,如果用户取消了就中断循环,
如果任务没有取消,正常读取 oneBlockSize 大小的文件分片,返回的数据块存到 baCurData,
将数据块 baCurData 添加到算法对象 algMD5 。
完成全部文件分块读取并添加给算法对象后,结束循环。
我们判断 dlgProgress.wasCanceled() ,如果没有取消任务,那么从算法对象获取 MD5 结果存到 baRet;
如果任务是被用户取消的,那么  baRet 是空数组。
接着我们调用  dlgProgress.setValue( nBlocksCount ) 结束进度,进度对话框自动重置并关闭,
最后返回 baRet 数组。

对于函数调用 fileIn.read(oneBlockSize) ,如果文件剩余部分有 oneBlockSize 大小,那么读取 oneBlockSize 大小的块数据返回,如果剩余部分不够 oneBlockSize 大小,那么读取剩余所有内容返回数据,这时候 baCurData 的数据长度是实际的数据块长度,而不会填充额外数据。
无论是 100 个数据分块,还是 101 个数据分块,上面循环体代码都能正确地分批次读取文件数据,并计算 MD5 校验值。
对于返回值,用户取消任务时,返回的数组是空的;用户没有取消任务,那么返回正确的 MD5 字节数组。

例子代码介绍到这,我们生成项目,运行程序, 文件名为空的情况下,直接点击“计算MD5”按钮,会弹出错误消息框:
run1
关闭消息框,我们点击“浏览”按钮,弹出文件对话框,选择文件名:
run2
我们在硬盘里选择一个几百兆或 1 GB 以上的文件名,然后点击“打开”按钮,选择的文件名会显示到单行编辑器,
我们再点击“计算MD5”按钮,可以看到进度对话框:
run3
当全部任务完成后,进度对话框自动关闭,文本浏览框显示出信息:
run4
文本浏览框显示了文件名、文件大小和 MD5 校验值。
本节内容就到这,下一节我们继续学习 Qt 通用对话框。



prev
contents
next