feat: SampleCalibration View and ViewModel

This commit is contained in:
2026-04-14 22:30:26 +03:00
parent 106bc74cbb
commit 93bcc6c692
11 changed files with 1543 additions and 1310 deletions
@@ -1,4 +1,4 @@
// Оригинальное решение для проблеммы сворачивания колнок в Grid от
// Оригинальное решение для проблемы сворачивания колонок в Grid от
// https://github.com/AvaloniaUI/Avalonia/discussions/6773#discussioncomment-1514604
using Avalonia;
@@ -39,4 +39,4 @@ public static class GridColumnHideBehavior
{
columnDefinition.SetValue(IsVisibleProperty, visibility);
}
}
}
@@ -0,0 +1,47 @@
using System.Globalization;
using Avalonia.Data.Converters;
namespace GSS2.UI.Core.Converters;
public class DateTimeConverter : IValueConverter
{
private static readonly string[][] FORMATS =
{
["dd", "d"],
["."],
["MM", "M"],
["."],
["yyyy", "yy"],
[" "],
["HH", "H"],
[":"],
["mm", "m"],
[":"],
["ss", "s"]
};
private static readonly string[] ALL_FORMATS = FORMATS
.Aggregate(new List<string> { "" }, (acc, segments) =>
acc.SelectMany(a => segments.Select(s => a + s)).ToList())
.ToArray();
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not DateTime dateTime)
return Avalonia.Data.BindingNotification.ExtractError(new FormatException($"Не удаётся распознать значение \"{value}\" как дату и время"));
return dateTime.ToLocalTime().ToString("dd.MM.yyyy HH:mm:ss");
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not string str)
return Avalonia.Data.BindingNotification.ExtractError(new FormatException($"Не удаётся распознать значение \"{value}\" как дату и время"));
var success = DateTime.TryParseExact(str, ALL_FORMATS, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var dateTime);
if (!success)
return Avalonia.Data.BindingNotification.ExtractError(new FormatException($"Не удаётся распознать строку \"{str}\" как дату и время"));
return dateTime.ToUniversalTime();
}
}
-5
View File
@@ -1,14 +1,9 @@
using Avalonia;
using Avalonia.Media;
using Avalonia.Input;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Mat = OpenCvSharp.Mat;
using Cv2 = OpenCvSharp.Cv2;
using GSS2.Core.Extentions;
namespace GSS2.UI.Core;
public class OpenCvImage : ZoomPanImage
@@ -1,343 +0,0 @@
using System.Collections.Specialized;
using System.Collections.ObjectModel;
using Microsoft.Extensions.Logging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using GSS2.Core.Hardware;
using GSS2.Core.Extentions;
using GSS2.Core.Analysis.AmineContent.Database.Calibration;
namespace GSS2.UI.Core.ViewModels;
public partial class AmineContentCalibrationViewModel : ViewModelBase
{
private readonly ILogger<AmineContentCalibrationViewModel> _logger;
private readonly AmineContentCalibrationContext _context;
private readonly IlluminatorService _illuminatorService;
private readonly CameraService _cameraService;
private readonly ImageStorageService _imageStorageService;
private CancellationTokenSource? _captureImagesTaskCts = null;
[ObservableProperty] private ObservableCollection<CalibrationRecord> _records;
[ObservableProperty] private CalibrationRecord _currentRecord;
[ObservableProperty] private bool _isMenuOpen = false;
[ObservableProperty] private bool _isNewRecord = false;
[ObservableProperty] private int _mainSectionsCurrentIndex = 0;
[ObservableProperty] private string _switchMainSectionButtonText = "К изображениям";
[ObservableProperty] private int _imageTypeSectionCurrentIndex = 0;
[ObservableProperty] private string _switchImageTypeSectionButtonText = "К изображениям пробы";
[ObservableProperty] private Task? _captureImagesTask = null;
[ObservableProperty] private ObservableCollection<KeyValuePair<IImage, ImageRecord>> _separatorImages = new();
[ObservableProperty] private bool _separatorImagesIsCapturing = false;
[ObservableProperty] private string _separatorImagesCapturingProgress = "";
[ObservableProperty] private int _separatorImagesSectionCurrentIndex = 0;
[ObservableProperty] private bool _separatorImagesSectionCanScrollForward = false;
[ObservableProperty] private bool _separatorImagesSectionCanScrollBackward = false;
[ObservableProperty] private ObservableCollection<KeyValuePair<IImage, ImageRecord>> _sampleImages = new();
[ObservableProperty] private bool _sampleImagesIsCapturing = false;
[ObservableProperty] private string _sampleImagesCapturingProgress = "";
[ObservableProperty] private int _sampleImagesSectionCurrentIndex = 0;
[ObservableProperty] private bool _sampleImagesSectionCanScrollForward = false;
[ObservableProperty] private bool _sampleImagesSectionCanScrollBackward = false;
public AmineContentCalibrationViewModel(ILogger<AmineContentCalibrationViewModel> logger, AmineContentCalibrationContext context, IlluminatorService illuminatorService, CameraService cameraService, ImageStorageService imageStorageService)
{
_logger = logger;
_logger.LogInformation("Initialization");
_context = context;
_illuminatorService = illuminatorService;
_cameraService = cameraService;
_imageStorageService = imageStorageService;
Records = new ObservableCollection<CalibrationRecord>(_context.CalibrationRecords);
if (Records.Count() > 0)
CurrentRecord = Records.First();
else
{
CurrentRecord = new CalibrationRecord()
{
SampleName = null,
SampleComment = null,
SampleBrand = null,
SampleMass = null,
SamplingDateTime = null,
SamplingPlace = null,
MixtureName = null,
MixtureAmineContent = null,
MixtureNormalRate = null,
MixtureActualRate = null,
SampleImages = new List<ImageRecord>(),
SeparatorImages = new List<ImageRecord>()
};
IsNewRecord = true;
}
_logger.LogInformation("Initialized");
}
partial void OnRecordsChanged(ObservableCollection<CalibrationRecord>? oldValue, ObservableCollection<CalibrationRecord> newValue)
{
oldValue?.CollectionChanged -= OnRecordsCollectionChanged;
newValue?.CollectionChanged += OnRecordsCollectionChanged;
}
partial void OnCurrentRecordChanged(CalibrationRecord value)
{
SeparatorImages = new(
value.SeparatorImages
.Select(i => new KeyValuePair<string, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "calibration"), i))
.Select(kv => new KeyValuePair<IImage, ImageRecord>(new Bitmap(kv.Key), kv.Value))
);
SampleImages = new(
value.SampleImages
.Select(i => new KeyValuePair<string, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "calibration"), i))
.Select(kv => new KeyValuePair<IImage, ImageRecord>(new Bitmap(kv.Key), kv.Value))
);
}
private void OnRecordsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs ea)
{
Console.WriteLine($"RecordsCollectionChanged {ea.Action}");
}
[RelayCommand] private void ToggleMenu() => IsMenuOpen = !IsMenuOpen;
[RelayCommand] private void CloseMenu() => IsMenuOpen = false;
[RelayCommand]
private void CreateRecord()
{
var record = new CalibrationRecord()
{
SampleName = null,
SampleComment = null,
SampleBrand = null,
SampleMass = null,
SamplingDateTime = null,
SamplingPlace = null,
MixtureName = null,
MixtureAmineContent = null,
MixtureNormalRate = null,
MixtureActualRate = null,
SampleImages = new List<ImageRecord>(),
SeparatorImages = new List<ImageRecord>()
};
CurrentRecord = record;
IsNewRecord = true;
}
// [RelayCommand]
// private void DeleteRecord()
// {
// //TODO
// // if (Record == null) return;
// // _context.CalibrationRecords.Remove(Record);
// // Records.Remove(Record);
// // if (Records.Count() > 0)
// // Record = Records.First();
// // else
// // {
// // var record = new CalibrationRecord();
// // _context.CalibrationRecords.Add(record);
// // Records.Add(record);
// // Record = record;
// // }
// IsNewRecord = true;
// CreateRecord();
// }
// [RelayCommand]
// private void SaveRecord()
// {
// //TODO
// // _context.SaveChanges();
// IsNewRecord = false;
// }
[RelayCommand]
private void SwitchMainSection()
{
MainSectionsCurrentIndex = MainSectionsCurrentIndex == 0 ? 1 : 0;
SwitchMainSectionButtonText = MainSectionsCurrentIndex == 0 ?
"К изображениям" :
"К общей информации";
}
[RelayCommand]
private void SwitchImageTypeSection()
{
ImageTypeSectionCurrentIndex = ImageTypeSectionCurrentIndex == 0 ? 1 : 0;
SwitchImageTypeSectionButtonText = ImageTypeSectionCurrentIndex == 0 ?
"К изображениям пробы" :
"К изображениям сепаратора";
}
[RelayCommand]
private void CaptureImage()
{
if (CaptureImagesTask is not null)
return;
_captureImagesTaskCts = new CancellationTokenSource();
var cancellationToken = _captureImagesTaskCts.Token;
var imageTypeSectionCurrentIndex = ImageTypeSectionCurrentIndex;
CaptureImagesTask = new Task(async () =>
{
_illuminatorService.TurnOn();
(double visible, double uv365, double uv254)[] intensities =
{
(0.02500, 0.0, 0.0),
(0.01250, 0.0, 0.0),
(0.00625, 0.0, 0.0),
(0.00000, 1.0, 0.0),
(0.00000, 0.0, 1.0),
(0.00000, 1.0, 1.0),
(0.01250, 1.0, 0.0),
(0.01250, 0.0, 1.0),
(0.01250, 1.0, 1.0),
(0.00625, 1.0, 0.0),
(0.00625, 0.0, 1.0),
(0.00625, 1.0, 1.0)
};
Dispatcher.UIThread.Invoke(() =>
{
if (imageTypeSectionCurrentIndex == 0)
SeparatorImagesIsCapturing = true;
else
SampleImagesIsCapturing = true;
});
try
{
var imageRecords = new List<ImageRecord>();
foreach (var (i, intensity) in intensities.Enumerate())
{
cancellationToken.ThrowIfCancellationRequested();
Dispatcher.UIThread.Invoke(() =>
{
if (imageTypeSectionCurrentIndex == 0)
SeparatorImagesCapturingProgress = $"Получение изображений: {i + 1} / {intensities.Count()}";
else
SampleImagesCapturingProgress = $"Получение изображений: {i + 1} / {intensities.Count()}";
});
_illuminatorService.SetIntensity(intensity.visible, intensity.uv365, intensity.uv254);
OpenCvSharp.Mat? image;
image = await _cameraService.CaptureImage(cancellationToken, 5);
if (image is null)
{
_logger.LogError("Cannot get image");
return;
}
var bytes = image.ImEncode(".png");
string fileName;
using (var stream = new MemoryStream(bytes))
fileName = await _imageStorageService.SaveAsync(stream, ".png", "calibration", cancellationToken);
image.Dispose();
image = null;
imageRecords.Add(
new ImageRecord()
{
ImagePath = fileName,
VisibleIntensity = intensity.visible,
Uv365Intensity = intensity.uv365,
Uv254Intensity = intensity.uv254,
}
);
}
_illuminatorService.TurnOff();
Dispatcher.UIThread.Invoke(() =>
{
if (imageTypeSectionCurrentIndex == 0)
{
CurrentRecord.SeparatorImages.Clear();
CurrentRecord.SeparatorImages.AddRange(imageRecords);
SeparatorImages.Clear();
imageRecords
.Select(i => new KeyValuePair<string, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "calibration"), i))
.Select(kv => new KeyValuePair<IImage, ImageRecord>(new Bitmap(kv.Key), kv.Value))
.ToList()
.ForEach(SeparatorImages.Add);
}
else
{
CurrentRecord.SampleImages.Clear();
CurrentRecord.SampleImages.AddRange(imageRecords);
SampleImages.Clear();
imageRecords
.Select(i => new KeyValuePair<string, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "calibration"), i))
.Select(kv => new KeyValuePair<IImage, ImageRecord>(new Bitmap(kv.Key), kv.Value))
.ToList()
.ForEach(SampleImages.Add);
}
});
}
catch (TaskCanceledException)
{ }
catch (Exception ex)
{
_logger.LogError(ex, "Exception while image capturing");
}
finally
{
_illuminatorService.TurnOff();
_captureImagesTaskCts = null;
Dispatcher.UIThread.Invoke(() =>
{
if (imageTypeSectionCurrentIndex == 0)
{
SeparatorImagesIsCapturing = false;
SeparatorImagesCapturingProgress = "";
}
else
{
SampleImagesIsCapturing = false;
SampleImagesCapturingProgress = "";
}
CaptureImagesTask = null;
});
}
}, cancellationToken);
CaptureImagesTask.Start();
}
[RelayCommand] private void CencelImageCapturing() => _captureImagesTaskCts?.Cancel();
[RelayCommand] private void SeparatorImagesSectionPrevious() => SeparatorImagesSectionCurrentIndex--;
[RelayCommand] private void SeparatorImagesSectionNext() => SeparatorImagesSectionCurrentIndex++;
[RelayCommand] private void SampleImagesSectionPrevious() => SampleImagesSectionCurrentIndex--;
[RelayCommand] private void SampleImagesSectionNext() => SampleImagesSectionCurrentIndex++;
// [RelayCommand]
// private void SelectImagesFromAnotherRecord()
// {
// }
}
@@ -1,345 +0,0 @@
using System.Collections.Specialized;
using System.Collections.ObjectModel;
using Microsoft.Extensions.Logging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using GSS2.Core.Hardware;
using GSS2.Core.Extensions;
using GSS2.Core.Analysis.AmineContent.Database.Calibration;
using Microsoft.EntityFrameworkCore;
namespace GSS2.UI.Core.ViewModels;
public partial class AmineContentSeparatorCalibrationViewModel : ViewModelBase
{
private readonly ILogger<AmineContentSeparatorCalibrationViewModel> _logger;
private readonly AmineContentCalibrationContext _context;
private readonly IlluminatorService _illuminatorService;
private readonly CameraService _cameraService;
private readonly ImageStorageService _imageStorageService;
private CancellationTokenSource? _captureImagesTaskCts = null;
[ObservableProperty] private ObservableCollection<SeparatorRecord> _records;
[ObservableProperty] private SeparatorRecord? _selectedRecord;
[ObservableProperty] private SeparatorRecord _editingRecord;
[ObservableProperty] private bool _isMenuOpen = false;
[ObservableProperty] private int _mainSectionsCurrentIndex = 0;
[ObservableProperty] private bool _mainSectionCanScrollBackward = false;
[ObservableProperty] private bool _mainSectionCanScrollForward = true;
[ObservableProperty] private bool _mainSectionIsScrolling = true;
[ObservableProperty] private Task? _captureImagesTask = null;
[ObservableProperty] private ObservableCollection<AsyncImageRecordViewModel> _images = new();
[ObservableProperty] private bool _imagesIsCapturing = false;
[ObservableProperty] private string _imagesCapturingProgress = "";
[ObservableProperty] private int _imagesSectionCurrentIndex = 0;
[ObservableProperty] private bool _imagesSectionCanScrollForward = false;
[ObservableProperty] private bool _imagesSectionCanScrollBackward = false;
[ObservableProperty] private bool _imagesSectionIsScrolling = true;
public AmineContentSeparatorCalibrationViewModel(ILogger<AmineContentSeparatorCalibrationViewModel> logger, AmineContentCalibrationContext context, IlluminatorService illuminatorService, CameraService cameraService, ImageStorageService imageStorageService)
{
_logger = logger;
_logger.LogInformation("Initialization");
_context = context;
_illuminatorService = illuminatorService;
_cameraService = cameraService;
_imageStorageService = imageStorageService;
Records = new ObservableCollection<SeparatorRecord>(_context.SeparatorRecords.Include(r => r.Images));
SelectedRecord = Records.FirstOrDefault();
if (SelectedRecord is null)
EditingRecord = new()
{
Type = "",
Batch = "",
Images = new()
};
else
EditingRecord = CopyRecord(SelectedRecord);
_logger.LogInformation("Initialized");
}
partial void OnSelectedRecordChanged(SeparatorRecord? value)
{
if (SelectedRecord is null)
EditingRecord = new()
{
Type = "",
Batch = "",
Images = new()
};
else
EditingRecord = CopyRecord(SelectedRecord);
}
partial void OnEditingRecordChanged(SeparatorRecord value)
{
var images = value.Images
.Select(i =>
{
var path = _imageStorageService.GetFullPath(i.ImagePath, "separator");
if (path is null)
return null;
return new AsyncImageRecordViewModel(path, i);
})
.OfType<AsyncImageRecordViewModel>();
Images = new(images);
}
[RelayCommand] private void ToggleMenu() => IsMenuOpen = !IsMenuOpen;
[RelayCommand] private void CloseMenu() => IsMenuOpen = false;
[RelayCommand] private void CreateRecord() => SelectedRecord = null;
[RelayCommand]
private void DeleteRecord()
{
var deletedRecord = SelectedRecord;
CreateRecord();
if (deletedRecord is not null)
{
Records.Remove(deletedRecord);
_context.SeparatorRecords.Remove(deletedRecord);
_context.SaveChanges();
}
}
[RelayCommand]
private void SaveRecord()
{
if (SelectedRecord is null)
{
var newEntity = CopyRecord(EditingRecord);
for (int i = 0; i < newEntity.Images.Count(); i++)
{
var image = newEntity.Images[i];
if (_context.Entry(image).State is not EntityState.Detached)
continue;
var attachedImage = _context.ImageRecords.Add(image);
newEntity.Images.RemoveAt(i);
newEntity.Images.Insert(i, attachedImage.Entity);
}
if (_context.Entry(newEntity).State is EntityState.Detached)
{
var attachedRecord = _context.SeparatorRecords.Add(newEntity);
newEntity = attachedRecord.Entity;
}
_context.SaveChanges();
Records.Add(newEntity);
SelectedRecord = newEntity;
}
else
{
SelectedRecord.Type = EditingRecord.Type;
SelectedRecord.Batch = EditingRecord.Batch;
var imagesToRemove = SelectedRecord.Images
.Where(e => EditingRecord.Images.All(i => i.Id != e.Id))
.ToList();
foreach (var image in imagesToRemove)
SelectedRecord.Images.Remove(image);
foreach (var editedImage in EditingRecord.Images)
{
var existing = EditingRecord.Images
.FirstOrDefault(i => i.Id == editedImage.Id);
if (existing is null)
{
SelectedRecord.Images.Add(
new ImageRecord
{
ImagePath = editedImage.ImagePath,
VisibleIntensity = editedImage.VisibleIntensity,
Uv365Intensity = editedImage.Uv365Intensity,
Uv254Intensity = editedImage.Uv254Intensity
}
);
}
else
{
existing.ImagePath = editedImage.ImagePath;
existing.VisibleIntensity = editedImage.VisibleIntensity;
existing.Uv365Intensity = editedImage.Uv365Intensity;
existing.Uv254Intensity = editedImage.Uv254Intensity;
}
}
for (int i = 0; i < SelectedRecord.Images.Count(); i++)
{
var image = SelectedRecord.Images[i];
if (_context.Entry(image).State is not EntityState.Detached)
continue;
var attachedImage = _context.ImageRecords.Add(image);
SelectedRecord.Images.RemoveAt(i);
SelectedRecord.Images.Insert(i, attachedImage.Entity);
}
if (_context.Entry(SelectedRecord).State is EntityState.Detached)
{
var attachedRecord = _context.SeparatorRecords.Add(SelectedRecord);
var index = Records.IndexOf(SelectedRecord);
Records[index] = attachedRecord.Entity;
SelectedRecord = attachedRecord.Entity;
}
_context.SaveChanges();
}
}
[RelayCommand] private void SwitchMainSection() => MainSectionsCurrentIndex = MainSectionsCurrentIndex == 0 ? 1 : 0;
[RelayCommand]
private void CaptureImage()
{
if (CaptureImagesTask is not null)
return;
_captureImagesTaskCts = new CancellationTokenSource();
var cancellationToken = _captureImagesTaskCts.Token;
CaptureImagesTask = Task.Run(async () =>
{
_illuminatorService.TurnOn();
(double visible, double uv365, double uv254)[] intensities =
{
(0.02500, 0.0, 0.0),
(0.01250, 0.0, 0.0),
(0.00625, 0.0, 0.0),
(0.00000, 1.0, 0.0),
(0.00000, 0.0, 1.0),
(0.00000, 1.0, 1.0),
(0.01250, 1.0, 0.0),
(0.01250, 0.0, 1.0),
(0.01250, 1.0, 1.0),
(0.00625, 1.0, 0.0),
(0.00625, 0.0, 1.0),
(0.00625, 1.0, 1.0)
};
Dispatcher.UIThread.Invoke(() => ImagesIsCapturing = true);
try
{
var imageRecords = new List<ImageRecord>();
foreach (var (i, intensity) in intensities.Enumerate())
{
_logger.LogInformation("Capturing images: {} / {} ({};{};{})", i + 1, intensities.Count(), intensity.visible, intensity.uv365, intensity.uv254);
cancellationToken.ThrowIfCancellationRequested();
Dispatcher.UIThread.Invoke(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {intensities.Count()}");
_illuminatorService.SetIntensity(intensity.visible, intensity.uv365, intensity.uv254);
OpenCvSharp.Mat? image;
image = await _cameraService.CaptureImage(cancellationToken, 5);
if (image is null)
{
_logger.LogError("Cannot get image");
return;
}
var bytes = image.ImEncode(".png");
string fileName;
using (var stream = new MemoryStream(bytes))
fileName = await _imageStorageService.SaveAsync(stream, ".png", "separator", cancellationToken);
image.Dispose();
image = null;
imageRecords.Add(
new ImageRecord()
{
ImagePath = fileName,
VisibleIntensity = intensity.visible,
Uv365Intensity = intensity.uv365,
Uv254Intensity = intensity.uv254,
}
);
}
_illuminatorService.TurnOff();
var images = imageRecords
.Select(i =>
{
var path = _imageStorageService.GetFullPath(i.ImagePath, "separator");
if (path is null)
return null;
return new AsyncImageRecordViewModel(path, i);
})
.OfType<AsyncImageRecordViewModel>();
Dispatcher.UIThread.Invoke(() =>
{
EditingRecord.Images.Clear();
EditingRecord.Images.AddRange(imageRecords);
Images.Clear();
Images = new(images);
});
}
catch (TaskCanceledException)
{ }
catch (Exception ex)
{
_logger.LogError(ex, "Exception while image capturing");
}
finally
{
_illuminatorService.TurnOff();
_captureImagesTaskCts = null;
Dispatcher.UIThread.Invoke(() =>
{
ImagesIsCapturing = false;
ImagesCapturingProgress = "";
CaptureImagesTask = null;
});
}
}, cancellationToken);
}
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
[RelayCommand] private void ImagesSectionPrevious() => ImagesSectionCurrentIndex--;
[RelayCommand] private void ImagesSectionNext() => ImagesSectionCurrentIndex++;
private static SeparatorRecord CopyRecord(SeparatorRecord source)
{
return new SeparatorRecord
{
Id = source.Id,
Type = source.Type,
Batch = source.Batch,
Images = source.Images
.Select(i => new ImageRecord
{
Id = i.Id,
ImagePath = i.ImagePath,
VisibleIntensity = i.VisibleIntensity,
Uv365Intensity = i.Uv365Intensity,
Uv254Intensity = i.Uv254Intensity
})
.ToList()
};
}
}
@@ -39,8 +39,9 @@ public partial class AsyncImageRecordViewModel : ViewModelBase
IsLoading = false;
});
}
catch
catch (Exception ex)
{
Console.Error.WriteLine($"Ошибка во время загрузки изображения:\n{ex.Message}\n{ex.StackTrace}");
await Dispatcher.UIThread.InvokeAsync(() => IsLoading = false);
}
finally
@@ -1,598 +0,0 @@
<UserControl
x:Class="GSS2.UI.Core.Views.AmineContentSeparatorCalibrationView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia_converters="using:Avalonia.Data.Converters"
xmlns:converters="using:GSS2.UI.Core.Converters"
xmlns:core="using:GSS2.UI.Core"
xmlns:i="using:Avalonia.Xaml.Interactivity"
xmlns:ia="using:Avalonia.Xaml.Interactions.Custom"
xmlns:local="using:GSS2.UI.Core.Views"
xmlns:vm="using:GSS2.UI.Core.ViewModels"
x:DataType="vm:AmineContentSeparatorCalibrationViewModel">
<!-- #001e46 -->
<!-- #00357a -->
<!-- #0865b2 -->
<!-- #4182d2 -->
<!-- #67a0f3 -->
<!-- #8abfff -->
<!-- #acdfff -->
<UserControl.Resources>
<converters:NotEqualsConverter x:Key="NotEqualsConverter" />
<converters:EqualsConverter x:Key="EqualsConverter" />
<SolidColorBrush x:Key="HeaderBackground" Color="#00357a" />
<SolidColorBrush x:Key="MenuBackground" Color="#00357a" />
<SolidColorBrush x:Key="MenuBorder" Color="#0865b2" />
<SolidColorBrush x:Key="Overlay" Color="#0865b23f" />
<SolidColorBrush x:Key="ButtonBackground" Color="#0865b2" />
<SolidColorBrush x:Key="ButtonDangerBackground" Color="#b20808" />
<SolidColorBrush x:Key="ButtonForeground" Color="#ffffff" />
<SolidColorBrush x:Key="MainBackground" Color="#001e46" />
<!-- <SolidColorBrush x:Key="TextBoxBackground" Color="#0865b2" /> -->
<!-- <SolidColorBrush x:Key="TextBoxForeground" Color="#ffffff" /> -->
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="Button">
<Setter Property="MinHeight" Value="40" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="{StaticResource ButtonBackground}" />
<Setter Property="Foreground" Value="{StaticResource ButtonForeground}" />
</Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="{StaticResource ButtonDangerBackground}" />
</Style>
<Style Selector="Button.vertical">
<Setter Property="MinWidth" Value="40" />
</Style>
<Style Selector="TextBox">
<Setter Property="MinHeight" Value="40" />
<!-- <Setter Property="Background" Value="{StaticResource TextBoxBackground}" /> -->
<!-- <Setter Property="Foreground" Value="{StaticResource TextBoxForeground}" /> -->
</Style>
<Style Selector="TextBox.singleline">
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="TextBox.multiline">
<Setter Property="VerticalContentAlignment" Value="Top" />
</Style>
<Style Selector="TextBlock">
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="TextBlock.mini">
<Setter Property="Margin" Value="0,0,0,0" />
</Style>
<Style Selector="NumericUpDown">
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
</UserControl.Styles>
<Grid Background="{StaticResource MainBackground}">
<!-- Основная форма -->
<Grid IsEnabled="{Binding !IsMenuOpen}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Верхняя панель -->
<Border
Grid.Row="0"
Padding="8"
Background="{StaticResource HeaderBackground}">
<Grid ColumnSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Grid.Column="0"
Width="70"
Command="{Binding ToggleMenuCommand}"
Content="☰" />
<TextBlock
Grid.Column="1"
Margin="10,0,0,0"
VerticalAlignment="Center"
FontSize="18"
Text="{Binding EditingRecord.Id, StringFormat='Id: {0}'}" />
</Grid>
</Border>
<!-- Основной контент -->
<Grid
Grid.Row="1"
Margin="8"
ColumnSpacing="5"
RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Главная панель -->
<core:SectionPanel
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CanScrollBackward="{Binding MainSectionCanScrollBackward}"
CanScrollForward="{Binding MainSectionCanScrollForward}"
ClipToBounds="True"
CurrentIndex="{Binding MainSectionsCurrentIndex}"
IsScrolling="{Binding MainSectionIsScrolling}"
Orientation="Vertical">
<!-- Информация -->
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Classes="mini"
Text="Тип:" />
<TextBox
Grid.Row="1"
Classes="singleline"
Text="{Binding EditingRecord.Type}"
Watermark="Тип" />
<TextBlock
Grid.Row="2"
Classes="mini"
Text="Партия:" />
<TextBox
Grid.Row="3"
Classes="singleline"
Text="{Binding EditingRecord.Batch}"
Watermark="Партия" />
</Grid>
<!-- Изображения -->
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Изображения -->
<Grid Grid.Row="0">
<Grid ColumnSpacing="5">
<Grid.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding
Converter="{StaticResource NotEqualsConverter}"
ConverterParameter="0"
Path="Images.Count" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</Grid.IsVisible>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Grid.Column="0"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Classes="vertical"
Command="{Binding ImagesSectionPreviousCommand}"
Content="◀">
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="ImagesSectionCanScrollBackward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
</Button>
<core:SectionPanel
Grid.Column="1"
CanScrollBackward="{Binding ImagesSectionCanScrollBackward}"
CanScrollForward="{Binding ImagesSectionCanScrollForward}"
ChildrenSource="{Binding Images}"
ClipToBounds="True"
CurrentIndex="{Binding ImagesSectionCurrentIndex}"
Cyclic="True"
IsScrolling="{Binding ImagesSectionIsScrolling}"
Orientation="Horizontal">
<core:SectionPanel.ChildrenTemplate>
<DataTemplate>
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid
Grid.Row="0"
Grid.RowSpan="11"
Grid.Column="0">
<core:ZoomPanImage
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Black"
ImageSource="{Binding Image}"
IsVisible="{Binding IsLoading, Converter={x:Static BoolConverters.Not}}" />
<StackPanel
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{Binding IsLoading}">
<ProgressBar IsIndeterminate="True" />
<TextBlock
HorizontalAlignment="Center"
FontSize="18"
Text="Изображение загружается" />
</StackPanel>
</Grid>
<TextBlock
Grid.Row="0"
Grid.Column="1"
Text="Id: " />
<TextBox
Grid.Row="1"
Grid.Column="1"
Classes="singleline"
IsReadOnly="True"
Text="{Binding ImageRecord.Id}" />
<TextBlock
Grid.Row="2"
Grid.Column="1"
Text="Имя: " />
<TextBox
Grid.Row="3"
Grid.Column="1"
Classes="singleline"
IsReadOnly="True"
Text="{Binding ImageRecord.ImagePath}" />
<TextBlock
Grid.Row="4"
Grid.Column="1"
Text="Интенсивность видимого: " />
<TextBox
Grid.Row="5"
Grid.Column="1"
Classes="singleline"
IsReadOnly="True"
Text="{Binding ImageRecord.VisibleIntensity}" />
<TextBlock
Grid.Row="6"
Grid.Column="1"
Text="Интенсивность УФ 365 нм: " />
<TextBox
Grid.Row="7"
Grid.Column="1"
Classes="singleline"
IsReadOnly="True"
Text="{Binding ImageRecord.Uv365Intensity}" />
<TextBlock
Grid.Row="8"
Grid.Column="1"
Text="Интенсивность УФ 254 нм: " />
<TextBox
Grid.Row="9"
Grid.Column="1"
Classes="singleline"
IsReadOnly="True"
Text="{Binding ImageRecord.Uv254Intensity}" />
</Grid>
</DataTemplate>
</core:SectionPanel.ChildrenTemplate>
</core:SectionPanel>
<Button
Grid.Column="2"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Classes="vertical"
Command="{Binding ImagesSectionNextCommand}"
Content="▶">
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="ImagesSectionCanScrollForward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
</Button>
</Grid>
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="18"
Text="Изображения сепаратора отсутствуют">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding
Converter="{StaticResource EqualsConverter}"
ConverterParameter="0"
Path="Images.Count" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<StackPanel
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{Binding ImagesIsCapturing}">
<ProgressBar IsIndeterminate="True" />
<TextBlock
HorizontalAlignment="Center"
FontSize="18"
Text="{Binding ImagesCapturingProgress}" />
</StackPanel>
</Grid>
<!-- Кнопки -->
<Button
Grid.Row="1"
HorizontalAlignment="Stretch"
Command="{Binding CaptureImageCommand}"
Content="Получить изображения"
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNull}}" />
<Button
Grid.Row="1"
HorizontalAlignment="Stretch"
Command="{Binding CancelImageCapturingCommand}"
Content="Отменить съёмку"
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNotNull}}" />
</Grid>
</core:SectionPanel>
<!-- Общие кнопки главной панели -->
<Button
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Stretch"
Classes="danger"
Command="{Binding DeleteRecordCommand}"
Content="Удалить" />
<Button
Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Stretch"
Command="{Binding SaveRecordCommand}"
Content="Сохранить" />
<Button
Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Stretch"
Command="{Binding SwitchMainSectionCommand}"
Content="К информации"
IsVisible="{Binding MainSectionsCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=1}">
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="MainSectionCanScrollBackward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="MainSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
</Button>
<Button
Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Stretch"
Command="{Binding SwitchMainSectionCommand}"
Content="К изображениям"
IsVisible="{Binding MainSectionsCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=0}">
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="MainSectionCanScrollForward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="MainSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
</Button>
</Grid>
</Grid>
<!-- Полупрозрачный оверлей -->
<Border
Background="{StaticResource Overlay}"
IsEnabled="{Binding IsMenuOpen}"
IsVisible="{Binding IsMenuOpen}">
<Interaction.Behaviors>
<TappedTrigger>
<InvokeCommandAction Command="{Binding CloseMenuCommand}" />
</TappedTrigger>
</Interaction.Behaviors>
</Border>
<!-- Выезжающая панель -->
<Border
HorizontalAlignment="Left"
Background="{StaticResource MenuBackground}"
BorderBrush="{StaticResource MenuBorder}"
BorderThickness="0,0,1,0"
IsEnabled="{Binding IsMenuOpen}"
IsVisible="{Binding IsMenuOpen}">
<Border.Transitions>
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.25" />
</Transitions>
</Border.Transitions>
<Interaction.Behaviors>
<DataTriggerBehavior
Binding="{Binding IsMenuOpen}"
ComparisonCondition="Equal"
Value="True">
<ChangePropertyAction PropertyName="RenderTransform" Value="translateX(0px)" />
</DataTriggerBehavior>
<DataTriggerBehavior
Binding="{Binding IsMenuOpen}"
ComparisonCondition="Equal"
Value="False">
<ChangePropertyAction PropertyName="RenderTransform" Value="translateX(-320px)" />
</DataTriggerBehavior>
</Interaction.Behaviors>
<!-- Содержимое меню -->
<StackPanel Margin="10" Spacing="10">
<Button
MinWidth="200"
HorizontalAlignment="Stretch"
Command="{Binding CreateRecordCommand}"
Content="Новая запись" />
<ListBox ItemsSource="{Binding Records}" SelectedItem="{Binding SelectedRecord}">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Background" Value="{StaticResource ButtonBackground}" />
</Style>
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource MainBackground}" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="5">
<Border Margin="3" CornerRadius="5">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">
<Run Text="{Binding Id, StringFormat='[{0}] '}" />
<Run Text="{Binding Type}" />
<Run Text=": " />
<Run Text="{Binding Batch}" />
</TextBlock>
</Border>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Border>
</Grid>
</UserControl>
@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace GSS2.UI.Core.Views;
public partial class AmineContentSeparatorCalibrationView : UserControl
{
public AmineContentSeparatorCalibrationView()
{
InitializeComponent();
}
}