3

I have to simulate a dot matrix display of 30*90. Dots (LEDs) that are on are colored red and dots (LEDs) that are off are colored black.

I played with the code, creating 2700 Ellipse, also using this technique, in any case the program uses too much RAM (about 80mb) and has performance issues (there is a delay when updating all points).

My question is: In what way can I deal whit this problem efficiently?

Community
  • 1
  • 1
alecardv
  • 922
  • 1
  • 12
  • 24
  • Quick question, are the dots circles(shape, polygons) or points(pixel)? – Philippe Paré Mar 24 '16 at 03:12
  • Now I'm using **System.Windows.Shapes.Ellypse** but does not is a strict rule – alecardv Mar 24 '16 at 03:40
  • Well, you can always go to integrate a nice direct3d surface for optimal performance. – TomTom Mar 24 '16 at 10:52
  • Read this [Can a DirectX surface be plotted to a WPF control?](http://stackoverflow.com/questions/7357231/can-a-directx-surface-be-plotted-to-a-wpf-control). – J3soon Mar 24 '16 at 11:06
  • What do you mean by "updating all points"? Is the list of 2700 dots being recreated / added to, or are you just updating the colors of all dots? – Joe Amenta Mar 24 '16 at 11:31
  • Also, what scale do you mean by "efficiently"? How quickly do you need to be able to redraw the dots? The answer I posted below can do a few refreshes per second, but that may still not be good enough for your requirements. – Joe Amenta Mar 24 '16 at 12:32
  • @J3soon Thanks, I think that is a great solution. A little complex but very nice. I'm gona read all and try it. – alecardv Mar 24 '16 at 15:36
  • @alecardv DirectX is something very useful when drawing huge amounts of shapes, but it takes a lot of time to learn... Good luck ;) – J3soon Mar 24 '16 at 15:41
  • @JoeAmenta I need to update the color of the points or redraw all of them. If the interface does not freeze is good for me. But when I say "efficiently" I'm talking more about the ram usage, I think 80mb is a lot of ram for 2700 points drawed on the screen. – alecardv Mar 24 '16 at 15:41

2 Answers2

3

The exact code below requires C#6, but the few C#6 things it uses could be done the same with a lower version with more annoying code. Tested with .NET 4.6.1 on Windows 7 x64.

For the most part, this should be pretty standard stuff for a WPF developer. Binding modes are set to just what we use, to squeeze out a little more. We use a single instance of PropertyChangedEventArgsinstead of what you usually see "newing one up", because that would add more unnecessary pressure on the GC, and you said 80 MB is "a lot", so this is already pushing it.

Comments inline on the trickier stuff, mainly focusing on the XAML.

The only somewhat "interesting" thing is the ItemsControl / Canvas trick. I think I picked this up from over here.

An earlier edit of the driver here had all dots being flipped at once every update, but that wasn't really showcasing the stuff you can do when you keep the bindings. It also had the converter expose Color values as wrappers around SolidColorBrush objects (forgot that I could just expose these as Brush properties, and the TypeConverter would make it just as easy to set in XAML).

XAML

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:me="clr-namespace:WpfApplication1"
        Title="MainWindow"
        DataContext="{Binding RelativeSource={RelativeSource Self}, Mode=OneTime}"
        SizeToContent="WidthAndHeight">
    <Window.Resources>
        <!-- A value converter that we can use to convert a bool property
             to one of these brushes we specify, depending on its value. -->
        <me:BooleanToBrushConverter x:Key="converter"
                                    TrueBrush="Red"
                                    FalseBrush="Black" />
    </Window.Resources>

    <!-- The main "screen".  Its items are the Dots, which we only need to read
         once (the collection itself doesn't change), so we set Mode to OneTime. -->
    <ItemsControl Width="300"
                  Height="900"
                  ItemsSource="{Binding Dots, Mode=OneTime}">

        <!-- We use just a single Canvas to draw dots on. -->
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>

        <!-- On each presenter, set the Canvas.Left and Canvas.Top attached
             properties to the X / Y values.  Again, the dots themselves don't
             move around after being initialized, so Mode can be OneTime. -->
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="ContentPresenter">
                <Setter Property="Canvas.Left"
                        Value="{Binding Path=XPos, Mode=OneTime}" />

                <Setter Property="Canvas.Top"
                        Value="{Binding Path=YPos, Mode=OneTime}" />
            </Style>
        </ItemsControl.ItemContainerStyle>

        <!-- Now, we just need to tell the ItemsControl how to draw each Dot.
             Width and Height are 10 (this is the same 10 that we multiplied x and
             y by when the Dots were created).  The outline is always black.
             As for Fill, we use IsOn to tell us which Brush to use.  Since IsOn
             is a bool, we use our converter to have it toggle the Brush. -->
        <ItemsControl.Resources>
            <DataTemplate DataType="{x:Type me:Dot}">
                <Ellipse Width="10"
                         Height="10"
                         Stroke="Black"
                         Fill="{Binding IsOn,
                                        Mode=OneWay,
                                        Converter={StaticResource converter}}" />
            </DataTemplate>
        </ItemsControl.Resources>
    </ItemsControl>
</Window>

Codebehind

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Windows;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            // Initialize all the dots.
            var dotSeq = from x in Enumerable.Range(0, 30)
                         from y in Enumerable.Range(0, 90)
                         select new Dot(x * 10, y * 10);

            Dot[] allDots = dotSeq.ToArray();
            this.Dots = new ReadOnlyCollection<Dot>(allDots);

            // Start a dedicated background thread that picks a random dot,
            // flips its state, and then waits a little while before repeating.
            BackgroundWorker bw = new BackgroundWorker();
            bw.DoWork += delegate { RandomlyToggleAllDots(allDots); };
            bw.RunWorkerAsync();

            this.InitializeComponent();
        }

        public ReadOnlyCollection<Dot> Dots { get; }

        private static void RandomlyToggleAllDots(Dot[] allDots)
        {
            Random random = new Random();
            while (true)
            {
                Dot dot = allDots[random.Next(allDots.Length)];
                dot.IsOn = !dot.IsOn;
                Thread.Sleep(1);
            }
        }
    }
}

Dot.cs

using System.ComponentModel;

namespace WpfApplication1
{
    public sealed class Dot : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        internal Dot(double xPos, double yPos)
        {
            this.XPos = xPos;
            this.YPos = yPos;
        }

        public double XPos { get; }

        public double YPos { get; }

        #region IsOn

        // use a single event args instance
        private static readonly PropertyChangedEventArgs IsOnArgs =
            new PropertyChangedEventArgs(nameof(IsOn));

        private bool isOn;
        public bool IsOn
        {
            get
            {
                return this.isOn;
            }

            set
            {
                if (this.isOn == value)
                {
                    return;
                }

                this.isOn = value;
                this.PropertyChanged?.Invoke(this, IsOnArgs);
            }
        }

        #endregion IsOn
    }
}

BooleanToBrushConverter.cs

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;

namespace WpfApplication1
{
    [ValueConversion(typeof(bool), typeof(Brush))]
    public sealed class BooleanToBrushConverter : IValueConverter
    {
        public Brush TrueBrush { get; set; }
        public Brush FalseBrush { get; set; }

        public object Convert(object value, Type _, object __, CultureInfo ___) =>
            (bool)value ? this.TrueBrush : this.FalseBrush;

        public object ConvertBack(object _, Type __, object ___, CultureInfo ____) =>
            DependencyProperty.UnsetValue; // unused
    }
}
Community
  • 1
  • 1
Joe Amenta
  • 4,662
  • 2
  • 29
  • 38
  • Another approach could be to do this with 2 `ListCollectionView`s on one source collection that use live shaping on the `IsOn` property, or perhaps instead of live shaping, we just defer refreshes throughout the duration of the full update. The relative advantages and disadvantages w.r.t. performance would depend partly on how many dots are expected to change during each update. – Joe Amenta Mar 24 '16 at 12:09
  • Your solution works very well, I'm trying to understand all. I'd appreciate if you add some comments. Thank you!. – alecardv Mar 24 '16 at 16:12
  • @alecardv: I've simplified a few things, added more comments, and changed the driver to just flip random selections of dots in a loop instead of randomly resetting every dot every iteration. – Joe Amenta Mar 25 '16 at 02:37
1

You may want to consider using DrawingVisual class which is the most high performance way of drawing vector images in WPF - see MSDN link. This approach is most like using GDI+ in Winforms. Be aware that things like hit testing / automatic layout / binding etc aren't available at this level - which may or may not be important to you.

auburg
  • 1,373
  • 2
  • 12
  • 22
  • Definitely consumes less memory, but I'm not sure if it is worth losing the ability to make binding. Anyway I'll try it. Thank you. – alecardv Mar 24 '16 at 16:17