October 6, 2011

Creating a Crawl Control in WPF

A few weeks ago I was watching CNN when an interesting blurb scrolled by on the crawl at the bottom of the screen.  Normally, the crawl is not something I notice, but I thought to myself, "I wonder if I can implement one in WPF."  I had never done any animations before in WPF, and this seemed like a good way to start.

So I gave it a shot.

My initial instinct was to bring up Google and see if anyone else had done it.  I stopped myself because this was a good learning opportunity.  A chance to put my nascent XAML skills to the test.  Afterward I could research the topic and see what I did or did not do right.

My first challenge was to come up with a good visual layout that would enable the basic scrolling animation.  I needed a panel of some type and a control to hold the scrolling text.  The text would come from a bindable list so naturally an ItemsControl was the best choice.  It would form the banner that would scroll across the screen.

The panel was a little tougher.  In WinForms, I would update the Left property to increasingly negative values to scroll it to the left; however, WPF framework elements do not have a Left property.  WPF's control layout resembles HTML more than WinForms.  Enter the Canvas control.  It enables child elements to be positioned using coordinates, and I could animate the Canvas.Left attached property to scroll the banner.

The complete control template is listed below.

<ControlTemplate TargetType="{x:Type Controls:CrawlList}">
    <Border Background="{TemplateBinding Background}"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}">
    </span><span style="color: green">&lt;!-- Use a Canvas as the parent panel to take advantage of absolute
         positioning which makes the animation easier. --&gt;
    </span><span style="color: blue">&lt;</span><span style="color: #a31515">Canvas </span><span style="color: red">x</span><span style="color: blue">:</span><span style="color: red">Name</span><span style="color: blue">="crawlCanvas" </span><span style="color: red">VerticalAlignment</span><span style="color: blue">="Stretch"
            </span><span style="color: red">HorizontalAlignment</span><span style="color: blue">="Stretch"&gt;

        </span><span style="color: green">&lt;!-- An ItemsControl forms the scrolling banner. --&gt;
        </span><span style="color: blue">&lt;</span><span style="color: #a31515">ItemsControl </span><span style="color: red">x</span><span style="color: blue">:</span><span style="color: red">Name</span><span style="color: blue">="crawlItems"
                      </span><span style="color: red">ItemsSource</span><span style="color: blue">="{</span><span style="color: #a31515">TemplateBinding </span><span style="color: red">ItemsSource</span><span style="color: blue">}"
                      </span><span style="color: red">Canvas.Top</span><span style="color: blue">="0"
                      </span><span style="color: red">Canvas.Left</span><span style="color: blue">="{</span><span style="color: #a31515">TemplateBinding </span><span style="color: red">Left</span><span style="color: blue">}"
                      </span><span style="color: red">Canvas.Bottom</span><span style="color: blue">="{</span><span style="color: #a31515">Binding </span><span style="color: red">Width</span><span style="color: blue">,
                                      </span><span style="color: red">ElementName</span><span style="color: blue">=crawlCanvas}"
                      </span><span style="color: red">Canvas.Right</span><span style="color: blue">="{</span><span style="color: #a31515">Binding </span><span style="color: red">Height</span><span style="color: blue">,
                                     </span><span style="color: red">ElementName</span><span style="color: blue">=crawlCanvas}"&gt;

            </span><span style="color: green">&lt;!-- The default item template is a simple text block.
                 It can be updated to a different template using
                 CrawlList.ItemTemplate.--&gt;
            </span><span style="color: blue">&lt;</span><span style="color: #a31515">ItemsControl.ItemTemplate</span><span style="color: blue">&gt;
                &lt;</span><span style="color: #a31515">DataTemplate</span><span style="color: blue">&gt;
                    &lt;</span><span style="color: #a31515">TextBlock </span><span style="color: red">Text</span><span style="color: blue">="{</span><span style="color: #a31515">Binding</span><span style="color: blue">}" /&gt;
                &lt;/</span><span style="color: #a31515">DataTemplate</span><span style="color: blue">&gt;
            &lt;/</span><span style="color: #a31515">ItemsControl.ItemTemplate</span><span style="color: blue">&gt;

            </span><span style="color: green">&lt;!-- By default the ItemsPanel property contans a StackPanel
                 with a vertical orientation.  Replace it with a StackPanel
                 with a horizontal orientation. --&gt;
            </span><span style="color: blue">&lt;</span><span style="color: #a31515">ItemsControl.ItemsPanel</span><span style="color: blue">&gt;
                &lt;</span><span style="color: #a31515">ItemsPanelTemplate</span><span style="color: blue">&gt;
                    &lt;</span><span style="color: #a31515">StackPanel </span><span style="color: red">x</span><span style="color: blue">:</span><span style="color: red">Name</span><span style="color: blue">="crawlItemsPanel"
                                </span><span style="color: red">Orientation</span><span style="color: blue">="Horizontal" /&gt;
                &lt;/</span><span style="color: #a31515">ItemsPanelTemplate</span><span style="color: blue">&gt;
            &lt;/</span><span style="color: #a31515">ItemsControl.ItemsPanel</span><span style="color: blue">&gt;
        &lt;/</span><span style="color: #a31515">ItemsControl</span><span style="color: blue">&gt;
    &lt;/</span><span style="color: #a31515">Canvas</span><span style="color: blue">&gt;

&lt;/</span><span style="color: #a31515">Border</span><span style="color: blue">&gt;

</ControlTemplate>

In the control's template, the Canvas.Left attached property is bound to the CrawlControl's Left dependency property.  This was my workaround because I could not find a way to animate Canvas.Left directly.  It seems like a hack.  Perhaps there is a better way?

After hooking up the Left property, all I had to do was animate it with an instance of the DoubleAnimation class.  There is also a CrawlAnimation dependency property for custom animations.

private void StartCrawlAnimation()
{
    if ((_banner != null) && (_banner.ActualWidth > 0))
    {
        DoubleAnimationBase doubleAnimation =
            CrawlAnimation ?? BuildDefaultAnimation();
        BeginAnimation(LeftProperty, doubleAnimation);
    }
}

private void EndCrawlAnimation() { BeginAnimation(LeftProperty, null); }

private DoubleAnimationBase BuildDefaultAnimation() { double bannerWidth = _banner.ActualWidth; double fromValue = _crawlCanvas.ActualWidth; double toValue = -1 * bannerWidth; double speed = CrawlSpeed;

<span style="color: #2b91af">Duration </span>duration = <span style="color: blue">new </span><span style="color: #2b91af">Duration</span>(
    <span style="color: #2b91af">TimeSpan</span>.FromSeconds(bannerWidth / speed));

<span style="color: blue">return new </span><span style="color: #2b91af">DoubleAnimation</span>(fromValue, toValue, duration)
    { RepeatBehavior = <span style="color: #2b91af">RepeatBehavior</span>.Forever };

}

So there it is.  My first animation.  I was surprised at how easy WPF makes animations.

You can download the complete source code.  The code behind is in CrawlList.cs, and the template is in CrawlListStyle.xaml.  To see it in action, run the solution, and from the main window, click the "Crawl List" button to bring up a simple testing view.

© Joe Buschmann 2020