301 lines
8.6 KiB
C#
301 lines
8.6 KiB
C#
using Pulsar.Server.Utilities;
|
|||
|
|
using System;
|
||
|
|
using System.Diagnostics;
|
||
|
|
using System.Drawing;
|
||
|
|
using System.Windows.Forms;
|
||
|
|
using System.ComponentModel;
|
||
|
|
using System.Drawing.Drawing2D;
|
||
|
|
|
||
|
|
namespace Pulsar.Server.Controls
|
||
|
|
{
|
||
|
|
public interface IRapidPictureBox
|
||
|
|
{
|
||
|
|
bool Running { get; set; }
|
||
|
|
Image GetImageSafe { get; set; }
|
||
|
|
|
||
|
|
void Start();
|
||
|
|
void Stop();
|
||
|
|
void UpdateImage(Bitmap bmp, bool cloneBitmap = false);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Custom PictureBox Control designed for rapidly-changing images.
|
||
|
|
/// </summary>
|
||
|
|
public class RapidPictureBox : PictureBox, IRapidPictureBox
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// True if the PictureBox is currently streaming images, else False.
|
||
|
|
/// </summary>
|
||
|
|
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||
|
|
public bool Running { get; set; }
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Returns the width of the original screen.
|
||
|
|
/// </summary>
|
||
|
|
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||
|
|
[Browsable(false)]
|
||
|
|
public int ScreenWidth { get; private set; }
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Returns the height of the original screen.
|
||
|
|
/// </summary>
|
||
|
|
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||
|
|
[Browsable(false)]
|
||
|
|
public int ScreenHeight { get; private set; }
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Provides thread-safe access to the Image of this Picturebox.
|
||
|
|
/// </summary>
|
||
|
|
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||
|
|
public Image GetImageSafe
|
||
|
|
{
|
||
|
|
get
|
||
|
|
{
|
||
|
|
lock (_imageLock)
|
||
|
|
{
|
||
|
|
return _frame;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
set
|
||
|
|
{
|
||
|
|
lock (_imageLock)
|
||
|
|
{
|
||
|
|
var old = _frame;
|
||
|
|
_frame = value as Bitmap;
|
||
|
|
old?.Dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
RequestRepaint();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// The lock object for the Picturebox's image.
|
||
|
|
/// </summary>
|
||
|
|
private readonly object _imageLock = new object();
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Latest frame to draw. We avoid assigning to PictureBox.Image to prevent cross-thread UI access and allocations.
|
||
|
|
/// </summary>
|
||
|
|
private Bitmap _frame;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Small placeholder assigned to base.Image so existing code paths that check Image != null keep working.
|
||
|
|
/// </summary>
|
||
|
|
private Bitmap _placeholder;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Prevent flooding the message queue; coalesce multiple UpdateImage calls into one repaint.
|
||
|
|
/// </summary>
|
||
|
|
private bool _repaintPending;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// The Stopwatch for internal FPS measuring.
|
||
|
|
/// </summary>
|
||
|
|
private Stopwatch _sWatch;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// The internal class for FPS measuring.
|
||
|
|
/// </summary>
|
||
|
|
private FrameCounter _frameCounter;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Subscribes an Eventhandler to the FrameUpdated event.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="e">The Eventhandler to set.</param>
|
||
|
|
public void SetFrameUpdatedEvent(FrameUpdatedEventHandler e)
|
||
|
|
{
|
||
|
|
_frameCounter.FrameUpdated += e;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Unsubscribes an Eventhandler from the FrameUpdated event.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="e">The Eventhandler to remove.</param>
|
||
|
|
public void UnsetFrameUpdatedEvent(FrameUpdatedEventHandler e)
|
||
|
|
{
|
||
|
|
_frameCounter.FrameUpdated -= e;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Starts the internal FPS measuring.
|
||
|
|
/// </summary>
|
||
|
|
public void Start()
|
||
|
|
{
|
||
|
|
_frameCounter = new FrameCounter();
|
||
|
|
|
||
|
|
_sWatch = Stopwatch.StartNew();
|
||
|
|
|
||
|
|
Running = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Stops the internal FPS measuring.
|
||
|
|
/// </summary>
|
||
|
|
public void Stop()
|
||
|
|
{
|
||
|
|
_sWatch?.Stop();
|
||
|
|
|
||
|
|
Running = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Updates the Image of this Picturebox.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="bmp">The new bitmap to use.</param>
|
||
|
|
/// <param name="cloneBitmap">If True the bitmap will be cloned, else it uses the original bitmap.</param>
|
||
|
|
public void UpdateImage(Bitmap bmp, bool cloneBitmap)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
CountFps();
|
||
|
|
|
||
|
|
if ((ScreenWidth != bmp.Width) || (ScreenHeight != bmp.Height))
|
||
|
|
UpdateScreenSize(bmp.Width, bmp.Height);
|
||
|
|
|
||
|
|
// Swap the frame without resizing; scaling is handled in OnPaint for speed.
|
||
|
|
lock (_imageLock)
|
||
|
|
{
|
||
|
|
var old = _frame;
|
||
|
|
_frame = cloneBitmap ? (Bitmap)bmp.Clone() : bmp;
|
||
|
|
old?.Dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
RequestRepaint();
|
||
|
|
}
|
||
|
|
catch (InvalidOperationException)
|
||
|
|
{
|
||
|
|
}
|
||
|
|
catch (Exception)
|
||
|
|
{
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Constructor, sets Picturebox double-buffered and initializes the Framecounter.
|
||
|
|
/// </summary>
|
||
|
|
public RapidPictureBox()
|
||
|
|
{
|
||
|
|
this.SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
|
||
|
|
|
||
|
|
_placeholder = new Bitmap(1, 1);
|
||
|
|
_placeholder.SetPixel(0, 0, Color.Transparent);
|
||
|
|
base.Image = _placeholder;
|
||
|
|
}
|
||
|
|
|
||
|
|
protected override void OnPaint(PaintEventArgs pe)
|
||
|
|
{
|
||
|
|
Bitmap localFrame = null;
|
||
|
|
lock (_imageLock)
|
||
|
|
{
|
||
|
|
if (_frame == null)
|
||
|
|
return;
|
||
|
|
localFrame = _frame;
|
||
|
|
}
|
||
|
|
|
||
|
|
var g = pe.Graphics;
|
||
|
|
g.SmoothingMode = SmoothingMode.None;
|
||
|
|
g.CompositingMode = CompositingMode.SourceCopy;
|
||
|
|
g.CompositingQuality = CompositingQuality.HighSpeed;
|
||
|
|
g.PixelOffsetMode = PixelOffsetMode.Half;
|
||
|
|
g.InterpolationMode = InterpolationMode.NearestNeighbor;
|
||
|
|
|
||
|
|
if (localFrame == null)
|
||
|
|
return;
|
||
|
|
|
||
|
|
var cs = this.ClientSize;
|
||
|
|
if (cs.Width <= 0 || cs.Height <= 0) return;
|
||
|
|
|
||
|
|
if (localFrame.Width == cs.Width && localFrame.Height == cs.Height)
|
||
|
|
{
|
||
|
|
g.DrawImageUnscaled(localFrame, 0, 0);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
g.DrawImage(localFrame, this.ClientRectangle);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
protected override void OnPaintBackground(PaintEventArgs pevent)
|
||
|
|
{
|
||
|
|
if (this.BackColor.A == 255)
|
||
|
|
{
|
||
|
|
using (var b = new SolidBrush(this.BackColor))
|
||
|
|
{
|
||
|
|
pevent.Graphics.FillRectangle(b, this.ClientRectangle);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
base.OnPaintBackground(pevent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void UpdateScreenSize(int newWidth, int newHeight)
|
||
|
|
{
|
||
|
|
ScreenWidth = newWidth;
|
||
|
|
ScreenHeight = newHeight;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void CountFps()
|
||
|
|
{
|
||
|
|
var deltaTime = (float)_sWatch.Elapsed.TotalSeconds;
|
||
|
|
_sWatch = Stopwatch.StartNew();
|
||
|
|
|
||
|
|
_frameCounter.Update(deltaTime);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void RequestRepaint()
|
||
|
|
{
|
||
|
|
if (_repaintPending)
|
||
|
|
return;
|
||
|
|
|
||
|
|
_repaintPending = true;
|
||
|
|
|
||
|
|
void doInvalidate()
|
||
|
|
{
|
||
|
|
if (!IsDisposed)
|
||
|
|
{
|
||
|
|
Invalidate();
|
||
|
|
//Update();
|
||
|
|
}
|
||
|
|
_repaintPending = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (IsHandleCreated && InvokeRequired)
|
||
|
|
{
|
||
|
|
try { BeginInvoke((Action)(doInvalidate)); } catch { _repaintPending = false; }
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
doInvalidate();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
protected override void Dispose(bool disposing)
|
||
|
|
{
|
||
|
|
if (disposing)
|
||
|
|
{
|
||
|
|
lock (_imageLock)
|
||
|
|
{
|
||
|
|
_frame?.Dispose();
|
||
|
|
_frame = null;
|
||
|
|
}
|
||
|
|
try
|
||
|
|
{
|
||
|
|
if (ReferenceEquals(base.Image, _placeholder))
|
||
|
|
{
|
||
|
|
base.Image = null;
|
||
|
|
}
|
||
|
|
_placeholder?.Dispose();
|
||
|
|
}
|
||
|
|
catch { }
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
_placeholder = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
base.Dispose(disposing);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|