WPF: Text scrollen ähnlich einem Live-Ticker (mit Anhalten)

WPF: Text scrollen ähnlich einem Live-Ticker (mit Anhalten)

Gar nicht so leicht, einen deskriptiven Titel zu finden, der nicht schon wieder komisch klingt. Nun ja.

Ich habe für eine Anwendung mit folgenden Anforderungen eine Lösung gesucht:

  • WPF Anwendung
  • Scrollen ähnlich einem Nachrichtenticker
  • Einzelne Werte werden einzeln eingegeben und scrollen jeweils nur einen Platz weiter, das Scrollen stoppt als zwischendurch

So etwas läßt sich natürlich ohne Probleme z. B. mit einem Timer oder Thread erledigen, allein.. wenn schon WPF, dann wollte ich doch auch die bereits vorhandene Möglichkeit der Animation nutzen.

Relativ bald bin ich – überraschenderweise 😉 – bei StackOverflow fündig geworden (Antwort von Gishu).

Richtig Googeln

Manchmal habe ich das Gefühl, daß ein Großteil meiner Arbeit direkt aus Google herausfällt.
Ich stelle mal grob folgende Rechnung auf: 90% aller Probleme kann ich direkt lösen, auch ohne Handbuch/Hilfe. Bei vlt. 50% der Probleme hilft mir die Hilfe weiter. 90% der restlichen Probleme lassen sich per Google lösen. Der Rest sind dann natürlich die spannenden Probleme.

Wobei hier auch interessant ist, daß es ja ungefähr 100 Möglichkeiten gibt, nach einer Antwort zu suchen. Lange, deskriptive Suchterme – am besten auf Englisch – helfen ungemein. Schon des öfteren habe ich mich durch Seiten durchgehangelt – Stichwörter von passenden Seiten zu Suchbegriffe bei neuen Suchen umfunktioniert.

Nun ja – spannend jedenfalls, was es jetzt bereits alles im Internet gibt. Fast glaube ich schon, alles, was nur Standardprobleme beinhaltet programmiert sich fast von selbst. Ganz im Gegensatz zur Zeit vor 10 Jahren..

Eine kurze Einführung

Jedenfalls bin ich bei StackOverflow fündig geworden, habe den Code dort (ständiges Scrollen mit einem Wraparound) für meine Bedürfnisse angepaßt, und etwas aufgeräumt. Naja, minimal. Das Ganze lebt nach wie vor von globalen Variablen – allzu viele variable Eingabeparameter wird man ja nicht haben, wenn man nicht plötzlich 10 Textblöcke scrollen möchte. Zumindest greife ich nicht direkt auf die Werte im XAML zu, sondern setze diese an einer Stelle, so daß Änderungen hier mit einem sehr überschaubaren Aufwand durchgeführt werden könnten, so, wie es sein sollte.

Der Code

Window1.xaml.cs – der „Code Behind%rdquo;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Animation;
 
namespace WpfApplicationScroll
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }
 
        FrameworkElement container;
        TextBlock block;
        Queue<string> sq = new Queue<string>();
        const int maxQueueSize = 3;
 
        /// <summary>
        /// construct a concatenated string from the queue
        /// </summary>
        private string StringFromQueue()
        {
            string res = "";
            List<string> l = sq.ToList();
            l.Reverse();
            for (int i = 0; i < l.Count; ++i)
                res += ((i > 0) ? " « " : "") + l[i];
            return res;
        }
 
        /// <summary>
        /// calculate stringsize for output on screen
        /// </summary>
        private double GetStringSize(string s)
        {
            System.Globalization.CultureInfo enUsCultureInfo;
            Typeface fontTF;
            FormattedText frmmtText;
            enUsCultureInfo = System.Globalization.CultureInfo.GetCultureInfo("en-us");
            fontTF = new Typeface(block.FontFamily, block.FontStyle, block.FontWeight, block.FontStretch);
            frmmtText = new FormattedText(s, enUsCultureInfo, FlowDirection.LeftToRight, fontTF, block.FontSize, block.Foreground);
            double stringSize = frmmtText.Width;
            return stringSize;
        }
 
        /// <summary>
        /// get all relevant information for string animation
        /// </summary>
        private void AddStringAndGetParameters(string s, out string o, out double newx, out double stringsize)
        {
            string oldString = StringFromQueue();
            sq.Enqueue(s);
            string newString = StringFromQueue();
            if (sq.Count > maxQueueSize)
                sq.Dequeue();
            double oldSize = GetStringSize(oldString);
            double newSize = GetStringSize(newString);
            newx = oldSize - newSize;
            o = StringFromQueue();
            stringsize = GetStringSize(o);
        }
 
        int cnt = 0;
        private void button1_Click(object sender, RoutedEventArgs e)
        {
            block = this.textBlock1;
            container = this.canvas1;
            //for (int i = 0; i < 5; ++i)
            {
                double textBoxWidth = 10;
                double animationTimeInS = 0.3; // can be long - if you click 2x, animations don't interfere but start fresh
                string s = txtTexttoScroll.Text; // get string to add from textbox
                string text;
                double startx;
                double stopx = 0;
                AddStringAndGetParameters(s + cnt++, out text, out startx, out textBoxWidth);
                block.Width = textBoxWidth;
                block.Text = text;
                Storyboard _sb = new Storyboard();
                Duration durX = new Duration(TimeSpan.FromSeconds(animationTimeInS));
                DoubleAnimation daX = new DoubleAnimation(startx, stopx, durX);
                Storyboard.SetTargetName(daX, "rtTTransform");
                Storyboard.SetTargetProperty(daX, new PropertyPath(TranslateTransform.XProperty));
                _sb.Children.Add(daX);
                _sb.Begin(block);
            }
        }
    }
}

Window1.xaml – Beschreibung der Oberfläche

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<window x:Class="WpfApplicationScroll.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="150" Width="350">
    <grid>
        <dockPanel>
            <stackPanel DockPanel.Dock="Bottom" Orientation="Horizontal">
                <textBox x:Name="txtTexttoScroll">Some Text</textBox>
                <button x:Name="button1" Click="button1_Click">Add Text</button>
            </stackPanel>
            <grid>
                <grid.ColumnDefinitions>
                    <columnDefinition Width="*" />
                    <columnDefinition Width="10*" />
                    <columnDefinition Width="*" />
                </grid.ColumnDefinitions>
                <!--<canvas Name="canvas1" Height ="20" ClipToBounds="True" Background="AliceBlue" Grid.Column="1">-->
                <canvas Name="canvas1" Height ="20" ClipToBounds="True" Grid.Column="1">
                    <textBlock Canvas.Left="0" Canvas.Top="0" Height="18" Name="textBlock1" Width="{Binding ElementName=canvas1, Path=ActualWidth}" Text="Have a nice day!" FontSize="12" TextWrapping="NoWrap" VerticalAlignment="Center">
                    <textBlock.RenderTransform>
                        <transformGroup>
                            <scaleTransform ScaleX="1" ScaleY="1"/>
                            <skewTransform AngleX="0" AngleY="0"/>
                            <rotateTransform Angle="0"/>
                            <translateTransform x:Name="rtTTransform"/>
                        </transformGroup>
                    </textBlock.RenderTransform>
                    </textBlock>
                </canvas>
            </grid>
        </dockPanel>
    </grid>
</window>

Eine kurze Erklärung

Im Hauptfenster gibt es insbesondere einen Canvas und darin einen TextBlock. Der Canvas ist der Hintergrund – das, was man als Betrachter als eigentliche TextBox wahrnimmt. Das jedoch ist in Wahrheit der TextBlock, der seine Position im Canvas ändert und dadurch den Text, den er beinhaltet scrollen läßt.

Die Startposition ergibt sich so, daß ein neuer Eintrag links neben den aktuellen gesetzt wird, die Startposition ist also negativ. Anschließend wird soweit gescrollt, daß der String bei 0 im Canvas anlangt, und so komplett angezeigt wird.

Unterstützt wird das ganze von einer Queue, die die eingefügten Strings beinhaltet und so das Löschen alter Einträge erlaubt.

Der eingefügte Counter dient nur mir, um die Einträge besser unterscheiden zu können und hat im produktiven Einsatz vermutlich wenig verloren.

Motivation

Wichtig für mich waren das Scrollen von links, daß ein zusätzlicher String so erscheint, als würde er sich direkt in den vorhandenen String integrieren (er erscheint ja erst, wenn er hinzugefügt wird, es sieht jedoch so aus, als würde er von links hineinschweben). Auch wichtig war, daß die Anzeige stehen bleibt.

Eigentliche Motivation (ganz am Anfang, daher fällt es mir zuletzt ein) war, daß der Anwender besser mitbekommen soll, in welcher zeitlichen Reihenfolge Elemente hinzugefügt werden, wo also das neueste und wo das älteste Element sind. Das ist bei einem „harten” Ändern des Inhaltes einer TextBox nicht möglich. Und.. so wie Web 2.0 runde Buttons sind, so ist WPF weiche Übergänge (so wie in einer schlechten Excel-Präsentation).

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.