使用 QTableView 和 QSelectionModel 选择日历样式

Calendar Style Selection With QTableView and QSelectionModel

我正在使用 QTableView 构建自定义日历视图,并希望有一个 QItemSelectionModel 可以按天和周连续选择单元格。不确定从哪里开始,因为选择模型不与视图交互。视图的 onCurrentChange 方法提供当前索引,在 selectionModel 中不起作用。

视图通常连接到更复杂的日历模型;这里的 table 模型是为了说明。

from PyQt5.QtCore import QModelIndex, QDate
from PyQt5.QtCore import Qt, QAbstractTableModel, QItemSelectionModel

from PyQt5.QtWidgets import QTableView
import typing


class TableModel(QAbstractTableModel):
    def __init__(self):
        super(TableModel, self).__init__()

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...) -> typing.Any:
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return QDate.longDayName(section + 1)

    def data(self, index, role):
        if role == Qt.DisplayRole:
            return index.row() * 7 + index.column() + 1

    def rowCount(self, index):
        return 6

    def columnCount(self, index):
        return 7


class CalendarSelectionModel(QItemSelectionModel):

    def __init__(self, *args, **kwargs):
        super(CalendarSelectionModel, self).__init__(*args, *kwargs)

    def currentChanged(self, current: QModelIndex, previous: QModelIndex) -> None:
        print(current, previous)


class CalendarView(QTableView):

    def __init__(self):
        super(CalendarView, self).__init__()

    # def currentChanged(self, current: QModelIndex, previous: QModelIndex) -> None:
    #     print(current)


if __name__ == '__main__':
    import sys
    from PyQt5.QtWidgets import QApplication, QTableView

    app = QApplication(sys.argv)

    model = TableModel()

    cal = CalendarView()
    cal.setModel(model)
    sel = CalendarSelectionModel(model)
    cal.setSelectionModel(sel)
    cal.show()

    cal.resize(860, 640)

    sys.exit(app.exec_())

这是日历样式选择的解决方案。在 table 上设置 QAbstractItemView.NoSelection 并将选择逻辑放在 currentChanged 中是方法。

import typing

from PyQt5.QtCore import QDate
from PyQt5.QtCore import Qt, QAbstractTableModel, QItemSelectionModel, QItemSelection, QModelIndex
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QTableView, QAbstractItemView


class TableModel(QAbstractTableModel):
    def __init__(self):
        super(TableModel, self).__init__()

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...) -> typing.Any:
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return QDate.longDayName(section + 1)

    def data(self, index, role):
        if role == Qt.DisplayRole:
            return index.row() * 7 + index.column() + 1

    def rowCount(self, index):
        return 6

    def columnCount(self, index):
        return 7


class CalendarView(QTableView):

    def __init__(self):
        super(CalendarView, self).__init__()
        self.setSelectionMode(QAbstractItemView.NoSelection)
        self._start: typing.Union[QModelIndex, None] = None

    def mousePressEvent(self, e: QMouseEvent) -> None:
        self._start = self.indexAt(e.pos())
        self.clearSelection()
        self.selectionModel().select(self._start, QItemSelectionModel.Select)
        super(CalendarView, self).mousePressEvent(e)

    def index(self, row, col):
        return self.model().index(row, col)

    def currentChanged(self, current: QModelIndex, previous: QModelIndex) -> None:
        if self.state() == QAbstractItemView.DragSelectingState:
            self.clearSelection()
            if current.row() == self._start.row():  # we only have one row, select from start to current
                selection = QItemSelection(self._start, self.model().index(self._start.row(), current.column()))
            else:  # more than one row selected make the other 2 selections
                selection = QItemSelection(self._start, current)
                if current.row() < self._start.row():  # stencil out diagonal
                    right = QItemSelection(current, self.index(self._start.row() - 1, 6))
                    left = QItemSelection(self.index(current.row() + 1, 0), self._start)
                    selection.merge(right, QItemSelectionModel.Select)
                    selection.merge(left, QItemSelectionModel.Select)
                    if current.column() > self._start.column():
                        stencil = QItemSelection(self.index(current.row(), current.column() - 1),
                                                 self.index(current.row(), 0))
                        stencil2 = QItemSelection(self.index(self._start.row(), self._start.column() + 1),
                                                  self.index(self._start.row(), 6))
                        selection.merge(stencil, QItemSelectionModel.Deselect)
                        selection.merge(stencil2, QItemSelectionModel.Deselect)
                else:
                    right = QItemSelection(self._start, self.index(current.row() - 1, 6))
                    left = QItemSelection(self.index(self._start.row() + 1, 0), current)
                    selection.merge(right, QItemSelectionModel.Select)
                    selection.merge(left, QItemSelectionModel.Select)
                    if current.column() < self._start.column():  # stencil out diagonal
                        stencil = QItemSelection(self.index(self._start.row(), self._start.column() - 1),
                                                 self.index(self._start.row(), 0))
                        stencil2 = QItemSelection(self.index(current.row(), current.column() + 1),
                                                  self.index(current.row(), 6))
                        selection.merge(stencil, QItemSelectionModel.Deselect)
                        selection.merge(stencil2, QItemSelectionModel.Deselect)
            self.selectionModel().select(selection, QItemSelectionModel.Select)


if __name__ == '__main__':
    import sys
    from PyQt5.QtWidgets import QApplication, QTableView

    app = QApplication(sys.argv)

    model = TableModel()

    cal = CalendarView()
    cal.setModel(model)
    cal.show()

    cal.resize(860, 640)

    sys.exit(app.exec_())