Custom window border with no gaps?

I want to write a stand-alone app that uses a non-native title bar. I’ve overwritten the necessary look and feel methods to draw my own windows-border, etc., but there are thin, probably 0.5px, dark lines between my main component and the border component. Also, the whole window suddenly doesn’t use the rounding of Windows 11 anymore. It’s completely square.

Ideally, the border around the window would be completely transparent and inset, not adding to the total size, so we can have a more modern look, similar to Spotify, and other modern apps.

Is that possible with JUCE?

Take a look at the standalonefilterwindow for an example of how this is done - I’ve done the same thing in my app and it allows for customizing everything you need.

1 Like

You must have misunderstood me. I’ve been using JUCE for 6+ years and know how to turn the native title bar off and thus use custom paint functions for the title bar and border.

The issue is that the corners of the window are no longer rounded on Windows 11 and that, even when using “fillAll” for the border and the main component, there is a thin black line between the border and main component.

Maybe you can share a screenshot of your app in which you fixed both issues?

1 Like

Have you tried setting a breakpoint at ResizableWindow::setBackgroundColour() and launching your application? There’s a call to Desktop::canUseSemiTransparentWindows() there which may reveal where your issue lies when going back in the stacktrace (It’s not part of the Desktop class that is causing issues iirc, but for me that’s where I started looking and it led me down the correct path).

It took me back a few steps to ResizableWindow::getBorderThickness() always returning, while not using a native title bar, a value equal or greater than 1, which was caused by the main class inheriting from documentWindow (which inherits ResizableWindow) returning that default value. Overriding that virtual method in the hosting document window to return {} resolved this for me, along with of course setting the document window background to .withAlpha(0.f)

EDIT: And for the corner I believe I just don’t paint in the document window but instead only do in the audio editor, I’d have to check when I’m next home.

Edit2: Got home, I actually just don’t paint in the document window and then reduce the clip region on the editor with a path. Could probably do that the other way around though.

With a border size of 0, your window is no longer resizable and if you don’t paint the corners, Windows 11 will cast a square shadow and the close button will still be square, thus peeking out.

This is more complex than setting a couple of attributes and colors.

That’s why I asked.

I guess you can take a look at the ms implementation Apply rounded corners in desktop apps - Windows apps | Microsoft Learn

Good luck, looks pretty straight forward.

How about this? Apologies for the eye-bleeding colors; wanted to make sure I could see any gaps.

 class MainWindow : public juce::DocumentWindow, public juce::LookAndFeel_V4
 {
    public:
        MainWindow(juce::String name)
            : DocumentWindow(name, juce::Colours::transparentBlack, 0)
        {            
            setUsingNativeTitleBar(false);
            setOpaque(false);
            setDropShadowEnabled(false);

            setContentNonOwned(&mainComponent, false);

            setResizable(true, false);
            centreWithSize(500, 500);

            setVisible(true);

            setLookAndFeel(this);
        }

        ~MainWindow()
        {
            setLookAndFeel(nullptr);
        }

        void closeButtonPressed() override
        {
            JUCEApplication::getInstance()->systemRequestedQuit();
        }

        void paint(juce::Graphics& g) override
        {
            getLookAndFeel().drawResizableWindowBorder(g, getWidth(), getHeight(), juce::BorderSize<int>{ 1 }, * this);

            getLookAndFeel().drawDocumentWindowTitleBar(*this, g,
                getWidth(), getTitleBarHeight(),
                0, getWidth(),
                nullptr, false);
        }

        void drawResizableWindowBorder(juce::Graphics& g,
            int  	w,
            int  	h,
            const juce::BorderSize< int >& border,
            juce::ResizableWindow& window
        ) override
        {
            g.setColour(juce::Colours::purple);
            g.fillRoundedRectangle(getLocalBounds().toFloat(), 10.0f);
        }

        void drawDocumentWindowTitleBar(juce::DocumentWindow&,
            juce::Graphics& g,
            int  	w,
            int  	h,
            int  	titleSpaceX,
            int  	titleSpaceW,
            const juce::Image* icon,
            bool  	drawTitleTextOnLeft) override
        {
            g.setColour(juce::Colours::black);
            g.setFont({ 20.0f, juce::Font::bold });
            g.drawText(getTitle(), 0, 0, w, h, juce::Justification::centred);
        }

        void drawCornerResizer(juce::Graphics&, int w, int h, bool isMouseOver, bool isMouseDragging) override
        {
        }

        void drawResizableFrame(juce::Graphics&, int w, int h, const juce::BorderSize<int>&) override
        {
        }

    private:
        class MainComponent : public juce::Component
        {
        public:
            MainComponent() = default;
            ~MainComponent() override = default;

            void paint(juce::Graphics& g) override
            {
                g.fillAll(juce::Colours::yellow);
            }

        private:
            JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent)
        } mainComponent;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainWindow)
   };

Matt

1 Like

Thanks. Can you try with the same color for everything? The black gap is pretty subtle, so it works best if you try to simulate a “no border” look. If you use the yellow also for the border, you will probably see the gap.

Maybe it’s also a scaling issue? I use 200% on my 4K monitor, so maybe without HIDPI scaling it looks “normal”?

Unfortunately, this also loses the shadow that Windows usually draws…

Sure, here’s an all-yellow version at 200% scaling. I used the Windows Magnifier app and zoomed in to 600% and I can’t see a gap.

Could still be a scaling issue on your end, though; I don’t have a 4k monitor.

Just tried this with setDropShadowEnabled(true). That messed up the nice round corners. Fixing that will probably require a custom DropShadower.

Matt

I think Windows 11 is drawing that specific shadow, so by turning the drop-shadow off, the app just tells the OS not to draw it.

So to maybe re-phrase my question:

Without rewriting parts of JUCE, is it possible to have a borderless, resizeable, rounded window, with the OS shadow?

I think the previous post from LightAndSoundLTD is correct; you’ll need to set the window attribute for the Desktop Window Manager. You don’t need to rewrite any part of JUCE, but you will need to add some Windows-specific code to your app to set the DWMWA_WINDOW_CORNER_PREFERENCE window attribute. Docs here:

https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwm_window_corner_preference

Setting that attribute gave me a window with rounded corners and a drop shadow.

If you don’t want the thin dark border around the window, set the DWMWA_BORDER_COLOR attribute:

I think this cleaner; I don’t have to make the window transparent and I only had to override a single look-and-feel method. You can also have a native title bar:

I still had to call setDropShadowEnabled(false) in my main window constructor to get this to work properly. As far as I can tell, setDropShadowEnabled does not enable or disable the OS shadow; instead, it sets a flag that tells the peer to create a DropShadower that paints the shadow (see LookAndFeel::createDropShadowerForComponent).

Code is below; check out parentHierachyChanged().

Matt

    class MainWindow : public juce::DocumentWindow, public juce::LookAndFeel_V4
    {
    public:
        MainWindow(juce::String name)
            : DocumentWindow(name, juce::Colours::yellow, DocumentWindow::allButtons)
        {
            setUsingNativeTitleBar(false);
            setDropShadowEnabled(false);

            setContentNonOwned(&mainComponent, false);

            setResizable(true, false);
            centreWithSize(500, 500);

            setVisible(true);

            getCurrentColourScheme().setUIColour(juce::LookAndFeel_V4::ColourScheme::widgetBackground, juce::Colours::yellow);
            Component::setColour(juce::DocumentWindow::ColourIds::textColourId, juce::Colours::black);

            setLookAndFeel(this);
        }

        ~MainWindow()
        {
            setLookAndFeel(nullptr);
        }

#if JUCE_WINDOWS
        static uint32_t colourToColorRef(juce::Colour colour)
        {
            uint32_t colorRef = colour.getBlue();
            colorRef |= (colorRef << 8) | colour.getGreen();
            return (colorRef << 8) | colour.getRed();
        }

        void parentHierarchyChanged() override
        {
            if (auto peer = getPeer(); peer != nullptr && juce::SystemStats::getOperatingSystemType() >= juce::SystemStats::Windows11)
            {
                auto windowHandle = (HWND)peer->getNativeHandle();
                auto value = DWMWCP_ROUND;
                auto hr = DwmSetWindowAttribute(windowHandle, DWMWA_WINDOW_CORNER_PREFERENCE, &value, sizeof(value));
                jassert(SUCCEEDED(hr));

                COLORREF transparentBorderColor = colourToColorRef(juce::Colours::yellow);
                hr = DwmSetWindowAttribute(windowHandle, DWMWA_BORDER_COLOR, &transparentBorderColor, sizeof(transparentBorderColor));
            }
        }
#endif

        void closeButtonPressed() override
        {
            JUCEApplication::getInstance()->systemRequestedQuit();
        }

        void drawResizableFrame(juce::Graphics& g, int w, int h, const juce::BorderSize<int>&) override
        {
        }

    private:
        class MainComponent : public juce::Component
        {
        public:
            MainComponent() = default;
            ~MainComponent() override = default;

            void paint(juce::Graphics& g) override
            {
                g.fillAll(juce::Colours::yellow);
            }

        private:
            JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent)
        } mainComponent;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainWindow)
    };

Thank you so much. Now I have rounded corners, a shadow on the desktop, and the black gap is also gone.

I had to also overwrite the look and feel method drawResizableFrame and do nothing in there. That was what removed the black border.

The black back on the side is expected. The component doesn’t extend into the border (yet)

Thanks again. I’m sooo happy :smiley:

Slick!

This was helpful for me as well; I hadn’t tried transparent windows in Direct2D mode and they don’t work properly at the moment.

Matt

1 Like