前言

阅读此教程需要一定的C++和JUCE基础,如果没有可以翻阅我以往的视频和文章。
核心内容为基于Slider的一种新UI交互设计(Ableton Style)。
Github原地址:https://github.com/szkkng/NumberBox
我的Bilibili频道:香芋派Taro
我的公众号:香芋派的烘焙坊
我的音频技术交流群:1136403177
我的个人微信:JazzyTaroPie

准备

在Projucer中的添加

创建一个GUI project命名为NumberBox:

添加NumberBox的.h/.cpp文件:

此时目录如下:

基本模型

在这一章,我们将设计NumberBox最基础的部分。

自定义Slider

NumberBox的本质依旧是一个Slider,通过拖拽来改变它的值,所以我们可以通过自定义juce::Slider来实现。
首先我将会展示.h/.cpp中所有的代码,然后我会在后文对其做出解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//NumberBox.h
#pragma once

#include <JuceHeader.h>

class NumberBox : public juce::Slider
{
public:
NumberBox();
~NumberBox();

void paint (juce::Graphics& g) override;
void mouseDown (const juce::MouseEvent& event) override;
void mouseUp (const juce::MouseEvent& event) override;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//NumberBox.cpp
#include "NumberBox.h"

NumberBox::NumberBox()
{
setSliderStyle (juce::Slider::LinearBarVertical);
setColour (juce::Slider::trackColourId, juce::Colours::transparentWhite);
setTextBoxIsEditable (false);
setVelocityBasedMode (true);
setVelocityModeParameters (0.5, 1, 0.09, false);
setRange (0, 100, 0.01);
setValue (50.0);
setDoubleClickReturnValue (true, 50.0);
setTextValueSuffix (" %");
setWantsKeyboardFocus (true);
onValueChange = [&]()
{
if (getValue() < 10)
setNumDecimalPlacesToDisplay(2);
else if (10 <= getValue() && getValue() < 100)
setNumDecimalPlacesToDisplay(1);
else
setNumDecimalPlacesToDisplay(0);
};
}

NumberBox::~NumberBox(){}

void NumberBox::paint (juce::Graphics& g)
{
if (hasKeyboardFocus (false))
{
auto bounds = getLocalBounds().toFloat();
auto h = bounds.getHeight();
auto w = bounds.getWidth();
auto len = juce::jmin (h, w) * 0.15f;
auto thick = len / 1.8f;

g.setColour (findColour (juce::Slider::textBoxOutlineColourId));

// Left top
g.drawLine (0.0f, 0.0f, 0.0f, len, thick);
g.drawLine (0.0f, 0.0f, len, 0.0f, thick);

// Left bottom
g.drawLine (0.0f, h, 0.0f, h - len, thick);
g.drawLine (0.0f, h, len, h, thick);

// Right top
g.drawLine (w, 0.0f, w, len, thick);
g.drawLine (w, 0.0f, w - len, 0.0f, thick);

// Right bottom
g.drawLine (w, h, w, h - len, thick);
g.drawLine (w, h, w - len, h, thick);
}
}

void NumberBox::mouseDown (const juce::MouseEvent& event)
{
juce::Slider::mouseDown (event);

setMouseCursor (juce::MouseCursor::NoCursor);
}

void NumberBox::mouseUp (const juce::MouseEvent& event)
{
juce::Slider::mouseUp (event);

juce::Desktop::getInstance().getMainMouseSource().setScreenPosition (event.source.getLastMouseDownPosition());

setMouseCursor (juce::MouseCursor::NormalCursor);
}

构造函数

在构造函数中被用到的几个函数中,下列的几个是非常重要的:

  • setSliderStyle
  • setColour
  • setVelocityBasedMode
  • setVelocityModeParameters
  • onValueChange

首先,setSlider() 是一个用来定义slider风格的成员函数,如果你把它设置成juce::Slider::LinearBarVertical,它看起来是这样的:

然而,此时表示Slider值大小的填充颜色还是会随着Slider的值变动,所以我们通过在setColour中添加juce::Colours::tranparentWhite来让它保持透明。
剩下的那些函数在我之前讲旋钮的地方有说过,不清楚的同学可以翻阅我以往的文章,这边就跳过了。

paint

通过这个函数,我们对NumberBox施加了lock-on mark,在它被focus的时候四个角上会出现这样的focus mark:

我们可以通过hasKeyboardFocus() 函数来判断NumberBox是否正在被focus,同时我们还需要使用setWantsKeyBoardFocus(true) 赋予其focus。但是至此我们还没有设计CuntomLookAndFeel,所以lock-on mark暂时不会被渲染出来。

mouseDown/mouseUP

mouseDown() 中,我们让光标在鼠标在点击下去的瞬间被隐藏,因为光是velocity mode的话直到开始拖拽鼠标才会被隐藏。
mouseUp() 中,当鼠标抬起时,光标出现,同时鼠标回到它点击时的位置,即拖拽前的位置。

MainComponent.h/.cpp

好了,现在我们已经完成了NumberBox最基础的设计,把它添加到MainComponent中。
首先在MainComponent.h中引入NumberBox:

1
2
//MainComponent.h
#include "NumberBox.h"

然后如下声明三种颜色的NumberBox对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//MainComponent.h
class MainComponent : public juce::Component
{
public:
・・・
private:
NumberBox blueBox, greenBox, yellowBox;

juce::Colour blue = juce::Colour::fromFloatRGBA (0.43f, 0.83f, 1.0f, 1.0f);
juce::Colour green = juce::Colour::fromFloatRGBA (0.34f, 0.74f, 0.66f, 1.0f);
juce::Colour yellow = juce::Colour::fromFloatRGBA (1.0f, 0.71f, 0.2f, 1.0f);
juce::Colour black = juce::Colour::fromFloatRGBA (0.08f, 0.08f, 0.08f, 1.0f);

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

最后,将MainComponent.cpp修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//MainComponent.cpp
MainComponent::MainComponent()
{
setSize (500, 300);
setWantsKeyboardFocus (true);

blueBox.setColour (juce::Slider::textBoxTextColourId, blue);
blueBox.setColour (juce::Slider::textBoxOutlineColourId, blue);

greenBox.setColour (juce::Slider::textBoxTextColourId, green);
greenBox.setColour (juce::Slider::textBoxOutlineColourId, green);

yellowBox.setColour (juce::Slider::textBoxTextColourId, yellow);
yellowBox.setColour (juce::Slider::textBoxOutlineColourId, yellow);

addAndMakeVisible (blueBox);
addAndMakeVisible (greenBox);
addAndMakeVisible (yellowBox);
}

MainComponent::~MainComponent()
{
}

void MainComponent::paint (juce::Graphics& g)
{
g.fillAll (black);
}

void MainComponent::resized()
{
auto bounds = getLocalBounds().withSizeKeepingCentre (80, 30);

blueBox.setBounds (bounds.withX (50));
greenBox.setBounds (bounds.withX (205));
yellowBox.setBounds (bounds.withX (360));
}

编译运行

CustomLookAndFeel

在这一章中,我们将改变caret的颜色,同时让focus mark出现。

Customizing LookAndFeel

在NumberBox.h的最上方声明并override这两个函数 createCaretComponent() 和**createSliderTextBox()**:

1
2
3
4
5
6
7
//NumberBox.h
class CustomLookAndFeel : public juce::LookAndFeel_V4
{
public:
juce::CaretComponent* createCaretComponent (juce::Component* keyFocusOwner) override;
juce::Label* createSliderTextBox (juce::Slider& slider) override;
};

然后在NumberBox class中声明CustomLookAndFeel class对象:

1
2
3
4
5
6
7
8
9
//NumberBox.h
class NumberBox : public juce::Slider, public juce::KeyListener
{
public:
・・・
private:
CustomLookAndFeel customLookAndFeel;
・・・
};

上面两个override的函数的额定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//NumberBox.cpp
juce::CaretComponent* CustomLookAndFeel::createCaretComponent (juce::Component* keyFocusOwner)
{
auto caret = new juce::CaretComponent (keyFocusOwner);

caret->setColour (juce::CaretComponent::caretColourId, keyFocusOwner->findColour (juce::Label::textColourId));

return caret;
}

juce::Label* CustomLookAndFeel::createSliderTextBox (juce::Slider& slider)
{
auto* l = new juce::Label();

l->setJustificationType (juce::Justification::centred);
l->setColour (juce::Label::textColourId, slider.findColour (juce::Slider::textBoxTextColourId));
l->setColour (juce::Label::textWhenEditingColourId, slider.findColour (juce::Slider::textBoxTextColourId));
l->setColour (juce::Label::outlineWhenEditingColourId, juce::Colours::transparentWhite);
l->setFont (18);

return l;
}

createCaretComponent() 函数中,我们将caret的颜色设置成与juce::Label::textColourId相同的颜色,而juce::Label::textColourId又是被设定跟createSliderTextBox()juce::Slider::textBoxTextColourId相同的颜色。因此,你能够通过在NumberBox处改变颜色就能改变caret的颜色。
然后调用setLookAndFeel() 函数来将CustomLookAndFeel应用到NumberBox中:

1
2
3
4
5
6
7
8
9
10
11
//NumberBox.cpp
NumberBox::NumberBox()
{
setLookAndFeel (&customLookAndFeel);
・・・
}

NumberBox::~NumberBox()
{
setLookAndFeel (nullptr);
}

编译运行

编辑模式

在最后一章中,我们将对NumberBox加入一个新特性,就是可编辑性。你可以点击后输入你想要的值,然后按下回车更新数据。

Customizing Label

当juce::TextEditor可编辑时,juce::Lebal将显示它,所以我们需要改变一些TextEditor的设置。
同时,当juce::Label监测到有输入并显示juce::TextEditor时,最先输入的那个字符将被当成显示TextEditor的开关,所以它的值不会被输入进去,所以这个问题也是我们需要解决的。
基于以上,NumberBox.h/.cpp如下:

1
2
3
4
5
6
7
8
9
//NumberBox.h
class CustomLabel : public juce::Label
{
public:
static juce::String initialPressedKey;

juce::TextEditor* createEditorComponent() override;
void editorShown (juce::TextEditor* editor) override;
};
1
2
3
4
5
6
7
//NumberBox.h
class CustomLookAndFeel : public juce::LookAndFeel_V4
{
public:
・・・
CustomLabel* createSliderTextBox (juce::Slider& slider) override;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//NumberBox.cpp
juce::String CustomLabel::initialPressedKey = "";

juce::TextEditor* CustomLabel::createEditorComponent()
{
auto* ed = juce::Label::createEditorComponent();

ed->setJustification (juce::Justification::centred);
ed->setColour (juce::TextEditor::backgroundColourId, juce::Colours::transparentWhite);
ed->setInputRestrictions (5, "0123456789.");
ed->setIndents (4, -1);

return ed;
}

void CustomLabel::editorShown (juce::TextEditor* editor)
{
editor->clear();
editor->setText (initialPressedKey);
}
1
2
3
4
5
6
//NumberBox.cpp
CustomLabel* CustomLookAndFeel::createSliderTextBox (juce::Slider& slider)
{
auto* l = new CustomLabel();
・・・
}

关键点在于createSliderTextBox的返回值已经从juce::Label转变为CustomLabel。通过这样,现在customized label被用于绘制NumberBox。

Overring keyPressed

接下来重写**keyPressed()**,它将在得到focus和有输入时被调用。

1
2
3
4
5
6
//NumberBox.h
class NumberBox : public juce::Slider
{
public:
・・・
bool keyPressed (const juce::KeyPress& k) override;
1
2
3
4
5
6
7
8
9
10
11
12
13
//NumberBox.cpp
bool NumberBox::keyPressed (const juce::KeyPress& k)
{
if ('0' <= k.getKeyCode() && k.getKeyCode() <= '9')
{
CustomLabel::initialPressedKey = juce::String::charToString (k.getTextCharacter());
showTextBox();

return true;
}

return false;
}

当一个0~9的数字被输入时,text editor就会被渲染出来,同时带有你输入的数字。
为了让NumberBox能够得到keyboard focus,把setWantsKeyBoardFocus() 设置为true:

1
2
3
4
5
6
//NumberBox.cpp
NumberBox::NumberBox()
{
・・・
setWantsKeyboardFocus (true);
}

编译运行

总结

这篇教程解释了如何自定义NumberBox的UI和功能。如果有不明白的地方,欢迎反馈!