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>
|
||||
@@ -43,7 +43,7 @@
|
||||
ckbExecuteWorkflow = new ToolStripButton();
|
||||
ckbStopOnError = new ToolStripButton();
|
||||
ckbStopOnNoError = new ToolStripButton();
|
||||
listView1 = new ListView();
|
||||
virtualizedImageList1 = new Hawkeye.VisionBuilder.Features.ImagesStrip.Components.VirtualizedImageList();
|
||||
toolStrip1.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
@@ -173,25 +173,21 @@
|
||||
ckbStopOnNoError.Size = new Size(97, 23);
|
||||
ckbStopOnNoError.Text = "Stop on no error";
|
||||
//
|
||||
// listView1
|
||||
// virtualizedImageList1
|
||||
//
|
||||
listView1.Alignment = ListViewAlignment.Left;
|
||||
listView1.Dock = DockStyle.Fill;
|
||||
listView1.HeaderStyle = ColumnHeaderStyle.None;
|
||||
listView1.LabelWrap = false;
|
||||
listView1.Location = new Point(0, 26);
|
||||
listView1.MultiSelect = false;
|
||||
listView1.Name = "listView1";
|
||||
listView1.Size = new Size(876, 171);
|
||||
listView1.TabIndex = 1;
|
||||
listView1.UseCompatibleStateImageBehavior = false;
|
||||
virtualizedImageList1.Dock = DockStyle.Fill;
|
||||
virtualizedImageList1.Location = new Point(0, 26);
|
||||
virtualizedImageList1.Name = "virtualizedImageList1";
|
||||
virtualizedImageList1.Size = new Size(876, 171);
|
||||
virtualizedImageList1.TabIndex = 1;
|
||||
virtualizedImageList1.ViewModel = null;
|
||||
//
|
||||
// ImageStripPanel
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(7F, 15F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(876, 197);
|
||||
Controls.Add(listView1);
|
||||
Controls.Add(virtualizedImageList1);
|
||||
Controls.Add(toolStrip1);
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
@@ -212,12 +208,12 @@
|
||||
private ToolStripButton btnToEnd;
|
||||
private ToolStripSeparator toolStripSeparator1;
|
||||
private ToolStripButton cbRepeat;
|
||||
private ListView listView1;
|
||||
private ToolStripLabel toolStripLabel1;
|
||||
private ToolStripNumberControl ncInterval;
|
||||
private ToolStripButton ckbExecuteWorkflow;
|
||||
private ToolStripLabel lblSelectedImage;
|
||||
private ToolStripButton ckbStopOnError;
|
||||
private ToolStripButton ckbStopOnNoError;
|
||||
private Components.VirtualizedImageList virtualizedImageList1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Drawing.Drawing2D;
|
||||
using Hawkeye.VisionBuilder.Workflow.Datatypes;
|
||||
using OpenCvSharp;
|
||||
using OpenCvSharp.Extensions;
|
||||
using VisionBuilder.UI.Common.Processing;
|
||||
@@ -8,35 +9,51 @@ namespace Hawkeye.VisionBuilder.Features.ImagesStrip
|
||||
{
|
||||
public partial class ImageStripPanel : Form, IImageSource
|
||||
{
|
||||
|
||||
|
||||
public ImagesViewModel ImagesViewModel { get; set; }
|
||||
|
||||
public ImageStripPanel()
|
||||
{
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
listView1.SelectedIndexChanged += ListView1_SelectedIndexChanged;
|
||||
|
||||
ImagesViewModel = new ImagesViewModel(20);
|
||||
//ImagesViewModel.CurrentImageChanged += ImagesViewModel_CurrentImageChanged;
|
||||
//ImagesViewModel.ImagesAdded += ImagesViewModel_ImageAdded;
|
||||
//ImagesViewModel.ImagesRemoved += ImagesViewModel_ImageRemoved;
|
||||
virtualizedImageList1.ViewModel=ImagesViewModel;
|
||||
virtualizedImageList1.ImageSelected += VirtualizedImageList1_ImageSelected;
|
||||
}
|
||||
|
||||
private void ListView1_SelectedIndexChanged(object? sender, EventArgs e)
|
||||
private void VirtualizedImageList1_ImageSelected(object? sender, string e)
|
||||
{
|
||||
if (listView1.SelectedItems.Count > 0)
|
||||
{
|
||||
lblSelectedImage.Text = $"{listView1.SelectedIndices[0] + 1}/{listView1.Items.Count}";
|
||||
var path = (string)listView1.SelectedItems[0].Tag;
|
||||
listView1.SelectedItems[0].ImageKey = path;
|
||||
var img = new Mat(path);
|
||||
var image = new Workflow.Datatypes.HawkeyeImage()
|
||||
{
|
||||
Filename = path,
|
||||
ImageData = img
|
||||
};
|
||||
listView1.LargeImageList.Images.RemoveByKey(path);
|
||||
listView1.LargeImageList.Images.Add(path, img.Resize(new OpenCvSharp.Size(108, 108)).ToBitmap());
|
||||
ImageSelected(image);
|
||||
}
|
||||
lblSelectedImage.Text = $"{this.ImagesViewModel.CurrentIndex + 1}/{ImagesViewModel.TotalCount}";
|
||||
ImageSelected(new HawkeyeImage() { Filename = ImagesViewModel.CurrentImagePath, ImageData = new Mat(ImagesViewModel.CurrentImagePath) });
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//private void ImagesViewModel_CurrentImageChanged(object? sender, CurrentImageChangedEventArgs e)
|
||||
//{
|
||||
// var path = e.NewImagePath;
|
||||
// for (int i = 0; i < listView1.Items.Count; i++)
|
||||
// {
|
||||
// var item = listView1.Items[i];
|
||||
// var itemPath = (string)item.ImageKey;
|
||||
// if (itemPath == path)
|
||||
// {
|
||||
// SetSelectedImage(i);
|
||||
// ImageSelected(new HawkeyeImage(){Filename = ImagesViewModel.CurrentImagePath, ImageData = new Mat(ImagesViewModel.CurrentImagePath)});
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
//}
|
||||
|
||||
|
||||
public bool IsExecutingWorkflow
|
||||
{
|
||||
get => ckbExecuteWorkflow.Checked;
|
||||
@@ -44,145 +61,53 @@ namespace Hawkeye.VisionBuilder.Features.ImagesStrip
|
||||
}
|
||||
|
||||
public event Action<Workflow.Datatypes.HawkeyeImage> ImageSelected = delegate { };
|
||||
public void LoadImage(Mat image)
|
||||
{
|
||||
ImageList imageList = new ImageList();
|
||||
imageList.ImageSize = new Size(108, 108);
|
||||
listView1.LargeImageList = imageList;
|
||||
listView1.Items.Clear();
|
||||
imageList.Images.Add("image", image.ToBitmap());
|
||||
var item = listView1.Items.Add("image");
|
||||
item.Tag = new Workflow.Datatypes.HawkeyeImage()
|
||||
{
|
||||
Filename = "image",
|
||||
ImageData = image
|
||||
};
|
||||
item.Text = "";
|
||||
item.ImageKey = "image";
|
||||
|
||||
|
||||
listView1.SelectedIndices.Clear();
|
||||
listView1.SelectedIndices.Add(0);
|
||||
}
|
||||
public Bitmap FitImage(Image image, int targetWidth, int targetHeight)
|
||||
{
|
||||
var aspectRatio = (float)image.Width / image.Height;
|
||||
int width;
|
||||
int height;
|
||||
if ((float)targetWidth / targetHeight > aspectRatio)
|
||||
{
|
||||
width = (int)(targetHeight * aspectRatio);
|
||||
height = targetHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
width = targetWidth;
|
||||
height = (int)(targetWidth / aspectRatio);
|
||||
}
|
||||
|
||||
Bitmap bitmap = new Bitmap(targetWidth, targetHeight);
|
||||
using (Graphics graphics = Graphics.FromImage(bitmap))
|
||||
{
|
||||
graphics.DrawImage(image, new Rectangle(0, 0, width, height));
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void LoadImages(string directory)
|
||||
{
|
||||
listView1.BeginUpdate();
|
||||
ImageList imageList = new ImageList();
|
||||
imageList.ImageSize = new Size(108, 108);
|
||||
listView1.LargeImageList = imageList;
|
||||
listView1.Items.Clear();
|
||||
var mock = new Bitmap(108, 108);
|
||||
using (Graphics g = Graphics.FromImage(mock))
|
||||
{
|
||||
var brush = new HatchBrush(HatchStyle.Percent50, Color.LightGray, Color.Transparent);
|
||||
g.FillRectangle(brush, 0, 0, 108, 108);
|
||||
|
||||
g.DrawString("Not loaded", new Font("Arial", 12), Brushes.Gray, new PointF(0, 0));
|
||||
}
|
||||
var allImages = Directory.GetFiles(directory, "*.bmp", SearchOption.AllDirectories)
|
||||
.Concat(Directory.GetFiles(directory, "*.png", SearchOption.AllDirectories))
|
||||
.Concat(Directory.GetFiles(directory, "*.jpg", SearchOption.AllDirectories))
|
||||
.Where(x => !Path.GetFileNameWithoutExtension(x).Contains("_analysis"))
|
||||
//.Select(x=>new {path=x,bmp = new Bitmap(x)})
|
||||
.Select(x => new { path = x, bmp = mock })
|
||||
.ToArray();
|
||||
|
||||
|
||||
imageList.Images.Add("none", mock);
|
||||
|
||||
|
||||
foreach (var image in allImages)
|
||||
{
|
||||
|
||||
|
||||
var item = listView1.Items.Add(image.path);
|
||||
item.Text = "";
|
||||
item.Tag = image.path;
|
||||
item.ImageKey = "none";
|
||||
}
|
||||
|
||||
if (listView1.Items.Count > 0)
|
||||
{
|
||||
listView1.SelectedIndices.Clear();
|
||||
listView1.SelectedIndices.Add(0);
|
||||
toolStrip1.Enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
toolStrip1.Enabled = false;
|
||||
}
|
||||
listView1.EndUpdate();
|
||||
//listView1.BeginUpdate();
|
||||
ImagesViewModel.LoadFromDirectory(directory);
|
||||
//listView1.EndUpdate();
|
||||
}
|
||||
|
||||
private void btnToStart_Click(object sender, EventArgs e)
|
||||
{
|
||||
listView1.SelectedIndices.Clear();
|
||||
listView1.SelectedIndices.Add(0);
|
||||
listView1.EnsureVisible(listView1.SelectedIndices[0]);
|
||||
ImagesViewModel.First();
|
||||
}
|
||||
|
||||
public void Prev()
|
||||
{
|
||||
btnPrev_Click(null, null);
|
||||
ImagesViewModel.Previous();
|
||||
|
||||
}
|
||||
private void btnPrev_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (listView1.SelectedIndices.Count == 0) return;
|
||||
var index = listView1.SelectedIndices[0];
|
||||
index--;
|
||||
if (index < 0) index++;
|
||||
listView1.SelectedIndices.Clear();
|
||||
listView1.SelectedIndices.Add(index);
|
||||
listView1.EnsureVisible(listView1.SelectedIndices[0]);
|
||||
Prev();
|
||||
}
|
||||
|
||||
public void Next()
|
||||
{
|
||||
btnNext_Click(null, null);
|
||||
ImagesViewModel.Next();
|
||||
}
|
||||
|
||||
private void btnNext_Click(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
if (listView1.SelectedIndices.Count == 0) return;
|
||||
var index = listView1.SelectedIndices[0];
|
||||
index++;
|
||||
if (index > listView1.Items.Count - 1) index--;
|
||||
listView1.SelectedIndices.Clear();
|
||||
listView1.SelectedIndices.Add(index);
|
||||
listView1.EnsureVisible(listView1.SelectedIndices[0]);
|
||||
Next();
|
||||
}
|
||||
|
||||
private void btnToEnd_Click(object sender, EventArgs e)
|
||||
{
|
||||
listView1.SelectedIndices.Clear();
|
||||
listView1.SelectedIndices.Add(listView1.Items.Count - 1);
|
||||
listView1.EnsureVisible(listView1.SelectedIndices[0]);
|
||||
ImagesViewModel.Last();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private void btnPlay_Click(object sender, EventArgs e)
|
||||
{
|
||||
Task.Run(Playback);
|
||||
@@ -194,14 +119,17 @@ namespace Hawkeye.VisionBuilder.Features.ImagesStrip
|
||||
{
|
||||
Action a = () =>
|
||||
{
|
||||
if (listView1.SelectedIndices.Count == 0) return;
|
||||
var index = listView1.SelectedIndices[0];
|
||||
index++;
|
||||
if (index > listView1.Items.Count - 1)
|
||||
if (ImagesViewModel.TotalCount == 0)
|
||||
{
|
||||
btnPlay.Checked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImagesViewModel.CurrentIndex > ImagesViewModel.TotalCount - 1)
|
||||
{
|
||||
if (cbRepeat.Checked)
|
||||
{
|
||||
index = 0;
|
||||
ImagesViewModel.First();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -210,19 +138,14 @@ namespace Hawkeye.VisionBuilder.Features.ImagesStrip
|
||||
}
|
||||
|
||||
}
|
||||
listView1.SelectedIndices.Clear();
|
||||
listView1.SelectedIndices.Add(index);
|
||||
listView1.EnsureVisible(listView1.SelectedIndices[0]);
|
||||
Next();
|
||||
};
|
||||
listView1.Invoke(a);
|
||||
virtualizedImageList1.Invoke(a);
|
||||
virtualizedImageList1.Invalidate();
|
||||
await Task.Delay(int.Parse((string)ncInterval.Text));
|
||||
}
|
||||
}
|
||||
|
||||
public string GetName()
|
||||
{
|
||||
return "ImageStripPanel";
|
||||
}
|
||||
|
||||
public Task<Mat> GetImage(CancellationToken token)
|
||||
{
|
||||
@@ -231,19 +154,13 @@ namespace Hawkeye.VisionBuilder.Features.ImagesStrip
|
||||
|
||||
public Mat GetImage()
|
||||
{
|
||||
if (listView1.SelectedIndices.Count == 0) return null;
|
||||
|
||||
var path = listView1.SelectedItems[0].ImageKey;
|
||||
|
||||
return new Mat(path);
|
||||
|
||||
return new Mat(ImagesViewModel.CurrentImagePath);
|
||||
}
|
||||
|
||||
public string GetImagePath()
|
||||
{
|
||||
if (listView1.SelectedIndices.Count == 0) return null;
|
||||
var path = listView1.SelectedItems[0].Tag as string;
|
||||
if (path == null) return null;
|
||||
return path;
|
||||
return ImagesViewModel.CurrentImagePath;
|
||||
}
|
||||
|
||||
public bool StopOnErrors
|
||||
@@ -277,42 +194,11 @@ namespace Hawkeye.VisionBuilder.Features.ImagesStrip
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveImage(Workflow.Datatypes.HawkeyeImage currentImage)
|
||||
{
|
||||
if (listView1.SelectedIndices.Count == 0) return;
|
||||
var index = listView1.SelectedIndices[0];
|
||||
listView1.Items.RemoveAt(index);
|
||||
if (listView1.Items.Count > 0)
|
||||
{
|
||||
if (index > listView1.Items.Count - 1) index--;
|
||||
listView1.SelectedIndices.Clear();
|
||||
listView1.SelectedIndices.Add(index);
|
||||
listView1.EnsureVisible(listView1.SelectedIndices[0]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public List<string> GetFileList()
|
||||
{
|
||||
return listView1.Items.Cast<ListViewItem>().Select(x => (string)x.Tag).ToList();
|
||||
}
|
||||
|
||||
public void ReSelect()
|
||||
{
|
||||
if(listView1.SelectedIndices.Count == 0) return;
|
||||
var index = listView1.SelectedIndices[0];
|
||||
listView1.SelectedIndices.Clear();
|
||||
listView1.SelectedIndices.Add(index);
|
||||
listView1.EnsureVisible(listView1.SelectedIndices[0]);
|
||||
}
|
||||
|
||||
public void SelectImage(string nextFile)
|
||||
{
|
||||
var index = listView1.Items.Cast<ListViewItem>().Where(x => (string)x.Tag == nextFile).Select(x => x.Index).FirstOrDefault();
|
||||
if (index == -1) return;
|
||||
listView1.SelectedIndices.Clear();
|
||||
listView1.SelectedIndices.Add(index);
|
||||
listView1.EnsureVisible(listView1.SelectedIndices[0]);
|
||||
ImagesViewModel.SelectByPath(nextFile);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
426
Hawkeye.VisionBuilder/Features/ImagesStrip/ImagesViewModel.cs
Normal file
426
Hawkeye.VisionBuilder/Features/ImagesStrip/ImagesViewModel.cs
Normal file
@@ -0,0 +1,426 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Hawkeye.VisionBuilder.Features.ImagesStrip;
|
||||
|
||||
public class ImagesViewModel
|
||||
{
|
||||
private const int DefaultBufferSize = 10;
|
||||
|
||||
private string? _directoryPath;
|
||||
private List<string> _allImagePaths = new();
|
||||
private int _currentIndex = -1;
|
||||
private int _bufferSize;
|
||||
private HashSet<int> _currentLoadedIndices = new();
|
||||
|
||||
public ObservableCollection<ImageItem> LoadedImages { get; } = new();
|
||||
|
||||
public int BufferSize
|
||||
{
|
||||
get => _bufferSize;
|
||||
set
|
||||
{
|
||||
if (value < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(value), "Buffer size must be non-negative.");
|
||||
|
||||
_bufferSize = value;
|
||||
if (_currentIndex >= 0)
|
||||
LoadWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public int CurrentIndex => _currentIndex;
|
||||
public int CurrentRelativeIndex => _currentIndex - (_currentIndex - _bufferSize < 0 ? 0 : _currentIndex - _bufferSize);
|
||||
|
||||
public int CurrentIndexOnLoaded => GetIndexOfLoaded();
|
||||
|
||||
public int TotalCount => _allImagePaths.Count;
|
||||
|
||||
public string? CurrentImagePath => _currentIndex >= 0 && _currentIndex < _allImagePaths.Count
|
||||
? _allImagePaths[_currentIndex]
|
||||
: null;
|
||||
|
||||
public bool CanGoNext => _currentIndex < _allImagePaths.Count - 1;
|
||||
|
||||
public bool CanGoPrevious => _currentIndex > 0;
|
||||
|
||||
public bool HasImages => _allImagePaths.Count > 0;
|
||||
|
||||
// Events
|
||||
public event EventHandler<ImageAddedEventArgs[]>? ImagesAdded;
|
||||
public event EventHandler<ImageRemovedEventArgs[]>? ImagesRemoved;
|
||||
public event EventHandler? ImagesCleared;
|
||||
public event EventHandler<CurrentImageChangedEventArgs>? CurrentImageChanged;
|
||||
public event EventHandler<ImagesLoadedEventArgs>? ImagesLoaded;
|
||||
public event EventHandler? WindowRefreshed;
|
||||
|
||||
public ImagesViewModel() : this(DefaultBufferSize)
|
||||
{
|
||||
}
|
||||
|
||||
public ImagesViewModel(int bufferSize)
|
||||
{
|
||||
if (bufferSize < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(bufferSize), "Buffer size must be non-negative.");
|
||||
|
||||
_bufferSize = bufferSize;
|
||||
}
|
||||
|
||||
private int GetIndexOfLoaded()
|
||||
{
|
||||
return LoadedImages.FirstOrDefault(img => img.IsCurrent)?.Index ?? -1;
|
||||
}
|
||||
|
||||
public int GetIndexFromPath(string path)
|
||||
{
|
||||
return _allImagePaths.IndexOf(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads all image files from the specified directory path.
|
||||
/// </summary>
|
||||
/// <param name="directoryPath">The directory path containing images</param>
|
||||
/// <param name="searchPattern">Optional search pattern (default: *.* for all files)</param>
|
||||
/// <param name="supportedExtensions">Optional list of supported extensions (e.g., .jpg, .png, .bmp)</param>
|
||||
public void LoadFromDirectory(string directoryPath, string searchPattern = "*.*", string[]? supportedExtensions = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(directoryPath))
|
||||
throw new ArgumentException("Directory path cannot be null or empty.", nameof(directoryPath));
|
||||
|
||||
if (!Directory.Exists(directoryPath))
|
||||
throw new DirectoryNotFoundException($"Directory not found: {directoryPath}");
|
||||
|
||||
_directoryPath = directoryPath;
|
||||
|
||||
var defaultExtensions = new[] { ".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tif", ".tiff" };
|
||||
var extensions = supportedExtensions ?? defaultExtensions;
|
||||
|
||||
var files = Directory.GetFiles(directoryPath, searchPattern, SearchOption.AllDirectories)
|
||||
.Where(f => !Path.GetFileNameWithoutExtension(f).Contains("_analysis") && extensions.Contains(Path.GetExtension(f), StringComparer.OrdinalIgnoreCase))
|
||||
.OrderBy(f => f)
|
||||
.ToList();
|
||||
|
||||
_allImagePaths = files;
|
||||
_currentIndex = files.Count > 0 ? 0 : -1;
|
||||
|
||||
LoadWindow();
|
||||
|
||||
OnImagesLoaded(new ImagesLoadedEventArgs(files.Count, _directoryPath));
|
||||
OnCurrentImageChanged(new CurrentImageChangedEventArgs(CurrentIndex,CurrentIndex,CurrentImagePath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to the first image.
|
||||
/// </summary>
|
||||
public bool First()
|
||||
{
|
||||
if (_allImagePaths.Count == 0)
|
||||
return false;
|
||||
|
||||
if (_currentIndex == 0)
|
||||
return false;
|
||||
|
||||
var oldIndex = _currentIndex;
|
||||
_currentIndex = 0;
|
||||
LoadWindow();
|
||||
OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to the previous image.
|
||||
/// </summary>
|
||||
public bool Previous()
|
||||
{
|
||||
if (!CanGoPrevious)
|
||||
return false;
|
||||
|
||||
var oldIndex = _currentIndex;
|
||||
_currentIndex--;
|
||||
LoadWindow();
|
||||
OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to the next image.
|
||||
/// </summary>
|
||||
public bool Next()
|
||||
{
|
||||
if (!CanGoNext)
|
||||
return false;
|
||||
|
||||
var oldIndex = _currentIndex;
|
||||
_currentIndex++;
|
||||
LoadWindow();
|
||||
OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to the last image.
|
||||
/// </summary>
|
||||
public bool Last()
|
||||
{
|
||||
if (_allImagePaths.Count == 0)
|
||||
return false;
|
||||
|
||||
var lastIndex = _allImagePaths.Count - 1;
|
||||
if (_currentIndex == lastIndex)
|
||||
return false;
|
||||
|
||||
var oldIndex = _currentIndex;
|
||||
_currentIndex = lastIndex;
|
||||
LoadWindow();
|
||||
OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to the Nth image (0-based index).
|
||||
/// </summary>
|
||||
public bool SelectNth(int index)
|
||||
{
|
||||
if (index < 0 || index >= _allImagePaths.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(index), $"Index must be between 0 and {_allImagePaths.Count - 1}.");
|
||||
|
||||
|
||||
var oldIndex = _currentIndex;
|
||||
_currentIndex = index;
|
||||
LoadWindow();
|
||||
OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SelectByPath(string path)
|
||||
{
|
||||
var index = _allImagePaths.IndexOf(path);
|
||||
if (index < 0)
|
||||
return false;
|
||||
|
||||
return SelectNth(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all loaded images and resets the state.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
// Fire remove events for all currently loaded images
|
||||
|
||||
OnImagesRemoved(_currentLoadedIndices.OrderByDescending(i => i).Select(x => new ImageRemovedEventArgs(x, _allImagePaths[x])).ToArray());
|
||||
_allImagePaths.Clear();
|
||||
LoadedImages.Clear();
|
||||
_currentLoadedIndices.Clear();
|
||||
_currentIndex = -1;
|
||||
_directoryPath = null;
|
||||
|
||||
OnImagesCleared();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the current window by reloading images from disk.
|
||||
/// </summary>
|
||||
public void RefreshWindow()
|
||||
{
|
||||
LoadWindow();
|
||||
OnWindowRefreshed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the directory, useful when files have been added or removed.
|
||||
/// </summary>
|
||||
public void ReloadDirectory(bool preserveCurrentImage = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_directoryPath))
|
||||
return;
|
||||
|
||||
string? currentPath = preserveCurrentImage ? CurrentImagePath : null;
|
||||
LoadFromDirectory(_directoryPath);
|
||||
|
||||
if (preserveCurrentImage && currentPath != null)
|
||||
{
|
||||
var newIndex = _allImagePaths.IndexOf(currentPath);
|
||||
if (newIndex >= 0)
|
||||
{
|
||||
_currentIndex = newIndex;
|
||||
LoadWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadWindow()
|
||||
{
|
||||
if (_currentIndex < 0 || _allImagePaths.Count == 0)
|
||||
{
|
||||
|
||||
OnImagesRemoved(_currentLoadedIndices.OrderByDescending(i => i).Select(x => new ImageRemovedEventArgs(x, _allImagePaths[x])).ToArray());
|
||||
LoadedImages.Clear();
|
||||
_currentLoadedIndices.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
int startIndex = Math.Max(0, _currentIndex - _bufferSize);
|
||||
int endIndex = Math.Min(_allImagePaths.Count - 1, _currentIndex + _bufferSize);
|
||||
|
||||
// Create a set of new indices that should be loaded
|
||||
var newIndices = new HashSet<int>();
|
||||
for (int i = startIndex; i <= endIndex; i++)
|
||||
{
|
||||
newIndices.Add(i);
|
||||
}
|
||||
|
||||
// Find indices to remove (in current but not in new)
|
||||
var indicesToRemove = _currentLoadedIndices.Except(newIndices).OrderByDescending(i => i).ToList();
|
||||
|
||||
// Find indices to add (in new but not in current)
|
||||
var indicesToAdd = newIndices.Except(_currentLoadedIndices).OrderBy(i => i).ToList();
|
||||
|
||||
// Remove images that are no longer in the window
|
||||
foreach (var index in indicesToRemove)
|
||||
{
|
||||
var item = LoadedImages.FirstOrDefault(img => img.Index == index);
|
||||
if (item != null)
|
||||
{
|
||||
LoadedImages.Remove(item);
|
||||
}
|
||||
_currentLoadedIndices.Remove(index);
|
||||
}
|
||||
OnImagesRemoved(indicesToRemove.Select(x=>new ImageRemovedEventArgs(x, _allImagePaths[x])).ToArray());
|
||||
|
||||
// Add new images to the window
|
||||
foreach (var index in indicesToAdd)
|
||||
{
|
||||
var item = new ImageItem(index, _allImagePaths[index], index == _currentIndex);
|
||||
|
||||
// Insert at the correct position to maintain order
|
||||
int insertIndex = 0;
|
||||
for (int i = 0; i < LoadedImages.Count; i++)
|
||||
{
|
||||
if (LoadedImages[i].Index < index)
|
||||
insertIndex = i + 1;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
LoadedImages.Insert(insertIndex, item);
|
||||
_currentLoadedIndices.Add(index);
|
||||
}
|
||||
OnImagesAdded(indicesToAdd.Select(x => new ImageAddedEventArgs(x, _allImagePaths[x])).ToArray());
|
||||
|
||||
// Update IsCurrent flag for all items
|
||||
for (int i = 0; i < LoadedImages.Count; i++)
|
||||
{
|
||||
var currentItem = LoadedImages[i];
|
||||
if (currentItem.IsCurrent != (currentItem.Index == _currentIndex))
|
||||
{
|
||||
// Replace with updated item
|
||||
LoadedImages[i] = new ImageItem(currentItem.Index, currentItem.FilePath, currentItem.Index == _currentIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnImagesAdded(ImageAddedEventArgs[] e)
|
||||
{
|
||||
ImagesAdded?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnImagesRemoved(ImageRemovedEventArgs[] e)
|
||||
{
|
||||
ImagesRemoved?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnImagesCleared()
|
||||
{
|
||||
ImagesCleared?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected virtual void OnCurrentImageChanged(CurrentImageChangedEventArgs e)
|
||||
{
|
||||
CurrentImageChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnImagesLoaded(ImagesLoadedEventArgs e)
|
||||
{
|
||||
ImagesLoaded?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnWindowRefreshed()
|
||||
{
|
||||
WindowRefreshed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public class ImageItem
|
||||
{
|
||||
public int Index { get; }
|
||||
public string FilePath { get; }
|
||||
public string FileName { get; }
|
||||
public bool IsCurrent { get; }
|
||||
|
||||
public ImageItem(int index, string filePath, bool isCurrent)
|
||||
{
|
||||
Index = index;
|
||||
FilePath = filePath;
|
||||
FileName = Path.GetFileName(filePath);
|
||||
IsCurrent = isCurrent;
|
||||
}
|
||||
}
|
||||
|
||||
public class ImageAddedEventArgs : EventArgs
|
||||
{
|
||||
public int Index { get; }
|
||||
public string FilePath { get; }
|
||||
|
||||
public ImageAddedEventArgs(int index, string filePath)
|
||||
{
|
||||
Index = index;
|
||||
FilePath = filePath;
|
||||
}
|
||||
}
|
||||
|
||||
public class ImageRemovedEventArgs : EventArgs
|
||||
{
|
||||
public int Index { get; }
|
||||
public string FilePath { get; }
|
||||
|
||||
public ImageRemovedEventArgs(int index, string filePath)
|
||||
{
|
||||
Index = index;
|
||||
FilePath = filePath;
|
||||
}
|
||||
}
|
||||
|
||||
public class CurrentImageChangedEventArgs : EventArgs
|
||||
{
|
||||
public int OldIndex { get; }
|
||||
public int NewIndex { get; }
|
||||
public string? NewImagePath { get; }
|
||||
|
||||
public CurrentImageChangedEventArgs(int oldIndex, int newIndex, string? newImagePath)
|
||||
{
|
||||
OldIndex = oldIndex;
|
||||
NewIndex = newIndex;
|
||||
NewImagePath = newImagePath;
|
||||
}
|
||||
}
|
||||
|
||||
public class ImagesLoadedEventArgs : EventArgs
|
||||
{
|
||||
public int TotalCount { get; }
|
||||
public string DirectoryPath { get; }
|
||||
|
||||
public ImagesLoadedEventArgs(int totalCount, string directoryPath)
|
||||
{
|
||||
TotalCount = totalCount;
|
||||
DirectoryPath = directoryPath;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user