Working with AIR Features

As mentioned previously, AIR allows additional features beyond those of a Flex web application. In the following sections, you’ll learn about working with these features.

Accessing the Local Filesystem

Arguably, one of the most significant features of AIR applications is that they can access the local filesystem. That means that with Flex AIR applications, you can read, write, create, move, and delete files and directories.

Referencing files and directories

As far as AIR is concerned, all files and directories are similar enough that AIR defines just one type of object for both: the flash.filesystem.File class. Instances of the File class reference files or directories. When you want to get a reference to an existing file or directory, the ideal way to do so is to use a relative reference. Using relative references helps to avoid absolute references that might be platform- or system-specific, thereby allowing applications to run across many computers and many operating systems. To facilitate retrieving relative references, AIR defines a handful of built-in references to common directories. These include the following:

File.applicationDirectory

The directory in which the AIR application is installed.

File.applicationStorageDirectory

The directory in which the AIR application can store files. This directory is unique to the AIR application, but it is also a different directory than the directory in which the application is installed.

File.desktopDirectory

The desktop directory for the current user.

File.documentsDirectory

The documents directory for the current user.

File.userDirectory

The current user’s directory.

These static properties of the File class automatically map to correct directories for a given system and user. Each property is a File object. Once you have a File object that references a directory, you can retrieve relative references using the resolvePath() method. The resolvePath() method will return a new File object that references a file or directory relative to the File object from which the method is called. The method requires one parameter specifying the relative path. For example, the following code creates a File object that references a file called example.jpg on the desktop:

var image:File = File.desktopDirectory.resolvePath("example.jpg");

You can use forward slashes as a delimiter between directories, and you can use two consecutive dots (..) to indicate the parent directory. For example, the following will reference a file called example.txt in the parent directory of the AIR application install directory:

var textFile:File = File.applicationDirectory.resolvePath("../example.txt");

Although relative paths are recommended, you can also create File objects that reference files or directories using absolute paths by using the constructor and passing it an absolute path as a parameter. For example, the following creates a File object that references the C: drive (presumably for a Windows machine):

var cDrive:File = new File("C:/");

Retrieving a directory listing

You can retrieve a listing of a directory by calling the getDirectoryListingAsync() method on a File object that references a directory. The getDirectoryListingAsync() method, as the name implies, is asynchronous. That means you’ll have to listen for a directoryListing event (use the flash.events.FileListEvent.DIRECTORY_LISTING constant) before you can work with the directory listing.

Note

There is a synchronous getDirectoryListing() method as well. However, we recommend that you always use asynchronous methods when there are both asynchronous and synchronous versions. This is because asynchronous methods are not likely to cause an AIR application to lock up, as are synchronous methods.

When the directoryListing event is handled, the parameter passed to the listener method will be of type FileListEvent, which has a files property containing an array of File objects (the files and directories contained within the directory).

All File objects have many properties that may be of use when retrieving a directory listing. Here are a few key properties in this context:

Name

The name of the file or directory.

modificationDate

The date when the file or directory was last modified.

size

The size of the file or directory in bytes.

isDirectory

A Boolean value indicating whether the object references a directory (if false, the object references a file). This is useful for determining whether you can run file- or directory-specific operations.

Example 21-1 retrieves all the files and directories from the desktop and displays them in a text area component.

Example 21-1. Displaying files and directories on the desktop

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute" creationComplete="creationCompleteHandler();">
    <mx:Script>
        <![CDATA[

            private function creationCompleteHandler():void {
                var desktop:File = File.desktopDirectory;
                desktop.addEventListener(FileListEvent.DIRECTORY_LISTING,
directoryListingHandler);
                desktop.getDirectoryListingAsync();
            }

            private function directoryListingHandler(event:FileListEvent):void {
                var count:int = event.files.length;
                var file:File;
                for(var i:int = 0; i < count; i++) {
                    file = event.files[i] as File;
                    textArea.text += file.name + " | " +
                                     file.modificationDate + " | " +
                                     file.size + " | " + file.isDirectory + "
";
                }
            }

        ]]>
    </mx:Script>
    <mx:TextArea id="textArea" width="100%" height="100%" />
</mx:WindowedApplication>

Creating directories

Typically, you create a directory on the filesystem with a specific path and a specific name. For example, you may want to programmatically create a mediaFiles directory in the application storage directory for the purpose of storing video and audio files that the AIR application downloads. When you want to create a directory in this fashion, you need to do the following:

  1. Create a File object that references the nonexistent directory.

  2. Call the createDirectory() method on that object.

For example, the following creates a mediaFiles directory in the application storage directory:

var mediaFilesDirectory:File = File.applicationStorageDirectory.resolvePath
("mediaFiles");
mediaFilesDirectory.createDirectory();

If the directory already exists, no action takes place. If the directory doesn’t yet exist, it is created. Furthermore, if parent directories of that directory don’t exist, they are created as well.

Note

You can create a new temporary directory using the static File.createTempDirectory() method, which returns a reference to the new directory. The directory is created in the system’s temporary directory path. The directory is not deleted automatically.

Reading and writing files

Reading and writing files requires use of a File object and a flash.filesystem.FileStream object. You can use a FileStream object to open a file (File object) and then use a variety of methods to read and write bytes.

As we already mentioned, the first step when reading or writing a file is to open the file using a FileStream object. The openAsync() method of a FileStream object requires two parameters: a reference to the File object and the mode in which you want to open the file. You can open a file in the following modes: read, write, append, or update. You can use the READ, WRITE, APPEND, and UPDATE constants of the flash.filesystem.FileMode class to indicate which mode you want to use. The read mode simply allows for reading. The write, append, and update modes allow for writing to the file, but they are each subtly different. The write mode always truncates the file, which means you’ll overwrite any existing data. The update and append modes retain existing data, but the append mode automatically begins to write at the end of the file. Any of the writing modes will create the file if it doesn’t already exist.

Note

You can also open a file for reading and writing in a synchronous fashion using the open() method. However, as previously noted, we’ll be looking only at asynchronous methods in detail in this chapter.

Once you’ve opened a file, you can read or write (depending on the mode) using the various read and write methods of the FileStream class. The FileStream class implements both the flash.utils.IDataInput and flash.utils.IDataOutput interfaces, which are also implemented by ActionScript classes such as ByteArray, Socket, and URLStream. Because these interfaces aren’t specific to Flex or AIR, we will not discuss them in great detail in this chapter. You can read more about the interfaces on the Adobe website at http://livedocs.adobe.com/flex/3/langref/flash/utils/IDataOutput.html and http://livedocs.adobe.com/flex/3/langref/flash/utils/IDataInput.html. The basic premise is that using methods such as writeBytes(), writeUTFBytes(), writeInt(), and writeObject(), you can write data to a file, and using methods such as readBytes(), readUTFBytes(), readInt(), and readObject(), you can read data from a file. The data is always read and written as bytes, but the read and write methods automatically convert the data to more developer-friendly formats such as strings, integers, and so on.

Note

Because you can read and write bytes, you can even go so far as to write a compression utility. An example of such a project is available at http://code.google.com/p/ascompress.

The openAsync() method opens a file for reading or writing asynchronously. This has no effect on how you need to structure code for writing data. You can call the write methods immediately after you call openAsync(). However, reading data requires that the data is available in the FileStream buffer before you can use it (e.g., display it in a component). When you call openAsync() using the read file mode, two events indicate when data is available: progress and complete. The progress event (which is of type ProgressEvent) notifies you each time more bytes are available in the buffer. The complete event notifies you when all the bytes have been read from the file into the buffer.

Example 21-2 shows how to read and write text to a file.

Example 21-2. Reading and writing a text file

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
        <![CDATA[

            // Create a file reference to a file on the desktop.
            private var _file:File = File.desktopDirectory.
resolvePath("example.txt");

            private function saveFile():void {
                var stream:FileStream = new FileStream();

                // Open the file for writing.
                stream.openAsync(_file, FileMode.WRITE);

                // Write the contents of the text area plus a timestamp.
                stream.writeUTFBytes(textArea.text + "

[this file saved on " +
(new Date()) + "]");
            }

            private function readFile():void {

                // Test if the file exists. If it does then open it for reading.
                // Otherwise, display a message to the user.
                if(_file.exists) {
                    var stream:FileStream = new FileStream();

                    // Listen for the complete event before trying to use the
                    // data.
                    stream.addEventListener(Event.COMPLETE, readCompleteHandler)
                    stream.openAsync(_file, FileMode.READ);
                }
                else {
                    textArea.text = "You must save the file before reading it";
                }
            }

            // Display the data in the text area.
            private function readCompleteHandler(event:Event):void {
                var stream:FileStream = event.target as FileStream;
                textArea.text = stream.readUTFBytes(stream.bytesAvailable);
            }
        ]]>
    </mx:Script>
    <mx:TextArea id="textArea" width="100%" height="80%" />
    <mx:Button label="Read" click="readFile();" />
    <mx:Button label="Save" click="saveFile();" />
</mx:WindowedApplication>

Although this example illustrates reading and writing text, you can use the same basic principles to read and write any sort of data, including binary data such as images and video.

Note

AIR will automatically serialize and deserialize custom data types when you write to a file and then read from a file if you have added the [RemoteClass] metadata tag to the custom data type class. Then you only need to write an instance of the class to the file using the writeObject() method of the file stream, and you can read the object from a file stream using readObject().

Using Local SQL Databases

AIR includes a SQLite database engine, which allows AIR applications to create and work with databases using Structured Query Language (SQL). Databases are exceptionally useful for AIR applications for a variety of reasons, including the following:

  • Creating sometimes-connected applications that have offline data storage that allows the application to be used even when the user is not connected to the Internet

  • Storing persistent application data for offline applications

AIR SQLite databases allow for a tremendous amount of functionality. In this chapter, we’ll focus exclusively on the basics, such as creating databases and database connections and running SQL statements. To learn more about all of the details of working with SQLite databases with AIR, visit the Adobe web site at http://livedocs.adobe.com/air/1/devappsflash/SQL_01.html.

Note

You can use SQLite Admin for AIR, written by Christophe Coenraets, to administrate SQLite databases on your computer. This program is available at http://coenraets.org/blog/2008/02/sqlite-admin-for-air-10.

Creating a database connection

When you want to work with a local database using AIR, you must first create a connection to the database using an instance of flash.data.SQLConnection. SQLite databases are written to disk as files. Therefore, when you create a SQLConnection object, you must tell it what file to use. You can do that using the openAsync() method, as in the following code:

var databaseFile:File = File.applicationStorageDirectory.resolveFile("example.db");
var sqlConnection:SQLConnection = new SQLConnection();
sqlConnection.openAsync(databaseFile);

Note

If a database file doesn’t yet exist, the default behavior of the openAsync() method is to create the file. You can optionally specify a second parameter that indicates the mode in which to open the database. You can use the flash.data.SQLMode constants of CREATE (the default), READ, and UPDATE for this purpose. If you use the read or update mode and the file does not exist, the SQLConnection object will dispatch an error event.

Typically, an AIR application will need to run SQL statements as soon as the connection is opened. For example, an AIR application may need to create tables or read data from existing tables when the application starts. Because the openAsync() method opens a connection asynchronously, you must listen for the open event before executing any SQL statements. Therefore, the preceding code would typically include adding an event listener for the open event, as in the following code example on the next page.

var databaseFile:File = File.applicationStorageDirectory.resolveFile("example.db");
var sqlConnection:SQLConnection = new SQLConnection();
sqlConnection.addEventListener(Event.OPEN, openHandler);
sqlConnection.openAsync(databaseFile);

Running SQL statements

Once you’ve established a connection to a database, you can execute SQL statements. The SQLite engine used by AIR supports most standard SQL statements, including CREATE TABLE, INSERT, UPDATE, DELETE, and SELECT, among others. Regardless of what SQL statements you want to run, the steps are the same:

  1. Create a flash.data.SQLStatement object.

  2. Set the sqlConnection property of the statement object to the SQLConnection object.

  3. Assign the SQL text to the text property of the statement object.

  4. Call the execute() method.

Example 21-3 creates a connection to a database when the application starts and then runs a CREATE TABLE SQL statement to create a table if it doesn’t yet exist.

Example 21-3. Running a basic SQL statement

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
    creationComplete="creationCompleteHandler();">
    <mx:Script>
        <![CDATA[
            private var _sqlConnection:SQLConnection;

            private function creationCompleteHandler():void {
                var file:File = File.applicationStorageDirectory.
resolvePath("example.db");
                _sqlConnection = new SQLConnection();
                _sqlConnection.addEventListener(Event.OPEN, openHandler);
                _sqlConnection.openAsync(file);
            }

            private function openHandler(event:Event):void {
                var statement:SQLStatement = new SQLStatement();
                statement.sqlConnection = _sqlConnection;
                statement.text = "CREATE TABLE IF NOT EXISTS inventory(" +
                                 "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                                 "title TEXT, isbn TEXT, count INTEGER)";
                statement.execute();
            }

        ]]>
    </mx:Script>
</mx:WindowedApplication>

Note

In Example 21-3, the columns of the table are declared as INTEGER and TEXT. These are called storage classes. SQLite supports the following storage classes: NULL, INTEGER, REAL, TEXT, and BLOB. Other types used by other database engines, such as VARCHAR, are automatically mapped to the corresponding SQLite storage classes (TEXT in the case of VARCHAR).

When a SQL statement successfully finishes executing, the SQLStatement object dispatches a result event of type flash.events.SQLEvent. This can be useful for making sure one dependent statement doesn’t run before the previous one is complete. Example 21-4 builds on Example 21-3 by listening for the result event for the CREATE TABLE statement and then running a SELECT statement. This example also adds a data grid and an ArrayCollection data provider for displaying the results of the SELECT statement.

Example 21-4. Retrieving table data

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
    creationComplete="creationCompleteHandler();">
    <mx:Script>
        <![CDATA[
            import mx.collections.ArrayCollection;

            private var _sqlConnection:SQLConnection;

            [Bindable]
            private var _tableData:ArrayCollection = new ArrayCollection();

            private function creationCompleteHandler():void {
                var file:File = File.applicationStorageDirectory.
resolvePath("example.db");

                _sqlConnection = new SQLConnection();
                _sqlConnection.addEventListener(Event.OPEN, openHandler);
                _sqlConnection.openAsync(file);
            }

            private function openHandler(event:Event):void {
                var statement:SQLStatement = new SQLStatement();
                statement.sqlConnection = _sqlConnection;
                statement.addEventListener(SQLEvent.RESULT, createHandler);
                statement.text = "CREATE TABLE IF NOT EXISTS inventory(" +
                                 "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                                 "title TEXT, isbn TEXT, count INTEGER)";
                statement.execute();
            }

            private function createHandler(event:Event):void {
                retrieveData();
            }

            private function retrieveData(event:Event = null):void {
                var statement:SQLStatement = new SQLStatement();
                statement.sqlConnection = _sqlConnection;
                statement.addEventListener(SQLEvent.RESULT, retrieveDataHandler);
                statement.text = "SELECT id, title, isbn, count FROM inventory";
                statement.execute();
            }

            private function retrieveDataHandler(event:SQLEvent):void {
            }

        ]]>
    </mx:Script>
    <mx:DataGrid id="tableData" dataProvider="{_tableData}"
        width="100%" height="50%" />
</mx:WindowedApplication>

At this point, the preceding example handles the result event of the SELECT statement, but it doesn’t actually retrieve the results. That requires an additional step, which we’ll look at in Retrieving resultsˮ later in the chapter. However, before retrieving results, the database must have data, which requires that we insert data. We’ll look at how to insert data next.

Using parameters to insert data

Inserting data into a local database is as simple as using a standard INSERT statement. You can also update existing records by using UPDATE statements. However, in both cases it is important to ensure that data that is written to the database is free of malicious SQL added by a user. Therefore, any parameters should be provided by way of a built-in parameterization mechanism of the SQLStatement class.

When you want to use a parameter with an INERT or UPDATE statement, you can use a named placeholder in the SQL text by preceding the placeholder with an at sign (@) or a colon (:). For example, the following creates two placeholders called @a and @b:

statementObject.text = "INERT INTO exampleTable(column1, column2) VALUES(@a, @b)";

When you use named placeholders, you must define the values using the parameters property of the statement object. The parameters property is an associative array in which you must add entries for each placeholder where the placeholders are used as keys. For example, the following defines @a and @b as the values from text input components:

statementObject.parameters["@a"] = textInputA.text;
statementObject.parameters["@b"] = textInputB.text;

When you define parameters in this way, you are making sure the user cannot intentionally or accidentally insert values that would be problematic. Example 21-5 shows statement parameterization in context. This example builds on Example 21-4, allowing the user to add a new record to the table via a form.

Example 21-5. Adding new records

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
    creationComplete="creationCompleteHandler();">
    <mx:Script>
        <![CDATA[
            import mx.collections.ArrayCollection;

            private var _sqlConnection:SQLConnection;

            [Bindable]
            private var _tableData:ArrayCollection = new ArrayCollection();

            private function creationCompleteHandler():void {
                var file:File = File.applicationStorageDirectory.
resolvePath("example.db");

                _sqlConnection = new SQLConnection();
                _sqlConnection.addEventListener(Event.OPEN, openHandler);
                _sqlConnection.openAsync(file);
            }

            private function openHandler(event:Event):void {
                var statement:SQLStatement = new SQLStatement();
                statement.sqlConnection = _sqlConnection;
                statement.addEventListener(SQLEvent.RESULT, createHandler);
                statement.text = "CREATE TABLE IF NOT EXISTS inventory(" +
                                 "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                                 "title TEXT, isbn TEXT, count INTEGER)";
                statement.execute();
            }

            private function createHandler(event:Event):void {
                retrieveData();
            }

            private function retrieveData(event:Event = null):void {
                var statement:SQLStatement = new SQLStatement();
                statement.sqlConnection = _sqlConnection;
                statement.addEventListener(SQLEvent.RESULT, retrieveDataHandler);
                statement.text = "SELECT id, title, isbn, count FROM inventory";
                statement.execute();
            }

            private function retrieveDataHandler(event:SQLEvent):void {
            }

            private function addBook():void {
                var statement:SQLStatement = new SQLStatement();
                statement.sqlConnection = _sqlConnection;

                // When the value is inserted run retrieveData() to query for the
                // latest table data.
                statement.addEventListener(SQLEvent.RESULT, retrieveData);
                statement.text = "INSERT INTO inventory(title, isbn, count)" +
                                 "VALUES(@title, @isbn, @count)";

                // Parameterize the statement using the values from the
                // text input controls
                statement.parameters["@title"] = bookTitle.text;
                statement.parameters["@isbn"] = bookIsbn.text;
                statement.parameters["@count"] = bookCount.text;
                bookTitle.text = "";
                bookIsbn.text = "";
                bookCount.text = "";
                statement.execute();
            }

        ]]>
    </mx:Script>
    <mx:DataGrid id="tableData" dataProvider="{_tableData}"
        width="100%" height="50%" />
    <mx:Form>
        <mx:FormItem label="title">
            <mx:TextInput id="bookTitle" />
        </mx:FormItem>
        <mx:FormItem label="isbn">
            <mx:TextInput id="bookIsbn" />
        </mx:FormItem>
        <mx:FormItem label="count">
            <mx:TextInput id="bookCount" restrict="0-9" />
        </mx:FormItem>
    </mx:Form>
    <mx:Button label="Add Book" click="addBook();" />
</mx:WindowedApplication>

Retrieving results

As you learned earlier, all SQL statements dispatch result events when they complete. This is true for all SQL statements regardless of whether they return a value or not. However, for some SQL statements this is particularly important. For example, when you run a SELECT statement you typically expect the statement to return a result set for use in the application. If you want to retrieve the result of a SQL statement you must handle the result event and then call the getResult() method of the SQLStatement object. The getResult() method returns a SQLResult object, which has a data property that is an array of the records returned. Example 21-6 shows this in context. This example builds on Example 21-5 by displaying the results in a data grid component.

Example 21-6. Displaying results in a data grid

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
    creationComplete="creationCompleteHandler();">
    <mx:Script>
        <![CDATA[
            import mx.collections.ArrayCollection;

            private var _sqlConnection:SQLConnection;

            [Bindable]
            private var _tableData:ArrayCollection = new ArrayCollection();

            private function creationCompleteHandler():void {
                var file:File = File.applicationStorageDirectory.
resolvePath("example.db");

                _sqlConnection = new SQLConnection();
                _sqlConnection.addEventListener(Event.OPEN, openHandler);
                _sqlConnection.openAsync(file);
            }

            private function openHandler(event:Event):void {
                var statement:SQLStatement = new SQLStatement();
                statement.sqlConnection = _sqlConnection;
                statement.addEventListener(SQLEvent.RESULT, createHandler);
                statement.text = "CREATE TABLE IF NOT EXISTS inventory(" +
                                 "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                                 "title TEXT, isbn TEXT, count INTEGER)";
                statement.execute();
            }

            private function createHandler(event:Event):void {
                retrieveData();
            }

            private function retrieveData(event:Event = null):void {
                var statement:SQLStatement = new SQLStatement();
                statement.sqlConnection = _sqlConnection;
                statement.addEventListener(SQLEvent.RESULT, retrieveDataHandler);
                statement.text = "SELECT id, title, isbn, count FROM inventory";
                statement.execute();
            }

            private function retrieveDataHandler(event:SQLEvent):void {
                var result:SQLResult = event.target.getResult();
                _tableData = new ArrayCollection(result.data);
            }

            private function addBook():void {
                var statement:SQLStatement = new SQLStatement();
                statement.sqlConnection = _sqlConnection;
                statement.addEventListener(SQLEvent.RESULT, retrieveData);
                statement.text = "INSERT INTO inventory(title, isbn, count)" +
                                 "VALUES(@title, @isbn, @count)";
                statement.parameters["@title"] = bookTitle.text;
                statement.parameters["@isbn"] = bookIsbn.text;
                statement.parameters["@count"] = bookCount.text;
                bookTitle.text = "";
                bookIsbn.text = "";
                bookCount.text = "";
                statement.execute();
            }


        ]]>
    </mx:Script>
    <mx:DataGrid id="tableData" dataProvider="{_tableData}"
        width="100%" height="50%" />
    <mx:Form>
        <mx:FormItem label="title">
            <mx:TextInput id="bookTitle" />
        </mx:FormItem>
        <mx:FormItem label="isbn">
            <mx:TextInput id="bookIsbn" />
        </mx:FormItem>
        <mx:FormItem label="count">
            <mx:TextInput id="bookCount" restrict="0-9" />
        </mx:FormItem>
    </mx:Form>
    <mx:Button label="Add Book" click="addBook();" />
</mx:WindowedApplication>

Managing Windows

As you’ve already read (and likely seen if you’ve tested any of the examples in this chapter), AIR applications run outside the web browser. That means AIR applications are responsible for managing their own windows. All AIR applications have at least one window, but they can also have more than one window. In the next few sections, we’ll look at a variety of topics related to AIR windows.

Creating a window

All AIR windows are instances of flash.display.NativeWindow. However, when building AIR applications using Flex it is far simpler to create windows using the Window component, which hides much of the complexity of working directly with NativeWindow objects. Therefore, when you want to create a window, you should create a new MXML document with a Window tag as the root tag, as in the following example:

<?xml version="1.0" encoding="utf-8"?>
<mx:Window xmlns:mx="http://www.adobe.com/2006/mxml">
</mx:Window>

When creating new windows, it’s important to consider what type of window you want to create. You’ll use two types of windows on a consistent basis: normal and utility. Normal windows use the full system chrome, and they appear in the Windows task bar and OS X window menu. Utility windows, on the other hand, use a system chrome without a title or icon, and they do not appear in the task bar or window menu. The main application window is typically a normal window, and usually you’ll want to use the normal type of window when creating new windows that are conceptually new instances of something within the application (e.g., new photos in a photo editing application) if you want those instances to show up on the task bar or window menu. Utility windows are usually best suited for palettes or other parts of an application that shouldn’t be accessible via the task bar or window menu.

You can specify the window type using the type property of the Window object. You cannot change the type of a window once it has been opened. The default type is normal. The following sets the type of a window using MXML:

<?xml version="1.0" encoding="utf-8"?>
<mx:Window xmlns:mx="http://www.adobe.com/2006/mxml" type="utility">
</mx:Window>

Windows also have a handful of properties you can set to affect the window appearance, such as the title bar and the status bar. Use the status property to customize the text that appears in the status bar and use the title property and the titleIcon property to customize the text and icon that appear in the title bar.

You can place any MXML or ActionScript code within a window document just as you would an application document or a component document.

Opening and closing windows

The first thing you must do if you want to open a window is create an instance of the window. You should use ActionScript to create the instance of the window, and not MXML. Example 21-7 creates an instance of a window (ExampleWindow) when the user clicks a button.

Example 21-7. Creating a new window

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
        <![CDATA[

            private function newWindow():void {
                var window:ExampleWindow = new ExampleWindow();
            }

        ]]>
    </mx:Script>
    <mx:Button label="New Window" click="newWindow();" />
</mx:WindowedApplication>

Once you have created an instance of a window, you can open it by calling the open() method, as in Example 21-8.

Example 21-8. Displaying a window

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
        <![CDATA[

            private function newWindow():void {
                var window:ExampleWindow = new ExampleWindow();
                window.open();
            }

        ]]>
    </mx:Script>
    <mx:Button label="New Window" click="newWindow();" />
</mx:WindowedApplication>

You can close a window using the close() method. However, closing a window in this fashion prevents the window from being reopened, so you should use the close() method only when you’re certain that you really want to close the window. In contrast, if you want to hide a window (but be able to reopen it later), you should merely set the visible property to false.

Users can also close windows by clicking on the window’s Close button, which has the same effect as calling the close() method. If you want to merely hide the window when the user clicks the Close button, you must do the following:

  1. Listen for the closing event for the window.

  2. When the closing event occurs cancel the event.

  3. Set the visible property of the window to false.

Example 21-9 illustrates how this works.

Example 21-9. Hiding a window instead of closing it

<?xml version="1.0" encoding="utf-8"?>
<mx:Window xmlns:mx="http://www.adobe.com/2006/mxml"
    width="200" height="200" closing="closingHandler(event);">
    <mx:Script>
        <![CDATA[

            private function closingHandler(event:Event):void {
                event.preventDefault();
                visible = false;
            }

        ]]>
    </mx:Script>
    <mx:Label text="New Window" />
</mx:Window>

Managing opened windows

An AIR application keeps references to all opened windows. This is important and useful for a variety of reasons. One common example of when this is useful is when the user closes the main application window and you want the entire application to close. By default, all opened windows remain open, even if the main application window closes. To close opened windows when the main window closes you must call the close() method of all the opened windows.

To retrieve references to all the opened windows you have to work with lower-level (ActionScript, not Flex component) types, such as flash.desktop.NativeApplication and flash.display.NativeWindow. Although Flex shelters you from working with these types directly in most cases, this is one instance in which you have to. All AIR applications have just one instance of NativeApplication, and that one instance is accessible via the nativeApplication property of the WindowedApplication instance. The NativeApplication instance has a property called openedWindows, which is an array of NativeWindow objects. Note that every Window component manages exactly one NativeWindow instance, and it is the NativeWindow instance that is stored in the openedWindows array, not the Window component. However, many of the Window and NativeWindow APIs overlap. For example, both have close() methods, allowing you to close all opened windows, as in the following example. In this example, when the user clicks the Close button for the main window, the application loops through all the opened windows and closes them:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
closing="closingHandler(event);">
    <mx:Script>
        <![CDATA[

            private function closingHandler(event:Event):void {
                var windows:Array = nativeApplication.openedWindows;
                for(var i:int = 0; i < windows.length; i++) {
                    windows[i].close();
                }
            }

            private function newWindow():void {
                var window:ExampleWindow = new ExampleWindow();
                window.open();
            }

        ]]>
    </mx:Script>
    <mx:Button label="New Window" click="newWindow();" />
</mx:WindowedApplication>

Working with Clipboards

AIR applications can utilize copy and paste as well as drag and drop behaviors, not only within the application itself but also between AIR applications and even between the AIR application and non-AIR applications or the operating system. For example, a user of an AIR application can drag and drop an image from the AIR application onto the desktop where it can be saved as an image file, and a user can copy an image from a web page and paste it into an AIR application.

Both the copy and paste and drag and drop behaviors use clipboards, and they both use similar operations. However, they are different enough that we’ll look at each independently.

Copy and paste

Copy and paste operations clearly have two poles: copying and pasting, each of which you’ll need to know how to handle in an AIR application. Both copying and pasting use the system clipboard, which is accessible via a static generalClipboard property of the flash.system.Clipboard class. The generalClipboard property is a reference to a Clipboard object that maps to the system clipboard that the computer system uses for storing and retrieving data for copy and paste operations. For example, if the user copies text from a text editor or a web page, that text gets written to the system clipboard and you can access it in an AIR application via the Clipboard.generalClipboard object.

To copy data from an AIR application to the system clipboard you use the setData() method. The setData() method requires that you specify two pieces of information: the format in which to write the data and the data to write to the clipboard. AIR recognizes four formats: text, bitmap, URL, and file list. These formats correspond to the TEXT_FORMAT, BITMAP_FORMAT, URL_FORMAT, and FILE_LIST_FORMAT constants of the flash.system.ClipboardFormats class, and they also correspond to four different MIME types that the system clipboard typically uses. Example 21-10 takes a screenshot of the AIR application contents when the user clicks on the button, and it writes the bitmap to the system clipboard. You can then paste the image into another application that accepts data of that format (such as Microsoft Word).

Example 21-10. Copying a snapshot to the system clipboard

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
        <![CDATA[

            private function takeSnapshot():void {
                var bitmapData:BitmapData = new BitmapData(width, height);
                bitmapData.draw(this);
                var clipboard:Clipboard = Clipboard.generalClipboard;
                clipboard.clear();
                clipboard.setData(ClipboardFormats.BITMAP_FORMAT, bitmapData);
            }

        ]]>
    </mx:Script>
    <mx:FileSystemDataGrid />
    <mx:Button label="Take Snapshot" click="takeSnapshot();" />
</mx:WindowedApplication>

Note

In Example 21-10, we call the clear() method on the clipboard before writing data to it. The clear() method removes all data in all formats from the clipboard. This is generally a good idea when working with the system clipboard because it ensures that other data in other formats will not be pasted into another application by mistake.

Pasting from the system clipboard into an AIR application is simply a matter of retrieving the data using the getData() method of the system clipboard. This requires that you know the format of the data you want to retrieve. The formats for retrieving data are the same as those for writing data to the system clipboard. Example 21-11 allows the user to paste an image into the application either by way of copying an image from an application (such as a web browser) or by copying a path (such as a URL) to an image.

Example 21-11. Pasting an image from a clipboard

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
        <![CDATA[

            var _bitmap:Bitmap;

            private function pasteImage():void {
                var clipboard:Clipboard = Clipboard.generalClipboard;
                if(clipboard.hasFormat(ClipboardFormats.BITMAP_FORMAT)) {
                    if(_bitmap == null) {
                        _bitmap = new Bitmap();
                        imageCanvas.rawChildren.addChild(_bitmap);
                    }
                    _bitmap.bitmapData = clipboard.getData(
ClipboardFormats.BITMAP_FORMAT) as BitmapData;
                    _bitmap.visible = true;
                    image.visible = false;
                    imageCanvas.width = _bitmap.bitmapData.width;
                    imageCanvas.height = _bitmap.bitmapData.height;
                }
                else if(clipboard.hasFormat(ClipboardFormats.TEXT_FORMAT)) {
                    image.source = clipboard.getData(
ClipboardFormats.TEXT_FORMAT) as String;
                    image.visible = true;
                    _bitmap.visible = false;
                }
            }

        ]]>
    </mx:Script>
    <mx:Canvas id="imageCanvas">
        <mx:Image id="image" />
    </mx:Canvas>
    <mx:Button label="Paste Image" click="pasteImage();" />
</mx:WindowedApplication>

You’ll notice in Example 21-11 that we cast the value retrieved from the getData() method. Because any data can be written to the clipboard, it is necessary to cast the return value appropriately if assigning it to a typed variable or property.

Drag and drop

Drag and drop operations use the same basic clipboard principles as copy and paste. However, instead of using the system clipboard, drag and drop operations require a nonsystem instance of the Clipboard class. That means you must construct an instance of the Clipboard class using a new statement. You can then write data to the clipboard and read data from the clipboard.

There are two aspects to a drag and drop operation: the drag initiation and the drop. Both rely on the flash.desktop.NativeDragManager class. For initiating a drag operation you can call the static doDrag() method, which requires that you pass it a display object that is triggering the drag operation and the clipboard to use. Example 21-12 allows a user to drag an image from the AIR application to another application that accepts bitmap data (such as Word).

Example 21-12. Dragging an image from an AIR application

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
        <![CDATA[

            private function imageMouseDownHandler():void {
                var bitmapData:BitmapData = new BitmapData(image.width,
image.height);
                bitmapData.draw(image);
                var clipboard:Clipboard = new Clipboard();
                clipboard.setData(ClipboardFormats.BITMAP_FORMAT, bitmapData);
                NativeDragManager.doDrag(image, clipboard);
            }

        ]]>
    </mx:Script>
    <mx:Image id="image"
        source="http://www.rightactionscript.com/samplefiles/image2.jpg"
        mouseDown="imageMouseDownHandler();" />
</mx:WindowedApplication>

Note

You’ll notice that in Example 21-12 the doDrag() method is called in the event handler for a mouseDown event. The doDrag() method will work only when called in an event handler for a mouseDown event or a mouseMove event with the mouse button down.

When you want to handle the drop aspect of a drag and drop operation, you need to listen for two primary events: nativeDragEnter and nativeDragDrop. The nativeDragEnter event occurs when the user drags something over an interactive object. At that point, you must determine whether that interactive object should accept drops for the data being dragged over the object. This is possible by calling the NativeDragManager.acceptDragDrop() method and passing it a reference to the interactive object. Only then will the object be capable of receiving notifications when the user actually drops anything on it, thus causing a nativeDragDrop event. Both nativeDragEnter and nativeDragDrop events are of type flash.events.NativeDragEvent, which has a clipboard property that references a Clipboard object that contains the data for the operation. Example 21-13 allows the user to drag an image (from a web page, for example) or a URL into the AIR application and drop it on the canvas.

Example 21-13. Dropping an image into an AIR application

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
        <![CDATA[

            private var _bitmap:Bitmap;

            private function nativeDragEnterHandler(event:NativeDragEvent):void {
                var clipboard:Clipboard = event.clipboard;
                if(clipboard.hasFormat(ClipboardFormats.BITMAP_FORMAT)) {
                    NativeDragManager.acceptDragDrop(imageCanvas);
                }
            }

            private function nativeDragDropHandler(event:NativeDragEvent):void {
                var clipboard:Clipboard = event.clipboard;
                if(clipboard.hasFormat(ClipboardFormats.BITMAP_FORMAT)) {
                    if(_bitmap == null) {
                        _bitmap = new Bitmap();
                        imageCanvas.rawChildren.addChild(_bitmap);
                    }
                    _bitmap.bitmapData = clipboard.getData(
ClipboardFormats.BITMAP_FORMAT) as BitmapData;
                    _bitmap.visible = true;
                    image.visible = false;
                    imageCanvas.width = _bitmap.bitmapData.width;
                    imageCanvas.height = _bitmap.bitmapData.height;
                }
                else if(clipboard.hasFormat(ClipboardFormats.TEXT_FORMAT)) {
                    image.source = clipboard.getData(
ClipboardFormats.TEXT_FORMAT) as String;
                    image.visible = true;
                    _bitmap.visible = false;
                }
            }

        ]]>
    </mx:Script>
    <mx:Canvas id="imageCanvas" backgroundColor="#FFFFFF" width="100%" height="100%"
        nativeDragEnter="nativeDragEnterHandler(event);"
        nativeDragDrop="nativeDragDropHandler(event);">
        <mx:Image id="image" />
    </mx:Canvas>
</mx:WindowedApplication>

Using HTML

Arguably one of the “coolest” features of AIR applications for Flex developers is the ability to render HTML content in Flex components. AIR includes the WebKit engine, the same HTML engine used by the Safari web browser, and this engine allows AIR applications to render HTML. Not only can you load and render HTML, but because it renders inside a Flex component, you can treat the component just as you would any other component, applying transforms, filters (blurs, etc.), and effects to the HTML.

All you need to do to render HTML in a Flex-based AIR application is to use the HTML component. You can set the URL to load and render for an HTML component by setting the location property. The following example loads the O’Reilly web site into an HTML component:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:HTML id="html" location="http://www.oreilly.com" width="100%" height="80%" />
</mx:WindowedApplication>

This example illustrates that you can apply a filter to an HTML component:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
        <![CDATA[

            private function toggleBlur():void {
                if(html. filters.length > 0) {
                    html. filters = [];
                }
                else {
                    html. filters = [new BlurFilter()];
                }
            }

        ]]>
    </mx:Script>
    <mx:HTML id="html" location="http://www.oreilly.com" width="100%" height="80%" />
    <mx:Button toggle="true" label="Toggle Blur" click="toggleBlur();" />
</mx:WindowedApplication>

If you test this example, you might notice that the filter is applied to the entire HTML component, including the scroll bars. If you prefer to access just the rendered HTML, you can access the lower-level HTMLLoader object nested within the HTML component via the htmlLoader property. The following rewrite of the preceding example applies the filter only to the HTML and not to the scroll bars:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
        <![CDATA[

            private function toggleBlur():void {
                if(html.htmlLoader.filters.length > 0) {
                    html.htmlLoader.filters = [];
                }
                else {
                    html.htmlLoader.filters = [new BlurFilter()];
                }
            }

        ]]>
    </mx:Script>
    <mx:HTML id="html" location="http://www.oreilly.com" width="100%" height="80%" />
    <mx:Button toggle="true" label="Toggle Blur" click="toggleBlur();" />
</mx:WindowedApplication>

You can navigate through the browsing history of an HTML component using the following properties and methods: historyLength, historyPosition, historyBack(), historyForward(), and historyGo(). The following example adds back and forward buttons:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
        <![CDATA[

            private function back():void {
                if(html.historyPosition > 0) {
                    html.historyBack();
                }
            }

            private function forward():void {
                if(html.historyPosition < html.historyLength - 1) {
                    html.historyForward();
                }
            }

        ]]>
    </mx:Script>
    <mx:HBox>
        <mx:Button label="Back" click="back();" />
        <mx:Button label="Forward" click="forward();" />
    </mx:HBox>
    <mx:HTML id="html" location="http://www.oreilly.com" width="100%" height="80%" />
</mx:WindowedApplication>
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset