1

I worked on Windows platform. If I used native frameless window flags like:

::SetWindowLongPtr((HWND)winId(), GWL_STYLE, WS_POPUP | WS_SYSMENU | WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX);

I recieve frameless window that correctly works with default Windows windows composer - it correctly change the state when I press "WIN" key + arrows.

When I try to use Qt library with following frameless window flags:

setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | Qt::CustomizeWindowHint);

I recieve the window that doesnt responce on the "WIN" key + arrows. So it doesnt works with default Windows window composer.

Which compbination of Qt window flags would have similar behavior like native flags above?

AeroSun
  • 2,401
  • 2
  • 23
  • 46
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/203187/discussion-on-question-by-aerosun-how-to-integrate-the-qt-frameless-window-in-wi). – Samuel Liew Nov 27 '19 at 13:50

1 Answers1

4

So, indeed this has little to do with Qt, it seems. Having anything but a full frame on a Windows window seems to disable the "snap" shortcuts, even if the window can still be resized or moved with keyboard arrows (from the Alt+Space system menu).

The workaround is actually pretty simple. Basically to implement the QWidget::nativeEvent() virtual method and ignore the WM_NCCALCSIZE message.

I ran into some painting issues when snapping from one screen to another, but worked around that with a mask (notes in code comments). Would be nice to find a cleaner solution (it may qualify as a Qt bug actually).

As a bonus this also allows for rounded corners. The painting/styling is based on a rounded message box I made for another answer, and the painting code is documented more fully over there.

The system menu and all interactions with keys (move/resize/snap/etc) work. Mouse handling could be added by implementing the WM_NCHITTEST message in nativeEvent() (see references below).

Only tested on Win7, would be curious how it acts on Win10.

FramelessWidget

#include <QPainter>
#include <QPalette>
#include <QStyle>
#include <QStyleOption>
#include <QWidget>
#include <qt_windows.h>

class FramelessWidget : public QWidget
{
    Q_OBJECT
  public:
    explicit FramelessWidget(QWidget *p = nullptr, Qt::WindowFlags f = Qt::Window) :
      // the flags set here should "match" the GWL_STYLE flags set below
      QWidget(p, f | Qt::WindowMinMaxButtonsHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint | Qt::FramelessWindowHint)
    {
      setAttribute(Qt::WA_TranslucentBackground);  // for rounded corners
      // set flags which will override what Qt does, especially with the Qt::FramelessWindowHint which essentially disables WS_SIZEBOX/WS_THICKFRAME
      SetWindowLongPtr(HWND(winId()), GWL_STYLE, WS_POPUPWINDOW | WS_CAPTION | WS_SIZEBOX | WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_CLIPCHILDREN);
    }

    // these settings are only used when no styleSheet is set
    qreal radius = 0.0;        // desired radius in absolute pixels
    qreal borderWidth = -1.0;  // -1: use style hint frame width; 0: no border; > 0: use this width.

  protected:
    bool nativeEvent(const QByteArray &eventType, void *message, long *result) override
    {
      MSG *msg = static_cast<MSG*>(message);
      if (msg && msg->message == WM_NCCALCSIZE) {
        // Just return 0 and mark event as handled. This will draw the widget contents
        // into the full size of the frame, instead of leaving room for it.
        *result = 0;
        return true;
      }
      return QWidget::nativeEvent(eventType, message, result);
    }

    // Override paint event because of transparent background. 
    // Can be styled using CSS or QPalette with backgroundRole()/foregroundRole().
    void paintEvent(QPaintEvent *) override
    {
      QPainter p(this);
      p.setRenderHints(QPainter::Antialiasing);
      QStyleOption opt;
      opt.initFrom(this);
      // be sure to use the full frame size, not the default rect() which is inside frame.
      opt.rect.setSize(frameSize());  

      if (testAttribute(Qt::WA_StyleSheetTarget)) {
        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
        setMask(QRegion(opt.rect));  // see notes below
        return;
      }

      QRectF rect(opt.rect);
      qreal penWidth = borderWidth;
      if (penWidth < 0.0)
        penWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &opt, this);
      if (penWidth > 0.0) {
        p.setPen(QPen(palette().brush(foregroundRole()), penWidth));
        const qreal dlta = (penWidth * 0.5);
        rect.adjust(dlta, dlta, -dlta, -dlta);
      }
      else {
        p.setPen(Qt::NoPen);
      }
      p.setBrush(palette().brush(backgroundRole()));
      if (radius > 0.0)
        p.drawRoundedRect(rect, radius, radius, Qt::AbsoluteSize);
      else
        p.drawRect(rect);

      // Setting a mask works around an issue with artifacts when switching screens with Win+arrow
      // keys. I don't think it's the actual mask which does it, rather it triggers the region 
      // around the widget to be polished but I'm not sure. As support for my theory, the mask 
      // doesn't even have to follow the border radius.
      setMask(QRegion(opt.rect));
    }

};  // FramelessWidget

Test implementation


int main(int argc, char *argv[])
{
  QApplication a(argc, argv);
  //QLoggingCategory::setFilterRules(QStringLiteral("qt.qpa.windows = true\n"));
  FramelessWidget msgBox;
  msgBox.setWindowTitle("Frameless window test");
  msgBox.setLayout(new QVBoxLayout);
  msgBox.layout()->addWidget(new QLabel(QStringLiteral("<h3>Frameless rounded widget.</h3>"), &msgBox));
  QLabel *lbl = new QLabel(QStringLiteral(
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean fermentum erat rhoncus, "
    "scelerisque eros ac, hendrerit metus. Nunc ac lorem id tortor porttitor mollis. Nunc "
    "tristique orci vel risus convallis, non hendrerit sapien condimentum. Phasellus lorem tortor, "
    "mollis luctus efficitur id, consequat eget nulla. Nam ac magna quis elit tristique hendrerit id "
    "at erat. Integer id tortor elementum, dictum urna sed, tincidunt metus. Proin ultrices tempus "
    "lacinia. Integer sit amet fringilla nunc."
  ), &msgBox);
  lbl->setWordWrap(true);
  msgBox.layout()->addWidget(lbl);
  QPushButton *pb = new QPushButton(QStringLiteral("Close"), &msgBox);
  QObject::connect(pb, &QPushButton::clicked, qApp, &QApplication::quit);
  msgBox.layout()->addItem(new QSpacerItem(1,1, QSizePolicy::Expanding, QSizePolicy::Expanding));
  msgBox.layout()->addWidget(pb);

  msgBox.setStyleSheet(QStringLiteral(
    "FramelessWidget { "
      "border-radius: 12px; "
      "border: 3px solid palette(shadow); "
      "background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #ffeb7f, stop: 1 #d09d1e); "
    "}"
  ));

  msgBox.show();
  return a.exec();
}

enter image description here

References:

Maxim Paperno
  • 4,485
  • 2
  • 18
  • 22
  • Maxim, do you know how to add the shadow to the above frameless window. The old way - "DwmExtendFrameIntoClientArea" is not working on win10 :( – AeroSun Dec 12 '19 at 16:31
  • @AeroSun No, unfortunately... haven't had luck with the shadows. In that other answer I quote with the rounded message box, I had tried adding a QGraphicsEffect shadow, but that doesn't work (not really surprising I suppose). I haven't tried it with this version though. If there's a "native" way to force a shadow, I'm not aware of it... nor do I understand when the OS draws the shadow vs. when it doesn't. – Maxim Paperno Dec 12 '19 at 19:56
  • 1
    See https://bugreports.qt.io/browse/QTBUG-84466 for a qt patch that properly fixes the multi screen issue at least for me. – Cyriuz May 15 '21 at 00:00