Evgenii Legotckoi
Evgenii LegotckoiJuly 2, 2016, 10:13 a.m.

Qt/C++ - Lesson 052. Customization Qt Audio player in the style of AIMP

Articles in this series:

The article on the audio player in Qt we learned how to play the audio tracks and switch between them in the playlist.

How about to customize the look of the player so that he looked like, for example, AIMP? Just for comparison, look at the original and AIMP appearance of the player after customization.


The main problem of such customization Qt applications is that Qt has no means to customize the frame of the application window. That is, in the case of Windows, you must use for decorating windows WinAPI, and in case of unix and linux systems use the appropriate API decorators windows.

The only thing that can make Qt - it's completely disable the window frame. Then you need to write a completely decorating the windows by means of the Qt, without using the operating system's capabilities, write the logic of the collapse buttons, and maximize the window, as well as the logic of moving and changing the application window is resized. Well and, accordingly, to paint all styles of interface elements using the mechanism QSS StyleSheet. An alternative design is QPalette, but not always occurs correctly processing styles as opposed to the application of QSS Stylesheet.

Disabling the frame of the application window

To correctly handle the application's interface, you'll need:

  • to disable the window frame;
  • make transparent the main widget of the window in which the entire interface;
  • and interface elements themselves placed inside another widget, which will be located inside the main and at the same time is not transparent;
  • also need to make to create a shadow around the widget interface to emulate the shadow of the application window, as is done in conventional windows and that those windows have AIMP.

Before turning off the window frame and the installation of the main widget in transparency mode in the Graphics Designer interface itself some changes were made.

As you can see the tree of elements, there is a main widget, which should be transparent, as well as the widget GUI. The area enclosed between two rectangular border highlighted in red - is the visible part of the main widget. It will be located on the shadow of the application window, as well as an area that will be used to change the size of the application window.

To turn off the frame of the window you have to set a flag Qt::FramelessWindowHint . To install the widget's background in a completely transparent mode you must set the attribute Qt::WA_TranslucentBackground .

The installation of the shade should be done not on the very basic widget, and the widget that contains the application interface. To create a shadow effect is used QGraphicsFropShadowEffect object class.

All this is done in the class constructor Widget, which is responsible for the application window.

/// Настройка UI
this->setWindowFlags(Qt::FramelessWindowHint);      // Disable window decoration
this->setAttribute(Qt::WA_TranslucentBackground);   // Making the background transparent main widget
this->setStyleSheet(StyleHelper::getWindowStyleSheet());    // Set the widget style

// We create a shadow effect
QGraphicsDropShadowEffect *shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setBlurRadius(9); // Set the blur radius
shadowEffect->setOffset(0);     // Set the shadow offset
ui->widgetInterface->setGraphicsEffect(shadowEffect);   // Set the shadow effect on the box
ui->widgetInterface->layout()->setMargin(0);            // Set the margins
ui->widgetInterface->layout()->setSpacing(0);           // Set the size of the space between the elements in the placement of the widget

QSS Stylesheet

As you have already noticed, in a piece of code resulted above used StyleHelper class, which is a static getWindowStyleSheet() method. This class is a complementary and returns QString , describing the style of interface design for the widget. These methods also do the installation icon buttons of resource files, some of these icons is shown below.

QSS styles are made on the basis of CSS styles in the web-imposition. They are responsible for exactly the same parameters. For example, padding, margin, etc.

To implement the design of the application window used StyleHelper class with the following contents.

stylehelper.h

I draw your attention that the class uses two methods, with the styles that are applied to one and the same element (button maximization / normalization of the application window size) under certain conditions. These methods getMaximizeStyleSheet() and 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

As you can see, the methods described in the class style of an entire application interface, as well as the styles of the so-called pseudo-responsible for the state of the object when it was clicked or when the mouse cursor is over the object. For these states, and the connection is also made of different icons, buttons with regard to the application.

#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; "
           "}";
}

Changing the size and position of the application window

As we turned off the frame of the application window, it is now the implementation of these tasks falls on our shoulders.

widget.h

Consider the widget header file to understand that we need to implement this functionality.

#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
    // Properties with the previous position of the mouse
    // Regarding this point there is a Central window position
    // Or window size. This property is set when you press the mouse
    // on the window, and in some other cases
    Q_PROPERTY(QPoint previousPosition READ previousPosition WRITE setPreviousPosition NOTIFY previousPositionChanged)

    // type of mouse clicks, we will determine if you move the cursor on the type
    // what needs to be done to move the window or resize it with one of the parties
    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;

    // Variable, from which will be repelled when working with moving and resizing the window
    MouseType m_leftMouseButtonPressed;
    QPoint m_previousPosition;

    MouseType checkResizableField(QMouseEvent *event);
};

#endif // WIDGET_H

widget.cpp

To implement the move and change the size of the application window, you need to keep track of exactly where there was a click. If we clicked inside the widget interface (but where there is no control that intercepts click events, such as a button), then we are able to move the application window. If you clicked on one of the four areas to resize, then we carry out the resizing respectively.

The figure below shows the red area to resize, and blue highlighted area to move the application window.

And now we shall understand with the code, all the points in it commented.

#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);      // Disable window decoration
    this->setAttribute(Qt::WA_TranslucentBackground);   // Making the background transparent main widget
    this->setStyleSheet(StyleHelper::getWindowStyleSheet());    // Set the widget style
    this->setMouseTracking(true);   // Turning on cursor tracking without pressing buttons

    // We create a shadow effect
    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);

    // Setting styles for all elements
    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);
    ///
    /* The code from the previous article */

    /// The connects to minimize / maximize / minimize / close buttons
    // Minimize the application window to the taskbar
    connect(ui->btn_minimize, &QToolButton::clicked, this, &QWidget::showMinimized);
    connect(ui->btn_maximize, &QToolButton::clicked, [this](){
        // When you click on the maximization / normalization window
        // Do check out the condition of the window and switch its mode
        if (this->isMaximized()) {
            // Note, each time setting a new style in the button
            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();
        }
    });
    // Closing the application window
    connect(ui->btn_close, &QToolButton::clicked, this, &QWidget::close);
    /* The code from the previous article */
}

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()
{
    /* The code from the previous article */
}

void Widget::mousePressEvent(QMouseEvent *event)
{
    // Clicking the left mouse button
    if (event->button() == Qt::LeftButton ) {
        // Determine which area there was a click
        m_leftMouseButtonPressed = checkResizableField(event);
        setPreviousPosition(event->pos()); // and set the click position
    }
    return QWidget::mousePressEvent(event);
}

void Widget::mouseReleaseEvent(QMouseEvent *event)
{
    // When you release the left mouse button click reset state
    if (event->button() == Qt::LeftButton) {
        m_leftMouseButtonPressed = None;
    }
    return QWidget::mouseReleaseEvent(event);
}

void Widget::mouseMoveEvent(QMouseEvent *event)
{
    // When you move the mouse, check the status of pressing the left mouse button
    switch (m_leftMouseButtonPressed) {
    case Move: {
        // At the same time we check to see if the window is maximized
        if (isMaximized()) {
            // When moving from maximized state
            // It is necessary to return the window to its normal state and set button styles
            // And by simple calculations to recalculate the position of the window,
            // that it was under the cursor
            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 {
            // If the window is not maximized, then just move it relative 
            // to the last stored position until you release the mouse button
            auto dx = event->x() - m_previousPosition.x();
            auto dy = event->y() - m_previousPosition.y();
            setGeometry(x() + dx, y() + dy, width(), height());
        }
        break;
    }
    case Top: {
        // To change the dimensions and check on maximizing
        // because we can not change the dimensions in a maximized window
        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:
        // If the cursor is moved over the window without pressing the button,
        // easy to keep track of it is in what area
        // and change the cursor
        checkResizableField(event);
        break;
    }
    return QWidget::mouseMoveEvent(event);
}

Widget::MouseType Widget::checkResizableField(QMouseEvent *event)
{
    QPointF position = event->screenPos();  // Determine the position of the cursor on the screen
    qreal x = this->x();                    // coordinates of the application window, ...
    qreal y = this->y();                    // ... ie coordinate upper-left corner of the window
    qreal width = this->width();            // Also, the width ...
    qreal height = this->height();          // ... and height of the window

    // We determine the areas in which may be the mouse pointer
    // On them will be determined by clicking the status
    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);

    // And depending on the area in which the cursor is located
    // We set the appearance of the cursor and return its status
    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;
    }
}

Conclusion

Thus, it is possible to create a fully customized application interface in Qt. It remains only to decide, and if it is necessary for you. If you are not particularly straining the system frame of the application window, it is possible and does not cost much to bother with it, but if you still really want to do with their own appearance even close button of the application window, note that the figures shown in this code, the article is not enough, to cover all the nuances of the application window. For example, there is no change in the size of both the width and height. And occasionally not entirely correct changes the appearance of the cursor when you move. However, I think that the basic idea of ​​customization is now clear.

Download Qt customized audio player in the style of AIMP

Video

We recommend hosting TIMEWEB
We recommend hosting TIMEWEB
Stable hosting, on which the social network EVILEG is located. For projects on Django we recommend VDS hosting.

Do you like it? Share on social networks!

t
  • May 21, 2017, 3:32 p.m.

Как сделать так, что бы только когда верхнюю полосу зажимаешь, то перетаскивалось окно и что бы оно оставалось на месте?

Evgenii Legotckoi
  • May 24, 2017, 3:28 a.m.

В методах mousePressEvent, mouseMoveEvent и т.д. в этом же самом уроке показано, как определять области, в которых находится курсор мыши. Это реализовано для изменения размеров, в методе checkResizableField определяется область нахождения курсора. Аналогично можно реализовать область нахождения курсора в верхней полосе. И сделать перетаскивание только в этой области. Так что изучите внимательно урок и сделайте по аналогии.

Comments

Only authorized users can post comments.
Please, Log in or Sign up
AD

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:50points,
  • Rating points-4
m

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:80points,
  • Rating points4
m

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:20points,
  • Rating points-10
Last comments
ИМ
Игорь МаксимовNov. 22, 2024, 10:51 p.m.
Django - Tutorial 017. Customize the login page to Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii LegotckoiNov. 1, 2024, 12:37 a.m.
Django - Lesson 064. How to write a Python Markdown extension Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZEOct. 19, 2024, 6:19 p.m.
Fb3 file reader on Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь МаксимовOct. 5, 2024, 5:51 p.m.
Django - Lesson 064. How to write a Python Markdown extension Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas5July 5, 2024, 9:02 p.m.
QML - Lesson 016. SQLite database and the working with it in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Now discuss on the forum
m
moogoNov. 22, 2024, 6:17 p.m.
Mosquito Spray System Effective Mosquito Systems for Backyard | Eco-Friendly Misting Control Device & Repellent Spray - Moogo ; Upgrade your backyard with our mosquito-repellent device! Our misters conce…
Evgenii Legotckoi
Evgenii LegotckoiJune 25, 2024, 1:11 a.m.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey1Nov. 15, 2024, 5:04 p.m.
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProjectJune 4, 2022, 1:49 p.m.
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…

Follow us in social networks