Capturing a screenshot across multiple displays - c#

As part of my application I'd like to be able to grap a screenshot of a chosen area on my desktop. I came across example code of how to do this a while ago (if I could remember where I would cite) and the example works nicely, however it has a problem with multiple displays.
It works by creating a transparent full screen form and then you draw a rubberband in it to designate the area. The issue is it spawns on the main display, and only allows you to grab content on that display or one to the right of it. If you have 3 displays and your master is the center there is no way to grab the left.
My question is how I can allow capture across all 3 displays.
Example code:
RubberBand.cs
public partial class RubberBand : Form
{
public Point lastLoc;
public Size lastSize;
bool mouseDown = false;
Point mouseDownPoint = Point.Empty;
Point mousePoint = Point.Empty;
Main mainform;
Pen pen;
Rectangle bounds = new Rectangle();
public RubberBand(Main mainform)
{
this.mainform = mainform;
InitializeComponent();
this.TopMost = true;
this.Opacity = .30;
this.TransparencyKey = System.Drawing.Color.White;
this.Location = new Point(0, 0);
DoubleBuffered = true;
pen = new Pen(System.Drawing.Color.DarkRed, 3);
pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
int maxX = 0;
int maxY = 0;
foreach (Screen screen in System.Windows.Forms.Screen.AllScreens)
{
int x = screen.Bounds.X + screen.Bounds.Width;
if (x > maxX)
maxX = x;
int y = screen.Bounds.Y + screen.Bounds.Height;
if (y > maxY)
maxY = y;
}
bounds.X = 0;
bounds.Y = 0;
bounds.Width = maxX;
bounds.Height = maxY;
this.Size = new Size(bounds.Width, bounds.Height);
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
mouseDown = true;
mousePoint = mouseDownPoint = e.Location;
}
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
mouseDown = false;
// corey
this.lastLoc = new Point(Math.Min(mouseDownPoint.X, mousePoint.X), Math.Min(mouseDownPoint.Y, mousePoint.Y));
this.lastSize = new Size(Math.Abs(mouseDownPoint.X - mousePoint.X), Math.Abs(mouseDownPoint.Y - mousePoint.Y));
this.Close();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
mousePoint = e.Location;
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
//base.OnPaint(e);
Region region = new Region(bounds);
if (mouseDown)
{
Rectangle selectionWindow = new Rectangle(
Math.Min(mouseDownPoint.X, mousePoint.X),
Math.Min(mouseDownPoint.Y, mousePoint.Y),
Math.Abs(mouseDownPoint.X - mousePoint.X),
Math.Abs(mouseDownPoint.Y - mousePoint.Y));
// make a hole, where we can see thru this form
region.Xor(selectionWindow);
e.Graphics.FillRegion(Brushes.Black, region);
}
else
{
e.Graphics.FillRegion(Brushes.LightGray, region);
e.Graphics.DrawLine(pen,
mousePoint.X, 0,
mousePoint.X, this.Size.Height);
e.Graphics.DrawLine(pen,
0, mousePoint.Y,
this.Size.Width, mousePoint.Y);
}
}
}
RubberBand.Designer.cs
partial class RubberBand
{
/// <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.SuspendLayout();
//
// RubberBand
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.White;
this.ClientSize = new System.Drawing.Size(284, 262);
this.Cursor = System.Windows.Forms.Cursors.Cross;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.Name = "RubberBand";
this.ShowInTaskbar = false;
this.Text = "RubberBand";
this.TransparencyKey = System.Drawing.Color.White;
this.ResumeLayout(false);
}
#endregion
}
And this is the code that calls it:
private void ShowRubberBand()
{
using (RubberBand rbf = new RubberBand(this))
{
rbf.ShowDialog();
Size sLastSize = rbf.lastSize;
if (sLastSize.Width > 0 && sLastSize.Height > 0)
{
Rectangle r = new Rectangle();
r.Location = rbf.lastLoc;
r.Size = sLastSize;
CaptureBitmap(r);
}
}
this.Show();
}
private void CaptureBitmap(Rectangle r)
{
bitmap = new Bitmap(r.Width, r.Height);
using (Graphics g = Graphics.FromImage(bitmap))
{
g.CopyFromScreen(r.Location, new Point(0, 0), r.Size);
}
PasteImage(bitmap);
}

this.Location = new Point(0, 0); is related to the main screen top left corner. Screens on the left or above the main(primary) screen have negative location coordinates. You have to take it into account when position your window and set its size.
In RubberBand constructor:
int maxX = 0;
int maxY = 0;
int minX = 0;
int minY = 0;
foreach (Screen screen in System.Windows.Forms.Screen.AllScreens)
{
int x = screen.Bounds.X + screen.Bounds.Width;
if (x > maxX)
maxX = x;
int y = screen.Bounds.Y + screen.Bounds.Height;
if (y > maxY)
maxY = y;
if(screen.Bound.X < minX) minX = screen.Bound.X;
if(screen.Bound.Y < minY) minY = screen.Bound.Y;
}
bounds.X = minX;
bounds.Y = minY;
bounds.Width = maxX - minX;
bounds.Height = maxY - minY;
this.Location = new Point(bounds.X, bounds.Y);
this.Size = new Size(bounds.Width, bounds.Height);

Related

Creating Custom Picturebox with Draggable and Resizable Selection Window

I'm using the following code to draw a selection rectangle over a picturebox and allow the user to select and drag it to the desired position.
What I intend to achieve is to allow the user to adjust the rectangle size by implementing option to adjust the rectangle size. Currently I have managed to achieve the following.
How to solve this issue?
public class DraggablePictureBox : PictureBox
{
Boolean hit1 = false, hit2 = false;
public Boolean notagimg = true;
public Boolean editedflag = false;
public Boolean notext = false;
public Boolean tdrawflag = false, tdrawflag2 = false;
Bitmap l;
public Form1 LaunchOrigin2 { get; set; }
public Point point = new Point(0, 0);
public Point point2 = new Point(0, 0);
public int sizemode = 1;
public DraggablePictureBox()
{
this.Invalidate();
}
protected override void OnMouseMove(MouseEventArgs e)
{
this.Cursor = Cursors.Arrow;
if (e.Button == MouseButtons.Left)
{
point = e.Location;
base.OnMouseDown(e);
this.Invalidate();
}
}
protected override void OnMouseDown(MouseEventArgs e)
{
PointF x = new PointF(e.X, e.Y);
Pen p = new Pen(Brushes.Red, 2f);
RectangleF rect2 = new RectangleF(1, 1, 1, 1);
RectangleF rect = new RectangleF(1, 1, 1, 1);
if (e.Button == MouseButtons.Left)
{
this.Cursor = Cursors.Hand;
//Creating Rectangles to check to find the selected object
if (notext == false)
{
rect = new RectangleF(point, new Size(400,400));
}
if (rect.Contains(x) && notext == false)
{
hit1 = true;
}
if (hit1 == true )
{
this.Invalidate();
}
this.Invalidate();
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
tdrawflag = false;
}
if (e.Button == MouseButtons.Left)
{
point = e.Location;
base.OnMouseDown(e);
this.Invalidate();
}
}
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
Pen p = new Pen(Brushes.Red, 5);
p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
Pen p2 = new Pen(Brushes.LightYellow, 5);
p2.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
if (!hit1)
{
pe.Graphics.DrawRectangle(p, new Rectangle(point, new Size(400, 400)));
}
else
{
pe.Graphics.DrawRectangle(p2, new Rectangle(point, new Size(400, 400)));
hit1 = false;
}
}
}
You have different options:
You can draw a resizable frame on the picture box
You can create a resizable control and add it to picture box
In this answer, I've taken the second option to be able to use built-in sizing features of the controls. Here is a screen capture which shows how it looks like in action:
Example - Creating a Frame Control
As an example, I'll create a resizable control and will add it to the picture box.
using System;
using System.Drawing;
using System.Windows.Forms;
public class FrameControl : Control
{
public FrameControl()
{
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
DoubleBuffered = true;
ResizeRedraw = true;
BackColor = Color.Transparent;
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
using (var p = new Pen(Color.Black, 4))
{
p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
e.Graphics.DrawRectangle(p, 0, 0, Width - 1, Height - 1);
}
}
const int WM_NCHITTEST = 0x84;
const int WM_SETCURSOR = 0x20;
const int WM_NCLBUTTONDBLCLK = 0xA3;
protected override void WndProc(ref Message m)
{
int borderWidth = 10;
if (m.Msg == WM_SETCURSOR) /*Setting cursor to SizeAll*/
{
if ((m.LParam.ToInt32() & 0xffff) == 0x2 /*Move*/)
{
Cursor.Current = Cursors.SizeAll;
m.Result = (IntPtr)1;
return;
}
}
if ((m.Msg == WM_NCLBUTTONDBLCLK)) /*Disable Mazimiz on Double click*/
{
m.Result = (IntPtr)1;
return;
}
base.WndProc(ref m);
if (m.Msg == WM_NCHITTEST)
{
var pos = PointToClient(new Point(m.LParam.ToInt32() & 0xffff,
m.LParam.ToInt32() >> 16));
if (pos.X <= ClientRectangle.Left + borderWidth &&
pos.Y <= ClientRectangle.Top + borderWidth)
m.Result = new IntPtr(13); //TOPLEFT
else if (pos.X >= ClientRectangle.Right - borderWidth &&
pos.Y <= ClientRectangle.Top + borderWidth)
m.Result = new IntPtr(14); //TOPRIGHT
else if (pos.X <= ClientRectangle.Left + borderWidth &&
pos.Y >= ClientRectangle.Bottom - borderWidth)
m.Result = new IntPtr(16); //BOTTOMLEFT
else if (pos.X >= ClientRectangle.Right - borderWidth &&
pos.Y >= ClientRectangle.Bottom - borderWidth)
m.Result = new IntPtr(17); //BOTTOMRIGHT
else if (pos.X <= ClientRectangle.Left + borderWidth)
m.Result = new IntPtr(10); //LEFT
else if (pos.Y <= ClientRectangle.Top + borderWidth)
m.Result = new IntPtr(12); //TOP
else if (pos.X >= ClientRectangle.Right - borderWidth)
m.Result = new IntPtr(11); //RIGHT
else if (pos.Y >= ClientRectangle.Bottom - borderWidth)
m.Result = new IntPtr(15); //Bottom
else
m.Result = new IntPtr(2); //Move
}
}
}
Then add the control to the picture box:
var s = 100;
var c = new FrameControl();
c.Size = new Size(s, s);
c.Location = new Point((pictureBox1.Width - s) / 2, (pictureBox1.Height - s) / 2);
pictureBox1.Controls.Add(c);
To add a fancy effect of filling outside of the frame with semi-transparent color:
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.ExcludeClip(pictureBox1.Controls[0].Bounds);
using (var b = new SolidBrush(Color.FromArgb(100, Color.Black)))
e.Graphics.FillRectangle(b, pictureBox1.ClientRectangle);
}
As you can see in the paint event, you can find the FrameControl using pictureBox1.Controls[0]. So you can find its location and size.
You can encapsulate all the logic of the picture box in a derived picture box.
Note: Flicker-free rendering
If you experience flickering when moving the frame, use the following code in your form:
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED
return cp;
}
}

Windows Forms Bitmap vs. Drawing Speed

I have data in a multidimensional array of doubles and I want to draw it on my form in a grid (with different values being different colors). Write now I am using a Panel control and in its OnPaint() I do do the drawing by calling Graphics.DrawRectangle() for each entry in the array. The data changes very frequently, so I call Refresh() on the panel when it does, but this is quite slow and I get a lot of flickering, I can also see the drawing of each rectangle happen sequentially, and I know that parallel drawing of the rectangles is not possible, because it is not thread-safe.
Would constructing a Bitmap where each pixel is an entry in the array, and then setting a PictureBox to show the Bitmap (and scale its size up) be faster than using the panel and OnPaint?
Here is a sample of the code I am using:
namespace PredPreySim
{
public partial class SimulationForm : Form
{
Simulation simulation; // instance of the simulation class
bool running = true;
int simWidth = 30;
int simHeight = 20;
int cellSize = 15;
System.Threading.Thread t;
public SimulationForm()
{
InitializeComponent();
simulation = new Simulation(simWidth, simHeight, numHerbivores, 0.3);
graphicsTimeDelay = Convert.ToInt32(GraphicsTimeDelay.Value);
ResizePanel();
}
private void ResizePanel()
{
panel1.Width = simWidth * cellSize;
panel1.Height = simHeight * cellSize;
}
// draws the board
private void panel1_Paint(object sender, PaintEventArgs e)
{
Graphics graphics = e.Graphics;
SolidBrush brush = new SolidBrush(Color.Green);
RectangleF rectangle = new Rectangle();
rectangle.Width = cellSize;
rectangle.Height = cellSize;
// draw all the plants
Color plantColor;
for (int i = 0; i < simWidth; ++i)
{
for (int j = 0; j < simHeight; ++j)
{
int r, g = 255, b;
r = b = 255 - (int)(255.0 * Math.Tanh(simulation.plants[i, j]));
plantColor = Color.FromArgb(100, r, g, b);
brush.Color = plantColor;
rectangle.Location = new PointF(i * cellSize, j * cellSize);
graphics.FillRectangle(brush, rectangle);
}
}
brush.Dispose();
graphics.Dispose();
}
private void StartStopButton_Click(object sender, EventArgs e)
{
if (running) // then stop
{
running = false;
StartStopButton.Text = "Start";
}
else // start
{
running = true;
StartStopButton.Text = "Stop";
t = new Thread(new ThreadStart(this.runSimulation));
t.Start();
t.Priority = System.Threading.ThreadPriority.Highest;
}
}
private void runSimulation()
{
while (running)
{
simulation.Update();
panel1.Invoke(new MethodInvoker(Refresh)); // makes the call thread-safe
}
t.Abort();
}
}
}

How to make a window non-capturable by screenshots? [duplicate]

I'm trying to write a magnifier application using a winform or wpf windows. The idea is to drag the window over a spot on the screen and magnify it. I know it exists commercially but need to build a customized version. The challenge I'm having is to capture the screen image behind the active application.
I have found code to capture a screen image below. But it includes the active window
{
Rectangle bounds = new Rectangle(this.Location, this.Size);
Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height);
Graphics g = Graphics.FromImage(bitmap);
g.CopyFromScreen(this.Location, Point.Empty, bounds.Size);
Bitmap bp2 = new Bitmap(bitmap); // local copy of image...
pictureBox1.Image = bp2;
}
Adding statements to hide the active application correct the image capture, but introduce a screen flicker which I'd like to avoid. (modified code below)
{
this.StartPosition = FormStartPosition.Manual; // get current window location
Point cur = this.Location;
this.Location = new Point(-500, -500); // hide the active app off screen.
Rectangle bounds = new Rectangle(cur, this.Size);
Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height);
Graphics g = Graphics.FromImage(bitmap);
g.CopyFromScreen(cur, Point.Empty, bounds.Size);
Bitmap bp2 = new Bitmap(bitmap); // local copy of image...
pictureBox1.Image = bp2;
this.Location = cur; // restore application location
}
Can someone suggest an alternative to capture a screen region, behind an active windows?
Thx.
Wrapping the Magnification API is pretty useful. I created a Winforms control that does this. Add a new class to your project and paste the code shown below. Compile. Drop the new control from the top of the toolbox onto a form.
The TrackMouse property controls which part of the screen is shown magnified. Set to False, it magnifies the area covered by the control, making it act like a magnifying glass. Set to True, it will operate like Windows' Magnifier, following the mouse.
The Magnification property controls the amount of magnification. You can already adjust it by using the mouse wheel.
The form you drop it should have its TopMost property set to True. You might want to tinker with its Region property to make it resemble a spyglass.
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.InteropServices;
class Magnifier : Control {
public Magnifier() {
if (!MagInitialize()) throw new NotSupportedException();
timer = new Timer { Interval = 45 };
timer.Tick += (o, ea) => { if (trackMouse) setSource(false); else Invalidate(); };
}
[DefaultValue(false)]
public bool TrackMouse {
get { return trackMouse; }
set { trackMouse = value; setSource(false); }
}
[DefaultValue(2.0f)]
public float Magnification {
get { return magnification; }
set { magnification = Math.Max(1, value); setSource(true); }
}
protected override CreateParams CreateParams {
get {
var cp = base.CreateParams;
if (!this.DesignMode) {
cp.ClassName = "Magnifier";
//cp.Style |= MS_SHOWMAGNIFIEDCURSOR;
this.SetStyle(ControlStyles.UserPaint, true);
}
return cp;
}
}
protected override void OnHandleCreated(EventArgs e) {
base.OnHandleCreated(e);
if (!this.DesignMode) {
setSource(true);
this.FindForm().LocationChanged += ParentLocationChanged;
timer.Start();
}
}
protected override void Dispose(bool disposing) {
if (disposing) {
var frm = this.FindForm();
if (frm != null) frm.LocationChanged -= ParentLocationChanged;
timer.Dispose();
MagUninitialize();
}
base.Dispose(disposing);
}
private void ParentLocationChanged(object sender, EventArgs e) {
if (!trackMouse) setSource(false);
}
protected override void OnSizeChanged(EventArgs e) {
setSource(false);
base.OnSizeChanged(e);
}
protected override void OnMouseWheel(MouseEventArgs e) {
this.Magnification += e.Delta / 100f;
((HandledMouseEventArgs)e).Handled = true;
}
private void setSource(bool newmag) {
if (!this.IsHandleCreated || this.DesignMode) return;
if (newmag) {
var xform = new MAGTRANSFORM();
xform.v11 = xform.v22 = magnification;
xform.v33 = 1.0f;
MagSetWindowTransform(this.Handle, ref xform);
}
Point center;
if (trackMouse) center = Cursor.Position;
else {
var rc = this.RectangleToScreen(this.Bounds);
center = new Point(rc.Left + rc.Width / 2, rc.Top + rc.Height / 2);
}
var scr = Screen.FromPoint(center);
var rect = new RECT();
rect.left = Math.Max(scr.Bounds.Left, center.X - (int)(this.Width / magnification / 2));
rect.top = Math.Max(scr.Bounds.Top, center.Y - (int)(this.Height / magnification / 2));
rect.right = rect.left + (int)(this.Width / magnification);
if (rect.right > scr.Bounds.Right) {
rect.right = scr.Bounds.Right;
rect.left = rect.right - (int)(this.Width / magnification);
}
rect.bottom = center.Y + (int)(this.Height / magnification);
if (rect.bottom > scr.Bounds.Bottom) {
rect.bottom = scr.Bounds.Bottom;
rect.top = rect.bottom - (int)(this.Height / magnification);
}
MagSetWindowSource(this.Handle, ref rect);
this.Invalidate();
}
private Timer timer;
private bool trackMouse;
private float magnification = 2.0f;
private struct RECT {
public int left, top, right, bottom;
}
private struct MAGTRANSFORM {
public float v11, v12, v13;
public float v21, v22, v23;
public float v31, v32, v33;
}
[DllImport("magnification.dll")]
private static extern bool MagInitialize();
[DllImport("magnification.dll")]
private static extern bool MagUninitialize();
[DllImport("magnification.dll")]
private static extern bool MagSetWindowSource(IntPtr hWnd, ref RECT rc);
[DllImport("magnification.dll")]
private static extern bool MagSetWindowTransform(IntPtr hWnd, ref MAGTRANSFORM xform);
private const int MS_SHOWMAGNIFIEDCURSOR = 1;
private const int MS_CLIPAROUNDCURSOR = 2;
private const int MS_INVERTCOLORS = 4;
}

Screen capture without active window

I'm trying to write a magnifier application using a winform or wpf windows. The idea is to drag the window over a spot on the screen and magnify it. I know it exists commercially but need to build a customized version. The challenge I'm having is to capture the screen image behind the active application.
I have found code to capture a screen image below. But it includes the active window
{
Rectangle bounds = new Rectangle(this.Location, this.Size);
Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height);
Graphics g = Graphics.FromImage(bitmap);
g.CopyFromScreen(this.Location, Point.Empty, bounds.Size);
Bitmap bp2 = new Bitmap(bitmap); // local copy of image...
pictureBox1.Image = bp2;
}
Adding statements to hide the active application correct the image capture, but introduce a screen flicker which I'd like to avoid. (modified code below)
{
this.StartPosition = FormStartPosition.Manual; // get current window location
Point cur = this.Location;
this.Location = new Point(-500, -500); // hide the active app off screen.
Rectangle bounds = new Rectangle(cur, this.Size);
Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height);
Graphics g = Graphics.FromImage(bitmap);
g.CopyFromScreen(cur, Point.Empty, bounds.Size);
Bitmap bp2 = new Bitmap(bitmap); // local copy of image...
pictureBox1.Image = bp2;
this.Location = cur; // restore application location
}
Can someone suggest an alternative to capture a screen region, behind an active windows?
Thx.
Wrapping the Magnification API is pretty useful. I created a Winforms control that does this. Add a new class to your project and paste the code shown below. Compile. Drop the new control from the top of the toolbox onto a form.
The TrackMouse property controls which part of the screen is shown magnified. Set to False, it magnifies the area covered by the control, making it act like a magnifying glass. Set to True, it will operate like Windows' Magnifier, following the mouse.
The Magnification property controls the amount of magnification. You can already adjust it by using the mouse wheel.
The form you drop it should have its TopMost property set to True. You might want to tinker with its Region property to make it resemble a spyglass.
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.InteropServices;
class Magnifier : Control {
public Magnifier() {
if (!MagInitialize()) throw new NotSupportedException();
timer = new Timer { Interval = 45 };
timer.Tick += (o, ea) => { if (trackMouse) setSource(false); else Invalidate(); };
}
[DefaultValue(false)]
public bool TrackMouse {
get { return trackMouse; }
set { trackMouse = value; setSource(false); }
}
[DefaultValue(2.0f)]
public float Magnification {
get { return magnification; }
set { magnification = Math.Max(1, value); setSource(true); }
}
protected override CreateParams CreateParams {
get {
var cp = base.CreateParams;
if (!this.DesignMode) {
cp.ClassName = "Magnifier";
//cp.Style |= MS_SHOWMAGNIFIEDCURSOR;
this.SetStyle(ControlStyles.UserPaint, true);
}
return cp;
}
}
protected override void OnHandleCreated(EventArgs e) {
base.OnHandleCreated(e);
if (!this.DesignMode) {
setSource(true);
this.FindForm().LocationChanged += ParentLocationChanged;
timer.Start();
}
}
protected override void Dispose(bool disposing) {
if (disposing) {
var frm = this.FindForm();
if (frm != null) frm.LocationChanged -= ParentLocationChanged;
timer.Dispose();
MagUninitialize();
}
base.Dispose(disposing);
}
private void ParentLocationChanged(object sender, EventArgs e) {
if (!trackMouse) setSource(false);
}
protected override void OnSizeChanged(EventArgs e) {
setSource(false);
base.OnSizeChanged(e);
}
protected override void OnMouseWheel(MouseEventArgs e) {
this.Magnification += e.Delta / 100f;
((HandledMouseEventArgs)e).Handled = true;
}
private void setSource(bool newmag) {
if (!this.IsHandleCreated || this.DesignMode) return;
if (newmag) {
var xform = new MAGTRANSFORM();
xform.v11 = xform.v22 = magnification;
xform.v33 = 1.0f;
MagSetWindowTransform(this.Handle, ref xform);
}
Point center;
if (trackMouse) center = Cursor.Position;
else {
var rc = this.RectangleToScreen(this.Bounds);
center = new Point(rc.Left + rc.Width / 2, rc.Top + rc.Height / 2);
}
var scr = Screen.FromPoint(center);
var rect = new RECT();
rect.left = Math.Max(scr.Bounds.Left, center.X - (int)(this.Width / magnification / 2));
rect.top = Math.Max(scr.Bounds.Top, center.Y - (int)(this.Height / magnification / 2));
rect.right = rect.left + (int)(this.Width / magnification);
if (rect.right > scr.Bounds.Right) {
rect.right = scr.Bounds.Right;
rect.left = rect.right - (int)(this.Width / magnification);
}
rect.bottom = center.Y + (int)(this.Height / magnification);
if (rect.bottom > scr.Bounds.Bottom) {
rect.bottom = scr.Bounds.Bottom;
rect.top = rect.bottom - (int)(this.Height / magnification);
}
MagSetWindowSource(this.Handle, ref rect);
this.Invalidate();
}
private Timer timer;
private bool trackMouse;
private float magnification = 2.0f;
private struct RECT {
public int left, top, right, bottom;
}
private struct MAGTRANSFORM {
public float v11, v12, v13;
public float v21, v22, v23;
public float v31, v32, v33;
}
[DllImport("magnification.dll")]
private static extern bool MagInitialize();
[DllImport("magnification.dll")]
private static extern bool MagUninitialize();
[DllImport("magnification.dll")]
private static extern bool MagSetWindowSource(IntPtr hWnd, ref RECT rc);
[DllImport("magnification.dll")]
private static extern bool MagSetWindowTransform(IntPtr hWnd, ref MAGTRANSFORM xform);
private const int MS_SHOWMAGNIFIEDCURSOR = 1;
private const int MS_CLIPAROUNDCURSOR = 2;
private const int MS_INVERTCOLORS = 4;
}

Huge FPS drop when PictureBox have an Image - Is there a way to improve the FPS of this WinForm code sample?

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;
}
}
}
"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)

Categories

Resources