情况
作为一个业余爱好项目,我决定冒险模拟一个令人难以置信的基本现实。我的想法是创建一个简单的宇宙,其中内部的物体将根据位置或速度等因素进行交互。人们将能够向物体添加力以使其移动。所以为了展示这个"宇宙",我必须学习窗口的形式。因此,我对这个话题的了解是有限的。我创建了一个计时器对象,以便在每个时间段之后,我都会调用宇宙的渲染来生成一个位图,并为此设置一个图片框。为了渲染宇宙,我只需将每个对象渲染到位图上,从而创建几乎一个对象的拼贴。
为了测试这一点,我创建了一个宇宙,并添加了一个块。在每次更新图像时,我都会根据按键向块添加一个力。但是,我遇到的是该程序有很多视觉"滞后"。有时它会运行得非常平稳,但几秒钟后它会冻结半秒钟,然后再次恢复工作。这有点明显,虽然不是很糟糕。无论如何,这是我想解决的问题,并且/或至少知道为什么这恰好成为一个更好的程序员。
我已经研究了修复程序,但没有成功。我包括了双重缓冲区之类的东西,并在使用后处理图形和位图对象,但这几乎没有影响。
只是表单显示的图像 - 没有什么穿插的
问题
是什么导致了这种"滞后",我该怎么做才能解决它?
《守则》
class Block
{
...
public Tuple<float, float> Dimension { get; }
public Tuple<float, float> Position { get; set; }
...
public void Render(Bitmap bitmap)
{
using (Graphics g = Graphics.FromImage(bitmap))
{
int x = bitmap.Width / 2;
int y = bitmap.Height / 2;
//g.DrawRectangle(Pens.Black, 0, 0, 500, 500);
g.DrawRectangle(Pens.DarkCyan, Position.Item1 + x - Dimension.Item1 / 2, Position.Item2 + y - Dimension.Item2 / 2,
Dimension.Item1, Dimension.Item2);
}
}
}
class Universe
{
...
public List<IObject> Objects;
public Tuple<int, int> Dimensions;
...
public Bitmap RenderUniverse()
{
Bitmap bitmap = new Bitmap(Dimensions.Item1, Dimensions.Item2);
foreach (var obj in Objects)
{
obj.Render(bitmap);
}
return bitmap;
}
}
public partial class Form1 : Form
{
Universe Universe;
const int SPF = 50; //MILLISECONDS TILL NEXT UPDATE
public Form1()
{
Timer timer = new Timer();
timer.Tick += Update;
timer.Interval = SPF;
timer.Start();
...
InitializeComponent();
DoubleBuffered = true;
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
this.ClientSize = new System.Drawing.Size(Universe.Dimensions.Item1, Universe.Dimensions.Item2);
this.pictureBox1.Size = new System.Drawing.Size(Universe.Dimensions.Item1, Universe.Dimensions.Item2);
this.pictureBox1.SizeMode = PictureBoxSizeMode.Normal;
}
public void Update(object o, EventArgs args)
{
...
if (pictureBox1.Equals(null))
pictureBox1.Image.Dispose();
pictureBox1.Image = Universe.RenderUniverse();
pictureBox1.Refresh();
}
}
编辑:
我现在已经拿走了图片盒并使用了OnPaint。但是,这并没有解决问题。
我不会使用图片框,而是直接在表面上绘制。直接在窗体上绘制或插入用作框架的控件(例如面板)并在其上绘制。
您应该在 Paint 事件中绘制。让我们绘制表单本身:
protected override void OnPaint(PaintEventArgs e)
{
int x = ClientSize.Width / 2;
int y = ClientSize.Height / 2;
e.Graphics.DrawRectangle(
Pens.DarkCyan,
_currentBlock.Position.Item1 + x - _currentBlock.Dimension.Item1 / 2,
_currentBlock.Position.Item2 + y - _currentBlock.Dimension.Item2 / 2,
_currentBlock.Dimension.Item1,
_currentBlock.Dimension.Item2);
}
然后,只要有更改,就可以通过调用窗体或正在绘制的控件的Invalidate
方法来触发绘制:
Invalidate();
这样做的好处是不会有拥塞,因为Windows只会在尚未绘制时才调用OnPaint
。
如果要强制立即更新,请改为致电Refresh
:
Refresh();
完整示例(使用命名空间System.Drawing
中的PointF
和SizeF
结构而不是元组):
interface IObject
{
SizeF Dimension { get; }
PointF Position { get; set; }
void Render(Graphics g, int x, int y);
}
class Block : IObject
{
public PointF Position { get; set; }
public SizeF Dimension { get; set; }
public void Render(Graphics g, int x, int y)
{
g.DrawRectangle(
Pens.DarkCyan,
Position.X + x - Dimension.Width / 2,
Position.Y + y - Dimension.Height / 2,
Dimension.Width,
Dimension.Height);
}
}
public partial class frmPaintOnForm : Form
{
private Timer _timer = new Timer();
private IObject _currentObject;
private List<IObject> _objects = new List<IObject> {
new Block{ Position = new PointF( 0, 0), Dimension = new SizeF(50,50) },
new Block{ Position = new PointF(10, 0), Dimension = new SizeF(50,50) },
new Block{ Position = new PointF(20, 0), Dimension = new SizeF(50,50) },
new Block{ Position = new PointF(20,10), Dimension = new SizeF(50,50) },
new Block{ Position = new PointF(20,20), Dimension = new SizeF(50,50) },
new Block{ Position = new PointF(20,30), Dimension = new SizeF(50,50) },
new Block{ Position = new PointF(10,30), Dimension = new SizeF(50,50) },
new Block{ Position = new PointF( 0,30), Dimension = new SizeF(50,50) },
new Block{ Position = new PointF( 0,20), Dimension = new SizeF(50,50) },
new Block{ Position = new PointF( 0,20), Dimension = new SizeF(50,50) },
new Block{ Position = new PointF( 0, 0), Dimension = new SizeF(50,50) },
new Block{ Position = new PointF( 0, 0), Dimension = new SizeF(55,50) },
new Block{ Position = new PointF( 0, 0), Dimension = new SizeF(60,50) },
new Block{ Position = new PointF( 0, 0), Dimension = new SizeF(65,50) },
new Block{ Position = new PointF( 0, 0), Dimension = new SizeF(60,50) },
new Block{ Position = new PointF( 0, 0), Dimension = new SizeF(55,50) },
new Block{ Position = new PointF( 0, 0), Dimension = new SizeF(50,50) },
new Block{ Position = new PointF( 0, 0), Dimension = new SizeF(50,55) },
new Block{ Position = new PointF( 0, 0), Dimension = new SizeF(50,60) },
new Block{ Position = new PointF( 0, 0), Dimension = new SizeF(50,65) },
new Block{ Position = new PointF( 0, 0), Dimension = new SizeF(50,60) },
new Block{ Position = new PointF( 0, 0), Dimension = new SizeF(50,55) },
};
private int _index;
public frmPaintOnForm()
{
InitializeComponent();
DoubleBuffered = true;
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
_timer.Interval = 17;
_timer.Tick += Timer_Tick;
_timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
// Select the next object by cycling through the object list and trigger drawing
_currentObject = _objects[_index];
_index = (_index + 1) % _objects.Count;
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
if (_currentObject != null) {
int x = ClientSize.Width / 2;
int y = ClientSize.Height / 2;
_currentObject.Render(e.Graphics, x, y);
}
}
}
请注意,您从不直接呼叫OnPaint
。操作系统(Windows)负责解决这个问题。通过调用Invalidate
,您只需向Windows提供提示,Windows即可决定何时以及是否调用OnPaint
。Windows可以在许多情况下调用它:当窗体被打开或从最小化中恢复时,当删除此窗体之上的另一个窗体时,当调整窗体大小时,当然,如果您通过调用Invalidate
或Refresh
来请求它。它还可能调用它以在将鼠标悬停在任务栏上时绘制窗体的微型图像。如果出现拥塞或窗体不可见,Windows 也可能决定省略某些调用。
我怀疑您的代码正在尝试清理旧图像:
if (pictureBox1.Equals(null))
pictureBox1.Image.Dispose();
pictureBox1.Image = Universe.RenderUniverse();
(尤其是第一行)没有做你认为它做的事情。您的评论表明您相信第一行:
检查 pictureBox1 是否已经具有位图(即它是 我第一次渲染宇宙)
但它实际上做的是检查pictureBox1
是否null
(它永远不会在你的代码流中)。
要解决此问题,请改用:
var oldImage = pictureBox1.Image;
pictureBox1.Image = Universe.RenderUniverse();
oldImage?.Dispose();