MOD-D3のUIをJUCEで再現する① : TwoValueなスライダー

概要

※ 英語ですがこちらの記事の方が最新です。よりシンプルな実装方法が見つかったのでまとめ直しています。

今回からsuzuki kentaro氏のMOD-D3をJUCEで作成することにチャレンジします。

こちらがMOD-D3です。先日v.1.0が無料でリリースされました。

全てを完璧に再現するとなると、結構な時間とボリュームになるので、記事をいくつかに分けてまとめていきます。

まずは下記スライダー部分の再現からチャレンジしてみようと思います。

ところが、スライダー部分だけでもかなりのボリュームだったので、当記事で再現するのは一旦以下のところまでとします。

それでは、新規プロジェクトの作成から始め、上記目標物を完成させるまでの手順と要点を解説していきます。

環境

  • macOS Big Sur 11.2.3
  • JUCE v6.0.8

新規プロジェクトの作成

今回は下記のようにModD3という名前で新規プロジェクトを作成することにします。

スライダーの作成

まず第一段階として、基本的なスライダーを作成します。

Sliderオブジェクトの宣言

今回は三つのスライダーを作成するので、Sliderオブジェクトを三つ用意します。

class ModD3AudioProcessorEditor  : public juce::AudioProcessorEditor
{
・・・
private:
    juce::Slider modSlider1;
    juce::Slider modSlider2;
    juce::Slider modSlider3;
    
    ModD3AudioProcessor& audioProcessor;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ModD3AudioProcessorEditor)
};

スライダーの詳細設定

作成したSliderオブジェクトに対して、様々なメンバ関数を呼び出すことで詳細設定を行うことができます。

とりあえず下記メンバ関数を呼び出すことにします。

ModD3AudioProcessorEditor::ModD3AudioProcessorEditor (ModD3AudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p)
{
    setSize (200, 300);
    
    addAndMakeVisible(&modSlider1);
    modSlider1.setSliderStyle(juce::Slider::TwoValueHorizontal);
    modSlider1.setTextBoxStyle(juce::Slider::NoTextBox, true, 100, 20);
    modSlider1.setRange(0.0, 100.0, 0.1);
    modSlider1.setMinValue(0.0);
    modSlider1.setMaxValue(100.0);
        
    addAndMakeVisible(&modSlider2);
    modSlider2.setSliderStyle(juce::Slider::TwoValueHorizontal);
    modSlider2.setTextBoxStyle(juce::Slider::NoTextBox, true, 100, 20);
    modSlider2.setRange(0.0, 100.0, 0.1);
    modSlider2.setMinValue(0.0);
    modSlider2.setMaxValue(100.0);
            
    addAndMakeVisible(&modSlider3);
    modSlider3.setSliderStyle(juce::Slider::TwoValueHorizontal);
    modSlider3.setTextBoxStyle(juce::Slider::NoTextBox, true, 100, 20);
    modSlider3.setRange(0.0, 100.0, 0.1);
    modSlider3.setMinValue(0.0);
    modSlider3.setMaxValue(100.0);
}

それぞれのメンバ関数の説明は以下の通りです。

メンバ関数説明
addAndMakeVisible()コンポーネントの可視化。
setSliderStyle()スライダーの種類を指定。
TwoValueHorizontalは二つのThumbを持つ横方向スライダー。
setTextBoxStyle()テキストボックスの種類やサイズ等を指定。
NoTextBoxはテキストボックスの非表示。
setMinValue()TwoValueHorizontalなスライダー用の最小値を設定。
setMaxValue()TwoValueHorizontalなスライダー用の最大値を設定。
setRange()値の範囲を設定。

スライダーの配置&大きさ

次にウィンドウの生成&サイズ変更時に実行される、メンバ関数resize()内の実装をします。

void ModD3AudioProcessorEditor::resized()
{
    modSlider1.setBounds(5, getHeight() / 2 - 90, getWidth() - 10, 30);
    modSlider2.setBounds(5, getHeight() / 2, getWidth() - 10, 30);
    modSlider3.setBounds(5, getHeight() / 2 + 90, getWidth() - 10, 30);
}

setBounds()の前半二つの引数はそれぞれx座標とy座標の値です。後半の二つの引数は、コンポーネントの横幅と縦幅の値です。この場合だと、スライダーの横幅と縦幅のことになります。

また、上記ようにgetHeight()getWidth()を使用してあげると、リサイズ時にいい感じに対応してくれます。この場合だと、getHeight()はウィンドウの縦幅、getWidthはウィンドウの横幅を取得します。

第一段階スライダーの完成

以上で最低限のスライダーの実装は完了しました。ビルドして現時点の様子を確認します。

当然のことですが、いかにもデフォルトっていう感じがします。

ラベルの作成

今回なぜラベルを作成するかというと、スライダーの値を表示させるためです。

TwoValueHorizontalなスライダーの場合、setTextBoxStyle()によって生成されるテキストボックスと相性が悪いように思いました。

なので、二つのThumbにそれぞれ対応したラベルを作成し、そこにスライダーの値を表示させます。

とりあえずこの章ではラベルの作成まで行い、次章でラベルとスライダーを連携させます。

Labelオブジェクトの宣言

Sliderオブジェクトの宣言時と同じように、下記クラス内でLabelオブジェクトを宣言します。

class ModD3AudioProcessorEditor  : public juce::AudioProcessorEditor
{
・・・
private:
    juce::Slider modSlider1;
    juce::Slider modSlider2;
    juce::Slider modSlider3;
    
    juce::Label leftLabelModSlider1;
    juce::Label rightLabelModSlider1;
    juce::Label leftLabelModSlider2;
    juce::Label rightLabelModSlider2;
    juce::Label leftLabelModSlider3;
    juce::Label rightLabelModSlider3;
    
    ModD3AudioProcessor& audioProcessor;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ModD3AudioProcessorEditor)
};

TwoValueHorizontalなスライダーのため、一つのスライダーに二つのラベルを用意します。

ラベルの可視化

addAndMakeVisible()でラベルを描写するように設定します。

ModD3AudioProcessorEditor::ModD3AudioProcessorEditor (ModD3AudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p)
{
・・・        
    addAndMakeVisible(leftLabelModSlider1);
    addAndMakeVisible(rightLabelModSlider1);
    addAndMakeVisible(leftLabelModSlider2);
    addAndMakeVisible(rightLabelModSlider2);
    addAndMakeVisible(leftLabelModSlider3);
    addAndMakeVisible(rightLabelModSlider3);
}

ラベルの配置と大きさ

ラベルの配置は、それぞれのスライダーの横幅や縦幅を取得し、その値に微調整する形で設定します。

void ModD3AudioProcessorEditor::resized()
{
    modSlider1.setBounds(5, getHeight() / 2 - 90, getWidth() - 10, 30);
    modSlider2.setBounds(5, getHeight() / 2, getWidth() - 10, 30);
    modSlider3.setBounds(5, getHeight() / 2 + 90, getWidth() - 10, 30);
    
    leftLabelModSlider1.setBounds(modSlider1.getWidth() / 2 - 65, modSlider1.getY() + 30, 60, 10);
    rightLabelModSlider1.setBounds(modSlider1.getWidth() / 2 + 15, modSlider1.getY() + 30, 60, 10);
    leftLabelModSlider2.setBounds(modSlider2.getWidth() / 2 - 65, modSlider2.getY() + 30, 60, 10);
    rightLabelModSlider2.setBounds(modSlider2.getWidth() / 2 + 15, modSlider2.getY() + 30, 60, 10);
    leftLabelModSlider3.setBounds(modSlider3.getWidth() / 2 - 65, modSlider3.getY() + 30, 60, 10);
    rightLabelModSlider3.setBounds(modSlider3.getWidth() / 2 + 15, modSlider3.getY() + 30, 60, 10);
}

また、ラベルの大きさ、特に縦幅はギリギリに設定しました。なぜかというと、スライダーに近い位置に配置するためです。もし縦幅が大きいと、スライダーと被ってしまい、その部分だけスライダーを動かせなくなってしまいます。

とりあえずラベルの最低限の実装は終わりです。現時点では、”何をラベルに表示するのか”を設定していないため、当然のことながらビルドしてもラベルは表示されません。

スライダーとラベルの連携

それではスライダーの値がラベルに表示されるように実装します。

リスナークラスの継承

まずはスライダー用のリスナークラスを継承させ、そのクラスで定義されているsliderValueChanged()をオーバライドします。

class ModD3AudioProcessorEditor  : public juce::AudioProcessorEditor, private juce::Slider::Listener
{
public:
・・・    
    void sliderValueChanged (juce::Slider *slider) override;

このメンバ関数は、スライダーの値が変更された時に呼び出される関数です。

次にaddListener()を呼び出し、各スライダーにリスナーを追加します。

ModD3AudioProcessorEditor::ModD3AudioProcessorEditor (ModD3AudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p)
{
    setSize (200, 300);
    
    addAndMakeVisible(&modSlider1);
    modSlider1.setSliderStyle(juce::Slider::TwoValueHorizontal);
    modSlider1.setTextBoxStyle(juce::Slider::NoTextBox, true, 100, 20);
    modSlider1.setRange(0.0, 100.0, 0.1);
    modSlider1.setMinValue(0.0);
    modSlider1.setMaxValue(100.0);
    modSlider1.addListener(this);
        
    addAndMakeVisible(&modSlider2);
    modSlider2.setSliderStyle(juce::Slider::TwoValueHorizontal);
    modSlider2.setTextBoxStyle(juce::Slider::NoTextBox, true, 100, 20);
    modSlider2.setRange(0.0, 100.0, 0.1);
    modSlider2.setMinValue(0.0);
    modSlider2.setMaxValue(100.0);
    modSlider2.addListener(this);
            
    addAndMakeVisible(&modSlider3);
    modSlider3.setSliderStyle(juce::Slider::TwoValueHorizontal);
    modSlider3.setTextBoxStyle(juce::Slider::NoTextBox, true, 100, 20);
    modSlider3.setRange(0.0, 100.0, 0.1);
    modSlider3.setMinValue(0.0);
    modSlider3.setMaxValue(100.0);
    modSlider3.addListener(this);
        
    addAndMakeVisible(leftLabelModSlider1);
    addAndMakeVisible(rightLabelModSlider1);
    addAndMakeVisible(leftLabelModSlider2);
    addAndMakeVisible(rightLabelModSlider2);
    addAndMakeVisible(leftLabelModSlider3);
    addAndMakeVisible(rightLabelModSlider3);
}

こうしてaddLister()にthisポインタを指定することで、各スライダーは「お、今スライダー動かしたな」と気づけるようになり、sliderValueChanged()が実行されます。

スライダーの値をラベルに表示させる

下記のように、それぞれのスライダーのif文内に処理を実装します。

void ModD3AudioProcessorEditor::sliderValueChanged(juce::Slider *slider)
{
    if (slider == &modSlider1)
    {
        leftLabelModSlider1.setText(modSlider1.getTextFromValue(modSlider1.getMinValue()), juce::dontSendNotification);
        rightLabelModSlider1.setText(modSlider1.getTextFromValue(modSlider1.getMaxValue()), juce::dontSendNotification);
    }
    
    if (slider == &modSlider2)
    {
        leftLabelModSlider2.setText(modSlider2.getTextFromValue(modSlider2.getMinValue()), juce::dontSendNotification);
        rightLabelModSlider2.setText(modSlider2.getTextFromValue(modSlider2.getMaxValue()), juce::dontSendNotification);
    }
    
    if (slider == &modSlider3)
    {
        leftLabelModSlider3.setText(modSlider3.getTextFromValue(modSlider3.getMinValue()), juce::dontSendNotification);
        rightLabelModSlider3.setText(modSlider3.getTextFromValue(modSlider3.getMaxValue()), juce::dontSendNotification);
    }
}

もしif文を用いないとどうなるかというと、modSlider1の値を動かしただけで、他のmodSliderの値の表示も変更されてしまいます。

if文の中身は、Labelオブジェクトに対して、メンバ関数setText()を呼び出し、何を表示させるかの設定をしています。

その”何を”の部分がgetTextFromValue()が返す値です。この関数は引数に渡されたValue型の値をString型の文字列に変換してくれる便利なものです。今回の場合だと、Value型であるSliderオブジェクトの最大値や最小値をString型に変換してくれます。

以上の実装で、各スライダーの各Thumbの値がそれぞれのラベルに表示されるようになります。処理の流れをまとめると以下のようになります。

  1. スライダーのThumbを動かす
  2. sliderValueChanged()が実行される
  3. スライダーの値を取得しラベルに値を表示する

二つのThumbを独立させる

スライダーの値がラベルに表示されるようになりましたが、ここで一つ課題があります。

下記のように一方のThumbがもう一方のThumbを乗り越えようとすると、追従してしまいます。

この解決策に悩みに悩んだ結果、思いついたのは下記のコメントアウトです。これは禁じ手です。

void setMinValue (double newValue, NotificationType notification, bool allowNudgingOfOtherValues)
    {
        // The minimum value only applies to sliders that are in two- or three-value mode.
        jassert (style == TwoValueHorizontal || style == TwoValueVertical
                  || style == ThreeValueHorizontal || style == ThreeValueVertical);

//        newValue = constrainedValue (newValue);
//
//        if (style == TwoValueHorizontal || style == TwoValueVertical)
//        {
//            if (allowNudgingOfOtherValues && newValue > static_cast<double> (valueMax.getValue()))
//                setMaxValue (newValue, notification, false);
//
//            newValue = jmin (static_cast<double> (valueMax.getValue()), newValue);
//        }
//        else
//        {
//            if (allowNudgingOfOtherValues && newValue > lastCurrentValue)
//                setValue (newValue, notification);
//
//            newValue = jmin (lastCurrentValue, newValue);
//        }

        if (lastValueMin != newValue)
        {
            lastValueMin = newValue;
            valueMin = newValue;
            owner.repaint();
            updatePopupDisplay (newValue);

            triggerChangeMessage (notification);
        }
    }
void setMaxValue (double newValue, NotificationType notification, bool allowNudgingOfOtherValues)
    {
        // The maximum value only applies to sliders that are in two- or three-value mode.
        jassert (style == TwoValueHorizontal || style == TwoValueVertical
                  || style == ThreeValueHorizontal || style == ThreeValueVertical);

//        newValue = constrainedValue (newValue);
//
//        if (style == TwoValueHorizontal || style == TwoValueVertical)
//        {
//            if (allowNudgingOfOtherValues && newValue < static_cast<double> (valueMin.getValue()))
//                setMinValue (newValue, notification, false);
//
//            newValue = jmax (static_cast<double> (valueMin.getValue()), newValue);
//        }
//        else
//        {
//            if (allowNudgingOfOtherValues && newValue < lastCurrentValue)
//                setValue (newValue, notification);
//
//            newValue = jmax (lastCurrentValue, newValue);
//        }

        if (lastValueMax != newValue)
        {
            lastValueMax = newValue;
            valueMax = newValue;
            owner.repaint();
            updatePopupDisplay (valueMax.getValue());

            triggerChangeMessage (notification);
        }
    }

なぜ禁じ手かというと、JUCEのスライダーの実装基盤だからです。このファイルを変更してしまうと、以前作成したもの、あるいは今後作成するものにも引き継がれてしまいます。JUCEでは自分が何か特別に実装したいと思ったものは、オーバライドをするのが基本です。

自分がまだ未熟ということもあり、この方法しか思いついきませんでした。もし安全な方法があれば教えていただけると幸いです。

結果、追従してしまう現象を解消することができました。

スライダーの値に単位をつける

もっと早い段階でやっておけばよかった設定ですが、つい忘れていました。下記のようにsetTextValueSuffix()を呼び出すことで、値の後部に任意の文字列を追加することができます。

ModD3AudioProcessorEditor::ModD3AudioProcessorEditor (ModD3AudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p)
{
    setSize (200, 300);
    
    addAndMakeVisible(&modSlider1);
    modSlider1.setSliderStyle(juce::Slider::TwoValueHorizontal);
    modSlider1.setTextBoxStyle(juce::Slider::NoTextBox, true, 100, 20);
    modSlider1.setRange(0.0, 100.0, 0.1);
    modSlider1.setMinValue(0.0);
    modSlider1.setMaxValue(100.0);
    modSlider1.setTextValueSuffix(" %");
    modSlider1.addListener(this);
        
    addAndMakeVisible(&modSlider2);
    modSlider2.setSliderStyle(juce::Slider::TwoValueHorizontal);
    modSlider2.setTextBoxStyle(juce::Slider::NoTextBox, true, 100, 20);
    modSlider2.setRange(0.0, 100.0, 0.1);
    modSlider2.setMinValue(0.0);
    modSlider2.setMaxValue(100.0);
    modSlider2.setTextValueSuffix(" %");
    modSlider2.addListener(this);
            
    addAndMakeVisible(&modSlider3);
    modSlider3.setSliderStyle(juce::Slider::TwoValueHorizontal);
    modSlider3.setTextBoxStyle(juce::Slider::NoTextBox, true, 100, 20);
    modSlider3.setRange(0.0, 100.0, 0.1);
    modSlider3.setMinValue(0.0);
    modSlider3.setMaxValue(100.0);
    modSlider3.setTextValueSuffix(" %");
    modSlider3.addListener(this);
        
    addAndMakeVisible(leftLabelModSlider1);
    addAndMakeVisible(rightLabelModSlider1);
    addAndMakeVisible(leftLabelModSlider2);
    addAndMakeVisible(rightLabelModSlider2);
    addAndMakeVisible(leftLabelModSlider3);
    addAndMakeVisible(rightLabelModSlider3);
}

第二段階スライダーの完成

以上でラベルの実装は完了し、ビルドをすると下記のようなスライダーが出来上がります。

LookAndFeelのカスタマイズ

ここからはスライダーの外観をカスタマイズしていきます。JUCEにはLookAndFeelというデフォルトの外観設定があります。

もし自分でカスタマイズしたければ、この後紹介するように、LookAndFeelクラスを継承した自作クラスを作成します。

ビフォーアフター

LookAndFeelのカスタマイズでどのように変わるのか、分かりやすく把握できるように、スライダーのビフォーアフターを用意しました。

まず下記がビフォーです。

次がアフターです。

カスタマイズ後は、下記のようにスライダーを動かしている時にマウスを消したり、外観を変更したりできるようになります。

解説は次の章でするので、この章ではLookAndFeelクラスの自作から、スライダーへの適用までの手順をサクッと一通りまとめます

MyLookAndFeelクラスの作成

カスタマイズをするといっても下記drawLinearSlider()のみをオーバーライドしたクラスを作成するだけです。LookAndFeel_V4を継承しています。

class MyLookAndFeel : public juce::LookAndFeel_V4
{
    void drawLinearSlider (juce::Graphics& g, int x, int y, int width, int height,
                                           float sliderPos,
                                           float minSliderPos,
                                           float maxSliderPos,
                                           const juce::Slider::SliderStyle style, juce::Slider& slider) override
    {
        auto trackWidth = fmin (2.0f, slider.isHorizontal() ? (float) height * 0.25f : (float) width * 0.25f);

        juce::Point<float> startPoint (slider.isHorizontal() ? (float) x : (float) x + (float) width * 0.5f,
                                 slider.isHorizontal() ? (float) y + (float) height * 0.5f : (float) (height + y));

        juce::Point<float> endPoint (slider.isHorizontal() ? (float) (width + x) : startPoint.x,
                               slider.isHorizontal() ? startPoint.y : (float) y);
        
        juce::Path backgroundTrack;
        backgroundTrack.startNewSubPath (startPoint);
        backgroundTrack.lineTo (endPoint);
        g.setColour (juce::Colours::grey.darker(1.0));
        juce::PathStrokeType pathStrokeType(0.8);
        float dashedLength[2] = {2, 4};
        pathStrokeType.createDashedStroke(backgroundTrack, backgroundTrack, dashedLength, 2);
        g.strokePath(backgroundTrack, pathStrokeType);

        juce::Path valueTrack;
        juce::Point<float> minPoint, maxPoint;

        minPoint = { slider.isHorizontal() ? minSliderPos : (float) width * 0.5f,
                     slider.isHorizontal() ? (float) height * 0.5f : minSliderPos };

        maxPoint = { slider.isHorizontal() ? maxSliderPos : (float) width * 0.5f,
                     slider.isHorizontal() ? (float) height * 0.5f : maxSliderPos };
        
        auto minThumbWidth = getSliderThumbRadius (slider) - 22;
        auto maxThumbWidth = getSliderThumbRadius (slider) - 26;

        valueTrack.startNewSubPath (minPoint);
        valueTrack.lineTo (maxPoint);
        g.setColour (juce::Colours::grey);
        g.strokePath (valueTrack, { trackWidth, juce::PathStrokeType::curved, juce::PathStrokeType::rounded });
        
        int thumbBeingDragged = slider.getThumbBeingDragged();
                
        if (thumbBeingDragged == 1 || thumbBeingDragged == 2)
        {
            slider.setMouseCursor(juce::MouseCursor::NoCursor);
            
            g.setColour (juce::Colours::white.darker(0.05));
            g.drawRoundedRectangle (juce::Rectangle<float>(static_cast<float> (thumbBeingDragged == 1 ? maxThumbWidth : minThumbWidth), static_cast<float> (thumbBeingDragged == 1 ? maxThumbWidth : minThumbWidth)).withCentre(thumbBeingDragged == 1 ? maxPoint : minPoint), 50, 1.0);
            
            g.setColour (juce::Colours::grey);
            g.fillEllipse (juce::Rectangle<float> (static_cast<float> (thumbBeingDragged == 1 ? minThumbWidth : maxThumbWidth), static_cast<float> (thumbBeingDragged == 1 ? minThumbWidth : maxThumbWidth)).withCentre (thumbBeingDragged == 1 ? minPoint : maxPoint));
        }
        else
        {
            slider.setMouseCursor(juce::MouseCursor::NormalCursor);

            g.setColour (juce::Colours::white.darker(0.05));
            g.drawRoundedRectangle (juce::Rectangle<float>(static_cast<float> (maxThumbWidth), static_cast<float> (maxThumbWidth)).withCentre(maxPoint), 50, 1.0);
            g.drawRoundedRectangle (juce::Rectangle<float>(static_cast<float> (minThumbWidth), static_cast<float> (minThumbWidth)).withCentre(minPoint), 50, 1.0);
        }
    }
};

MyLookAndFeelオブジェクトの宣言

次に下記のようにMyLookAndFeelオブジェクトを作成します。

class ModD3AudioProcessorEditor  : public juce::AudioProcessorEditor, private juce::Slider::Listener
{
・・・
private:
・・・
    MyLookAndFeel myLookAndFeel;
    
    ModD3AudioProcessor& audioProcessor;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ModD3AudioProcessorEditor)
};

スライダーにMyLookAndFeelを適用させる

このMyLookAndFeelをどのようにしてスライダーに適用させるかなのですが、setLookAndFeel()の引数にMyLookAndFeelオブジェクトのアドレスを渡してあげることで適用させます。

今回は下記コンストラクタ内で設定します。ついでにラベルの文字色もサクッと変更しておきます。

ModD3AudioProcessorEditor::ModD3AudioProcessorEditor (ModD3AudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p)
{
    setSize (200, 300);
    
    addAndMakeVisible(&modSlider1);
    modSlider1.setLookAndFeel(&myLookAndFeel);
    modSlider1.setSliderStyle(juce::Slider::TwoValueHorizontal);
    modSlider1.setTextBoxStyle(juce::Slider::NoTextBox, true, 100, 20);
    modSlider1.setMinValue(0.0);
    modSlider1.setMaxValue(100.0);
    modSlider1.setRange(0.0, 100.0, 0.1);
    modSlider1.setTextValueSuffix(" %");
    modSlider1.addListener(this);
        
    addAndMakeVisible(&modSlider2);
    modSlider2.setLookAndFeel(&myLookAndFeel);
    modSlider2.setSliderStyle(juce::Slider::TwoValueHorizontal);
    modSlider2.setTextBoxStyle(juce::Slider::NoTextBox, true, 100, 20);
    modSlider2.setMinValue(0.0);
    modSlider2.setMaxValue(100.0);
    modSlider2.setRange(0.0, 100.0, 0.1);
    modSlider2.setTextValueSuffix(" %");
    modSlider2.addListener(this);
            
    addAndMakeVisible(&modSlider3);
    modSlider3.setLookAndFeel(&myLookAndFeel);
    modSlider3.setSliderStyle(juce::Slider::TwoValueHorizontal);
    modSlider3.setTextBoxStyle(juce::Slider::NoTextBox, true, 100, 20);
    modSlider3.setMinValue(0.0);
    modSlider3.setMaxValue(100.0);
    modSlider3.setRange(0.0, 100.0, 0.1);
    modSlider3.setTextValueSuffix(" %");
    modSlider3.addListener(this);
    
        
    addAndMakeVisible(leftLabelModSlider1);
    leftLabelModSlider1.setColour(juce::Label::textColourId, juce::Colours::grey);
    
    addAndMakeVisible(rightLabelModSlider1);
    rightLabelModSlider1.setColour(juce::Label::textColourId, juce::Colours::grey);
    
    addAndMakeVisible(leftLabelModSlider2);
    leftLabelModSlider2.setColour(juce::Label::textColourId, juce::Colours::grey);
    
    addAndMakeVisible(rightLabelModSlider2);
    rightLabelModSlider2.setColour(juce::Label::textColourId, juce::Colours::grey);
    
    addAndMakeVisible(leftLabelModSlider3);
    leftLabelModSlider3.setColour(juce::Label::textColourId, juce::Colours::grey);
    
    addAndMakeVisible(rightLabelModSlider3);
    rightLabelModSlider3.setColour(juce::Label::textColourId, juce::Colours::grey);
}

また、MOD-D3の背景がスモーキーなブラック色なので、下記設定を行います。単純なblackだとあまりにも暗いので、brighter()でわずかに明るくしてあげることでスモーキーさを出します。

void ModD3AudioProcessorEditor::paint (juce::Graphics& g)
{
    g.fillAll (juce::Colours::black.brighter(0.05));
}

第三段階スライダーの完成

以上でMyLookAndFeelの実装が完了し、当記事の目標物が完成しました。次章でMyLookAndFeelクラス内部(オーバーライドしたdrawLinearSlider())の解説をしていきます。

MyLookAndFeelの解説

それではMyLookAndFeelクラス内の実装について軽く解説していきます。

drawLinearSlider()

カスタマイズしたMyLookAndFeelでオーバライドした関数はdrawLinearSlider()だけです。

この関数は直線スタイルのスライダーの外観設定を担当します。今回はこの関数を個人的にカスタマイズしたかったのでオーバライドしています。

スライダーの背景を点線にする

下記はスライダーのトラック部分を点線にする実装をしている箇所です。

     juce::Point<float> startPoint (slider.isHorizontal() ? (float) x : (float) x + (float) width * 0.5f,
                                 slider.isHorizontal() ? (float) y + (float) height * 0.5f : (float) (height + y));

        juce::Point<float> endPoint (slider.isHorizontal() ? (float) (width + x) : startPoint.x,
                               slider.isHorizontal() ? startPoint.y : (float) y);
        
        juce::Path backgroundTrack;
        backgroundTrack.startNewSubPath (startPoint);
        backgroundTrack.lineTo (endPoint);
        g.setColour (juce::Colours::grey.darker(1.0));
        juce::PathStrokeType pathStrokeType(0.8);
        float dashedLength[2] = {2, 4};
        pathStrokeType.createDashedStroke(backgroundTrack, backgroundTrack, dashedLength, 2);
        g.strokePath(backgroundTrack, pathStrokeType);

この願いを叶えることができる便利な関数はcreateDashedStroke()です。

上記のように点線の間隔や数を配列として引数に渡してあげることで、細かい点線を描くことができます。

クリック&ドラッグ時に外観を変える

今回どのように外観を変えたかというと、クリック&ドラッグしている時に灰色にThumbを塗りつぶされるようにしています。

     int thumbBeingDragged = slider.getThumbBeingDragged();
                
        if (thumbBeingDragged == 1 || thumbBeingDragged == 2)
        {
            slider.setMouseCursor(juce::MouseCursor::NoCursor);
            
            g.setColour (juce::Colours::white.darker(0.05));
            g.drawRoundedRectangle (juce::Rectangle<float>(static_cast<float> (thumbBeingDragged == 1 ? maxThumbWidth : minThumbWidth), static_cast<float> (thumbBeingDragged == 1 ? maxThumbWidth : minThumbWidth)).withCentre(thumbBeingDragged == 1 ? maxPoint : minPoint), 50, 1.0);
            
            g.setColour (juce::Colours::grey);
            g.fillEllipse (juce::Rectangle<float> (static_cast<float> (thumbBeingDragged == 1 ? minThumbWidth : maxThumbWidth), static_cast<float> (thumbBeingDragged == 1 ? minThumbWidth : maxThumbWidth)).withCentre (thumbBeingDragged == 1 ? minPoint : maxPoint));
        }
        else
        {
            slider.setMouseCursor(juce::MouseCursor::NormalCursor);

            g.setColour (juce::Colours::white.darker(0.05));
            g.drawRoundedRectangle (juce::Rectangle<float>(static_cast<float> (maxThumbWidth), static_cast<float> (maxThumbWidth)).withCentre(maxPoint), 50, 1.0);
            g.drawRoundedRectangle (juce::Rectangle<float>(static_cast<float> (minThumbWidth), static_cast<float> (minThumbWidth)).withCentre(minPoint), 50, 1.0);
        }

TwoValueHorizontalなスライダーにおいて、getThumbBeingDragged()はとても便利な関数です。

この関数は、最小値担当のThumbを動かしている時は1、最大値担当のThumbを動かしている時は2、何も動かしていない時は-1を返します。

この関数の返り値をif文で使うと、クリック&ドラッグされているThumbだけの色を変更できたりと、融通が効く設定を行うことができます。

クリック&ドラッグ時にマウスを消す

最後に解説するのは、クリック&ドラッグ時にマウスを消す方法です。

     int thumbBeingDragged = slider.getThumbBeingDragged();
                
     if (thumbBeingDragged == 1 || thumbBeingDragged == 2)
        {
            slider.setMouseCursor(juce::MouseCursor::NoCursor);
            
            g.setColour (juce::Colours::white.darker(0.05));
            g.drawRoundedRectangle (juce::Rectangle<float>(static_cast<float> (thumbBeingDragged == 1 ? maxThumbWidth : minThumbWidth), static_cast<float> (thumbBeingDragged == 1 ? maxThumbWidth : minThumbWidth)).withCentre(thumbBeingDragged == 1 ? maxPoint : minPoint), 50, 1.0);
            
            g.setColour (juce::Colours::grey);
            g.fillEllipse (juce::Rectangle<float> (static_cast<float> (thumbBeingDragged == 1 ? minThumbWidth : maxThumbWidth), static_cast<float> (thumbBeingDragged == 1 ? minThumbWidth : maxThumbWidth)).withCentre (thumbBeingDragged == 1 ? minPoint : maxPoint));
        }
        else
        {
            slider.setMouseCursor(juce::MouseCursor::NormalCursor);

            g.setColour (juce::Colours::white.darker(0.05));
            g.drawRoundedRectangle (juce::Rectangle<float>(static_cast<float> (maxThumbWidth), static_cast<float> (maxThumbWidth)).withCentre(maxPoint), 50, 1.0);
            g.drawRoundedRectangle (juce::Rectangle<float>(static_cast<float> (minThumbWidth), static_cast<float> (minThumbWidth)).withCentre(minPoint), 50, 1.0);
        }

この機能を実装するには、上記ハイライト部分のようにsetMouseCursor()を使用します。

setMouseCursor()は、引数にMouseCursorクラスで定義されている列挙体StandardCursorの列挙子を指定します。この列挙子にNoCursorというマウスを消すものや、NormalCursorという通常のマウスを表示するものがあるので、この二つをうまく使い分けています。

つまり、何もしていない時は上記if文のelseブロック内が実行されているので、通常のマウスが表示されています。また、二つのThumbどちらでもクリック&ドラッグすれば、if文本体のブロック内が実行されるので、マウスが消えます。

以上でMyLookAndFeelクラスの解説は終わりです。まだまだ未熟なため、もっと効率の良いコード書き方や誤った解釈等あれば、ブログのコメントやTwitterでぜひご指摘ください。

今後の課題

来週の記事までに実装したい機能をまとめると下記のようになります。

  • マウスホバーした時にもThumbの色を変更
  • かっこいいフォントへ変更
  • mapボタンの作成と、三つ目のThumbの表示

以上、三つの実装ができるよう来週もJUCEに取り組んでいこうと思います。

Comments

  1. okie says:

    This has been very helpful!

Copied title and URL