8.4 基于条目控件的自定义特性

本节介绍基于条目控件的定制特性,首先介绍条目的拖拽,列表控件、表格控件、树形控件内置了支持拖拽的特性,添加少许代码即可使用。然后介绍控件的右 键菜单构造方 法,采用的方法是修改基类 QWidget 的 contextMenuPolicy 属性,并添加弹出菜单槽函数,这种方法对于所有 QWidget 派生类控件都通用。最后简要介绍基于条目控件的样式表设置,定制高亮条目显示、双色交替行显示、表头和角按钮配色等。三个小节分别设置一个示例,学以致用。

8.4.1 条目的拖拽(Drag and Drop)

QListWidget、QTableWidget、 QTreeWidget 自带了拖拽条目的功能,拖拽通常有两种应用场景:
第一种场景是内部拖拽
仅在控件内部使用,用于调整条目的先后顺序,实现控件内的条目移动功能。对于树形条目,内部移动还可以改变条目的父子关系,比如将子节点提升为兄弟节 点。实现 控件内部拖拽只需要一句代码,设置拖拽模式为 QAbstractItemView::InternalMove,即
ui->treeWidget->setDragDropMode(QAbstractItemView::InternalMove);
对于列表控件、表格控件、树形控件,都是调用一样的函数 setDragDropMode(QAbstractItemView::InternalMove) 。QAbstractItemView::InternalMove 只会在控件内部移动条目,不会复制条目,仅仅调整排列顺序或层级。

拖拽的第二种场景,就是跨界拖拽
在控件之间拖拽,比如将 listWidget1 的条目 A 拖给 listWidget2,这种拖拽效果是复制一个新的条目 A 给 listWidget2,不是移动,而是条目新建和数据复制。结果就是 listWidget1 有自己的 A 条目, listWidget2 创建了新条目,新条目数据克隆自 listWidget1 的 A 条目。这种模式也支持 listWidget1 的条目自己拖给自己,就是克隆新的条目 A ,添加给 listWidget1 自身。不仅是同类型的条目控件之间可以拖拽,不同类型如 QListWidget、QTableWidget、 QTreeWidget ,它们三者之间也可以互相拖拽条目。效果都是复制源条目,将新建的条目添加给接收拖拽的控件。
为基于条目的控件启用跨界拖拽功能,需要进行如下设置:
(1)设置控件的 dragEnabled 属性为 true;
(2)设置控件的视口 viewport() 的 acceptDrops 属性为 true;
(3)为用户显示拖拽动作的鼠标效果,设置控件的 showDropIndicator 属性为 true;
(4)设置控件的拖拽模式为能拖能拽的 QAbstractItemView::DragDrop 。
拖拽条目一般都是单选模式,只拖拽一个。(其实多选模式可以拖拽多个,就是不太常见。)
对于单选模式拖拽条目,示例代码如下:
QListWidget *listWidget = new QListWidget(this);
listWidget->setSelectionMode(QAbstractItemView::SingleSelection); //单选模式
listWidget->setDragEnabled(true);  //可以拖出源条目
listWidget->viewport()->setAcceptDrops(true); //可以接收拖入
listWidget->setDropIndicatorShown(true); //启用拖拽的显示效果
listWidget->setDragDropMode(QAbstractItemView::DragDrop); //使用能拖能拽的模式

对于基于条目的三种控件,内部拖拽是条目移动,跨界拖拽是条目新建并复制。我们下面通过简单示例学习拖拽功能。
打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 itemdragdrop,创建路径 D:\QtProjects\ch08,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
我们打开 widget.ui 界面文件,按照下图拖入控件:
ui
第一行是列表控件,默认对象名 listWidget;
第二行是树形控件,默认对象名 treeWidget;
第三行是表格控件,默认对象名 tableWidget;
第四行是两个单选按钮,“内部拖拽”radioButtonInter、“跨界拖拽”radioButtonOuter,第四行使用水平布局。
窗口整体使用垂直布局,尺寸 500*500 。
我们右击单选按钮,为两个单选按钮分别添加槽函数,选第二个 clicked(bool) :
slot
三个条目控件的内容用代码编写,图形界面设置就到这,我们下面开始编辑头文件 widget.h 的代码:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QListWidget>//列表控件
#include <QTreeWidget>//树形控件
#include <QTableWidget>//表格控件

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

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

private slots:
    void on_radioButtonInter_clicked(bool checked);

    void on_radioButtonOuter_clicked(bool checked);

private:
    Ui::Widget *ui;
    //设置 QAbstractItemView 派生类的跨界拖拽功能
    //对列表控件、树形控件、表格控件通用,C++多态性
    void SetOuterDragDrop( QAbstractItemView *view );

};

#endif // WIDGET_H
widget.h 添加了三个条目控件类的头文件引用;
然后是两个单选按钮的槽函数;
最后添加了一个设置启用跨界拖拽功能的函数,这个函数对三种条目控件都是通用的。

下面来看 widget.cpp源文件的内容,首先是头文件包含、构造函数、析构函数:
#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //构造列表控件的条目
    for(int i=0; i<5; i++)
    {
        QListWidgetItem *itemL = new QListWidgetItem( ui->listWidget );
        itemL->setText( tr("listItem %1").arg(i) );
    }

    //设置树形控件2列
    ui->treeWidget->setColumnCount( 2 );
    //各列均匀拉伸
    ui->treeWidget->header()->setSectionResizeMode(QHeaderView::Stretch);
    //树形控件构造条目
    for(int i=0; i<5; i++)
    {
        QTreeWidgetItem *itemT = new QTreeWidgetItem( ui->treeWidget );
        itemT->setText(0, tr("treeItem %1, 0").arg(i) );
        itemT->setText(1, tr("t%1, 1").arg(i) );
    }

    //设置表格 3*3
    ui->tableWidget->setColumnCount( 3 );
    ui->tableWidget->setRowCount( 3 );
    //各列均匀拉伸
    ui->tableWidget->horizontalHeader()->setSectionResizeMode( QHeaderView::Stretch );
    //构造表格条目
    for(int i=0; i<3; i++)
    {
        for(int j=0; j<3; j++)
        {
            QTableWidgetItem *itemTA = new QTableWidgetItem();
            itemTA->setText( tr("tableItem %1, %2").arg(i).arg(j) );
            ui->tableWidget->setItem( i, j, itemTA );
        }
    }

    //默认选中内部移动模式
    ui->radioButtonInter->setChecked(true);
    on_radioButtonInter_clicked(true);//启用内部移动
}

Widget::~Widget()
{
    delete ui;
}
构造函数首先为列表控件新建了 5 个条目,按照行编号设置条目文本。
然后设置树形控件的列数为 2,设置树头视图使得各列均匀拉伸;
为树形控件新建了 5 个顶级条目,根据行号、列号设置 5 个条目的文本,每个条目 2 列文本。
然后设置表格为 3 行  3 列,设置水平表头使得各列均匀拉伸;
为表格新建 3*3 个条目,每个条目根据行号、列号设置文本。
最后设置“内部拖拽”单选按钮为选中状态,并调用该按钮槽函数设置三个条目控件为内部拖拽模式。

接下来看看“内部拖拽”单选按钮的槽函数:
void Widget::on_radioButtonInter_clicked(bool checked)
{
    if(checked)
    {
        //列表控件启用内部移动
        ui->listWidget->setDragDropMode(QAbstractItemView::InternalMove);
        //树形控件启用内部移动
        ui->treeWidget->setDragDropMode(QAbstractItemView::InternalMove);
        //表格控件启用内部移动
        ui->tableWidget->setDragDropMode(QAbstractItemView::InternalMove);
    }
}
这个函数非常简单,判断该按钮如果为选中状态,那么调用三个条目控件的 setDragDropMode() 函数设置拖拽模式为内部移动。

下面看看“跨界拖拽”单选按钮的槽函数:
void Widget::on_radioButtonOuter_clicked(bool checked)
{
    if(checked)
    {
        //列表控件启用跨界拖拽
        SetOuterDragDrop(ui->listWidget);
        //树形控件启用跨界拖拽
        SetOuterDragDrop(ui->treeWidget);
        //表格控件启用跨界拖拽
        SetOuterDragDrop(ui->tableWidget);
    }
}
这个函数也是超级简单的,检查按钮如果为选中状态,那就为三个条目控件调用 SetOuterDragDrop() 函数。
实际干活的是下面的 SetOuterDragDrop() 函数:
//启用跨界拖拽
void Widget::SetOuterDragDrop(QAbstractItemView *view)
{
    view->setSelectionMode(QAbstractItemView::SingleSelection); //单选模式
    view->setDragEnabled(true);  //可以拖出源条目
    view->viewport()->setAcceptDrops(true); //视口可以接收拖入
    view->setDropIndicatorShown(true); //启用拖拽的显示效果
    view->setDragDropMode(QAbstractItemView::DragDrop); //使用能拖能拽的模式
}
这个函数对 QAbstractItemView  派生类对象是通用的,QListWidget、QTableWidget、 QTreeWidget 都是它的派生类(孙辈的派生类)。该函数第一句是设置选中模式为单选;第二句是设置可以拖出源条目;第三句是设置控件的视口(显示可见部分)可以接收条目拖入;第四句是显示拖拽的鼠标效果;最后是设置拖拽模式为 QAbstractItemView::DragDrop,能够拖出和拽入。
通过单独的 SetOuterDragDrop() 函数设置跨界拖拽,避免为每个条目控件都敲五局代码,省了很多事,以后不管几个条目控件,都只需要调用 SetOuterDragDrop() 函数就能设置跨界拖拽了。
本示例代码讲解到这,下面构建运行该程序,程序启动时效果如下图:
run1
测试内部拖拽功能,对三个控件内部进行拖拽:
run2
列表控件的内部拖拽只改变条目的先后顺序;树形控件的内部拖拽不仅可以改变先后顺序,也可以改变层级关系;
测试表格控件内部拖拽,将(0,0)单元格条目拖到(0,1)单元格后,(0,0)单元格被清空,
(0,1)单元格填上了 "tableItem 0,0" 文本,表格控件的内部拖拽相当于剪切+粘贴,会清空源单元格,覆盖目的单元格。

我们点击“跨界拖拽”按钮,进入跨界拖动的模式:
run3
我们把树形控件的条目"treeItem 4,0     t4,1" 拖到列表控件和表格控件里面的底部空白位置,可以看到进入列表控件后被拆成两个条目,占据列表控件的末尾两行;拖给表格控件后,拆成两列,是两个单元格条目。
示例中列表控件只有 1 列,树形控件 2 列,表格控件 3 列,它们之间是可以互相拖入条目的,但是一般不建议列数不同的控件互相拖拽条目。这里只是测试功能,实际程序中要尽量在列数相同的情况下拖拽。读者还可以进行其他拖拽测试,我们下面小节开始讲解右键菜单的创建,这个知识稍微提前了一点讲解,但是很实用。

8.4.2 自定义右键菜单(ContextMenu)

右键菜单功能很常见,我们添加槽函数的操作就是通过右键菜单实现的。在 Qt 库中,每一条菜单项称为 QAction,多条菜单项组成一个菜单,菜单类是 QMenu。菜单项 QAction 不仅可以用于菜单,工具栏的按钮也是 QAction 实现的。QAction 常用构造函数如下:
QAction(const QString & text, QObject * parent)
QAction(const QIcon & icon, const QString & text, QObject * parent)
菜单项可以有文本和图标,第一个构造函数只有文本,第二个构造函数同时带了图标和文本。菜单项一般直接以窗口为 parent,方便全局管理。
用户点击菜单项触发如下信号:
void QAction::triggered(bool checked = false) //点击菜单项触发信号
参数里 checked 一般用不到,只有在设置菜单项可以勾选的时候才用。菜单项设置勾选功能是通过下面函数实现:
void  QAction::setCheckable(bool)

单独的菜单项是不能弹出显示的,需要将菜单项添加到菜单或工具栏才能使用。菜单 QMenu 的构造函数如下:
QMenu(QWidget * parent = 0)
QMenu(const QString & title, QWidget * parent = 0)
菜单本身可以有一个文本,比如 QtCreator 拥有多个菜单,第一个是“文件”菜单,“文件”两个字就是菜单的文本,如下图所示:
menu
通常情况下,由多个 QAction 组成一个 QMenu,然后由多个 QMenu 组成一个 QMenuBar。
QtCreator 有 8 个 QMenu 菜单,共同组成一行 QMenuBar(菜单条,或叫菜单栏),QMenuBar 以后到了主窗口程序章节再详细讲解,本小节只是初步学一下右键菜单功能。

右键菜单通常不显示菜单自身的文本,只会弹出各个菜单项。创建了 QAction 对象后,将其添加到菜单的函数如下:
void QMenu::addAction(QAction * action)
QMenu 还有其他 方便添加菜单项的函数,这里不一一列举了,以后主窗口程序章节会详细讲解这个类。
QMenu 的基类是 QWidget,本身也属于一个控件,弹出菜单一般使用如下两个函数:
void QMenu::popup(const QPoint & p, QAction * atAction = 0) //异步弹出菜单
QAction * QMenu::​exec(const QPoint & p, QAction * action = 0)//同步弹出菜单
这两个函数弹出菜单的效果是一样的,唯一的差别是 exec() 函数会返回被用户点击的菜单项指针,如果用户没有点击菜单项,那么返回 NULL。
这两个函数参数也是一样的,第一个是菜单显示位置的坐标 p,注意 p 是以屏幕左上角为原点(0,0),而控件反馈的坐标一般是相对控件自己的内部坐标,需要用转换函数 **widget->mapToGlobal( p ) ,将 p 转换为屏幕坐标。第二个参数 action 是指显示菜单时保证菜单项 action 恰好显示在 p 点位置,方便用户优先选择 action 菜单项。

QWidget 派生类控件都支持自定义右键菜单,但是默认没有启用这个功能,要启用这个功能,需要进行两步操作:
(1)设置控件的 contextMenuPolicy 属性数值为 Qt::CustomContextMenu,即自定义右键菜单模式;
(2)为请求弹出右键菜单的信号添加槽函数,该信号原型为:
void QWidget::customContextMenuRequested(const QPoint & pos)   //pos 是相对控件自身的坐标,需要转为全局屏幕坐标弹菜单
为请求右键菜单信号添加槽函数后,在槽函数里执行菜单的 popup() 或 exec() 就能弹出菜单了。

菜单类 QMenu 主要是显示用途,而实际的功能是通过菜单项 QAction 的槽函数实现,一般每个 QAction 都对应一个槽函数,该槽函数关联到 QAction::triggered() 信号。

下面我们通过一个示例,为列表控件添加自定义的菜单,实现添加、编辑、删除条目和清空所有条目的功能。
打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 contextmenu,创建路径 D:\QtProjects\ch08,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
我们打开 widget.ui 界面文件,按照下图拖入控件:
ui
第一行是标签,文本为“请用右键菜单操作:”;
第二行是列表控件,默认对象名 listWidget 。
窗口整体使用垂直布局,尺寸 400*300 。本例子的功能都使用代码实现,下面首先编辑 widget.h 文件代码:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QListWidget>//列表控件
#include <QMenu>//菜单
#include <QAction>//菜单项

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

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

    //添加槽函数
public slots:
    //弹出右键菜单的槽函数
    void onCustomContextMenuRequested(const QPoint & pos);
    //添加条目菜单项的槽函数
    void onAddItemTriggered();
    //编辑条目菜单项的槽函数
    void onEditItemTriggered();
    //删除条目菜单项的槽函数
    void onDelItemTriggered();
    //清空所有条目的菜单项槽函数
    void onClearAllTriggered();

private:
    Ui::Widget *ui;
    //保存右键菜单的指针
    QMenu *m_menuContext;
    //创建菜单并关联信号和槽函数
    void CreateMenu();
};

#endif // WIDGET_H
widget.h 文件先添加了列表控件、菜单、菜单项的头文件包含;
在窗口类里面手动添加了 public slots,总共五个槽函数:
第一个槽函数用于关联控件的请求弹出右键菜单信号;
后面四个槽函数对应四个菜单项的点击信号;
最后添加了菜单指针 m_menuContext,以及创建右键菜单、关联信号和槽的函数 CreateMenu()。

下面来分段查看源文件 widget.cpp 代码,首先是头文件包含、构造函数、析构函数:
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QDebug>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //创建菜单,并关联信号和槽函数
    CreateMenu();
}

Widget::~Widget()
{
    delete ui;
}
widget.cpp 添加了消息框和调试打印头文件,构造函数只添加了一句 CreateMenu() 函数调用。该函数具体代码如下:
void Widget::CreateMenu()
{
    //创建右键菜单对象
    m_menuContext = new QMenu(tr("ContextMenu")); //右键菜单其实不显示ContextMenu文本

    //创建“添加条目”菜单项并添加到菜单
    QAction *actAdd = new QAction(tr("添加条目"), this);
    m_menuContext->addAction( actAdd );
    //创建“编辑条目”菜单项并添加到菜单
    QAction *actEdit = new QAction(tr("编辑条目"), this);
    m_menuContext->addAction( actEdit );
    //创建“删除条目”菜单项并添加到菜单
    QAction *actDel = new QAction(tr("删除条目"), this);
    m_menuContext->addAction( actDel );
    //创建“清空所有”菜单项并添加到菜单
    QAction *actClearAll = new QAction(tr("清空所有"), this);
    m_menuContext->addAction( actClearAll );

    //设置列表控件可以有自定义右键菜单
    ui->listWidget->setContextMenuPolicy( Qt::CustomContextMenu );
    //关联弹出菜单信号
    connect(ui->listWidget, SIGNAL(customContextMenuRequested(QPoint)),
            this, SLOT(onCustomContextMenuRequested(QPoint)) );

    //为四个菜单项关联点击信号到槽函数
    connect(actAdd, SIGNAL(triggered()), this, SLOT(onAddItemTriggered()));
    connect(actEdit, SIGNAL(triggered()), this, SLOT(onEditItemTriggered()));
    connect(actDel, SIGNAL(triggered()), this, SLOT(onDelItemTriggered()));
    connect(actClearAll, SIGNAL(triggered()), this, SLOT(onClearAllTriggered()));
    //创建完毕
    return;
}
该函数首先创建菜单对象,保存到成员变量 m_menuContext;
然后分别创建了四个菜单项“添加条目”、“编辑条目”、“删除条目”、“清空所有”,并添加给菜单 m_menuContext;
接着设置列表控件的右键菜单策略为  Qt::CustomContextMenu ,即使用自定义的右键菜单;
关联 listWidget 请求弹出右键菜单的信号到槽函数;
最后将四个菜单项的点击信号关联到对应的槽函数上。

下面来看弹出右键菜单的槽函数代码:
//弹出右键菜单的槽函数
void Widget::onCustomContextMenuRequested(const QPoint & pos)
{
    //控件内的相对坐标转为屏幕坐标
    //是列表控件发出的信号,就用列表控件的转换函数
    QPoint screenPos = ui->listWidget->mapToGlobal( pos );
    //弹出菜单
    QAction *actRet = m_menuContext->exec( screenPos );
    if(NULL != actRet)//检查非空才能使用该指针
    {
        qDebug()<<tr("返回的菜单项:") + actRet->text();
    }
}
由于弹菜单的请求是由列表控件发出的,所以我们首先在该函数里使用列表控件的转换函数 mapToGlobal(),
将参数里的 pos 转为屏幕绝对坐标的 screenPos ;
然后调用菜单的 exec() 函数显示右键菜单;
m_menuContext->exec( screenPos ) 函数的返回值是用户点击的菜单项指针,如果没有点击就返回 NULL,
我们对返回值进行非空判断,对于非空指针打印菜单项的文本。

上面的槽函数仅仅是显示出右键菜单,并不具有实际的操作功能,实际的功能是通过四个菜单项关联的槽函数逐个实现的。
下面来看第一个“添加条目”菜单项的槽函数:
//添加条目菜单项的槽函数
void Widget::onAddItemTriggered()
{
    QListWidgetItem *itemNew = new QListWidgetItem(tr("新建条目"));
    //设置可以编辑
    itemNew->setFlags( itemNew->flags() | Qt::ItemIsEditable );
    //添加给控件
    ui->listWidget->addItem( itemNew );
    //设置新条目为选中的条目
    ui->listWidget->setCurrentItem( itemNew );
    //显示条目的编辑框
    ui->listWidget->editItem( itemNew );
}
该函数新建一个条目,默认文本“新建条目”,设置条目的双击可编辑标志位;
添加条目给列表控件,并设置新条目为当前选中状态;
最后自动开启新条目的编辑框,方便用户直接编辑新条目的文本。
因为条目设置了双击可编辑标志位,用户以后也可以双击编辑,而不需要使用单独的编辑控件来设置文本。

第二个是“编辑条目”菜单项的槽函数:
//编辑条目菜单项的槽函数
void Widget::onEditItemTriggered()
{
    //获取选中的条目
    QListWidgetItem *curItem = ui->listWidget->currentItem();
    if(NULL == curItem)
    {
        qDebug()<<tr("没有选中的条目。");
        return; //返回
    }
    //设置选中条目可以编辑
    curItem->setFlags( curItem->flags() | Qt::ItemIsEditable );
    //显示选中条目的编辑框
    ui->listWidget->editItem( curItem );
}
该函数首先获取选中的条目,如果没有选中的条目就打印信息并返回。
如果有选中的条目,设置该条目为双击可编辑状态,并显示该条目的编辑框,这样用户就可以修改条目文本了。
当用户点击其他位置,该条目的编辑框失去焦点时,列表控件自动关闭条目的编辑框。

第三个是“删除条目”菜单项的槽函数:
//删除条目菜单项的槽函数
void Widget::onDelItemTriggered()
{
    //获取选中的条目
    QListWidgetItem *curItem = ui->listWidget->currentItem();
    if(NULL == curItem)
    {
        qDebug()<<tr("没有选中的条目。");
        return; //返回
    }
    //删除条目
    delete curItem; curItem = NULL;
}
该函数比较简单,获取当前选中的条目,如果指针为空不处理;
如果指针不空,那么删除该条目,并将指针置空。

最后是“清空所有”菜单项的槽函数:
//清空所有条目的菜单项槽函数
void Widget::onClearAllTriggered()
{
    //判断条目个数,如果没条目不需要操作,直接返回
    int nCount = ui->listWidget->count();
    if(nCount < 1)
    {
        return;
    }
    //提示Yes、No询问消息框,获取返回值,防止用户误操作全删
    //如果用户选“Yes”就全删,否则不操作
    int buttonRet = QMessageBox::question(this, tr("清空所有"), tr("请确认是否清空所有条目?"));
    if( QMessageBox::Yes == buttonRet )//用户选择了“Yes”
    {
        ui->listWidget->clear();
    }
    else //否则不处理
    {
        return;
    }
}
该函数首先获取列表控件的条目数量,如果没有条目就不处理,直接返回。
如果有条目,那么先弹出询问对话框 QMessageBox::question() ,
询问对话框默认是 Yes 和 No 两个按钮,用户点击任意一个按钮都会返回该按钮的常量值,
Yes 按钮对应的数值就是 QMessageBox::Yes。
当用户点击了 Yes 按钮时,说明用户确认要清空所有条目,那么执行清空操作;
如果用户点击了 No或者两个都不点击,直接关闭询问对话框,那么不处理,保留所有条目。
这个函数针对用户可能的误操作做了询问处理,防止用户误操作导致所有条目都被删除。

一般删除全部内容之类的操作,程序都要弹出询问对话框,提醒用户确认一下是否真的全部删除。
如果不进行询问,用户误操作把几个小时编辑的内容全删了,那样用户就抓狂了。

这个示例的代码讲解到这,我们构建运行示例,通过右键菜单添加几个条目,然后测试“清空所有”菜单项的功能:
run
如果点击 Yes 按钮就真的全清空了,如果点击 No 或者不点击这两个按钮,直接点右上角 X 关闭对话框,那么条目都会保留。
读者还可以测试其他菜单项功能,这里不截图了。我们下面小节对基于条目的控件简单设置几个样式表,定制一下外观。

8.4.3 基于条目控件的样式表(Style Sheet)

本小节以表格控件为例,介绍基于条目的控件常用的样式表设置,涵盖表格控件各个部分的定制显示。
(1)表格整体的前景色、背景色
一般所有的控件都有前景色、背景色,设置方法都是一样的,样式表代码举例:
color: darkblue;
background-color: cyan;
color 就是前景色,上面设置为深蓝色;background-color 就是背景色,上面设置为青色。
(2)双色交替行的设置
如果希望表格奇偶行的颜色不同,两种颜色交替显示行的背景色,那么需要开启双色交替行的显示:
tableWidget->setAlternatingRowColors( true );  //这是C++代码
序号为偶数的行使用默认背景 background-color 颜色填充,序号为奇数的行使用 alternate-background-color 。
一般不用修改默认背景色,只修改交替背景色即可以,比如样式表代码:
alternate-background-color: skyblue;
这样就是白色背景行与天蓝色背景行交替显示。如果不设置样式表,默认交替的 alternate-background-color  是浅灰色的。
(3)高亮选中条目的颜色设置
高亮条目单独有前景色和背景色可以设置,样式表举例:
selection-color: red;
selection-background-color: yellow;
selection-color 就是高亮条目前景色,上面样式表就是红色文本;selection-background-color 是高亮条目的背景色,上面举例的样式表就是黄色背景填充。
(4)表格的网格线颜色设置
列表控件和树形控件都没有网格线,只有表格控件有网格线,样式表举例:
gridline-color: darkgreen;
上面就是将网格线设置成 深绿色。
(5)表头的颜色设置
树形控件和表格控件都有头部,头部的本质是 QHeaderView 类,我们设置该类的前景色、背景色就能修改表头配色,样式表举例:
QHeaderView{
    color: darkblue;
    background-color: cyan;
}
注意要用类名 QHeaderView 和大括号把前景色、背景色的样式表文本包裹起来,这样限定修改表头的配色,而不会影响表格控件整体的配色。
(6)角按钮的颜色设置
表格左上角有个特殊的角按钮,点击它会选中表格全部内容,比如下图左上角红色的部分就是表格的角按钮:
CornerButton
角按钮既不属于水平表头,也不属于垂直表头,而是单独的类 QTableCornerButton。这个角按钮没有函数可以获取它的指针,但是可以配置它的显示颜色。
设置角按钮的配色,需要同时设置背景色和边框的颜色,如果不设置边框颜色,在有些窗口主题里面会看不到角按钮的背景色效果。设置角按钮配色的样式表举例:
QTableCornerButton::section {
    background: red;
    border: 2px outset red;
}
上面设置角按钮的背景色为红色,边框宽度2像素,边框也是红色。
(7)配置所有条目的颜色
基于条目的控件都可以配置条目颜色,通过 **Widget::item 设置条目配色,样式表举例:
QTableWidget::item{
    color: darkblue;
    background-color: cyan;
}
这会配置所有条目的前景色为深蓝,即文字颜色,背景色用青色填充。注意,这个条目配色会覆盖掉双色交替行、 高亮选中条目的配色,一般不要单独设置 ::item 颜色,那样会看不到高亮选中的颜色了。
(8)配置滚动条的颜色
滚动条的类是 QScrollBar,我们对该类设置前景色和背景色即可,样式表举例:
QScrollBar{
    color: yellow;
    background-color: green;
}
上面配置滚动条用绿色背景填充,然后用前景色显示滚动条两端的三角形箭头颜色,如下图所示:
scrollbar
(9)加与不加 类名大括号 的区别
我们设置表格控件的前景色和背景色时,可以有两种写法,第一种不加类名和大括号:
color: red;
background-color: yellow; 
如果直接把上面文本设置为表格控件样式表,那么显示效果如下:
colorall
可以看到表格控件内嵌的所有子控件颜色全部改变了,无论是角按钮、水平表头、垂直表头、单元格、水平滚动条、垂直滚动条等等都变色了。不加类名和大括号时,样式表对表格控件所有组成部分都生效。
如果我们用类名 QTableWidget 和大括号包裹住样式表文本,比如:
QTableWidget{
    color: red;
    background-color: yellow;
}
将该文本设置为表格控件的样式表,显示效果如下:
colorpart
这次角按钮、两个表头、两个滚动条都没有变色。颜色改变的只有单元格以及右下角空白位置、狭缝等部位。
我们这里将角按钮、两个表头、两个滚动条称为表格控件的子控件区域;
单元格、右下角空白位置、狭缝等称为表格控件的直辖区域。

将样式表文本用类名和大括号包裹之后,样式就仅对表格控件的直辖区域生效,而子控件区域不会生效。子控件区域一般有自己的样式表定制方式,就是用子控件的类名和大括号包裹,在大括号里配置子控件颜色。

在配置控件颜色时,可以使用类名大括号包裹的方式,也可以不用类名大括号包裹,根据显示效果需求和用户爱好来定。
如果需要一体化配色,那么就不需要类名大括号包裹;如果希望子控件区域和直辖区域分别配色,那么就用类名大括号包裹样式表,注意子控件区域的配色总是需要子控件类名和大括号包裹。

另外说明一下:如果表格控件配置了整体的前景色和背景色,子控件也单独配置了前景色和背景色,那么在该子控件区域内优先使用单独配置的子控件颜色,即子控件配置优先。对于同一个颜色配置项,比如第一次指定前景色为红色,第二次指定前景色为蓝色,那么后配置的蓝色生效,即对同一颜色配置项,后配置的优先。

本小节样式表的内容介绍到这,下面通过一个简单例子测试几个样式表配色。
打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 tablestyle,创建路径 D:\QtProjects\ch08,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
我们打开 widget.ui 界面文件,按照下图拖入控件:
ui
第一行是表格控件,默认对象名 tableWidget;
第二行和第三行总共六个按钮,文本对象名分别为:
“双色交替行”pushButtonAlternatingRowColors、“选中条目定制”pushButtonSelectionCustom、“所有条目定制”pushButtonItemCustom,
“角按钮定制”pushButtonCornerButtonCustom,“表头定制”pushButtonHeaderCustom、“清空样式表”pushButtonClearStyle;
六个按钮使用网格布局,窗口整体使用垂直布局,窗口尺寸 440*330 。
布局完成后,我们为六个按钮逐一添加槽函数:
slot
本例子的功能都用代码实现,首先是头文件 widget.h 的代码:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

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

private slots:
    void on_pushButtonAlternatingRowColors_clicked();

    void on_pushButtonSelectionCustom_clicked();

    void on_pushButtonItemCustom_clicked();

    void on_pushButtonCornerButtonCustom_clicked();

    void on_pushButtonHeaderCustom_clicked();

    void on_pushButtonClearStyle_clicked();

private:
    Ui::Widget *ui;
};

#endif // WIDGET_H
六个按钮的槽函数是通过右键菜单添加的,文件代码没有手动添加代码,保持原样即可。

下面分段来看源文件 widget.cpp 的代码,首先是头文件包含、构造函数、析构函数:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //设置行列 4*4
    ui->tableWidget->setColumnCount(4);
    ui->tableWidget->setRowCount(4);
    //设置表格水平头,各列均匀分布
    ui->tableWidget->horizontalHeader()->setSectionResizeMode( QHeaderView::Stretch );
    //新建表格条目
    for(int i=0; i<4; i++)
    {
        for(int j=0; j<4; j++)
        {
            //新建条目,并根据行号、列号设置文本
            QTableWidgetItem *itemNew = new QTableWidgetItem();
            itemNew->setText( tr("tableItem %1, %2").arg(i).arg(j) );
            ui->tableWidget->setItem( i, j, itemNew );
        }
    }
    //构建完毕
}

Widget::~Widget()
{
    delete ui;
}
构造函数里首先设置表格为 4 列 4 行,设置水平表头,使得表格各列均匀拉伸;
然后用两重循环为表格创建条目,条目的文本根据行号、列号设置。

下面来看第一个按钮“双色交替行”对应的槽函数:
//本例子都用类名和大括号包住里面的内容
void Widget::on_pushButtonAlternatingRowColors_clicked()
{
    //启用双色交替行显示
    ui->tableWidget->setAlternatingRowColors( true );
    //定制样式表,交替行采用天蓝色,表格的网格线用深绿色
    QString strStyle = " QTableWidget{ alternate-background-color: skyblue; "
                    "gridline-color: darkgreen; } " ;
    //添加给表格控件,旧的样式表保留
    ui->tableWidget->setStyleSheet( ui->tableWidget->styleSheet() + strStyle );
}
该函数首先设置表格控件为双色交替行显示;
然后设置样式表的文本,设置交替行的颜色为天蓝色,设置网格线的颜色为深绿;
最后把文本设置为表格的样式表,并且用字符串拼接保留了控件原有旧的样式表。
本小节的例子都用类名和大括号包住样式表,因为后面需要对子控件进行定制。
如果样式表需要设置多次或子控件需要定制,那么一般建议用类名和大括号包裹样式表。

下面来看第二个按钮“选中条目定制”对应的槽函数:
void Widget::on_pushButtonSelectionCustom_clicked()
{
    //selection-color 是选中条目的前景色
    //selection-background-color 是选中条目的背景色
    QString strStyle = " QTableWidget{ selection-color: red; "
            "selection-background-color: yellow; } ";
    //添加给表格控件,旧的样式表保留
    ui->tableWidget->setStyleSheet( ui->tableWidget->styleSheet() + strStyle );
    //设置当前条目为高亮色
    QTableWidgetItem *curItem = ui->tableWidget->currentItem();
    if(NULL != curItem)
    {
        curItem->setSelected(true); //标上选中的高亮色
    }
}
该函数先设置样式表文本,选中高亮条目的前景色为红色(文字颜色),背景用黄色填充;
然后将文本设置为表格控件的样式表,并保留了旧的样式表;
最后获取当前条目(带虚线框的条目),如果非空,就将该条目设置为选中状态(虚框+高亮颜色),方便直接查看设置的选中高亮条目效果。

下面来看第三个按钮“所有条目定制”对应的槽函数:
void Widget::on_pushButtonItemCustom_clicked()
{
    // QTableWidget::item 就是所有条目的样式表配置
    // color 是前景色,background-color 是背景色
    QString strStyle = " QTableWidget::item{ "
            "color: blue; "
            "background-color: lightgreen; "
            "} " ;
    //设置给表格控件,QTableWidget::item 样式表与前面两个函数的样式表冲突
    ui->tableWidget->setStyleSheet( ui->tableWidget->styleSheet() + strStyle );
}
该函数比较简单,设置样式表文本,然后将文本设置为表格控件的样式表,注意 QTableWidget::item 样式表会与前面两个函数的样式表效果有冲突,实际运行时 QTableWidget::item 样式表会覆盖前面的双色交替行、选中高亮颜色的配置。

下面来看第四个按钮“角按钮定制”对应的槽函数:
void Widget::on_pushButtonCornerButtonCustom_clicked()
{
    // QTableCornerButton::section 就是设置表格左上角的按钮风格
    QString strStyle =  " QTableCornerButton::section{ "
       " background: green;  "
       " border: 2px outset green; "
       "} " ;
    //添加给表格控件,旧的样式表保留
    ui->tableWidget->setStyleSheet( ui->tableWidget->styleSheet() + strStyle );
}
该函数设置样式表文本,注意角按钮的样式表需要同时设置背景色和边框颜色,保证角按钮能显示出配置的颜色;
然后把文本设置为表格控件的样式表,并保留之前旧的样式表效果。

下面来看第五个按钮“表头定制”对应的槽函数:
void Widget::on_pushButtonHeaderCustom_clicked()
{
    //QHeaderView 就是表头的类,定制该类的样式表
    //前景色背景色的一般都是 color   background-color
    QString strStyle = " QHeaderView{ "
            "color: darkblue; "
            "background-color: cyan; "
            "} " ;
    //添加给表格控件,旧的样式表保留
    ui->tableWidget->setStyleSheet( ui->tableWidget->styleSheet() + strStyle );
}
该函数也是设置样式表文本,然后把文本设置为表格控件的样式表,并保留之前旧的样式表效果。
表格的水平表头和垂直表头都是 QHeaderView 类,该样式表对两个表头都管用。

下面来看第六个按钮“清空样式表”对应的槽函数:
void Widget::on_pushButtonClearStyle_clicked()
{
    //打印旧的样式表信息
    qDebug()<<"old style sheets: \r\n"<<ui->tableWidget->styleSheet()<<endl;
    //置空样式表
    ui->tableWidget->setStyleSheet("");
    //取消双色交替行的显示
    ui->tableWidget->setAlternatingRowColors( false );
}
这个清空函数打印了原本的样式表,然后将表格控件的样式表设置为空文本,顺便将双色交替行的显示效果关了。

示例的代码讲解到这,我们构建运行该例子,点击程序左边四个按钮,看到效果:
run1
双色交替行、选中高亮颜色、角按钮、表头的颜色均变成我们代码里设置的颜色了。
这时候如果我们点击“所有条目定制”按钮,看到下图效果:
run1
双色交替行和选中高亮色的效果都没了,全被 QTableWidget::item 样式表覆盖了。一般不要轻易配置 QTableWidget::item 样式表颜色,会与双色交替行和高亮选中色冲突。

本节和本章的内容到这,我们下一章讲解数据容器,比如列表、链表、向量、集合、映射等等,就是程序常用的数据结构类。图形程序内部也经常使用各种数据结构,数据结构是程序开发的基础。使用合适的数据结构,能够为程序的开发提供便利。


prev
contents
next