【问题标题】:Avoiding creating PictureBoxes again and again避免一次又一次地创建图片框
【发布时间】:2015-04-15 02:59:04
【问题描述】:

我遇到了以下问题。我的目的是在 Windows 窗体中从右向左移动几个图像。下面的代码工作得很好。困扰我的是,每次创建 PictureBox 对象时,此过程都会占用大量内存。每个图像从右到左不间断地跟随前一个图像。图像显示天空从一侧移动到另一侧。它应该看起来像一架飞机在空中飞行。

如何避免使用过多的内存?我可以用 PaintEvent 和 GDI 做些什么吗?我对图形编程不是很熟悉。

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

        }

    }

【问题讨论】:

  • 这是一个完全合理的主题问题,但它可能在Code Review 上得到了更好的接受(即没有立即被否决)。

标签: c# winforms memory picturebox


【解决方案1】:

您似乎正在加载same 位图100 次。你的记忆问题就在那里,不是 100 PictureBoxs。 PictureBox 的内存开销应该很低,因为它们不将图像包含在内存消耗中,引用的 Bitmap 更有可能消耗大量内存。

很容易修复 - 考虑加载位图一次,然后将其应用于您的所有PictureBoxs。

变化:

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

    }

...到:

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

    }

您可以将单个 PictureBox 拉伸到背景的宽度,但随着时间的推移移动它。当然,您需要在出现间隙的边缘绘制一些东西。

重复PictureBox 可能会使您有点闪烁,尽管这是我担心的事情之一,但它可能仍然有效。

或者我要做的是创建一个UserControl 并覆盖OnPaint,然后把它变成一个绘制位图问题,根本没有PictureBoxs。更快,更高效,无闪烁。 :) 这纯粹是可选的

如果您首先在屏幕外绘制 GraphicsBitmap 并将结果“bitblit”到可见屏幕,则您也有可能消除任何闪烁。

您是否介意给我一些代码作为参考,因为对我来说很难在代码中实现?我对图形编程不是很熟悉,我真的很想互相学习。没有闪烁的代码更好

根据要求,我已包含以下代码:

无闪烁离屏渲染用户控件

本质上,它的作用是创建一个我们将首先绘制的离屏位图。它与 UserControl 的大小相同。控件的OnPaint 调用DrawOffscreen,传入附加到离屏位图的Graphics。这里我们循环只渲染可见的瓦片/天空而忽略其他以提高性能。

完成后,我们通过一次操作将整个屏幕外位图快速显示到显示器上。这有助于消除:

  • 闪烁
  • 撕裂效果(通常与横向运动有关)

有一个Timer 计划根据自上次更新以来的时间更新所有图块的位置。这允许更逼真的运动,并避免在负载下加速和减速。瓷砖在OnUpdate 方法中移动。

一些重要的属性:

  • DesiredFps - 所需的帧/秒。这直接控制调用OnUpdate 方法的频率。它不直接控制调用OnPaint 的频率

  • NumberOfTiles - 我已将其设置为您的 100(云图像)

  • Speed - 位图移动的速度(以像素/秒为单位)。绑定到DesiredFps。这是与负载无关的;与计算机性能无关的价值

绘画 如果您在 Timer1OnTick 的代码中注明,我会在为所有内容设置动画后调用 Invalidate(Bounds);。这不会导致立即绘制,而是 Windows 将排队等待稍后完成的绘制操作。连续的未决操作将被融合为一个。这意味着我们可以在重载期间比绘画更频繁地为位置设置动画。 动画机制独立于绘画。这是一件好事,你不想等待油漆出现。

你会注意到我覆盖了OnPaintBackground 并且基本上什么都不做。我这样做是因为我不希望 .NET 在调用我的OnPaint 之前擦除背景并导致不必要的闪烁。我什至不费心擦除DrawOffscreen 中的背景,因为无论如何我们都会在其上绘制位图。但是,如果控件的大小调整为大于天空位图的高度,并且如果这是必需的,那么您可能想要。我想当您可以绘制多个天空位图时,性能影响可以忽略不计。

当你构建代码时,你可以在任何Form 上敲它。该控件将在工具箱中可见。下面我把它放在我的MainForm上。

该控件还演示了设计时属性和您可以在下面看到的默认值。这些设置似乎对我很有效。尝试更改它们以获得不同的效果。

如果您停靠控件并且您的表单可以调整大小,那么您可以在运行时调整应用程序的大小。对测量性能很有用。 WinForms 不是特别硬件加速的(与 WPF 不同),所以我不建议窗口太大

代码:

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

【讨论】:

  • 我同意,在 for 循环之外创建位图一次。
  • 非常感谢您的出色回应,非常感谢。
  • 你介意给我一些代码作为参考吗,因为对我来说很难在代码中实现?我对图形编程不是很熟悉,我真的很想互相学习。没有闪烁的代码更好。
  • @LuckyBuggy 我已按要求包含了无闪烁代码
  • 你能告诉我们using SkyAnimation.Properties;是什么吗?
【解决方案2】:

这不是对这个问题的直接答案——我认为这主要是因为您正在创建所有Bitmap 图像。您应该只创建一个,然后问题就会消失。

我在这里建议的是另一种编码方式,可以极大地减少代码。

我的所有代码在this.MaximizeBox = false; 行之后直接进入你的Background 构造函数。之后的所有内容都将被删除。

所以从加载图片开始:

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

接下来,根据传入的widthheight,计算出我需要多少个图片框才能将图像平铺在表单上:

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

现在创建将填充屏幕的实际图片框:

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();

接下来,将它们全部添加到Controls 集合中:

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

最后,使用 Microsoft 的 Reactive Framework (NuGet Rx-WinForms) 创建一个计时器,将更新图片框的 Left 位置:

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

最后,在启动对话框之前,我们需要一种方法来清理以上所有内容,以便表单干净地关闭。这样做:

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

现在你可以做ShowDialog:

this.ShowDialog();

就是这样。

除了删除Rx-WinForms之外,还需要在代码顶部添加以下using语句:

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

对我来说一切都很好:

【讨论】:

  • 我会定期在n &gt;= image.Width 线上收到System.InvalidOperationException was unhandled by user code - Object is currently in use elsewhere。有什么办法让动画更流畅?您可以看到瓷砖并非全部同时更新,并且可悲地看到撕裂效果。否则很好地使用 Rx
  • @MickyDuncan - 我建议添加一行var imageWidth = image.Width;,然后将n &gt;= image.Width 更改为n &gt;= imageWidth。这应该可以缓解这个问题。我没有看到任何动画问题,但也许我的屏幕与屏幕相比相对较大,所以我的图块很少。您可以尝试的一件事是在屏幕上放置一个面板并将图片框放入其中,然后移动面板。
【解决方案3】:

变量和名称尚未翻译成英文。不过还是希望大家能理解。

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

    }

}

问候, 幸运车

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-08-09
    • 1970-01-01
    • 2021-07-18
    • 2022-11-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多