HWNDComponent Bug?

I’m using the HWNDComponent in my plugin where the PluginProcessor is the actual owner of the HWND handle, and when the PluginEditor is constructed I simply take a pointer to the handle and set it on a HWNDComponent that is then addAndMakeVisible’d into the PluginEditor.

This works fine on first load, but when I close and reopen the plugin editor (Reaper, Win10), the HWNDComponent is no longer visible. I’m using the exact same approach with an NSViewComponent in macOS and do not have this issue there.

Is there something that I’m missing about using the HWNDComponent? Or is there perhaps a bug here in juce? I’m on juce 6.1.6. Thanks!

I tested this on JUCE develop, and was able to reproduce the issue, so I don’t think this behaviour has changed substantially since JUCE 6.

I think the main problem is that on Windows, DestroyWindow will automatically destroy any child windows, as well as the parent window. DestroyWindow is called when the plugin editor is closed, destroying not just the editor, but any HWNDs that it holds. I don’t think there’s an easy way to circumvent this and keep a child window alive.

You may need to rework your design a little, to create a new inner HWND each time the editor is opened.

FWIW the HWNDComponent docs do imply that the HWNDComponent takes ownership of the passed-in HWND. It doesn’t guarantee that the passed-in HWND may outlive the HWNDComponent.

Ahh ok, very helpful thanks @reuk!

I wonder, would it be possible then to try to setHWND(nullptr); just before the DestroyWindow call? I.e. trying to catch the editor just before it gets destroyed to ensure that the HWND I’m interested in is actually persisted correctly?

Edit: or perhaps I can just fork the HWNDComponent to use CloseWindow instead of DestroyWindow in the destructor. Will explore that tomorrow

Had a chance to try this today; if I remove the call to DestroyWindow and set all ShowWindow calls to SW_SHOWNA, I still see the behavior I mentioned in my original post. Am I missing something else?

When the editor is closed, the VST3 host calls removed() on the EditorView, which in turn calls removeFromDesktop() on the editor component. removeFromDesktop() deletes the component’s peer, which eventually calls DestroyWindow on the peer’s HWND. If that HWND has any children (including those held in a HWNDComponent), those will be destroyed too.

I think, in order to get this working, you would need to change the DestroyWindow call in the HWNDComponentPeer, or add some sort of ‘peer deletion’ notification so that the inner HWND can detach itself before the outer HWND is destroyed.

So I followed this up with a few failed attempts to fork juce and slice in the behavior we’re discussing above, and then I realized that the juce::WebBrowserComponent must be doing something similar. After looking through that code, it seems that the WebBrowserComponent keeps the HWND out of the parent tree of the plugin window anyways and just tries to float the window over the component as best it can. I took a quick hack at trying the same:

    class NonOwningHWNDComponent : public juce::Component
    {
    public:
        NonOwningHWNDComponent() = default;

        ~NonOwningHWNDComponent()
        {
            // Hide the window but do not destroy
            ShowWindow (hwnd, SW_HIDE);
        }

        void setHWND (void* handle)
        {
            hwnd = (HWND) handle;
            ShowWindow(hwnd, SW_SHOW);
        }

        void paint (juce::Graphics&) override {}

        void resized() override
        {
            if (auto* peer = getTopLevelComponent()->getPeer())
            {
                auto area = (peer->localToGlobal(peer->getAreaCoveredBy (*this).toFloat()) * peer->getPlatformScaleFactor()).getSmallestIntegerContainer();

                UINT flagsToSend =  SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER;

                //if (! wasMoved)   flagsToSend |= SWP_NOMOVE;
                //if (! wasResized) flagsToSend |= SWP_NOSIZE;

                //juce::ScopedThreadDPIAwarenessSetter threadDpiAwarenessSetter { hwnd };

                SetWindowPos (hwnd, HWND_TOP, area.getX(), area.getY(), area.getWidth(), area.getHeight(), 0);
            }
        }

    private:
        HWND hwnd = nullptr;
    };

This does feel slightly hacky, but it requires no change to juce itself and so far seems to solve the problem I originally posed. I need to implement the moved() callback still and tweak the activation/z-order flags, but besides that I think this is a goer. Before I get too confident though, am I setting myself up for failure going this route? Perhaps I’m missing something?

Thanks!

Edit: ah, well, maybe it’s not as close as I was hoping. It seems this approach registers the window with the desktop in a way that the user could, via the OS, just close the window from the platform toolbar (start menu bar).