基于PySide6的Qt模型/视图编程:实现数据列垂直成对显示的方案咨询
Hey there! Let's figure out the best way to achieve this vertical pair display (A above B, repeated for each data row) in PySide6's model-view framework. Your current QScrollArea + QDataWidgetMapper approach works, but it's not the most scalable or idiomatic—Qt's model-view system has cleaner, more maintainable solutions tailored for this kind of layout. Here are the top three options, ordered by ease of implementation and alignment with Qt's design principles:
1. Proxy Model + QTableView (Simplest, Reuses Existing Views)
This approach leverages a proxy model to transform your original 2-column data into a single-column view where each original row becomes two rows (A followed by B). You can use a standard QTableView with minimal changes, which means you get all its built-in features (selection, editing, sorting) for free.
Example Code:
from PySide6.QtCore import QAbstractProxyModel, QModelIndex, Qt from PySide6.QtWidgets import QApplication, QTableView, QWidget, QVBoxLayout from PySide6.QtGui import QStandardItemModel, QStandardItem class PairProxyModel(QAbstractProxyModel): def __init__(self, parent=None): super().__init__(parent) def rowCount(self, parent=QModelIndex()): if parent.isValid(): return 0 # Double the row count: each original row becomes two proxy rows return self.sourceModel().rowCount() * 2 def columnCount(self, parent=QModelIndex()): return 1 # Only show one column def mapFromSource(self, sourceIndex): if not sourceIndex.isValid(): return QModelIndex() # Map source column 0 (A) to even proxy rows, column 1 (B) to odd if sourceIndex.column() == 0: return self.createIndex(sourceIndex.row() * 2, 0, sourceIndex.internalPointer()) elif sourceIndex.column() == 1: return self.createIndex(sourceIndex.row() * 2 + 1, 0, sourceIndex.internalPointer()) return QModelIndex() def mapToSource(self, proxyIndex): if not proxyIndex.isValid(): return QModelIndex() # Reverse the mapping: even proxy rows go to source column 0, odd to 1 source_row = proxyIndex.row() // 2 source_col = 0 if proxyIndex.row() % 2 == 0 else 1 return self.sourceModel().index(source_row, source_col) def index(self, row, column, parent=QModelIndex()): if parent.isValid() or column != 0: return QModelIndex() source_row = row // 2 source_col = 0 if row % 2 == 0 else 1 source_index = self.sourceModel().index(source_row, source_col) return self.createIndex(row, column, source_index.internalPointer()) def parent(self, index): return QModelIndex() # No hierarchy, so return invalid index def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None # Delegate data requests to the source model source_index = self.mapToSource(index) return self.sourceModel().data(source_index, role) if __name__ == "__main__": app = QApplication([]) window = QWidget() layout = QVBoxLayout(window) # Create your original 2-column model source_model = QStandardItemModel() source_model.setHorizontalHeaderLabels(["A", "B"]) for i in range(5): source_model.appendRow([QStandardItem(f"A{i+1}"), QStandardItem(f"B{i+1}")]) # Attach the proxy model proxy_model = PairProxyModel() proxy_model.setSourceModel(source_model) # Use a standard QTableView view = QTableView() view.setModel(proxy_model) view.horizontalHeader().hide() # Hide the single-column header layout.addWidget(view) window.show() app.exec()
2. QListView + Custom Delegate (Most Flexible for Styling)
If you want full control over how each A/B pair looks (custom fonts, colors, spacing), a custom item delegate for QListView is perfect. This approach keeps your model intact and lets you draw each pair as a single list item with vertical layout.
Example Code:
from PySide6.QtCore import Qt, QSize from PySide6.QtWidgets import QApplication, QListView, QWidget, QVBoxLayout, QStyle from PySide6.QtGui import QStandardItemModel, QStandardItem, QStyledItemDelegate, QPainter, QTextOption class PairDelegate(QStyledItemDelegate): def __init__(self, parent=None): super().__init__(parent) self.item_height = 40 # Height for each individual A/B line def sizeHint(self, option, index): # Each list item takes twice the height of a single line return QSize(option.rect.width(), self.item_height * 2) def paint(self, painter, option, index): painter.save() # Draw background for selected items if option.state & QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) # Fetch A and B values from the model a_index = index.sibling(index.row(), 0) b_index = index.sibling(index.row(), 1) a_text = index.model().data(a_index, Qt.DisplayRole) or "" b_text = index.model().data(b_index, Qt.DisplayRole) or "" # Define rectangles for A and B a_rect = option.rect.adjusted(10, 0, -10, -self.item_height) b_rect = option.rect.adjusted(10, self.item_height, -10, 0) # Set text alignment (left-aligned, vertically centered) text_option = QTextOption(Qt.AlignLeft | Qt.AlignVCenter) # Set text color based on selection state text_color = option.palette.highlightedText().color() if option.state & QStyle.State_Selected else option.palette.text().color() painter.setPen(text_color) # Draw the text painter.drawText(a_rect, a_text, text_option) painter.drawText(b_rect, b_text, text_option) painter.restore() if __name__ == "__main__": app = QApplication([]) window = QWidget() layout = QVBoxLayout(window) # Original 2-column model source_model = QStandardItemModel() source_model.setHorizontalHeaderLabels(["A", "B"]) for i in range(5): source_model.appendRow([QStandardItem(f"A{i+1}"), QStandardItem(f"B{i+1}")]) # QListView with custom delegate view = QListView() view.setItemDelegate(PairDelegate()) view.setModel(source_model) view.setViewMode(QListView.ListMode) view.setUniformItemSizes(True) # Ensures consistent item heights layout.addWidget(view) window.show() app.exec()
3. Custom QAbstractItemView (Full Layout Control)
If you need a completely custom layout (e.g., arranging pairs in a grid instead of a single column), you can subclass QAbstractItemView and override methods to control how items are positioned and rendered. This is the most powerful option but requires more code, so only use it if the above two don't fit your needs.
Key Methods to Override:
visualRect(): Returns the on-screen rectangle for a given model indexindexAt(): Returns the model index at a given screen positionscrollTo(): Handles scrolling to bring an index into viewpaintEvent(): Draws items (or manage child widgets if you prefer widget-based rendering)
Which to Choose?
- Proxy Model + QTableView: Best if you want to reuse QTableView's built-in features (editing, sorting) with minimal code.
- QListView + Custom Delegate: Ideal if you need custom styling or want to keep the view as a single list of pairs.
- Custom QAbstractItemView: Only for complex, fully custom layouts that can't be achieved with the other two approaches.
Both the proxy model and delegate approaches are far more scalable than your current QScrollArea setup—they automatically sync with model data changes, support selection and editing out of the box, and follow Qt's model-view separation principles.
内容的提问来源于stack exchange,提问作者Andre Jonas

