Qt学习笔记(六)项视图类
许多应用程序允许用户搜索、查看和编辑属于某个数据集中的一些个别项。这些数据可能保存在文件中、数据库中或者网络服务器上。处理像这样的数据集的标准方式是使用Qt的项视图类(item view class)
Smalltalk语言普及了一种非常灵活的对于大数据集的可视化方法:模型-视图-控制器(Model-View-Contorller, MVC)。在MVC方法中,Model代表数据集,它对需要查看数据的获取以及任何存储负责,每种类型的数据集都有自己的模型,但不管底层的数据集是什么样,模型提供给视图view的API是相同的。视图代表的是面向用户的那些数据。在同一时间,任何大数据集只有有限的部分是可见的,所以这个有限的部分就是视图所请求的那部分数据,控制器controller是用户和视图之间的媒介,它把用户的操作转化为浏览或者编辑数据的请求,这部分数据是根据需要由视图传送给模型的数据。
借鉴MVC的方法,Qt也提供了一种模型/视图架构,模型和视图之间使用委托联系。委托对于项的如何显示和如何编辑提供精细控制。Qt对每种类型的视图都提供了默认的委托。 故我们通常不需要注意它。通过把一个模型注册到两个或者多个视图,就可以让用户使用不同的方式查看数据以及和数据交互。Qt对于多个视图会自动的保持同步,从而使对一个视图的改变会影响到全部视图。模型/视图架构的另一个好处是:如果决定改变底层数据集的存储方式,则只需要修改模型,而视图仍然能够继续正常工作。
在很多情况下,只需要把一小部分数据显示给用户,可以使用Qt提供的那些方便的项视图类(QListWidget, QTableWidget, QTreeWidget),并且可以把它们和项直接组装起来。它们把数据存在项中(如QTableWidget中包含了一些QTableWidgetItem)。实际上在这些方便的类的内部,使用了自定义的模型,就可以让这些项在视图中变得可见。
下面示例中,使用QListWidget每个项都由一个图标、一段文本和一个唯一的ID组成。
#ifndef FLOWCHARTSYMBOLPICKER_H
#define FLOWCHARTSYMBOLPICKER_H
#include <QDialog>
#include <QMap>
class QDialogButtonBox;
class QIcon;
class QListWidget;
class FlowChartSymbolPicker : public QDialog
{
Q_OBJECT
public:
FlowChartSymbolPicker(const QMap<int, QString> &symbolMap,
QWidget *parent = 0);
int selectedId() const { return id; }
void done(int result);
private:
QIcon iconForSymbol(const QString &symbolName);
QListWidget *listWidget;
QDialogButtonBox *buttonBox;
int id;
};
#endif
构造对话框时必须传递给它一个QMap<int, QString>,在它运行之后,可以通过selectedId()获得一个选中的ID(或者如果用户没有选中任何一项,返回-1)
#include <QtGui>
#include "flowchartsymbolpicker.h"
FlowChartSymbolPicker::FlowChartSymbolPicker(
const QMap<int, QString> &symbolMap, QWidget *parent)
: QDialog(parent)
{
id = -1;
listWidget = new QListWidget;
listWidget->setIconSize(QSize(60, 60));
QMapIterator<int, QString> i(symbolMap);
while (i.hasNext()) {
i.next();
QListWidgetItem *item = new QListWidgetItem(i.value(),
listWidget);
item->setIcon(iconForSymbol(i.value()));//设置项的图标
item->setData(Qt::UserRole, i.key()); //调用setData()函数,并且把任意ID保存到QListWidget中。iconForSymbol()私有函数给每个给定的符号名称返回一个ICon。
QListWidgetItem有几个角色(role),每个role都以一个关联的QVariant。最常用的role是Qt::DisplayRole, Qt::EditRole, Qt::IconRole,并且这些角色都有方便的设置和获取hanshu[setText(). setIcon()].通过指定一个大于等于Qt::UserRole的值,就可以定义自定义的角色,在上例子中使用Qt::UserRole存储每个项的ID。
}
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok
| QDialogButtonBox::Cancel);
connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(listWidget);
mainLayout->addWidget(buttonBox);
setLayout(mainLayout);
setWindowTitle(tr("Flowchart Symbol Picker"));
}
void FlowChartSymbolPicker::done(int result)
{
id = -1;
if (result == QDialog::Accepted) {
QListWidgetItem *item = listWidget->currentItem();
if (item)
id = item->data(Qt::UserRole).toInt();
}
QDialog::done(result);
}
done函数由QDialog重新实现,当用户单击OK或者Cancel时,就会调用它。如果用户单击OK,就会result == QDialog::Accepted,从而获得相应的项并且使用data()函数提取ID。如果对项的文本感兴趣,则可通过调用item->data(Qt::DisplayRole).toString()或者更为方便的item->text()来提取文本。
QIcon FlowChartSymbolPicker::iconForSymbol(const QString &symbolName)
{
QString fileName = ":/images/" + symbolName.toLower();
fileName.replace(' ', '-');
return QIcon(fileName);
}
默认情况下,QListWidget是只读的,如果想让用户编辑这些项,则可以使用QAbstractItemView::setEditTriggers()设置这个视图的编辑触发器。例如,QAbstractItemView::AnyKeyPressed这个设置值的意思是:用户只要一开始输入就进入项的编辑状态。
item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsEditable);
setEditTriggers(QAbstractItemView::AnyKeyPressed)
下例实现一个可以编辑数据的QTableWidget。
tableWidget = new QTableWidget(0, 2); //行和列的初始数字,后续会进行改变
tableWidget->setHorizontalHeaderLabels(
QStringList() << tr("X") << tr("Y")); //设置水平方向的表头
for (int row = 0; row < coordinates->count(); ++row) {
QPointF point = coordinates->at(row);
addRow();
tableWidget->item(row, 0)->setText(QString::number(point.x()));
tableWidget->item(row, 1)->setText(QString::number(point.y()));
}
默认情况下,QTableWidget会提供一个垂直表头,这个列的标签从1开始,所以不需要手工设置垂直表头的标签。
默认情况下,QTableWidget允许进行编辑。用户在这个视图中所做的任何修改都会自动影响这些QTableWidgetItem,为了防止编辑,可以setEidtTriggers(QAbstractItemView::NoEditTriggers).
void CoordinateSetter::addRow()
{
int row = tableWidget->rowCount();
tableWidget->insertRow(row);
QTableWidgetItem *item0 = new QTableWidgetItem;
item0->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
tableWidget->setItem(row, 0, item0);
QTableWidgetItem *item1 = new QTableWidgetItem;
item1->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
tableWidget->setItem(row, 1, item1);
tableWidget->setCurrentItem(item0);
}在上例中,当用户单击Add按钮的时候就会调用addRow()槽,这种方式在构造函数中也常用到。使用QTableWidget::insertRow()来插入一个新的行,然后创建两个QTableWidgetItem()项,并利用QTableWidget::insertRow()把它们添加到表中。除了该项,QTableWidget::setItem()还需要一行及一列。
QTreeWidget的例子。QTreeWidget默认是只读的。
class SettingsViewer : public QDialog
{
Q_OBJECT
public:
SettingsViewer(QWidget *parent = 0);
private slots:
void open();
private:
void readSettings();
void addChildSettings(QSettings &settings, QTreeWidgetItem *item,
const QString &group);
QTreeWidget *treeWidget;
QDialogButtonBox *buttonBox;
QString organization;
QString application;
};
SettingsViewer::SettingsViewer(QWidget *parent)
: QDialog(parent)
{
organization = "Trolltech";
application = "Designer";
treeWidget = new QTreeWidget;
treeWidget->setColumnCount(2); //设置两列
treeWidget->setHeaderLabels(
QStringList() << tr("Key") << tr("Value")); //设置列标题
treeWidget->header()->setResizeMode(0, QHeaderView::Stretch);//设置两列的重定义模式为stretch
treeWidget->header()->setResizeMode(1, QHeaderView::Stretch);
...........
.........
setWindowTitle(tr("Settings Viewer"));
readSettings();
}在构造函数最后,读取Settings
void SettingsViewer::readSettings()
{
QSettings settings(organization, application);
treeWidget->clear();
addChildSettings(settings, 0, "");
treeWidget->sortByColumn(0);
treeWidget->setFocus();
setWindowTitle(tr("Settings Viewer - %1 by %2")
.arg(application).arg(organization));
}
使用预定义模型
QT提供了几种可以在视图类中使用的预定义模型:
QStringListModel //存储一个字符串列表 ; QStandardItemModel // 存储任意分层次的数据
QDirModel //封装本地文件系统; QSqlQueryModel //封装一个SQL结果集
QSqlTableModel //封装一个SQL表; QSqlRelationalTableModel //利用外键封装一个SQL表
QSortFilterProxyModel // 排序或筛选另一个模型
实例:利用QStringListModel模型和QListView来实现列表显示。
class TeamLeadersDialog : public QDialog
{
Q_OBJECT
public:
TeamLeadersDialog(const QStringList &leaders, QWidget *parent = 0);
QStringList leaders() const;
private slots:
void insert();
void del();
private:
QListView *listView;
QDialogButtonBox *buttonBox;
QStringListModel *model;
};
TeamLeadersDialog::TeamLeadersDialog(const QStringList &leaders,
QWidget *parent)
: QDialog(parent)
{
model = new QStringListModel(this); //创建 QListStringModel模型
model->setStringList(leaders); //设置数据集
listView = new QListView;
listView->setModel(model); //QListView 设置模型
listView->setEditTriggers(QAbstractItemView::AnyKeyPressed
| QAbstractItemView::DoubleClicked);
................
}
实例:利用QDirModel模型和QTreeView控件来实现文件列表
class DirectoryViewer : public QDialog
{
Q_OBJECT
public:
DirectoryViewer(QWidget *parent = 0);
private slots:
void createDirectory();
void remove();
private:
QTreeView *treeView;
QDirModel *model;
QDialogButtonBox *buttonBox;
};
DirectoryViewer::DirectoryViewer(QWidget *parent)
: QDialog(parent)
{
model = new QDirModel;
model->setReadOnly(false);
model->setSorting(QDir::DirsFirst | QDir::IgnoreCase | QDir::Name);
treeView = new QTreeView;
treeView->setModel(model);
treeView->header()->setStretchLastSection(true);
treeView->header()->setSortIndicator(0, Qt::AscendingOrder);
treeView->header()->setSortIndicatorShown(true);
treeView->header()->setClickable(true);
QModelIndex index = model->index(QDir::currentPath());
treeView->expand(index);
treeView->scrollTo(index);
treeView->resizeColumnToContents(0);
..........................
}
void DirectoryViewer::createDirectory()
{
QModelIndex index = treeView->currentIndex();
if (!index.isValid())
return;
QString dirName = QInputDialog::getText(this,
tr("Create Directory"),
tr("Directory name"));
if (!dirName.isEmpty()) {
if (!model->mkdir(index, dirName).isValid())
QMessageBox::information(this, tr("Create Directory"),
tr("Failed to create the directory"));
}
}
void DirectoryViewer::remove()
{
QModelIndex index = treeView->currentIndex();
if (!index.isValid())
return;
bool ok;
if (model->fileInfo(index).isDir()) {
ok = model->rmdir(index);
} else {
ok = model->remove(index);
}
if (!ok)
QMessageBox::information(this, tr("Remove"),
tr("Failed to remove %1").arg(model->fileName(index)));
}
实现自定义模型
Qt 默认的数据模型(model)提供一系列方便的方法来处理和观察数据。但是这些默认的数据模型还不足以有效地处理某些比较特殊的数据源(data sources)。对于这些特殊的情况,根据底层数据建立自定义的数据模型是非常有必要的。
在开始创建自定义的数据模型之前,首先回顾在Qt model/view 架构中的关键性的概念。每个数据模型中的数据单元(data element)有一个模型索引(model index)和一系列的属性(attributes/roles)。其中,数据单元的属性可以具有任意的数据类型。其中较为常见的属性包括:Qt::DisplayRole 和 Qt::EditRole;其它的属性用于辅助类(supplementary)数据,包括:Qt::ToolTipRole,Qt::StatusTipRole 和 Qt::WhatsThisRole;还有一些用于控制基本的显示属性:Qt::FontRole,Qt::TextAlignmentRole,Qt::TextColorRole 和 Qt::BackgroundColorRole 等等。
对于一个列表数据模型(list model),其模型索引是行号(row number),这个索引值可以由 QModelIndex::row( ) 得到,有效的索引是非负的;对于一个表格模型(table model),其模型索引是行号(row number)和列号(column number)两个,可以由 QModelIndex::row( ) 和 QModelIndex::column( ) 得到。不论是列表数据模型还是表格数据模型,它们的数据单元的父类都是根(root)。根由一个非法的 QModelIndex 索引。下面是 Qt 中的数据模型的示意图:
第一个例子介绍一个只读性的表格数据模型。这张表格可以显示对应的汇率值,程序效果如下:
这个程序使用一个简单的表格控件(QTableWidget)就可以实现,但是我们希望使用自定义的数据模型,以便利用好数据之间的某些特点减小数据内存使用量。正常情况下下,如果我们需要保存表格中目前正在交易的162个汇率值,那么需要保存 162X162=26244 个值,然而使用自定义的数据模型 CurrencyModel ,我们只需要保存 162 个数值即可(每种汇率相对美元的值)。
类 CurrencyModel 当与 QTableView 类搭配使用,模型中的数据由 QMap<QString, double> 这个数据结构保存;Map 中的每个 key 值是一种汇率的代号,而每个 value 则是用美元表达的汇率值。下面的代码片段说明 Map 如何组织数据,以及模型的使用方法。
首先在 main 函数中,创建一个 QMap<QString, double> 作为数据源。
QMap<QString, double> currencyMap;
currencyMap.insert("AUD", 1.3259);
currencyMap.insert("CHF", 1.2970);
...
currencyMap.insert("SGD", 1.6901);
currencyMap.insert("USD", 1.0000);
CurrencyModel currencyModel;
currencyModel.setCurrencyMap(currencyMap);
QTableView tableView;
tableView.setModel(¤cyModel);
tableView.setAlternatingRowColors(true);
现在看一下数据模型的实现,首先是它的头文件:
class CurrencyModel : public QAbstractTableModel
{
public:
CurrencyModel(QObject *parent = 0);
void setCurrencyMap(const QMap<QString, double> &map);
int rowCount(const QModelIndex &parent) const;
int columnCount(const QModelIndex &parent) const;
QVariant data(const QModelIndex &index, int role) const;
QVariant headerData(int section, Qt::Orientation orientation, int role) const;
private:
QString currencyAt(int offset) const;
QMap<QString, double> currencyMap;
};
由于 QAbstractTableModel 最接近我们的数据源,所以选择由这个类的派生类作为数据模型。Qt 提供了许多的模型基类,包括:QAbstractListModel,QAbstractTableModel 和 QAbstractItemModel 等等。在处理一维,二维数据集合时,前两种数据模型可以提供方便,下面是数据模型的类继承图:
作为只读型的表格数据模型,必须重载三个函数:rowCount( ),columnCount( ) 和 data( ) 。在这个例子中,我们也重载了 headerData( ) 函数,并提供了一个函数用于初始化数据:setCurrencyMap( ) 。这些函数重载好后,将数据模型给入 view ,则 view 会自动地加载模型中的数据并将数据显示出来。
CurrencyModel::CurrencyModel(QObject *parent)
: QAbstractTableModel(parent)
{
}
构造函数中无需多事,仅仅传递一个 parent 指针给父类。
int CurrencyModel::rowCount(const QModelIndex & /* parent */) const
{
return currencyMap.count();
}
int CurrencyModel::columnCount(const QModelIndex & /* parent */) const
{
return currencyMap.count();
}
表格的行数或者列数,就是 map 中的汇率数目。
QVariant CurrencyModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (role == Qt::TextAlignmentRole) {
return int(Qt::AlignRight | Qt::AlignVCenter);
} else if (role == Qt::DisplayRole) {
QString rowCurrency = currencyAt(index.row());
QString columnCurrency = currencyAt(index.column());
if (currencyMap.value(rowCurrency) == 0.0)
return "####";
double amount = currencyMap.value(columnCurrency)
/ currencyMap.value(rowCurrency);
return QString("%1").arg(amount, 0, 'f', 4);
}
return QVariant();
}
data( ) 函数返回某个数据单元的众多属性中的某一个属性的值。数据单元由 QModelIndex 索引。对于表格数据模型,QModelIndex 的有用的部分是数据单元的行号与列号,这些值可以由 row( ) 和 column( ) 取得。特别这里的 arg 用法是:
QString QString::arg ( double a, int fieldWidth = 0, char format = 'g', int precision = -1, const QChar & fillChar = QLatin1Char( ' ' ) ) const
首先,a 是要显示的数字;fieldWidth 指明了a 填入的最小空间,同时空余空间将由 fillChar 填充;format 是数字的显示样式,如“E” 是以科学计数法显示,“f” 是以实数方式显式等等;precision 则是指保留小数点后数字的位数。
如果是 Qt::TextAlignmentRole,将返回一个适合数据单元的布局(Alignment);如果属性是 Qt::DisplayRole,那么查找每个汇率的值并计算其转换率。
我们也可以将返回值类型写成 double,但是后面我们就没有办法决定数字需要显示的小数点后数字的位数(除非使用自定义的 delegate)。所以,这里我们返回一个字符串,以便控制其显示。
QVariant CurrencyModel::headerData(int section,
Qt::Orientation /* orientation */,
int role) const
{
if (role != Qt::DisplayRole)
return QVariant();
return currencyAt(section);
}
headerData( ) 这个函数由 view 调用,以为它的水平和垂直表头赋值。这里的 section 参数是列号或者行号(取决于方向)。因为行和列有相同的汇率代号,所以这里不考虑方向(Orientation),而直接返回给定 section 号对应的汇率代号。
void CurrencyModel::setCurrencyMap(const QMap<QString, double> &map)
{
currencyMap = map;
reset();
}
调用者可以通过 setCurrencyMap( ) 来改变汇率 map 的对象,QAbstractItemModel::reset( ) 函数将通知所有正在使用模型的 views,模型中的数据单元已失效,views 必须更新显示出来的数据单元。
QString CurrencyModel::currencyAt(int offset) const { return (currencyMap.begin() + offset).key(); }
currencyAt( ) 函数返回汇率 map 中给定偏移量位置处的 key 值,也就是汇率代号。这里使用了 STL 型的遍历器遍历 map 查找数据单元,并调用这个数据单元的 key( ) 。其中,QMap::begin( ) 返回一个指向 map 中第一个数据单元的 STL 型的遍历器。
自定义委托
- createEditor(): 返回修改数据的组件;
- setEditorData(): 为editor提供编辑的原始数据;
- updateEditorGeometry(): 保证editor显示在 item view 的合适位置以及大小;
- setModelData(): 根据editor 的数据更新model的数据。