2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[JUCE][MacOS]JUCEのNSViewComponentをフロントに持ってくる方法

Last updated at Posted at 2019-10-23

#前置き
JUCEでマルチプラットフォーム・音楽分析アプリケーションを開発し始めて、早一年半。
主に音楽学者や作曲家向けに、高度な信号処理アルゴリズムや主成分分析、SQLなどを使った大量のデータのソーティング機能を、より身近でお手軽に使えるワークプレースを提供する目的で、ソフトウェアの開発を行っています。

プロジェクトページです。
IRiMaS (Interactive Research in Music as Sound)

音楽学(Musicology)の中でも、Ethnographyという分野では、体系的な楽譜や文章による記録手法が確立されていない民族音楽を、ビデオや画像をふんだんに使用して分析・説明する、文化人類学的なアプローチをとる必要があります。この場合、単なる動画再生ソフトだけでなく、動画上にコメントや画像、図形などを設置し、さらに再生速度などを自由にコントロールする、いわゆる簡単な動画編集機能が必要になります。

そんなのフリーの動画編集でできるよ!って言っても、複数のソフトウェアを同時に起動して作業するのは嫌だ!ってことで、音楽学に必要なあらゆる機能が一つになったソフトウェアの需要は近年高まりつつあるようです。

#本題
そこで、JUCEで動画プレイヤーを実装するために、VideoComponentを使用します。
しかしながら、VideoComponentは実装されたOSによって、OS準拠の動画プレイヤーが実装されます。
例えばMacの場合はQuickTime Playerが、NSViewに貼り付けられた上で、JUCE準拠のComponentに貼り付けられます。

ここで起こる問題が、Heavy-Weight(ヘビー君)とLight-Weight(ライトちゃん) Componentの問題です。

VideoComponentに限らず、OpenGL、Web系のComponentは全てヘビー君であり、ライトちゃんであるJuceのComponentと同じレベルで使用すると、ヘビー君が常に、あらゆるライトちゃんよりも前面に表示される、という問題が起こります。

Heavy_Light.png

例えばこんな議論

JUCEでは、OpenGLContextに限っては、OpenGLContextの背面に隠れたライトちゃんを、OpenGLエンジンでレンダリングするという力技で、前面にライトちゃんを表示できるようにしています。

この記事では以下の二つの解決策をMacOS限定で提案しています。

・VieoComponentはどうする?
・複数のヘビー君を貼り付けた時、ヘビー君同士のZ-Orderはどう変更する?

#JUCE LibraryのNSViewComponentをハックする

せっかくマルチプラットフォームで開発しているのに、結局OS別に対応せざるを得ない結論に。
もしベターな方法を知っている人がいましたら、是非ご教示ください。よろしくお願いします。

###手順1 Juce::ComponentにOpenGLContextを実装する
ヘビー君と対等に対峙できるのは、ヘビー君だけです。
ライトちゃんをヘビー君にするには、OpenGLContextを使用します。
これは非常に簡単にできます。
Juce::Componentを継承したクラスのメンバに以下を追加。

Component.cpp
OpenGLContext openGLContext;

そして、コンストラクトに

Component.cpp
this->openGLContext.attachTo(*this);

これでOpenGLContextに、Juce::Componentが追加されました。

かならずディコンストラクタに以下を書きます。

Component.cpp
this->openGLContext.detach();

こうすることで、Juce::Componentもヘビー君に変わりました。

###手順2 ヘビー同士のZ-Orderを変更する
ヘビー君同士は、作られた順番に前面に表示され、後から順番を変更することはできません。なので、これだけでは実用的ではありません。
ここから、NSViewComponentハックになります。皆さん、バックアップをとって、先に進んでください。また、この記事では最低限の非常にラフな実装を紹介しています。

では、JUCE Libraryにダイブしましょう。

1・OpenGLContextのattachTo(Component& component)にアクセス

OpenGLContext.cpp
void OpenGLContext::attachTo (Component& component)
{
    component.repaint();

    if (getTargetComponent() != &component)
    {
        detach();
        attachment.reset (new Attachment (*this, component));
    }
}

ここから、ひたすらcomponentの終着点まで掘り進めていきます。

Attachmentクラスのコンストラクタ。

Attachment.cpp
 Attachment (OpenGLContext& c, Component& comp)
       : ComponentMovementWatcher (&comp), context (c)
    {
        if (canBeAttached (comp))
            attach();
    }

ここのattach()にジャンプします。

Attachment.cpp
void attach()
    {
        auto& comp = *getComponent();
        auto* newCachedImage = new CachedImage (context, comp,
                                                context.openGLPixelFormat,
                                                context.contextToShareWith);
        comp.setCachedComponentImage (newCachedImage);

        start();
    }

次は、CachedImageクラスです。
ちなみに、getComponent()はOpenGLContextから渡された、ライトちゃんであるComponentクラスのポインタを返しています。

CachedImage.cpp
CachedImage (OpenGLContext& c, Component& comp,
                 const OpenGLPixelFormat& pixFormat, void* contextToShare)
        : ThreadPoolJob ("OpenGL Rendering"),
          context (c), component (comp)
    {
        nativeContext.reset (new NativeContext (component, pixFormat, contextToShare,
                                                c.useMultisampling, c.versionRequired));

        if (nativeContext->createdOk())
            context.nativeContext = nativeContext.get();
        else
            nativeContext.reset();
    }

次はNativeContextクラス!

NativeContext.cpp
NativeContext (Component& component,
                   const OpenGLPixelFormat& pixFormat,
                   void* contextToShare,
                   bool shouldUseMultisampling,
                   OpenGLVersion version) : owner(component) // keitaro
    {
        NSOpenGLPixelFormatAttribute attribs[64] = { 0 };
        createAttribs (attribs, version, pixFormat, shouldUseMultisampling);

        NSOpenGLPixelFormat* format = [[NSOpenGLPixelFormat alloc] initWithAttributes: attribs];

        static MouseForwardingNSOpenGLViewClass cls;
        view = [cls.createInstance() initWithFrame: NSMakeRect (0, 0, 100.0f, 100.0f)
                                       pixelFormat: format];

        if ([view respondsToSelector: @selector (setWantsBestResolutionOpenGLSurface:)])
            [view setWantsBestResolutionOpenGLSurface: YES];

        [[NSNotificationCenter defaultCenter] addObserver: view
                                                 selector: @selector (_surfaceNeedsUpdate:)
                                                     name: NSViewGlobalFrameDidChangeNotification
                                                   object: view];

        renderContext = [[[NSOpenGLContext alloc] initWithFormat: format
                                                    shareContext: (NSOpenGLContext*) contextToShare] autorelease];

        [view setOpenGLContext: renderContext];
        [format release];
        
        viewAttachment = NSViewComponent::attachViewToComponent (component, view);
    }

やっとNSViewComponentが出現しました。ダンジョン奥地で伝説のポケモンに遭遇した気分。ですが、さらに奥に進みます。
上のコードの、

NativeContext.cpp
        viewAttachment = NSViewComponent::attachViewToComponent (component, view);

この部分です。

NSViewComponent.cpp
ReferenceCountedObject* NSViewComponent::attachViewToComponent (Component& comp, void* view)
{
    return new NSViewAttachment ((NSView*) view, comp);
}

ここで、NSViewComponentからNSViewAttachment型のデータがが返されています。NSViewAttachmentにアクセス。

NSViewAttachment.cpp
NSViewAttachment (NSView* v, Component& comp)
        : ComponentMovementWatcher (&comp),
          view (v), owner (comp),
          currentPeer (nullptr)
    {
        [view retain];
        [view setPostsFrameChangedNotifications: YES];
        updateAlpha();

        if (owner.isShowing())
            componentPeerChanged();

        attachViewWatcher (view);
    }

やっとNSViewにたどり着きました。ここの、componentPeerChanged()を見てみます。

NSViewAttachment.cpp
void componentPeerChanged() override
    {
        auto* peer = owner.getPeer();

        if (currentPeer != peer)
        {
            currentPeer = peer;

            if (peer != nullptr)
            {
                auto peerView = (NSView*) peer->getNativeHandle();
                [peerView addSubview: view];
                componentMovedOrResized (false, false);
            }
            else
            {
                removeFromParent();
            }
        }

        [view setHidden: ! owner.isShowing()];
    }

ここで受け取ったライトちゃんComonentを、NSViewに貼り付けているのがわかります。

NSViewAttachment.cpp
if (peer != nullptr)
            {
                auto peerView = (NSView*) peer->getNativeHandle();
                [peerView addSubview: view];
                componentMovedOrResized (false, false);
            }

特定のNSViewを前面に持ってくる為には、以下の処理が必要です。

NSViewAttachment.cpp
if (peer != nullptr)
            {
                auto peerView = (NSView*) peer->getNativeHandle();
                [view retain];
                [view removeFromSuperview];
                [peerView addSubview:view];
                [view release];
            }

上の処理は、NSViewAttachmentの中に書くのではなく、一つ前のクラスNativeContextの中に書きます。NativeContextのクラス中の適当な箇所に、以下のメンバとメソッドを追加します。

NativeContext.cpp

  //NSViewAttachmentと同じ要領でownerを追加
  //ownerはNativeContextのコンストラクタで初期化する owner(component)
    Component& owner; 

    void bringViewToFront()
    {
    
        auto* peer = owner.getPeer();
        currentPeer = peer;

        //NSViewを貼り直すことでZ-Indexを更新
        if (peer != nullptr)
        {
            auto peerView = (NSView*) peer->getNativeHandle();
            [view retain];
            [view removeFromSuperview];
            [peerView addSubview:view];
            [view release];
        }
        else
        {
            std::cout << "removeFromParent\n";
        }

        [view setHidden: ! owner.isShowing()];

    }

あとは、このNativeContextのbringViewToFront()メソッドをOpenGLContextから呼び出せるようにするだけです。どんどん遡りながら、public メソッドを追加していきましょう。。。

CachedImage.cpp

void bringViewToFront()
    {
        nativeContext->bringViewToFront();
    }
Attachment.cpp

void bringViewToFront()
    {
        auto& comp = *getComponent();
        auto* cach = static_cast<CachedImage*>(comp.getCachedComponentImage());
        cach->bringViewToFront();
    }
OpenGLContext.hpp
void bringViewToFront();
OpenGLContext.cpp
void OpenGLContext:: bringViewToFront()
{
    if(attachment.get() != nullptr)
    {
        attachment->bringViewToFront();
    }
}

最後に、OpenGLContextを実装したライトちゃんに、以下のメソッドを追加

LightComponent.cpp
    void bringViewToFront() { this->openGLContext. bringViewToFront(); }

これで、ライトちゃんを前面に持って来たい時は、このメソッドを呼び出せばOKです。

これまでは、メニューバーよりもヘビー君が前面に来て、こんな訳の分からない状態になっていたのが。。。
Screenshot 2019-10-23 at 12.31.45.png

以下の様に!!

Heavy_Light2.png

ただし、MacOSでのみ有効は解決策です。

#パフォーマンスは?

今の所、問題は出ていません。カクることもなく、問題なく動いています。
ただ一点、Windowサイズを手動で連続的に変更すると、メニューバーのアイコンがカクカクします。これは、OpenGLのレンダリングタイミングと、Componentのサイズ変更のタイミングにずれが生じているのが原因?と考えています。

これについては、以下の様な議論がありますが、解決策は示されていません。
Resizing stuttering on OpenGL rendered UI

誰か良い方法がありましたら、教えてください。よろしくお願いします。。。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?