JUCEプログラミング、The TableListBox class1、デモアプリの実装と不具合の調査

JUCEコンポーネントで表が作成できます。

何かしら表を作りたいタイミングはくるはず・・・

JUCEチュートリアル、「Interface Design」の最後の項目、「The TableListBox class」チュートリアルです。表形式でコンテンツを並べるようなGUIコンポーネントのTableListBoxクラスというものを使用して、表でデータを並べるアプリを実装していきます。今回は、デモアプリを動作させるところまで行います。一部、チュートリアルの内容が古いようで、チュートリアルそのままでは動作しない点も調査しました。

公式のチュートリアルページはこちらになります。

https://docs.juce.com/master/tutorial_table_list_box.html

目次

こんな人の役に立つかも

・JUCEプログラミングを勉強している人

・JUCEチュートリアル「The TableListBox class」をやっている人

・JUCE、表形式のコンポーネントを実装したい人

実装できるデモアプリ

TableListBoxというクラスを利用して、次のスクショのように、項目を表形式で並べるようなアプリを実装します。表のコンテンツは、XMLのデータとして保存してあるものを読み込んで表示しています。ファイルからXMLを読み込む、という点での例にもなりそうです。

The TableListBoxについて

ListBoxクラスというコンポーネントがJUCEでは表を作成するクラスになります。ここでいう表は、項目をリストとして羅列して、項目が多い場合、縦方向にスクロールを可能にするようなものです。また、ListBoxクラスの表は、ListBoxModelクラスというListBoxクラスを管理するための関数群が定義されたクラスもあります。ListBoxクラスとListBoxModelクラスのオブジェクトを利用して表をなんやかんやする、というイメージです。このListBoxクラスをより実用的に、それぞれの列項目を定義することができるTableListBoxクラスというものを利用するのが、今回の目的を達成するのにより近いです。

次のようにTableListBoxクラスは、列のヘッダーに関するリスナーなども継承しています。

チュートリアル実装時の不具合

今回のチュートリアルは、単純にやっていけば実装ができるというものではありませんでした。一部、古い箇所?(もともと間違っていた??)部分があり、該当箇所を正しくしないと動作しませんでした。

変更の箇所

この変更箇所については、JUCE Forumの次の記事が参考になりました。

https://forum.juce.com/t/a-fix-for-tablelistbox-tutorial/44668

ということで、この修正を反映してアプリを実装していきます。

デモアプリの実装

JUCEプロジェクトの作成

今回も例のごとく、Projucerの「GUI」テンプレートを利用してデモアプリを実装していきます。

Projucerから、GUIテンプレートで新規プロジェクトを作成します。

リソースの配置

まずは、今回のアプリはXMLデータを外部に準備して、データを読み込みますので、読み込むためのXMLファイルを以下のプロジェクト階層へ配置します。「Resources」フォルダを作成して、チュートリアルからダウンロードできるXMLファイル「TableData.xml」を配置しました。

↓公式サイトでダウンロードできるZIPファイルへのリンクをここにも貼り付けておきます。

https://docs.juce.com/tutorials/ZIPs/TableListBoxTutorial.zip

今回利用するXMLの構造は次のようになっています。

<TABLE_DATA>
    <HEADERS>
        <COLUMN columnId="1" name="ID" width="50"/>
        //...略...
    </HEADERS>
    <DATA>
        <ITEM ID="01" Module="juce_module" Name="JUCE example classes" Version="5.2.0" License="ISC" Groups="2" Dependencies="1" Description="..." Select="0"/>
        //...略...
    </DATA>
</TABLE_DATA>

「HEADERS」が今回の列項目になります。それぞれID、項目名、幅というパラメータを持ちます。また、データを「DATA」という要素の中に並べています。データは、先ほど定義したHEADERSの項目を列とするので、それに対応したデータを定義しています。

実装プログラム

MainComponent.h

MainComponent.hに、次のTableTutorialComponentクラスを作成しました。

class TableTutorialComponent : public juce::Component,
    public juce::TableListBoxModel
{
public:
    TableTutorialComponent()
    {
        loadData();

        addAndMakeVisible(table);

        table.setColour(juce::ListBox::outlineColourId, juce::Colours::grey);
        table.setOutlineThickness(1);

        if (columnList != nullptr)
        {
            for (auto* columnXml = columnList->getFirstChildElement();
                columnXml != nullptr;
                columnXml = columnXml->getNextElement())
            {
                table.getHeader().addColumn(columnXml->getStringAttribute("name"),
                    columnXml->getIntAttribute("columnId"),
                    columnXml->getIntAttribute("width"),
                    50,
                    400,
                    juce::TableHeaderComponent::defaultFlags);
            }
        }

        table.getHeader().setSortColumnId(1, true);

        table.setMultipleSelectionEnabled(true);
    }

    int getNumRows() override
    {
        return numRows;
    }

    void paintRowBackground(juce::Graphics& g, int rowNumber, int /*width*/, int /*height*/, bool rowIsSelected) override
    {
        auto alternateColour = getLookAndFeel().findColour(juce::ListBox::backgroundColourId)
            .interpolatedWith(getLookAndFeel().findColour(juce::ListBox::textColourId), 0.03f);
        if (rowIsSelected)
            g.fillAll(juce::Colours::lightblue);
        else if (rowNumber % 2)
            g.fillAll(alternateColour);
    }

    void paintCell(juce::Graphics& g, int rowNumber, int columnId,
        int width, int height, bool rowIsSelected) override
    {
        g.setColour(rowIsSelected ? juce::Colours::darkblue : getLookAndFeel().findColour(juce::ListBox::textColourId));
        g.setFont(font);

        if (auto* rowElement = dataList->getChildElement(rowNumber))
        {
            auto text = rowElement->getStringAttribute(getAttributeNameForColumnId(columnId));

            g.drawText(text, 2, 0, width - 4, height, juce::Justification::centredLeft, true);                      
        }

        g.setColour(getLookAndFeel().findColour(juce::ListBox::backgroundColourId));
        g.fillRect(width - 1, 0, 1, height);                                                                          
    }

    void sortOrderChanged(int newSortColumnId, bool isForwards) override
    {
        if (newSortColumnId != 0)
        {
            TutorialDataSorter sorter(getAttributeNameForColumnId(newSortColumnId), isForwards);
            dataList->sortChildElements(sorter);

            table.updateContent();
        }
    }

    Component* refreshComponentForCell(int rowNumber, int columnId, bool /*isRowSelected*/,
        Component* existingComponentToUpdate) override
    {
        if (columnId == 9)
        {
            auto* selectionBox = static_cast<SelectionColumnCustomComponent*> (existingComponentToUpdate);

            if (selectionBox == nullptr)
                selectionBox = new SelectionColumnCustomComponent(*this);

            selectionBox->setRowAndColumn(rowNumber, columnId);
            return selectionBox;
        }

        if (columnId == 8)
        {
            auto* textLabel = static_cast<EditableTextCustomComponent*> (existingComponentToUpdate);

            if (textLabel == nullptr)
                textLabel = new EditableTextCustomComponent(*this);

            textLabel->setRowAndColumn(rowNumber, columnId);
            return textLabel;
        }

        jassert(existingComponentToUpdate == nullptr);
        return nullptr;
    }

    int getColumnAutoSizeWidth(int columnId) override
    {
        if (columnId == 9)
            return 50;

        int widest = 32;

        for (auto i = getNumRows(); --i >= 0;)
        {
            if (auto* rowElement = dataList->getChildElement(i))
            {
                auto text = rowElement->getStringAttribute(getAttributeNameForColumnId(columnId));

                widest = juce::jmax(widest, font.getStringWidth(text));
            }
        }

        return widest + 8;
    }

    int getSelection(const int rowNumber) const
    {
        return dataList->getChildElement(rowNumber)->getIntAttribute("Select");
    }

    void setSelection(const int rowNumber, const int newSelection)
    {
        dataList->getChildElement(rowNumber)->setAttribute("Select", newSelection);
    }

    juce::String getText(const int columnNumber, const int rowNumber) const
    {
        return dataList->getChildElement(rowNumber)->getStringAttribute(getAttributeNameForColumnId(columnNumber));
    }

    void setText(const int columnNumber, const int rowNumber, const juce::String& newText)
    {
        const auto& columnName = table.getHeader().getColumnName(columnNumber);
        dataList->getChildElement(rowNumber)->setAttribute(columnName, newText);
    }

    //==============================================================================
    void resized() override
    {
        table.setBoundsInset(juce::BorderSize<int>(8));
    }

private:
    juce::TableListBox table{ {}, this };
    juce::Font font{ 14.0f };

    std::unique_ptr<juce::XmlElement> tutorialData;
    juce::XmlElement* columnList = nullptr;
    juce::XmlElement* dataList = nullptr;
    int numRows = 0;

    //==============================================================================
    class EditableTextCustomComponent : public juce::Label
    {
    public:
        EditableTextCustomComponent(TableTutorialComponent& td)
            : owner(td)
        {
            setEditable(false, true, false);
        }

        void mouseDown(const juce::MouseEvent& event) override
        {
            owner.table.selectRowsBasedOnModifierKeys(row, event.mods, false);

            Label::mouseDown(event);
        }

        void textWasEdited() override
        {
            owner.setText(columnId, row, getText());
        }

        void setRowAndColumn(const int newRow, const int newColumn)
        {
            row = newRow;
            columnId = newColumn;
            setText(owner.getText(columnId, row), juce::dontSendNotification);
        }

    private:
        TableTutorialComponent& owner;
        int row, columnId;
        juce::Colour textColour;
    };

    //==============================================================================
    class SelectionColumnCustomComponent : public Component
    {
    public:
        SelectionColumnCustomComponent(TableTutorialComponent& td)
            : owner(td)
        {
            addAndMakeVisible(toggleButton);

            toggleButton.onClick = [this] { owner.setSelection(row, (int)toggleButton.getToggleState()); };
        }

        void resized() override
        {
            toggleButton.setBoundsInset(juce::BorderSize<int>(2));
        }

        void setRowAndColumn(int newRow, int newColumn)
        {
            row = newRow;
            columnId = newColumn;
            toggleButton.setToggleState((bool)owner.getSelection(row), juce::dontSendNotification);
        }

    private:
        TableTutorialComponent& owner;
        juce::ToggleButton toggleButton;
        int row, columnId;
    };

    //==============================================================================
    class TutorialDataSorter
    {
    public:
        TutorialDataSorter(const juce::String& attributeToSortBy, bool forwards)
            : attributeToSort(attributeToSortBy),
            direction(forwards ? 1 : -1)
        {}

        int compareElements(juce::XmlElement* first, juce::XmlElement* second) const
        {
            auto result = first->getStringAttribute(attributeToSort)
                .compareNatural(second->getStringAttribute(attributeToSort));

            if (result == 0)
                result = first->getStringAttribute("ID")
                .compareNatural(second->getStringAttribute("ID"));

            return direction * result;
        }

    private:
        juce::String attributeToSort;
        int direction;
    };

    //==============================================================================
    void loadData()
    {
        auto dir = juce::File::getCurrentWorkingDirectory();

        int numTries = 0;

        while (!dir.getChildFile("Resources").exists() && numTries++ < 15)
            dir = dir.getParentDirectory();

        auto tableFile = dir.getChildFile("Resources").getChildFile("TableData.xml");

        if (tableFile.exists())
        {
            tutorialData = juce::XmlDocument::parse(tableFile);

            dataList = tutorialData->getChildByName("DATA");
            columnList = tutorialData->getChildByName("HEADERS");

            numRows = dataList->getNumChildElements();
        }
    }

    juce::String getAttributeNameForColumnId(const int columnId) const
    {
        for (auto* columnXml = columnList->getFirstChildElement();
            columnXml != nullptr;
            columnXml = columnXml->getNextElement())
        {
            if (columnXml->getIntAttribute("columnId") == columnId)
                return columnXml->getStringAttribute("name");
        }

        return {};
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TableTutorialComponent)
};

ほぼコピペです・・・

このプログラムをMainComponentクラスの上に貼り付けました。ちなみに、前述した修正点は2箇所あり、次のfor文を変更しています。

for (auto* columnXml : columnList->getChildIterator())

このfor文を次のように変更しています。

for (auto* columnXml = columnList->getFirstChildElement();
             columnXml != nullptr;
             columnXml = columnXml->getNextElement())

また、MainComponentクラスには、privateなメンバとして「table」を追加しておきます。

class MainComponent  : public juce::Component
{
public:

//...略...
private:
    //↓追加しました。
    TableTutorialComponent table;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

MainComponent.cpp

最後に、MainConponent.cppを次のように変更しました。

MainComponent::MainComponent()
{
    //[1]tableコンポーネントを可視化します。
    addAndMakeVisible(table);

    setSize(1200, 600);
}

MainComponent::~MainComponent()
{
}


void MainComponent::paint (juce::Graphics& g)
{
    //[2]背景の塗りつぶしのみに変更しました。
    g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
}

void MainComponent::resized()
{
    //[3]tableコンポーネントを配置します。
    table.setBounds(getLocalBounds());
}

[1]では、コンストラクタでtableを初期化します。可視化するのみです。ウィンドウサイズは今回は1200×600としております。

[2]では、paint関数を背景の塗りつぶしの処理のみに変更しました。

[3]では、resized関数で、tableをGUIに配置しています。getLocalBounds関数をsetBounds関数の引数に取っているので、tableを描画領域全体に配置している形になります。

よかったらシェアしてね!
目次
閉じる