10.3 堆栈控件和标签页控件

本节介绍 Stacked Widget、Tab Widget ,这两个容器也支持多标签页轮流展示,Stacked Widget不带标题栏,需要配合如组合框之类的控件来切换标签页;Tab Widget 功能最为丰富,自带了标题栏,可以通过点击标题栏自由切换各个页面显示。

10.3.1 Stacked Widget 堆栈控件

堆栈控件类名为 QStackedWidget ,功能相对简单,没有标题栏,通常与组合框、列表控件或者一组单选按钮来使用,通过搭配的控件选中条目变化,切换 各个页面。
在 Qt 设计师界面,堆栈控件显示如下图所示:
stack1
堆栈控件在设计师界面显示时,右上角有两个左右箭头切换子页面,进行分别显示编辑,但是程序编译运行时,没有这两个切换按钮:
stack2
因此程序运行时,堆栈控件需要依赖其他控件来切换页面。
QStackedWidget 添加子页面的函数如下:
int    addWidget(QWidget * widget)
将控件添加给堆栈容器后,返回值是该页面的序号。如果希望将页面插入到指定序号位置,使用下面函数:
int    insertWidget(int index, QWidget * widget)
insertWidget() 参数里的 index 如果超出序号范围,那么新页面放到最末尾,返回值总是真实的序号值。
删除页面使用如下函数:
void    removeWidget(QWidget * widget)
注意该函数不会释放页面占用的内存,仅仅是从容器上卸载,卸载下的标签页仍存在,需要手动 delete 才会释放内存。
堆栈控件的子页面计数使用如下函数:
int    count() const
获取当前显示页面的序号或页面指针,使用下面两个函数:
int    currentIndex() const      //当前页面序号
QWidget *    currentWidget() const   //当前页面指针
页面指针和页面序号可以互查:
int    indexOf(QWidget * widget) const   //根据页面指针查序号,如果页面不属于容器,返回值是 -1
QWidget *    widget(int index) const  //根据序号查页面指针,如果序号不合法,返回 NULL
注意判断返回值内容,-1  和 NULL 需要单独判断来处理,避免产生程序 bug。

堆栈控件有两个槽函数,均是设置当前页面函数:
void    setCurrentIndex(int index)    //根据序号设置当前页面
void    setCurrentWidget(QWidget * widget)   //根据页面指针设置当前页面

堆栈控件有两个信号,分别是当前页面序号变化和卸载操作的页面序号:
void    currentChanged(int index)   //当前页面序号变化,参数是新页面序号
void    widgetRemoved(int index)   //参数序号的页面被卸载掉
这两个信号可以用于同步显示的序号类控件,比如堆栈控件卸载了一个页面,那么组合框条目也要删掉该页面对应的条目。
除了堆栈控件 QStackedWidget ,还有一个功能类似的  QStackedLayout 可以承载多个标签页,QStackedLayout 是布局器,必须要有宿主窗口或容器,而 QStackedWidget 就是打包好的容器控件,功能类似的,都是容纳子页面。
下面我们通过图片信息的例子学习 QStackedWidget 使用。
我们打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 imageinfo,创建路径 D:\QtProjects\ch10,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
我们打开界面文件 widget.ui,将窗口尺寸拉大,设为 600*480,向其中拖入三个控件:
ui1
左上角是按钮,对象名 pushButtonOpen,文本为“打开图片”;
左边下部是列表控件对象名 listWidgetIndex,主要是配合右边的堆栈控件使用;
右边是堆栈控件,对象名 stackedWidget 。
stackedWidget 默认有两个子页面 page 和 page_2,我们选中 stackedWidget ,在右下角属性栏看见 currentIndex 为 0, currentPageName 为 page,我们修改当前页面的名称为 pageView,并向 pageView 页面拖入一个滚动区域控件 scrollArea:
ui2
pageView 只存放一个滚动区域,后面我们用代码实现图片预览,现在我们选中 stackedWidget 堆栈控件,当前页面序号是 0,这时我们点击上 面的水平布局按钮,为堆栈控件当前的页面设置布局器:
ui3
设置布局器之后,堆栈控件变得很小,我们重新将它尺寸拉大:
ui4
序号 0 的页面就完成布局了。我们点击 stackedWidget 右上角很小的黑色向右箭头,切换到序号为 1 的新页面 page_2:
ui5
我们修改当前页面名称为 pageInfo,并向其中拖入文本浏览框,对象名 textBrowserInfo,这个文本浏览框专门显示图片相关信息:
ui6
我们对该页面布局,选中 stackedWidget 堆栈控件,然后点击上面的水平布局按钮,为堆栈控件的当前页面设置布局,然后将 stackedWidget 堆栈控件尺寸拉大:
ui7
头两个子页面设置完成,我们在 stackedWidget 堆栈控件当前页面为 pageInfo 的时候,在右上角对象树视图,
右击 stackedWidget 控件, 右键菜单选择“插入页”-->“在当前页之后”,添加新的页面:
ui8
(注:删除页面和调整页面顺序也在 stackedWidget 右键菜单实现,“N的页面N”子菜单里面可以删除页面,“改变页顺序”可以调整页面顺序。)
添加的新页面名称默认为 page,页面序号为 2 :
ui9
我们修改新页面的对象名为 pageConvert,然后向其中拖入三个控件:
ui10
三个控件分别为:标签文本“扩展名类型”;组合框 comboBoxExtFormat;pushButtonConvert 按钮,文本“转换格式”。
然后同样地,我们选中 stackedWidget ,点击上面水平布局器按钮,对 stackedWidget 的当前页面布局,并将 stackedWidget 尺寸拉大:
ui11
界面右边的堆栈控件布局就完成了,我们下面编辑界面的左边部分。
右键点击列表控件,在右击菜单选择“编辑项目”,弹出列表控件的条目编辑界面:
list
点击右下角绿色加号按钮,为列表控件添加三个条目:
list2
添加好条目后,点击“OK”按钮,回到主窗口。我们选中“打开图片”按钮和列表控件,点击上面的垂直布局器,进行垂直布局:
left
我们将左边的布局器尺寸调整一下,变得窄一些,让左边布局器和右边的堆栈控件不重叠:
left2
最后我们选中主窗口 Widget ,点击上面的水平布局器按钮,为窗口设置水平的主布局器:
main
默认的主界面布局器左右各半,左边的布局器显得太宽了,我们选中主窗口 Widget,然后在右下角的属性栏最底部,看到主布局器的参数,我们将 layoutStretch 比例修改为英文的  1,5
main
主窗口尺寸为 600*480 , 这样我们的界面设置和布局就完成了。
我们现在需要为界面里的两个按钮添加槽函数,分别右击 “打开图片”、“转换格式”两个按钮,在右键菜单选择“转到槽...”,为按钮添加 clicked 信号对应的槽函数:
slot
添加好两个按钮槽函数之后,我们保存并关闭界面文件,下面开始代码的编写。
我们打开头文件 widget.h 编辑如下:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QImage>
#include <QPixmap>
#include <QLabel>
#include <QImageWriter> //获取转换输出支持的图片格式
#include <QFileInfo>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();
    //初始化控件函数
    void InitControls();

private slots:
    void on_pushButtonOpen_clicked();

    void on_pushButtonConvert_clicked();

private:
    Ui::Widget *ui;
    //图片预览标签
    QLabel *m_pLabelPreview;
    //图片文件名
    QString m_strImageName;
    //加载图片对象
    QImage m_image;

};

#endif // WIDGET_H
我们添加图片、显示标签、图像保存、文件信息等类型的头文件包含,然后为窗口类添加初始化控件的函数 InitControls();
添加显示图片控件指针 m_pLabelPreview 、图片文件名字符串 m_strImageName、以及用于加载图片的对象 m_image 。
头文件还有两个槽函数,是打开图片和转换格式两个按钮对应的槽函数。
接下来我们分块编辑源文件 widget.cpp ,首先是构造和初始化部分:
#include "widget.h"
#include "ui_widget.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QDebug>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //初始化控件函数
    InitControls();
}

Widget::~Widget()
{
    delete ui;
}

//初始化控件函数
void Widget::InitControls()
{
    //第 0 号预览标签页
    //新建预览标签
    m_pLabelPreview = new QLabel();
    ui->scrollArea->setWidget( m_pLabelPreview );
    m_pLabelPreview->setStyleSheet( "background-color: lightgray;" );

    //第 1 号标签页,简单显示文本
    ui->textBrowserInfo->setText( tr("用于显示图片文件信息。") );

    //第 2 号标签页,需要填充扩展名类型的组合框
    //获取支持保存的图片格式
    QList<QByteArray> listTypes = QImageWriter::supportedImageFormats();
    int nCount = listTypes.count();
    for(int i=0; i<nCount; i++)
    {
        ui->comboBoxExtFormat->addItem( listTypes[i] );
    }

    //关联列表控件的序号信号变化到堆栈控件的切换序号槽函数
    connect(ui->listWidgetIndex, SIGNAL(currentRowChanged(int)),
            ui->stackedWidget, SLOT(setCurrentIndex(int)) );
    //默认显示头一个标签页
    ui->stackedWidget->setCurrentIndex( 0 );
}
添加了文件对话框和信息提示框类型的头文件包含,然后在窗口构造函数添加了 InitControls() 函数调用。
InitControls() 函数用于初始化控件,该函数功能如下:
对于序号 0 的标签页面,先新建一个预览标签 m_pLabelPreview,并将预览标签对象设置给 滚动区域 ui->scrollArea,然后为预览标签设置一个浅灰色的背景色。
对于序号 1 的标签页面,简单为文本浏览框设置一行字符串。
对于序号 2 的标签页面,我们获取图片保存类 QImageWriter 支持的所有保存格式列表,存到 listTypes ;
然后循环遍历格式列表,逐一添加给组合框 ui->comboBoxExtFormat。
然后将列表框与堆栈控件联动的关键,就是将 ui->listWidgetIndex 对象的序号变化信号 currentRowChanged(int)
关联到 ui->stackedWidget 对象的设置序号槽函数 setCurrentIndex(int)。

这样当列表选中条目变化时,堆栈控件就会同步切换对应的标签页来显示。
InitControls() 函数最后设置堆栈控件默认显示序号为 0 的标签页。

下面我们编辑“打开图片”按钮对应的槽函数:
//打开图片文件
void Widget::on_pushButtonOpen_clicked()
{
    //获取文件名
    QString strFileName = QFileDialog::getOpenFileName(this, tr("打开图片文件"), "",
                           "Images (*.png *.bmp *.jpg);;All files(*)" );
    if( strFileName.isEmpty() ) //判断是否为空
    {
        return;
    }
    //文件名存在,尝试加载
    QImage imgTemp;
    if( ! imgTemp.load( strFileName ) ) //判断加载是否失败
    {
        QMessageBox::warning(this, tr("打开文件失败"), tr("加载图片数据失败,不支持该格式。"));
        return;
    }
    //成功加载,保存文件名和图片对象
    m_strImageName = strFileName;
    m_image = imgTemp;
    // 0 号标签预览
    m_pLabelPreview->setPixmap( QPixmap::fromImage( m_image ) );
    // 1 号标签页显示文件信息
    QString strInfo = m_strImageName + tr("\r\n");
    strInfo += tr("图片尺寸: %1 x %2\r\n").arg( m_image.width() ).arg( m_image.height() );
    strInfo += tr("颜色深度: %1\r\n").arg( m_image.depth() );
    //设置到文本浏览框
    ui->textBrowserInfo->setText( strInfo );
}
我们调用文件对话框的静态函数,获取要打开的文件名;
判断 strFileName 是否为空,如果为空就返回,不处理;如果检查是非空的文件名,继续后面代码。
定义临时的图片对象 imgTemp,根据文件名加载图片。判断返回值,如果加载失败,那么弹窗提示出错,返回不处理;
如果成功加载图片文件,那么继续后面代码。
加载成功后,保存文件名、图片对象到成员变量 m_strImageName、m_image ;
对于 0 号标签页面,我们设置预览控件 m_pLabelPreview 显示新的图像,QPixmap::fromImage( m_image ) 是将 QImage 转为 QPixmap 方便标签预览显示。
对于 1 号标签页面,我们构造信息字符串 strInfo,包含图片文件路径、图片尺寸、图片颜色深度等信息,设置给文本浏览框显示。
最后的 2 号标签页面不需要设置,因为初始化时已经构造完成。

接下来我们编辑“转换格式”按钮对应的槽函数:
//转换新格式图片
void Widget::on_pushButtonConvert_clicked()
{
    //要转换的新格式
    QString strNewExt = ui->comboBoxExtFormat->currentText();
    //判断与旧的文件格式是否一样
    if( m_strImageName.endsWith( strNewExt, Qt::CaseInsensitive ) )
    {
        QMessageBox::warning(this, tr("转换图片格式"), tr("新旧图片扩展名一样,不需要转换。"));
        return;
    }
    //需要转换新格式
    QFileInfo fi( m_strImageName );
    //新名字
    QString strNewName = fi.absolutePath() + tr("/")
            +  fi.completeBaseName()  + tr(".") + strNewExt ;
    qDebug()<<strNewName;
    //转换格式保存为新文件
    if( m_image.save( strNewName ) )
    {
        QMessageBox::information(this, tr("转换图片格式"),
             tr("转换成功,新文件为:\r\n") + strNewName );
    }
    else
    {
        QMessageBox::warning(this, tr("转换图片格式"), tr("转换失败!"));
    }
}
该函数先获取转换后新的扩展名存到 strNewExt ;
判断新扩展名是否与旧的文件扩展名一致,如果一样的不需要转换,只有扩展名不一样才进行后续转换。
如果是新的不一样的扩展名,我们根据旧文件名定义文件信息对象 fi ;
然后根据旧文件的路径、文件基本名、新扩展名拼接生成新的转换后文件名,存到 strNewName 。
然后调用保存函数 m_image.save( strNewName ),该函数自动根据扩展名转换格式,如果转换成功,返回 true,我们弹窗显示转换成功,并显示新的文件名;如果保存失败,返回 false,弹窗显示出错。

示例代码就介绍到这,我们生成项目,运行示例程序:
run1
点击“打开图片”按钮,选中一个图片打开,如果图片较大,可以拖动滚轮,轮流显示大图各个部分:
run2
我们点击列表控件第二个条目“图片信息”,堆栈控件自动切换页面如下:
run3
第二个页面显示了图片的文件名、尺寸和颜色深度。
我们点击列表第三个条目“图片转换”,页面再次切换:
run4
点开组合框的条目列表,可以看到 Qt 支持很多格式的图片格式写入,具体的转换格式功能请读者自行测试,我们下面学习功能最丰富的 Tab Widget 标签页控件。

10.3.2 Tab Widget 标签页控件

标签页控件类型名为 QTabWidget,可以容纳多个标签页,并且自带页面标题栏,例如 Qt Assistant 文档浏览器的首选项对话框:
tabwid
通过点击标题栏按钮,标签页控件每次呈现一个对应的标签页。围绕标题栏和标签页的设置,QTabWidget 提供了很多的功能函数,下面分类介绍这些功能。
(1)添加页面
主要是追加和插入页面函数:
int    addTab(QWidget * page, const QString & label)        //根据页面指针、页面标题文本追加新页面到末尾
int    addTab(QWidget * page, const QIcon & icon, const QString & label)  //根据页面指针、页面标题图标和文本追加新页面到末尾
int    insertTab(int index, QWidget * page, const QString & label)  //根据指定序号、页面指针和页面标题文本插入页面到该序号位置
int    insertTab(int index, QWidget * page, const QIcon & icon, const QString & label)  //根据指定序号、页面指针、页面标题图标和文本插入页面到该序号位置
addTab() 和 insertTab() 函数返回值都是添加新页面的真实序号位置。如果 insertTab() 参数指定序号不合法,超出正常范围,那么实际的新页面序号一定是返回值的数值,而不是参数里的非法数值。
(2)卸载页面
之所以叫卸载页面,是因为这些函数只是将页面从 QTabWidget 卸载下来,去掉对应的标题按钮,但不会删除该页面本身。
void    removeTab(int index)   //卸载指定序号的页面
void    clear()  //卸载所有子页面,QTabWidget对象彻底为空
卸载的页面可以重新添加或者用 delete 彻底删除,取决于实际场景用途。
(3)访问函数
获取子页面的数量,使用下面函数:
int    count() const
根据序号查页面指针,或者根据页面指针查序号,使用下面一对函数:
QWidget *    widget(int index) const  //根据序号查页面指针
int    indexOf(QWidget * w) const      //根据页面指针查序号
注意 widget() 函数如果参数序号超出合法范围,那么返回 NULL 指针;
 indexOf() 函数参数指针如果非法或者不属于 QTabWidget 对象,那么返回序号 -1 。
一定要检查返回值是否正常,对应异常返回值要单独判断处理,避免出现 bug 。

获取当前显示的页面和页面序号,使用下面函数:
int    currentIndex() const   //当前页面的序号
QWidget *    currentWidget() const  //当前页面的指针
注意如果 QTabWidget 对象没有任何子页面,那么返回的当前页面指针为 NULL,序号是 -1 。
一定不要对没有子页面的 QTabWidget 对象调用这些函数。

每个子页面可以设置禁用或者启用,当禁用子页面时,该页面控件能显示,但是无法点击操作:
void QTabWidget::​setTabEnabled(int index, bool enable)  //设置指定序号页面是否启用
bool QTabWidget::​isTabEnabled(int index) const    //判断指定序号页面是否处于启用状态

(4)标题栏定制函数
QTabWidget 自己拥有标题栏,可以点击标题栏按钮切换各个子页面。标题栏具有很多的定制功能函数,下面介绍这些函数:
① 标题栏显示位置
TabPosition    tabPosition() const
void    setTabPosition(TabPosition)
QTabWidget::TabPosition 枚举类型共有上下左右四个位置,英文习惯称为北南西东:

常量 数值 描述
QTabWidget::North 0 标题栏绘制在页面上方。
QTabWidget::South 1 标题栏绘制在页面下方。
QTabWidget::West 2 标题栏绘制在页面左侧,并且标题文本图标向左旋转 90 度。
QTabWidget::East 3 标题栏绘制在页面右侧,并且标题文本图标向右旋转 90 度。

标题栏绘制在页面上方和下方,标题文本和图标都是正常的水平显示,标题文字和图标都是正的。
但是左右两侧的情况特殊,显示在左侧时,标题文本和图标做了向左 90 度旋转,序号 0 的标签页面在最上方,序号大的标签页面排在下面:
left
标题栏显示在右侧的时候,标题文本和图标做了向右90度旋转,序号 0 的标签在最上方,序号大的标签页面排在下面:
right
②标题栏按钮的形状
TabShape    tabShape() const
void    setTabShape(TabShape s)
QTabWidget::TabShape 形状共有两种,上面截图都是默认的圆角矩形按钮,还有三角形(其实是梯形):

常量 数值 描述
QTabWidget::Rounded 0 标题栏按钮绘制成圆角矩形。
QTabWidget::Triangular 1 标题栏按钮绘制成三角形(其实是梯形)。

QTabWidget::Triangular 实际的按钮外观如下图所示:
Triangular
③标题栏的文本、图标、工具提示、帮助信息设置
这些内容比较像列表控件的条目,QTabWidget 的标题按钮也可以设置这些信息:

读取函数 设置函数 描述
QString tabText(int index) const void setTabText(int index, const QString & label) 读取或设置指定序号标题按钮的文本。
QIcon tabIcon(int index) const void setTabIcon(int index, const QIcon & icon) 读取或设置指定序号标题按钮的图标。
QString tabToolTip(int index) const void setTabToolTip(int index, const QString & tip) 读取或设置指定序号标题按钮的工具提示。
QString tabWhatsThis(int index) const void setTabWhatsThis(int index, const QString & text) 读取或设置指定序号标题按钮的帮助信息。

QTabWidget 可以指定图标的尺寸,使用下面函数:
QSize    iconSize() const   //获取标题栏按钮图标的尺寸,各个按钮的图标都一样大
void    setIconSize(const QSize & size)  //设置标题栏按钮图标的尺寸
iconSize 默认情况下与外观风格 style 是相关的,这里设置的图标尺寸是指图标可以占据的最大尺寸,但是如果图片尺寸本身很小,那么不会放大图片,而只会增加图片周围的空白填充区域。

标题栏中,如果标签页文本太长了,可以设置文本省略显示的模式:
Qt::TextElideMode    elideMode() const     //获取标签长文本显示模式
void    setElideMode(Qt::TextElideMode)   //设置标签长文本显示模式
Qt::TextElideMode 枚举值如下:

常量 数值 描述
Qt::ElideLeft 0 长文本的左边被省略,显示省略号到左边。
Qt::ElideRight 1 长文本的右边被省略,显示省略号到右边。
Qt::ElideMiddle 2 长文本的中间被省略,显示省略号到中间。
Qt::ElideNone 3 不省略,尽量显示完整的长文本。

默认的 elideMode() 是与显示风格有关,可以不省略显示,或者用右边省略。

标题栏当标签按钮个数小于 2,就是只有 1 个子页面时可以设置自动隐藏:
bool    tabBarAutoHide() const    //获取是否自动隐藏标题栏,只有一个子页面时有用
void    setTabBarAutoHide(bool enabled)   //设置标题栏是否自动隐藏,只有一个子页面时有用
如果标题栏的标签按钮有 2 个或以上,那么 tabBarAutoHide 属性没有用,这个 tabBarAutoHide 属性与单词字面意义不一样,功能比较鸡肋。

④标题栏其他设置
标题栏可以设置拖动标签按钮移动各个标签页的显示顺序(内部子页面序号不会变,只是显示位置不同了):
bool    isMovable() const
void    setMovable(bool movable)

标题栏可以设置拖动标签页是否可以关闭:
bool    tabsClosable() const
void    setTabsClosable(bool closable)
默认标签页是不关闭的,总是显示所有子标签;如果设置了标签页可以关闭,那么每个标签按钮文本旁边有 X 号,如果用户点击 X 号,那么触发信号:
void QTabBar::​tabCloseRequested(int index)   //触发信号,提示用户点击X号想关闭标签页
用户点击 X 号之后,QTabWidget 不会自动关闭标签页,程序员需要为上面信号关联处理的槽函数,通过槽函数代码来进行标签页卸载或隐藏。

标题栏可以设置滚动按钮的显示,当标签页比较多的时候,显示滚动箭头控制标签按钮的切换和显示:
bool    usesScrollButtons() const
void    setUsesScrollButtons(bool useButtons)

标题栏可以设置类似苹果系统的文档模式,不显示标签页的边框,方便更多地显示文档:
bool    documentMode() const
void    setDocumentMode(bool set)

QTabWidget 的标题栏本质是一个 QTabBar 对象,标题栏功能是 QTabBar 实现的,很多关于标题栏的函数是将 QTabBar 相应的函数进行改造套壳。
可以直接获取 QTabWidget 内嵌的 QTabBar 对象:
QTabBar *    tabBar() const    //获取标题栏的对象指针
QTabBar 有很多定制函数可以使用,常用的 QTabBar 函数 QTabWidget 都做了套壳封装,如果需要其他精细的定制可以调用 QTabBar 的函数来实现,例如 QTabBar 的每一个按钮都可以自定义。

QTabWidget 标题栏在水平显示的情况下,还可以自定义一个角落控件:
QWidget *    cornerWidget(Qt::Corner corner = Qt::TopRightCorner) const        //默认为 NULL
void    setCornerWidget(QWidget * widget, Qt::Corner corner = Qt::TopRightCorner)  //设置定义的角落控件
角落按钮只有标题栏水平显示时才能生效( QTabWidget::North 、QTabWidget::South ),角落控件支持的显示位置有:

常量 数值 描述
Qt::TopLeftCorner 0 角落控件显示到左上角。
Qt::TopRightCorner 1 角落控件显示到右上角。
Qt::BottomLeftCorner 2 角落控件显示到左下角。
Qt::BottomRightCorner 3 角落控件显示到右下角。

QTabWidget 默认没有角落控件,如果需要显示 logo 图片之类的,可以新建一个标签控件,设置为角落控件。

(5)信号和槽函数
QTabWidget 在当前页面序号变化时触发如下信号:
void    currentChanged(int index)
注意参数可能是 -1,代表 QTabWidget 里面没有设置当前页面,或者还没有添加任何子页面。
当用户点击标题栏的标签按钮时,根据单击或双击触发下面信号:
void    tabBarClicked(int index)   //指定序号标签按钮被单击
void    tabBarDoubleClicked(int index)  //指定序号标签按钮被双击

当程序里面设置 QTabWidget 标题栏属性 setTabsClosable( true ) 的时候,标题按钮显示 X 号,用户点击 X 号触发如下信号:
void    tabCloseRequested(int index)
注意 QTabWidget 只发送上面信号,不会自动关闭标签页,如果需要关闭标签页,需要程序员自己写槽函数来关闭。

QTabWidget 有两个槽函数,都是设置当前显示页面:
void    setCurrentIndex(int index)  //根据序号设置当前页
void    setCurrentWidget(QWidget * widget)  //根据页面指针设置当前页

QTabWidget 内容介绍到这,下面我们学习一个文件属性例子。
我们打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 fileattribute,创建路径 D:\QtProjects\ch10,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
我们打开界面文件 widget.ui,向其中拖入一个 Tab Widget 控件,稍微拉大该控件:
ui1
我们点击标签页控件的“Tab 1” 页面,“Tab 1”成为当前页面,并且让 tabWidget 对象保持选中状态,我们在右下角属性栏编辑当前页面的属性,修改 currentTabText 设置为“文件名称”,currentTabName 设置为 tabFileName,如下图所示:
ui2
然后向“文件名称”标签页拖入控件,如下图所示:
ui3
第一行是标签“文件全名”,单行编辑器 lineEditFullName,“选择文件”按钮 pushButtonSelectFile;
第二行是标签“文件短名”,单行编辑器 lineEditShortName;
第三行是标签“文件大小”,单行编辑器 lineEditFileSize。
将三行控件尽量在水平和垂直方向排列整齐,然后我们点击选中 tabWidget 容器对象,再点击上方的栅格布局按钮,实现当前页面的栅格布局,栅格布局自动调整了 tabWidget  尺寸,我们将它再拉大一些:
ui4
第一个标签页的控件和布局就完成了。下面我们点击“Tab 2”页面,然该页面成为新的当前页面, 修改当前页面的标签文本和标签对象名,即 currentTabText 设置为“文件时间”,currentTabName 设置为 tabFileTime, 如下图所示:
ui5
然后我们向“文件时间”标签页拖入三行控件,如下图所示:
ui6
该标签页第一行是标签“创建时间”,单行编辑器 lineEditTimeCreated;
第二行是标签“访问时间”,单行编辑器 lineEditTimeRead;
第三行是标签“修改时间”,单行编辑器 lineEditTimeModified。
将三行控件水平和垂直方向尽量对齐,然后选中 tabWidget 对象,再点击上方的栅格布局按钮,实现栅格布局,并将容器对象尺寸拉大:
ui7
这样第二个标签页的布局器也设置完成。我们选中主窗口本身的 Widget 对象,点击上面的垂直布局器按钮实现主界面布局,虽然主界面只有一个直接儿子节点,就是容器对象 tabWidget,还是需要设置窗口的主布局器,以实现自动布局:
ui8

完成以上控件编辑和布局之后,我们再点击“文件名称”标签页,右击“选择文件”按钮,为该按钮添加 clicked() 信号的槽函数。
按钮的槽函数添加完成后,我们就可以保存并关闭该 widget.ui 界面文件。

关闭 widget.ui 界面文件之后,我们在左边项目管理页面,右击“fileattribute”项目根名称, 在右键菜单选择“添加新文件”:
addnew1
弹出“新建文件”对话框,我们在该对话框的左边一栏选择“Qt”,中间一栏选择“Qt 设计师界面类”:
addnew2
然后点击右下角“Choose...” 按钮,进入如下界面:
addnew3
在中间界面模板里面,点击“Widget”,然后点击“下一步”按钮,进入如下界面:
addnew4
我们修改类名为 TabPreview,下面几行的信息会根据类名自动修改,修改后如下:
addnew5
点击“下一步”按钮,进入项目管理界面:
addnew6
点击“完成”按钮,这样就为项目新增了一个窗口类 TabPreview,这个窗口以后我们会将它设置为标签页控件的第三个子页面。
打开 tabpreview.ui 界面文件,我们向其中拖入三个按钮和一个堆栈容器:
newui1
三个按钮是普通的 QPushButton,我们将按钮尺寸拉伸为水平窄、垂直长的竖条形状,然后注意设置文本:
第一个按钮 pushButtonTextPreview,文本为 "文\n本\n预\n览",按钮文本支持使用 "\n" 换行显示;
第二个按钮 pushButtonImagePreview,文本为 "图\n像\n预\n览" ;
第三个按钮  pushButtonBytePreview,文本为 "字\n节\n预\n览" 。
这样三个按钮就形成的竖直的排列。
这样设置 QPushButton 按钮文本,是因为 QTabWidget 标题栏如果显示在左侧或右侧时,会对文本进行 90 度旋转,旋转后的文本并不符合中文垂直阅读的习惯,因为汉字被旋转了 90度,并不好看。我们这里相当于使用普通按钮和堆栈控件组合了一个自定义的多标签页控件,按钮的文本垂直排列更符合中文垂直排版习惯。
下面我们选中堆栈控件 stackedWidget,对序号 0 的子页面进行修改,设置 currentPageName 为 pageTextPreview,
向其中拖入一个文本浏览框,对象名 textBrowserText,如下所示:
newui2
我们点击选中 stackedWidget 对象,然后点击上面的垂直布局按钮,为当前页面进行布局:
newui3
对 0 号当前页面布局完成后,我们点击堆栈控件右上角的黑色向右箭头,切换到 1 号标签页面,将 1 号标签页面作为新的当前页面:
newui4
在选中堆栈控件时,我们修改新的 1 号页面 currentPageName 为 pageImagePreview,并拖入一个 Label :
newui5
预览图像的标签对象名为 labelImagePreview,文本为 “图像预览区域”,并且修改属性 alignment 为水平的 AlignHCenter,垂直的 AlignVCenter,
使得标签对象的文本同时处于水平和垂直居中。
然后我们点击选中堆栈控件对象 stackedWidget,为新的当前页面设置布局,就是 1 号页面设置布局,点击上面垂直布局按钮,进行垂直布局:
newui6
下面我们右击对象树的 stackedWidget 容器,在右键菜单选择“插入页”-->“在当前页之后”,我们在序号 1 的页面后再加一个新页面:
newui7
添加新的序号为 2 的页面之后,我们修改新的 2 号当前页面对象名为 pageBytePreview:
newui8
然后我们为序号 2 的页面添加一个文本浏览框,对象名设置为 textBrowserByte:
newui9
然后我们选中堆栈控件 stackedWidget,再次为序号 2 的当前页面设置布局,点击上面垂直布局按钮,进行垂直布局:
newui10
这样就实现了堆栈控件三个子页面的控件编辑和布局。
下面我们对该窗口进行布局,先选中左边三个按钮,进行垂直布局:
newui11
然后我们选中该窗口根名称 TabPreview,点击上面的水平布局按钮,对该窗口设置布局器为水平布局:
newui12
该窗口布局器设置好之后,我们发现左边按钮比较矮胖,这时候可以对三个按钮的属性进行调整,我们按住 Ctrl 键,点击三个按钮,同时选中该三个按钮,然后在右下角属性栏设置 sizePolicy 的垂直策略为 Expanding,修改 maximumSize 的宽度为 52,如下图所示:
newui13
这样我们的按钮看着比较正常一些,垂直方向自动拉伸,水平方向比较窄。目前我们看到按钮的上下边界要比右边编辑框的上下边界宽,
在垂直方向稍微有点偏差,上边线和下边线没有对齐,是堆栈控件内部子页面布局器的原因。
我们在对象树点击第 0 号子页面,看到该页面属性栏的布局器参数:
newui14
布局器有四个边界保留宽度,即 layoutLeftMargin、layoutTopMargin、layoutRightMargin、layoutBottomMargin,将它们设置为 0,我们的编辑器就可以扩展到最外面边界,不保留任何冗余。 我们把堆栈控件三个子页面依次选中,把四个布局边界都设置为 0 。
newui15
现在左边三个按钮上下边界就和右边编辑器同样高度了。附带说明一下,layoutSpacing 是布局器里面各个子控件的布局间隙,比如上面三个按钮的间隙都是 6 。
完成布局调整后,我们保存 tabpreview.ui 界面文件,然后关闭该文件,开始编写代码。
主窗口的代码相对比较简单,我们先编辑 widget.h 头文件的代码:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QFile>
#include <QFileInfo>
#include <QFileDialog>
#include "tabpreview.h"

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

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


signals:
    //文件名变化时触发该信号
    void fileNameChanged(const QString &fileName);

private slots:
    void on_pushButtonSelectFile_clicked();

private:
    Ui::Widget *ui;
    //自定义第三个标签页面
    TabPreview *m_pTabPreview;

    //文件全名
    QString m_strFileName;
    //文件信息查看对象
    QFileInfo m_fileInfo;
};

#endif // WIDGET_H
文件开头添加了文件、文件信息类、文件对话框以及定制的 TabPreview 标签页类的头文件包含。
类声明里,我们声明了信号 fileNameChanged( ) ,用于文件名变化时,将新文件名传递给  TabPreview 标签页。
on_pushButtonSelectFile_clicked() 是“选择文件”按钮对应的槽函数,当选择新文件名时,更新多个标签页的内容。
声明了 TabPreview  的指针,保存第三个标签页“文件预览”的页面对象。
类末尾添加了变量保存文件名称和文件信息对象。

接下来我们分两段编辑 widget.cpp 文件,首先是构造函数部分:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QMessageBox>
#include <QDateTime>

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

    //新建文件预览标签页
    m_pTabPreview = new TabPreview();
    //添加给标签页控件
    ui->tabWidget->addTab( m_pTabPreview, tr("文件预览") );

    //关联信号和槽函数,更新文件名
    connect( this, SIGNAL(fileNameChanged(QString)),
            m_pTabPreview, SLOT(onFileNameChanged(QString)) );
}

Widget::~Widget()
{
    delete ui;
}
构造函数里面,新建第三个“文件预览”标签页对象,存到 m_pTabPreview 指针;
然后将新标签页添加给 ui->tabWidget 标签页控件;
关联主窗口的 fileNameChanged( QString ) 信号到“文件预览”标签页的 onFileNameChanged( QString ) 槽函数,将修改的新文件名传给子标签页。m_pTabPreview 子标签页的功能由它自己类的代码实现,主窗口完全不用操心 m_pTabPreview 这个子页面干了什么,我们只需要通过信号和槽函数机制传递文件名参数即可。

下面我们编辑“选择文件”按钮的槽函数:
//选择新文件查看属性
void Widget::on_pushButtonSelectFile_clicked()
{
    QString strName = QFileDialog::getOpenFileName(this, tr("选择文件"),
                      tr(""), tr("All files(*)") );
    strName = strName.trimmed();  //剔除空格
    if( strName.isEmpty() )
    {
        return; //没有文件名
    }
    //获取了文件名
    m_strFileName = strName;
    m_fileInfo.setFile( m_strFileName );
    //文件大小
    qint64 nFileSize = m_fileInfo.size();

    //设置“文件名称”的标签页控件
    ui->lineEditFullName->setText( m_strFileName );
    ui->lineEditShortName->setText( m_fileInfo.fileName() );
    ui->lineEditFileSize->setText( tr("%1 字节").arg( nFileSize ) );

    //三个时间
    QString strTimeCreated = m_fileInfo.created().toString("yyyy-MM-dd  HH:mm:ss");
    QString strTimeRead = m_fileInfo.lastRead().toString("yyyy-MM-dd  HH:mm:ss");
    QString strTimeModified = m_fileInfo.lastModified().toString("yyyy-MM-dd  HH:mm:ss");

    //设置“文件时间”的标签页控件显示
    ui->lineEditTimeCreated->setText( strTimeCreated );
    ui->lineEditTimeRead->setText( strTimeRead );
    ui->lineEditTimeModified->setText( strTimeModified );

    //触发文件名称修改的信号
    emit fileNameChanged(m_strFileName);
}
该函数开头调用文件对话框的静态函数获取新的文件名,将文件名剔除首尾空白字符,然后判断文件名是否为空,如果文件名是空的,直接返回,不需要处理。
文件名如果非空,那么我们保存文件名到成员 m_strFileName,并将新文件名设置给文件信息对象 m_fileInfo, 获取文件大小存到 nFileSize。

然后我们针对标签页控件的第一个“文件名称”页面,更新控件:
文件全名设置为 m_strFileName,这个全名包含文件夹路径和文件名本身,例如   D:/file/img.jpg   ;
文件短名就是 m_fileInfo.fileName(),不包括文件夹路径的,只有文件名本身 ,例如 img.jpg ;
然后设置文件大小的文本,显示字节数。

在设置第一个页面后,我们针对第二个“文件时间”页面,获取文件创建时间、最后读取时间、最后修改时间,转换为时间字符串;
设置给“文件时间”页面的三个单行编辑器显示三种时间。

槽函数末尾我们触发 fileNameChanged(m_strFileName) 信号,将新文件名传递给第三个标签页 “文件预览”, 至于第三个标签页的功能,我们下面编辑 TabPreview 类的内容。
我们编辑 tabpreview.h 头文件内容:
#ifndef TABPREVIEW_H
#define TABPREVIEW_H

#include <QWidget>
#include <QButtonGroup>
#include <QPixmap>
#include <QFile>

namespace Ui {
class TabPreview;
}

class TabPreview : public QWidget
{
    Q_OBJECT

public:
    explicit TabPreview(QWidget *parent = 0);
    ~TabPreview();
    //初始化控件
    void InitControls();

public slots:
    void onFileNameChanged(const QString &fileName);

private:
    Ui::TabPreview *ui;
    //保存文件名
    QString m_strFileName;
    //按钮分组对象
    QButtonGroup m_buttonGroup;
    //保存预览图像
    QPixmap m_image;
};

#endif // TABPREVIEW_H
TabPreview 也是 QWidget 的派生类,基于 QWidget 开发的派生类,如果指定父对象为 NULL,那么可以作为独立子窗口弹窗显示;如果添加给窗口或者控件的子节点,那么就会成为子控件。QWidget 派生类可以是独立窗口,也可以是控件,取决于它父对象是什么。
不论是开发窗口还是子控件, QWidget 派生类本身的代码是独立自成一体的,开发代码通常不需要区分窗口或子控件的形式。
通过信号和槽机制,窗口和控件之间保持高度的独立性,各模块之间关系松耦合,很容易移植模块集成到别的项目。
我们在主窗口代码把 TabPreview 添加给标签页对象,作为第三个标签页,所以是以子控件形式存在。

TabPreview 添加了 InitControls() 函数用于初始化各个控件;
添加了自定义的槽函数 onFileNameChanged() 用于更新文件名,并处理预览界面的更新。
添加了文件名字符串 m_strFileName,按钮分组对象 m_buttonGroup,以及加载图像预览的 m_image 对象。
QButtonGroup 按钮分组用于管理多个按钮,并可以为每个按钮映射特征值,所有按钮点击时,触发相同的信号,信号带有映射的特征值以映射不同按钮。这个功能可以方便地管理一组很多个按钮,只需要关联一个信号和一个槽函数,要不然 100 个按钮配 100 个槽函数,太繁琐啰嗦。按钮分组就非常方便管理很多功能类似的按钮。

下面我们编辑 tabpreview.cpp 文件的内容,首先是构造函数和初始化部分:
#include "tabpreview.h"
#include "ui_tabpreview.h"
#include <QDebug>

TabPreview::TabPreview(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::TabPreview)
{
    ui->setupUi(this);
    //初始化控件
    InitControls();
}

TabPreview::~TabPreview()
{
    delete ui;
}

//初始化控件
void TabPreview::InitControls()
{
    //设置二态按钮,类似复选框的选中和未选中状态
    ui->pushButtonTextPreview->setCheckable( true );
    ui->pushButtonImagePreview->setCheckable( true );
    ui->pushButtonBytePreview->setCheckable( true );
    //设置按钮分组,分组里的按钮默认互斥,只有一个处于选中状态
    //为每个按钮分配一个整数序号,分组对象可以自动发送带序号的点击信号
    m_buttonGroup.addButton( ui->pushButtonTextPreview, 0 );
    m_buttonGroup.addButton( ui->pushButtonImagePreview, 1 );
    m_buttonGroup.addButton( ui->pushButtonBytePreview, 2 );
    //设置分组的触发信号到堆栈控件切换页面槽函数
    connect( &m_buttonGroup, SIGNAL(buttonClicked(int)),
            ui->stackedWidget, SLOT(setCurrentIndex(int)) );

    //设置所有按钮的选中状态外观
    this->setStyleSheet( "QPushButton:checked { background-color: yellow }" );
    //设置字节浏览框的背景色
    ui->textBrowserByte->setStyleSheet( "background-color: #AAEEFF" );
    //图片预览标签背景色
    ui->labelImagePreview->setStyleSheet( "background-color: #E0E0E0" );
}
构造函数调用了 InitControls() 函数,该初始化函数内容如下:
将“文本预览”、“图像预览”、“字节预览”三个按钮都设置为二态按钮 setCheckable( true ),就是可以有弹起、按下两种状态;
然后将三个按钮添加到分组对象 m_buttonGroup,并分别映射整数 0、1、2 ,
按钮分组默认就有使得成员按钮状态互斥的功能,所以三个按钮只会出现唯一的按钮处于按下状态(checked 属性为 true),其他都是弹起状态(checked 属性为 false)。
我们关联分组对象 m_buttonGroup 的信号 buttonClicked(int) 到堆栈控件的槽函数 setCurrentIndex(int) ,这样各个按钮被点击时,自动切换对应的堆栈控件子页面。
接着我们设置样式表,this->setStyleSheet( "QPushButton:checked { background-color: yellow }" ) ,会将本界面里的所有按钮的按下状态显示为黄色背景色,显得高亮,与其他按钮区分;
函数后面两句设置 ui->textBrowserByte 、ui->labelImagePreview 的背景色,这样三个堆栈子页面切换时,我们看背景色就能区分清楚三个不同的子页面。

tabpreview.cpp 文件最后是自定义的函数代码,用于更新文件名和该文件的三种预览效果:
//文件名变化时,更新预览
void TabPreview::onFileNameChanged(const QString &fileName)
{
    m_strFileName = fileName;
    //这里主要按图片和非图片区分处理
    //尝试按图片加载
    bool isImage = m_image.load( m_strFileName );
    //判断
    if( isImage )
    {
        ui->labelImagePreview->setText("");
        ui->labelImagePreview->setPixmap( m_image );
    }
    else
    {
        m_image = QPixmap();    //空图
        ui->labelImagePreview->setPixmap( m_image );
        ui->labelImagePreview->setText( tr("不是支持的图片,无法以图片预览。") );
    }

    //文本预览和字节预览统一使用前 200 字节内容来显示
    QFile fileIn( m_strFileName );
    if( ! fileIn.open( QIODevice::ReadOnly ) )
    {
        //无法读取,无法预览
        qDebug()<<tr("文件无法打开:")<<m_strFileName;
    }
    else
    {
        QByteArray baData = fileIn.read( 200 );
        //转为文本
        QString strText = QString::fromLocal8Bit( baData );
        //普通文本预览
        ui->textBrowserText->setText( strText );
        //转为HEX字节文本显示,大写的十六进制字母
        QString strHex = baData.toHex().toUpper();
        //十六进制字节浏览
        ui->textBrowserByte->setText( strHex );
    }

    //根据文件形式,设置默认的预览界面
    if( isImage )  //判断图片
    {
        //通过函数点击按钮一次,与鼠标点击一样,触发 clicked() 信号
        ui->pushButtonImagePreview->click();
    }
    else //非图片
    {
        //文本
        if( m_strFileName.endsWith( ".txt", Qt::CaseInsensitive )
                || m_strFileName.endsWith( ".h", Qt::CaseInsensitive )
                || m_strFileName.endsWith( ".cpp", Qt::CaseInsensitive )
                || m_strFileName.endsWith( ".c", Qt::CaseInsensitive ) )
        {
            ui->pushButtonTextPreview->click();
        }
        else //其他
        {
            ui->pushButtonBytePreview->click();
        }
    }
}
槽函数先保存新的文件名到成员变量;
调用 m_image.load( m_strFileName )  尝试加载文件,如果加载成功,那么我们将 ui->labelImagePreview 文本置空,并设置预览图像对象 m_image;如果加载失败,文件可能不是图像,也可能是我们不支持的图像格式,那么设置 m_image 为空图像,并设置给 ui->labelImagePreview,并设置标签文本,提示不是支持的图片,不能以图片形式预览。

无论是不是图片、文本或其他类型文件,对于文本预览和字节预览,我们简单化处理,直接打开该文件;
打开文件成功,读取 200 字节字节内容存到 baData,如果文件不够 200 字节长度,那么会自动读取现有存在的字节内容;
调用 QString::fromLocal8Bit( baData ) 将字节数据转换为文本字符串 strText,显示到 ui->textBrowserText 文本框;
调用 baData.toHex().toUpper() 把字节数据转为十六进制的字节描述字符串,比如 AA33 就代表两字节的内容,将字节的十六进制字符串显示到 ui->textBrowserByte 文本框。

槽函数最后是简单的判断文件格式,如果是图片文件,那么调用 ui->pushButtonImagePreview->click(),通过代码点击一次“图像预览”按钮,切换到图像预览页面;
如果不是图片,那么判断文件名的末尾扩展名,如果是 .txt   .h   .cpp   .c 扩展名,那么当做文本文件,通过代码点击一次“文本预览”按钮,切换到文本预览页面,其他情况都当做二进制字节文件,预览字节的十六进制内容。

本例子中,主窗口 Widget 类和孙子控件 TabPreview 功能都是非常独立的,Widget 类不操心 TabPreview 有什么功能,只是将 TabPreview 对象添加给标签页控件的第三个页面;TabPreview 也不关心谁是父对象,不管自己作为独立窗口还是作为子孙控件,TabPreview 代码不用修改,只需要把文件名传给槽函数 onFileNameChanged( QString ) 就行了。

Qt 的窗口或控件类开发具有很好的模块独立性,窗口或控件之间交互只需要编写信号和槽函数就行了,数据交互通过信号和槽函数实现,其他代码都是完全独立的。如果其他项目使用 TabPreview 模块功能,那么将 tabpreview.ui、tabpreview.h、tabpreview.cpp 三个文件复制并添加到新项目,就可以直接集成使用,TabPreview 模块代码压根不用修改,只要在主窗口新建 TabPreview 对象,并关联信号和槽函数就搞定了。

我们编译运行上面示例,例如打开一个文本文件 :
run1
第一个“文件名称”标签页显示了文件全名、文件短名、文件大小;
点击“文件时间”页面,显示如下:
run2
点击“文件预览”页面,自动显示了文本预览的堆栈控件子页面:
run3
如果我们在第一个“文件名称”标签页打开图片文件,那么默认就会显示图片预览的子页面:
run4
可以手动点击堆栈控件左边的三个按钮,切换预览子页面,比如点击“字节预览”按钮,显示如下内容:
run5
这个示例不仅展示了 QTabWidget 标签页控件,我们也通过分组按钮和堆栈控件实现了自定义的竖排按钮的多页面控件 TabPreview 。
TabPreview 的按钮竖排文本比较适合汉字竖排的习惯。 QTabWidget 标签页控件当标题栏显示在左侧或右侧时,会将汉字旋转 90 度,不太符合汉字习惯。
本节的内容讲解到这,下面留两个练习题,请读者自己实现。

tip 练习
① fileattribute 例子中,将单行编辑器都设置成只读模式,因为没有修改文件属性的功能。
② fileattribute 例子中,将标签页控件顶部的标题栏三个文本修改为 "文件名称(&N)" 、"文件时间(&T)"、"文件预览(&P)",修改后运行示例,测试快捷键 Alt+N、Alt+T、Alt+P 的功能。



prev
contents
next