2025, Dec 25 09:03

Как вернуть плоский фон ячеек в Qt 6.7 на Windows 11 (QTableWidget/QTableView)

Почему setBackground не заливает ячейки в QTableWidget/QTableView на Windows 11 (Qt 6.7), и как исправить: смена стиля на Fusion или делегат с плоской заливкой.

В Windows 11 повсеместно появились скруглённые углы — и начиная с Qt 6.7 этот стиль проявляется и внутри элементов QTableWidget/QTableView. Нюанс в том, что при установке фона через item.setBackground(QColor(...)) цвет больше не заполняет всю ячейку: заливка обрезается внутренними отступами и скруглениями. Стили через stylesheets могут залить ячейку «под завязку», но тогда теряется возможность раскрашивать только отдельные строки по данным модели. Ниже — что именно происходит и как получить плоский фон на всю ячейку, не ломая остальное.

Как воспроизвести проблему

Ниже минимальный пример, который задаёт цвет ячейки через роль фона элемента. В Windows 11 при стиле Qt для Windows 11 вы увидите вокруг цвета скруглённые отступы.

from PySide6.QtWidgets import QMainWindow, QApplication, QTableWidget, QTableWidgetItem, QVBoxLayout
from PySide6.QtGui import QColor

class AppFrame(QMainWindow):
    def __init__(self):
        super().__init__()

        host_layout = QVBoxLayout()

        grid = QTableWidget()
        grid.setRowCount(1)
        grid.setColumnCount(1)

        cell = QTableWidgetItem()
        grid.setItem(0, 0, cell)
        cell.setBackground(QColor(255, 0, 0))

        self.setCentralWidget(grid)
        host_layout.addWidget(grid)
        self.setLayout(host_layout)

if __name__ == "__main__":
    qtapp = QApplication([])

    ui = AppFrame()
    ui.show()

    qtapp.exec()

Почему так происходит

Начиная с Qt 6.7 добавлен новый QStyle, лучше соответствующий Windows 11. Этот стиль, внутреннее имя «windows11», намеренно рисует скругления для представлений элементов. Стандартный QStyledItemDelegate опирается на стиль при отрисовке (контрол CE_ItemViewItem). В реализации стиля элементы часто получают скруглённые формы, когда QStyleOptionViewItem.viewItemPosition не равен Middle; даже в «прямоугольном» варианте используются вертикальные отступы, из‑за которых фон не заполняет всю область ячейки.

В соответствующей ветке отрисовки для прямоугольного случая область рисования уменьшается за счёт верхнего и нижнего отступов — отсюда и видимые «поля» внутри ячейки.

...
} else {
    painter->drawRect(rect.marginsRemoved(QMargins(0, 2, 0, 2)));
}

Решение 1: сменить QStyle

Самый простой способ — вообще не использовать стиль Windows 11. Можно назначить другой стиль для всего приложения или для конкретного виджета. Fusion — надёжный кроссплатформенный вариант, не добавляющий скруглений фона у элементов. Применяйте стиль как можно раньше при старте приложения, чтобы избежать разнобоя в поведении.

from PySide6.QtWidgets import QApplication, QStyleFactory

# на уровне отдельного виджета
some_view.setStyle(QStyleFactory.create('fusion'))

# для всего приложения
QApplication.setStyle('fusion')

Такой подход делает поведение предсказуемым, но интерфейс становится менее «родным» для Windows 11. Если применять стиль точечно, к одному виджету, учтите: setStyle() не распространяется на дочерние элементы; полосы прокрутки, заголовки и viewport могут остаться на стилях приложения. Это также может повлиять на расчёт размеров, включая sizeHint для строк и столбцов.

Решение 2: делегат, который рисует плоский фон

Если хочется сохранить стиль приложения и при этом получить заливку на всю ячейку, используйте делегат. Делегат элементов отвечает за отрисовку. Есть два хода. Во‑первых, установка viewItemPosition в Middle уменьшает «скруглённость»; однако стиль Windows 11 всё равно добавляет внутренние верхний и нижний отступы, поэтому одного этого недостаточно для полностью плоского фона.

from PySide6.QtWidgets import QStyledItemDelegate, QStyleOptionViewItem

class PositionDelegate(QStyledItemDelegate):
    def initStyleOption(self, opt, idx):
        super().initStyleOption(opt, idx)
        opt.viewItemPosition = QStyleOptionViewItem.ViewItemPosition.Middle

# использование: table_view.setItemDelegate(PositionDelegate(table_view))

Надёжнее всего сначала самостоятельно заливать фон, а затем позволять стандартному делегату отрисовать содержимое. При этом используется BackgroundRole как есть, так что вы по‑прежнему можете раскрашивать отдельные строки или ячейки через модель или API элемента.

from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QBrush, QGradient
from PySide6.QtWidgets import QStyledItemDelegate

class FlatFillDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        bg = index.data(Qt.ItemDataRole.BackgroundRole)
        if isinstance(bg, (QBrush, QColor, QGradient, int)):
            painter.fillRect(option.rect, bg)
        super().paint(painter, option, index)

# установка для представления, например:
# grid.setItemDelegate(FlatFillDelegate(grid))

На этом этапе стиль всё ещё может накладывать поверх закруглённую подсветку выделения или наведения. При непрозрачном фоне это даёт заметную «вкладыш‑форму» со скруглениями внутри вашей заливки при выделении или наведении. При полупрозрачном фоне смешивание делает эффект ещё заметнее. Чтобы нейтрализовать его, дополните делегат минимальным стилем, который оставляет обычные элементы прозрачными и возвращает подсветку только для выбранных.

QTableView::item {
    background: transparent;
}
QTableView::item:selected {
    background: palette(highlight);
}

Такая связка сохраняет заливку во весь прямоугольник и при этом даёт стандартную подсветку, когда пользователь выбирает элемент.

Почему это важно понимать

Qt опирается на QStyle, чтобы обеспечивать «родной» вид на разных платформах. Когда платформенный стиль меняется — как это произошло в Windows 11 — визуальное поведение типовых виджетов может незаметно сдвигаться. Полагаться целиком на отрисовку через стиль — значит автоматически наследовать эти изменения: порой это хорошо, а порой, как с фонами ячеек таблиц, — не очень. Понимая границы ответственности стиля, легче решить, что выбрать: заменить стиль ради предсказуемости или точечно переопределить поведение делегатом.

Практические нюансы

Выбор стиля затрагивает всё приложение. Глобальный стиль вроде fusion обеспечивает единообразную отрисовку всех виджетов, но отличается от внешнего вида ОС. Назначение стиля только одному виджету не распространяется на его составные части — заголовки, полосы прокрутки, viewport — и может привести к смешанной эстетике. Делегаты дают точечный контроль над рисованием, не отказываясь от платформенного стиля, но при подключении таблиц стилей помните: представления — составные виджеты, и частичная стилизация может иметь побочные эффекты. Держите стили минимальными и нацеленными ровно на то, что нужно.

Доступность стилей зависит от платформы и версии Qt. Универсально доступен только один стиль Windows с именем windows. Стиль windowsxp есть лишь в Qt 5 на Windows. Стиль windowsvista доступен на Windows, включая более новые версии Qt и ОС. Стиль windows11 рассчитан на Windows 11 и новее. Если задаёте стили в коде, делайте проверки и применяйте их как можно раньше при запуске приложения.

Итоги

Если setBackground() больше не заливает ячейки QTableWidget/QTableView целиком в Windows 11, дело в платформенном стиле. Чтобы вернуть плоскую заливку на всю ячейку, либо смените стиль на вроде fusion, либо сохраните «родной» вид и используйте делегат, который рисует BackgroundRole по всему прямоугольнику; при необходимости дополните его небольшим stylesheet для управления оверлеями выделения. Выбирайте вариант, соответствующий вашим требованиям к единообразию интерфейса и целевым платформам, и применяйте его на раннем этапе инициализации, чтобы поведение оставалось предсказуемым.