【问题标题】:Storing persistent information about items in view存储有关视图中项目的持久信息
【发布时间】:2015-02-19 16:20:10
【问题描述】:

我有自己的观点,直接基于QAbstractItemView。 一般来说,我需要存储一些关于特定模型项目的信息。

所以在我看来,我有一张从QModelIndex 到描述特定项目的结构的映射。 然后我主要在视图的paintEvent 上使用这些数据。

问题是,QModelIndex 不是持久的,它可能会过时。 因此,当从模型中插入或删除行时,一些 QModelIndex 可能会变得无效,我不应该对它们进行中继。

那么我如何在模型中的项目和我在视图中使用的一些装饰数据之间建立关系?

QPersistentModelIndex 似乎是处理此类事情的合适工具,但我知道它的性能(我的模型和视图可能很大)。

QPersistentModelIndex 的另一个问题是它可能不应该用作地图的键(在我的情况下),因为它可能(并且将会)改变并使地图不一致。

我查看了 Qt 的 QTreeView 和 QListView 实现,看看它们如何处理行删除/插入,但似乎它们只是删除了所有数据。

所以在这一点上,我看不到任何简单的方法来解决我的问题。

【问题讨论】:

  • 我几乎可以肯定您不想实现自己的视图。
  • 你为什么这么认为?
  • @lpapp:你能解释一下吗?
  • 如果你真的想创建自己的视图,也许你不应该使用 QAbstractItemModel,而只是创建更适合你需要的东西。 QAbstractItemModel 旨在同时用于所有 Qt 视图,即使为此目的,它似乎也非常复杂。

标签: c++ qt view model qt5


【解决方案1】:

您可以安全地使用QPersistentModelIndex 作为map 或 哈希键。 即使底层 QModelIndex 发生变化,“持久”部分也会确保所有 QPersistentModelIndex 在保持其身份的同时保持最新,即 operator ==qHash() 返回一致的值。

话虽如此,您不应该在视图中存储有关索引的数据。数据应该由模型存储。 这似乎是它在 Qt 类中的完成方式:视图对QAbstractItemModel::data() 进行了大量调用。

我认为值得存储在视图中的唯一数据是“缓存数据”,即以下值:

  • 模型不直接提供
  • 需要根据模型数据进行大量计算
  • 特定于视图

如果不满足这三个条件中的任何一个,我个人的偏好是将数据存储在模型中。


编辑

与我原来的答案相反,您不能安全地将 QPersistentModelIndex 用作 QMap 键。

原因是QMap使用operator <插入和查找数据。

这里的问题是bool QPersistentModelIndex::operator<(const QPersistentModelIndex &other) const 只是bool QModelIndex::operator<(const QModelIndex &other) const 的代理,它按行和列比较索引。 这意味着QPersistentModelIndex 的排序顺序不是持久的,并且会在索引移入模型时发生变化。

但是,QMap 不知道排序顺序已更改。在这一点上,在 QMap<QPersistentModelIndex, T> 中搜索一个值就像在一个未排序的数组上进行二分搜索一样被破坏。

请注意,此问题不仅限于QMap,它会影响所有基于键顺序的容器,例如std::map。但是,不依赖排序的容器(QHashstd::unordered_map、...)不受影响。

【讨论】:

  • 我匹配第三种情况。我仍然担心的是这种解决方案的性能。但是,我会尝试一下并执行分析。我认为 qt 模型有一个唯一的项目 ID 会很棒。什么会非常有效。我的第一个猜测是QModelIndex::internalId() 是解决方案,但测试证明它不是(至少对于某些标准 Qt 模型不是)
  • @PatrickParker QPersistentModelIndex 在模型更新时更新。如果之前插入了一行,那么QPersistentModelIndex::row()返回的值会加一。如果之前删除了一行,它将减少一。如果QPersistentModelIndex引用的“单元格”被删除,QPersistentModelIndex::isValid()将返回false。
  • @PatrickParker QPersistentModelIndex 使用QPersistentModelIndexData 实现。引用相同“单元格”的QPersistentModelIndex 的2 个实例将共享QPersistentModelIndexData 的相同实例。 2 QPersistentModelIndex 相等当且仅当它们在内部使用相同的 QPersistentModelIndexData *QPersistentModelIndex 的 qHash 只是其内部 QPersistentModelIndexData * 的 qHash。
  • @PatrickParker 我刚刚编辑了我的答案。我花了一些时间研究 Qt 代码并运行测试,QPersistentModelIndex 不能用作QMap 键。它们的排序基于模型更改时将更改的行/列,默默地打破了QMap 内部排序。
  • @BenjaminT 感谢您的确认。因此,如果我们添加不调用或不实现moveRows 的规定(意味着行号仅因删除或插入而更改),那么它可能是安全的吗?我想我们需要进一步保证在rowsAboutToBeRemoved期间从QMap中删除索引,因为删除的行可能会将我们的QPersistentModelIndex键跳转到逻辑行号-1——也违反了假定的排序顺序。
【解决方案2】:

与其尝试将某些数据映射到某些模型项,不如让每个项都存储其数据。这涉及使用委托进行绘画,而不是依赖QAbstractItemView 绘画事件。

让我们有一个简单的有状态类来表示我们的项目数据,并使用Q_DECLARE_METATYPE 宏使其成为 Qt 元对象系统中的新公民。

#include <QMetaType>
class ItemData
{
public:
    ItemData() = default;
    ItemData(int d) : _data(d){}
    int data() const { return _data; }
    void paint(QPainter *painter, QRect rect);
private:
    int _data;
};
Q_DECLARE_METATYPE(ItemData)

现在是委托,确实是一个非常简单的委托:

#include <QStyledItemDelegate>
class ItemDelegate : public QStyledItemDelegate
{
public:
    // ...
    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
    // ...
};

委托绘制方法,是事情发生的地方:

void ItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    if(index.data().canConvert<ItemData>())
    {
        ItemData itemdata = qvariant_cast<ItemData>(index.data());
        itemdata.paint(painter, option.rect);
    }
    else
    {
        QStyledItemDelegate::paint(painter, option, index);
    }
}

在这里我们可以使用模型索引来表达它的含义:检索项目数据。不过,我们的项目可以自己绘制。让它根据它的内部状态画一些圆圈:

void ItemData::paint(QPainter *painter, QRect rect)
{
    QRect r(rect.left() + 2, rect.top() + 2, rect.height() - 4, rect.height() - 4);
    for(int i=0; i<_data; ++i)
    {
        painter->drawEllipse(r);
        r.moveLeft(r.left() + rect.height() + 2);
    }
}

在更灵活的设计中,数据与渲染分离,因此ItemData 类没有paint 方法,并且由委托自己执行绘制:

void ItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    if(index.data().canConvert<ItemData>())
    {
        ItemData itemdata = qvariant_cast<ItemData>(index.data());
        //itemdata.paint(painter, option.rect);
        QRect rect = option.rect;
        QRect r(rect.left() + 2, rect.top() + 2, rect.height() - 4, rect.height() - 4);
        for(int i=0; i<itemdata.data(); ++i)
        {
            painter->drawEllipse(r);
            r.moveLeft(r.left() + rect.height() + 2);
        }
    }
    else
    {
        QStyledItemDelegate::paint(painter, option, index);
    }
}

这样,我们可以为不同的视图实现并选择不同的代理,同时保持一个一致的模型并在它们之间共享。

在像QListWidget 这样的基于项目的小部件中使用委托和数据非常简单。 在表单构造函数中:

ui->listWidget->setItemDelegate(new ItemDelegate());
for(int i=0; i<10; ++i)
{
    QListWidgetItem * item = new QListWidgetItem();
    item->setData(0, QVariant::fromValue(ItemData(i + 1)));
    ui->listWidget->addItem(item);
}

不过,在基于模型的模型中并没有太大的不同:不是将数据设置为项目,而是将数据设置为模型,再次使用QVariant::fromValue

【讨论】:

  • 您的解决方案很有趣,但它需要我“覆盖” DisplayRole(或模型中的任何其他角色)。我要存储的数据是特定于视图的,我不想破坏模型提供的数据(可以在另一个视图中使用)。我希望将它们存储在自定义角色中,但我怎么能 100% 不覆盖特定于模型的数据(因为某些模型使用自定义角色 - 例如 QFileSystemModel)。对不起,我很挑剔;)
  • @Michal 创建 QIdentityProxyModel 的子类并在那里覆盖 userdata 角色。您的视图可以是此代理的唯一用户和消费者。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-11-08
  • 1970-01-01
  • 1970-01-01
  • 2016-08-25
  • 1970-01-01
  • 2018-07-29
  • 1970-01-01
相关资源
最近更新 更多