none
Repaint Problem mit DrawVisual RRS feed

  • Frage

  • Hallo,

    ich habe folgende XAML Definition

    <Window x:Class="Graphics2D0T.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Animation" Height="350" Width="525">
      <DockPanel>
        <Button DockPanel.Dock="Bottom" Content="Move" Height="23" Name="button1" Width="475" Click="move_Click" />
        <Canvas DockPanel.Dock="Top" Name="canvas1" Width="493" Height="274" />
      </DockPanel>
    </Window>
    

    Dazu folgende code behind datei

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    
    namespace Graphics2D0T
    {
      /// <summary>
      /// Interaction logic for MainWindow.xaml
      /// </summary>
      public partial class MainWindow : Window
      {
        private AnimatedRectangle ac;
    
        public MainWindow()
        {
          InitializeComponent();
          ac = new AnimatedRectangle();
          canvas1.Children.Insert(0, ac);
        }
    
        private void move_Click(object sender, RoutedEventArgs e)
        {
          ac.StartMove();
        }
      }
    }

    und nachfolgende Klasse AnimatedRectangle

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows.Media;
    using System.Windows.Shapes;
    using System.Windows;
    using System.Threading;
    using System.Windows.Threading;
    
    
    namespace Graphics2D0T
    {
      class AnimatedRectangle : FrameworkElement
      {
        private VisualCollection _children;
        private int x;
        private int y;
    
        private static Action EmptyDelegate = delegate() { };
    
        public AnimatedRectangle()
        {
          _children = new VisualCollection(this);
          x = 10;
          y = 10;
          ShowAnimatedRectangle(0);
        }
    
        public void StartMove()
        {
          for (int i = 1; i < 150; i++)
          {
            x += 2;
            ShowAnimatedRectangle(120);
          }
        }
    
        private void ShowAnimatedRectangle(int sleep)
        {
          if (_children.Count > 0)
            _children.RemoveAt(0);
          DrawingVisual dv = new DrawingVisual();
          using (DrawingContext dctx = dv.RenderOpen())
          {
            dctx.DrawRectangle(Brushes.Red, null, new Rect(x, y, 100, 50));
            dctx.DrawRectangle(Brushes.Green, null, new Rect(x, y +50, 100, 50));
          }
          _children.Add(dv);
           this.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
          if (sleep > 0)
            Thread.Sleep(sleep);
        }
    
        // Provide a required override for the VisualChildrenCount property.
        protected override int VisualChildrenCount
        {
          get { return _children.Count; }
        }
    
        // Provide a required override for the GetVisualChild method.
        protected override Visual GetVisualChild(int index)
        {
          if (index < 0 || index >= _children.Count)
          {
            throw new ArgumentOutOfRangeException();
          }
    
          return _children[index];
        }
      }
    }
    

    Wenn ich im DrawingContext nur ein Rectangle mit DrawRectangle zeichne, dann wandert das Rechteck wie gewünscht von links nach rechts. Wenn ich allerdings zwei Rechtecke mit DrawRectangle zeichen, dann machen diese Rechtecke einige Bewegungen, dann frieren sie ein und springen später dann in einer Bewegung an die Endposition. Was mache ich da falsch?

    Vielen Dank

    Walter Taus

     

    Donnerstag, 10. März 2011 23:33

Alle Antworten

  • Bei mir klappt der Code.
    Was mir aufällt ist, dass die Rechtecke oder das Rechteck nie bis zum Ende kommen.
    Da scheint wohl ein Fehler in der Berechnung zu sein...
    Freitag, 11. März 2011 07:22
    Beantworter
  • Ist aber sehr interessant. Ich habe auf zwei Systemen mit Windows 7 und Net 4.0 getestet und habe auf beiden das gleiche Verhalten. Diese Animation ist eigentlich Teil einer viel größeren Animation, die bei mir natürlich auch nicht funktioniert.  Daher habe ich auch nicht wirklich darauf geachtet, dass die Rechtecke bis zum Ende wandern. Was könnte der Grund sein, dass der Code in meiner Umgebung nicht funktioniert?
    Walter Taus
    Freitag, 11. März 2011 13:14
  • Schau dir mal das an, evtl. hilft dir das weiter:

    http://msdn.microsoft.com/de-de/library/ms742196.aspx
    Freitag, 11. März 2011 14:27
    Beantworter


  • namespace Graphics2D0T
    {
    class AnimatedRectangle : FrameworkElement
    {
    ....

    private void ShowAnimatedRectangle(int sleep)
    {
    if (_children.Count > 0)
    _children.RemoveAt(0);
    DrawingVisual dv = new DrawingVisual();
    using (DrawingContext dctx = dv.RenderOpen())
    {
    dctx.DrawRectangle(Brushes.Red, null, new Rect(x, y, 100, 50));
    dctx.DrawRectangle(Brushes.Green, null, new Rect(x, y +50, 100, 50));
    }
    _children.Add(dv);
    this.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
    if (sleep > 0)
    Thread.Sleep(sleep);
    }


    }
    }

     

    Wenn ich im DrawingContext nur ein Rectangle mit DrawRectangle zeichne, dann wandert das Rechteck wie gewünscht von links nach rechts. Wenn ich allerdings zwei Rechtecke mit DrawRectangle zeichen, dann machen diese Rechtecke einige Bewegungen, dann frieren sie ein und springen später dann in einer Bewegung an die Endposition. Was mache ich da falsch ?

     

    Du verwendest Thread.Sleep.
    Das ist im UI-Umfeld und speziell bei WPF mit seinen DispatcherObjects und deren Thread-Affinität
    ganz furchtbar böse.
    Schätzungsweise bekommt der UI-Thread zwischen den Sleep-Phasen einfach nicht mehr genügend
    "Luft" um neu zu rendern und ggf zu measuren. Nebenbei sind die Benutzerelemente in der Schlafphase
    nicht mehr ansprechbar. Das muss nicht sein.
    Um ein Interval Wartezeit einzubauen, würde ich einen Timer, am besten einen Dispatcher-Timer,
    empfehlen, wie im Code unten.
    Jedenfalls flutschen die beiden Vierecke dann ganz gleichmässig von links nacht rechts über den Screen,
    fast schon wie in einem TranslateTransform ... :-)


    Christoph

      internal class AnimatedRectangle : FrameworkElement
      {
        private VisualCollection _children;
        private int x;
        private int y;
        private int cycles = 0;
        private const int CYCLES_MAX = 150;
    
        public AnimatedRectangle()
        {
          _children = new VisualCollection(this);
          x = 10;
          y = 10;
          ShowAnimatedRectangle();
        }
    
        public void StartMove()
        {
          cycles = 0;
    
          var timer = new DispatcherTimer(DispatcherPriority.Render);
          timer.Interval = new TimeSpan(120 * 1000); //120 millisec
          timer.Tick += OnDispatcherTimerTick;
          timer.Start();
        }
    
        private void OnDispatcherTimerTick(object sender, EventArgs e)
        {
          x += 2;
          ShowAnimatedRectangle();
          if (cycles++ >= CYCLES_MAX)
          {
            var timer = sender as DispatcherTimer;
            timer.Stop();
          }
        }
    
        private void ShowAnimatedRectangle()
        {
          if (_children.Count > 0)
          {
            _children.RemoveAt(0);
          }
          DrawingVisual dv = new DrawingVisual();
          using (DrawingContext dctx = dv.RenderOpen())
          {
            dctx.DrawRectangle(Brushes.Red, null, new Rect(x, y, 100, 50));
            dctx.DrawRectangle(Brushes.Green, null, new Rect(x, y + 50, 100, 50));
          }
    
          if (this.CheckAccess())
          {
            _children.Add(dv);
          }
          else
          {
            this.Dispatcher.Invoke(DispatcherPriority.Render, (Action)(() => _children.Add(dv)));
          }
        }
    
        // Provide a required override for the VisualChildrenCount property.
        protected override int VisualChildrenCount
        {
          get { return _children.Count; }
        }
    
        // Provide a required override for the GetVisualChild method.
        protected override Visual GetVisualChild(int index)
        {
          if (index < 0 || index >= _children.Count)
          {
            throw new ArgumentOutOfRangeException();
          }
    
          return _children[index];
        }
      }
    




    Sonntag, 13. März 2011 03:29
  • Hallo Christoph,

    vielen Dank, funktioniert super. Meine eigentliche Programmstruktur ist leider komplexer und involviert eine rekursive Methode.

    public void StartMove 
    {
      Move(0);
    }
    
    private void Move(int i) {
      if (i == 2 || i == 5) 
      {
        // jetzt sollte ShowAnimatedRectangle() durchgeführt werden
      }
      else
      {
         Move(++i);
         // jetzt sollte ShowAnimatedRectangle durchgeführt werden
         Move(--i);
      }
    }

    In der Rekursion, darf ich nur zu bestimmten Zeitpunkten die Rechtecke zeichnen. Die Rekursion muss dann für eine Zeitspanne angehalten werden, damit man am Schirm die Zeichenoperation wahrnehmen kann, und dann muss mit der Rekursion weitergemacht werden. Ich weiß nicht, wie ich die Zeichenoperation mit meiner Rekursion synchronisieren kann.

       Walter


    Walter Taus
    Sonntag, 13. März 2011 18:45
  • Hallo Christoph,

    vielen Dank, funktioniert super. Meine eigentliche Programmstruktur ist leider komplexer und involviert eine rekursive Methode.

    public void StartMove 
    {
     Move(0);
    }
    
    private void Move(int i) {
     if (i == 2 || i == 5) 
     {
      // jetzt sollte ShowAnimatedRectangle() durchgeführt werden
     }
     else
     {
       Move(++i);
       // jetzt sollte ShowAnimatedRectangle durchgeführt werden
       Move(--i);
     }
    }

     

    In der Rekursion, darf ich nur zu bestimmten Zeitpunkten die Rechtecke zeichnen. Die Rekursion muss dann für eine Zeitspanne angehalten werden, damit man am Schirm die Zeichenoperation wahrnehmen kann, und dann muss mit der Rekursion weitergemacht werden. Ich weiß nicht, wie ich die Zeichenoperation mit meiner Rekursion synchronisieren kann.

    Da kann ich von aussen nicht so viel zu sagen.
    Mir scheint die rekurisve Struktur wird dazu benötigt, eben immer wieder
    ShowAnimatedRectangle aufzurufen und dabei zunächst die "Steuervariable" i zu inkrementieren, nachher wieder zu
    dekrementieren.
    Da Du beim Aufruf über den Timer keinen Parameter übergeben kannst, müsstest Du diesen eben in eine Membervaraible auslagern.
    So genau kann ich Dir hier leider nicht assistieren, da ich Deine Programmstruktur nicht in Gänze
    und v.a. den eigentlichen Sinn und Zweck sowie die ganzen Feinheiten des Codes nicht kenne.

    Christoph
    Donnerstag, 17. März 2011 02:24
  • Hallo Christoph,

    eigentlich will ich eine Rekursion für die Türme von Hanoi programmieren. Dabei sollte nach jeder Bewegung einer Scheibe frisch gezeichnet werden und danach muss mit der Rekursion für einige Zeit gewartet werden damit die Zeichnoperation abgeschlossen werden kann. In Java funktioniert das hervorragend aber mit WPF finde ich keine Lösung.

      internal class Hanoi : FrameworkElement
      {
        public const int MIN_BRICK_X = 20;
        public const int MIN_BRICK_Y = 10;
        public const int DELTA_Y = 20;
        public const int GAP_X = 20;
        public const int GAP_Y = 20;
        public static SolidColorBrush[] BRUSHES_TABLE = { Brushes.Blue, Brushes.Green, Brushes.Yellow, Brushes.Gray, Brushes.Pink };
    
        private Turm[] towers;
        private VisualCollection _children;
        private DispatcherTimer timer;
    
        public Hanoi(int n)
        {
          towers = new Turm[3];
          towers[0] = new Turm("A", n);
          towers[0].Init();
          towers[1] = new Turm("B", n);
          towers[2] = new Turm("C", n);
          Width = n * MIN_BRICK_X * 3 + 6 * GAP_X;
          Height = n * MIN_BRICK_Y + DELTA_Y + GAP_Y;
          _children = new VisualCollection(this);
          timer = new DispatcherTimer(DispatcherPriority.Render);
          timer.Interval = new TimeSpan(250 * 1000);
          timer.Tick += OnDispatcherTimerTick;
          ShowTowers();
        }
    
        public void StartMove()
        {
          Move(towers[0].GetN, towers[0], towers[1], towers[2]);
         }
    
        private void OnDispatcherTimerTick(object sender, EventArgs e)
        {
        }
    
        private void Move(int n, Turm start, Turm zwischen, Turm ziel)
        {
          if (n == 1)
          {
            start.Shift(ziel);
            ShowTowers();
          }
          else
          {
            Move(n - 1, start, ziel, zwischen);
            start.Shift(ziel);
            ShowTowers();
            Move(n - 1, zwischen, start, ziel);
          }
        }
    
    
    
        private void ShowTowers()
        {
          if (_children.Count > 0)
            _children.RemoveAt(0);
          DrawingVisual dv = new DrawingVisual();
          using (DrawingContext dctx = dv.RenderOpen())
          {
            int n = towers[0].GetN;
            int ix = 0;
            foreach (Turm turm in towers)
            {
              Scheibe sch;
              int off = GAP_X * (2 + ix) + (n * ix) * MIN_BRICK_X;
              for (int i = n - 1; i >= 0; i--)
              {
                sch = turm.GetScheibe(i);
                if (sch == null)
                  continue;
                int v = sch.Size;
                dctx.DrawRectangle(BRUSHES_TABLE[v % BRUSHES_TABLE.Length], null, new Rect(off + (n - v) * MIN_BRICK_X / 2, (n - 1 - i) * MIN_BRICK_Y + DELTA_Y, v * MIN_BRICK_X, MIN_BRICK_Y));
              }
              ix++;
            }
            dctx.DrawRectangle(Brushes.Red, null, new Rect(GAP_X, n * MIN_BRICK_Y + DELTA_Y, n * MIN_BRICK_X * 3 + 4 * GAP_X, MIN_BRICK_Y));
          }
          if (this.CheckAccess())
            _children.Add(dv);
          else
            this.Dispatcher.Invoke(DispatcherPriority.Render, (Action)(() => _children.Add(dv)));
        }
    
        // Provide a required override for the VisualChildrenCount property.
        protected override int VisualChildrenCount
        {
          get { return _children.Count; }
        }
    
        // Provide a required override for the GetVisualChild method.
        protected override Visual GetVisualChild(int index)
        {
          if (index < 0 || index >= _children.Count)
          {
            throw new ArgumentOutOfRangeException();
          }
    
          return _children[index];
        }
    
      }
    }

    Am Ende der ShowTowers Methode müsste die weitere Durchführung für eine Zeitspanne angehalten werden (vor allem darf die Rekursion nicht weitergehen).

    Weitere benötigte Klassen nachfolgend

      class Turm
      {
        private string id;
        private Scheibe[] scheibenA;
    
        public Turm(String s, int n)
        {
          id = s;
          scheibenA = new Scheibe[n];
        }
    
        public void Init()
        {
          for (int i = 0; i < scheibenA.Length; i++)
            scheibenA[i] = new Scheibe(scheibenA.Length - i);
        }
    
        public Scheibe GetScheibe(int i) { return scheibenA[i]; }
    
        public int GetN { get { return scheibenA.Length; } }
    
        public void Shift(Turm z) {
    			int top = 0;
    			for (top = scheibenA.Length - 1; top >= 0; top--)
    				if (scheibenA[top] != null)
    					break;
          Console.WriteLine("Bewege Scheibe " + scheibenA[top].Size + " von Turm " + id + " nach Turm " + z.id);
    			for(int i = 0; i <= scheibenA.Length; i++){
    				if (z.scheibenA[i] == null) {
    					z.scheibenA[i] = scheibenA[top];
    					scheibenA[top] = null;
    					break;
    				}
    			}
    		}
    
      }
    
      class Scheibe
      {
        private int size;
    
        public Scheibe(int i) { size = i; }
    
        public int Size { get { return size; } }
    
      }
    

    Die xaml und die zugehörige code behind Datei sind ganz oben bereits enthalten.

    Über Ideen würde ich mich sehr freuen

       Walter


    Walter Taus
    Samstag, 19. März 2011 14:14
  • Hallo Walter

    ich habe es leider nicht zum Laufen gekriegt, sic!
    Auch bei mir sind erst alle Scheiben auf Turm A und am Ende alle auf C
    Dazwischen tut sich visuell nichts.
    Während der Rekursion der Move-Aufrufe wird OnRender gar nicht aufgerufen
    (erkennbar am ausbleibenden LayoutUpdated-Event).
    Es wird erst am Schluss aufgerufen, daher der korrekte Eindruck, dass es nur
    ein Start- und ein Schlußbild gibt aber keinen Übergang.
    Merkwürdigerweise ändert sich das auch nicht, wenn man das Neuzeichnen mit
    InvalidateVisual erzwingt. Dabei ist das eigentlich dazu da.
    Es scheint so, dass erst nach Verlassen des ganzen Move-Stacks das Neuzeichen
    ausgeführt werden kann.

    Mit einem Timer kommt der Algorithmus bzw. die Rekursion durcheinander und außer-
    dem werden die Timer-Ticks erst ausgeführt nachdem alle 512 Aufrufe von Move()
    abgearbeitet wurden - auch wenn die Timer-Priorität höchstmöglich ist (Send).

    Ich denke es würde gehen, wenn Move nicht rekursiv wäre - irgendwie mag WPF nicht rendern
    wenn es merkt dass in einem Stack ständig neu gelayoutet wird und der Stack
    noch nicht ganz verlassen ist (meine Vermutung).

    Übrigens, wenn Du Rectangle-Shapes in einem Canvas verschieben würdest, würde es
    schätzungsweise auch gehen, das könnte auch als Animation gehen, so dass man die
    Bewegung der Scheibe darstellen kann und außerdem kann man dabei auch eine Zeit
    einstellen wie lange der Übergang gehen soll.

    Christoph




    Montag, 21. März 2011 00:48