GUI는 개발자가 프로그램을 개발하는데 있어서 가장 귀찮은 부분일겁니다.
이벤트 처리도 그렇고, 그래픽을 그럴듯하게 구현하는 것이 직관적이지 못하기 때문입니다.
다행히도 Juce에서는 기본적인 GUI를 지원합니다.
GUI의 종류를 각각 설명하고, 공통적인 사용법을 다루겠습니다.
1. Text
일반적인 Text는 juce::Label을 이용하면 됩니다.
juce::Label label;
label.setText("Text", juce::dontSendNotification);
label.setJustificationType(juce::Justification::centred);
label.attachToComponent(&component, false);
setText에서 두번째 parameter는 이벤트 처리를 위해 notification을 발생시킬지의 여부입니다.
notification을 보내고 싶은 경우에는 juce::sendNotification을 사용하면 됩니다.
setJustificationType은 정렬 타입입니다. centred는 가운데정렬이 되겠죠.
attachToComponent는 다른 GUI Component에 연동하여 자동으로 위치를 지정하고 싶을 때 사용하면 됩니다.
두번째 parameter는 그 Component의 왼쪽에 놓을지의 여부입니다.
false인 경우에는... Component의 위에 놓는 것 같습니다.
그 밖에, 글을 수정할 수 있게 하고 싶다면 label.setEditable(true); 를 사용하면 됩니다.
쉽죠? 넘어갑시다.
2. Button
버튼의 종류가 많군요. 여기서는 Image, Text, Toggle만 알아보겠습니다.
버튼은 공통적으로 이벤트 함수를 직접 할당해줍니다.
예를 들어 클릭 이벤트는 다음과 같이 할당해주면 됩니다.
button.onClick = [this] () {
//do something
};
TextButton은 말그대로 텍스트 버튼입니다.
juce::TextButton textButton("Button");
음.
더 알아볼 게 없네요.
ImageButton은 이미지 버튼입니다.
일반 이미지 (normal image), 마우스를 올렸을 때 이미지 (over image), 마우스를 눌렀을 때 이미지 (down image) 총 3개가 필요합니다.
각각의 이미지가 따로 있으면 각각 넣어주면 되고, 아니면 같은 이미지를 세 번 넣어주면 되겠죠.
juce::ImageButton button;
juce::Image image = juce::ImageFileFormat.loadFrom("image.png");
if (image.isValid())
button.setImages(true, true, true,
image, 0.7, juce::Colours::transparentBlack,
image, 1, juce::Colours::transparentBlack,
image, 1, Colours::black.withAlpha(0.8f),
0.5);
parameter가 좀 많습니다.
1] 버튼 크기를 이미지 크기에 맞게 조절할지 여부입니다.
2] 버튼 크기가 변할 때 이미지 크기도 조절할지 여부입니다.
3] 이미지 비율을 유지할지 여부입니다.
4] 일반 이미지입니다.
5] 일반 이미지를 그릴 때 투명도입니다. 0이면 투명합니다.
6] 일반 이미지에 오버레이로 적용할 색입니다. transparent면 오버레이를 적용하지 않는 효과가 있겠죠.
7] 마우스를 올렸을 때의 이미지인, 오버 이미지입니다.
8] 오버 이미지의 투명도
9] 오버 이미지의 오버레이로 적용할 색
10] 마우스를 눌렀을 때 이미지인, 다운 이미지입니다.
11] 다운 이미지의 투명도
12] 다운 이미지의 오버레이로 적용할 색
13] 버튼 영역 안에서 마우스를 눌렀을 때 버튼을 눌렀는지 여부를 결정할 알파 채널의 threshold입니다.
다시 말해서, 지정한 값 이상의 투명도인 픽셀를 클릭했을 때만 버튼을 눌렀다고 판정해줍니다.
기본값은 0이고, 이 경우에는 버튼 영역 안에만 들어가면 모두 버튼을 누른 것으로 판정해주지만
0.5라면 투명도 0.5, 알파 채널 값 255 * 0.5 > 127 이상인 픽셀일 경우에만 누른 것으로 판정해줍니다.
** 13번 보니까 괜찮은 아이디어네요. 나중에 프레임워크에 써먹어야지
마지막으로 ToggleButton입니다. 설문지 양식에서 볼 법한 체크 버튼입니다.
단일로 사용하면 Yes/No를 정하는 버튼으로 사용할 수 있습니다.
juce::ToggleButton toggleButton {"Agree to something"};
toggleButton.setToggleState(false, juce::dontSendNotification);
bool agree = toggleButton.getToggleState();
setToggleState로 체크 상태를 설정할 수 있고, getToggleState로 상태 값을 가져올 수 있습니다.
Yes/No 이외에도 여러 가지의 항목 중에서 하나에만 체크해야 할 수 있습니다.
그런 경우에는 Radio Group으로 묶어줘야 됩니다.
enum RadioID
{
Gender = 1001
};
maleButton.setRadioGroupId(Gender);
femaleButton.setRadioGroupID(Gender);
예시는 공식 튜토리얼에서 가져왔습니다.
3. ComboBox
여러 개의 항목이 있는 리스트에서 선택하여 지정하는 GUI Component입니다.
juce::ComboBox list;
list.addItem("First", 1);
juce::StringArray strArray;
str.add("Second", 2);
str.add("Third", 3);
list.addItemList(strArray, 2);
int id = list.getSelectedId();
단일 항목을 addItem으로 추가하거나, 여러 개를 포함하는 array를 addItemList로 추가할 수 있습니다.
두번째 parameter는 추가할 위치의 인덱스입니다.
여기서 유의해야 하는 것은 시작 인덱스가 0이 아닌 1이라는 점입니다.
현재 인덱스를 가져오려면 getSelectdId를 사용하면 됩니다.
4. Slider
Slider는 Audio Plugin에서 흔히 볼 수 있는, 대표적인 GUI입니다.
시작값과 끝값을 지정하고, 그 구간 내로 노브를 움직여서 값을 정할 수 있습니다.
juce::Slider slider;
slider.setRange(0, 1);
slider.setTextValueSuffix(" s");
Slider에도 여러 스타일이 있는데, 원형 노브는 다음과 같이 지정해주면 됩니다.
juce::Slider slider(juce::Slider::RotaryHorizontalVerticalDrag);
//...
Slider에 대해서는 조금 있다가 좀 더 다루겠습니다.
공통적인 사용법
그래픽 요소가 있는 Component는 모두 부모가 되는 Component에 추가되고 그래픽 영역을 지정해줘야 실제로 그래픽으로 표현됩니다.
MainComponent()
{
//...
addAndMakeVisible(component);
//...
}
void resized() override
{
component.setBounds(10, 10, 400, 300);
}
생성자에서 addAndMakeVisible로 추가해준 뒤에 필요한 세팅을 해주고,
resized에서 그래픽이 그려질 boundary를 지정해주면 됩니다.
getLocalBounds()를 이용해서 윈도우 및 Component 크기에 대응해서 유동적으로 영역을 조절할 수도 있습니다.
auto area = getLocalBounds();
component.setBounds(area.removeFromTop(36).removeFromRight(getWidth() - 150).reduced(8));
예를 들어 이 코드에서 component는 부모 component에서 위에서 36px, 오른쪽에서 너비 - 150 만큼 이동하고, 전체적인 크기를 8px 축소시킨 영역에 그려집니다.
저는 이게 생각보다 직관적으로 다가오지가 않아서 그냥 resize를 불가능하게 막아버렸습니다.
추가 설명
1] Slider는 노브 뿐만 아니라 값을 표시할 TextBox도 뒤따라오기 때문에, TextBox의 위치를 지정할 수 있습니다.
juce::Slider attack(juce::Slider::RotaryHorizontalVerticalDrag, juce::Slider::TextEntryBoxPosition::TextBoxBelow);
이런 식으로 하면 TextBox를 아래에 배치할 수 있습니다.
위의 Attack은 label인데 이건 attachToComponent를 사용하면 된다고 언급했었죠.
juce::Label attackLabel;
attackLabel.setText("Attack");
attackLabel.attachToComponent(&attack, false);
false면 component 위쪽에, true면 component 왼쪽에 배치됩니다.
2] 지난 포스팅에서 ValueTree의 parameter를 Slider 혹은 Button 등에 연동시킬 수 있다고 했는데,
Attachment를 사용하면 됩니다.
//juce::AudioProcessorValueTreeState valueTree;
typedef juce::AudioProcessorValueTreeState::SliderAttachment SliderAttachment;
typedef juce::AudioProcessorValueTreeState::ButtonAttachment ButtonAttachment;
std::unique_ptr<SliderAttachment> attackAttachment;
attackAttachment.reset(new SliderAttachment(valueTree, "attack", attack));
attack.setListener(this);
Attachment의 type 이름이 너무 길기 때문에, 축약형으로 typedef를 해줬습니다.
Attachment에는 연결할 value tree, parameter의 name, 연결할 component (여기선 Slider) 순으로 넣어주면 됩니다.
버그인지 의도된 사항인지, 혹은 제가 코드를 잘못 짜서 꼬인 건지 잘 모르겠지만
attachment를 등록한 이후에 listener 같은 걸 연결해줘야 제대로 적용되더군요.
attach 이전에 연결하면 인식하지 못해서 추적을 못하는 건가 싶은데, 자세한 건 잘 모르겠습니다.
라이브러리 소스 코드를 읽어봐도 워낙 복잡해서;;
3] 방금 잠깐 언급했듯이, 경우에 따라 Component에서 이벤트를 트리거해야 될 수 있습니다.
연결할 listener는 관련 Component의 Listener를 상속받은 클래스 객체여야 합니다.
class Something : private juce::Slider::Listener
{
private:
void sliderValueChanged(juce::Slider* slider) override;
};
void Something::sliderValueChanged(juce::Slider* slider)
{
//do something
}
Slider를 움직이거나 setValue 등을 통해서 값을 수정한 경우, 그 Slider 객체를 parameter로 해서 sliderValueChanged를 호출합니다. 관련 이벤트를 트리거하고 싶다면 이 함수 내부에 코드를 작성하면 됩니다.
저같은 경우에는 다른 값에 의존하여 range가 제한되는 (예를 들면 샘플의 시작 구간과 끝 구간은 시작 구간 <= 끝 구간을 만족해야 합니다) 경우를 이 함수 내에서 처리하도록 했습니다.
4] GUI의 색상 변경
까먹고 이걸 안 썼네
윈도우의 background color, title color 등을 포함해서 GUI component의 색상은 LookAndFeel에서 정한 색을 참조하여 렌더링합니다. 다음 이미지는 색상 변경이 없는 기본 윈도우입니다.
component의 색상을 변경하려면, LookAndFeel에서 그에 맞는 id를 찾아서 수정해주면 됩니다.
코드는 PluginEditor의 constructor 같은 곳에 작성하면 됩니다.
auto& look = getLookAndFeel();
look.setColour(juce::ResizableWindow::backgroundColourId, juce::Colour(17, 17, 17));
look.setColour(juce::Slider::ColourIds::rotarySliderOutlineColourId, juce::Colour(47, 47, 47));
look.setColour(juce::Slider::ColourIds::rotarySliderFillColourId, juce::Colours::white);
look.setColour(juce::Slider::thumbColourId, juce::Colours::white);
참고로 색상 변경이 적용되려면 당연히 렌더링할 때도 LookAndFeel의 색상을 참조해야 합니다.
void paint(juce::Graphics& g) override
{
g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
}
결과는 다음과 같이 나옵니다.
사실 저 코드만으로는 이렇게 안 나옵니다. 타이틀 바는 수정을 안 해줬으니까요.
LookAndFeel에는 타이틀 바와 관련된 ID가 없습니다. PluginEditor가 다루는 영역이 아니기 때문입니다.
타이틀 바의 색상은 윈도우 생성 시에 지정해주는데, 이 코드는 juce library의 소스 코드에 있습니다.
라이브러리를 사용할 때 소스 코드를 그대로 제공해주기 때문에 여기를 직접 수정해서 색상을 변경할 수도 있지만, 이 방법은 좀 지저분해보입니다. 애플리케이션에 개별적으로 색상을 지정해주는 게 아니라 그냥 원본의 default color 자체를 수정하는 거니까요.
타이틀 바에 관련된 설정은 LookAndFeel_V4에 있습니다. 무슨 버전마다 클래스가 따로 나뉘어있는 건지 그 이유는 잘 모르겠습니다만, 아무튼 있습니다.
아까도 말했듯이 타이틀 바는 PluginEditor가 아니라 윈도우 수준에서 렌더링하는 것이기 때문에 윈도우에서 LookAndFeel을 가져와서, V4로 캐스팅한 다음에 변경해줘야 됩니다.
그러면 윈도우부터 가져와야 되겠죠.
auto window = juce::ResizableWindow::getTopLevelWindow(0);
if (window)
{
auto look = dynamic_cast<juce::LookAndFeel_V4*>(&window->getLookAndFeel());
auto scheme = look->getCurrentColourScheme();
scheme.setUIColour(juce::LookAndFeel_V4::ColourScheme::widgetBackground, juce::Colours::black);
look->setColourScheme(scheme);
}
추가로, 타이틀 바는 standalone application에서나 볼 수 있는 그래픽입니다.
Plugin은 대체로 이런 타이틀 바 없이 PluginEditor을 바로 렌더링합니다.
따라서 저는 코드 구분을 위해 standalone인지 여부를 파악하게끔 했습니다.
뭐... 굳이 조건문을 추가해줄 필요는 없습니다. 렌더링이 안되는데 바꾸든 말든 보이진 않을테니까요.
bool _STANDALONE = juce::JUCEApplicationBase::isStandaloneApp();
if (_STANDALONE)
{
auto window = juce::ResizableWindow::getTopLevelWindow(0);
if (window)
{
auto look = dynamic_cast<juce::LookAndFeel_V4*>(&window->getLookAndFeel());
auto scheme = look->getCurrentColourScheme();
scheme.setUIColour(juce::LookAndFeel_V4::ColourScheme::widgetBackground, juce::Colours::black);
look->setColourScheme(scheme);
}
}
그러면 GUI는 이쯤하고, 남은 것들 다음 포스팅에서 정리하고 끝내야겠습니다.
'C++ > Sound' 카테고리의 다른 글
[JUCE] Juce Framework로 사운드 프로그래밍 #7 Pitch Shift (0) | 2022.07.11 |
---|---|
[JUCE] Juce Framework로 사운드 프로그래밍 #6 ADSR (0) | 2022.07.10 |
[JUCE] Juce Framework로 사운드 프로그래밍 #4 Processor와 Value Tree (0) | 2022.06.24 |
[JUCE] Juce Framework로 사운드 프로그래밍 #3 신디사이저 만들기 (0) | 2022.06.12 |
[JUCE] Juce Framework로 사운드 프로그래밍 #2 구조 대충 알아보기 (0) | 2022.06.08 |