WPF Scrolling Marque Example (in C#)

Posted by Adrian Thu, 16 Jul 2009 21:28:00 GMT

If you're trying to implement a scrolling marquee in your WPF application, or just programatically moving text and other elements around a canvas, you might find this example useful.

This example requires a Window with nothing more than a Canvas. We've defined this in our XAML file:

<Window x:Class="WpfCanvasTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Component Workshop WPF Marquee Example" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen" ResizeMode="NoResize">
    <Grid>
    <Canvas x:Name="canvas1" ClipToBounds="True" Width="900" Height="75">
    </Canvas>
  </Grid>
</Window>

All of the action happens in the window's code. This is shown below. You can also download a zip containing the project files at the bottom of this page. There are three major aspects to the code: Adding the TextBlocks to the Canvas (see the code in AddTextBlock, it's nothing special), setting the initial position (this actually happens in the Dispatcher that is initiated in the window's contsructor. We have to do it in the dispatcher because we need the the .NET framework to calculate the widths of our TextBlocks, and this can't happen until we release the thread that we just used to create the TextBlocks). Finally, and the most complicated part of this example is to move each text block left by a certain amount each timer tick. The reason it is complicated is because when a TextBlock drops off the left-hand-side of the screen we remove it from the LinkedList that contains all the TextBlocks, add it to the end of the list and recalculate its position.

The linked list makes the code a little tricker. A simpler implementation might create the TextBlocks in a single StackPanel and then scroll just the panel. When it disappears off the side of the canvas, you could reset it back to the starting position and it'd start over again.

namespace WpfCanvasTest
{
  public partial class Window1 : Window
  {
    const double gap = 100.0; // pixel gap between each TextBlock
    const int timer_interval = 16; // number of ms between timer ticks. 16 is near 1/60th second, for smoother updates on LCD displays
    const double move_amount = 2.5; // number of pixels to move each timer tick. 1 - 1.5 is ideal, any higher will introduce judders

    private LinkedList<TextBlock> textBlocks = new LinkedList<TextBlock>();
    private Timer timer = new Timer();

    public Window1()
    {
      InitializeComponent();

      // A snapshot of today's news (16th July 2009)

      AddTextBlock("Headlines:");
      AddTextBlock("Enhanced Apollo footage released by NASA, original, high quality footage lost");
      AddTextBlock("UK Swine Flu deaths hit 29");
      AddTextBlock("Sport:");
      AddTextBlock("England batsmen struggle in final session on first day of second test against Australia for Ashes");
      AddTextBlock("Owen will be success -- Ronaldo");
      AddTextBlock("Tech:");
      AddTextBlock("Twitter exec's email hacked, stolen documents published on-line");
      AddTextBlock("62% of Sun's shareholders vote to approval Oracle deal");

      canvas1.Dispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(delegate(Object state)
      {
        var node = textBlocks.First;
        
        while (node != null)
        {
          double left = 0;

          if (node.Previous != null)
          {
            Canvas.SetLeft(node.Value, Canvas.GetLeft(node.Previous.Value) + node.Previous.Value.ActualWidth + gap);
          }
          else
          {
            Canvas.SetLeft(node.Value, canvas1.Width + gap);
          }
          
          node = node.Next;
        }

        return null;

      }), null);

      timer.Interval = timer_interval;
      timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
      timer.Start();
    }

    void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
      canvas1.Dispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(delegate(Object state)
      {
        var node = textBlocks.First;
        var lastNode = textBlocks.Last;

        while (node != null)
        {
          double newLeft = Canvas.GetLeft(node.Value) - move_amount;

          if (newLeft < (0 - (node.Value.ActualWidth + gap)))
          {
            textBlocks.Remove(node);

            var lastNodeLeftPos = Canvas.GetLeft(lastNode.Value);

            textBlocks.AddLast(node);

            if ((lastNodeLeftPos + lastNode.Value.ActualWidth + gap)> canvas1.Width) // Last element is offscreen
            {
              newLeft = lastNodeLeftPos + lastNode.Value.ActualWidth + gap;
            }
            else
            {
              newLeft = canvas1.Width + gap;
            }
          }
          
          Canvas.SetLeft(node.Value, newLeft);

          node = node == lastNode ? null : node.Next;
        }

        return null;

      }), null);
    }

    void AddTextBlock(string Text)
    {
      TextBlock tb = new TextBlock();
      tb.Text = Text;
      tb.FontSize = 28;
      tb.FontWeight = FontWeights.Normal;
      tb.Foreground = Brushes.Black;

      canvas1.Children.Add(tb);

      Canvas.SetTop(tb, 20);
      Canvas.SetLeft(tb, -999);

      textBlocks.AddLast(tb);
    }
  }
}

Download the source code and compiled demo.


About

We are a small British company that produces business-oriented software and solutions. These articles are a product of our daily work - information that we think might be useful to share. We hope you find them useful.

Our Software

These are some of our products. Several are open source, some are web-based and others are proprietary:

Categories

Archives

Syndicate

ml> ._trackPageview(); } catch(err) {} ml> l> pageTracker._trackPageview(); } catch(err) {} ml> ._trackPageview(); } catch(err) {} ml> l>