Monday, October 30, 2006

Gantt Charts in Flex DataGrids in less than 1 hour !

Here is an example of how you can use an item renderer in a mx:DataGrid to create a basic Gantt Chart. First things first, what is a Gantt Chart?
A Gantt chart is a popular type of bar chart that illustrates a project schedule. Gantt charts illustrate the start and finish dates of the terminal elements and summary elements of a project.
- http://en.wikipedia.org/wiki/Gantt_chart
Those of you who use Microsoft Project are already intimately familiar with Gantt Charts. They show a visual representation of task durations, with respect to sequential execution of tasks. They make it easier to visualize sequential and parallel tasks and determine task execution progress.

Q: What is one of the most commmonly used Flex elements for displaying data in horizontal rows?
A: A mx:DataGrid

Why reinvent the wheel? The mx:DataGrid already does just about everything that you could possibly need a Gantt Chart to do, except the graphical visualization, so why start anywhere else? Here is what I did...



First, I created a dataGrid to be the base of my Gantt Chart:
<mx:DataGrid top="0" left="0" right="0" bottom="0" id="dg" >
<mx:columns>
<mx:DataGridColumn headerText="Title" dataField="@title" width="80"/>
<mx:DataGridColumn headerText="Duration" dataField="@start" itemRenderer="renderers.SimpleGanttRenderer"/>
</mx:columns>
</mx:DataGrid>
I used a custom item renderer to to handle the graphical visualization within the mx:DataGrid. My datasource is an xml object that contains the title, start, duration of each task. I left it as integer values to keep the demo simple. Real world data would use actual dates, thus the logic in the item render would need to be updated to calculate the rectangle based off of acutual date values.
<GanttData>
<series duration="20">
<task duration="2" start="0" title="Task 1" />
<task duration="2" start="1" title="Task 2" />
<task duration="7" start="3" title="Task 3" />
<task duration="5" start="8" title="Task 4" />
<task duration="6" start="8" title="Task 5" />
<task duration="4" start="14" title="Task 6" />
<task duration="3" start="17" title="Task 7" />
</series>
</GanttData>
Within the item renderer, I extended the UIComponent class and overrode the updateDisplayList function to draw the rectangle for the gantt chart. Like I said before, this example is very simple. In a real world solution, you would probably want something that is more pleasing to the eye than a red rectangle. Or, you could color code the rectangles based on completion status.
package renderers
{
import mx.core.UIComponent;
import mx.core.IDataRenderer;
import flash.display.Graphics;
import mx.controls.listClasses.IListItemRenderer;
import mx.utils.GraphicsUtil;
import flash.geom.Rectangle;
import util.SimpleGanttUtil;
import flash.events.Event;
import mx.events.FlexEvent;
import mx.controls.listClasses.BaseListData;
import mx.controls.dataGridClasses.DataGridListData;

[Event(name="dataChange", type="mx.events.FlexEvent")]

public class SimpleGanttRenderer extends UIComponent implements IDataRenderer, IListItemRenderer
{
private var _data : Object = null;

[Bindable("dataChange")]
public function get data():Object
{
return _data;
}

public function set data(value:Object):void
{
this._data = value;
this.invalidateProperties();

dispatchEvent(new FlexEvent(FlexEvent.DATA_CHANGE));
}

override protected function updateDisplayList(w:Number, h:Number):void
{
super.updateDisplayList(w, h);

var g:Graphics = graphics;

g.clear();

if ( _data != null)
{
g.lineStyle(1, 0x000000, 1);
g.beginFill(0xFF0000, .5);

var r:Rectangle = calculateRectangle(w,h);

GraphicsUtil.drawRoundRectComplex(g, r.x, r.y, r.width, r.height, 0, 0, 0, 0);
g.endFill();
}
}

private function calculateRectangle(w:Number, h:Number) : Rectangle
{
var xmlData : XML = XML(_data);

var rect_x : int = 1+ ((w-2) * (xmlData.@start/SimpleGanttUtil.duration));
var rect_y : int = 1;
var rect_width : int = (w-2) * (xmlData.@duration/SimpleGanttUtil.duration);
var rect_height : int = h-2;

return new Rectangle( rect_x, rect_y, rect_width, rect_height );
}

}
}
Comments:
  1. This does not include linking of tasks. That will require a lot more work.
  2. This could easily be extended to show "now" within the updateDisplayList function.
  3. This demo does not show any dates or tooltips... These can be added very easily on the mx:DataGrid component.
  4. This entire demo, including the blog post took less than 2 hours. Getting the basic Gantt Chart working took less than 1 hour. Making it look good and writing this post took the rest of the time. :)
Any questions or comments, feel free to contact me at andrew.trice( at )cynergysystems.com.Enjoy!

Wednesday, October 25, 2006

Changing the header separator on a mx:DataGrid

I've been asked this numerous times...
How do you get rid of (or change) the vertical lines on the header of a mx:DataGrid? I know how to get rid of the horizontal and vertical gridlines on a datagrid using CSS/styles for the mx:DataGrid control, but just can't get the header lines to go away.
... And here is the quick and easy answer: There are many ways to do this. Looking at the API documentation for the mx:DataGrid control, you will see that there is a css style called "headerSeparatorSkin". The headerSeparatorSkin style defines the skin that is used to render the vertical line that separates the column headers in the datagrid control. The quickest and easiest way to get rid of those header separator lines was to change the header separator skin to a tansparent gif (or png) image.


<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="feedRequest.send();">

<mx:Style source="/styles.css" />

<mx:DataGrid
id="dg"
dataProvider="{feedRequest.lastResult.rss.channel.item}"
top="0" bottom="0" left="0" right="0"
headerSeparatorSkin="@Embed('/assets/transparent.gif')" >

<mx:columns>
<mx:DataGridColumn headerText="Posts" dataField="title"/>
<mx:DataGridColumn headerText="Date" dataField="pubDate"/>
</mx:columns>

</mx:DataGrid>

<mx:HTTPService
id="feedRequest"
url="http://www.cynergysystems.com/blogs/rss/andrewtrice"
useProxy="false" />

</mx:Application>
As I said before, that is not the only way to get rid of the header separator skin. You could also create a custom class (skin) that handles the drawing of the separator. In this class, basically tell it to do nothing, rather than drawing the actual separator. You'll notice that the rendered swf looks identical, but the code is slightly more complicated.



The code for the main functionality of this demo is exactly the same as the code above, except for one line:
headerSeparatorSkin="skin.DataGridHeaderSeparator"

This instructs the Flex application to use my skin.DataGridHeaderSeparator class to draw the header separator object, instead of using the default mx.skins.halo.DataGridHeaderSeparator object; Now, here is the code for my skin.DataGridHeaderSeparator class. You can see that I have overridden the measuredWidth property to always return 0 and the setActualSize function to clear the graphics for this component.
package skin
{
import mx.skins.halo.DataGridHeaderSeparator;

public class DataGridHeaderSeparator extends mx.skins.halo.DataGridHeaderSeparator
{
override public function get measuredWidth():Number
{
return 0;
}

override public function setActualSize(newWidth:Number, newHeight:Number):void
{
graphics.clear();
}
}
}

The second approach acutally has a slightly smaller file size, since it does not need to embed the 1x1 pixel transparent gif image.

Now, you can use this same approach to re-style your datagrid component's header separators. For instance, you could have images as the separators instead of just "the lines".



<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="feedRequest.send();">

<mx:Style source="/styles.css" />

<mx:DataGrid
id="dg"
dataProvider="{feedRequest.lastResult.rss.channel.item}"
top="0" bottom="0" left="0" right="0"
headerSeparatorSkin="@Embed('/assets/fx.png')" >

<mx:columns>
<mx:DataGridColumn headerText="Posts" dataField="title"/>
<mx:DataGridColumn headerText="Date" dataField="pubDate"/>
</mx:columns>

</mx:DataGrid>

<mx:HTTPService
id="feedRequest"
url="http://www.cynergysystems.com/blogs/rss/andrewtrice"
useProxy="false" />

</mx:Application>

Another possibility is that you could use the drawing API to draw an item as the header separator, instead of the default lines.



package skin
{
import mx.skins.halo.DataGridHeaderSeparator;
import flash.display.Graphics;

public class DataGridHeaderSeparator2 extends mx.skins.halo.DataGridHeaderSeparator
{
override public function get measuredWidth():Number
{
return 11;
}

override public function setActualSize(newWidth:Number, newHeight:Number):void
{
var g:Graphics = graphics;

g.clear();

g.beginFill(0xFF0000);
g.moveTo(0,0);
g.lineTo(11, 0);
g.lineTo(6,19);
g.lineTo(0,0);
g.endFill();
}
}
}

Really, this post just touches on a bigger picture: Flex skinning. Any component that has a skin can be re-skinned in these ways. "Skin by images" and "Skin by actionscript". For more on Flex skinning, be sure to check out these links.This is just scratching the surface on Flex skinning, you can expect much more on this in the future.

mx:ApplicationControlBar with a "true" dock to the bottom

There has been a thread on Flexcoders recently, asking how to dock a component to the bottom of a Flex application, specifically a mx:ApplicationControlBar.

For all components, there is an easy way: Just use css positioning to set bottom="0".

For a mx:ApplicationControlBar, there is an easy way, and a hard way to do this. The easy way is to not use a "true" dock to the application. Do not set dock="true" on the mx:ApplcationControlBar. As mentioned above, use css positioning to set bottom="0" left="0" and right="0". This will give the illusion that the ApplicationControlBar is docked to the bottom of the application. This will work for most people.

The hard way is to extend the mx.core.Application class and override the layoutChrome and get viewMetrics methods so that the ApplicationControlBar is truly docked at the bottom of the screen. You can just create an instance of an ApplicationControlBar and set docked="true".

What are the benefits of this approach? ... Extending the mx.core.Appliation class allows you to dock the control bar without using any css positioning tricks. Therefore, all componets inside of the application use the default layout of the application container. You won't need to do any positioning tricks to prevent your components from overlapping. Setting bottom="0" on a component within the application will render the bottom of the component directly above the ApplicationControlBar that is docked to the bottom of the screen.

Take a look at this example:



I started with the example from the ApplicationControlBar Flex 2 API documentation. I extended the mx.core.Application class and overrode the layoutChrome and get viewMetrics to change the border/view metrics for the application instance, and "physically" moved the controlBar location. I based this off of the framework SDK source, which everyone can view. On a default windows installation, this can be found in C:\Program Files\Adobe\Flex Builder 2\Flex SDK 2\frameworks\source\mx\core.
package components
{
import mx.core.Application;
import mx.core.EdgeMetrics;
import mx.core.IInvalidating;

public class BottomDockApplication extends Application
{
override public function get viewMetrics():EdgeMetrics
{
var vm:EdgeMetrics = super.viewMetrics;

var thickness:Number = getStyle("borderThickness");
vm.top = thickness;

if (controlBar && controlBar.includeInLayout)
{
vm.bottom += thickness;
vm.bottom += Math.max(controlBar.getExplicitOrMeasuredHeight(), thickness);
}

return vm;
}

override protected function layoutChrome(unscaledWidth:Number,
unscaledHeight:Number):void
{
super.layoutChrome(unscaledWidth, unscaledHeight);

var bm:EdgeMetrics = borderMetrics;
var thickness:Number = getStyle("borderThickness");

var em:EdgeMetrics = new EdgeMetrics();

em.left = bm.left - thickness;
em.top = thickness;
em.right = bm.right - thickness;
em.bottom = unscaledHeight - Math.max(controlBar.getExplicitOrMeasuredHeight(), thickness);

if (controlBar && controlBar.includeInLayout)
{
if (controlBar is IInvalidating)
IInvalidating(controlBar).invalidateDisplayList();
controlBar.setActualSize(width - (em.left + em.right), controlBar.getExplicitOrMeasuredHeight());
controlBar.move(em.left, em.bottom);
}
}

}
}

Thursday, October 12, 2006

FlashTracer: Flash/Flex Trace FireFox Plugin

Chris Scott, a fellow Cynergy employee introduced me to the FlashTracer FireFox plugin that was released earlier this week. It is awesome. You can get trace output from any .swf file, in any location, without running in debug mode. Here's a screen shot:



It has already proven extremely useful for me, and I've only been using it for about an hour now.

Multiple Cameras In Flex... Demystifying the Mystery

I've seen this in Flexcoders a number of times... How do you use multiple cameras within your flex application? I ran into the same issue for one of my projects, and here's what I have found. You CAN use multiple cameras within a flex application, but there are a few things that you will need to watch out for.

There is either a bug in the flash player/framework, or the documentation is inaccurate. I'm sure this has caused a number of headaches (other than those I directly experienced). The API documentation for the Camera object shows the signature of the Camera.getCamera method to be:
public static function getCamera(name:String = null):Camera
This implies that it is possible to get cameras based on the name of the camera, which one would assume to be Camera.names[ index ]. Well, this is not correct. It wouldn't work every time that I tried Camera.getCamera( Camera.names[ index ] )). After some experimentation, I realized that it would work if you pass the string representation of the camera index, rather than specifying the name of camera. For example, lets say that I have 3 cameras attached to the current system:

0: Camera A
1: Camera B
2: Camera C

According to the API documenation, using Camera.getCamera( "Camera B") should return a refference to Camera B, but it does not. I have found that using Camera.getCamera( "1") will return a valid refference to Camera B.

Below is a screenshot from a demo application that I put together that takes advantage of this trick, and will allow you to select a camera based on the string representation of the index. It shows you a list of all of the detected cameras in your system, and allows you to open display whichever one you select.



Before I post a link to actually use this application, I need to cover a few bases:
  1. The Flex API documentation does warn against using this method. I pursued this for my own needs.
    In general, you shouldn't pass a value for the name parameter; simply use getCamera() to return a reference to the default camera. By means of the Camera settings panel, the user can specify the default camera Flash should use.
    -http://livedocs.macromedia.com/flex/2/langref/flash/media/Camera.html
  2. Some cameras are detected with multiple aliases by the flash player. Trying to open a connection to one camera using multiple aliases will crash the flash player, and most likely crash your browser. One of my cameras, for instance shows up twice, as "Live! Cam Notebook Pro" and "Live! Cam Notebook Pro (VFW)". They both refer to the same physical camera. Trying to open a stream to both aliases ALWAYS crashes my browser. Opening multiple connections to the same alias does not cause a problem. I have had over 10 instances of the same camera open, without any problems.

  3. If you use this method, do not try to swap cameras on a Video object instance. Create a new Video object and attach the camera to it. It has been my experience that attempting to swap the source camera on an existing Video object will crash the flash player and subsequently crash the user's browser.

  4. The physical order of camera connections on your machine can make a difference. In one case on my laptop, I had Camera A attached at USB port 1 and Camera B attached at USB port 2. I could only get images from Camera A, not from Camera B. I swapped the USB ports for the 2 cameras, and then I could get images from both. Although, I did not have this problem on my desktop computer.
With that out of the way, here is my application:

View FLV Demonstration

Launch Application

** Side note: Be sure to check out the animations that I put on the MDI manager for window tiling and cascading. :)

Questions or comments, you can contact me at andrew.trice( at )cynergysystems.com.

ObjectUtil - Stop debugging the old way

As far as I know, not enough people take advantage of the ObjectUtil class. Everyone, from novice to expert should know about it, because it is extremely useful.

The function that I use the most is the "toString" method. The "toString" method "pretty-prints the specified Object into a String. All properties will be in alpha ordering".

I've noticed that a lot of people debug their applications by doing trace statements like:
trace( myObject.toString() );
trace( myObject.myAttribute.toString() );
trace( myObject.myProperty.toString() );
trace( myObject.myArray.toString() );
Wouldn't it be much easier if you could do that with one statement? Well, you can. Use the ObjectUtil.toString() method to get a string representation of your object including extended properties.
trace( ObjectUtil.toString( myObject ) );
Now, lets look at a real world example. In this demo I use a http service to get the latest RSS feed from MXNA. Then I compare the output of a toString statement and an ObjectUtil.toString based on the http service. Take a look at the output and you can decide for yourself which one works for you.



Launch Application
View Source
Download Source

Word of caution: ObjectUtil does not work in all scenarios. For instance: you will get an exception if you do this
trace( ObjectUtil.toString(mx.core.Application.application) );

Questions or comments, you can contact me at andrew.trice( at )cynergysystems.com.