Пользовательский интерфейс в Qt

Псевдосостояния и псевдоэлементы в QSS (:hover, ::indicator и т.д.)

Введение

Виджеты Qt имеют несколько визуальных состояний — обычное, под курсором мыши (hover), нажатое, отключённое, и так далее — и QSS позволяет задать разный стиль для каждого состояния через псевдосостояния. Кроме того, сложные виджеты (чекбоксы, радиокнопки, скроллбары) состоят из нескольких визуальных подэлементов, стилизуемых отдельно через псевдоэлементы. В статье разберём, как использовать оба механизма для тонкой настройки внешнего вида.

Концепция

Псевдосостояние записывается через двоеточие после селектора (:hover, :pressed, :disabled, :checked, :focus) и отражает текущее динамическое состояние виджета — Qt автоматически переключает применяемые стили при изменении состояния, без необходимости в JavaScript-подобной логике, как в вебе. Псевдоэлемент записывается через два двоеточия (::indicator для индикатора чекбокса/радиокнопки, ::handle для ручки слайдера, ::drop-down для выпадающей части комбобокса) и позволяет стилизовать конкретную визуальную часть сложного составного виджета отдельно от остальной его части.

Пример кода: Псевдосостояния и псевдоэлементы в QSS (

app.setStyleSheet(R"(
    /* Базовый стиль кнопки */
    QPushButton {
        background-color: #f3f4f6;
        border: 1px solid #d1d5db;
        border-radius: 4px;
        padding: 8px 16px;
    }

    /* Псевдосостояние :hover — стиль при наведении курсора мыши */
    QPushButton:hover {
        background-color: #e5e7eb;
        border-color: #9ca3af;
    }

    /* Псевдосостояние :pressed — стиль в момент нажатия */
    QPushButton:pressed {
        background-color: #d1d5db;
    }

    /* Псевдосостояние :disabled — стиль для неактивной кнопки */
    QPushButton:disabled {
        background-color: #f9fafb;
        color: #9ca3af;
        border-color: #e5e7eb;
    }

    /* Комбинация псевдосостояний — фокус И hover одновременно */
    QPushButton:focus:hover {
        border: 2px solid #2563eb;
    }

    /* Псевдоэлемент ::indicator — квадратик чекбокса как отдельная стилизуемая часть */
    QCheckBox::indicator {
        width: 18px;
        height: 18px;
        border: 2px solid #9ca3af;
        border-radius: 3px;
    }

    /* Псевдоэлемент в сочетании с псевдосостоянием — индикатор именно отмеченного чекбокса */
    QCheckBox::indicator:checked {
        background-color: #2563eb;
        border-color: #2563eb;
        image: url(:/icons/checkmark.svg);
    }

    /* Псевдоэлемент ::handle слайдера — стилизация именно перемещаемой "ручки" */
    QSlider::handle:horizontal {
        background-color: #2563eb;
        width: 16px;
        border-radius: 8px;
        margin: -4px 0; /* выравнивание ручки относительно желобка слайдера */
    }

    /* Псевдоэлемент ::groove слайдера — стилизация желобка, по которому движется ручка */
    QSlider::groove:horizontal {
        background-color: #e5e7eb;
        height: 4px;
        border-radius: 2px;
    }

    /* Псевдоэлемент ::drop-down комбобокса — выпадающая стрелка справа */
    QComboBox::drop-down {
        border: none;
        width: 24px;
    }
)");

Пояснения к коду

Правила :hover, :pressed, :disabled показывают типичный набор интерактивных состояний кнопки — Qt автоматически отслеживает реальное состояние виджета (наведён ли курсор, нажата ли кнопка мышью, отключена ли через setEnabled(false)) и применяет соответствующий блок стилей без какого-либо дополнительного кода в C++. Комбинация :focus:hover показывает, что псевдосостояния можно объединять цепочкой — стиль применяется только когда оба условия выполняются одновременно (виджет имеет фокус клавиатуры И курсор находится над ним).

QCheckBox::indicator и его комбинация с :checked показывают разделение составного виджета на части — у чекбокса есть текстовая метка и квадратик-индикатор, и эти две части можно стилизовать совершенно независимо. Аналогично QSlider::handle/QSlider::groove разделяют слайдер на подвижную ручку и неподвижный желобок — оба псевдоэлемента нужно стилизовать согласованно (как в примере, где высота желобка и отрицательный margin ручки подобраны так, чтобы ручка визуально центрировалась по высоте желобка), иначе визуальное расположение частей может выглядеть некорректно.

Подводные камни

  • Стилизация только псевдоэлемента ::indicator без базового стиля самого QCheckBox может привести к тому, что чекбокс будет выглядеть рассогласованно — например, текстовая метка останется в нативном системном стиле, а индикатор — в кастомном, что визуально может смотреться неаккуратно; для целостного вида обычно нужно продумывать стиль составного виджета как единое целое, а не стилизовать только одну его часть в изоляции.
  • Подбор margin/padding для согласования ::handle и ::groove слайдера требует ручной, эмпирической настройки — нет автоматического способа гарантировать идеальное визуальное совмещение ручки с желобком только на основе их размеров; значения, использованные в примере (margin: -4px 0 для ручки высотой 16px и желобка высотой 4px), подбирались опытным путём для конкретных размеров, и при изменении размеров эту настройку нужно пересчитывать вручную.
  • Не все виджеты поддерживают одинаковый набор псевдоэлементов — список доступных псевдоэлементов специфичен для конкретного класса виджета и описан в документации Qt для каждого виджета отдельно; попытка использовать псевдоэлемент, не поддерживаемый конкретным виджетом (например, ::indicator для QPushButton, у которого такого псевдоэлемента нет), просто тихо не даёт никакого эффекта, без явной ошибки или предупреждения.
  • Различие в поддержке псевдосостояний между платформами при использовании нативного стиля поверх QSS. Если приложение использует платформенный стиль (Windows Vista style, macOS style) и применяет лишь частичный QSS поверх него, некоторые псевдосостояния могут визуально конфликтовать с уже встроенной в нативный стиль логикой отображения состояний — для полностью предсказуемого, единообразного на всех платформах вида часто проще использовать кросс-платформенный базовый стиль (Fusion) как основу, поверх которого QSS работает наиболее предсказуемо.