Визуальная позиция элемента в QTableView - PullRequest
1 голос
/ 11 марта 2020

Я работаю с PyQt5 / PySide2. У меня есть QTableView с QSortFilterProxyModel, а данные обрабатываются с помощью QStandardItemModel.

. Я использую метод QStandardItemModel.findItems(), чтобы найти несколько ячеек в первой строке таблицы. Результатом является список QStandardItem с. Теперь я хочу упорядочить эти элементы по строкам, в которых они отображаются в таблице GUI, то есть так, как их видит пользователь. Есть ли способ архивировать это? Чтобы перевести индексы прокси или модели в «просмотр» индексов.

Я думал, что это можно сделать с помощью метода QSortFilterProxyModel.mapFromSource(), но, похоже, индексы прокси не имеют желаемого порядка.

здесь это минимальный воспроизводимый пример, написанный на PyQt5:

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from collections import deque
from random import randint


class Splash(QWidget):

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

        # create model
        self.model = QStandardItemModel(self)
        self.model.setHorizontalHeaderLabels(["column 1", "column 2"])

        # create sort proxy
        self.proxy = NumberSortModel()
        self.proxy.setSourceModel(self.model)

        # create view
        self.table = CustomQTableView(self)
        self.table.setGeometry(0, 0, 275, 575)
        self.table.setModel(self.proxy)
        self.table.setSortingEnabled(True)

        # create buttons
        button = QPushButton('Find cells containing 1', self)
        button.move(300, 70)
        button.clicked.connect(lambda: self.table.search_string("1"))

        button1 = QPushButton('next', self)
        button1.move(300, 100)
        button1.clicked.connect(self.table._search_next)

        button2 = QPushButton('previous', self)
        button2.move(300, 130)
        button2.clicked.connect(self.table._search_previous)

        # fill model
        for i in range(15):
            self.model.appendRow([QStandardItem(str(i)),
                                  QStandardItem(str(randint(1, 100)))])

        self.show()


# takes care of the coloring of results
class _HighlightDelegate(QStyledItemDelegate):

    def __init__(self, parent=None) -> None:

        QStyledItemDelegate.__init__(self, parent)
        self._parent = parent

    def paint(self, painter: "QPainter", option: "QStyleOptionViewItem",
              index: "QModelIndex"):

        painter.save()
        if len(self._parent.proxy_indices) > 0:
            if index == self._parent.proxy_indices[0]:
                painter.fillRect(option.rect, Qt.red)
            elif index in self._parent.proxy_indices:
                painter.fillRect(option.rect, option.palette.highlight())
        else:
            if (option.state & QStyle.State_Selected):
                painter.fillRect(option.rect, option.palette.highlight())
            elif (option.state & QStyle.State_None):
                painter.fillRect(option.rect, option.palette.base())

        painter.drawText(option.rect, Qt.AlignLeft, index.data(Qt.DisplayRole))

        painter.restore()


class CustomQTableView(QTableView):

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

        self.real_indices = deque()
        self.proxy_indices = deque()

        self.horizontalHeader().sortIndicatorChanged.connect(self._re_sort)

        self.setItemDelegate(_HighlightDelegate(self))

    def _re_sort(self):

        # pretty print indices
        def ind_to_py(indices):

            py_ind = list()
            for i in indices:
                py_ind.append((i.row(), i.column(), i.data(Qt.DisplayRole)))

            return py_ind

        print("real  ", ind_to_py(self.real_indices))
        print("proxy ", ind_to_py(self.proxy_indices))

        real_ind, proxy_ind = zip(*sorted(zip(self.real_indices, self.proxy_indices),
                                          key=lambda x: (x[1].row(),
                                                         x[1].column())))

        self.real_indices = deque(real_ind)
        self.proxy_indices = deque(proxy_ind)

        print("sorted real ", ind_to_py(self.real_indices))
        print("sorted proxy", ind_to_py(self.proxy_indices))
        print("---------------------------------------------------")

        self.re_draw()

    @property
    def _model(self):
        return self.model().sourceModel()

    def re_draw(self):
        self.viewport().update()

    # we are always searching only in first column
    def search_string(self, string: str):

        indices = self._model.findItems(string, Qt.MatchContains, 0)

        # get QModelIndex from found data
        self.real_indices = deque([i.index() for i in indices])
        self.proxy_indices = [QPersistentModelIndex(self.model().mapFromSource(i))
                              for i in self.real_indices]

        # sort indeces according to their row and column
        self._re_sort()

        # update the view to highlight data
        self.re_draw()

    def _search_next(self):
        self.real_indices.rotate(-1)
        self.proxy_indices.rotate(-1)
        self.re_draw()

    def _search_previous(self):
        self.real_indices.rotate(1)
        self.proxy_indices.rotate(1)
        self.re_draw()


# custom implementation to sort according to numbers not strings
class NumberSortModel(QSortFilterProxyModel):

    def lessThan(self, left_index: "QModelIndex",
                 right_index: "QModelIndex") -> bool:

        left_var: str = left_index.data(Qt.EditRole)
        right_var: str = right_index.data(Qt.EditRole)

        try:
            return float(left_var) < float(right_var)
        except (ValueError, TypeError):
            pass

        try:
            return left_var < right_var
        except TypeError:  # in case of NoneType
            return True


if __name__ == '__main__':
    import sys

    app = QApplication(sys.argv)
    ex = Splash()
    sys.exit(app.exec_())

Короче говоря, когда я запускаю поиск и щелкаю рядом, красная отмеченная ячейка перемещается вниз. Он перемещается вверх при нажатии предыдущей. Но когда я применяю сортировку, нажимая на заголовок таблицы, это портит следующие / предыдущие функции. Я хочу, чтобы красная ячейка всегда была на go вниз независимо от примененной сортировки, если щелкнуть по следующему так же, как и к предыдущему.

app minimal example

1 Ответ

2 голосов
/ 11 марта 2020

Логика c картины не должна быть сделана напрямую, а через роли, которые делегат должен использовать для рисования.

С другой стороны, следующий элемент должен быть выбран на основе на визуальной позиции элемента, поэтому он должен отображаться с использованием прокси.

Учитывая вышеизложенное, решение:

CurrentRole = Qt.UserRole + 1000
SelectedRole = Qt.UserRole + 1001

# takes care of the coloring of results
class _HighlightDelegate(QStyledItemDelegate):
    def initStyleOption(self, option: "QStyleOptionViewItem", index: "QModelIndex"):
        super().initStyleOption(option, index)
        is_current = index.data(CurrentRole) or False
        is_selected = index.data(SelectedRole) or False
        if is_current:
            option.backgroundBrush = QColor(Qt.red)
            option.palette.setColor(QPalette.Normal, QPalette.Highlight, QColor(Qt.red))
        elif is_selected:
            option.backgroundBrush = option.palette.highlight()


class CustomQTableView(QTableView):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.selected_indexes = []
        self.current_index = None
        self.setItemDelegate(_HighlightDelegate(self))

    @property
    def _model(self):
        return self.model().sourceModel()

    def search_string(self, string: str):
        # restore
        for index in self.selected_indexes:
            self._model.setData(QModelIndex(index), False, SelectedRole)
        if self.current_index is not None:
            self._model.setData(QModelIndex(self.current_index), False, CurrentRole)
        self.current_index = None

        column = 0
        match_indexes = self._model.match(
            self._model.index(0, column), Qt.DisplayRole, string, -1, Qt.MatchContains
        )
        self.selected_indexes = [
            QPersistentModelIndex(index) for index in match_indexes
        ]
        self._sort_indexes_by_view()
        if self.selected_indexes:
            self.current_index = self.selected_indexes[0]

        for index in self.selected_indexes:
            self._model.setData(QModelIndex(index), True, SelectedRole)
        if self.current_index is not None:
            self._model.setData(QModelIndex(self.current_index), True, CurrentRole)

    def _search_next(self):
        if self.current_index is not None:
            self._model.setData(QModelIndex(self.current_index), False, CurrentRole)
            self._sort_indexes_by_view()
            pos = self.selected_indexes.index(self.current_index)
            next_pos = (pos + 1) % len(self.selected_indexes)
            self.current_index = self.selected_indexes[next_pos]
            self._model.setData(QModelIndex(self.current_index), True, CurrentRole)

    def _search_previous(self):
        if self.current_index is not None:
            self._model.setData(QModelIndex(self.current_index), False, CurrentRole)
            self._sort_indexes_by_view()
            pos = self.selected_indexes.index(self.current_index)
            next_pos = (pos - 1) % len(self.selected_indexes)
            self.current_index = self.selected_indexes[next_pos]
            self._model.setData(QModelIndex(self.current_index), True, CurrentRole)

    def _sort_indexes_by_view(self):
        self.selected_indexes.sort(
            key=lambda index: self.model().mapFromSource(QModelIndex(index)).row()
        )
...