问题
I'm looking for the fastest way to refresh an image (and draw some shapes) on a Winform. I'm using a PictureBox now, but I'm open for suggestions.
The way I'm getting the max FPS possible I took from here: "My last post on render loops (hopefully)"
This is a game loop pattern, but it can also be used to other purpose, like to display live images acquired from a camera.
I'm drawing a red circle in different positions.
As you can see, the FPS drops a lot when a Bitmap is loaded at the PictureBox. Especially in the SizeMode Zoom (the one I want to use).
My PC values for FPS:
Form size (start size)
Bitmap ON: 140 (Zoom); 1000 (Normal); 135 (Stretch); 880 (Auto); 1000 (Center)
Bitmap OFF: 3400 !!
Form size (after Zoom 100% click)
Bitmap ON: 40 (Zoom); 150 (Normal); 65 (Stretch); 150 (Auto); 150 (Center)
Bitmap OFF: 540 !!
Edit 1: Well, this results was for a 1024x1024 image. But I realizy that the form Height was not getting the right value, I found out that it is because the form size is limited by the Screen resolution. So I've edited the code to load a 800x600 Bitmap. Now the Zoom 100% button works. As you can see, at zoom 100% the FPS is 350, but if you increase the size of the PictureBox one more pixel, the FPS drops to 35, about 10x slower ¬¬ !
The big question is: How can I improve the FPS, specially in zoom mode?
PS: I know WinForm is not the best way to go, I also know WPF. But for now I'm looking the solution using WinForm. Ok? The reason is that I have a big scary project using it. I'm planing to move to WPF, but I'm still learning it. :)
Here is the full code, so you can test it by yourself. Form1.cs:
//.NET 4.0 Client Profile
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Windows.Forms;
//You need to add System.Activities.Presentation.dll
using System.Activities.Presentation.Hosting;
namespace WindowsFormsApplication2
{
public enum DrawSource
{
None = 0,
PictureBox = 1,
Grapghics = 2,
}
public partial class Form1 : Form
{
private readonly Timer _updateFPSTimer;
private bool _isIdle;
private Point _location;
private double _updateCount;
private readonly Bitmap _backImage;
public Form1()
{
InitializeComponent();
this.DoubleBuffered = this.ckbDoubleBufferForm.Checked;
this.pictureBox1.DoubleBuffered = this.ckbDoubleBufferPictureBox.Checked;
_backImage = new Bitmap(800, 600, PixelFormat.Format24bppRgb);
Graphics g = Graphics.FromImage(_backImage);
int penWidth = 10;
Pen pen = new Pen(Brushes.Blue, penWidth);
g.DrawRectangle(pen, new Rectangle(new Point(penWidth, penWidth), new Size(_backImage.Size.Width - 2 * penWidth, _backImage.Size.Height - 2 * penWidth)));
this.cbxSizeMode.DataSource = Enum.GetValues(typeof(PictureBoxSizeMode));
this.cbxSizeMode.SelectedItem = PictureBoxSizeMode.Zoom;
this.cbxBitmapDrawSource.DataSource = Enum.GetValues(typeof(DrawSource));
this.cbxBitmapDrawSource.SelectedItem = DrawSource.PictureBox;
this.pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
Application.Idle += Application_Idle;
_updateFPSTimer = new Timer();
_updateFPSTimer.Tick += UpdateFPSTimer_Tick;
_updateFPSTimer.Interval = Convert.ToInt32(1000.0 / 5); //Period in s = 1 s / 5 Hz
_updateFPSTimer.Start();
}
private void Application_Idle(object sender, EventArgs e)
{
while (this.ckbRun.Checked && IsAppStillIdle())
this.pictureBox1.Refresh();
}
void UpdateFPSTimer_Tick(object sender, EventArgs e)
{
GetFPS();
}
private void GetFPS()
{
double fps = _updateCount / (Convert.ToDouble(_updateFPSTimer.Interval) / 1000.0);
_updateCount = 0;
lblFps.Text = fps.ToString("0.00");
}
private void PictureBox1_Paint(object sender, PaintEventArgs e)
{
DrawCircle(e.Graphics);
}
private void DrawCircle(Graphics g)
{
if (this.pictureBox1.Image == null && ((DrawSource)this.cbxBitmapDrawSource.SelectedItem == DrawSource.Grapghics))
g.DrawImage(_backImage, 0, 0, g.ClipBounds.Width, g.ClipBounds.Height);
var rect = new Rectangle(_location, new Size(30, 30));
g.DrawEllipse(Pens.Red, rect);
_location.X += 1;
_location.Y += 1;
if (_location.X > g.ClipBounds.Width)
_location.X = 0;
if (_location.Y > g.ClipBounds.Height)
_location.Y = 0;
_updateCount++;
}
/// <summary>
/// Gets if the app still idle.
/// http://blogs.msdn.com/b/tmiller/archive/2005/05/05/415008.aspx
/// </summary>
/// <returns></returns>
private bool IsAppStillIdle()
{
Message msg;
return !PeekMessage(out msg, IntPtr.Zero, 0, 0, 0);
}
#region Unmanaged Get PeekMessage
// http://blogs.msdn.com/b/tmiller/archive/2005/05/05/415008.aspx
[System.Security.SuppressUnmanagedCodeSecurity] // We won't use this maliciously
[DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern bool PeekMessage(out Message msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags);
#endregion
private void btnZoomReset_Click(object sender, EventArgs e)
{
if (_backImage != null)
{
//Any smarter way to do this?
//Note that the maximum Form size is limmited by designe by the Screen resolution.
//Rectangle screenRectangle = RectangleToScreen(this.ClientRectangle);
//int titleHeight = screenRectangle.Top - this.Top;
//Borders
Size border = new Size();
border.Width = this.Width - this.pictureBox1.Width;
border.Height = this.Height - this.pictureBox1.Height;
this.Width = border.Width + _backImage.Width;
this.Height = border.Height + _backImage.Height;
Console.WriteLine("PictureBox size: " + this.pictureBox1.Size.ToString());
}
}
private void SizeMode_SelectedIndexChanged(object sender, EventArgs e)
{
PictureBoxSizeMode mode;
Enum.TryParse<PictureBoxSizeMode>(cbxSizeMode.SelectedValue.ToString(), out mode);
this.pictureBox1.SizeMode = mode;
}
private void DoubleBufferForm_CheckedChanged(object sender, EventArgs e)
{
this.DoubleBuffered = this.ckbDoubleBufferForm.Checked;
}
private void DoubleBufferPictureBox_CheckedChanged(object sender, EventArgs e)
{
this.pictureBox1.DoubleBuffered = this.ckbDoubleBufferPictureBox.Checked;
}
private void BitmapDrawSource_SelectedIndexChanged(object sender, EventArgs e)
{
if ((DrawSource)this.cbxBitmapDrawSource.SelectedItem == DrawSource.PictureBox)
this.pictureBox1.Image = _backImage;
else
this.pictureBox1.Image = null;
}
}
}
Designer.CS:
namespace WindowsFormsApplication2
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.pictureBox1 = new WindowsFormsApplication2.PictureBoxDoubleBuffer();
this.label1 = new System.Windows.Forms.Label();
this.lblFps = new System.Windows.Forms.Label();
this.ckbRun = new System.Windows.Forms.CheckBox();
this.btnZoomReset = new System.Windows.Forms.Button();
this.label2 = new System.Windows.Forms.Label();
this.cbxSizeMode = new System.Windows.Forms.ComboBox();
this.gpbDoubleBuffer = new System.Windows.Forms.GroupBox();
this.ckbDoubleBufferPictureBox = new System.Windows.Forms.CheckBox();
this.ckbDoubleBufferForm = new System.Windows.Forms.CheckBox();
this.cbxBitmapDrawSource = new System.Windows.Forms.ComboBox();
this.label3 = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
this.gpbDoubleBuffer.SuspendLayout();
this.SuspendLayout();
//
// pictureBox1
//
this.pictureBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.pictureBox1.BackColor = System.Drawing.SystemColors.ControlDarkDark;
this.pictureBox1.DoubleBuffered = true;
this.pictureBox1.Location = new System.Drawing.Point(8, 84);
this.pictureBox1.Name = "pictureBox1";
this.pictureBox1.Size = new System.Drawing.Size(347, 215);
this.pictureBox1.TabIndex = 0;
this.pictureBox1.TabStop = false;
this.pictureBox1.Paint += new System.Windows.Forms.PaintEventHandler(this.PictureBox1_Paint);
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(134, 13);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(30, 13);
this.label1.TabIndex = 3;
this.label1.Text = "FPS:";
//
// lblFps
//
this.lblFps.AutoSize = true;
this.lblFps.Location = new System.Drawing.Point(160, 13);
this.lblFps.Name = "lblFps";
this.lblFps.Size = new System.Drawing.Size(10, 13);
this.lblFps.TabIndex = 4;
this.lblFps.Text = "-";
//
// ckbRun
//
this.ckbRun.Appearance = System.Windows.Forms.Appearance.Button;
this.ckbRun.AutoSize = true;
this.ckbRun.Checked = true;
this.ckbRun.CheckState = System.Windows.Forms.CheckState.Checked;
this.ckbRun.Location = new System.Drawing.Point(8, 8);
this.ckbRun.Name = "ckbRun";
this.ckbRun.Size = new System.Drawing.Size(37, 23);
this.ckbRun.TabIndex = 5;
this.ckbRun.Text = "Run";
this.ckbRun.UseVisualStyleBackColor = true;
//
// btnZoomReset
//
this.btnZoomReset.Location = new System.Drawing.Point(51, 8);
this.btnZoomReset.Name = "btnZoomReset";
this.btnZoomReset.Size = new System.Drawing.Size(74, 23);
this.btnZoomReset.TabIndex = 7;
this.btnZoomReset.Text = "Zoom 100%";
this.btnZoomReset.UseVisualStyleBackColor = true;
this.btnZoomReset.Click += new System.EventHandler(this.btnZoomReset_Click);
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(5, 37);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(57, 13);
this.label2.TabIndex = 8;
this.label2.Text = "SizeMode:";
//
// cbxSizeMode
//
this.cbxSizeMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbxSizeMode.FormattingEnabled = true;
this.cbxSizeMode.Location = new System.Drawing.Point(73, 34);
this.cbxSizeMode.Name = "cbxSizeMode";
this.cbxSizeMode.Size = new System.Drawing.Size(84, 21);
this.cbxSizeMode.TabIndex = 9;
this.cbxSizeMode.SelectedIndexChanged += new System.EventHandler(this.SizeMode_SelectedIndexChanged);
//
// gpbDoubleBuffer
//
this.gpbDoubleBuffer.Controls.Add(this.ckbDoubleBufferPictureBox);
this.gpbDoubleBuffer.Controls.Add(this.ckbDoubleBufferForm);
this.gpbDoubleBuffer.Location = new System.Drawing.Point(221, 8);
this.gpbDoubleBuffer.Name = "gpbDoubleBuffer";
this.gpbDoubleBuffer.Size = new System.Drawing.Size(99, 65);
this.gpbDoubleBuffer.TabIndex = 10;
this.gpbDoubleBuffer.TabStop = false;
this.gpbDoubleBuffer.Text = "Double Buffer";
//
// ckbDoubleBufferPictureBox
//
this.ckbDoubleBufferPictureBox.AutoSize = true;
this.ckbDoubleBufferPictureBox.Checked = true;
this.ckbDoubleBufferPictureBox.CheckState = System.Windows.Forms.CheckState.Checked;
this.ckbDoubleBufferPictureBox.Location = new System.Drawing.Point(9, 38);
this.ckbDoubleBufferPictureBox.Name = "ckbDoubleBufferPictureBox";
this.ckbDoubleBufferPictureBox.Size = new System.Drawing.Size(77, 17);
this.ckbDoubleBufferPictureBox.TabIndex = 8;
this.ckbDoubleBufferPictureBox.Text = "PictureBox";
this.ckbDoubleBufferPictureBox.UseVisualStyleBackColor = true;
this.ckbDoubleBufferPictureBox.CheckedChanged += new System.EventHandler(this.DoubleBufferPictureBox_CheckedChanged);
//
// ckbDoubleBufferForm
//
this.ckbDoubleBufferForm.AutoSize = true;
this.ckbDoubleBufferForm.Checked = true;
this.ckbDoubleBufferForm.CheckState = System.Windows.Forms.CheckState.Checked;
this.ckbDoubleBufferForm.Location = new System.Drawing.Point(9, 15);
this.ckbDoubleBufferForm.Name = "ckbDoubleBufferForm";
this.ckbDoubleBufferForm.Size = new System.Drawing.Size(49, 17);
this.ckbDoubleBufferForm.TabIndex = 7;
this.ckbDoubleBufferForm.Text = "Form";
this.ckbDoubleBufferForm.UseVisualStyleBackColor = true;
this.ckbDoubleBufferForm.CheckedChanged += new System.EventHandler(this.DoubleBufferForm_CheckedChanged);
//
// cbxBitmapDrawSource
//
this.cbxBitmapDrawSource.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbxBitmapDrawSource.FormattingEnabled = true;
this.cbxBitmapDrawSource.Items.AddRange(new object[] {
"PictureBox",
"Graphics"});
this.cbxBitmapDrawSource.Location = new System.Drawing.Point(73, 57);
this.cbxBitmapDrawSource.Name = "cbxBitmapDrawSource";
this.cbxBitmapDrawSource.Size = new System.Drawing.Size(84, 21);
this.cbxBitmapDrawSource.TabIndex = 12;
this.cbxBitmapDrawSource.SelectedIndexChanged += new System.EventHandler(this.BitmapDrawSource_SelectedIndexChanged);
//
// label3
//
this.label3.AutoSize = true;
this.label3.Location = new System.Drawing.Point(5, 60);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(68, 13);
this.label3.TabIndex = 11;
this.label3.Text = "Bitmap draw:";
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(361, 304);
this.Controls.Add(this.cbxBitmapDrawSource);
this.Controls.Add(this.label3);
this.Controls.Add(this.gpbDoubleBuffer);
this.Controls.Add(this.cbxSizeMode);
this.Controls.Add(this.label2);
this.Controls.Add(this.btnZoomReset);
this.Controls.Add(this.ckbRun);
this.Controls.Add(this.lblFps);
this.Controls.Add(this.label1);
this.Controls.Add(this.pictureBox1);
this.DoubleBuffered = true;
this.Name = "Form1";
this.Text = "Max FPS tester";
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
this.gpbDoubleBuffer.ResumeLayout(false);
this.gpbDoubleBuffer.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
PictureBoxDoubleBuffer pictureBox1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label lblFps;
private System.Windows.Forms.CheckBox ckbRun;
private System.Windows.Forms.Button btnZoomReset;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.ComboBox cbxSizeMode;
private System.Windows.Forms.GroupBox gpbDoubleBuffer;
private System.Windows.Forms.CheckBox ckbDoubleBufferPictureBox;
private System.Windows.Forms.CheckBox ckbDoubleBufferForm;
private System.Windows.Forms.ComboBox cbxBitmapDrawSource;
private System.Windows.Forms.Label label3;
}
}
PictureBoxDoubleBuffer.cs:
using System.Windows.Forms;
namespace WindowsFormsApplication2
{
public class PictureBoxDoubleBuffer : PictureBox
{
public bool DoubleBuffered
{
get { return base.DoubleBuffered; }
set { base.DoubleBuffered = value; }
}
public PictureBoxDoubleBuffer()
{
base.DoubleBuffered = true;
}
}
}
回答1:
"The big question is: How can I improve the FPS, specially in zoom mode?"
In Zoom (and Stretch) mode the Image() is probably being resized for each Refresh()...a very expensive operation. You could roll your own "zoom" mode that creates a new image that is already zoomed and assign that instead, leaving the mode at "Normal".
Here's a quick example using a cheat zoom with another PictureBox (obviously you could do the zoom mathematically by computing aspect ratio, etc...but that hurts my brain in the morning):
private void cbxSizeMode_SelectedIndexChanged(object sender, EventArgs e)
{
PictureBoxSizeMode mode;
Enum.TryParse<PictureBoxSizeMode>(cbxSizeMode.SelectedValue.ToString(), out mode);
this.pictureBox1.SizeMode = mode;
if (this.pictureBox1.SizeMode == PictureBoxSizeMode.Zoom)
{
using (PictureBox pb = new PictureBox())
{
pb.Size = pictureBox1.Size;
pb.SizeMode = PictureBoxSizeMode.Zoom;
pb.Image = _backImage;
Bitmap bmp = new Bitmap(pb.Size.Width, pb.Size.Height);
pb.DrawToBitmap(bmp, pb.ClientRectangle);
this.pictureBox1.SizeMode = PictureBoxSizeMode.Normal;
this.pictureBox1.Image = bmp;
}
}
else
{
pictureBox1.Image = _backImage;
}
}
You'll need to call this method when the form first runs if Zoom is the default selection, otherwise it won't have custom zoom image.
Now your Zoom mode should zoom along! (sorry, couldn't resist doing that)
来源:https://stackoverflow.com/questions/16988078/huge-fps-drop-when-picturebox-have-an-image-is-there-a-way-to-improve-the-fps