Friday, May 1, 2009

Beat Box Application - Part 2: Custom Panel Hit Testing

The beat box application is constructed using a nested set of custom panels. The Kit Panel is a vertical arrangement of ellipses, one for each drum sound. Kit Panels are arranged horizontally on a Beat Panel and played from left to right. The drum rhythm is controlled by a timer that generates 120 / beats per min. The complete source code for the beat box can be downloaded here:

http://drumpanel.codeplex.com/

Although it only took about 4 hours to build this application, during its development I discovered some code refactoring that I now use as best practice when creating custom panels. I thought I would discuss them here on the blog, because once implemented in the source code, the issues become obscured.

Custom panel layout and hit test behaviors

Did you know that the width and height of a custom panel are not calculated by default just because you load the XAML elements? Furthermore, you will not really notice this because the XAML elements will layout correctly even if the width and height are set to zero or NAN. However, having the correct width and height are critical to painting the background color and to detecting a mouse hit on the panel itself.


Watching this video could save you hours of fustration:

video


Taking advantage of WPF dependency properties

WPF is a model that forces you to think about graphic interaction differently than using HTML, WinForms, or ASP.NET. The introduction of binding and use of the dependency property pattern let me refactor the code into a very simplified form.
 public int MovingCursor
{
get { return (int)GetValue(MovingCursorProperty); }
set { SetValue(MovingCursorProperty, value); }
}

// Using a DependencyProperty as the backing store for Cursor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MovingCursorProperty =
DependencyProperty.Register("MovingCursor", typeof(int), typeof(BeatPanel),
new PropertyMetadata(0,OnCursorChange));

protected static void OnCursorChange(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
BeatPanel oPanel = obj as BeatPanel;
oPanel.KitRest((int)args.OldValue);
oPanel.KitPlay((int)args.NewValue);
}

What I really love about dependency properties in this example is that they automatically track changes in the value. When a value changes, you can work with both the old value and the new value. This is great in an application in which I am moving the cursor, because in this case I not only need to change the color of the background on the currently selected item, but I also need to clear the background color of the previously selected item, and start and stop the sounds. This in turn lets me simplify the API for playing the beat:
        public void PlayBeat()
{
MovingCursor++;
if (MovingCursor >= MaxCount)
MovingCursor = 0;
}
public void BackBeat()
{
MovingCursor--;
if (MovingCursor <= 0)
MovingCursor = MaxCount-1;
}

And it can easily be driven from the key board or mouse just by setting the value of the MovingCursor. Here are some examples:
        void BeatPanel_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point oPoint = e.GetPosition(null);
foreach (UIElement oElement in VisualTreeHelper.FindElementsInHostCoordinates(oPoint, this))
{
KitPanel oKit = oElement as KitPanel;
if (oKit != null)
MovingCursor = (int)oKit.Tag;
}
}

No comments: