避免一次又一次地创建PictureBoxes

我有以下问题。 我的目的是在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 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(); 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; } } } 

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

它很容易修复 – 考虑一次加载位图,然后将其应用到所有PictureBox

更改:

  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并将其转换为绘制位图问题,而根本没有PictureBox 。 更快更有效,没有闪烁。 :)这纯粹是可选的

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

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

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

无闪烁的屏幕外渲染UserControl

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

完成所有操作后,我们将整个屏幕外位图切换到一次操作中。 这有助于消除:

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

有一个Timer ,可根据自上次更新以来的时间更新所有图块的位置。 这样可以实现更逼真的运动,避免加载时的加速和减速。 Tile在OnUpdate方法中移动。

一些重要的属性:

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

  • NumberOfTiles - 我把它设置为你的100(云图)

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

绘画如果你在Timer1OnTick的代码中Timer1OnTick我调用Invalidate(Bounds); 动画完一切后。 这不会导致立即绘制,而是Windows会将绘制操作排队,以便稍后完成。 连续的待处理操作将融合为一个。 这意味着我们可以比重载期间的绘画更频繁地设置动画位置。 动画技师独立于油漆 。 这是一件好事,你不想等待油漆的发生。

你会注意到我重写OnPaintBackground并且基本上什么都不做。 我这样做是因为我不希望.NET在调用OnPaint之前擦除背景并导致不必要的闪烁。 我甚至不打扰在DrawOffscreen删除背景,因为我们只是想在它上面绘制位图。 但是,如果控件的大小调整大于天空位图的高度,如果需要,那么您可能需要。 当你无论如何都在绘制多个天空位图时,我认为性能命中是微不足道的。

在构建代码时,您可以在任何Form 。 该控件将显示在工具箱中。 下面我在我的MainForm

工具箱中的NoFlickerControl

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

设计时属性

如果停靠控件并且窗体可resize,则可以在运行时调整应用程序的大小。 用于衡量性能。 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 { ///  ///  public partial class NoFlickerControl : UserControl { #region Fields private readonly List _tiles = new List(); 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 ///  /// This controls how often we recalculate object positions ///  ///  /// This can be independant of rendering FPS ///  ///  /// The frames per second. ///  [DefaultValue(Defaults.DesiredFps)] public int DesiredFps { get; set; } [DefaultValue(Defaults.NumberOfTiles)] public int NumberOfTiles { get; set; } ///  /// Gets or sets the sky to draw. ///  ///  /// The sky. ///  [Browsable(false)] public Bitmap Sky { get; set; } ///  /// Gets or sets the speed in pixels/second. ///  ///  /// The speed. ///  [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(); } ///  /// Handles the SizeChanged event of the NoFlickerControl control. ///  /// The source of the event. /// The  instance containing the event data. private void NoFlickerControl_SizeChanged(object sender, EventArgs e) { HandleResize(); } ///  /// Raises the  event. ///  /// A  that contains the event data.  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)); } } ///  /// Paints the background of the control. ///  /// A  that contains the event data. 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 } ///  /// Responsible for updating/translating game objects, not drawing ///  /// The total milliseconds since last update. ///  /// 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 ///  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; } } 

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

我在这里建议的另一种编码方法是极大地削减代码。

我的所有代码都直接在你的Background构造函数中,直到this.MaximizeBox = false; 。 之后的一切都被删除了。

首先加载图像:

 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; 

这一切都很适合我:

对话

变量和名称尚未翻译成英文。 但我希望你们所有人都能理解这一点。

 using System; using System.Drawing; using System.Windows.Forms; using System.Collections.Generic; ///  /// Scrolling Background - Bewegender Hintergrund ///  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 ///  /// 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. ///  /// Gibt die Breite des Fensters an und passt den darin befindlichen Hintergrund bzgl. der Breite automatisch an. /// Gibt die Höhe des Fensters an und passt den darin befindlichen Hintergrund bzgl. der Höhe automatisch an. /// Geschwindigkeit der Bilder /// Titel des Fensters /// Pfad des Bildes, welches als Hintergrund dient 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); } } 

此致,Lucky Buggy