by Joe Ward

Joe Ward


10 June 2010

Rectangular windows are fine, and appropriate for most applications. However, just because they are the easiest to draw doesn't mean that all your windows have to be curve impoverished. The !Square sample application creates a window based on the ellipse, rather than the rectangle. The window uses vector graphics for most of its chrome, so there are no issues with bitmap scaling to limit the window size or aspect ratio.

The !Square sample application, shown in Figure 1, illustrates how to extend the NativeWindow class to create windows with alternative visuals and behavior using the AIR APIs and the rich Flash graphics capabilities.

Note: This is a sample application provided, as is, for instructional purposes.

This sample application includes the following files and classes:

  • NotSquare-app.xml: The AIR application descriptor file
  • A stub class that creates the main application window and closes
  • !Square.fla: Flash document for use with Adobe Flash CS3 Professional.
  • Extends the NativeWindow class
  • Extends the Sprite class to create a content container that clips anything outside the window client area
  • Implements the gripper controls for resizing the window
  • Implements a button for minimizing or restoring the window
  • Implements a button for maximizing the window
  • Implements a button for closing the window
  • Loads the images used for the dock and system tray icons
  • A class providing some sample content for the window
  • A visual object
  • Simulates spring forces
  • SourceViewer.js: Implements a source code browser using an HTML window
  • Sample AIR icon files and bitmap graphics used by the window chrome

Testing the application

Launch the !Square application (!Square.air). Resize the window using any of the eight grippers along the window drag bar. Move the window using the drag bar. Drag the white disk to the edge of the window to observe that any part of the disk that is outside the window client area is properly clipped.

Understanding the code

The !Square application uses several graphics functions not specific to AIR. For more information about these functions, see the ActionScript 3.0 Language Reference.

Extending the NativeWindow class

The RoundWindow class extends the AIR NativeWindow class to specialize the constructor and to define the methods and properties to draw its window chrome. Because you cannot use a custom class for the initial application window, the !Square example uses the initial window primarily to launch an instance of the RoundWindow class to serve as the main application window. The initial window created by AIR is never made visible and is closed once initialization is complete.

The constructor of the RoundWindow class creates its own NativeWindowInitOptions object and uses it to create the underlying native window. The constructor then creates the window chrome elements and activates the window to make it visible and active.

public function RoundWindow(title:String=""){ var initOptions:NativeWindowInitOptions = new NativeWindowInitOptions(); initOptions.systemChrome = NativeWindowSystemChrome.NONE; initOptions.transparent = true; super(initOptions); this.minSize = new Point(350,350); bounds = new Rectangle(0,0,viewWidth,viewHeight); this.title = title; stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; stage.addChild(clippedStage); //Put buttons on conventional side of window if(isWindows()){ closeButton = new CloseButton(287); minRestoreButton = new MinRestoreButton(283.5); maxButton = new MaxButton(280); } else { closeButton = new CloseButton(252); minRestoreButton = new MinRestoreButton(255.5); maxButton = new MaxButton(259); } addWindowDressing(); addEventListener(NativeWindowBoundsEvent.RESIZE,onBoundsChange); draw(viewWidth,viewHeight); activate(); }

Drawing elliptical borders

The border of the window is drawn using standard Flash drawing functions provided by the Graphics class, which can be accessed through the graphics property of any display object. The border is composed of three rings. When you draw overlapping figures between the calls to beginFill() and endFill(), only the difference between the figures, in this case ellipses, will be filled. The following function draws the three rings by drawing two ellipses for each ring:

with({ clear(); beginFill(bevelColor,1); drawEllipse(0,0,viewWidth,viewHeight); drawEllipse(4,4,viewWidth-8,viewHeight-8); endFill(); beginFill(borderColor,1); drawEllipse(4,4,viewWidth-8,viewHeight-8); drawEllipse(16,16,viewWidth-32,viewHeight-32); endFill(); beginFill(bevelColor,1); drawEllipse(16,16,viewWidth-32,viewHeight-32); drawEllipse(20,20,viewWidth-40,viewHeight-40); endFill(); }

Drawing a section of an ellipse

Because the Graphics class does not have a function for drawing just a piece of an elliptical arc, drawing the resizing gripper onto the border is actually more challenging than drawing the border itself. You could use the curveTo() method, but calculating the proper control point locations to match the elliptical border has its own mathematical challenges. For this task, !Square uses a simpler polyline technique based on the parametric equation of the ellipse. A point on the border is calculated based on the height and width and angle from the midpoint of the ellipse. Then the next point is calculated by increasing the angle a small amount and a line is drawn between them. This process is repeated until the desired arc is drawn.

Figure 2 shows the formula that can be used to calculate a point along the ellipse, given the angle, and the width and the height of the ellipse.

The grippers are arranged around the border at preset angles, so the angle is known. The width and height of the ellipse is the width and height of the window for the outer edge of the window border (viewWidth and viewHeight), and the width and height minus the thickness of the border for the inner edge of the window border.

The drawing routine uses the following formula to calculate the x and y coordinates of the four corners of the gripper, points A, B, C, and D:

var A:Point = new Point(); A.x = Math.cos(startAngle) * viewWidth/2; A.y = Math.sin(startAngle) * viewHeight/2; var B:Point = new Point(); B.x = Math.cos(startAngle) * (viewWidth-40)/2; B.y = Math.sin(startAngle) * (viewHeight-40)/2; var C:Point = new Point(); C.x = Math.cos(stopAngle) * (viewWidth-40)/2; C.y = Math.sin(stopAngle) * (viewHeight-40)/2; var D:Point = new Point(); D.x = Math.cos(stopAngle) * viewWidth/2; D.y = Math.sin(stopAngle) * viewHeight/2;;

The start and stop angles are calculated by adding and subtracting half the angular width of the gripper from the preset gripper angle. Defining the length with angles rather than a distance makes the math a bit easier and also provides a more pleasing effect when the window is resized since the gripper length stays proportional to the border diameter.

var startAngle:Number = (angleRadians - spreadRadians); var stopAngle:Number = (angleRadians + spreadRadians);

The routine next draws the shape of the gripper between the four points (see Figure 3).

First, a straight line is drawn from A to B:

moveTo(A.x,A.y); lineTo(B.x,B.y);

Next, a series of line segments is drawn between B and C. If enough segments are used, then the visual effect is indistinguishable from an actual curve. !Square uses ten segments, which seems sufficient.

for(var i:int = 1; i < 10; i++){ lineTo(Math.cos(startAngle + i * incAngle) * (viewWidth-40)/2, Math.sin(startAngle + i * incAngle) * (viewHeight-40)/2); }

Another straight line is drawn from C to D and the shape is closed by drawing a polyline segment from D back to A. The shape is started with the beginBitmapFill() method so when endFill() is called the area defined by the drawing commands is filled with a bitmap texture.

Clipping the client area

The RoundWindow class uses a sprite with a clipping mask applied as the container for its contents. Any content objects that are drawn outside the border of the window are clipped. Window chrome elements are added directly to the stage so that they are not clipped.

Clipping can be implemented in Flash by setting the mask property of a Sprite object with another Sprite object. The masking sprite is not drawn, but only the parts in the first sprite that fall under the shape defined by the mask's graphics commands are visible.

In !Square, the clipped sprite is defined by the ClippedStage class. This class creates a clipping mask using the familiar commands for drawing an ellipse based on the width and height of the window. Any part of an object added as a child of the ClippedStage object that falls outside the ellipse are clipped.

private function setClipMask(bounds:Rectangle):void{;,1);,0,bounds.width,bounds.height);; }

The class also listens for resize events from the parent window and responds by redrawing the clipping mask based on the new window dimensions.

private function onResize(event:NativeWindowBoundsEvent):void{ setClipMask(event.afterBounds); }

This type of clipping is not limited to simple shapes, so the technique can be used for any window that has areas which should be masked.

Resizing an elliptical window

To resize the window, the border and grippers must be redrawn based on the new width and height of the window. The resize event object includes an afterBounds property that reports the new dimensions. The width and height from this property are passed to the window draw() method.

private function onBoundsChange(boundsEvent:NativeWindowBoundsEvent):void{ draw(boundsEvent.afterBounds.width, boundsEvent.afterBounds.height); }

Handling content in an elliptical window

Because the content in the !Square window is aware of its container (the springs are attached to the border), it must react to changes in window size and shape. It does this by listening for the window resize event and recalculating the dependent variables.

private function onResize(event:NativeWindowBoundsEvent):void{ center.x = event.afterBounds.width/2; center.y = event.afterBounds.height/2; eye.x -= event.afterBounds.x - event.beforeBounds.x; eye.y -= event.afterBounds.y - event.beforeBounds.y; for each (var spring:Spring in springs){ spring.outerEnd = calculateSpringAnchorPoint(spring.anchorAngle); } }

Since the content is always animated, it does not need to be redrawn by the resize listener. Redrawing occurs on the next frame event anyway. For non-animated content, you may need to resize or reposition the window content objects directly.


Prerequisite knowledge

General experience of building applications with Flash Professional is suggested. For more details on getting started with this Quick Start, refer to Building the Quick Start sample applications with Flash.


Additional Requirements

User level