itmo_conspects

Лекция 7. Продвинутые инструменты Qt

QSS для виджетов - это способ описания внешнего вида элементов интерфейса с помощью правил, похожих на CSS. Строки стилей можно назначать конкретному виджету или всему приложению через setStyleSheet(). Это позволяет отделить оформление от логики и быстро менять тему программы

Правила QSS состоят из селектора и блока объявлений в фигурных скобках. Доступны несколько видов селекторов:

Стили можно загружать из файла или задавать прямо в коде. Пример применения ко всему приложению:

qApp->setStyleSheet(
    "QPushButton { background-color: red; color: white; }"
    "QPushButton:hover { background-color: green; }"
);

Пользовательская графика в Qt реализуется через переопределение метода paintEvent и использование класса QPainter. Любой наследник QWidget может рисовать на своей поверхности. Система вызывает paintEvent тогда, когда виджету требуется перерисовка

Внутри paintEvent создаётся объект QPainter painter(this), который предоставляет инструменты для рисования геометрических фигур, текста и изображений:

Внешний вид линий и заливок настраивается отдельными объектами. QPen отвечает за контур: цвет, толщина, стиль (сплошной, пунктир, штрихпунктир). QBrush управляет заливкой: можно задать сплошной цвет, градиент или повторяющуюся текстуру. Чтобы заставить виджет перерисоваться, достаточно вызвать update() - он запланирует новый вызов paintEvent. Рисовать можно не только на виджете, но и на любом устройстве вывода из иерархии QPaintDevice, например на QImage или QPixmap:

class MyWidget : public QWidget {
protected:
    void paintEvent(QPaintEvent *) override {
        QPainter painter(this);
        painter.setRenderHint(QPainter::Antialiasing);
        QPen pen(Qt::blue, 3);
        painter.setPen(pen);
        painter.setBrush(Qt::yellow);
        painter.drawEllipse(50, 50, 100, 60);
    }
};

Фильтр событий - это механизм, позволяющий перехватывать события любого QObject без создания подкласса. Он даёт возможность централизованно обрабатывать события, например, для нескольких однотипных виджетов. Установка фильтра выполняется вызовом target->installEventFilter(this), где this - объект-наблюдатель

Объект-наблюдатель обязан переопределить метод eventFilter(QObject *watched, QEvent *event). Если фильтр возвращает true, событие считается обработанным и не передаётся целевому объекту. Возврат false направляет событие обычным путём - оно попадёт в метод event() целевого виджета

Типичный пример использования - ограничение ввода. Создаётся класс-фильтр, который перехватывает события нажатия клавиш, анализирует вводимый символ и, если тот не соответствует условию (например, не является цифрой), возвращает true, подавляя событие. В противном случае событие пропускается к полю ввода

class DigitFilter : public QObject {
protected:
    bool eventFilter(QObject *obj, QEvent *event) override {
        if (event->type() == QEvent::KeyPress) {
            QKeyEvent *key = static_cast<QKeyEvent*>(event);
            if (!key->text().isEmpty() && !key->text().at(0).isDigit())
                return true; // событие подавлено
        }
        return false;
    }
};

// Установка фильтра
QLineEdit *edit = new QLineEdit;
DigitFilter *filter = new DigitFilter;
edit->installEventFilter(filter);

Разделение памяти между потоками

Истинное разделение (True sharing) - это ситуация, когда несколько ядер процессора интенсивно читают и/или пишут в одну и ту же кэш-линию из-за доступа к одной и той же переменной. Это вызывает постоянную синхронизацию кэшей по протоколу когерентности, что резко снижает производительность, даже если доступ к переменной защищён мьютексом или она атомарна

Например: два потока увеличивают общий счётчик int counter. Каждый делает 1000 итераций counter++. Из-за того, что счётчик находится в одной кэш-линии, ядра будут постоянно обмениваться этой линией, и в результате время выполнения может быть в десятки раз больше, чем если бы каждый поток работал со своей локальной переменной

Чтобы бороться с истинным разделением, можно:

Ложное разделение (False sharing) - это скрытая проблема производительности, когда потоки формально работают с разными переменными, но те попадают в одну кэш-линию процессора. Размер кэш-линии обычно составляет 64 байта. Когда один поток пишет в свою переменную, вся линия помечается изменённой, и другие ядра вынуждены загружать её заново, даже если их собственные данные не менялись. В результате возникает лишний трафик памяти и промахи кэша, замедляющие многопоточную программу

Представьте структуру с двумя счётчиками int a и int b, расположенными рядом. Поток 1 меняет только a, поток 2 - только b. Настоящего конфликта нет, но из-за близкого расположения переменные делят одну кэш-линию. Постоянная синхронизация линии между кэшами ядер приводит к падению производительности. Решением служит разнесение переменных по разным линиям с помощью выравнивания или отступов. Например, можно пометить поля ключевым словом alignas(64) или вставить массив-заполнитель char padding[64] между a и b. Тогда каждая переменная окажется в собственной кэш-линии, и потоки перестанут мешать друг другу

struct Counters {
    alignas(64) int a;
    alignas(64) int b;
};

Counters cnt;
// Поток 1: cnt.a, Поток 2: cnt.b - false sharing исключён