【问题标题】:Apparent Lag with Windows FormsWindows 窗体的明显滞后
【发布时间】:2018-03-06 20:35:11
【问题描述】:

情况

作为一个业余爱好项目,我决定冒险模拟一个令人难以置信的基本现实。我的想法是创建一个简单的宇宙,其中内部的对象将根据位置或速度等因素进行交互。一个人将能够向物体添加力以使其移动。所以为了展示这个“宇宙”,我必须学习 Window 的 Forms。因此,我对这个主题的了解是有限的。我创建了一个计时器对象,这样在每个时间段之后,我都会调用宇宙的渲染来生成一个位图并为其设置一个图片框。为了渲染宇宙,我只需将每个对象渲染到位图上,从而创建几乎一个对象拼贴。

为了测试这一点,我创建了一个 Universe,并添加了一个块。在每次更新图像时,我都会根据按键向块添加一个力。但是,我遇到的是该程序有很多视觉“滞后”。有时它会运行得非常顺利,但几秒钟后它会冻结半秒钟,然后重新开始工作。这有点明显,虽然不是非常糟糕。无论如何,这是我想要解决的问题,和/或至少知道为什么这会成为一个更好的程序员。

我已经研究过修复方法,但没有成功。我包括了诸如双缓冲和使用后处理图形和位图对象之类的东西,但这几乎没有效果。

Just the image of what the forms displays - nothing intersting

问题

是什么导致了这种“滞后”,我可以做些什么来解决它?

代码

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。但是,这并没有解决问题。

【问题讨论】:

  • 从代码中看不出什么。这是垃圾收集器几乎从不运行的那种程序。它使用大量非托管内存,但绘图对象太小而无法让 GC 工作。请密切注意内存使用情况并检查是否可以将 perfmon.exe 中可见的 GC 性能计数器与减速相关联。保留位图而不是重复重新创建它,使用 Graphics.Clear() 重新初始化它。

标签: c# windows forms graphics


【解决方案1】:

我不会使用图片框,而是直接在表面上绘制。直接在窗体上绘制或插入用作框架的控件(例如面板)并在其上绘制。

您应该在 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 也可能决定忽略一些调用。

【讨论】:

  • @Oliver_Jacot-Descombes Paint 事件是如何工作的。我怎么称呼它?
  • 你永远不会调用它。您调用 Invalidate() 或 Refresh()。 Windows 将此用作提示并决定何时实际调用 OnPaint。例如,如果您调整窗体大小,Windows 也可能会调用它。
  • 我不太明白这将如何允许每个对象呈现? onpaint 只影响表单。我如何让它从对象的类中渲染每个对象?
  • 那么这是否意味着每次您执行 Invalidate 时都会触发 OnPaint?
  • 我添加了一个完整的例子。
【解决方案2】:

我怀疑您的代码试图清理旧图像:

if (pictureBox1.Equals(null))
    pictureBox1.Image.Dispose();
pictureBox1.Image = Universe.RenderUniverse();

(尤其是第一行)没有按照您的想法进行。您的评论表明您相信第一行:

检查 pictureBox1 是否已经有位图(即它是 我第一次渲染宇宙)

但它实际上所做的是检查pictureBox1 是否为null(它从不在您的代码流中)。

要解决此问题,请改用:

var oldImage = pictureBox1.Image;
pictureBox1.Image = Universe.RenderUniverse();
oldImage?.Dispose();

【讨论】:

  • 这不是真的。一开始就为空
  • 我不同意@RetAFVLib。当代码运行时,pictureBox 永远不会为空。 pictureBox1.Image 可能是 - 但这完全是另一回事。试试我的代码,看看效果如何。
猜你喜欢
  • 2011-01-03
  • 2023-01-29
  • 1970-01-01
  • 2012-11-19
  • 2018-12-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多