您似乎正在加载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。更快,更高效,无闪烁。 :) 这纯粹是可选的
如果您首先在屏幕外绘制 Graphics 和 Bitmap 并将结果“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;
}
}