Open and read data from text and binary files.
Level: Beginner
Platforms: Windows , macOS , Linux
Classes: File, FileInputStream, FilenameComponent, TextEditor, String, MemoryBlock
Download the demo project for this tutorial here: PIP | ZIP . Unzip the project and open the first header file in the Projucer.
If you need help with this step, see Tutorial: Projucer Part 1: Getting started with the Projucer.
The demo project presents a simple window which allows you to select a file, using a FilenameComponent object. This file is opened, read as a string, and displayed in a TextEditor component.
This tutorial illustrates the basic techniques for reading files using JUCE. Files are one aspect of cross-platform development that need to be handled carefully because file systems on different operating systems sometimes work in quite different ways. As a developer using JUCE you are not immune to these problems, but JUCE makes the experience more consistent across different platforms and will often raise assertions in your debug builds if it can see that your code does something that may lead to problems.
The File class in JUCE represents an absolute path to a file or a directory (whether it actually exists or not). The simplest way to create a File object is to pass it a String that contains the absolute path. For example, on macOS , Linux or Android the following would be an absolute path:
But the File class enables ways of manipulating paths by requesting child files that are relative to a parent directory, obtaining the parent directory, and so on. For example, the code above could be rewritten using the File::getChildFile() function like so:
In this example the code has become more verbose but in real-world scenarios it is common to need to access multiple files from the same directory. Therefore, it makes sense to store the parent directory in one File object and request child files as and when they are needed.
On Windows, an equivalent absolute path might be:
Dealing with child and parent directories is the same on each platform. JUCE handles the platform differences (such as path separators).
In this tutorial we are going to use a FilenameComponent object to allow the user to select a file using a standard operating system window. We can attach a listener to the FilenameComponent object (see Tutorial: Listeners and Broadcasters) and when the file changes we can obtain the currently chosen file as a File object.
A FilenameComponent object shows a text box that contains an absolute path. It also provides a button for the user to select a file from the operating system. There is also a drop-down menu that contains a list of recently used files. This is populated automatically during use, but these recently-used files can be added manually too (for example, hardcoded into your application or from a preferences file).
As we will see in a moment, the FilenameComponent constructor has quite a few arguments—and it doesn't have a default constructor. When this is the case, it is often easier to store child components in std::unique_ptr objects. (Since this means that they don't have to be initialised in the class initialiser list in the constructor.) We also need a TextEditor component, which we will use to display the file's contents. The TextEditor class does have a default constructor, but for consistency we store both component objects in std::unique_ptr objects:
In our MainContentComponent
constructor we allocate a new FilenameComponent object and initialise it with some settings suitable for opening files (since the FilenameComponent class can also be used for choosing locations for saving files too). Within this constructor we can provide a list of file suffixes that we wish to be able to select (for example, "*.txt;*.foo"
). We can also enforce a suffix (which is more useful for saving files). In both of these arguments we pass an empty string, which means that no filtering will be performed. Other arguments are self-explanatory, defining other ways that we want the FilenameComponent object to behave (see the comments in the code):
In the FilenameComponentListener callback we obtain the currently chosen file and pass it to our readFile()
function:
In each of the examples that follow, the readFile()
function will read the chosen file in different ways. But we need somewhere to display the results, so we set up a TextEditor component in our MainContentComponent
constructor too:
While the File class is designed primarily to store and manipulate paths to files, there are a few convenient functions for reading files in really simple ways. For example, the File::loadFileAsString() function does exactly what it says: it reads a whole file into a String object. Of course, if the file selected isn't a text file then the result may be impossible to make sense of (although JUCE won't crash). This function can detect and read both UTF-8 and UTF-16 formats:
Notice that we check to see if the file chosen actually exists [1] . Since we chose the file from the operating systems then this shouldn't fail, but it's good practice to make these kinds of checks when dealing with files. Run the app and load the juce.txt
text file provided in the Resources
directory of the demo project. The result will be as shown in the following screenshot:
There is an equivalent function—File::loadFileAsData()—to read an entire file into a MemoryBlock object.
For more control over the file reading process, you will need to use a FileInputStream object. One way to do this is to construct a FileInputStream object by passing its constructor the File object that represents the file you want to read [2] .
Add the following code:
We are going to read the file line by line, but we are also going to detect lines that start with the "*" character. Then we'll format these lines in a different font, using these lines as headings for the other text. Add the following code:
The next part is to read data from the inputStream
object, in a while()
loop, until the stream is exhausted [3] . Add this:
You can see that this:
textContent
object accordingly,textContent
object.Loading the same juce.txt
file should result in the something like the following screenshot:
FileReadingTutorial_02.h
file of the demo project.The InputStream, and therefore FileInputStream, classes also have functions for reading files in smaller pieces, right down to reading a byte at a time. To illustrate this, let's load a text file and display each word in a different colour. First let's add a function to generate random colours. Add this function which generates a random colour, but clips the brightness to a specified minimum (this is to ensure that the colour will be visible against the black background):
Now let's add a function that reads data from a FileInputStream object until it reaches a space character. This will then return the text up to, and including, the space. It creates a small memory buffer, using the MemoryBlock class, then uses the InputStream::readByte() function to read bytes one at a time from the inputStream
object:
The String::fromUTF8() [4] function attempts to convert the raw binary data to a String object.
Finally, in our readFile()
function, we use our readUpToNextSpace
function to read the words from the text file until the stream is exhausted. Add the following code:
Running this code will result in something like the following screenshot.
FileReadingTutorial_03.h
file of the demo project.An alternative way of creating a FileInputStream object is to use the File::createInputStream() function. This function returns a FileInputStream object on the heap, allocated using the new
operator. This means that it is very important that you deallocate the object when you are finished. Ideally, you should use a std::unique_ptr object for this. A slight difference here is that the File::createInputStream() function will return a nullptr
value if the file stream fails to open. The following code shows the typical pattern that you should use in this case:
'\n'
, '\r'
, and '\t'
.) In this tutorial we have looked at reading string data from a file. In addition to reading single bytes from a file, the InputStream class also includes functions for reading other fundamental types. For example:
int
(32-bit) from the stream.short
(16-bit) integer from the stream.float
(32-bit) from the stream.These all read multi-byte values using the little endian byte order. To read them as big endian values there are alternative versions—for example, the InputStream::readIntBigEndian() function. You can also read a block of data from a stream using the InputStream::read() or InputStream::readIntoMemoryBlock() functions.
These are useful if you need to read existing files in binary format, or if you really need to have your data stored as binary. In most cases, it is probably preferable to use XML (using the XmlDocument and XmlElement classes) or JSON (by storing data in var objects) for storing and reading your data.
This tutorial has introduced simple file reading technique using JUCE through reading a text file in various ways. In particular you should be able to: