QDataWidgetMapperを用いてモデル・ビューが同期できない
解決したいこと
モデル内の長さ5のリストと5つのQDoubleSpinBoxの値をそれぞれ同期したい。
PythonおよびQtPyを用いて下図のようなアプリケーションを作成します。
5つのスピンボックスで値を更新するとモデルに反映され、その値を他のウィジェットで使用できます(例ではPrintボタンで表示できます)
他のWidgetでモデルの値が変更されたとき、スピンボックスの値も変更されます(例ではResetボタンでモデルの値を上書きしています)
発生している問題・エラー
WidgetとModelを接続する例としてQDataWidgetMapperを用いるものがあったため
そのように実装したのですが、Aの値しか反映されません。
おそらくsetCurrentIndexで指定された値のみが反映される?ようですが
モデルのリストと複数のWidgetを同時に同期するにはどうすればよいですか
- なにか必要な関数定義を忘れている?
- 実はこの機能は別のクラスで実現する必要がある?
- 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