음악, 삶, 개발

Juce 로 만드는 GUI 파트1 : Component 클래스 본문

개발 공부/Juce 공부방

Juce 로 만드는 GUI 파트1 : Component 클래스

Lee_____ 2020. 10. 26. 16:27

< 참고자료 >


< 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)wh 는 우리가 만들 프로그램창의 크기를 나타낸다.

이 코드를 컴파일해보면 아래와 같은 창이 뜬다.

넓이 800, 높이 500 의 Canvas 객체

우리는 위의 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;

};

이제 부모-자식-손자 관계가 형성되었다.

ParentChild 를 멤버로, ChildGrandChild 를 멤버로 가지고있다.

하지만 여전히 지금의 코드는 멤버로만 가지고있을뿐, 각 Component 에 자식이 등록된것은 아니다.

한마디로 서류상 등록이 안된것이다.

우리도 자식을 나으면 동사무소에 찾아가 가족관계임을 등록하듯이, 

Component 안에서 자식 Component 를 등록해주어야한다.

이때 사용하는 함수가 있다.

void Component::addAndMakeVisible(Component& child, int zOrder = -1);	

 

addAndMakeVisible 함수를 부모 ComponentConstructor 에서 호출해주면,

가족관계증명서에 자식을 등록한것이 된다.

(여기서 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 에서 그림을 그리는지 배울것이다.