Статті цього циклу:
У статті по роботі з аудіо плеєром Qt ми познайомилися з тим, як програвати аудіо треки і перемикатися між ними в плейлисті.
А як щодо того, щоб кастомізувати зовнішній вигляд плеєра так, щоб він був схожим, наприклад, на AIMP? Відразу для порівняння подивимося на оригінальний AIMP та зовнішній вигляд плеєра після кастомізації.
Основною проблемою подібної кастомізації програм Qt є те, що Qt не має засобів для кастомізації обрамлення вікна програми. Тобто у випадку Windows необхідно для декорування вікна використовувати WinAPI, а у разі unix та linux систем необхідно використовувати відповідні API декораторів вікон.
Єдине, що може зробити Qt – це повністю відключити обрамлення вікна. Тоді потрібно повністю написати декорування вікна засобами Qt, не використовуючи можливості операційної системи, написати логіку роботи кнопок згортання та максимізації вікна, а також логіку переміщення та зміни розмірів вікна програми. Ну і відповідно розписати всі стилі елементів інтерфейсу, використовуючи механізм QSS StyleSheet. Альтернативним варіантом оформлення є QPalette, але не завжди обробка стилів відбувається коректно на відміну застосування QSS Stylesheet.
Відключення обрамлення вікна програми
Для того, щоб інтерфейс програми коректно оброблявся, нам знадобиться:
- відключити обрамлення вікна;
- зробити прозорим основний віджет вікна, де знаходиться весь інтерфейс;
- а самі елементи інтерфейсу помістити всередину ще одного віджету, який розташовуватиметься всередині основного і при цьому не буде прозорим;
- також потрібно створити зробити тінь навколо віджету з інтерфейсом, щоб емулювати тінь від вікна програми, як це зроблено у звичайних вікон і в тому числі у вікна AIMP.
Перед відключенням обрамлення вікна та встановленням основного віджету в режим прозорості у графічному дизайнері було зроблено деякі зміни самого інтерфейсу.
Як бачите по дереву елементів, є основний віджет, який має бути прозорим, а також віджет із графічним інтерфейсом. Область укладеного між межами двох прямокутних виділень червоного кольору - це видима частина основного віджету. У ній буде розташована тінь від вікна програми, а також та область, яка використовуватиметься для зміни розмірів вікна програми.
Для вимкнення обрамлення вікна потрібно встановити прапорець Qt::FramelessWindowHint . Для встановлення фону віджета повністю прозорий режим необхідно встановити атрибут Qt::WA_TranslucentBackground .
Установку тіні необхідно робити не на сам основний віджет, але в віджет, який містить інтерфейс програми. Для створення ефекту тіні використовується об'єкт класу QGraphicsFropShadowEffect.
Все це виготовляємо в конструкторі класу Widget, який відповідає за вікно програми.
/// Настройка UI this->setWindowFlags(Qt::FramelessWindowHint); // Отключаем оформление окна this->setAttribute(Qt::WA_TranslucentBackground); // Делаем фон главного виджета прозрачным this->setStyleSheet(StyleHelper::getWindowStyleSheet()); // Устанавливаем стиль виджета // Создаём эффект тени QGraphicsDropShadowEffect *shadowEffect = new QGraphicsDropShadowEffect(this); shadowEffect->setBlurRadius(9); // Устанавливаем радиус размытия shadowEffect->setOffset(0); // Устанавливаем смещение тени ui->widgetInterface->setGraphicsEffect(shadowEffect); // Устанавливаем эффект тени на окно ui->widgetInterface->layout()->setMargin(0); // Устанавливаем размер полей ui->widgetInterface->layout()->setSpacing(0); // Устанавливаем размер пространства между элементами в размещении виджета
Таблиця стилів QSS
Як ви вже встигли помітити, у шматку наведеного вище коду використовується клас StyleHelper, у якого викликає статичний метод getWindowStyleSheet() . Цей клас є допоміжним і повертає QString, описує стиль оформлення віджету інтерфейсу. У цих методах також робиться установка іконок кнопок з файлів ресурсів, частина цих іконок наведено нижче.
QSS стилі виконані за принципом CSS стилів у веб-верстки. Вони відповідають за такі самі параметри. Наприклад, padding, margin і т.д.
Для оформлення вікна цього додатка використовується клас StyleHelper з наступним вмістом.
stylehelper.h
Звертаю вашу увагу, що в класі використовується два методи, зі стилями, які застосовуються до одного і того ж елемента (кнопки максимізації/нормалізації розміру вікна програми) за певних умов. Це методи getMaximizeStyleSheet() та getRestoreStyleSheet().
#ifndef STYLEHELPER_H #define STYLEHELPER_H #include <QString> class StyleHelper { public: static QString getWindowStyleSheet(); static QString getLabelStyleSheet(); static QString getCloseStyleSheet(); static QString getMaximizeStyleSheet(); static QString getRestoreStyleSheet(); static QString getMinimizeStyleSheet(); static QString getNextStyleSheet(); static QString getPreviousStyleSheet(); static QString getStopStyleSheet(); static QString getPlayStyleSheet(); static QString getPauseStyleSheet(); static QString getMenuStyleSheet(); static QString getTableViewStyleSheet(); }; #endif // STYLEHELPER_H
stylehelper.cpp
Як бачите, у методах класу описується стиль всього інтерфейсу програми, а також стилі для так званих псевдокласів, які відповідають за стан об'єкта, коли на нього натиснули, або коли курсор миші знаходиться над цим об'єктом. Для цих станів також робиться і підключення різних іконок щодо кнопок у додатку.
#include "stylehelper.h" QString StyleHelper::getWindowStyleSheet() { return "QWidget { " "background-color: #454545; " "border: 1px solid black; " "}"; } QString StyleHelper::getLabelStyleSheet() { return "QLabel { " "color: #8f8f8f; " "border: none; " "margin: 6px; " "}"; } QString StyleHelper::getCloseStyleSheet() { return "QToolButton { " "image: url(:/buttons/close-orange.png);" "background-color: #292929; " "icon-size: 12px;" "padding-left: 10px;" "padding-right: 10px;" "padding-top: 5px;" "padding-bottom: 5px;" "border: 1px solid #292929; " "}" "QToolButton:hover {" "image: url(:/buttons/close.png); " "}" "QToolButton:pressed { " "image: url(:/buttons/close.png);" "background-color: #de8e37; " "}"; } QString StyleHelper::getMaximizeStyleSheet() { return "QToolButton { " "image: url(:/buttons/window-maximize-gray.png);" "background-color: #292929;" "icon-size: 12px;" "padding-left: 10px;" "padding-right: 10px;" "padding-top: 5px;" "padding-bottom: 5px;" "border: 1px solid #292929; " "}" "QToolButton:hover {" "image: url(:/buttons/window-maximize.png); " "}" "QToolButton:pressed { " "image: url(:/buttons/window-maximize.png);" "background-color: #de8e37; " "}"; } QString StyleHelper::getRestoreStyleSheet() { return "QToolButton { " "image: url(:/buttons/window-restore-gray.png);" "background-color: #292929;" "icon-size: 12px;" "padding-left: 10px;" "padding-right: 10px;" "padding-top: 5px;" "padding-bottom: 5px;" "border: 1px solid #292929; " "}" "QToolButton:hover {" "image: url(:/buttons/window-restore.png); " "}" "QToolButton:pressed { " "image: url(:/buttons/window-restore.png);" "background-color: #de8e37; " "}"; } QString StyleHelper::getMinimizeStyleSheet() { return "QToolButton { " "image: url(:/buttons/window-minimize-gray.png);" "background-color: #292929;" "icon-size: 12px;" "padding-left: 10px;" "padding-right: 10px;" "padding-top: 5px;" "padding-bottom: 5px;" "border: 1px solid #292929; " "}" "QToolButton:hover { " "image: url(:/buttons/window-minimize.png); " "}" "QToolButton:pressed { " "image: url(:/buttons/window-minimize.png);" "background-color: #de8e37; " "}"; } QString StyleHelper::getNextStyleSheet() { return "QToolButton { " "image: url(:/buttons/skip-next.png);" "icon-size: 24px;" "padding: 6px;" "margin: 6px;" "border: none;" "}" "QToolButton:pressed { " "image: url(:/buttons/skip-next-orange.png)" "}"; } QString StyleHelper::getPreviousStyleSheet() { return "QToolButton { " "image: url(:/buttons/skip-previous.png);" "icon-size: 24px;" "padding: 6px;" "margin: 6px;" "border: none;" "}" "QToolButton:pressed { " "image: url(:/buttons/skip-previous-orange.png)" "}"; } QString StyleHelper::getStopStyleSheet() { return "QToolButton { " "image: url(:/buttons/stop.png);" "icon-size: 24px;" "padding: 6px;" "margin: 6px;" "border: none;" "}" "QToolButton:pressed { " "image: url(:/buttons/stop-orange.png)" "}"; } QString StyleHelper::getPlayStyleSheet() { return "QToolButton { " "image: url(:/buttons/play.png);" "icon-size: 48px;" "padding: 6px;" "margin: 6px;" "border: none;" "}" "QToolButton:pressed { " "image: url(:/buttons/play-orange.png)" "}"; } QString StyleHelper::getPauseStyleSheet() { return "QToolButton { " "image: url(:/buttons/pause.png);" "icon-size: 24px;" "padding: 6px;" "margin: 6px;" "border: none;" "}" "QToolButton:pressed { " "image: url(:/buttons/pause-orange.png)" "}"; } QString StyleHelper::getMenuStyleSheet() { return "QToolButton { " "color: #8f8f8f;" "background-color: #292929;" "icon-size: 12px;" "padding-left: 10px;" "padding-right: 10px;" "padding-top: 5px;" "padding-bottom: 5px;" "border: 1px solid #292929; " "}" "QToolButton:hover {" "color: white;" "}" "QToolButton:pressed { " "color: white; " "background-color: #de8e37; " "}"; } QString StyleHelper::getTableViewStyleSheet() { return "QTableView { " "background-color: white; " "color: black; " "border: 1px solid #e2e2de;" "}" "QTableView::item:selected {" "background-color: #de8e37;" "}" "QHeaderView::section:horizintal {" "background-color: white;" "border-style: none;" "color: black; " "border: 1px solid #e2e2de; " "padding: 6px; " "}"; }
Зміна розмірів та положення вікна програми
Оскільки ми відключили обрамлення вікна, то тепер реалізація цих завдань лягає на наші плечі.
віджет.h
Розглянемо заголовний файл віджету, щоб зрозуміти, що нам знадобиться для реалізації цього функціоналу.
#ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QStandardItemModel> #include <QMediaPlayer> #include <QMediaPlaylist> #include <QMouseEvent> namespace Ui { class Widget; } class Widget : public QWidget { Q_OBJECT // Свойство с точкой предыдущей позиции мыши // Относительно данной точки идёт пересчёт позиции окна // Или размеров окна. При этом свойство устанавливается при нажатии мыши // по окну и в ряде иных случаев Q_PROPERTY(QPoint previousPosition READ previousPosition WRITE setPreviousPosition NOTIFY previousPositionChanged) // тип клика мыши, при перемещении курсора по этому типу будем определять // что именно нужно сделать, перенести окно, или изменить его размер с одной из сторон enum MouseType { None = 0, Top, Bottom, Left, Right, Move }; public: explicit Widget(QWidget *parent = 0); ~Widget(); QPoint previousPosition() const; public slots: void setPreviousPosition(QPoint previousPosition); signals: void previousPositionChanged(QPoint previousPosition); private slots: void on_btn_add_clicked(); protected: void mousePressEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); private: Ui::Widget *ui; QStandardItemModel *m_playListModel; QMediaPlayer *m_player; QMediaPlaylist *m_playlist; // Переменная, от которой будем отталкиваться при работе с перемещением и изменением размера окна MouseType m_leftMouseButtonPressed; QPoint m_previousPosition; MouseType checkResizableField(QMouseEvent *event); }; #endif // WIDGET_H
widget.cpp
Для реалізації переміщення та зміни розмірів вікна програми потрібно відстежувати, де саме стався клік. Якщо ми клацнули всередині віджета з інтерфейсом (але там де немає елементів керування, які перехоплюють події кліків, наприклад, кнопки), тоді ми маємо можливість перемістити вікно програми. Якщо ж клікнули в одній із чотирьох областей для зміни розмірів, то тоді здійснюємо зміну розміру відповідно.
На малюнку нижче червоним показано області зміни розмірів, а синім виділено область для переміщення вікна програми.
А тепер розберемося з кодом, всі моменти прокоментовані.
#include "widget.h" #include "ui_widget.h" #include <QFileDialog> #include <QDir> #include <QGraphicsDropShadowEffect> #include "stylehelper.h" Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget), m_leftMouseButtonPressed(None) { ui->setupUi(this); /// Настройка UI this->setWindowFlags(Qt::FramelessWindowHint); // Отключаем оформление окна this->setAttribute(Qt::WA_TranslucentBackground); // Делаем фон главного виджета прозрачным this->setStyleSheet(StyleHelper::getWindowStyleSheet()); // Устанавливаем стиль виджета this->setMouseTracking(true); // Включаем отслеживание курсора без нажатых кнопокы // Создаём эффект тени QGraphicsDropShadowEffect *shadowEffect = new QGraphicsDropShadowEffect(this); shadowEffect->setBlurRadius(9); // Устанавливаем радиус размытия shadowEffect->setOffset(0); // Устанавливаем смещение тени ui->widgetInterface->setGraphicsEffect(shadowEffect); // Устанавливаем эффект тени на окно ui->widgetInterface->layout()->setMargin(0); // Устанавливаем размер полей ui->widgetInterface->layout()->setSpacing(0); ui->label->setText("AIMP Fake Player"); ui->label->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); // Установка стилей для всех элементов ui->currentTrack->setStyleSheet(StyleHelper::getLabelStyleSheet()); ui->label->setStyleSheet(StyleHelper::getLabelStyleSheet()); ui->btn_close->setStyleSheet(StyleHelper::getCloseStyleSheet()); ui->btn_maximize->setStyleSheet(StyleHelper::getMaximizeStyleSheet()); ui->btn_minimize->setStyleSheet(StyleHelper::getMinimizeStyleSheet()); ui->btn_next->setStyleSheet(StyleHelper::getNextStyleSheet()); ui->btn_previous->setStyleSheet(StyleHelper::getPreviousStyleSheet()); ui->btn_stop->setStyleSheet(StyleHelper::getStopStyleSheet()); ui->btn_play->setStyleSheet(StyleHelper::getPlayStyleSheet()); ui->btn_pause->setStyleSheet(StyleHelper::getPauseStyleSheet()); ui->btn_add->setStyleSheet(StyleHelper::getMenuStyleSheet()); ui->playlistView->setStyleSheet(StyleHelper::getTableViewStyleSheet()); ui->btn_add->setText(tr("Добавить")); ui->btn_next->setCursor(Qt::PointingHandCursor); ui->btn_previous->setCursor(Qt::PointingHandCursor); ui->btn_stop->setCursor(Qt::PointingHandCursor); ui->btn_play->setCursor(Qt::PointingHandCursor); ui->btn_pause->setCursor(Qt::PointingHandCursor); ui->horizontalLayout->setSpacing(6); /// /* Код из предыдущей статьи */ /// коннекты для кнопок сворачивания/максимизации/минимизации/закрытия // Сворачивание окна приложения в панель задач connect(ui->btn_minimize, &QToolButton::clicked, this, &QWidget::showMinimized); connect(ui->btn_maximize, &QToolButton::clicked, [this](){ // При нажатии на кнопку максимизации/нормализации окна // Делаем проверку на то, в каком состоянии находится окно и переключаем его режим if (this->isMaximized()) { // Заметьте, каждый раз устанавливаем новый стиль в эту кнопку ui->btn_maximize->setStyleSheet(StyleHelper::getMaximizeStyleSheet()); this->layout()->setMargin(9); this->showNormal(); } else { ui->btn_maximize->setStyleSheet(StyleHelper::getRestoreStyleSheet()); this->layout()->setMargin(0); this->showMaximized(); } }); // Закрытие окна приложения connect(ui->btn_close, &QToolButton::clicked, this, &QWidget::close); /* Код из предыдущей статьи */ } Widget::~Widget() { delete ui; delete m_playListModel; delete m_playlist; delete m_player; } QPoint Widget::previousPosition() const { return m_previousPosition; } void Widget::setPreviousPosition(QPoint previousPosition) { if (m_previousPosition == previousPosition) return; m_previousPosition = previousPosition; emit previousPositionChanged(previousPosition); } void Widget::on_btn_add_clicked() { /* Код из предыдущего урока */ } void Widget::mousePressEvent(QMouseEvent *event) { // При клике левой кнопкой мыши if (event->button() == Qt::LeftButton ) { // Определяем, в какой области произошёл клик m_leftMouseButtonPressed = checkResizableField(event); setPreviousPosition(event->pos()); // и устанавливаем позицию клика } return QWidget::mousePressEvent(event); } void Widget::mouseReleaseEvent(QMouseEvent *event) { // При отпускании левой кнопки мыши сбрасываем состояние клика if (event->button() == Qt::LeftButton) { m_leftMouseButtonPressed = None; } return QWidget::mouseReleaseEvent(event); } void Widget::mouseMoveEvent(QMouseEvent *event) { // При перемещении мыши, проверяем статус нажатия левой кнопки мыши switch (m_leftMouseButtonPressed) { case Move: { // При этом проверяем, не максимизировано ли окно if (isMaximized()) { // При перемещении из максимизированного состояния // Необходимо вернуть окно в нормальное состояние и установить стили кнопки // А также путём нехитрых вычислений пересчитать позицию окна, // чтобы оно оказалось под курсором ui->btn_maximize->setStyleSheet(StyleHelper::getMaximizeStyleSheet()); this->layout()->setMargin(9); auto part = event->screenPos().x() / width(); this->showNormal(); auto offsetX = width() * part; setGeometry(event->screenPos().x() - offsetX, 0, width(), height()); setPreviousPosition(QPoint(offsetX, event->y())); } else { // Если окно не максимизировано, то просто перемещаем его относительно // последней запомненной позиции, пока не отпустим кнопку мыши auto dx = event->x() - m_previousPosition.x(); auto dy = event->y() - m_previousPosition.y(); setGeometry(x() + dx, y() + dy, width(), height()); } break; } case Top: { // Для изменения размеров также проверяем на максимизацию // поскольку мы же не можем изменить размеры у максимизированного окна if (!isMaximized()) { auto dy = event->y() - m_previousPosition.y(); setGeometry(x(), y() + dy, width(), height() - dy); } break; } case Bottom: { if (!isMaximized()) { auto dy = event->y() - m_previousPosition.y(); setGeometry(x(), y(), width(), height() + dy); setPreviousPosition(event->pos()); } break; } case Left: { if (!isMaximized()) { auto dx = event->x() - m_previousPosition.x(); setGeometry(x() + dx, y(), width() - dx, height()); } break; } case Right: { if (!isMaximized()) { auto dx = event->x() - m_previousPosition.x(); setGeometry(x(), y(), width() + dx, height()); setPreviousPosition(event->pos()); } break; } default: // Если курсор перемещается по окну без зажатой кнопки, // то просто отслеживаем в какой области он находится // и изменяем его курсор checkResizableField(event); break; } return QWidget::mouseMoveEvent(event); } Widget::MouseType Widget::checkResizableField(QMouseEvent *event) { QPointF position = event->screenPos(); // Определяем позицию курсора на экране qreal x = this->x(); // координаты окна приложения, ... qreal y = this->y(); // ... то есть координату левого верхнего угла окна qreal width = this->width(); // А также ширину ... qreal height = this->height(); // ... и высоту окна // Определяем области, в которых может находиться курсор мыши // По ним будет определён статус клика QRectF rectTop(x + 9, y, width - 18, 7); QRectF rectBottom(x + 9, y + height - 7, width - 18, 7); QRectF rectLeft(x, y + 9, 7, height - 18); QRectF rectRight(x + width - 7, y + 9, 7, height - 18); QRectF rectInterface(x + 9, y + 9, width - 18, height - 18); // И в зависимости от области, в которой находится курсор // устанавливаем внешний вид курсора и возвращаем его статус if (rectTop.contains(position)) { setCursor(Qt::SizeVerCursor); return Top; } else if (rectBottom.contains(position)) { setCursor(Qt::SizeVerCursor); return Bottom; } else if (rectLeft.contains(position)) { setCursor(Qt::SizeHorCursor); return Left; } else if (rectRight.contains(position)) { setCursor(Qt::SizeHorCursor); return Right; } else if (rectInterface.contains(position)){ setCursor(QCursor()); return Move; } else { setCursor(QCursor()); return None; } }
Підсумок
Таким чином, можна створити повністю кастомізований додатковий інтерфейс на Qt. Залишається тільки вирішити, а чи це Вам потрібно. Якщо вас не особливо напружує системне обрамлення вікна програми, то, можливо, і не варто сильно морочитися з цим, але якщо все-таки дуже хочеться зробити з власним зовнішнім виглядом навіть кнопки закриття вікна програми, то врахуйте, що наведеного в цій статті коду явно недостатньо. щоб покрити всі нюанси роботи вікна програми. Наприклад, тут немає зміни розміру одночасно за шириною та висотою. А також періодично не зовсім коректно змінюється зовнішній вигляд курсору під час переміщення. Хоча, гадаю, що основна ідея кастомізації тепер ясна.
Завантажити кастомізований Qt Аудіо плеєр у стилі AIMP
Как сделать так, что бы только когда верхнюю полосу зажимаешь, то перетаскивалось окно и что бы оно оставалось на месте?
В методах mousePressEvent, mouseMoveEvent и т.д. в этом же самом уроке показано, как определять области, в которых находится курсор мыши. Это реализовано для изменения размеров, в методе checkResizableField определяется область нахождения курсора. Аналогично можно реализовать область нахождения курсора в верхней полосе. И сделать перетаскивание только в этой области. Так что изучите внимательно урок и сделайте по аналогии.