メインコンテンツまでスキップ

チュートリアル:ファイル読み込み

📚 Source Page

テキストファイルとバイナリファイルからデータを開いて読み込みます。

レベル: 初級
プラットフォーム: Windows, macOS, Linux
クラス: File, FileInputStream, FilenameComponent, TextEditor, String, MemoryBlock

はじめに

このチュートリアルのデモプロジェクトをこちらからダウンロードしてください:PIP | ZIP。プロジェクトを解凍し、最初のヘッダファイルをProjucerで開いてください。

この手順についてサポートが必要な場合は、チュートリアル:Projucer Part 1: Projucerを始めようを参照してください。

デモプロジェクト

デモプロジェクトは、FilenameComponentオブジェクトを使用してファイルを選択できるシンプルなウィンドウを表示します。このファイルは開かれ、文字列として読み込まれ、TextEditorコンポーネントに表示されます。

JUCEでのファイル操作

このチュートリアルでは、JUCEを使用したファイル読み込みの基本的な技術を説明します。ファイルはクロスプラットフォーム開発の一側面であり、異なるオペレーティングシステム上のファイルシステムは時として非常に異なる方法で動作するため、慎重に扱う必要があります。JUCEを使用する開発者としてこれらの問題から免除されるわけではありませんが、JUCEは異なるプラットフォーム間でより一貫した体験を提供し、デバッグビルドでコードが問題につながる可能性のある何かを行っていることを検出した場合、アサーションを発生させることがよくあります。

Fileクラス

JUCEのFileクラスは、ファイルまたはディレクトリへの_絶対_パス(実際に存在するかどうかに関わらず)を表します。Fileオブジェクトを作成する最も簡単な方法は、絶対パスを含むStringを渡すことです。例えば、macOS、Linux、またはAndroidでは、以下が絶対パスになります:

juce::File path ("/path/to/file.txt");

しかし、Fileクラスは、親ディレクトリに対して相対的な子ファイルを要求したり、親ディレクトリを取得したりすることで、パスを操作する方法を提供します。例えば、上記のコードはFile::getChildFile()関数を使用して以下のように書き換えることができます:

juce::File path (File ("/path").getChildFile ("to").getChildFile ("file.txt"));

この例ではコードがより冗長になりましたが、実際のシナリオでは同じディレクトリから複数のファイルにアクセスする必要があることがよくあります。したがって、親ディレクトリを1つのFileオブジェクトに格納し、必要に応じて子ファイルを要求することは理にかなっています。

Windowsでは、同等の絶対パスは次のようになります:

juce::File path ("C:\\path\\to\\file.txt");

子ディレクトリと親ディレクトリの扱いは各プラットフォームで同じです。JUCEがプラットフォームの違い(パス区切り文字など)を処理します。

FilenameComponentクラス

このチュートリアルでは、FilenameComponentオブジェクトを使用して、ユーザーが標準のオペレーティングシステムウィンドウを使用してファイルを選択できるようにします。FilenameComponentオブジェクトにリスナーをアタッチでき(チュートリアル:リスナーとブロードキャスターを参照)、ファイルが変更されると、現在選択されているファイルをFileオブジェクトとして取得できます。

FilenameComponentオブジェクトは、絶対パスを含むテキストボックスを表示します。また、オペレーティングシステムからファイルを選択するためのボタンも提供します。最近使用したファイルのリストを含むドロップダウンメニューもあります。これは使用中に自動的に入力されますが、これらの最近使用したファイルは手動で追加することもできます(例えば、アプリケーションにハードコードされたり、設定ファイルから取得したりできます)。

すぐに見るように、FilenameComponentコンストラクタにはかなりの数の引数があり、デフォルトコンストラクタがありません。このような場合、子コンポーネントをstd::unique_ptrオブジェクトに格納する方が簡単です(これは、コンストラクタ内のクラス初期化子リストで初期化する必要がないためです)。結果を表示するために使用するTextEditorコンポーネントも必要です。TextEditorクラスにはデフォルトコンストラクタ_がありますが_、一貫性のために両方のコンポーネントオブジェクトをstd::unique_ptrオブジェクトに格納します:

std::unique_ptr<juce::FilenameComponent> fileComp;
std::unique_ptr<juce::TextEditor> textContent;

MainContentComponentコンストラクタで、新しいFilenameComponentオブジェクトを割り当て、ファイルを開くのに適した設定で初期化します(FilenameComponentクラスはファイルを保存する場所を選択するためにも使用できます)。このコンストラクタ内で、選択できるファイルサフィックスのリストを提供できます(例:"*.txt;*.foo")。サフィックスを強制することもできます(これはファイルを保存する場合により便利です)。これらの両方の引数で空の文字列を渡します。これはフィルタリングが行われないことを意味します。他の引数は自明であり、FilenameComponentオブジェクトに望む他の動作方法を定義します(コード内のコメントを参照):

MainContentComponent()
{
fileComp.reset (new juce::FilenameComponent ("fileComp",
{}, // current file
false, // can edit file name,
false, // is directory,
false, // is for saving,
{}, // browser wildcard suffix,
{}, // enforced suffix,
"Select file to open")); // text when nothing selected
addAndMakeVisible (fileComp.get());
fileComp->addListener (this);

FilenameComponentListenerコールバックで、現在選択されているファイルを取得し、readFile()関数に渡します:

void filenameComponentChanged (juce::FilenameComponent* fileComponentThatHasChanged) override
{
if (fileComponentThatHasChanged == fileComp.get())
readFile (fileComp->getCurrentFile());
}

以下の各例では、readFile()関数は異なる方法で選択されたファイルを読み込みます。しかし、結果を表示する場所が必要なので、MainContentComponentコンストラクタでTextEditorコンポーネントも設定します:

textContent.reset (new juce::TextEditor());
addAndMakeVisible (textContent.get());
textContent->setMultiLine (true);
textContent->setReadOnly (true);
textContent->setCaretVisible (false);

setSize (600, 400);
}

ファイル全体を文字列に読み込む

Fileクラスは主にファイルへのパスを格納および操作するために設計されていますが、本当にシンプルな方法でファイルを読み込むための便利な関数がいくつかあります。例えば、File::loadFileAsString()関数はまさにその名の通りのことを行います:ファイル全体をStringオブジェクトに読み込みます。もちろん、選択されたファイルがテキストファイルでない場合、結果は意味不明かもしれません(ただしJUCEはクラッシュしません)。この関数はUTF-8とUTF-16の両方の形式を検出して読み込むことができます:

void readFile (const juce::File& fileToRead)
{
if (!fileToRead.existsAsFile()) // [1]
return;

auto fileText = fileToRead.loadFileAsString();

textContent->setText (fileText);
}

選択されたファイルが実際に存在するかどうかを確認していることに注意してください [1]。オペレーティングシステムからファイルを選択したため、これは失敗しないはずですが、ファイルを扱う際にはこの種のチェックを行うことが良い習慣です。アプリを実行し、デモプロジェクトのResourcesディレクトリにあるjuce.txtテキストファイルをロードしてください。結果は以下のスクリーンショットのようになります:

テキストファイルの読み込みと表示
テキストファイルの読み込みと表示

ファイル全体をMemoryBlockオブジェクトに読み込む同等の関数 --- File::loadFileAsData() --- もあります。

ファイルを行ごとに読み込む

ファイル読み込みプロセスをより制御するには、FileInputStreamオブジェクトを使用する必要があります。これを行う1つの方法は、読み込みたいファイルを表すFileオブジェクトをコンストラクタに渡してFileInputStreamオブジェクトを構築することです [2]。

ヒント

FileInputStreamクラスは、データを読み込むストリームの基本クラスであるInputStreamクラスのサブクラスです。

以下のコードを追加してください:

void readFile (const juce::File& fileToRead)
{
if (!fileToRead.existsAsFile())
return; // file doesn't exist

juce::FileInputStream inputStream (fileToRead); // [2]

if (!inputStream.openedOk())
return; // failed to open

ファイルを行ごとに読み込みますが、「*」文字で始まる行を検出します。次に、これらの行を異なるフォントでフォーマットし、他のテキストの見出しとしてこれらの行を使用します。以下のコードを追加してください:

textContent->clear();

auto normalFont = textContent->getFont();
auto titleFont = normalFont.withHeight (normalFont.getHeight() * 1.5f).boldened();
juce::String asterix ("*");

次の部分は、ストリームが使い果たされるまで[3]、while()ループでinputStreamオブジェクトからデータを読み込むことです。以下を追加してください:

while (!inputStream.isExhausted()) // [3]
{
auto line = inputStream.readNextLine();

if (line.startsWith (asterix))
{
line = line.removeCharacters (asterix);
textContent->setFont (titleFont);
}
else
{
textContent->setFont (normalFont);
}

// append the text to the textContent
textContent->insertTextAtCaret (line + juce::newLine);
}
}

これが行うことは:

  • InputStream::readNextLine()関数を使用して行を読み込みます
  • 行が「*」で始まるかどうかを確認します
  • それに応じてtextContentオブジェクトのフォントを設定します
  • 必要に応じて「*」文字を削除します
  • textContentオブジェクトにテキスト行を追加します

同じjuce.txtファイルをロードすると、以下のスクリーンショットのようになるはずです:

ファイルを行ごとに読み込む
ファイルを行ごとに読み込む
ヒント

このサブセクションの例のコードは、デモプロジェクトのFileReadingTutorial_02.hファイルにあります。

ファイルをバイトごとに読み込む

InputStream、したがってFileInputStreamクラスには、ファイルをより小さな単位で、一度に1バイトまで読み込む関数もあります。これを説明するために、テキストファイルをロードして各単語を異なる色で表示しましょう。まず、ランダムな色を生成する関数を追加しましょう。この関数はランダムな色を生成しますが、明るさを指定された最小値に制限します(これは黒い背景に対して色が見えるようにするためです):

static juce::Colour getRandomColour (float minBrightness)
{
auto& random = juce::Random::getSystemRandom();
juce::Colour colour ((juce::uint8) random.nextInt (256),
(juce::uint8) random.nextInt (256),
(juce::uint8) random.nextInt (256));

return colour.getBrightness() >= minBrightness ? colour
: colour.withBrightness (minBrightness);
}

次に、FileInputStreamオブジェクトからスペース文字に達するまでデータを読み込む関数を追加しましょう。これにより、スペースを含むまでのテキストが返されます。MemoryBlockクラスを使用して小さなメモリバッファを作成し、InputStream::readByte()関数を使用してinputStreamオブジェクトから一度に1バイトずつバイトを読み込みます:

static juce::String readUpToNextSpace (juce::FileInputStream& inputStream)
{
juce::MemoryBlock buffer (256);
auto* data = static_cast<char*> (buffer.getData());
size_t i = 0;

while ((data[i] = inputStream.readByte()) != 0 && i < buffer.getSize())
if (data[i++] == ' ')
break;

return juce::String::fromUTF8 (data, (int) i); // [4]
}

String::fromUTF8() [4]関数は、生のバイナリデータをStringオブジェクトに変換しようとします。

最後に、readFile()関数で、readUpToNextSpace関数を使用してストリームが使い果たされるまでテキストファイルから単語を読み込みます。以下のコードを追加してください:

void readFile (const juce::File& fileToRead)
{
if (!fileToRead.existsAsFile())
return; // file doesn't exist

juce::FileInputStream inputStream (fileToRead);

if (!inputStream.openedOk())
return; // failed to open

textContent->clear();

while (!inputStream.isExhausted())
{
auto nextWord = readUpToNextSpace (inputStream);
textContent->setColour (juce::TextEditor::textColourId, getRandomColour (0.75f));
textContent->insertTextAtCaret (nextWord);
}
}

このコードを実行すると、以下のスクリーンショットのようになります。

ファイルをより小さなチャンクで読み込む
ファイルをより小さなチャンクで読み込む
ヒント

このサブセクションの例のコードは、デモプロジェクトのFileReadingTutorial_03.hファイルにあります。

FileInputStreamオブジェクトを作成する別の方法は、File::createInputStream()関数を使用することです。この関数は、new演算子を使用してヒープに割り当てられたFileInputStreamオブジェクトを返します。これは、終了したらオブジェクトを解放することが_非常に_重要であることを意味します。理想的には、このためにstd::unique_ptrオブジェクトを使用する必要があります。ここでのわずかな違いは、ファイルストリームが開けない場合、File::createInputStream()関数がnullptr値を返すことです。以下のコードは、この場合に使用すべき典型的なパターンを示しています:

void readFile (const juce::File& fileToRead)
{
if (!fileToRead.existsAsFile())
return; // file doesn't exist

if (std::unique_ptr<juce::FileInputStream> inputStream { fileToRead.createInputStream() })
{
textContent->clear();

while (!inputStream->isExhausted())
{
auto nextWord = readUpToNextSpace (*inputStream);
textContent->setColour (juce::TextEditor::textColourId, getRandomColour (0.75f));
textContent->insertTextAtCaret (nextWord);
}
}
}
注記

演習:一部の行末の単語の色が次の行の先頭の単語の色と同じであることに気づくかもしれません。これは、区切り文字としてスペース文字のみを検索しているためです。改行、キャリッジリターン、タブ文字も検索するようにコードを変更してください(これらは文字'\n''\r''\t'です)。

バイナリデータの読み込み

このチュートリアルでは、ファイルから文字列データを読み込む方法を見てきました。ファイルから単一のバイトを読み込むことに加えて、InputStreamクラスには他の基本型を読み込む関数も含まれています。例えば:

これらはすべてリトルエンディアンのバイト順序を使用してマルチバイト値を読み込みます。ビッグエンディアン値として読み込むには代替バージョンがあります --- 例えば、InputStream::readIntBigEndian()関数。InputStream::read()またはInputStream::readIntoMemoryBlock()関数を使用してストリームからデータブロックを読み込むこともできます。

これらは、既存のファイルをバイナリ形式で読み込む必要がある場合や、データをバイナリとして格納する本当の必要がある場合に便利です。ほとんどの場合、データの格納と読み込みにはXML(XmlDocumentXmlElementクラスを使用)またはJSON(varオブジェクトにデータを格納することで)を使用する方がおそらく望ましいでしょう。

まとめ

このチュートリアルでは、テキストファイルをさまざまな方法で読み込むことを通じて、JUCEを使用したシンプルなファイル読み込み技術を紹介しました。特に以下のことができるはずです:

  • FilenameComponentFilenameComponentListenerを使用してファイルパスを格納し、ユーザーにファイルを選択する手段を提供する。
  • ファイルの内容全体をStringオブジェクトに読み込む。
  • 適切なInputStream関数を使用してファイルをより小さなチャンクで読み込む。

関連項目