Avoid creating PictureBoxes again and again

I have the following problem. My intention is to move multiple images from right to left in a windows form. The code below works very well. I am worried about the fact that every time a PictureBox is created, this procedure consumes a huge amount of memory. Each image follows the previous image continuously from left to right. Images show the sky moving from one side to the other. It should look like a plane flying in the air.

How can I avoid using too much memory? Is there something I can do with PaintEvent and GDI? I am not very familiar with graphic 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 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; } } } 
+5
source share
3 answers

It seems you are loading the same bitmap 100 times . There is a memory problem, not 100 PictureBox s. A PictureBox should have low overhead because they do not include the image in memory consumption, it is a reference Bitmap , which is more likely to consume large amounts of memory.

Easily fixed - consider downloading a bitmap once, and then apply it to all your PictureBox s.

Edit:

  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; } } 

... in:

  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 can only have a single PictureBox stretched to the width of the background, but replacing it over time. Of course, you need to draw something on the edge where a space appears.

You may get a little flicker with the repetition of the PictureBox , although this is one of the things I worry about, but it can still serve.

Or what I would do is create a UserControl and override OnPaint and just turn it into a bitmap problem and not have a PictureBox at all. Much faster and more efficient and does not flicker. :) This is purely optional

You also have the opportunity to eliminate any flicker if you first draw on the screen Graphics and Bitmap and "bitlit" the results on a visible screen.

Could you give me some code that serves as a starting point, because it is difficult for me to embed it in the code? I am not very good at graphic programming, and I really want to learn from each other. Better code does not flicker

As requested, I have included the following code:

Flicker Free Offscreen Rendering UserControl

In essence, this is what you need to create an off-screen raster image, which we will draw first. This is the same size as UserControl. The OnPaint control calls DrawOffscreen , passing in Graphics , which is connected to an off-screen bitmap. Here we turn around only by displaying tiles / sky that are visible and ignore others in order to increase productivity.

As soon as all this is done, we will close the entire screen bitmap on the display in one operation. This eliminates:

  • Flicker
  • Tear effects (usually associated with lateral movement)

There is a Timer that should update the position of all fragments based on the time since the last update. This allows a more realistic movement and avoids acceleration and deceleration under load. Tiles are moved using the OnUpdate method.

Some important properties:

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

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

  • Speed - speed in pixels per second; bitmaps move. Associated with DesiredFps . It is load independent; computer independent value

Picture If you noted in the code for Timer1OnTick , I call Invalidate(Bounds); after animating everything. This does not cause immediate coloring, and Windows will pause the drawing operation, which will be performed later. Successive pending operations will be merged into one. This means that we can animate positions more often than draw under heavy load. The animation mechanism is independent of paint . This is a good thing, you do not want to wait for the appearance of colors.

You will notice that I redefine OnPaintBackground and essentially do nothing. I do this because I do not want .NET to erase the background and cause unnecessary flickering before calling my OnPaint . I don’t even want to erase the background in DrawOffscreen , because we are still going to draw bitmap images. However, if the control has been changed more than the height of the raster image of the sky, and if this is a requirement, then you may want to. The performance degradation is pretty slight, I suppose, when you possibly draw a few raster maps anyway.

When you create the code, you can execute it on any Form . The control will be visible on the toolbar. Below I ran it on MainForm .

NoFlickerControl in the Toolbox

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

Design-time properties

If you installed the control and your form changes, you can resize the application at runtime. Useful for measuring performance. WinForms is not particularly hardware accelerated (unlike WPF), so I would not recommend that the window 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 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 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; } } 
+1
source

This is not a direct answer to this question - I think that it is primarily due to all the Bitmap images you created. You must create only one, and then the problem will disappear.

What I am proposing here is an alternative coding method that greatly reduces code.

All my code goes right in your Background constructor after the line this.MaximizeBox = false; . Everything after that is deleted.

So, start by loading the image:

 var image = new Bitmap(@"C:\MyPath\Sky.jpg"); 

Next, find out how many drawers for the image you need to draw on the form based on the width and height passed in:

 var countX = width / image.Width + 2; var countY = height / image.Height + 2; 

Now create real windows with pictures that will fill the screen:

 var pictureBoxData = ( from x in Enumerable.Range(0, countX) from y in Enumerable.Range(0, countY) let positionX = x * image.Width let positionY = y * image.Height let pictureBox = new PictureBox() { Image = image, Location = new Point(positionX, positionY), Size = new Size(image.Width, image.Height), } select new { positionX, positionY, pictureBox, } ).ToList(); 

Then add them all to the Controls collection:

 pictureBoxData.ForEach(pbd => this.Controls.Add(pbd.pictureBox)); 

Finally, use the Microsoft Reactive Framework (NuGet Rx-WinForms ) to create a timer that updates the Left position for image fields:

 var subscription = Observable .Generate( 0, n => true, n => n >= image.Width ? 0 : n + 1, n => n, n => TimeSpan.FromMilliseconds(10.0)) .ObserveOn(this) .Subscribe(n => { pictureBoxData .ForEach(pbd => pbd.pictureBox.Left = pbd.positionX - n); }); 

Finally, before starting the dialog, we need a way to clear all of the above so that the form closes cleanly. Do it:

 var disposable = new CompositeDisposable(image, subscription); this.FormClosing += (s, e) => disposable.Dispose(); 

Now you can do ShowDialog :

 this.ShowDialog(); 

What is it.

In addition to nugetting Rx-WinForms you need to add the following using statements to the beginning of the code:

 using System.Reactive.Linq; using System.Reactive.Disposables; 

It all worked well for me:

dialog

0
source

Variables and names have not been translated into English. Nevertheless, I hope this is clear to all of you.

 using System; using System.Drawing; using System.Windows.Forms; using System.Collections.Generic; /// <summary> /// Scrolling Background - Bewegender Hintergrund /// </summary> public class ScrollingBackground : Form { /* this = fremde Attribute und Methoden, * ohne this = eigene Attribute und Methoden */ private PictureBox picBoxImage; private PictureBox[] listPicBoxAufeinanderfolgendeImages; private Timer timerBewegungImage; private const int constIntAnzahlImages = 2, constIntInterval = 1, constIntPositionY = 0; private int intPositionX = 0, intFeinheitDerBewegungen, intBreite, intHoehe; private string stringTitel, stringBildpfad; // Konstruktor der Klasse Hintergrund /// <summary> /// Initialisiert eine neue Instanz der Klasse Hintergrund unter Verwendung der angegebenen Ganzzahlen und Zeichenketten. /// Es wird ein Windows-Fenster erstellt, welches die Möglichkeit hat, ein eingefügtes Bild als bewegenden Hintergrund darzustellen. /// </summary> /// <param name="width">Gibt die Breite des Fensters an und passt den darin befindlichen Hintergrund bzgl. der Breite automatisch an.</param> /// <param name="height">Gibt die Höhe des Fensters an und passt den darin befindlichen Hintergrund bzgl. der Höhe automatisch an.</param> /// <param name="speed">Geschwindigkeit der Bilder</param> /// <param name="title">Titel des Fensters</param> /// <param name="path">Pfad des Bildes, welches als Hintergrund dient</param> public ScrollingBackground(int width, int height, int speed, string title, string path) { // Klassennutzer können Werte setzen intBreite = width; intHoehe = height; intFeinheitDerBewegungen = speed; stringTitel = title; stringBildpfad = path; // Windows-Fenster wird erschaffen this.Text = title; this.Size = new Size(this.intBreite, this.intHoehe); this.StartPosition = FormStartPosition.CenterScreen; this.FormBorderStyle = FormBorderStyle.FixedSingle; this.MaximizeBox = false; // Die Bewegung des Bildes wird durch den Timer ermöglicht. timerBewegungImage = new Timer(); timerBewegungImage.Tick += new EventHandler(bewegungImage_XRichtung_Tick); timerBewegungImage.Interval = constIntInterval; timerBewegungImage.Start(); listPicBoxAufeinanderfolgendeImages = new PictureBox[2]; imageInWinFormLadenBeginn(); this.ShowDialog(); } // Bewegungsrichtung des Bildes private void bewegungImage_XRichtung_Tick(object sender, EventArgs e) { for (int i = 0; i < constIntAnzahlImages; i++) { picBoxImage = listPicBoxAufeinanderfolgendeImages[i]; // Flackerreduzierung - Minimierung des Flackerns zwischen zwei Bildern this.DoubleBuffered = true; // Bilder werden in X-Richtung bewegt picBoxImage.Location = new Point(picBoxImage.Location.X - intFeinheitDerBewegungen, picBoxImage.Location.Y); // Zusammensetzung beider gleicher Bilder, welche den Effekt haben, die Bilder ewig fortlaufend erscheinen zu lassen if (listPicBoxAufeinanderfolgendeImages[1].Location.X <= 0) { imageInWinFormLadenFortsetzung(); } } } // zwei PictureBoxes mit jeweils zwei gleichen Bildern werden angelegt private void imageInWinFormLadenBeginn() { Bitmap bitmapImage = new Bitmap(stringBildpfad); for (int i = 0; i < constIntAnzahlImages; i++) { // Bild wird in Fenster geladen picBoxImage = new PictureBox(); picBoxImage.Image = bitmapImage; // Bestimmung der Position und Größe des Bildes picBoxImage.SetBounds(intPositionX, constIntPositionY, intBreite, intHoehe); this.Controls.Add(picBoxImage); listPicBoxAufeinanderfolgendeImages[i] = picBoxImage; // zwei PictureBoxes mit jeweils zwei gleichen Bildern werden nebeneinander angefügt intPositionX += intBreite; } } // Wiederholte Nutzung der PictureBoxes private void imageInWinFormLadenFortsetzung() { // erste PictureBox mit Image wird wieder auf ihren Anfangswert "0" gesetzt - Gewährleistung der endlos laufenden Bilder picBoxImage = listPicBoxAufeinanderfolgendeImages[0]; picBoxImage.SetBounds(intPositionX = 0, constIntPositionY, intBreite, intHoehe); // zweite PictureBox mit Image wird wieder auf ihren Anfangswert "intBreite" gesetzt - Gewährleistung der endlos laufenden Bilder picBoxImage = listPicBoxAufeinanderfolgendeImages[1]; picBoxImage.SetBounds(intPositionX = intBreite, constIntPositionY, intBreite, intHoehe); } } 

Regards, Lucky Buggy

0
source

All Articles