QIdentityProxyModel在与QCompleter一起使用时需要重写rowCount吗?

huangapple go评论66阅读模式
英文:

QIdentityProxyModel requires overriding rowCount when used with QCompleter?

问题

这是您的翻译代码部分:

我有一个`QSqlQueryModel`,用于获取像`1`、`2`等这样的ID在用户界面中我们将这个字段显示为4位整数:`0001`、`0002`

这是我`QIdentityProxyModel`的代理子类用于添加零前缀

``` py
class ZeroPrefixProxy(QIdentityProxyModel):

    def __init__(self, parent=None):
        super().__init__(parent)

    def data(self, index, role):
        d = self.sourceModel().data(index, role)
        return f'{d:04}' if role in (Qt.DisplayRole, Qt.EditRole) else d

这是我如何将它设置为QLineEdit的自动完成器:

def setIdCompleterModel(self):
    # model是加载的QSqlQueryModel
    proxy = ZeroPrefixProxy(self.ui.txtId)
    proxy.setSourceModel(model)
    self.ui.txtId.setCompleter(QCompleter(proxy, self.ui.txtId))

无论我输入什么(10001),都不会显示建议。然而,当我取消注释上面的任一片段时,事情就会运行得很好。

我不想做任何一种方法,因为它们似乎是没有意义的:

  • QIdentityProxyModel已经实现了columCount(它工作正常)
  • 我没有调用data的理由(我最初只是为了测试)

我漏掉了什么?为什么简单的子类实现不起作用?

设置:ArchLinux、Qt 5.15.10、PySide2 5.15.2.1

MCVE

如果我注释掉ZeroPrefixProxy.data方法,这段代码在我的设置上可以正常工作:

import sys

from PySide2.QtCore import Qt, QIdentityProxyModel, QModelIndex
from PySide2.QtWidgets import QApplication, QMainWindow, QLineEdit, QCompleter
from PySide2.QtGui import QStandardItem, QStandardItemModel

class ZeroPrefixProxy(QIdentityProxyModel):

    def __init__(self, parent=None):
        super().__init__(parent)

    # 注释掉这个方法会使事情正常工作
    def data(self, index, role=Qt.DisplayRole):
        return self.sourceModel().data(index, role)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        useStdModel = False
        if useStdModel:
            model = QStandardItemModel(5, 1, self)
            for i in range(1, 6):
                item = QStandardItem()
                # setData as ctor only takes an str, we need an int
                item.setData(i, Qt.EditRole)
                model.setItem(i-1, 0, item)
        else:
            db = QSqlDatabase.addDatabase('QPSQL')
            host = 'localhost'
            dbname = 'test'
            db.setHostName(host)
            db.setDatabaseName(dbname)
            db.setUserName('pysider')
            if not db.open():
                print('DB not open')
            model = QSqlQueryModel()
            model.setQuery('SELECT id FROM items')

        proxy = ZeroPrefixProxy(self)
        proxy.setSourceModel(model)
        lineEdit = QLineEdit()
        comp = QCompleter(proxy, lineEdit)
        lineEdit.setCompleter(comp)
        self.setCentralWidget(lineEdit)

app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

<details>
<summary>英文:</summary>

I&#39;ve a `QSqlQueryModel` which fetches IDs like `1`, `2,`, etc. In the UI we display this field as a 4-digit int: `0001`, `0002`, etc.

Here&#39;s my proxy subclass of `QIdentityProxyModel` to add the zero prefix:

``` py
class ZeroPrefixProxy(QIdentityProxyModel):

    def __init__(self, parent=None):
        super().__init__(parent)

    def data(self, index, role):
        d = self.sourceModel().data(index, role)
        return f&#39;{d:04}&#39; if role in (Qt.DisplayRole, Qt.EditRole) else d

# Uncommenting this works
#    def rowCount(self, parent=QModelIndex()):
#        return self.sourceModel().rowCount(parent)

Here's how I set it to a QLineEdits completer:

def setIdCompleterModel(self):
    # model is a loaded QSqlQueryModel
    proxy = ZeroPrefixProxy(self.ui.txtId)
    proxy.setSourceModel(model)
    self.ui.txtId.setCompleter(QCompleter(proxy, self.ui.txtId))
    # Uncommenting this works
    # proxy.data(proxy.index(0, 0), Qt.DisplayRole)

No suggestions are displayed irrespective of what I type (1 or 0001). However, when I uncomment either snippets above things work great.

I do not want to do either as they seem pointless:

  • QIdentityProxyModel already implements columCount (it works correctly)
  • I've no reason to call data (I originally wrote it just to test)

What am I missing? Why is the simple subclass implementation not working?

Setup: ArchLinux, Qt 5.15.10, PySide2 5.15.2.1

MCVE

This code works on my setup only if I comment out ZeroPrefixProxy.data:

import sys

from PySide2.QtCore import Qt, QIdentityProxyModel, QModelIndex
from PySide2.QtWidgets import QApplication, QMainWindow, QLineEdit, QCompleter
from PySide2.QtGui import QStandardItem, QStandardItemModel

class ZeroPrefixProxy(QIdentityProxyModel):

    def __init__(self, parent=None):
        super().__init__(parent)

    # Commenting this method out makes things work
    def data(self, index, role=Qt.DisplayRole):
        return self.sourceModel().data(index, role)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        useStdModel = False
        if useStdModel:
            model = QStandardItemModel(5, 1, self)
            for i in range(1, 6):
                item = QStandardItem()
                # setData as ctor only takes an str, we need an int
                item.setData(i, Qt.EditRole)
                model.setItem(i-1, 0, item)
        else:
            db = QSqlDatabase.addDatabase(&#39;QPSQL&#39;)
            host = &#39;localhost&#39;
            dbname = &#39;test&#39;
            db.setHostName(host)
            db.setDatabaseName(dbname)
            db.setUserName(&#39;pysider&#39;)
            if not db.open():
                print(&#39;DB not open&#39;)
            model = QSqlQueryModel()
            model.setQuery(&#39;SELECT id FROM items&#39;)

        proxy = ZeroPrefixProxy(self)
        proxy.setSourceModel(model)
        lineEdit = QLineEdit()
        comp = QCompleter(proxy, lineEdit)
        lineEdit.setCompleter(comp)
        self.setCentralWidget(lineEdit)

app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

答案1

得分: 1

基本的一维和二维模型通常仅依赖于给定的 QModelIndex 的行和列。

一个适当、准确和安全可靠的模型应该在理论上确保 QModelIndex 的 model() 实际上是相同的,否则返回一个无效的结果(在 Python 中是 None)。

您的示例适用于 QSqlQueryModel,因为 SQL 模型在本质上是二维的,所以假设给定的索引实际上属于相同的模型,然后它将尝试基于那些行/列坐标返回模型数据。

从理论上讲,这是一个 bug,但如果我们还考虑到 SQL 模型通常包含数千条记录,那么这可能是出于优化原因而做的:在我看来,这是一个明显的情况,其中"请求原谅,而不是许可"比相反更好。

如果您使用了一个使用列表的列表作为数据模型和基本的行/列组合来获取其值的基本 QAbstractTableModel,那么您的示例也将起作用:

class MyModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, parent=None):
        return len(self._data)

    def columnCount(self, parent=None):
        if self._data:
            return len(self._data[0])
        return 0

    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            return self._data[index.row()][index.column()]

由于 QIdentityProxyModel 不改变原始模型的布局,行/列组合将始终与返回的数据一致,即使 QModelIndex 的模型实际上并不相同。

如果您使用了带有激活过滤的 QSortFilterProxyModel,那么可能会返回意外的结果。

一个适当和可扩展的模型应该始终确保 QModelIndex 的模型实际上是相同的:QStandardItemModel 实际上强制执行这一点,因为它使用内部指针来引用其结构中的任何项目,这是必要的,因为它还允许树结构的可能性,它期望有一个父项目(索引)以便正确映射行和列组合。它不仅基于行/列组合检索数据,还基于父项和实际的模型索引所有权。

这就是代理模型的 mapToSource() 的整个目的:它返回一个(可能有效)索引,实际上映射到具有属于该模型的正确索引的源模型中。

因此,在代理模型中,data()(或任何与索引相关的函数,如flags())的正确实现需要调用该函数,不管对源模型的实现做出了什么样的假设:

class ZeroPrefixProxy(QIdentityProxyModel):

    def __init__(self, parent=None):
        super().__init__(parent)

    # 将此方法注释掉会使事情正常工作
    def data(self, index, role=Qt.DisplayRole):
        return self.sourceModel().data(self.mapToSource(index), role)
英文:

Elementary mono and bi-dimensional models often rely just on the row and column of the given QModelIndex.

A proper, accurate and safety reliable model should theoretically ensure that the QModelIndex's model() is actually the same, otherwise return an invalid result (None in Python).

Your example works for QSqlQueryModel because SQL models are 2D by their nature, so the assumption is that the given index actually belongs to the same model and then it will try to return the model data based on those row/column coordinates.

This is theoretically a bug, but if we also consider that SQL models often contain thousands of records, it has probably been done for optimization reasons: in my opinion, this is a clear case in which "ask forgiveness, not permission" is better than the opposite.

Your example would have also worked if you used a basic QAbstractTableModel that uses a list of lists as data model and a basic row/column combination to get its values:

class MyModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, parent=None):
        return len(self._data)

    def columnCount(self, parent=None):
        if self._data:
            return len(self._data[0])
        return 0

    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            return self._data[index.row()][index.column()]

Since the QIdentityProxyModel leaves the original model layout unchanged, the row/column combination will always be consistent with the returned data, even if the model of the QModelIndex is not really the same.

If you had used a QSortFilterProxyModel with filtering active instead, that would have probably returned unexpected results.

A proper and extensible model should always ensure that the model of the QModelIndex actually is the same: QStandardItemModel actually enforces that because it uses internal pointers to reference any item within its structure, and that's necessary since it also allows the possibility of tree structures, which expect a parent item (index) in order to properly map a row and column combination. It retrieves the data not just based on the row/column combination, but based on the parent and the actual model-index ownership.

That's the whole purpose of mapToSource() of proxy models: it returns a (possibly valid) index that actually maps to the source model with the correct index belonging to that model only.

So, the correct implementation of data() (or any index-related function, such as flags()) in a proxy model requires a call to that function, no matter any assumption made on the source model implementation:

class ZeroPrefixProxy(QIdentityProxyModel):

    def __init__(self, parent=None):
        super().__init__(parent)

    # Commenting this method out makes things work
    def data(self, index, role=Qt.DisplayRole):
        return self.sourceModel().data(self.mapToSource(index), role)

答案2

得分: 1

在您的MCVE中,QSqlQueryModel + 代理的情况不起作用,因为源模型在被代理模型使用之前被销毁。

Qt是一个C++框架,因此必须小心管理对象的生命周期。根据对象树和所有权 - Qt文档,所有Qt对象都派生自QObjects1,它们存在于一个树中。任何对象都可以使用QObject.setParent附加到另一个对象作为子对象。只要父对象存在,子对象、孙子对象和后代对象就会保持活动状态。

那根/父对象呢?当一个对象没有父对象时,它由其绑定的变量保持活动状态。只要变量在范围内,对象就会保持活动状态。在您的MVCE中,持有MainWindow对象的window变量就是一个很好的例子;它在整个程序的生命周期内都处于范围内,无论程序可能调用的多少个函数/方法。

当变量超出范围时,它持有的对象也会被删除。这是预期的,因为根据编译器的观点,当引用该值的变量超出范围时,不需要继续保留该值,因为程序员不能再次引用该值。

这就是您的MCVE中的情况,model = QSqlQueryModel()变量(其初始化器缺少传递parent参数);model是局部于MainWindow.__init__方法。当该方法结束时,model超出范围,QSqlQueryModel对象被销毁。为什么QStandardItemModel仍然存在呢?好吧,它的初始化器接受一个正确传递的父对象(第三个参数),这个父对象是MainWindow,一直存在。将父对象设置为QSqlQueryModel(self)可以解决这个问题,因为模型是附加到MainWindow的,它将一直存在,直到窗口被销毁。

之所以有时会起作用,比如通过model.data()等调用,只是纯粹的运气。有关详细信息,请参阅https://stackoverflow.com/questions/2397984/undefined-unspecified-and-implementation-defined-behavior QIdentityProxyModel在与QCompleter一起使用时需要重写rowCount吗?

1: 一个通用的基础

英文:

In your MCVE, the QSqlQueryModel + proxy case doesn't work because the source model is destroyed before it's used by the proxy model.

Qt is a C++ framework and hence object lifetimes are to be carefully managed. According to Object Trees & Ownership - Qt documentation, QObjects (from which all Qt objects derive<sup>1</sup>) live in a tree. Any object can be attached to another as a child using QObject.setParent. The children, grandchildren and descendants are alive as long as the parent is.

What about the root/parent object? When an object has no parent, it's kept alive by the variable it's bound to. As long as the variable is in-scope the object is alive. In your MVCE, window variable holding the MainWindow object is a good example of this; it's in-scope throughout the program's life time irrespective of the many functions/methods the program might end up calling.

When the variable goes out of scope, the object it's holding gets deleted too. This is expected, as according to the compiler, there's no need to hold on to a value, when its reference, the variable goes out of scope, as the value can't be referred-to again by the programmer.

This is the case in your MCVE's model = QSqlQueryModel() variable (whose initializer is missing to pass in parent argument); model is local to MainWindow.__init__ method. When the method ends, model goes out of scope and QSqlQueryModel object gets destroyed. Why does the QStandardItemModel live on then? Well, it's initializer takes in a parent (third argument) which is correctly fed as the MainWindow which is alive all along. Setting the parent to QSqlQueryModel(self) fixes this issue because the model is parented to MainWindow and it'll live until the window is destroyed.

The point that it works sometimes due to calls like model.data(), etc. is just sheer luck. Refer https://stackoverflow.com/questions/2397984/undefined-unspecified-and-implementation-defined-behavior for details why QIdentityProxyModel在与QCompleter一起使用时需要重写rowCount吗?

<sub>1: a universal base</sub>

huangapple
  • 本文由 发表于 2023年7月17日 17:36:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/76703161.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定