Search code examples
c++qtwindows-10-desktop

Drawing a highlighting border around a region of the screen in Qt5 on Windows 10


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.


Solution

  • 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.