Пользовательский Picturebox с рисованием и редактированием фигурC#

Место общения программистов C#
Ответить
Anonymous
 Пользовательский Picturebox с рисованием и редактированием фигур

Сообщение Anonymous »

Я написал этот компонент для рендеринга потоковой передачи с камеры. Над потоковой передачей пользователь может нарисовать одну или несколько фигур (управляемых через интерфейс). Доступные фигуры:
  • Точка
  • Прямоугольник
  • Эллипс
  • Эллипс
  • Прямоугольник
  • Эллипс
  • Прямоугольник
  • Эллипс li>
    Ломаная линия
Помимо рисования фигур, пользователь может выбрать одну и изменить ее размер и положение. Пользователь также может видеть самую горячую точку изображения, которая затем будет постоянно перемещаться внутри изображения.
Изображение постоянно обновляется и обрабатывается событием: UpdateRadiometricImage
Это мой полный код компонента:

Код: Выделить всё

using Imager.IRBinding;
using Imager.Services;
using Imager.Shapes;
using Imager.Types;
using Imager.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Web.UI.WebControls.Expressions;
using System.Windows.Forms;

namespace Imager.Components
{
public class RadiometricImage : PictureBox, IDisposable
{
// -------- PER IL DISEGNO
internal string Serial { get; set; }
internal List Shapes = new List(); // Forme permanenti

internal List SelectedShapes { get; set; } = new List(); // Forme selezionate

public ShapeType ShapeToDraw { get; set; } = ShapeType.None;
public ModeType CurrentMode { get; set; } = ModeType.None;
public PolylineShape CurrentPolyline { get; set; } = null; // Polilinea temporanea
public bool IsDrawingPolyline { get; set; } = false; // Flag per il disegno polilinea

private Point currentCursorPoint; // Posizione corrente del cursore
private Point? startPoint = null; // Punto iniziale del disegno
private IShape tempShape = null; // Forma temporanea
private Point? selectedHandler = null; // Handler selezionato per modifica
private bool isDragging = false; // Flag per indicare il trascinamento
private Point dragStartPoint; // Punto iniziale del trascinamento
private Point? activeResizeHandler = null; // Handler attivo per il resize
private int selectedResizeHandler; // Handler selezionato per il resize
// --------

// ------------ AREA DI MISURA PANNELLO
public event EventHandler ShapeChanged;
public event EventHandler ThermalImageUpdated;
// -----------

// --------- MOSTRA PUNTO CALDO GLOBALE
public bool ShowHotSpot { get; set; } = false;
private readonly PointShape _hotSpot = new PointShape();
// ---------

private bool _disposed = false;
private Rectangle _imageBounds;
private readonly ToolTip _toolTip = new ToolTip();

private Bitmap _paletteImage = null;
public Bitmap PaletteImage
{
get => _paletteImage;
set
{
Image = _paletteImage = value;
CalculateImageBounds();
}
}

public ushort[,] ThermalImage { get; set; } = null;
public EvoIRFrameMetadata Metadata { get; set; }

public RadiometricImage()
{
Console.WriteLine("RadiometricImage created.");

BackColor = Color.Black;
Dock = DockStyle.Fill;
DoubleBuffered = true;
SizeMode = PictureBoxSizeMode.Zoom;

Resize += (s, e) =>
{
Console.WriteLine("Resize event triggered.");
CalculateImageBounds();
};

MouseMove += RadiometricImage_MouseMove;
MouseLeave += RadiometricImage_MouseLeave;

MouseDown += RadiometricImage_MouseDown;
MouseUp += RadiometricImage_MouseUp;

Paint += RadiometricImage_Paint;
}

internal void UpdateRadiometricImage(ThermalPaletteImage img)
{
PaletteImage = img.PaletteImage;
Metadata = img.IRFrameMetadata;
ThermalImage = img.ThermalImage;

ThermalImageUpdated?.Invoke(this, EventArgs.Empty);  // Notifica il cambiamento
}

private void RadiometricImage_MouseLeave(object sender, EventArgs e)
{
Console.WriteLine("Mouse fouri dall'immagine, nascondo tooltip");
_toolTip.Hide(this);
}

private void RadiometricImage_Paint(object sender, PaintEventArgs e)
{
if (ShowHotSpot)
{
int maxI = 0, maxJ = 0;
ushort max = ThermalImage[0, 0];

for (int i = 0; i < ThermalImage.GetLength(0); ++i)
{
for (int j = 0; j < ThermalImage.GetLength(1); ++j)
{
if (ThermalImage[i, j] > max)
{
max = ThermalImage[i, j];
maxI = i;
maxJ = j;
}
}
}

_hotSpot.Position = new Point(maxJ, maxI);
_hotSpot.Name = $"{Conversion.RealTemperatureValue(max)} °C";
_hotSpot.Draw(e.Graphics, FromImageToContainer, Color.Black);
DrawShapeName(e.Graphics, _hotSpot, Color.Black);
}

foreach (var shape in Shapes)
{
Console.WriteLine("shape");

shape.Draw(e.Graphics, FromImageToContainer);
DrawShapeName(e.Graphics, shape);
if (SelectedShapes.Contains(shape))
{
DrawShapeHandlers(e.Graphics, shape);
}
}

if (ShapeToDraw == ShapeType.Polyline && CurrentPolyline != null)
{
Console.WriteLine("polyline shape");
CurrentPolyline.Draw(e.Graphics, FromImageToContainer);
DrawPolylinePreview(e.Graphics);
DrawShapeHandlers(e.Graphics, CurrentPolyline);
}

tempShape?.Draw(e.Graphics, FromImageToContainer);
}

internal void RadiometricImage_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
{
if (e.KeyCode == Keys.Delete)
{
if (SelectedShapes.Any())
{
foreach (var shape in SelectedShapes.ToList())
{
RemoveShape(shape);
}

SelectedShapes.Clear();
Invalidate();
}
}
else if (e.KeyCode == Keys.Escape)
{
ResetDrawingState();
Invalidate();
}

if (SelectedShapes.Any())
{
const int moveStep = 1;
switch (e.KeyCode)
{
case Keys.Left:
MoveSelectedShapes(new Point(-moveStep, 0), true);
break;
case Keys.Right:
MoveSelectedShapes(new Point(moveStep, 0), true);
break;
case Keys.Up:
MoveSelectedShapes(new Point(0, -moveStep), true);
break;
case Keys.Down:
MoveSelectedShapes(new Point(0, moveStep), true);
break;
}
}
}

private void RadiometricImage_MouseUp(object sender, MouseEventArgs e)
{
Console.WriteLine($"MouseUp at {e.Location}");

if (isDragging)
{
isDragging = false;
Cursor = Cursors.Default;
}
else if (activeResizeHandler.HasValue)
{
activeResizeHandler = null;
Cursor = Cursors.Default;
}
else if ((ShapeToDraw == ShapeType.Rectangle || ShapeToDraw == ShapeType.Ellipse) &&  tempShape != null)
{
tempShape.Name = $"Area {Shapes.Count + 1}";
AddShape(tempShape);
tempShape = null;
startPoint = null;
}
selectedHandler = null;
Cursor = Cursors.Default;

// Invalidate();
}

private void RadiometricImage_MouseMove(object sender, MouseEventArgs e)
{
Console.WriteLine($"MouseMove at {e.Location}");

if (CurrentMode == ModeType.None && _imageBounds.Contains(e.Location))
{
var position = FromContainerToImage(e.Location);
double temperature = CameraService.GetPixelValue(ThermalImage, position);
_toolTip.Show($"{temperature} °C", this, e.X + 10, e.Y + 10);
}
else
{
_toolTip.Hide(this);
}

currentCursorPoint = e.Location;

if (activeResizeHandler.HasValue && SelectedShapes.Count == 1 && activeResizeHandler.Value != e.Location)
{
Console.WriteLine($"Active Handler: {activeResizeHandler.Value}");
var shape = SelectedShapes.First();
if (_imageBounds.Contains(e.Location))
{

shape.Resize(FromContainerToImage(activeResizeHandler.Value), FromContainerToImage(e.Location)); // Ridimensiona la forma
var aux = shape.GetHandlers();
activeResizeHandler = FromImageToContainer(aux.ElementAt(selectedResizeHandler)); // Aggiorna la posizione dell'handler
Invalidate();
}

}
else if (isDragging && SelectedShapes.Any())
{
MoveSelectedShapes(e.Location);
dragStartPoint = e.Location;
}
else
{
foreach (var shape in SelectedShapes)
{
var pos = FromContainerToImage(e.Location);
bool onHandler = false;
if (shape is PolylineShape polyline)
{
foreach (var handler in polyline.GetHandlers())
{
if (DrawHelper.IsCloseToPoint(e.Location, FromImageToContainer(handler)))
{
Cursor = Cursors.Hand;
onHandler = true;
break;
}
}
}
else if (!(shape is PointShape point))
{
foreach (var handler in shape.GetHandlers())
{
if (DrawHelper.IsCloseToPoint(e.Location, FromImageToContainer(handler)))
{
Cursor = DrawHelper.GetResizeCursor(handler, shape); // Ottieni il cursore corretto
onHandler = true;
break;
}
}
}
if (!onHandler)
{
Cursor = Cursors.Default;
}
}
}
if (CurrentMode == ModeType.Draw)
{
if (ShapeToDraw == ShapeType.Polyline && selectedHandler.HasValue) MovePolylineHandler(FromContainerToImage(e.Location));
if ((ShapeToDraw == ShapeType.Rectangle || ShapeToDraw == ShapeType.Ellipse) && startPoint.HasValue) UpdateTemporaryShape(e.Location);

Invalidate();
}
}

private void RadiometricImage_MouseDown(object sender, MouseEventArgs e)
{
Console.WriteLine($"MouseDown at {e.Location}");

if (CurrentMode == ModeType.Select)
{
HandleSelectionMouseDown(e.Location);
if (SelectedShapes.Any() &&  isDragging)
{
Cursor = Cursors.SizeAll;
}
}

if (CurrentMode == ModeType.Draw)
{
if (_imageBounds.Contains(e.Location))
{
switch (ShapeToDraw)
{
case ShapeType.Point:
AddPointShape(e.Location);
break;
case ShapeType.Rectangle:
case ShapeType.Ellipse:
startPoint = FromContainerToImage(e.Location);
break;
case ShapeType.Polyline:
HandlePolylineMouseDown(e);
break;
default:
break;
}
}
}
}

public void AddShape(IShape shape)
{
Shapes.Add(shape);
OnShapeAdded(shape);
Invalidate(); // Per ridisegnare shape.GetRegion(FromImageToContainer)
}

public void RemoveShape(IShape shape)
{
if (Shapes.Remove(shape))
{
OnShapeRemoved(shape);
Invalidate(); // Per ridisegnare shape.GetRegion(FromImageToContainer)
}
}

#region HELPERS

private Point FromContainerToImage(Point src)
{
if (Image == null) return Point.Empty;

float xRatio = (float)(src.X - _imageBounds.Left) / _imageBounds.Width;
float yRatio = (float)(src.Y - _imageBounds.Top) / _imageBounds.Height;

return new Point((int)(xRatio * Image.Width), (int)(yRatio * Image.Height));
}

private Point FromImageToContainer(Point src)
{
if (Image == null) return Point.Empty;

float xRatio = (float)src.X / Image.Width;
float yRatio = (float)src.Y / Image.Height;

return new Point((int)(xRatio * _imageBounds.Width + _imageBounds.Left), (int)(yRatio * _imageBounds.Height + _imageBounds.Top));
}

private void CalculateImageBounds()
{
if (Image == null)
{
_imageBounds = new Rectangle(0, 0, 0, 0);
return;
}

float xRatio = (float)Width / Image.Width;
float yRatio = (float)Height / Image.Height;

float ratio = Math.Min(xRatio, yRatio);

int width = (int)(Image.Width * ratio);
int height = (int)(Image.Height * ratio);

_imageBounds = new Rectangle((Width - width) / 2, (Height - height) / 2, width, height);
}

private void DrawHandler(Graphics g, Point point)
{
const int size = 10;
var rect = new Rectangle(point.X - size / 2, point.Y - size / 2, size, size);
using (var brush = new SolidBrush(SystemColors.Highlight))
{
g.FillRectangle(brush, rect);
}
}

private void DrawShapeHandlers(Graphics g, IShape shape)
{
foreach (var handler in shape.GetHandlers().Select(x => FromImageToContainer(x)))
{
DrawHandler(g, handler);
}
}

private void ResetDrawingState()
{
if (ShapeToDraw == ShapeType.Polyline && IsDrawingPolyline)
{
CurrentPolyline = null;
IsDrawingPolyline = false;
}
else if ((ShapeToDraw == ShapeType.Rectangle || ShapeToDraw == ShapeType.Ellipse) && tempShape != null)
{
tempShape = null;
startPoint = null;
}
else if (SelectedShapes.Any())
{
SelectedShapes.Clear();
}

// CurrentMode = ModeType.None;
}

private void HandlePolylineMouseDown(MouseEventArgs e)
{
if (CurrentPolyline != null)
{
var point = FromContainerToImage(e.Location);

if (CurrentPolyline.Points.Count > 0 &&  DrawHelper.IsCloseToPoint(point, CurrentPolyline.Points[0]))
{
CurrentPolyline.ClosePolyline();
CurrentPolyline.IsClosed = true;
CurrentPolyline.Name = $"Area {Shapes.Count + 1}";
AddShape(CurrentPolyline);
CurrentPolyline = null;
IsDrawingPolyline = true;
}
else
{
CurrentPolyline.Points.Add(point);
}
}
}

private void AddPointShape(Point location)
{
var pointShape = new PointShape() { Name = $"Area {Shapes.Count + 1}", Position = FromContainerToImage(location) };
AddShape(pointShape);
}

private void UpdateTemporaryShape(Point location)
{
if (startPoint.HasValue && _imageBounds.Contains(location))
{
var rect = DrawHelper.GetRectangleFromPoints(startPoint.Value, FromContainerToImage(location));
switch (ShapeToDraw)
{
case ShapeType.Rectangle:
tempShape = new RectangleShape { Rect = rect };
break;
case ShapeType.Ellipse:
tempShape = new EllipseShape { Bounds = rect };
break;
}
}
}

private void MoveSelectedShapes(Point location, bool keyboard = false)
{
var bounds = new Rectangle(0, 0, Image.Width, Image.Height);
if (keyboard)
{
foreach (var shape in SelectedShapes)
{
shape.Move(bounds, location);
}
}
else
{
var convertedLocation = FromContainerToImage(location);
var convartedStartPoint = FromContainerToImage(dragStartPoint);

var deltaX = convertedLocation.X - convartedStartPoint.X;
var deltaY = convertedLocation.Y - convartedStartPoint.Y;

foreach (var shape in SelectedShapes)
{
shape.Move(bounds, new Point(deltaX, deltaY));
}
dragStartPoint = location;
}
}

private void MovePolylineHandler(Point location)
{
if (CurrentPolyline != null && selectedHandler.HasValue)
{
var index = CurrentPolyline.Points.IndexOf(selectedHandler.Value);
if (index >= 0)
{
CurrentPolyline.Points[index] = location;
}
}
}

private void DrawShapeName(Graphics g, IShape shape, Color color = default)
{
var center = shape.NamePosition(FromImageToContainer, Image.Height);

if (color == default) color = Color.White;

using (var font = new Font("Arial", 14))
{
using (var brush = new SolidBrush(color))
{
g.DrawString(shape.Name, font, brush, center);
}
}
}

private void HandleSelectionMouseDown(Point location)
{
SelectedShapes.Clear();
activeResizeHandler = null; // Resetta l'handler attivo

foreach (var shape in Shapes)
{
if (!(shape is PointShape point))
{
int i = 0;
foreach (var handler in shape.GetHandlers())
{
var h = FromImageToContainer(handler);
if (DrawHelper.IsCloseToPoint(location, h))
{
SelectedShapes.Add(shape);
activeResizeHandler = h;  // Memorizza l'handler attivo per il resize
selectedResizeHandler = i;
Console.WriteLine($"Handler: {h}");
return;
}
i++;
}
}
if (shape.PointOnBound(FromContainerToImage(location)))
{
SelectedShapes.Add(shape);
dragStartPoint = location;
isDragging = true;
Invalidate();
return;
}
}
}

private void DrawPolylinePreview(Graphics g)
{
if (CurrentPolyline != null && CurrentPolyline.Points.Count > 0)
{
var lastPoint = CurrentPolyline.Points.Last();
if (DrawHelper.IsCloseToPoint(FromContainerToImage(currentCursorPoint), CurrentPolyline.Points[0]))
{
g.DrawLine(Pens.White, FromImageToContainer(lastPoint), FromImageToContainer(CurrentPolyline.Points[0]));
}
else
{
g.DrawLine(Pens.Gray, FromImageToContainer(lastPoint), currentCursorPoint);
}
}
}

#endregion

#region AREA_DI_MISURA

private void OnShapeAdded(IShape shape)
{
ShapeChanged?.Invoke(this, new ShapeChangedEventArgs(shape, AreaOperation.Added));
}

private void OnShapeRemoved(IShape shape)
{
ShapeChanged?.Invoke(this, new ShapeChangedEventArgs(shape, AreaOperation.Removed));
}

#endregion

protected override void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
if (PaletteImage != null)
{
PaletteImage.Dispose();
PaletteImage = null;
}

if (Shapes != null)
{
Shapes.Clear();
Shapes = null;
}

if (SelectedShapes != null)
{
SelectedShapes.Clear();
SelectedShapes = null;
}

if (ThermalImage != null) ThermalImage = null;
if (CurrentPolyline != null) CurrentPolyline = null;

Resize -= (s, e) => CalculateImageBounds();
MouseMove -= RadiometricImage_MouseMove;
MouseDown -= RadiometricImage_MouseDown;
MouseUp -= RadiometricImage_MouseUp;
Paint -= RadiometricImage_Paint;
}
}

base.Dispose(disposing);

_disposed = true;
}
}
}
Код работает, но когда потоковая передача с камеры ведется в высоком разрешении или нарисовано много фигур, все приложение тормозит, вызывая неудобства для пользователя. Есть ли у вас какие-либо советы и предложения по оптимизации этого компонента в лучшем случае?

Подробнее здесь: https://stackoverflow.com/questions/792 ... nd-editing
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C#»