VST3 and juce::HWNDComponent

Hi,

I’ve been playing around with the juce::HWNDComponent, and while I don’t have much experience with the Win32 SDK, it was quite easy to make a HWND visible in a standalone plugin.

This was also the case with VST3 when the VST3 is first opened, but whenever I would try to reopen the pluginwindow, the HWND would not show up anymore. The juce::HWNDComponent object lives in the PluginProcessor, so it stays alive after the PluginEditor is destroyed, and adds itself to the pluginprocessor when the PluginEditor is created again.

In pseudocode:

// Processor
class Processor
{
public:
    Processor() {
        hwnd.setHwnd(customHwnd);
    }

    juce::HWNDComponent hwnd;
}

// Editor
class Editor
{

     Editor(Processor& p) {
          addAndMakeVisible(p.hwnd);
     }

     void setBounds() {  p.hwnd.setBounds(getLocalBounds()); }
}

Isn’t this supposed to work? Does anyone have an idea how to debug this?

The reason I’m looking into this is because I need to display a Win32 HWND from another framework/application inside the juce::PluginEditor. The Mac NSView counterpart does not seem to have this issue.

The framework I’m trying to integrate offers a way to obtain the HWND handle, but it does not expose any way to recreate a view after its been destroyed, so it would make sense to keep the HWND alive after the PluginEditor is destroyed.

I’ve taken a look into why this happens and created solutions which would be able to fix this. If you agree with these suggested changes I could open a PR, if that would be helpfull. Thanks for reading!

Why does this happen?

There are two places where the native HWND view is deleted

1 juce::HWNDComponent

the juce::HWNDComponent, which displays a native Win32 HWND, automatically deletes the HWND when it is destroyed. This means destroying the PluginEditor view also destroys the borrowed native view. There is no way to destroy the HWNDComponent without destroying the HWND handle.

// in juce::HWNDComponent
~Pimpl() override
{
	removeFromParent();
	DestroyWindow (hwnd);
}

2 juce::~HWNDComponentPeer

The JuceVST3Editor, before its destroyed, invokes removeFromDesktop on the PluginEditor component,

// in juce_VST3_Wrapper.cpp
tresult PLUGIN_API removed() override
        {
            if (component != nullptr)
            {
               #if JUCE_WINDOWS
                component->removeFromDesktop();

which deletes a HWNDComponentPeer, which in turn leads to a DestroyWindow Windows SDK call.

The DestroyWindow SDK function deletes all child HWNDs associated with the HWND to delete. The native HWND is added as a child inside the juce::HWNDComponent, which makes sense, but this means that it will be automatically deleted upon this DestroyWindow call. Thus before we even enter the destructor of the PluginEditor, the native HWND inside the juce::HWNDComponent is already marked as deleted after the removeFromDesktop call.

Proposed solutions:

1 juce::HWNDComponent

Add a flag for the juce::HWNDComponent, which makes sure it does not automatically delete the displayed HWND upon destruction. This is a very easy, low risk code change, which would be nice to have in general. The juce::HWNDComponent’s Pimpl destructor would look like this:

~Pimpl() override
{
	removeFromParent();
	if (ownsWindow)
	{
		DestroyWindow (hwnd);
	}
}

2 juce::~HWNDComponentPeer

As the removeFromDesktop call currently deletes all child windows associated with the PluginEditor, I think would make sense to give JUCE developers a hook/option before this removeFromDesktop() method is called. The simplest solution would be to add a virtual function to the juce::Component which is called whenever removeFromDesktop is invoked, but there are lots of options here.

I’ve locally added a virtual function to juce::Component called removingFromDesktop(), which I then override to remove the native HWND view as a child of the PluginEditor HWND, so that the DestroyWindow call does not delete the native HWND.

void Component::removeFromDesktop()
{
    // if component methods are being called from threads other than the message
    // thread, you'll need to use a MessageManagerLock object to make sure it's thread-safe.
    JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED_OR_OFFSCREEN

	// NEW: invoke removingFromDesktop, everytime this function is called
    removingFromDesktop();
    
    if (flags.hasHeavyweightPeerFlag)
    {
        
        if (auto* handler = getAccessibilityHandler())
            notifyAccessibilityEventInternal (*handler, InternalAccessibilityEvent::windowClosed);

        ComponentHelpers::releaseAllCachedImageResources (*this);

        auto* peer = ComponentPeer::getPeerFor (this);
        jassert (peer != nullptr);

        flags.hasHeavyweightPeerFlag = false;
        delete peer;

        Desktop::getInstance().removeDesktopComponent (this);
    }
}
1 Like