Octpascal
@Octpascal

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

QDataWidgetMapperを用いてモデル・ビューが同期できない

解決したいこと

モデル内の長さ5のリストと5つのQDoubleSpinBoxの値をそれぞれ同期したい。

PythonおよびQtPyを用いて下図のようなアプリケーションを作成します。
5つのスピンボックスで値を更新するとモデルに反映され、その値を他のウィジェットで使用できます(例ではPrintボタンで表示できます)
他のWidgetでモデルの値が変更されたとき、スピンボックスの値も変更されます(例ではResetボタンでモデルの値を上書きしています)

image.png

発生している問題・エラー

WidgetとModelを接続する例としてQDataWidgetMapperを用いるものがあったため
そのように実装したのですが、Aの値しか反映されません。

おそらくsetCurrentIndexで指定された値のみが反映される?ようですが
モデルのリストと複数のWidgetを同時に同期するにはどうすればよいですか

  1. なにか必要な関数定義を忘れている?
  2. 実はこの機能は別のクラスで実現する必要がある?
  3. Qtにそれぞ実現するシステムは存在しない?

存在しない(3.)のであれば
すべてのWidgetのSignalをモデルと接続するのがもっともスマートな実装でしょうか
その場合はQDataWidgetMapperが必要なさそうですが、このクラスのユースケースはいつなのでしょうか

from dataclasses import dataclass
import sys
from PySide2.QtCore import QObject
from qtpy.QtCore import QModelIndex, Qt, QAbstractItemModel
from qtpy.QtWidgets import QDataWidgetMapper, QVBoxLayout, QApplication, QLabel, QDoubleSpinBox, QPushButton, QWidget


# DoubleSpinBoxの制約事項を定義
@dataclass(frozen=True)
class ValueConstraints:
    NAME: str
    DEFAULT: float = 50.0
    RANGE: tuple[float, float] = (0.0, 100.0)
    SINGLE_STEP: float = 0.1
    DECIMALS: int = 1

    def __post_init__(self):
        assert self.RANGE[0] <= self.DEFAULT and self.DEFAULT <= self.RANGE[1]


# A, B, C, D, Eの5つを設定
constraints_list = [ValueConstraints(NAME=name) 
    for name in ['A', 'B', 'C', 'D', 'E']]


# モデル
class SpinBoxValuesModel(QAbstractItemModel):
    def __init__(self, parent: QObject | None = None, init_values: list[float] = [0.0]*5) -> None:
        super().__init__(parent)
        self.values = init_values

    def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
        return len(self.values)

    def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
        return 1

    def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex:
        if not self.hasIndex(row, column, parent):
            return QModelIndex()

        return self.createIndex(row, column)

    def parent(self, index: QModelIndex) -> QModelIndex:
        return QModelIndex

    def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> float:
        print('data is called')

        if not index.isValid():
            return None

        row = index.row()
        if role in {Qt.DisplayRole, Qt.EditRole}:
            return self.values[row]

        return None

    def setData(self, index: QModelIndex, value: float, role: int = Qt.EditRole) -> bool:
        print('setData is called')

        if not index.isValid():
            return False

        row = index.row()
        if role == Qt.EditRole:
            if value != self.values[row]:
                self.values[row] = value
                self.dataChanged.emit(index, index)
                return True

        return False

    def flags(self, index: QModelIndex) -> Qt.ItemFlags:
        if not index.isValid():
            return Qt.NoItemFlags

        return Qt.ItemIsEditable | Qt.ItemIsEnabled

    def setAllValues(self, values: list[float]):
        self.values = values
        start_index = self.index(0, 0)
        end_index = self.index(self.rowCount() - 1, 0)
        self.dataChanged.emit(start_index, end_index)


# ビューでDoubleSpinBoxを定義. QDataWidgetMapperを用いてViewと割り当てる
class SpinBoxListWidget(QWidget):
    def __init__(self, parent: QWidget | None = None,
                 constraints_list: list[ValueConstraints] = constraints_list) -> None:
        super().__init__(parent)

        self.model = SpinBoxValuesModel(
            init_values=[con.DEFAULT for con in constraints_list]
        )
        self.mapper = QDataWidgetMapper(self)
        self.mapper.setModel(self.model)
        self.mapper.setSubmitPolicy(QDataWidgetMapper.AutoSubmit)

        self.spin_box_layout(constraints_list=constraints_list)

    def spin_box_layout(self, constraints_list: list[ValueConstraints]):
        self.v_layout = QVBoxLayout(self)
        self.paired_widgets: tuple[QLabel, QDoubleSpinBox] = []

        for i, constraints in enumerate(constraints_list):
            label = QLabel(self)
            label.setText(constraints.NAME)

            spin = QDoubleSpinBox(self)
            spin.setRange(*constraints.RANGE)
            spin.setSingleStep(constraints.SINGLE_STEP)
            spin.setValue(constraints.DEFAULT)
            spin.setDecimals(constraints.DECIMALS)

            # モデルの割当
            self.mapper.addMapping(spin, i)

            self.paired_widgets.append((label, spin))

            self.v_layout.addWidget(label)
            self.v_layout.addWidget(spin)

            if i < len(constraints_list):
                self.v_layout.addStretch()

        self.setLayout(self.v_layout)
        self.mapper.toFirst()


# 例を動かすためのWindow
class TestWidget(QWidget):
    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)

        self.setLayout(QVBoxLayout(self))
        self.list_spinbox_widget = SpinBoxListWidget(self)
        self.layout().addWidget(self.list_spinbox_widget)

        self.print_button = QPushButton('Print', self)
        self.layout().addWidget(self.print_button)

        self.reset_button = QPushButton('Reset', self)
        self.layout().addWidget(self.reset_button)

        self.list_spinbox_widget.model.dataChanged.connect(self.print_func)
        self.print_button.clicked.connect(self.print_func)
        self.reset_button.clicked.connect(self.reset_values)

    def print_func(self):
        print(self.list_spinbox_widget.model.values)

    def reset_values(self):
        self.list_spinbox_widget.model.setAllValues(
            [con.DEFAULT for con in constraints_list]
        )

if __name__=='__main__':
    app = QApplication(sys.argv)

    window = TestWidget()
    window.show()
    app.exec_()
0

No Answers yet.

Your answer might help someone💌