Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoiding creating PictureBoxes again and again

I've got the following problem. My intention is to move several images from the right to the left in a Windows Form. The code below works quite fine. What bothers me is the fact that every time a PictureBox object is created, this procedure eats up enormous amounts of memory. Each image follows the previous image uninterruptedly from the right to the left. The images display a sky moving from one side to another. It should look like a plane's flying through the air.

How is it possible to avoid using too much memory? Is there something I can do with PaintEvent and GDI? I'm not very familiar with graphics programming.

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Collections.Generic;

public class Background : Form
    {

        private PictureBox sky, skyMove;
        private Timer moveSky;
        private int positionX = 0, positionY = 0, width, height;
        private List<PictureBox> consecutivePictures;


        public Background(int width, int height)
        {

            this.width = width;
            this.height = height;

            // Creating Windows Form
            this.Text = "THE FLIGHTER";
            this.Size = new Size(width, height);
            this.StartPosition = FormStartPosition.CenterScreen;
            this.FormBorderStyle = FormBorderStyle.FixedSingle;
            this.MaximizeBox = false;


            // The movement of the sky becomes possible by the timer.
            moveSky = new Timer();
            moveSky.Tick += new EventHandler(moveSky_XDirection_Tick);
            moveSky.Interval = 10;
            moveSky.Start();


            consecutivePictures = new List<PictureBox>();



            skyInTheWindow();

            this.ShowDialog();

        }

        // sky's direction of movement
        private void moveSky_XDirection_Tick(object sender, EventArgs e)
        {

            for (int i = 0; i < 100; i++)
            {
                skyMove = consecutivePictures[i];

                skyMove.Location = new Point(skyMove.Location.X - 6, skyMove.Location.Y);

            }


        }

        private void skyInTheWindow()
        {

            for (int i = 0; i < 100; i++)
            {
                // Loading sky into the window
                sky = new PictureBox();
                sky.Image = new Bitmap("C:/MyPath/Sky.jpg");
                sky.SetBounds(positionX, positionY, width, height);
                this.Controls.Add(sky);

                consecutivePictures.Add(sky);

                positionX += width;
            }   

        }

    }
like image 368
Lucky Buggy Avatar asked Nov 09 '22 18:11

Lucky Buggy


1 Answers

You seem to be loading the same bitmap 100 times. There's your memory problem right there, not the 100 PictureBoxs. A PictureBox should have a low memory overhead because they don't include the image in their memory consumption, it is the referenced Bitmap that is much more likely to consume large amounts of memory.

It's easily fixed - consider loading the bitmap once and then applying it to all your PictureBoxs.

Change:

    private void skyInTheWindow()
    {

        for (int i = 0; i < 100; i++)
        {
            // Loading sky into the window
            sky = new PictureBox();
            sky.Image = new Bitmap("C:/MyPath/Sky.jpg");
            sky.SetBounds(positionX, positionY, width, height);
            this.Controls.Add(sky);

            consecutivePictures.Add(sky);

            positionX += width;
        }   

    }

...to:

    private void skyInTheWindow()
    {
        var bitmap = new Bitmap("C:/MyPath/Sky.jpg");  // load it once

        for (int i = 0; i < 100; i++)
        {
            // Loading sky into the window
            sky = new PictureBox();
            sky.Image = bitmap; // now all picture boxes share same image, thus less memory
            sky.SetBounds(positionX, positionY, width, height);
            this.Controls.Add(sky);

            consecutivePictures.Add(sky);

            positionX += width;
        }

    }

You could just have a single PictureBox stretched to the width of the background but shift it over time. Of course you'll need to draw something on the edge where a gap would appear.

You might get a bit of flicker with repeated PictureBox though which is one of the things I'm worried about but it might still serve.

Or what I'd do is create a UserControl and override OnPaint and just turn it into a draw bitmap issue and not have PictureBoxs at all. Much faster and efficient and no flicker. :) This is purely optional

You have the potential to eliminate any flicker too if you draw first to an offscreen Graphics and Bitmap and "bitblit" the results to the visible screen.

Would you mind giving me some code which serves as a point of reference because for me it's hard to implement into code? I'm not very familiar in graphics programming and I really want to learn from one another. The code without flickering is better

As requested I have included the code below:

Flicker Free Offscreen Rendering UserControl

Essentially what this does is to create an offscreen bitmap that we will draw into first. It is the same size as the UserControl. The control's OnPaint calls DrawOffscreen passing in the Graphics that is attached to the offscreen bitmap. Here we loop around just rendering the tiles/sky that are visible and ignoring others so as to improve performance.

Once it's all done we zap the entire offscreen bitmap to the display in one operation. This serves to eliminate:

  • Flicker
  • Tearing effects (typically associated with lateral movement)

There is a Timer that is scheduled to update the positions of all the tiles based on the time since the last update. This allows for a more realistic movement and avoids speed-ups and slow-downs under load. Tiles are moved in the OnUpdate method.

Some important properties:

  • DesiredFps - desired frames/second. This directly controls how frequently the OnUpdate method is called. It does not directly control how frequently OnPaint is called

  • NumberOfTiles - I've set it to your 100 (cloud images)

  • Speed - the speed in pixels/second the bitmaps move. Tied to DesiredFps. This is a load-independent; computer-performance-independent value

Painting If you note in the code for Timer1OnTick I call Invalidate(Bounds); after animating everything. This does not cause an immediate paint rather Windows will queue a paint operation to be done at a later time. Consecutive pending operations will be fused into one. This means that we can be animating positions more frequently than painting during heavy load. Animation mechanic is independent of paint. That's a good thing, you don't want to be waiting for paints to occur.

You will note that I override OnPaintBackground and essentially do nothing. I do this because I don't want .NET to erase the background and causing unnecessary flicker prior to calling my OnPaint. I don't even bother erasing the background in DrawOffscreen because we're just going to draw bitmaps over it anyway. However if the control was resized larger than the height of the sky bitmap and if it is a requirement then you may want to. Performance-hit is pretty negligible I suppose when you are arguably drawing multiple sky-bitmaps anyway.

When you build the code, you can plonk it on any Form. The control will be visible in the Toolbox. Below I have plonked it on my MainForm.

NoFlickerControl in the Toolbox

The control also demonstrates design-time properties and defaults which you can see below. These are the settings that seem to work well for me. Try changing them for different effects.

Design-time properties

If you dock the control and your form is resizable then you can resize the app at runtime. Useful for measuring performance. WinForms is not particularly hardware-accelerated (unlike WPF) so I wouldn't recommend the window to be too large.

Code:

#region

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using SkyAnimation.Properties;

#endregion

namespace SkyAnimation
{
    /// <summary>
    /// </summary>
    public partial class NoFlickerControl : UserControl
    {
        #region Fields

        private readonly List<RectangleF> _tiles = new List<RectangleF>();
        private DateTime _lastTick;
        private Bitmap _offscreenBitmap;
        private Graphics _offscreenGraphics;
        private Bitmap _skyBitmap;

        #endregion

        #region Constructor

        public NoFlickerControl()
        {
            // set defaults first
            DesiredFps = Defaults.DesiredFps;
            NumberOfTiles = Defaults.NumberOfTiles;
            Speed = Defaults.Speed;

            InitializeComponent();



            if (DesignMode)
            {
                return;
            }

            _lastTick = DateTime.Now;


            timer1.Tick += Timer1OnTick;
            timer1.Interval = 1000/DesiredFps; // How frequenty do we want to recalc positions
            timer1.Enabled = true;

        }

        #endregion

        #region Properties

        /// <summary>
        ///     This controls how often we recalculate object positions
        /// </summary>
        /// <remarks>
        ///     This can be independant of rendering FPS
        /// </remarks>
        /// <value>
        ///     The frames per second.
        /// </value>
        [DefaultValue(Defaults.DesiredFps)]
        public int DesiredFps { get; set; }

        [DefaultValue(Defaults.NumberOfTiles)]
        public int NumberOfTiles { get; set; }

        /// <summary>
        ///     Gets or sets the sky to draw.
        /// </summary>
        /// <value>
        ///     The sky.
        /// </value>
        [Browsable(false)]
        public Bitmap Sky { get; set; }

        /// <summary>
        ///     Gets or sets the speed in pixels/second.
        /// </summary>
        /// <value>
        ///     The speed.
        /// </value>
        [DefaultValue(Defaults.Speed)]
        public float Speed { get; set; }

        #endregion

        #region Methods

        private void HandleResize()
        {
            // the control has resized, time to recreate our offscreen bitmap
            // and graphics context

            if (Width == 0
                || Height == 0)
            {
                // nothing to do here
            }

            _offscreenBitmap = new Bitmap(Width, Height);
            _offscreenGraphics = Graphics.FromImage(_offscreenBitmap);
        }

        private void NoFlickerControl_Load(object sender, EventArgs e)
        {
            SkyInTheWindow();

            HandleResize();
        }

        private void NoFlickerControl_Resize(object sender, EventArgs e)
        {
            HandleResize();
        }

        /// <summary>
        ///     Handles the SizeChanged event of the NoFlickerControl control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
        private void NoFlickerControl_SizeChanged(object sender, EventArgs e)
        {
            HandleResize();
        }

        /// <summary>
        ///     Raises the <see cref="E:System.Windows.Forms.Control.Paint" /> event.
        /// </summary>
        /// <param name="e">A <see cref="T:System.Windows.Forms.PaintEventArgs" /> that contains the event data. </param>
        protected override void OnPaint(PaintEventArgs e)
        {
            var g = e.Graphics;
            var rc = e.ClipRectangle;

            if (_offscreenBitmap == null
                || _offscreenGraphics == null)
            {
                g.FillRectangle(Brushes.Gray, rc);
                return;
            }

            DrawOffscreen(_offscreenGraphics, ClientRectangle);

            g.DrawImageUnscaled(_offscreenBitmap, 0, 0);

        }

        private void DrawOffscreen(Graphics g, RectangleF bounds)
        {
            // We don't care about erasing the background because we're
            // drawing over it anyway
            //g.FillRectangle(Brushes.White, bounds);

            //g.SetClip(bounds);



            foreach (var tile in _tiles)
            {
                if (!(bounds.Contains(tile) || bounds.IntersectsWith(tile)))
                {
                    continue;
                }

                g.DrawImageUnscaled(_skyBitmap, new Point((int) tile.Left, (int) tile.Top));
            }
        }

        /// <summary>
        ///     Paints the background of the control.
        /// </summary>
        /// <param name="e">A <see cref="T:System.Windows.Forms.PaintEventArgs" /> that contains the event data.</param>
        protected override void OnPaintBackground(PaintEventArgs e)
        {
            // NOP

            // We don't care painting the background here because
            // 1. we want to do it offscreen
            // 2. the background is the picture anyway
        }

        /// <summary>
        ///     Responsible for updating/translating game objects, not drawing
        /// </summary>
        /// <param name="totalMillisecondsSinceLastUpdate">The total milliseconds since last update.</param>
        /// <remarks>
        ///     It is worth noting that OnUpdate could be called more times per
        ///     second than OnPaint.  This is fine.  It's generally a sign that
        ///     rendering is just taking longer but we are able to compensate by
        ///     tracking time since last update
        /// </remarks>
        private void OnUpdate(double totalMillisecondsSinceLastUpdate)
        {
            // Remember that we measure speed in pixels per second, hence the
            // totalMillisecondsSinceLastUpdate
            // This allows us to have smooth animations and to compensate when
            // rendering takes longer for certain frames

            for (int i = 0; i < _tiles.Count; i++)
            {
                var tile = _tiles[i];
                tile.Offset((float)(-Speed * totalMillisecondsSinceLastUpdate / 1000f), 0);
                _tiles[i] = tile;
            }

        }

        private void SkyInTheWindow()
        {
            _tiles.Clear();

            // here I load the bitmap from my embedded resource
            // but you easily could just do a new Bitmap ("C:/MyPath/Sky.jpg");

            _skyBitmap = Resources.sky400x400;

            var bounds = new Rectangle(0, 0, _skyBitmap.Width, _skyBitmap.Height);

            for (var i = 0; i < NumberOfTiles; i++)
            {
                // Loading sky into the window
                _tiles.Add(bounds);
                bounds.Offset(bounds.Width, 0);
            }
        }

        private void Timer1OnTick(object sender, EventArgs eventArgs)
        {
            if (DesignMode)
            {
                return;
            }

            var ellapsed = DateTime.Now - _lastTick;
            OnUpdate(ellapsed.TotalMilliseconds);

            _lastTick = DateTime.Now;


            // queue cause a repaint
            // It's important to realise that repaints are queued and fused
            // together if the message pump gets busy
            // In other words, there may not be a 1:1 of OnUpdate : OnPaint
            Invalidate(Bounds);
        }

        #endregion
    }

    public static class Defaults
    {
        public const int DesiredFps = 30;
        public const int NumberOfTiles = 100;
        public const float Speed = 300f;
    }
}
like image 131
MickyD Avatar answered Dec 09 '22 03:12

MickyD