Статьи данного цикла:
В статье по работе с аудио плеером в 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 Stylesheet
Как вы уже успели заметить, в куске приведённого выше кода используется класс 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; " "}"; }
Изменение размеров и положения окна приложения
Поскольку мы отключили обрамление окна приложения, то теперь реализация этих задач ложится на наши плечи.
widget.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 определяется область нахождения курсора. Аналогично можно реализовать область нахождения курсора в верхней полосе. И сделать перетаскивание только в этой области. Так что изучите внимательно урок и сделайте по аналогии.