Monday, March 3, 2008

Finally, Some Code! (Top Drawer, Part I)

As part of my learning the Flex platform, I plan to write some sample Flex applications to see how various things work in the system. In particular, I want to play around with MXML, ActionScript3, Flex components, Flash APIs, and general UI and graphics functionality in the platform. And I definitely want to play around with filter effects, states, and transitions, of course; I find them very moving.

To begin with, I wrote a simple vector drawing application. There's not enough of them out there, right? I remember a decent one on the original Macintosh in the mid 80's, but who's got one of those machines lying around these days? It's way easier to write a new version in Flex than to search around EBay for an old Mac and then find some place to store the thing when you're not using the drawing application.

TopDrawer: The Application

Here's the application, hopefully running here inline in your browser window.

(Apologies if it doesn't work for you; I think I've used some very basic tags to embed it in the page, and it may not work correctly if you aren't running the right version of the Flash player (version 9) that it requires.)

The cool thing about the application (apart from the amazingly beautiful UI, of course. Check out that gradient!) is that the basic GUI and the functionality of creating some simple shapes from mouse gestures was actually up and running within about an hour of my first attack on it. That seemed pretty good, for someone like me that's very new to the platform. Basic UI elements, simple mouse-handling, and simple manipulation of Flash Shape objects was quite straightforward. Finishing up the application to its current state too a bit longer, of course, as I made the UI more complex, added features, allowed selection and editing, and other nifty features. But I was heartened by my initial productivity on the application. My experience so far, albeit quite limited, is that Flex allows you to get up and running with a decent looking UI in pretty short order.

TopDrawer is pretty much like what you'd expect from a vector drawing application, although arguably a bit skimpy on features (like no Open/Save, for example, and only 6 drawing primitives, and limited editing capability, and, well, pretty big limitations overall). You select a drawing primitive from the panel, click and drag on the canvas to create it, select an object by clicking on it, and move it around with the mouse. There are some drawing attributes that you can change, such as the current color and stroke width. You can also change canvas attributes like whether drawing snaps to a grid, and what size the grid has.

Underneath, TopDrawer consists of a Flex Application component to hold the main GUI, a custom component for the canvas (a subclass of UIComponent), subclasses of Shape for the various geometric primitives, and various small helper classes. There are some particular aspects of the Flex stack that I wanted to learn more about, like custom components, styles, and events, so I made efforts to integrate some of those elements into the application.

A reasonable way to approach a blog like this where an application is discussed is to perfect the application first, and then present the finished code. This shows the correct way to go about this type of application and avoids any embarrassment over code that the author would later regret.

But where's the fun in that?

I figure the whole point of my writing this blog, especially at this point in my career at Adobe when I'm completely new to the Flex development platform, is that I can try to teach while I learn. Like if your Biology teacher didn't show up for class one day and one of the students got up and winged it instead. (Okay, hopefully I'll do a tad better than that, or at least better than I would trying to teach Biology). Because I think it's instructive to see not only what the finished code should be like, but why it's better than some other approaches that seemed like a good idea at the time.

So what I want to do instead is to show the application as I initially wrote it; not a finished product, but one which I was reasonably happy with for a first attack at the problem. I'll go over the various aspects of the code and application so that you can see how things fit together and make the application work. Hopefully, along the way, folks new to Flex, Flash, and ActionScript will learn a bit about coding in this new environment.

Then when I'm done, I'll make some changes to it and show why the changes make the code and the application better.

In case you're thinking “why should I spend the time looking at the wrong code?” I'll tip my hand and say that the initial version of the application is not actually bad; there's a lot in it that is perfectly valid for a Flex application. It's just that the later version of the application is better in some respects. So hopefully there is plenty to learn from both. When I reach a piece of code that will be improved upon later, [I'll note it like this] so that you can view it with the right degree of skepticism and an appropriately cocked eyebrow.

One more note: an entire walkthrough of the application, even this relatively small application, would take too much space for a single blog entry. So I'll stretch it over the next few entries, going over one or more files in each one. When I'm done, I'll post the entire source tree (what, you think I want to post it now and risk you skipping off before the stunning conclusion?)

Architecture

Before I dive into an actual code walkthrough, I wanted to explain how things work together at a high level.

The main application is driven from the file TopDrawer.mxml (which I'll be going over in this article, below). This file sets up the main GUI of the application, including the drawing canvas (our custom component, ArtCanvas), the drawing primitive palette (instances of a custom component that displays an icon, DrawingShapeIcon), and the drawing attribute selection items (a color chooser, a combobox for the line width, and a checkbox/textfield for the grid snap).

Most of the interaction of the application is handled in ArtCanvas.as, which tracks mouse drags and performs object creation/movement appropriately. Objects are created with subclasses of the Flash Shape object, which store the rendering operations necessary for each shape.

That's pretty much it (I told you it was a simple application). There are some other classes and pieces of functionality that I'll discuss in the context of the code walkthrough.

The Code

TopDrawer.mxml

You can view or download the full source for this file here; I'd suggest you bring up the file in a different window so that you can see the snippets I'll go over below in the larger context of the entire file.

This file sets up the main Application window. The logic in this file is mostly declarative, setting up the look of the GUI and hooking up events statically in MXML, although there is some dynamic logic specified in the file as well.

Let's take a look at the code:

    <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
           layout="absolute"  xmlns:comps="components.*"
           creationComplete="windowCreated();"
           keyDown="canvas.handleKeyDown(event)"> 
Some interesting thing to note here include:
  • Xmlns:comps=”components.*”: This is an xml-ism that says “I have some custom components in my own package that I will be referring to in here”. Everything prefaced by “mx” (such as the Application component itself) is a core Flex component. Components prefaced with ‘comps” (such as ArtCanvas, which we'll see soon) are my subclasses.
  • creationComplete=”windowCreated();”: This hooks out the creationComplete event on the window and asks Flex to call the windowCreated() function, where we'll run some additional tasks that are appropriate to perform at that time.
  • keyDown=”canvas.handleKeyDown(event)”: This hooks out the keyDown event on the application and calls a handler function to process the event. This is done in order to process delete-key events to remove the currently-selected object. As we'll see later, there's a better way to do this.
      <mx:Style>
           <!-- initial values for the snap grid - these can be changed in the GUI -->
           ArtCanvas {
             gridSize: 20;
             gridSnap: false;
           }
      </mx:Style> 

I declared a couple of custom styles in the ArtCanvas class, just for the heck of it (no, really: my goal in writing this application was to learn about various Flex capabilities). This section sets those styles, which control whether drawing is ‘snapped' to an invisible grid of the specified size, to some default values. Note that these values are also settable in the GUI itself (probably not the most obvious use of Styles ever created, but it was an excuse to play with Styles…).

      <mx:Script>
           <![CDATA[
             /**
              * Some  things need to happen at window creation time, such as adding
              * listeners  for a custom event to change the drawing mode
              */
             private function windowCreated():void
             {
                   trace("windowCreated");
                   // setFocus()  allows key events (like hitting the delete key) to be
                   // processed  automatically, without having to click in a component first
                   setFocus();
                   // These event  listeners are set up to be called when the canvas mode
                   // changes.  This allows all drawing-mode icons to highlight/de-highlight
                   // themselves  appropriately when the mode changes.
                   canvas.addEventListener("drawingModeChange",  lineIcon.modeChanged);
                   canvas.addEventListener("drawingModeChange",  scribbleIcon.modeChanged);
                   canvas.addEventListener("drawingModeChange",  rectangleIcon.modeChanged);
                   canvas.addEventListener("drawingModeChange",  rectangleFilledIcon.modeChanged);
                   canvas.addEventListener("drawingModeChange",  ellipseIcon.modeChanged);
                   canvas.addEventListener("drawingModeChange", ellipseFilledIcon.modeChanged);
                   // Setting the  initial mode here, after window creation, allows the appropriate
                   // mode icon  to be highlighted through the event listener mechanism above
                   canvas.currentShapeMode  = ArtCanvas.LINE;
             }
           ]]>
         </mx:Script> 

The Script/CDATA tags are the way that MXML files set apart a section of ActionScript code. You can write the code in a separate file and then simply include that file here, but it's sometimes convenient to write small functions in the same file.

In this case, I wrote a handler for the creationComplete event on the application window, and use that opportunity to set some properties. First, I call setFocus(), which sets input focus to the application window to begin with. This works in conjunction with the keyDown handler that I set up earlier so that delete-key events can automatically be processed. Without the setFocus() call, the window would not have input focus and the keyDown even would not be received.

[This setFocus() approach is not the best way to go about what I want. We'll find a better approach later]

There are then several calls to add eventListeners to the drawing primitive icons. This is done so that when the drawing mode is changed, which is propagated through the custom event drawingModeChange, all icons get the message and highlight or de-highlight themselves appropriately.

[Although this message-sending approach is better than simply having the icons peek into the inner workings of each other and the drawing canvas, it still creates a coupling between the canvas state and the icon state. We'll see a better way to handle this later.]

Finally, now that our event listeners are set up, we set the value of the canvas' current drawing mode, currentShapeMode, which will cause the events to get sent around and the appropriate icon to get highlighted in the palette.

That's all of the ActionScript code in the application file; now back to the GUI. First, we see the creation of our drawing canvas here:

      <comps:ArtCanvas id="canvas"  drawingColor="{drawingColorChooser.selectedColor}"
           strokeWidth="{lineWidth.selectedItem.width}"
           left="105"  top="10" right="10" bottom="10"/> 

This tag creates our custom component, ArtCanvas, and sets some initial properties on it. Note that we're setting the dimension properties directly because the toplevel window, our Application component, has an absolute layout. This would look different if we were using a layout manager here. Also, note that the basic GUI was created in the FlexBuilder3 design tool (a nifty WYSIWYG GUI builder), so the hard-coded values here actually came from that tool, based on where I placed things with the mouse.

The other interesting thing to note here is that we are creating a binding between the canvas' drawingColor and strokeWidth variables with the appropriate GUI items that control those values. So when the user changes the values in the color chooser or the line-width combobox, those values will automatically be changed in the canvas. Databinding rocks; no more setting up listeners to detect these changes and then doing boilerplate property setting appropriately.

[While the databinding aspect of this of this is very cool, this approach does set up a coupling between the canvas itself with the other GUI components. We'll see a better approach later]

    <mx:HBox x="10" y="10" width="87" height="108" horizontalAlign="center">
        <mx:VBox height="100%">
            <comps:DrawingShapeIcon id="lineIcon"
                source="@Embed(source='assets/icons/line.png')"
                mode="{ArtCanvas.LINE}"
                click="canvas.currentShapeMode = ArtCanvas.LINE;"
                width="32" height="32"/>

This next section sets up the palette of drawing modes. The first two tags are simple container/layout objects, where HBox lays out its children horizontally and VBox lays out its children vertically. The overall palette consists of the one HBox with 2 VBox children, where each VBox has three palette item. Each palette item is managed by a custom Image subclass, DrawingShapeIcon. All of the icons are set up similarly; I'll just go through this single “lineIcon” and assume that you get the idea.

We set up an id for each icon, which we refer to elsewhere when we set up the event listeners; in this case, the id is lineIcon. The source item is the image file that will be displayed in this Image subclass. Note that I chose to embed the icon image here. The Flex Image component allows you to either dynamically fetch the image at runtime, which allows you to deploy a smaller SWF file, or to embed the image in the SWF file, which enables faster loading of the image. In this case, the icon images are quite small, so I opted for embedding since the footprint was negligible.

The mode value is a property of DrawingShapeIcon and determines the drawing mode that will be selected when the user clicks on that icon. Note that mode is databound to the constant value LINE in ArtCanvas.

[This data-binding of constant values is the most obvious and terse way of initializing the mode value in mxml. But it's not the most efficient; we'll see a better approach later]

Speaking of clicking, we hook out the click event to perform a single line of ActionScript code which sets the value of the shape mode on the canvas. You can see from this that execution of ActionScript code is not relegated to <mx:Script> sections or entire other ActionScript files and classes; you can call ActionScript code directly in event handlers if you need to. Typically, you would only do this for very brief amounts of code, otherwise things could get unreadable pretty quickly.

      <mx:ColorPicker id="drawingColorChooser" x="10" y="126"  width="87" height="33"/> 

Next up is a simple color-chooser component. Note that we don't specify anything meaningful in this mxml code apart from the id and placement, but that an earlier component in GUI used the id of this item to data-bind between the currently chosen color and the current drawing color on the canvas.

    <mx:ComboBox id="lineWidth" x="10" y="167"  width="87"
            itemRenderer="components.LineWidthRenderer"  editable="false">
        <mx:ArrayCollection>
            <mx:Object width="1"  label="width 1"/>
            <mx:Object width="2"  label="width 2"/>
            <mx:Object width="3"  label="width 3"/>
            <mx:Object width="4"  label="width 4"/>
            <mx:Object width="5"  label="width 5"/>
        </mx:ArrayCollection>
    </mx:ComboBox> 

The ComboBox component allows a WYSIWIG way of setting the stroke width. Instead of just specifying the width with some numeric value, you can see the line width in the popup item directly and choose the appropriate one.

There are a couple of interesting things about the attributes for this component. One thing is the itemRenderer attribute; this specifies a custom component which will be called whenever the combobox needs to render one of the items in the popup. We'll see the code later, but this component basically gets the data value for the item (the width and label values above) and draws a line appropriately.

The other interesting thing about this component is the ArrayCollection item; this item is used as the "dataProvider" for ComboBox. It basically populates the popup menu with the Objects listed inside the ArrayCollection. These items are used later to determine how to render any particular item in the popup (by the itemRenderer, discussed above).

[Although this line-width component does most of what I wanted, you'll notice that the default display of the combobox (when the user is not clicking on it) displays a text-only representation of the value of the currently-selected item (which takes the value of the label field from the dataProvider). We'll see if we can get a more interesting graphical display of the selected item in a later attempt.]

    <mx:CheckBox id="snapCheckbox" x="10" y="197"  label="snap"
            change="{canvas.setStyle('gridSnap',  snapCheckbox.selected)}"
            selected="{canvas.getStyle('gridSnap')}"/>
        <mx:TextInput id="gridSizeField" x="70" y="197"  width="27"
               enabled="{snapCheckbox.selected}"
               text="{canvas.getStyle('gridSize')}"
               change="{canvas.setStyle('gridSize', gridSizeField.text)}"/> 

Lastly, we see the two items that control the grid styles. The CheckBox controls whether the snapping grid is active and the TextInput field controls the size of the grid.

Because the grid parameters were specified by Styles, versus properties, the code to change them is a bit more verbose than it would be otherwise. So, for example, when the checkbox component issues a change event, we need to send a new style value to the drawing canvas. Similarly, to have the checkbox reflect the value of the current selection value, we need to get the current style (which we then databind to, to automatically reflect changes in the style).

The text field is similar to the checkbox in its use of styles to send the values back and forth to the canvas. One interesting point is that the text field's enabled property is databound to the value of the snapCheckbox item's selected field. So the text field will only be editable if gridSnap is actually true; otherwise, the field is disabled and appears grayed-out to the user. It's a nice bit of UI completeness which really should be there in a real app, but tends to be tedious to wire up and therefore not necessarily implemented as a first-order item.

To Be Continued...

We're obviously not done; I've only shown the MXML code for the application window GUI. I also want to walk through the code for ArtCanvas, which handles the mouse interaction to create and edit the shapes, and some of the other ActionScript classes that store and render the shapes. Walking through these other files will show us more about ActionScript, Flex, and Flash programming than we have seen so far.

So look soon for the next in the series; it'll be absolutely Top Drawer.

6 comments:

Anonymous said...

Fantastic. I can see this blog being a really great journey for users just making their first Flexy steps. I like your idea of explaining the modifications you've made to improve it. Keep it up!

Yes, the flex app seems to run fine here, although the 'Clear' button appears to stop working once you've used it once.

Aziz said...

Excellent article.
It’s very interesting to see how much you can accomplish with so few lines of code (compared to Swing). Nice look and feel too.
Thanks.

Chet Haase said...

Aziz: To be clear (and honest), the code shown so far is not the full extent. So while I agree that it takes relatively few lines of mxml code to create a nice-looking UI, there's definitely a bit more going on here behind the scenes (like all of the logic for handing the mouse events and creating/editing the shapes).
I hope to walk through the other interesting files in the next blog or two, then you'll have seen it all.
It's not complex code, to be sure, but I just wanted to make sure that it's understood that I've only shown the code for the GUI layer so far....

thustle: Thanks for the sanity-check. I'll see what's up with the Clear functionality.

Chet Haase said...

thustle: the clear button problem is fixed, so it should work now (here and on the other parts in this series).

Anonymous said...

Is there a chance to save the drawing as a png?

Chet Haase said...

Tim: That's totally doable, but not with the current browser-based version of the app. If the app were ported to an AIR app, you'd have access to the AIR functionality, such as saving to local files. A demo for anyother day...