From e5eaa88e51123a867eb2171002b3dbd9e164ce7d Mon Sep 17 00:00:00 2001
From: meelstorm <8780022+TesAnti@users.noreply.github.com>
Date: Fri, 14 Nov 2025 10:26:43 +0100
Subject: [PATCH] sliding window for vision builder image strip pannel
---
.../Components/ImageThumbnailControl.cs | 150 ++++++
.../VirtualizedImageList.Designer.cs | 47 ++
.../Components/VirtualizedImageList.cs | 272 +++++++++++
.../Components/VirtualizedImageList.resx | 120 +++++
.../ImagesStrip/ImageStripPanel.Designer.cs | 24 +-
.../Features/ImagesStrip/ImageStripPanel.cs | 244 +++-------
.../Features/ImagesStrip/ImagesViewModel.cs | 426 ++++++++++++++++++
7 files changed, 1090 insertions(+), 193 deletions(-)
create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/Components/ImageThumbnailControl.cs
create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.Designer.cs
create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.cs
create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.resx
create mode 100644 Hawkeye.VisionBuilder/Features/ImagesStrip/ImagesViewModel.cs
diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/Components/ImageThumbnailControl.cs b/Hawkeye.VisionBuilder/Features/ImagesStrip/Components/ImageThumbnailControl.cs
new file mode 100644
index 0000000..c70b509
--- /dev/null
+++ b/Hawkeye.VisionBuilder/Features/ImagesStrip/Components/ImageThumbnailControl.cs
@@ -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
+{
+ ///
+ /// Represents a single image thumbnail item with an image and filename label.
+ ///
+ 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);
+ }
+ }
+}
diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.Designer.cs b/Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.Designer.cs
new file mode 100644
index 0000000..e2f4b2c
--- /dev/null
+++ b/Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.Designer.cs
@@ -0,0 +1,47 @@
+namespace Hawkeye.VisionBuilder.Features.ImagesStrip.Components
+{
+ partial class VirtualizedImageList
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ 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;
+ }
+}
diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.cs b/Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.cs
new file mode 100644
index 0000000..251283a
--- /dev/null
+++ b/Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.cs
@@ -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
+{
+ ///
+ /// A virtualized image list control that efficiently displays image thumbnails
+ /// by loading only the images in the current window (managed by ImagesViewModel).
+ ///
+ public partial class VirtualizedImageList : UserControl
+ {
+ private ImagesViewModel _viewModel;
+ private Dictionary _thumbnailControls = new Dictionary();
+ private bool _isUpdating = false;
+
+ ///
+ /// Gets or sets the ImagesViewModel that manages the image data and windowing logic.
+ ///
+ 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();
+ }
+ }
+ }
+
+ ///
+ /// Event raised when a thumbnail is clicked by the user.
+ ///
+ public event EventHandler 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);
+ }
+ }
+}
diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.resx b/Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.resx
new file mode 100644
index 0000000..8b2ff64
--- /dev/null
+++ b/Hawkeye.VisionBuilder/Features/ImagesStrip/Components/VirtualizedImageList.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.Designer.cs b/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.Designer.cs
index dfb626c..0438739 100644
--- a/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.Designer.cs
+++ b/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.Designer.cs
@@ -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;
}
}
diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.cs b/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.cs
index 534c71a..e86019f 100644
--- a/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.cs
+++ b/Hawkeye.VisionBuilder/Features/ImagesStrip/ImageStripPanel.cs
@@ -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 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 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 GetFileList()
- {
- return listView1.Items.Cast().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().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);
}
}
diff --git a/Hawkeye.VisionBuilder/Features/ImagesStrip/ImagesViewModel.cs b/Hawkeye.VisionBuilder/Features/ImagesStrip/ImagesViewModel.cs
new file mode 100644
index 0000000..cfbb539
--- /dev/null
+++ b/Hawkeye.VisionBuilder/Features/ImagesStrip/ImagesViewModel.cs
@@ -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 _allImagePaths = new();
+ private int _currentIndex = -1;
+ private int _bufferSize;
+ private HashSet _currentLoadedIndices = new();
+
+ public ObservableCollection 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? ImagesAdded;
+ public event EventHandler? ImagesRemoved;
+ public event EventHandler? ImagesCleared;
+ public event EventHandler? CurrentImageChanged;
+ public event EventHandler? 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);
+ }
+
+ ///
+ /// Loads all image files from the specified directory path.
+ ///
+ /// The directory path containing images
+ /// Optional search pattern (default: *.* for all files)
+ /// Optional list of supported extensions (e.g., .jpg, .png, .bmp)
+ 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));
+ }
+
+ ///
+ /// Navigate to the first image.
+ ///
+ 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;
+ }
+
+ ///
+ /// Navigate to the previous image.
+ ///
+ public bool Previous()
+ {
+ if (!CanGoPrevious)
+ return false;
+
+ var oldIndex = _currentIndex;
+ _currentIndex--;
+ LoadWindow();
+ OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath));
+
+ return true;
+ }
+
+ ///
+ /// Navigate to the next image.
+ ///
+ public bool Next()
+ {
+ if (!CanGoNext)
+ return false;
+
+ var oldIndex = _currentIndex;
+ _currentIndex++;
+ LoadWindow();
+ OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath));
+
+ return true;
+ }
+
+ ///
+ /// Navigate to the last image.
+ ///
+ 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;
+ }
+
+ ///
+ /// Navigate to the Nth image (0-based index).
+ ///
+ 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);
+ }
+
+ ///
+ /// Clears all loaded images and resets the state.
+ ///
+ 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();
+ }
+
+ ///
+ /// Refreshes the current window by reloading images from disk.
+ ///
+ public void RefreshWindow()
+ {
+ LoadWindow();
+ OnWindowRefreshed();
+ }
+
+ ///
+ /// Reloads the directory, useful when files have been added or removed.
+ ///
+ 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();
+ 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;
+ }
+}
\ No newline at end of file