sliding window for vision builder image strip pannel
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using OpenCvSharp;
|
||||
using OpenCvSharp.Extensions;
|
||||
|
||||
namespace Hawkeye.VisionBuilder.Features.ImagesStrip.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single image thumbnail item with an image and filename label.
|
||||
/// </summary>
|
||||
public class ImageThumbnailControl : Panel
|
||||
{
|
||||
private const int ThumbnailSize = 108;
|
||||
private const int BorderWidth = 3;
|
||||
|
||||
private readonly PictureBox _pictureBox;
|
||||
private readonly Label _label;
|
||||
private bool _isSelected;
|
||||
private string _imagePath;
|
||||
|
||||
public string ImagePath
|
||||
{
|
||||
get => _imagePath;
|
||||
set
|
||||
{
|
||||
_imagePath = value;
|
||||
LoadImage(value);
|
||||
_label.Text = Path.GetFileNameWithoutExtension(value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set
|
||||
{
|
||||
if (_isSelected != value)
|
||||
{
|
||||
_isSelected = value;
|
||||
Invalidate(); // Trigger repaint to show/hide border
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler ThumbnailClicked;
|
||||
|
||||
public ImageThumbnailControl()
|
||||
{
|
||||
// Set up the container panel
|
||||
Width = ThumbnailSize + (BorderWidth * 2);
|
||||
Height = ThumbnailSize + 25 + (BorderWidth * 2); // Extra space for label
|
||||
Margin = new Padding(5);
|
||||
Cursor = Cursors.Hand;
|
||||
|
||||
// Create PictureBox for the image
|
||||
_pictureBox = new PictureBox
|
||||
{
|
||||
Width = ThumbnailSize,
|
||||
Height = ThumbnailSize,
|
||||
SizeMode = PictureBoxSizeMode.StretchImage,
|
||||
Location = new System.Drawing.Point(BorderWidth, BorderWidth),
|
||||
BackColor = Color.Black
|
||||
};
|
||||
_pictureBox.Click += OnThumbnailClick;
|
||||
|
||||
// Create Label for the filename
|
||||
_label = new Label
|
||||
{
|
||||
Width = ThumbnailSize,
|
||||
Height = 20,
|
||||
Location = new System.Drawing.Point(BorderWidth, ThumbnailSize + BorderWidth + 2),
|
||||
TextAlign = ContentAlignment.MiddleCenter,
|
||||
AutoEllipsis = true,
|
||||
Font = new Font("Segoe UI", 8f)
|
||||
};
|
||||
_label.Click += OnThumbnailClick;
|
||||
|
||||
Controls.Add(_pictureBox);
|
||||
Controls.Add(_label);
|
||||
|
||||
// Enable double buffering to reduce flicker
|
||||
DoubleBuffered = true;
|
||||
}
|
||||
|
||||
private void LoadImage(string imagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(imagePath))
|
||||
{
|
||||
using (var mat = new Mat(imagePath))
|
||||
{
|
||||
var resized = mat.Resize(new OpenCvSharp.Size(ThumbnailSize, ThumbnailSize));
|
||||
var bitmap = resized.ToBitmap();
|
||||
|
||||
// Dispose old image if exists
|
||||
var oldImage = _pictureBox.Image;
|
||||
_pictureBox.Image = bitmap;
|
||||
oldImage?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If image loading fails, show error in label
|
||||
_label.Text = "Error loading image";
|
||||
_label.ForeColor = Color.Red;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnThumbnailClick(object sender, EventArgs e)
|
||||
{
|
||||
ThumbnailClicked?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected override void OnPaint(PaintEventArgs e)
|
||||
{
|
||||
base.OnPaint(e);
|
||||
|
||||
// Draw selection border if selected
|
||||
if (_isSelected)
|
||||
{
|
||||
using (var pen = new Pen(Color.DodgerBlue, BorderWidth))
|
||||
{
|
||||
var rect = new Rectangle(
|
||||
BorderWidth / 2,
|
||||
BorderWidth / 2,
|
||||
Width - BorderWidth,
|
||||
Height - BorderWidth
|
||||
);
|
||||
e.Graphics.DrawRectangle(pen, rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// Dispose the image to free memory
|
||||
_pictureBox?.Image?.Dispose();
|
||||
_pictureBox?.Dispose();
|
||||
_label?.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.Designer.cs
generated
Normal file
47
Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.Designer.cs
generated
Normal file
@@ -0,0 +1,47 @@
|
||||
namespace Hawkeye.VisionBuilder.Features.ImagesStrip.Components
|
||||
{
|
||||
partial class VirtualizedImageList
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
|
||||
|
||||
#region Component 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()
|
||||
{
|
||||
flowLayoutPanel = new FlowLayoutPanel();
|
||||
SuspendLayout();
|
||||
//
|
||||
// flowLayoutPanel
|
||||
//
|
||||
flowLayoutPanel.AutoScroll = true;
|
||||
flowLayoutPanel.Dock = DockStyle.Fill;
|
||||
flowLayoutPanel.Location = new Point(0, 0);
|
||||
flowLayoutPanel.Name = "flowLayoutPanel";
|
||||
flowLayoutPanel.Size = new Size(800, 150);
|
||||
flowLayoutPanel.TabIndex = 0;
|
||||
flowLayoutPanel.WrapContents = false;
|
||||
//
|
||||
// VirtualizedImageList
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(7F, 15F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
Controls.Add(flowLayoutPanel);
|
||||
Name = "VirtualizedImageList";
|
||||
Size = new Size(800, 150);
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Hawkeye.VisionBuilder.Features.ImagesStrip.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// A virtualized image list control that efficiently displays image thumbnails
|
||||
/// by loading only the images in the current window (managed by ImagesViewModel).
|
||||
/// </summary>
|
||||
public partial class VirtualizedImageList : UserControl
|
||||
{
|
||||
private ImagesViewModel _viewModel;
|
||||
private Dictionary<int, ImageThumbnailControl> _thumbnailControls = new Dictionary<int, ImageThumbnailControl>();
|
||||
private bool _isUpdating = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ImagesViewModel that manages the image data and windowing logic.
|
||||
/// </summary>
|
||||
public ImagesViewModel ViewModel
|
||||
{
|
||||
get => _viewModel;
|
||||
set
|
||||
{
|
||||
if (_viewModel != null)
|
||||
{
|
||||
// Unsubscribe from old view model
|
||||
_viewModel.ImagesAdded -= OnImagesAdded;
|
||||
_viewModel.ImagesRemoved -= OnImagesRemoved;
|
||||
_viewModel.CurrentImageChanged -= OnCurrentImageChanged;
|
||||
_viewModel.ImagesCleared -= OnImagesCleared;
|
||||
}
|
||||
|
||||
_viewModel = value;
|
||||
|
||||
if (_viewModel != null)
|
||||
{
|
||||
// Subscribe to new view model
|
||||
_viewModel.ImagesAdded += OnImagesAdded;
|
||||
_viewModel.ImagesRemoved += OnImagesRemoved;
|
||||
_viewModel.CurrentImageChanged += OnCurrentImageChanged;
|
||||
_viewModel.ImagesCleared += OnImagesCleared;
|
||||
|
||||
// Load initial images
|
||||
RefreshImages();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a thumbnail is clicked by the user.
|
||||
/// </summary>
|
||||
public event EventHandler<string> ImageSelected;
|
||||
|
||||
public VirtualizedImageList()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnImagesAdded(object sender, ImageAddedEventArgs[] e)
|
||||
{
|
||||
if (_isUpdating) return;
|
||||
|
||||
_isUpdating = true;
|
||||
flowLayoutPanel.SuspendLayout();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var args in e.OrderBy(x => x.Index))
|
||||
{
|
||||
AddThumbnail(args.Index, args.FilePath);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
flowLayoutPanel.ResumeLayout();
|
||||
_isUpdating = false;
|
||||
}
|
||||
|
||||
UpdateCurrentSelection();
|
||||
}
|
||||
|
||||
private void OnImagesRemoved(object sender, ImageRemovedEventArgs[] e)
|
||||
{
|
||||
if (_isUpdating) return;
|
||||
|
||||
_isUpdating = true;
|
||||
flowLayoutPanel.SuspendLayout();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var args in e.OrderByDescending(x => x.Index))
|
||||
{
|
||||
RemoveThumbnail(args.Index);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
flowLayoutPanel.ResumeLayout();
|
||||
_isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCurrentImageChanged(object sender, CurrentImageChangedEventArgs e)
|
||||
{
|
||||
UpdateCurrentSelection();
|
||||
ScrollToCurrentImage();
|
||||
}
|
||||
|
||||
private void OnImagesCleared(object sender, EventArgs e)
|
||||
{
|
||||
ClearAllThumbnails();
|
||||
}
|
||||
|
||||
private void AddThumbnail(int index, string filePath)
|
||||
{
|
||||
// Create new thumbnail control
|
||||
var thumbnail = new ImageThumbnailControl
|
||||
{
|
||||
ImagePath = filePath,
|
||||
Tag = index // Store the index in Tag for later reference
|
||||
};
|
||||
|
||||
thumbnail.ThumbnailClicked += OnThumbnailClicked;
|
||||
|
||||
// Find the correct insertion position to maintain order
|
||||
int insertPosition = 0;
|
||||
foreach (Control control in flowLayoutPanel.Controls)
|
||||
{
|
||||
if (control.Tag is int controlIndex && controlIndex < index)
|
||||
{
|
||||
insertPosition++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
flowLayoutPanel.Controls.Add(thumbnail);
|
||||
flowLayoutPanel.Controls.SetChildIndex(thumbnail, insertPosition);
|
||||
_thumbnailControls[index] = thumbnail;
|
||||
}
|
||||
|
||||
private void RemoveThumbnail(int index)
|
||||
{
|
||||
if (_thumbnailControls.TryGetValue(index, out var thumbnail))
|
||||
{
|
||||
thumbnail.ThumbnailClicked -= OnThumbnailClicked;
|
||||
flowLayoutPanel.Controls.Remove(thumbnail);
|
||||
thumbnail.Dispose();
|
||||
_thumbnailControls.Remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearAllThumbnails()
|
||||
{
|
||||
_isUpdating = true;
|
||||
flowLayoutPanel.SuspendLayout();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var thumbnail in _thumbnailControls.Values)
|
||||
{
|
||||
thumbnail.ThumbnailClicked -= OnThumbnailClicked;
|
||||
thumbnail.Dispose();
|
||||
}
|
||||
|
||||
_thumbnailControls.Clear();
|
||||
flowLayoutPanel.Controls.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
flowLayoutPanel.ResumeLayout();
|
||||
_isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCurrentSelection()
|
||||
{
|
||||
if (_viewModel == null) return;
|
||||
|
||||
int currentIndex = _viewModel.CurrentIndex;
|
||||
|
||||
// Update IsSelected property for all thumbnails
|
||||
foreach (var kvp in _thumbnailControls)
|
||||
{
|
||||
kvp.Value.IsSelected = (kvp.Key == currentIndex);
|
||||
}
|
||||
|
||||
// Raise event for external listeners
|
||||
if (_thumbnailControls.TryGetValue(currentIndex, out var currentThumbnail))
|
||||
{
|
||||
ImageSelected?.Invoke(this, currentThumbnail.ImagePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void ScrollToCurrentImage()
|
||||
{
|
||||
if (_viewModel == null) return;
|
||||
|
||||
int currentIndex = _viewModel.CurrentIndex;
|
||||
|
||||
if (_thumbnailControls.TryGetValue(currentIndex, out var thumbnail))
|
||||
{
|
||||
// Scroll the thumbnail into view
|
||||
flowLayoutPanel.ScrollControlIntoView(thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnThumbnailClicked(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is ImageThumbnailControl thumbnail && _viewModel != null)
|
||||
{
|
||||
string imagePath = thumbnail.ImagePath;
|
||||
|
||||
// Notify view model of selection
|
||||
_viewModel.SelectByPath(imagePath);
|
||||
|
||||
// Raise event for external listeners
|
||||
ImageSelected?.Invoke(this, imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshImages()
|
||||
{
|
||||
if (_viewModel == null) return;
|
||||
|
||||
ClearAllThumbnails();
|
||||
|
||||
_isUpdating = true;
|
||||
flowLayoutPanel.SuspendLayout();
|
||||
|
||||
try
|
||||
{
|
||||
// Add all currently loaded images
|
||||
foreach (var imageItem in _viewModel.LoadedImages.OrderBy(x => x.Index))
|
||||
{
|
||||
AddThumbnail(imageItem.Index, imageItem.FilePath);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
flowLayoutPanel.ResumeLayout();
|
||||
_isUpdating = false;
|
||||
}
|
||||
|
||||
UpdateCurrentSelection();
|
||||
ScrollToCurrentImage();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (_viewModel != null)
|
||||
{
|
||||
_viewModel.ImagesAdded -= OnImagesAdded;
|
||||
_viewModel.ImagesRemoved -= OnImagesRemoved;
|
||||
_viewModel.CurrentImageChanged -= OnCurrentImageChanged;
|
||||
_viewModel.ImagesCleared -= OnImagesCleared;
|
||||
}
|
||||
|
||||
ClearAllThumbnails();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
Reference in New Issue
Block a user