Files
i2p 773d05f8f1
Pulsar .NET 9.0 Windows Release / build (push) Waiting to run
Mirror to Codeberg and Gitea / mirror (push) Waiting to run
initial commit
2026-08-27 10:57:58 -06:00

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