Темный прозрачный слой над QMainWindow в Qt

Мне нужно реализовать окно "Загрузка..." в моем приложении, но я предпочитаю покрывать весь QMainWindow темным прозрачным слоем с текстом выше. Кто-нибудь знает, как это сделать? Я не уверен, как перекрывать виджеты/макеты в Qt. Любая помощь будет оценена.

Ответ 1

Этот ответ находится в серии моих ответов, связанных с оверлеем: первый, второй, третий.

Самое тривиальное решение - просто добавить дочерний прозрачный виджет к QMainWindow. Этот виджет должен просто отслеживать размер его родительского окна. Важно правильно обрабатывать изменения родительского виджета и z-порядок с братьями и сестрами. Ниже приведен правильный пример того, как это сделать.

Если вы хотите складывать наложения, последующие оверлеи должны быть дочерними элементами OverlayWidget, в z-порядке. Если они должны быть братьями и сестрами OverlayWidget, их порядок укладки undefined.

Это решение имеет преимущество обеспечения минимальной связи с другим кодом. Это не требует каких-либо знаний от виджета, к которому вы применяете наложение. Вы можете применить наложение к QMainWindow или любому другому виджету, виджет также может быть в макете.

Повторное выполнение события QMainWindow paint не будет считаться лучшим дизайном. Это делает его привязанным к определенному классу. Если вы действительно думаете, что экземпляр QWidget слишком много накладных расходов, вам лучше иметь измерения, чтобы показать, что это так.

Можно, конечно, сделать наложение просто a QObject и поместить код рисования в фильтр событий. Это было бы альтернативным решением. Это сложнее сделать, так как вам также необходимо правильно обработать атрибут родительского виджета Qt::WA_StaticContents и с помощью виджета, потенциально вызывающего его метод scroll(). Работа с отдельным виджетами является самой простой.

снимок экрана примерa

// https://github.com/KubaO/stackoverflown/tree/master/questions/overlay-widget-19362455
#include <QtGui>
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
#include <QtWidgets>
#endif

class OverlayWidget : public QWidget
{
   void newParent() {
      if (!parent()) return;
      parent()->installEventFilter(this);
      raise();
   }
public:
   explicit OverlayWidget(QWidget * parent = {}) : QWidget{parent} {
      setAttribute(Qt::WA_NoSystemBackground);
      setAttribute(Qt::WA_TransparentForMouseEvents);
      newParent();
   }
protected:
   //! Catches resize and child events from the parent widget
   bool eventFilter(QObject * obj, QEvent * ev) override {
      if (obj == parent()) {
         if (ev->type() == QEvent::Resize)
            resize(static_cast<QResizeEvent*>(ev)->size());
         else if (ev->type() == QEvent::ChildAdded)
            raise();
      }
      return QWidget::eventFilter(obj, ev);
   }
   //! Tracks parent widget changes
   bool event(QEvent* ev) override {
      if (ev->type() == QEvent::ParentAboutToChange) {
         if (parent()) parent()->removeEventFilter(this);
      }
      else if (ev->type() == QEvent::ParentChange)
         newParent();
      return QWidget::event(ev);
   }
};

class LoadingOverlay : public OverlayWidget
{
public:
   LoadingOverlay(QWidget * parent = {}) : OverlayWidget{parent} {
      setAttribute(Qt::WA_TranslucentBackground);
   }
protected:
   void paintEvent(QPaintEvent *) override {
      QPainter p{this};
      p.fillRect(rect(), {100, 100, 100, 128});
      p.setPen({200, 200, 255});
      p.setFont({"arial,helvetica", 48});
      p.drawText(rect(), "Loading...", Qt::AlignHCenter | Qt::AlignVCenter);
   }
};

int main(int argc, char * argv[])
{
   QApplication a{argc, argv};
   QMainWindow window;
   QLabel central{"Hello"};
   central.setAlignment(Qt::AlignHCenter | Qt::AlignTop);
   central.setMinimumSize(400, 300);
   LoadingOverlay overlay{&central};
   QTimer::singleShot(5000, &overlay, SLOT(hide()));
   window.setCentralWidget(&central);
   window.show();
   return a.exec();
}

Ответ 2

Я бы предложил выполнить модальный, бескаркасный диалог сверху и добавить графический эффект на фоновый виджет. Это IMHO очень гибкое и короткое решение, не касаясь непосредственно системы событий.

Производительность может быть плохой - можно улучшить это, вызвав drawSource(), но у меня пока нет надежного решения.

class DarkenEffect : public QGraphicsEffect
{
public:
    void draw( QPainter* painter ) override
    {
        QPixmap pixmap;
        QPoint offset;
        if( sourceIsPixmap() ) // No point in drawing in device coordinates (pixmap will be scaled anyways)
            pixmap = sourcePixmap( Qt::LogicalCoordinates, &offset );
        else // Draw pixmap in device coordinates to avoid pixmap scaling;
        {
            pixmap = sourcePixmap( Qt::DeviceCoordinates, &offset ); 
            painter->setWorldTransform( QTransform() );
        }
        painter->setBrush( QColor( 0, 0, 0, 255 ) ); // black bg
        painter->drawRect( pixmap.rect() );
        painter->setOpacity( 0.5 );
        painter->drawPixmap( offset, pixmap );
    }
};

// prepare overlay widget 
overlayWidget->setWindowFlags( Qt::FramelessWindowHint | Qt::Dialog | Qt::WindowStaysOnTopHint );

// usage
parentWidget->setGraphicsEffect( new DarkenEffect );
overlayWidget->exec();
parentWidget->setGraphicsEffect( nullptr );

Ответ 3

Если вы говорите об использовании отдельного макета/виджета над своим "Главным окном", вы можете просто сделать окно "Загрузка..." модальным либо через редактор пользовательского интерфейса, либо в конструктор для вашего пользовательского интерфейса.