0

I am trying to simulate a LED display board with c# . I need a control which contains 1536 clickable controls to simulate LEDs (96 in width and 16 in Height). I used a panel named pnlContainer for this and user will add 1536 tiny customized panels at runtime. These customized panels should change their color by click event at runtime. Everything works . But adding this number of tiny panels to the container takes long time ( about 10 secs). What is your suggestion to solve this issue? Any tips are appreciated.

this is my custome panel:

public partial class LedPanel : Panel
{
    public LedPanel()
    {
        InitializeComponent();
    }

    protected override void OnPaint(PaintEventArgs pe)
    {
        base.OnPaint(pe);

    }

    protected override void OnMouseDown(MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left)
        {
            if (this.BackColor == Color.Black)
            {
                this.BackColor = Color.Red;
            }
            else
            {
                this.BackColor = Color.Black;
            }
        }
    }
}

and this is piece of code which adds tiny panels to the pnlContainer :

private void getPixels(Bitmap img2)
    {

        pnlContainer.Controls.Clear();

        for (int i = 0; i < 96; i++)
        {
            for (int j = 0; j < 16; j++)
            {

                Custom_Controls.LedPanel led = new Custom_Controls.LedPanel();
                led.Name = i.ToString() + j.ToString();
                int lWidth = (int)(pnlContainer.Width / 96);
                led.Left = i * lWidth;
                led.Top = j * lWidth;
                led.Width = led.Height = lWidth;
                if (img2.GetPixel(i, j).R>numClear.Value)
                {
                    led.BackColor = Color.Red;
                }
                else
                {
                    led.BackColor = Color.Black;
                }
                led.BorderStyle = BorderStyle.FixedSingle;


                pnlContainer.Controls.Add(led);

            }
        }

    }

application is very slow at runtime

Is there any better approach or better control instead of panelto do this?

Kia Boluki
  • 315
  • 3
  • 15
  • 2
    Yes. anything is better than adding 1k+ controls to a Winforms application. I suggest you do it all in code, i.e. code the Paint event and the MouseClcik to do all work needed. Also create a data structure used to look up where the clicks go and what state to paint where. Maybe a class or a tupel with a color or state and a rectangle or location.. - Also note that GetPixel is a slow operation as it does a lot more than one would hope for. Acceissng your data structure will be a lot faster, but loading from the image will always take some time inless you use lockbit, which atm premature.. – TaW Aug 20 '18 at 17:54
  • I also thought about this, but the problem is that I finally want to provide an array of black and red spots. How can I find the number and place of red dots if I use Paint – Kia Boluki Aug 20 '18 at 18:01
  • You need to store the data in some 2d-array of some data structure, maybe just bool. Then you can calculate where to paint what from the indices. Added advantage: you are flexible wrt 'led(pixel'-size.. – TaW Aug 20 '18 at 18:05
  • 1
    If you just want to optimize the intial load, maybe you can add the panels to a list instead of the controls collection. When all are there you can do an panel.Controls.AddRange(theList.ToArray()), which will be a lot faste than 1k+ single Adds.. – TaW Aug 20 '18 at 18:10
  • Even if you unlock the bitmap and use a 2D array to store the red-black pixels, drawing hundreds of small rects it will still be slow but wayyyy faster than what you have now. – γηράσκω δ' αεί πολλά διδασκόμε Aug 20 '18 at 18:24
  • 1
    Something like this: [Grid I can paint on](https://stackoverflow.com/questions/50497438/grid-i-can-paint-on?answertab=active#tab-top)? (VB.Net, but it's written in C#) In that example there are no controls, but each cell can have it's on event(s). – Jimi Aug 20 '18 at 18:36

2 Answers2

2

I agree with what @TaW recommends. Don't put 1000+ controls on a form. Use some sort of data structure, like an array to keep track of which LEDs need to be lit and then draw them in the Paint event of a Panel.

Here's an example. Put a Panel on a form and name it ledPanel. Then use code similar to the following. I just randomly set the values of the boolean array. You would need to set them appropriately in response to a click of the mouse. I didn't include that code, but basically you need to take the location of the mouse click, determine which array entry needs to be set (or unset) and then invalidate the panel so it will redraw itself.

public partial class Form1 : Form
{
    //set these variables appropriately
    int matrixWidth = 96;
    int matrixHeight = 16;

    //An array to hold which LEDs must be lit
    bool[,] ledMatrix = null;

    //Used to randomly populate the LED array
    Random rnd = new Random();

    public Form1()
    {
        InitializeComponent();

        ledPanel.BackColor = Color.Black;

        ledPanel.Resize += LedPanel_Resize;

        //clear the array by initializing a new one
        ledMatrix = new bool[matrixWidth, matrixHeight];

        //Force the panel to repaint itself
        ledPanel.Invalidate();
    }

    private void LedPanel_Resize(object sender, EventArgs e)
    {
        //If the panel resizes, then repaint.  
        ledPanel.Invalidate();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        //clear the array by initializing a new one
        ledMatrix = new bool[matrixWidth, matrixHeight];

        //Randomly set 250 of the 'LEDs';
        for (int i = 0; i < 250; i++)
        {
            ledMatrix[rnd.Next(0, matrixWidth), rnd.Next(0, matrixHeight)] = true;
        }

        //Make the panel repaint itself
        ledPanel.Invalidate();
    }

    private void ledPanel_Paint(object sender, PaintEventArgs e)
    {
        //Calculate the width and height of each LED based on the panel width
        //and height and allowing for a line between each LED
        int cellWidth = (ledPanel.Width - 1) / (matrixWidth + 1);
        int cellHeight = (ledPanel.Height - 1) / (matrixHeight + 1);

        //Loop through the boolean array and draw a filled rectangle
        //for each one that is set to true
        for (int i = 0; i < matrixWidth; i++)
        {
            for (int j = 0; j < matrixHeight; j++)
            {
                if (ledMatrix != null)
                {
                    //I created a custom brush here for the 'off' LEDs because none
                    //of the built in colors were dark enough for me. I created it
                    //in a using block because custom brushes need to be disposed.
                    using (var b = new SolidBrush(Color.FromArgb(64, 0, 0)))
                    {
                        //Determine which brush to use depending on if the LED is lit
                        Brush ledBrush = ledMatrix[i, j] ? Brushes.Red : b;

                        //Calculate the top left corner of the rectangle to draw
                        var x = (i * (cellWidth + 1)) + 1;
                        var y = (j * (cellHeight + 1) + 1);

                        //Draw a filled rectangle
                        e.Graphics.FillRectangle(ledBrush, x, y, cellWidth, cellHeight);
                    }
                }
            }
        }
    }

    private void ledPanel_MouseUp(object sender, MouseEventArgs e)
    {
        //Get the cell width and height
        int cellWidth = (ledPanel.Width - 1) / (matrixWidth + 1);
        int cellHeight = (ledPanel.Height - 1) / (matrixHeight + 1);

        //Calculate which LED needs to be turned on or off
        int x = e.Location.X / (cellWidth + 1);
        int y = e.Location.Y / (cellHeight + 1);

        //Toggle that LED.  If it's off, then turn it on and if it's on, 
        //turn it off
        ledMatrix[x, y] = !ledMatrix[x, y];

        //Force the panel to update itself.
        ledPanel.Invalidate();
    }
}

I'm sure there can be many improvements to this code, but it should give you an idea on how to do it.

Chris Dunaway
  • 10,974
  • 4
  • 36
  • 48
  • Please include a `DoubleBuffered = true;` in the constructor so it won't flicker! – TaW Aug 21 '18 at 07:09
  • @Chris Dunaway . Thank you . but how can I detect which `rectangle` is clicked . every `rectangle` should change its color by clicking from red to black or black to red and the corresponding value in the array should change – Kia Boluki Aug 21 '18 at 07:51
  • 1
    @mohammadboluki - To do that you have to handle the MouseUp event of the panel and use the `MouseEventArgs.Location` property to get the coordinates of the mouse click. Use those values to calculate the location in the boolean array and toggle that value. I added the code. – Chris Dunaway Aug 21 '18 at 16:26
  • @TaW - Adding `DoubleBuffered = true` to the form did not resolve the flicker. I had to create a new class named `LedPanel` that derives from `Panel`. I set `DoubleBuffered = true` in that constructor and that seemed to resolve the flicker. – Chris Dunaway Aug 21 '18 at 16:27
  • Indeed. I thought you were already using a class but that was the other answer; my bad. Sorry about that. You can also use a function to enable DoubleBuffering on any control. See [here](https://stackoverflow.com/questions/44185298/update-datagridview-very-frequently/44188565#44188565) – TaW Aug 21 '18 at 16:51
0

@Chris and @user10112654 are right. here is a code similar to @Chris but isolates the displaying logic in a separate class. (@Chris answered your question when I was writing the code :))))
just create a 2D array to initialize the class and pass it to the Initialize method.

    public class LedDisplayer
    {
        public LedDisplayer(Control control)
        {
            _control = control;
            _control.MouseDown += MouseDown;
            _control.Paint += Control_Paint;

            // width and height of your tiny boxes
            _width = 5;
            _height = 5;

            // margin between tiny boxes
            _margin = 1;
        }

        private readonly Control _control;
        private readonly int _width;
        private readonly int _height;
        private readonly int _margin;
        private bool[,] _values;

        // call this method first of all to initialize the Displayer
        public void Initialize(bool[,] values)
        {
            _values = values;
            _control.Invalidate();
        }

        private void MouseDown(object sender, MouseEventArgs e)
        {
            var firstIndex = e.X / OuterWidth();
            var secondIndex = e.Y / OuterHeight();
            _values[firstIndex, secondIndex] = !_values[firstIndex, secondIndex];
            _control.Invalidate(); // you can use other overloads of Invalidate method for the blink problem
        }

        private void Control_Paint(object sender, PaintEventArgs e)
        {
            if (_values == null)
                return;

            e.Graphics.Clear(_control.BackColor);
            for (int i = 0; i < _values.GetLength(0); i++)
                for (int j = 0; j < _values.GetLength(1); j++)
                    Rectangle(i, j).Paint(e.Graphics);
        }

        private RectangleInfo Rectangle(int firstIndex, int secondIndex)
        {
            var x = firstIndex * OuterWidth();
            var y = secondIndex * OuterHeight();
            var rectangle = new Rectangle(x, y, _width, _height);

            if (_values[firstIndex, secondIndex])
                return new RectangleInfo(rectangle, Brushes.Red);

            return new RectangleInfo(rectangle, Brushes.Black);
        }

        private int OuterWidth()
        {
            return _width + _margin;
        }

        private int OuterHeight()
        {
            return _height + _margin;
        }
    }

    public class RectangleInfo
    {
        public RectangleInfo(Rectangle rectangle, Brush brush)
        {
            Rectangle = rectangle;
            Brush = brush;
        }

        public Rectangle Rectangle { get; }
        public Brush Brush { get; }

        public void Paint(Graphics graphics)
        {
            graphics.FillRectangle(Brush, Rectangle);
        }
    }

this is how it's used in the form:

    private void button2_Click(object sender, EventArgs e)
    {
        // define the displayer class 
        var displayer = new LedDisplayer(panel1);

        // define the array to initilize the displayer
        var display = new bool[,]
        {
            {true, false, false, true },
            {false, true, false, false },
            {false, false, true, false },
            {true, false, false, false }
        };

        // and finally
        displayer.Initialize(display);
    }
Ali Doustkani
  • 728
  • 7
  • 14
  • 1
    Well meant but with basic errors. Winforms graphics basic rule #1 : Never use `control.CreateGraphics`! Never try to cache a `Graphics` object! Either draw into a `Bitmap bmp` using a `Graphics g = Graphics.FromImage(bmp)` or in the `Paint` event of a control, using the `e.Graphics` parameter.. – TaW Aug 21 '18 at 07:06