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();
|
ckbExecuteWorkflow = new ToolStripButton();
|
||||||
ckbStopOnError = new ToolStripButton();
|
ckbStopOnError = new ToolStripButton();
|
||||||
ckbStopOnNoError = new ToolStripButton();
|
ckbStopOnNoError = new ToolStripButton();
|
||||||
listView1 = new ListView();
|
virtualizedImageList1 = new Hawkeye.VisionBuilder.Features.ImagesStrip.Components.VirtualizedImageList();
|
||||||
toolStrip1.SuspendLayout();
|
toolStrip1.SuspendLayout();
|
||||||
SuspendLayout();
|
SuspendLayout();
|
||||||
//
|
//
|
||||||
@@ -173,25 +173,21 @@
|
|||||||
ckbStopOnNoError.Size = new Size(97, 23);
|
ckbStopOnNoError.Size = new Size(97, 23);
|
||||||
ckbStopOnNoError.Text = "Stop on no error";
|
ckbStopOnNoError.Text = "Stop on no error";
|
||||||
//
|
//
|
||||||
// listView1
|
// virtualizedImageList1
|
||||||
//
|
//
|
||||||
listView1.Alignment = ListViewAlignment.Left;
|
virtualizedImageList1.Dock = DockStyle.Fill;
|
||||||
listView1.Dock = DockStyle.Fill;
|
virtualizedImageList1.Location = new Point(0, 26);
|
||||||
listView1.HeaderStyle = ColumnHeaderStyle.None;
|
virtualizedImageList1.Name = "virtualizedImageList1";
|
||||||
listView1.LabelWrap = false;
|
virtualizedImageList1.Size = new Size(876, 171);
|
||||||
listView1.Location = new Point(0, 26);
|
virtualizedImageList1.TabIndex = 1;
|
||||||
listView1.MultiSelect = false;
|
virtualizedImageList1.ViewModel = null;
|
||||||
listView1.Name = "listView1";
|
|
||||||
listView1.Size = new Size(876, 171);
|
|
||||||
listView1.TabIndex = 1;
|
|
||||||
listView1.UseCompatibleStateImageBehavior = false;
|
|
||||||
//
|
//
|
||||||
// ImageStripPanel
|
// ImageStripPanel
|
||||||
//
|
//
|
||||||
AutoScaleDimensions = new SizeF(7F, 15F);
|
AutoScaleDimensions = new SizeF(7F, 15F);
|
||||||
AutoScaleMode = AutoScaleMode.Font;
|
AutoScaleMode = AutoScaleMode.Font;
|
||||||
ClientSize = new Size(876, 197);
|
ClientSize = new Size(876, 197);
|
||||||
Controls.Add(listView1);
|
Controls.Add(virtualizedImageList1);
|
||||||
Controls.Add(toolStrip1);
|
Controls.Add(toolStrip1);
|
||||||
MaximizeBox = false;
|
MaximizeBox = false;
|
||||||
MinimizeBox = false;
|
MinimizeBox = false;
|
||||||
@@ -212,12 +208,12 @@
|
|||||||
private ToolStripButton btnToEnd;
|
private ToolStripButton btnToEnd;
|
||||||
private ToolStripSeparator toolStripSeparator1;
|
private ToolStripSeparator toolStripSeparator1;
|
||||||
private ToolStripButton cbRepeat;
|
private ToolStripButton cbRepeat;
|
||||||
private ListView listView1;
|
|
||||||
private ToolStripLabel toolStripLabel1;
|
private ToolStripLabel toolStripLabel1;
|
||||||
private ToolStripNumberControl ncInterval;
|
private ToolStripNumberControl ncInterval;
|
||||||
private ToolStripButton ckbExecuteWorkflow;
|
private ToolStripButton ckbExecuteWorkflow;
|
||||||
private ToolStripLabel lblSelectedImage;
|
private ToolStripLabel lblSelectedImage;
|
||||||
private ToolStripButton ckbStopOnError;
|
private ToolStripButton ckbStopOnError;
|
||||||
private ToolStripButton ckbStopOnNoError;
|
private ToolStripButton ckbStopOnNoError;
|
||||||
|
private Components.VirtualizedImageList virtualizedImageList1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Drawing.Drawing2D;
|
using System.Drawing.Drawing2D;
|
||||||
|
using Hawkeye.VisionBuilder.Workflow.Datatypes;
|
||||||
using OpenCvSharp;
|
using OpenCvSharp;
|
||||||
using OpenCvSharp.Extensions;
|
using OpenCvSharp.Extensions;
|
||||||
using VisionBuilder.UI.Common.Processing;
|
using VisionBuilder.UI.Common.Processing;
|
||||||
@@ -10,33 +11,49 @@ namespace Hawkeye.VisionBuilder.Features.ImagesStrip
|
|||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
|
public ImagesViewModel ImagesViewModel { get; set; }
|
||||||
|
|
||||||
public ImageStripPanel()
|
public ImageStripPanel()
|
||||||
{
|
{
|
||||||
|
|
||||||
InitializeComponent();
|
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 = $"{this.ImagesViewModel.CurrentIndex + 1}/{ImagesViewModel.TotalCount}";
|
||||||
{
|
ImageSelected(new HawkeyeImage() { Filename = ImagesViewModel.CurrentImagePath, ImageData = new Mat(ImagesViewModel.CurrentImagePath) });
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//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
|
public bool IsExecutingWorkflow
|
||||||
{
|
{
|
||||||
get => ckbExecuteWorkflow.Checked;
|
get => ckbExecuteWorkflow.Checked;
|
||||||
@@ -44,145 +61,53 @@ namespace Hawkeye.VisionBuilder.Features.ImagesStrip
|
|||||||
}
|
}
|
||||||
|
|
||||||
public event Action<Workflow.Datatypes.HawkeyeImage> ImageSelected = delegate { };
|
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)
|
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));
|
//listView1.BeginUpdate();
|
||||||
}
|
ImagesViewModel.LoadFromDirectory(directory);
|
||||||
var allImages = Directory.GetFiles(directory, "*.bmp", SearchOption.AllDirectories)
|
//listView1.EndUpdate();
|
||||||
.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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void btnToStart_Click(object sender, EventArgs e)
|
private void btnToStart_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
listView1.SelectedIndices.Clear();
|
ImagesViewModel.First();
|
||||||
listView1.SelectedIndices.Add(0);
|
|
||||||
listView1.EnsureVisible(listView1.SelectedIndices[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Prev()
|
public void Prev()
|
||||||
{
|
{
|
||||||
btnPrev_Click(null, null);
|
ImagesViewModel.Previous();
|
||||||
|
|
||||||
}
|
}
|
||||||
private void btnPrev_Click(object sender, EventArgs e)
|
private void btnPrev_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (listView1.SelectedIndices.Count == 0) return;
|
Prev();
|
||||||
var index = listView1.SelectedIndices[0];
|
|
||||||
index--;
|
|
||||||
if (index < 0) index++;
|
|
||||||
listView1.SelectedIndices.Clear();
|
|
||||||
listView1.SelectedIndices.Add(index);
|
|
||||||
listView1.EnsureVisible(listView1.SelectedIndices[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Next()
|
public void Next()
|
||||||
{
|
{
|
||||||
btnNext_Click(null, null);
|
ImagesViewModel.Next();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void btnNext_Click(object sender, EventArgs e)
|
private void btnNext_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (listView1.SelectedIndices.Count == 0) return;
|
Next();
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void btnToEnd_Click(object sender, EventArgs e)
|
private void btnToEnd_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
listView1.SelectedIndices.Clear();
|
ImagesViewModel.Last();
|
||||||
listView1.SelectedIndices.Add(listView1.Items.Count - 1);
|
|
||||||
listView1.EnsureVisible(listView1.SelectedIndices[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void btnPlay_Click(object sender, EventArgs e)
|
private void btnPlay_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
Task.Run(Playback);
|
Task.Run(Playback);
|
||||||
@@ -194,14 +119,17 @@ namespace Hawkeye.VisionBuilder.Features.ImagesStrip
|
|||||||
{
|
{
|
||||||
Action a = () =>
|
Action a = () =>
|
||||||
{
|
{
|
||||||
if (listView1.SelectedIndices.Count == 0) return;
|
if (ImagesViewModel.TotalCount == 0)
|
||||||
var index = listView1.SelectedIndices[0];
|
{
|
||||||
index++;
|
btnPlay.Checked = false;
|
||||||
if (index > listView1.Items.Count - 1)
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImagesViewModel.CurrentIndex > ImagesViewModel.TotalCount - 1)
|
||||||
{
|
{
|
||||||
if (cbRepeat.Checked)
|
if (cbRepeat.Checked)
|
||||||
{
|
{
|
||||||
index = 0;
|
ImagesViewModel.First();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -210,19 +138,14 @@ namespace Hawkeye.VisionBuilder.Features.ImagesStrip
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
listView1.SelectedIndices.Clear();
|
Next();
|
||||||
listView1.SelectedIndices.Add(index);
|
|
||||||
listView1.EnsureVisible(listView1.SelectedIndices[0]);
|
|
||||||
};
|
};
|
||||||
listView1.Invoke(a);
|
virtualizedImageList1.Invoke(a);
|
||||||
|
virtualizedImageList1.Invalidate();
|
||||||
await Task.Delay(int.Parse((string)ncInterval.Text));
|
await Task.Delay(int.Parse((string)ncInterval.Text));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetName()
|
|
||||||
{
|
|
||||||
return "ImageStripPanel";
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Mat> GetImage(CancellationToken token)
|
public Task<Mat> GetImage(CancellationToken token)
|
||||||
{
|
{
|
||||||
@@ -231,19 +154,13 @@ namespace Hawkeye.VisionBuilder.Features.ImagesStrip
|
|||||||
|
|
||||||
public Mat GetImage()
|
public Mat GetImage()
|
||||||
{
|
{
|
||||||
if (listView1.SelectedIndices.Count == 0) return null;
|
|
||||||
|
|
||||||
var path = listView1.SelectedItems[0].ImageKey;
|
return new Mat(ImagesViewModel.CurrentImagePath);
|
||||||
|
|
||||||
return new Mat(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetImagePath()
|
public string GetImagePath()
|
||||||
{
|
{
|
||||||
if (listView1.SelectedIndices.Count == 0) return null;
|
return ImagesViewModel.CurrentImagePath;
|
||||||
var path = listView1.SelectedItems[0].Tag as string;
|
|
||||||
if (path == null) return null;
|
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool StopOnErrors
|
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)
|
public void SelectImage(string nextFile)
|
||||||
{
|
{
|
||||||
var index = listView1.Items.Cast<ListViewItem>().Where(x => (string)x.Tag == nextFile).Select(x => x.Index).FirstOrDefault();
|
ImagesViewModel.SelectByPath(nextFile);
|
||||||
if (index == -1) return;
|
|
||||||
listView1.SelectedIndices.Clear();
|
|
||||||
listView1.SelectedIndices.Add(index);
|
|
||||||
listView1.EnsureVisible(listView1.SelectedIndices[0]);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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