问题
I'm trying to make an integer array and Bitmap work in harmony as part of a C# winforms project for fast PictureBox editing (avoiding the slow SetPixel command).
I added a Button and a PictureBox, a click event on the aformentioned Button and a closing event on the Form.
The code for the Form is now like so:
public partial class Form1 : Form
{
uint[] _Pixels { get; set; }
Bitmap _Bitmap { get; set; }
GCHandle _Handle { get; set; }
IntPtr _Addr { get; set; }
public Form1()
{
InitializeComponent();
int imageWidth = 100; //1920;
int imageHeight = 100; // 1080;
PixelFormat fmt = PixelFormat.Format32bppRgb;
int pixelFormatSize = Image.GetPixelFormatSize(fmt);
int stride = imageWidth * pixelFormatSize;
int padding = 32 - (stride % 32);
if (padding < 32)
{
stride += padding;
}
_Pixels = new uint[(stride / 32) * imageHeight + 1];
_Handle = GCHandle.Alloc(_Pixels, GCHandleType.Pinned);
_Addr = Marshal.UnsafeAddrOfPinnedArrayElement(_Pixels, 0);
_Bitmap = new Bitmap(imageWidth, imageHeight, stride / 8, fmt, _Addr);
pictureBox1.Image = _Bitmap;
}
private void button1_Click(object sender, EventArgs e)
{
for (int i = 0; i < _Pixels.Length; i++)
{
_Pixels[i] = ((uint)(255 | (255 << 8) | (255 << 16) | 0xff000000));
}
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
_Addr = IntPtr.Zero;
if (_Handle.IsAllocated)
{
_Handle.Free();
}
_Bitmap.Dispose();
_Bitmap = null;
_Pixels = null;
}
}
When run, a black image as expected is present in the beginning.
The image should turn white when the button is clicked, but I'm missing something.
What am I forgetting to do?
回答1:
WinForms controls don't redraw themselves "just because", they have to have a reason to do so.
Some data sources have the ability to tell their container that they have been updated. A Bitmap class doesn't. It has no way of telling the containing PictureBox (or whatever control you use to display it) to tell that it was updated. It could when you call SetPixel()
to set single pixels, or UnlockBits()
after manipulating a BitmapData
instance, but it can't when the bitmap was constructed using a pinned array that you manipulate entirely out of control of the Bitmap class anyway.
So the Bitmap class doesn't have events or other ways to notify its container of updates.
This means you need to tell the containing control that its data source has been updated, so the control can redraw itself.
You can do so as explained in How to refresh PictureBox, namely using pictureBox.Refresh()
. This causes the PictureBox control to invalidate itself, and upon the next (immediate) repaint to re-read the now altered bitmap data.
See also MSDN Blogs: Whats the difference between Control.Invalidate, Control.Update and Control.Refresh?.
回答2:
Solution
Add pictureBox1.Refresh()
after updating the _Pixels
array.
This updates quite quickly, and is capable of rendering smooth video at high resolutions.
Add reference to System.Runtime.InteropServices (available via nuget)
The code for the form is now like so:
public partial class Form1 : Form
{
uint[] _Pixels { get; set; }
Bitmap _Bitmap { get; set; }
GCHandle _Handle { get; set; }
IntPtr _Addr { get; set; }
public Form1()
{
InitializeComponent();
int imageWidth = 100; //1920;
int imageHeight = 100; // 1080;
PixelFormat fmt = PixelFormat.Format32bppRgb;
int pixelFormatSize = Image.GetPixelFormatSize(fmt);
int stride = imageWidth * pixelFormatSize;
int padding = 32 - (stride % 32);
if (padding < 32)
{
stride += padding;
}
_Pixels = new uint[(stride / 32) * imageHeight + 1];
_Handle = GCHandle.Alloc(_Pixels, GCHandleType.Pinned);
_Addr = Marshal.UnsafeAddrOfPinnedArrayElement(_Pixels, 0);
_Bitmap = new Bitmap(imageWidth, imageHeight, stride / 8, fmt, _Addr);
pictureBox1.Image = _Bitmap;
}
private void button1_Click(object sender, EventArgs e)
{
for (int i = 0; i < _Pixels.Length; i++)
{
_Pixels[i] = ((uint)(255 | (255 << 8) | (255 << 16) | 0xff000000));
}
pictureBox1.Refresh();
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
_Addr = IntPtr.Zero;
if (_Handle.IsAllocated)
{
_Handle.Free();
}
_Bitmap.Dispose();
_Bitmap = null;
_Pixels = null;
}
}
Testing
Naturally, I wanted to test performance of this method to see if it would be fast enough to render smooth video.
Replace Button click with Timer tick
My test so far is a simple one to begin with. I replaced the button click event
with a timer tick event
which fires once every millisecond, and I fill the _Pixels
array with random colour values.
To ensure that the PictureBox
will not try to refresh until any current refreshes have completed, I use a single bool IsRefreshing
variable.
Frames per second calculation
I also measure the time elapsed between each refresh, by incrementing a single int HzCount
variable, and resetting this variable to zero at the beginning of each PictureBox
refresh. Just before I reset the HzCount
*, I display the value in a TextBox
.
Intel(R) Core(TM) i7-5600U CPU @ 2.60GHz 2.60GHz with 16.0 GB RAM results:
72Hz@2560x1440
*Hertz or Hz means frames (or how many times something) elapsed per second. By extension, this means that MegaHertz translates to millions of times a second and GigaHertz translates to thousand-millions (USA billions) of times a second.
来源:https://stackoverflow.com/questions/39387698/simple-and-fast-real-time-graphics-for-c-sharp-computer-game-winforms