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.
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.
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:/");
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.
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>
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:
Create a File
object that
references the nonexistent directory.
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.
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 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.
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.
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.
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()
.
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.
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.
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);
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);
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:
Create a flash.data.SQLStatement
object.
Set the sqlConnection
property of the statement object to the SQLConnection
object.
Assign the SQL text to the text
property of the statement
object.
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>
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.
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>
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>
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.
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.
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:
Listen for the closing event for the window.
When the closing event occurs cancel the event.
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>
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>
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 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>
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 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>
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>
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>