Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dark transparent layer over a QMainWindow in Qt

I need to implement a "Loading..." window in my application but I do prefer to cover the whole QMainWindow with a dark transparent layer with a text above. Does anybody know how to do that? I am not sure how to overlap widgets/layouts in Qt. Any help will be appreciated.

like image 636
Didac Perez Parera Avatar asked Oct 14 '13 14:10

Didac Perez Parera


2 Answers

This answer is in a series of my overlay-related answers: first, second, third.

The most trivial solution is to simply add a child transparent widget to QMainWindow. That widget must merely track the size of its parent window. It is important to properly handle changes of widget parentage, and the z-order with siblings. Below is a correct example of how to do it.

If you want to stack overlays, subsequent overlays should be the children of OverlayWidget, in the z-order. If they were to be siblings of the OverlayWidget, their stacking order is undefined.

This solution has the benefit of providing minimal coupling to other code. It doesn't require any knowledge from the widget you apply the overlay to. You can apply the overlay to a QMainWindow or any other widget, the widget can also be in a layout.

Reimplementing QMainWindow's paint event would not be considered the best design. It makes it tied to a particular class. If you really think that a QWidget instance is too much overhead, you better had measurements to show that being the case.

It is possible, of course, to make the overlay merely a QObject and to put the painting code into an event filter. That'd be an alternative solution. It's harder to do since you have to also properly deal with the parent widget's Qt::WA_StaticContents attribute, and with the widget potentially calling its scroll() method. Dealing with a separate widget is the simplest.

screenshot of the example

// 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();
}
like image 76
Kuba hasn't forgotten Monica Avatar answered Oct 23 '22 21:10

Kuba hasn't forgotten Monica


I would suggest execute a modal, frameless dialog on top and add a graphics effect on the background widget. This is IMHO a very flexible and short solution without touching the event system directly.

The performance might be bad - one could improve that by calling drawSource(), but I haven't a reliable solution here yet.

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 );
like image 8
Zacharias Avatar answered Oct 23 '22 21:10

Zacharias