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

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

3 ответа

Решение

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

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

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

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

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

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

скриншот примера

// https://github.com/KubaO/stackrun/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();
}

Я бы предложил выполнить модальное диалоговое окно без рамки сверху и добавить графический эффект на фоновый виджет. Это ИМХО очень гибкое и короткое решение, не затрагивающее систему событий напрямую.

Производительность может быть плохой - ее можно улучшить, вызвав 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 );

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

Другие вопросы по тегам