음악, 삶, 개발
Juce 로 만드는 GUI 파트1 : Component 클래스 본문
< 참고자료 >
Tutorial: Parent and child components
Tutorial: Responsive GUI layouts using FlexBox and Grid
Tutorial: Advanced GUI layout techniques
FlexItem::Margin Struct Reference
Grid::TrackInfo Struct Reference
GridItem::Margin Struct Reference
GridItem::Property Struct Reference
< Component 란? >
Juce 의 Component 는 Adobe Illustrator 나 Photoshop에서 사용하는 Layer 와 비슷한 개념이다.
우리가 Illustrator 에서 그리는 요소 하나 하나를, Juce 에서는 Component 라고 부른다.
Illustrator 에서 Layer 가 작동하는 방식처럼, Juce 에서도 Component 안에 Component 가 될수있고,
또는 여러개의 Component 들이 하나의 Component 를 이룰수도있다.
< 마우스, 키보드 에 반응하는 Component >
Juce 의 Component 는 마우스나, 키보드 입력에 반응한다.
따라서 GUI 를 만들기위해 사용하는것이 Component 이다.
만약 나의 Component 가 사각형이라면,
마우스를 한번 클릭했을때는 빨간색, 다시 한번 더 클릭했을때는 노란색이 되게할수있는것이다.
어떻게 마우스에 반응하게끔 하는지는 추후 설명하도록 하겠다.
< Component 만들기 >
우리가 상상하는 버튼이나 슬라이더등을 만들기위해서의 시작은 juce::Component 를 상속받는것이다.
struct Panel : public juce::Component {
}
< 나의 Component 클래스안에 정의해야할 2가지 >
juce::Component 를 상속받아 나의 클래스를 만들었다면 코드를 작성해야한다.
1. 무엇을 그릴것인가?
2. 마우스나 키보드 입력이 오면 무엇을 할것인가?
2번은 현재 다루지않고, 1번에 대해서만 우선적으로 다루겠다.
< Component 안에 그림 그리기 : paint() 함수 >
juce::Component 에서 제공하는 paint() 함수를 사용하여 그림을 그린다.
struct Panel : juce::Component {
void paint(juce::Grpahics& g) override {
/* 여기다 그리면 됨! */
}
}
이 paint() 함수는 Juce 가 호출하는 콜백함수이며,
이 함수가 호출될때 인자로 juce::Graphics 객체가 reference 로 넘어온다.
이 juce::Graphics 객체에 juce::Graphics 가 지원하는 함수들을 사용하여 최종적으로 무언가를 그리게 된다.
다음 포스팅에서 juce::Graphics 클래스에 대해 자세히 다루겠다.
< 도화지 (Canvas) 클래스의 필요성 >
우리가 무언가를 그리기위해서는 도화지가 필요하다.
이 도화지안에, 다양한 요소들이 들어가는것이다.
우리는 도화지 클래스를 생성해야하며, 이 클래스는 가장 부모가 되는 클래스이다.
아래와 같이 만들수있다.
class Canvas : public juce::Component {
public :
Canvas() {
setSize (canvasW, canvasH); // 부모 클래스에서 getWidth(), getHeight() 로 가져올수있음.
}
private :
const int canvasW {800};
const int canvasH {500};
};
우리는 도화지의 크기를 정해야하는데,
이때 사용하는 함수가 setSize(int w, int h) 이다.
이 함수는 반드시 생성자안에서 호출되야하며,
도화지는 부모가 없는 최종 Component 이기때문에,
여기서 setSize(int w, int h) 의 w 와 h 는 우리가 만들 프로그램창의 크기를 나타낸다.
이 코드를 컴파일해보면 아래와 같은 창이 뜬다.
우리는 위의 Canvas 클래스로 넓이 800, 높이 500 의 Canvas 객체를 생성한것이다.
< 노란색 사각형 그려보기 >
무언가를 프로그래밍으로 그릴려면 뭐가 필요할까?
1. 도화지의 크기 정하기 : setSize
2. 도화지 : juce::Graphics& g
3. 색깔 : juce::Colour
코드
class Canvas : public juce::Component {
public :
Canvas() {
setSize (canvasW, canvasH); // 부모 클래스에서 getWidth(), getHeight() 로 가져올수있음.
}
void paint(juce::Graphics& g) override {
g.fillAll(backgroundColor);
}
private :
const int canvasW {500};
const int canvasH {500};
juce::Colour backgroundColor {juce::Colours::yellow};
};
출력
< Component 간에 부모-자식-손자 관계 형성하기 : addAndMakeVisible >
앞서 이야기했듯이, Component 안에 여러개의 Component 가 들어갈수있다고했었다.
이를 활용해 부모, 자식, 손자 Component 구조를 만들어보자.
struct GrandChild : public juce::Component {};
struct Child : public juce::Component {};
struct Parent : public juce::Component {};
위와 같이 기본적인 클래스들을 만들었다.
이 상태로는 아직 부모-자식-손자 관계가 형성되지않았다.
관계를 형성하기위해서는 한 클래스 객체가 다른 클래스의 멤버가 되어야한다.
struct GrandChild : public juce::Component {};
struct Child : public juce::Component {
GrandChild grandChild;
};
struct Parent : public juce::Component {
Child child;
};
이제 부모-자식-손자 관계가 형성되었다.
Parent 는 Child 를 멤버로, Child 는 GrandChild 를 멤버로 가지고있다.
하지만 여전히 지금의 코드는 멤버로만 가지고있을뿐, 각 Component 에 자식이 등록된것은 아니다.
한마디로 서류상 등록이 안된것이다.
우리도 자식을 나으면 동사무소에 찾아가 가족관계임을 등록하듯이,
각 Component 안에서 자식 Component 를 등록해주어야한다.
이때 사용하는 함수가 있다.
void Component::addAndMakeVisible(Component& child, int zOrder = -1);
이 addAndMakeVisible 함수를 부모 Component 의 Constructor 에서 호출해주면,
가족관계증명서에 자식을 등록한것이 된다.
(여기서 zOrder 에 대한 설명은 일단 생략하겠다. 어짜피 Optional 파라미터이기때문에 현재는 값을 지정하지않아도된다.)
이 addAndMakeVisible 함수는 우리가 정의한 함수가 아니라, juce::Component 에서 제공하는 함수이다.
당연히 우리의 클래스는 juce::Component 를 상속받았기때문에 juce::Component 의 모든 함수를 사용할수있는것이다.
struct GrandChild : public juce::Component {};
struct Child : public juce::Component {
Child() { addAndMakeVisible(grandChild); } // Child - GrandChild 가족관계증명서 등록 완료
GrandChild grandChild;
};
struct Parent : public juce::Component {
Parent() { addAndMakeVisible(child); } // Parent - Child 가족관계증명서 등록 완료
Child child;
};
만약 Parent 클래스가, GrandChild 와 Child 를 멤버로 가지게된다면 어떻게 될까?
struct Parent : public juce::Component {
Parent() {
addAndMakeVisible(child);
addAndMakeVisible(grandChild);
}
Child child;
GrandChild grandChild;
};
당연히 이때는 각 멤버를 Parent 가 addAndMakeVisible 를 각각 호출해주어야한다.
< Component 를 배치하는 3가지 방법 >
1. setBounds
void resized() override {
auto bounds = getLocalBounds();
auto panelWidth = bounds.getWidth() / 4;
leftPanel .setBounds (bounds.removeFromLeft (panelWidth));
rightPanel .setBounds (bounds.removeFromRight (panelWidth));
mainPanel .setBounds (bounds);
}
2. FlexBox
void resized() override {
juce::FlexBox fb;
juce::FlexItem left ((float) getWidth() / 4.0f, (float) getHeight(), leftPanel);
juce::FlexItem right ((float) getWidth() / 4.0f, (float) getHeight(), rightPanel);
juce::FlexItem main ((float) getWidth() / 2.0f, (float) getHeight(), mainPanel);
fb.items.addArray ( { left, main, right } );
fb.performLayout (getLocalBounds().toFloat());
}
3. Grid
void resized() override {
juce::Grid grid;
using Track = juce::Grid::TrackInfo;
using Fr = juce::Grid::Fr;
grid.templateRows = { Track (Fr (1)) };
grid.templateColumns = { Track (Fr (1)), Track (Fr (2)), Track (Fr (1)) };
grid.items = { juce::GridItem (leftPanel), juce::GridItem (mainPanel), juce::GridItem (rightPanel) };
grid.performLayout (getLocalBounds());
}
다음 파트에서는 실제적으로 어떻게 Component 에서 그림을 그리는지 배울것이다.