首页 Qt 学习之路 2 Qt 学习之路 2(47):视图选择

Qt 学习之路 2(47):视图选择

35 3.5K

选择是视图中常用的一个操作。在列表、树或者表格中,通过鼠标点击可以选中某一项,被选中项会变成高亮或者反色。在 Qt 中,选择也是使用了一种模型。在 model/view 架构中,这种选择模型提供了一种更通用的对选择操作的描述。对于一般应用而言,Qt 内置的选择模型已经足够,但是,Qt 还是允许你创建自己的选择模型,来实现一些特殊的操作。

Qt 使用QItemSelectionModel类获取视图中项目的选择情况。这个模型保持有项目的索引,并且独立于任何视图。这意味着,我们可以让不同的视图共享同一个选择模型,从来达到一种同步操作的目的。选择由选择区域组成。模型只将选区的开始和结束的索引位置记录下来,以保证对于很大的选区也有很好的性能。非连续选区则由多个连续选择组成。

选择会直接应用于选择模型所维护的那些被选中的索引上面。最新的选择就是当前选择。这意味着,即便界面上没有显示有任何项目被选择,如果通过某些命令对选区进行操作,同样会有作用。

在视图中,始终存在一个当前项和被选择项(即便从界面上看不到有任何选择)。与通常所想的不同,当前项和选择项是相互独立的两个状态。一个项目可以即是当前项又是选择项。下表是当前项和选择项的区别:

当前项选择项
只能有一个当前项。可以有多个选择项。
使用键盘或者鼠标点击可以改变当前项。选择项使用两种状态:选择和未选择,这取决于项目之前的状态和其它一些设置,例如,单选或多选。只有在用户进行交互的时候,这种状态才会发生改变。
当前项可以使用 F2 或者鼠标双击进行编辑(前提是程序允许)。当前项可以结合另外一个锚点指定被选择或者去除选择的一块选区(或二者的结合)。
当前项通常会有一个焦点框进行标识。选择项使用选区颜色进行标识。

在处理选择的时候,我们可以将QItemSelectionModel当成数据模型中所有数据项的选择状态的一个记录。一旦选择模型创建好,这些数据项就可以在不知道哪些项被选择的情况下进行选择、取消选择或者改变选择状态的操作。所有被选择项的索引都在可随时更改,其它组件也可以通过信号槽机制修改这些选择的信息。

标准视图类(QListViewQTreeView以及QTableView)已经提供了默认的选择模型,足以满足大多数应用程序的需求。某一个视图的选择模型可以通过selectionModel()函数获取,然后使用setSelectionModel()提供给其它视图共享,因此,一般没有必要新建选择模型。

如果需要创建一个选区,我们需要指定一个模型以及一对索引,使用这些数据创建一个QItemSelection对象。这两个索引应该指向给定的模型中的数据,并且作为一个块状选区的左上角和右下角的索引。为了将选区应用到模型上,需要将选区提交到选择模型。这种操作有多种实现,对于现有选择模型有着不同的影响。

下面我们来看一些代码片段。首选构建一个总数 32 个数据项的表格模型,然后将其设置为一个表格视图的数据:

QTableWidget tableWidget(8, 4);

QItemSelectionModel *selectionModel = tableWidget.selectionModel();

在代码的最后,我们获得QTableView的选择模型,以备以后使用。现在,我们没有修改模型中的数据,而是选择表格左上角的一些单元格。下面我们来看看代码如何实现:

QModelIndex topLeft = tableWidget.model()->index(0, 0, QModelIndex());
QModelIndex bottomRight = tableWidget.model()->index(5, 2, QModelIndex());

接下来,我们将获得的两个索引定义为选区。为达这一目的,我们首先构造一个QItemSelection对象,然后将其赋值给我们获取的选择模型:

QItemSelection selection(topLeft, bottomRight);
selectionModel->select(selection, QItemSelectionModel::Select);

正如前面我们说的,首先利用左上角和右下角的坐标构建一个QItemSelection对象,然后将这个对象设置为选择模型的选择区。select()函数的第一个参数就是需要选择的选区,第二个参数是选区的标志位。Qt 提供了很多不同的操作,可以参考下QItemSelectionModel::SelectionFlags的文档。在本例中,我们使用了QItemSelectionModel::Select,这意味着选区中所包含的所有单元格都会被选择。

下面就是我们的运行结果:

视图选区示例

现在我们知道如何设置选区。下面来看看如何获取选区。获取选区需要使用selectedIndexes()函数。该函数返回一个无序列表。我们可以通过遍历这个列表获得哪些被选择:

QModelIndexList indexes = selectionModel->selectedIndexes();
QModelIndex index;

foreach(index, indexes) {
    QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
    model->setData(index, text);
}

在选择发生更改时,选择模型会发出信号。我们可以连接selectionChanged()信号,在选区改变时检查哪个项目发生了变化。这个信号有两个参数:第一个是新选择的项目,第二个是刚刚被取消选择的项目。在下面的示例中,我们通过selectionChanged()信号,将所有新选择的项目填充字符串,将所有被取消选择的部分清空:

void MainWindow::updateSelection(const QItemSelection &selected,
                                 const QItemSelection &deselected)
{
    QModelIndex index;
    QModelIndexList items = selected.indexes();

    foreach (index, items) {
        QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
        model->setData(index, text);
    }

    items = deselected.indexes();

    foreach (index, items) {
        model->setData(index, "");
    }
 }

通过currentChanged(),我们可以追踪当前有焦点的项。同selectionChanged()信号类似,这个信号也有两个参数:第一个是新的当前项,第二个是上一个当前项。下面的代码则是该信号的使用:

void MainWindow::changeCurrent(const QModelIndex &current,
                               const QModelIndex &previous)
{
    statusBar()->showMessage(
        tr("Moved from (%1,%2) to (%3,%4)")
            .arg(previous.row()).arg(previous.column())
            .arg(current.row()).arg(current.column()));
}

这些信号可以用来监控选区的改变。如果你还要直接更新选区,我们还有另外的方法。

同样是利用前面所说的QItemSelectionModel::SelectionFlag,我们可以对选区进行组合操作。还记得我们在前面的select()函数中使用过的第二个参数吗?当我们替换这个参数,就可以获得不同的组合方式。最常用的就是QItemSelectionModel::Select,它的作用是将所有指定的选区都选择上。QItemSelectionModel::Toggle则是一种取反的操作:如果指定的部分原来已经被选择,则取消选择,否则则选择上。QItemSelectionModel::Deselect则是取消指定的已选择的部分。在下面的例子中,我们使用QItemSelectionModel::Toggle对前面的示例作进一步的操作:

QItemSelection toggleSelection;

topLeft = tableWidget.model()->index(2, 1, QModelIndex());
bottomRight = tableWidget.model()->index(7, 3, QModelIndex());
toggleSelection.select(topLeft, bottomRight);

selectionModel->select(toggleSelection, QItemSelectionModel::Toggle);

运行结果将如下所示:

视图选区示例 Toggle

默认情况下,选择操作会只会影响到指定的模型索引。但是,我们也可以改变这一设置。例如,只选择整行或者整列:

QItemSelection columnSelection;

topLeft = model->index(0, 1, QModelIndex());
bottomRight = model->index(0, 2, QModelIndex());

columnSelection.select(topLeft, bottomRight);

selectionModel->select(columnSelection,
                       QItemSelectionModel::Select | QItemSelectionModel::Columns);

QItemSelection rowSelection;

topLeft = model->index(0, 0, QModelIndex());
bottomRight = model->index(1, 0, QModelIndex());

rowSelection.select(topLeft, bottomRight);

selectionModel->select(rowSelection,
                       QItemSelectionModel::Select | QItemSelectionModel::Rows);

上面的代码,我们依然使用两个索引设置了一个区域,但是,在选择的使用我们使用了QItemSelectionModel::RowsQItemSelectionModel::Columns这两个参数,因此只会选择这两个区域中指定的行或者列:

视图选区示例 Rows Columns

使用QItemSelectionModel::Current参数可以将当前选区替换为新的选区;使用QItemSelectionModel::Clear则会将原来已有的选区全部取消。为了进行全选,我们可以设置选区为左上角和右下角两个索引:

QModelIndex topLeft = model->index(0, 0, parent);
QModelIndex bottomRight = model->index(model->rowCount(parent)-1,
model->columnCount(parent)-1, parent);

QItemSelection selection(topLeft, bottomRight);
selectionModel->select(selection, QItemSelectionModel::Select);

35 评论

hehuim 2013年3月30日 - 23:21

😀 博主的文章写的不错,浅显易懂,对初学者非常不错!谢谢哈!也希望大哥继续加油写!呵呵!

回复
豆子 2013年4月1日 - 09:03

谢谢

回复
andmoe 2013年4月1日 - 10:56

😛 啥时开始数据库讲座啊,期待!

回复
豆子 2013年4月2日 - 13:14

以后会的啦

回复
疯子 2013年9月9日 - 12:08

在构造函数里,QTableWidget tableWidget(8, 4);然后setCentralWidget(&tableWidget);显示的是一片空白,最后在堆上创建tableWidget才行,在栈里创建的widget是不是需要指定parent?如何指定呢?

回复
豆子 2013年9月9日 - 14:39

这是基础 C++ 的内容。如果在栈上面创建,构造函数结束时,对象已经超出定义域,将会被析构,所以显示不出来。即便是指定 parent 应该也不会有效果,因为存在 parent 并不会阻止对象析构。所以只能在堆上面创建。

回复
疯子 2013年9月9日 - 15:40

谢谢豆子。

回复
小张 2016年11月28日 - 11:14

文章上面是在栈上面创建的呢。

回复
疯子 2013年9月9日 - 16:26

豆子,你这篇文章中的代码是不是有部分没有实现其功能,就是选择区域后,单元格显示其行列坐标,你的截图也没有,我使用QStringListModel来显示单元格数据,setdata函数返回是false.

回复
豆子 2013年9月10日 - 15:21

应该是不全的,不过你在哪里看到单元格需要显示行列坐标?

回复
疯子 2013年9月11日 - 15:48

foreach (index, items) {
QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
model->setData(index, text);
}
这段函数不是吗?还是我理解错了...

回复
豆子 2013年9月12日 - 15:35

确实是这样,我想你可以试试把 setData(index, text) 改成 setData(index, text, Qt::DisplayRole);因为默认是 EditRole,只有在编辑时才会显示

回复
wxl91sl 2013年9月27日 - 14:45

老大!现在我遇到表格方面的问题,想请教一下,现在再做一个项目,用到qgraphics框架,想做个继承qgraphicsitem 的表格在scene中显示,但是不知道从何入手!希望能够指点一二

回复
豆子 2013年9月28日 - 15:10

如果没有理解错误的话,你是想在 QGraphicsScene 中显示 QTableView(或类似的其它组件),那么你应该使用 QGraphicsWidget,允许直接将 QWidget 类添加到 QGprahicsScene。你可以看看这个类的文档。

回复
wxl91sl 2013年9月28日 - 18:07

唉唉是的啊!要是像你说的那么简单我就不问了,因为牵扯到保存问题,我在QGraphicsScene上所有的东西都要继承QGraphicsitem,要是你说的那么简单我就不会在这篇文档下给你留言了,老大你有什么好的想法么?

回复
豆子 2013年9月28日 - 18:09

不清楚你说的什么保存问题?QGraphicsWidget 不能保存吗?

回复
wxl91sl 2013年9月29日 - 08:59

这么说吧!就是想在做个继承QGraphicsitem的表格!这个容易实现么?

豆子 2013年9月29日 - 09:13

如果要做一个通用组件恐怕不太容易,不过如果是特定的,虽然不容易,也不算很难。你可以考虑自己重写 paintEvent() 绘制表格线,并且显示其中的数据。如果还要求有滚动条,也需要自己绘制了

wxl91sl 2013年9月29日 - 09:03

因为做的一个项目!只想要一个widget,所有的东西都想在 这个widget的QGraphicsScene 上呈现!

wxl91sl 2013年9月29日 - 09:15

那里面的数据!要重写哪个函数呢?么么哒

豆子 2013年9月30日 - 08:57

数据的话你可以作为 QGraphicsItem 子类的一个成员变量,然后在 paintEvent() 里面用 drawText() 之类的函数绘制出来。

Const_Lin 2014年3月20日 - 09:15

豆子你好:
最后一段代码中,parent是什么?

回复
DS离心泵 2015年12月8日 - 17:29

就是你的底窗口

回复
DS离心泵 2015年12月8日 - 17:30

model->setData(index, text);
这句应该是tableWidget.model()->setData(index, text);

回复
豆子 2015年12月8日 - 17:37

因为是不同的项目,所以这里将 model 作为成员变量,因此没有使用 tableWidget.model()

回复
DS离心泵 2015年12月8日 - 17:55

明白了~谢谢~

回复
豆纸 2016年3月24日 - 13:49

为什么 showmessage 那一段都没显示过?

回复
豆纸 2016年3月24日 - 13:50

设置了 延时也不显示 不明白 求解

回复
豆纸 2016年3月24日 - 14:12

认真的 看了一下文章 已经解决了 谢谢

回复
wasai 2016年8月11日 - 18:02

请问下,怎样知道tableView中某个item在tableView这个坐标系的坐标呢?

回复
NICO 2016年10月23日 - 22:14

void MainWindow::updateSelection(const QItemSelection &selected,
const QItemSelection &deselected)
{
QModelIndex index;
QModelIndexList items = selected.indexes();

foreach (index, items) {
QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
model->setData(index, text);
}

items = deselected.indexes();

foreach (index, items) {
model->setData(index, "");
}
MainWindow里面的函数怎么有MODEL ,你是在MIANWINDOW.H里面定义的吗?

回复
豆子 2016年10月24日 - 21:33

model 是在 mainwindow.h 定义的一个模型

回复
小张 2016年11月28日 - 12:56

model应该是QAbstractItemModel吧,然而以下运行出错了
QItemSelection columnSelection;

topLeft = model->index(0, 1, QModelIndex());
bottomRight = model->index(0, 2, QModelIndex());
。。。
【Error - RtlWerpReportException failed with status code :-1073741823. Will try to launch the process directly】
把model改成(*tableWidget).model()就没问题了,什么问题

回复
小张 2016年11月28日 - 13:10

哦,知道了,未初始化

回复
小迪 2019年12月5日 - 23:24

大神您好,初学者的我问一下,用connect连接信号selectionChanged()和槽函数MainWindow::updateSelection(),但是不清楚他们函数的参数需要传递什么,能麻烦大神解答下么

回复

回复 Const_Lin 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2