3

I'm writing a desktop application on Windows 10 that does something similar to "share your screen/app window", and I've got the classic problem of trying to highlight the region of interest on the screen. So I need to draw a bright and thick rectangle, have the rectangle always be visible and 'on top', and have no interference with user input, mouse movement, etc. (i.e. all pass-through).

I can't make it work properly with Qt v5.7. I either get an opaque window (I can't see what's "below" it) with the right border, or a transparent window with only a black 1-pixel border.

I vaguely know that if I were to use Windows-specific APIs, I could create a window with WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVE style etc. (using something like a "chrome window" - example in C# here), but besides the fact that I haven't tried yet (and need to do in C++ and not C#), I'd rather do it with Qt is possible.

Case A I'm still a novice with Qt, but I thought that using a QFrame was the way to go, so I wrote some code:

//
// A- Displays a completely transparent rectangle with a black 1-pixel border.
//
QFrame  *frame = new QFrame();
frame->setFrameStyle(QFrame::Box | QFrame::Plain);
frame->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowTransparentForInput | Qt::WindowDoesNotAcceptFocus | Qt::WindowStaysOnTopHint);
frame->setAttribute(Qt::WA_TranslucentBackground, true);
frame->setGeometry(1000,500,600,300);    // Just some fixed values to test
frame->show();

That gives me this, a rectangle with a black 1-pixel thick border:

It's great, in that the rectangle stays on top of everything else, is transparent, pass-through for mouse input etc., cannot get focus, be resized or moved, and doesn't show up on the task bar.

ChromeWindowCaseA

Case B I thought the only problem left was to draw a thick bright border, so I coded this just before the call to frame->show():

// Set a solid green thick border.
frame->setObjectName("testframe");
frame->setStyleSheet("#testframe {border: 5px solid green;}");

.. but that gave me exactly, er, nothing. The QFrame was not showing at all!

Case C So as a test, I removed the setting of Qt::WA_TranslucentBackground. Here's the code:

//
// C- Displays an opaque pass-through window with a green 5-pixel border.
//
QFrame  *frame = new QFrame();
frame->setFrameStyle(QFrame::Box | QFrame::Plain);
frame->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowTransparentForInput | Qt::WindowDoesNotAcceptFocus | Qt::WindowStaysOnTopHint);
// Disabled Qt::WA_TranslucentBackground
//frame->setAttribute(Qt::WA_TranslucentBackground, true);
frame->setGeometry(1000,500,600,300);    // Just some fixed values to test
// Set a solid green thick border.
frame->setObjectName("testframe");
frame->setStyleSheet("#testframe {border: 5px solid green;}");
frame->show();

ChromeWindowCaseC

That window is still completely pass-through, stay-on-top, etc. and has the right border, but of course it's opaque.

Now, the fact that:

  • with Qt::WA_TranslucentBackground set and without changing the style sheet, I get a black 1-pixel border/frame;
  • with Qt::WA_TranslucentBackground set and with changing the style sheet, I don't get a border/frame at all (the window becomes invisible);
  • without Qt::WA_TranslucentBackground set and with changing the style sheet, I get the expected border/frame;

.. seems to imply that there's some style set for getting the black 1-pixel outside border when the window is transparent. When I changed the border style myself, this stays fully inside the window frame, and thus disappears when the window is transparent (case B) - I think.

Does anybody know what the right style sheet settings should be so that I get the window fully transparent with its 5-pixel green border?

Also, I couldn't find any documentation that tells me exactly what styles could apply to what type of widget/window (and QFrame in particular, for my test case). Does that exist anywhere? It would be handy to know, as I'd also like to use gradient colours for the border, and possibly other effects.

PHYL
  • 101
  • 2
  • 10
  • `WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVE` -- confirmed to work and it's fairly straightforward. If the abstraction layer (Qt) makes certain things too hard, it might be better to use Win32 API directly to get things done. Even more so if your app is for Windows only. – zett42 Apr 27 '17 at 19:03
  • I agree, but I'm trying Qt first (never mind my app being currently only for Windows) because it's used extensively where I work, and the rest of my app is Qt (UI and program control). There may be a simple fix to my problem, but if not, then I'll fall back on using CreateWindowEx() etc., although it looks like the code would be longer :-). – PHYL Apr 27 '17 at 21:47
  • You might want to take a look at using a parentless [`QRubberBand`](http://doc.qt.io/qt-5/qrubberband.html). Might have to use [`QWidget::setWindowOpacity`](http://doc.qt.io/qt-5/qwidget.html#windowOpacity-prop) but I think it'll give you something usable. – G.M. Apr 28 '17 at 11:06

2 Answers2

1

Try replacing the QFrame in your code with something like...

class rubber_band: public QRubberBand {
  using super = QRubberBand;
public:
  template<typename... Types>
  explicit rubber_band (const Types &... args)
    : super(args...)
    {
      setAttribute(Qt::WA_TranslucentBackground, true);
    }
protected:
  virtual void paintEvent (QPaintEvent *event) override
    {
      QPainter painter(this);
      QPen pen(Qt::green);
      pen.setWidth(10);
      painter.setPen(pen);
      painter.drawLine(rect().topLeft(), rect().topRight());
      painter.drawLine(rect().topRight(), rect().bottomRight());
      painter.drawLine(rect().bottomRight(), rect().bottomLeft());
      painter.drawLine(rect().bottomLeft(), rect().topLeft());
    }
};

An instance of the above rubber_band class should display as a green border with completely transparent body. Use as...

rubber_band *frame = new rubber_band(QRubberBand::Rectangle);
frame->setGeometry(1000, 500, 600, 300);
frame->show();

(Note: On platforms using X11 the above will require that a composite manager such as xcompmgr be running. That's usually not an issue.)

G.M.
  • 12,232
  • 2
  • 15
  • 18
  • An interesting suggestion, @G.M., thank you, and it almost worked ;-). I actually had a solution working this morning (my time), before you posted your comment and then your solution - I'll post it in a moment. I said the QRubberBand (which I didn't know about; but there's still lots I don't know about in Qt) _almost_ worked because there seems to be no way to dissociate the opacity of the border with that of the inner rectangle - unless it's possible to do it with a style sheet setting. I'll edit your solution with my findings. – PHYL Apr 28 '17 at 15:03
1

I found a solution, which is to:

  • set a mask on the QFrame to exclude the inside of the frame - so that becomes completely transparent.
  • not set Qt::WA_TranslucentBackground (otherwise nothing is visible, as per case B in the question).
  • I've also set the opacity to 0.5 so that the border that gets rendered is partly transparent.

The final code is:

//
// D- Displays a completely transparent pass-through window with a green 5-pixel translucent border.
//
QFrame  *frame = new QFrame();
frame->setFrameStyle(QFrame::Box | QFrame::Plain);
frame->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowTransparentForInput | Qt::WindowDoesNotAcceptFocus | Qt::WindowStaysOnTopHint);
frame->setGeometry(1000,500,600,300);    // Just some fixed values to test
// Set a solid green thick border.
frame->setObjectName("testframe");
frame->setStyleSheet("#testframe {border: 5px solid green;}");
// IMPORTANT: A QRegion's coordinates are relative to the widget it's used in. This is not documented.
QRegion wholeFrameRegion(0,0,600,300);
QRegion innerFrameRegion = wholeFrameRegion.subtracted(QRegion(5,5,590,290));
frame->setMask(innerFrameRegion);
frame->setWindowOpacity(0.5);
frame->show();

What we end up with is this:

ChromeWindowCaseD

I worried somewhat that all this masking would lead to a not very optimal solution, but I realised afterwards that the C# example using Windows APIs actually does something very similar (a rectangle within a rectangle to exclude the region to stay transparent).. so maybe this is the right way to go.

PHYL
  • 101
  • 2
  • 10
  • Sorry for not replying sooner but... as you've discovered for yourself using `QWidget::setMask` is the more/most general solution and should work in all "reasonable" circumstances. Note though that calling `QWidget::setWindowOpacity` also works with the `QRubberBand` solution but is still subject to the `X11` caveats pointed out elsewhere. Having said all that.... if it works for you then that's all that matters -- ship it! Subject to testing of course :-) – G.M. Apr 28 '17 at 17:36
  • No problem. Did you notice that I posted an edit to your solution with my findings trying out the `QRubberBand` solution? I did that edit just before posting my solution, which I had worked out before you posted yours. Most important finding, I think, was the effect of `setWindowOpacity()` indeed - my edit is waiting for "peer review" (probably yours I guess). PS: actual implementation is ongoing - testing will of course be done :-D. – PHYL Apr 28 '17 at 21:50
  • there is a bit problem with this, if i draw such border around a video play window, it flashes. how to resolve this? – Tony Apr 13 '18 at 03:11
  • Hi Tony. I had the `QFrame` around live video, and it was nice and steady. If your border flashes, it's either because it gets wiped and re-drawn (check your code to see if that could happen), or there is some interference from the environment (OS, other apps). Check the first case first to make sure/fix it. – PHYL Apr 14 '18 at 10:21