forked from amkovkov/GranuSightSoftware2
feat: SampleCalibration View and ViewModel
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
// Оригинальное решение для проблеммы сворачивания колнок в Grid от
|
// Оригинальное решение для проблемы сворачивания колонок в Grid от
|
||||||
// https://github.com/AvaloniaUI/Avalonia/discussions/6773#discussioncomment-1514604
|
// https://github.com/AvaloniaUI/Avalonia/discussions/6773#discussioncomment-1514604
|
||||||
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Media;
|
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
|
|
||||||
using Mat = OpenCvSharp.Mat;
|
using Mat = OpenCvSharp.Mat;
|
||||||
using Cv2 = OpenCvSharp.Cv2;
|
using Cv2 = OpenCvSharp.Cv2;
|
||||||
|
|
||||||
using GSS2.Core.Extentions;
|
|
||||||
|
|
||||||
namespace GSS2.UI.Core;
|
namespace GSS2.UI.Core;
|
||||||
|
|
||||||
public class OpenCvImage : ZoomPanImage
|
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;
|
IsLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine($"Ошибка во время загрузки изображения:\n{ex.Message}\n{ex.StackTrace}");
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => IsLoading = false);
|
await Dispatcher.UIThread.InvokeAsync(() => IsLoading = false);
|
||||||
}
|
}
|
||||||
finally
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,657 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
using GSS2.Core.Analysis.AmineContent;
|
||||||
|
using GSS2.Core.Analysis.AmineContent.Database.Calibration;
|
||||||
|
using GSS2.Core.Hardware;
|
||||||
using GSS2.UI.Core.ViewModels;
|
using GSS2.UI.Core.ViewModels;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace GSS2.ViewModels.AmineContent;
|
namespace GSS2.ViewModels.AmineContent;
|
||||||
|
|
||||||
public partial class SampleCalibrationViewModel : ViewModelBase
|
public partial class SampleCalibrationViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public SampleCalibrationViewModel()
|
private readonly ILogger<SampleCalibrationViewModel> _logger;
|
||||||
{
|
private readonly AmineContentCalibrationContext _context;
|
||||||
|
private readonly AmineContentImageCapturerService _imageCapturer;
|
||||||
|
private readonly ImageStorageService _imageStorage;
|
||||||
|
|
||||||
|
private CancellationTokenSource? _captureImagesTaskCts = null;
|
||||||
|
|
||||||
|
[ObservableProperty] public partial bool MenuOpened { get; set; } = false;
|
||||||
|
[ObservableProperty] public partial ObservableCollection<CalibrationRecord> Records { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty] public partial CalibrationRecord? SelectedRecord { get; set; }
|
||||||
|
[ObservableProperty] public partial CalibrationRecord EditingRecord { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty] public partial int MainSectionCurrentIndex { get; set; } = 0;
|
||||||
|
[ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false;
|
||||||
|
[ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = true;
|
||||||
|
[ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = true;
|
||||||
|
|
||||||
|
[ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0;
|
||||||
|
[ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false;
|
||||||
|
[ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = true;
|
||||||
|
[ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = true;
|
||||||
|
|
||||||
|
[ObservableProperty] public partial Task? CaptureImagesTask { get; set; } = null;
|
||||||
|
|
||||||
|
[ObservableProperty] public partial int ImagesSectionCurrentIndex { get; set; } = 0;
|
||||||
|
[ObservableProperty] public partial bool ImagesSectionCanScrollForward { get; set; } = false;
|
||||||
|
[ObservableProperty] public partial bool ImagesSectionCanScrollBackward { get; set; } = false;
|
||||||
|
[ObservableProperty] public partial bool ImagesSectionIsScrolling { get; set; } = true;
|
||||||
|
[ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false;
|
||||||
|
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
|
||||||
|
|
||||||
|
[ObservableProperty] public partial ObservableCollection<AsyncImageRecordViewModel> SeparatorImages { get; set; } = new();
|
||||||
|
[ObservableProperty] public partial int SeparatorImagesSectionCurrentIndex { get; set; } = 0;
|
||||||
|
[ObservableProperty] public partial bool SeparatorImagesSectionCanScrollForward { get; set; } = false;
|
||||||
|
[ObservableProperty] public partial bool SeparatorImagesSectionCanScrollBackward { get; set; } = false;
|
||||||
|
[ObservableProperty] public partial bool SeparatorImagesSectionIsScrolling { get; set; } = true;
|
||||||
|
|
||||||
|
[ObservableProperty] public partial ObservableCollection<AsyncImageRecordViewModel> SampleImages { get; set; } = new();
|
||||||
|
[ObservableProperty] public partial int SampleImagesSectionCurrentIndex { get; set; } = 0;
|
||||||
|
[ObservableProperty] public partial bool SampleImagesSectionCanScrollForward { get; set; } = false;
|
||||||
|
[ObservableProperty] public partial bool SampleImagesSectionCanScrollBackward { get; set; } = false;
|
||||||
|
[ObservableProperty] public partial bool SampleImagesSectionIsScrolling { get; set; } = true;
|
||||||
|
|
||||||
|
|
||||||
|
public SampleCalibrationViewModel(
|
||||||
|
ILogger<SampleCalibrationViewModel> logger,
|
||||||
|
AmineContentCalibrationContext context,
|
||||||
|
AmineContentImageCapturerService imageCapturer,
|
||||||
|
ImageStorageService imageStorage
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_context = context;
|
||||||
|
_imageCapturer = imageCapturer;
|
||||||
|
_imageStorage = imageStorage;
|
||||||
|
_logger.LogInformation("Инициализация");
|
||||||
|
|
||||||
|
// Получаем записи из базы данных
|
||||||
|
Records = new ObservableCollection<CalibrationRecord>(
|
||||||
|
_context.CalibrationRecords
|
||||||
|
.Include(r => r.SampleImages)
|
||||||
|
.Include(r => r.SeparatorImages)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Берём первую запись
|
||||||
|
SelectedRecord = Records.FirstOrDefault();
|
||||||
|
// Если записей нет, то создаём пустую
|
||||||
|
// на самом деле это излишне, потому что при изменении SelectedRecord
|
||||||
|
// вызовется OnSelectedRecordChanged который сделает то же самое, но сделано это для избавления от предупреждений
|
||||||
|
if (SelectedRecord is null)
|
||||||
|
EditingRecord = new()
|
||||||
|
{
|
||||||
|
SampleName = "",
|
||||||
|
SampleComment = "",
|
||||||
|
SampleBrand = "",
|
||||||
|
SampleMass = -1.0,
|
||||||
|
SamplingDateTime = DateTime.Now,
|
||||||
|
SamplingPlace = "",
|
||||||
|
MixtureName = "",
|
||||||
|
MixtureAmineContent = -1.0,
|
||||||
|
MixtureNormalRate = -1.0,
|
||||||
|
MixtureActualRate = -1.0,
|
||||||
|
MeasuredContent = -1.0,
|
||||||
|
SampleImages = new(),
|
||||||
|
SeparatorImages = new()
|
||||||
|
};
|
||||||
|
else
|
||||||
|
EditingRecord = CopyRecord(SelectedRecord);
|
||||||
|
|
||||||
|
_logger.LogInformation("Инициализировано");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при изменении <see cref="SelectedRecord"/>.<br/>
|
||||||
|
/// Копирует <see cref="SelectedRecord"/> в <see cref="EditingRecord"/> или издаёт пустую <see cref="EditingRecord"/>.
|
||||||
|
/// </summary>
|
||||||
|
partial void OnSelectedRecordChanged(CalibrationRecord? value)
|
||||||
|
{
|
||||||
|
// Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись)
|
||||||
|
// то создаём пустую запись
|
||||||
|
// TODO: Подумать над тем, чтобы сразу удалять несохранённые изображения из хранилища
|
||||||
|
if (SelectedRecord is null)
|
||||||
|
{
|
||||||
|
EditingRecord = new()
|
||||||
|
{
|
||||||
|
SampleName = "",
|
||||||
|
SampleComment = "",
|
||||||
|
SampleBrand = "",
|
||||||
|
SampleMass = -1.0,
|
||||||
|
SamplingDateTime = DateTime.Now,
|
||||||
|
SamplingPlace = "",
|
||||||
|
MixtureName = "",
|
||||||
|
MixtureAmineContent = -1.0,
|
||||||
|
MixtureNormalRate = -1.0,
|
||||||
|
MixtureActualRate = -1.0,
|
||||||
|
MeasuredContent = -1.0,
|
||||||
|
SampleImages = new(),
|
||||||
|
SeparatorImages = new()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Иначе копируем запись, отвязывая её от базы данных
|
||||||
|
EditingRecord = CopyRecord(SelectedRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при изменении <see cref="EditingRecord"/>.<br/>
|
||||||
|
/// Заполняет <see cref="Images"/> изображениями из <see cref="EditingRecord"/>.
|
||||||
|
/// </summary>
|
||||||
|
partial void OnEditingRecordChanged(CalibrationRecord value)
|
||||||
|
{
|
||||||
|
// Обновляем изображения сепаратора
|
||||||
|
{
|
||||||
|
var images = value.SeparatorImages
|
||||||
|
// Для каждого изображения
|
||||||
|
.Select(i =>
|
||||||
|
{
|
||||||
|
// Находим путь через ImageStorageService
|
||||||
|
string? path;
|
||||||
|
// Если ImagesSectionCurrentIndex == 0, то выбрана секция с изображениями сепаратора
|
||||||
|
if (ImagesSectionCurrentIndex == 0)
|
||||||
|
path = _imageStorage.GetFullPath(i.ImagePath, "separator");
|
||||||
|
// Если ImagesSectionCurrentIndex == 1, то выбрана секция с изображениями пробы
|
||||||
|
else if (ImagesSectionCurrentIndex == 1)
|
||||||
|
path = _imageStorage.GetFullPath(i.ImagePath, "sample");
|
||||||
|
else
|
||||||
|
throw new UnreachableException();
|
||||||
|
|
||||||
|
// Если изображения не найдено, возвращаем null
|
||||||
|
if (path is null)
|
||||||
|
return null;
|
||||||
|
// Создаём AsyncImageRecordViewModel
|
||||||
|
return new AsyncImageRecordViewModel(path, i);
|
||||||
|
})
|
||||||
|
// Оставляем только не null значения
|
||||||
|
.OfType<AsyncImageRecordViewModel>();
|
||||||
|
|
||||||
|
// Создаём новую коллекцию изображений
|
||||||
|
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой
|
||||||
|
SeparatorImages.Clear();
|
||||||
|
SeparatorImages = new(images);
|
||||||
|
}
|
||||||
|
// Обновляем изображения пробы
|
||||||
|
{
|
||||||
|
var images = value.SampleImages
|
||||||
|
// Для каждого изображения
|
||||||
|
.Select(i =>
|
||||||
|
{
|
||||||
|
// Находим путь через ImageStorageService
|
||||||
|
var path = _imageStorage.GetFullPath(i.ImagePath, "sample");
|
||||||
|
// Если изображения не найдено, возвращаем null
|
||||||
|
if (path is null)
|
||||||
|
return null;
|
||||||
|
// Создаём AsyncImageRecordViewModel
|
||||||
|
return new AsyncImageRecordViewModel(path, i);
|
||||||
|
})
|
||||||
|
// Оставляем только не null значения
|
||||||
|
.OfType<AsyncImageRecordViewModel>();
|
||||||
|
|
||||||
|
// Создаём новую коллекцию изображений
|
||||||
|
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой
|
||||||
|
SampleImages.Clear();
|
||||||
|
SampleImages = new(images);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Просто команды управления меню
|
||||||
|
[RelayCommand] private void OpenMenu() => MenuOpened = true;
|
||||||
|
[RelayCommand] private void CloseMenu() => MenuOpened = false;
|
||||||
|
[RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened;
|
||||||
|
|
||||||
|
// Команда создания новой записи
|
||||||
|
// вызывает цепочку действий по изменению EditingRecord и Images
|
||||||
|
[RelayCommand]
|
||||||
|
private void CreateRecord()
|
||||||
|
{
|
||||||
|
// Из-за того что, если создана новая запись, то SelectedRecord == null
|
||||||
|
// при простом присвоении SelectedRecord = null событие SelectedRecordChanged не вызовется,
|
||||||
|
// потому что событие срабатывает только при изменении значения, а значение было null и присвоилось ему null,
|
||||||
|
// поэтому если SelectedRecord == null, то нужно явно вызвать метод OnSelectedRecordChanged
|
||||||
|
if (SelectedRecord is not null)
|
||||||
|
SelectedRecord = null;
|
||||||
|
else
|
||||||
|
OnSelectedRecordChanged(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Команда удаления записи, создаёт новую запись и удаляет старую из списка и базы данных
|
||||||
|
[RelayCommand]
|
||||||
|
private void DeleteRecord()
|
||||||
|
{
|
||||||
|
// Сохраняем текущую запись
|
||||||
|
var deletedRecord = SelectedRecord;
|
||||||
|
// Создаём новую запись
|
||||||
|
CreateRecord();
|
||||||
|
|
||||||
|
// Если запись на была сохранена в базу данных, то ничего не делаем
|
||||||
|
if (deletedRecord is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Если запись была в без данных, то
|
||||||
|
// она была и в Records, удаляем её из Records
|
||||||
|
Records.Remove(deletedRecord);
|
||||||
|
// Удаляем запись из базы данных и сохраняем изменения
|
||||||
|
// TODO: Подумать над тем, что возможно стоит не удалять запись полностью, а добавить флаг удаления,
|
||||||
|
// но тогда нужно будет сохранять и изображения этой записи, как в базе данных, так и на диске,
|
||||||
|
// тогда удалённые записи будут заполнять диск
|
||||||
|
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища
|
||||||
|
_context.CalibrationRecords.Remove(deletedRecord);
|
||||||
|
_context.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Команда сохранения записи, сохраняет новую запись или изменения в отредактированной
|
||||||
|
[RelayCommand]
|
||||||
|
private void SaveRecord()
|
||||||
|
{
|
||||||
|
// Если это новая запись
|
||||||
|
if (SelectedRecord is null)
|
||||||
|
{
|
||||||
|
// Если это новая запись, то копируем её
|
||||||
|
var newRecord = CopyRecord(EditingRecord);
|
||||||
|
|
||||||
|
// Для каждого изображения сепаратора в записи
|
||||||
|
for (int i = 0; i < newRecord.SeparatorImages.Count(); i++)
|
||||||
|
{
|
||||||
|
var image = newRecord.SeparatorImages[i];
|
||||||
|
// Если изображение есть в базе данных, то пропускаем его
|
||||||
|
if (_context.Entry(image).State is not EntityState.Detached)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Если изображения нет в базе данных, то добавляем его в базу
|
||||||
|
var attachedImage = _context.ImageRecords.Add(image);
|
||||||
|
// Удаляем отвязанное от базы данных изображение из записи
|
||||||
|
newRecord.SeparatorImages.RemoveAt(i);
|
||||||
|
// Добавляем привязанное изображение к записи
|
||||||
|
newRecord.SeparatorImages.Insert(i, attachedImage.Entity);
|
||||||
|
}
|
||||||
|
// Для каждого изображения пробы в записи
|
||||||
|
for (int i = 0; i < newRecord.SampleImages.Count(); i++)
|
||||||
|
{
|
||||||
|
var image = newRecord.SampleImages[i];
|
||||||
|
// Если изображение есть в базе данных, то пропускаем его
|
||||||
|
if (_context.Entry(image).State is not EntityState.Detached)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Если изображения нет в базе данных, то добавляем его в базу
|
||||||
|
var attachedImage = _context.ImageRecords.Add(image);
|
||||||
|
// Удаляем отвязанное от базы данных изображение из записи
|
||||||
|
newRecord.SampleImages.RemoveAt(i);
|
||||||
|
// Добавляем привязанное изображение к записи
|
||||||
|
newRecord.SampleImages.Insert(i, attachedImage.Entity);
|
||||||
|
}
|
||||||
|
// Если записи нет в базе данных
|
||||||
|
if (_context.Entry(newRecord).State is EntityState.Detached)
|
||||||
|
{
|
||||||
|
// То добавляем её в базу
|
||||||
|
var attachedRecord = _context.CalibrationRecords.Add(newRecord);
|
||||||
|
// Заменяем текущую запись на привязанную к базе данных
|
||||||
|
newRecord = attachedRecord.Entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем изменения в базе данных
|
||||||
|
_context.SaveChanges();
|
||||||
|
// Добавляем запись в список
|
||||||
|
Records.Add(newRecord);
|
||||||
|
// Выбираем запись
|
||||||
|
SelectedRecord = newRecord;
|
||||||
|
}
|
||||||
|
// Если эта запись есть в базе
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Переносим данные из редактируемой записи в выбранную
|
||||||
|
SelectedRecord.SampleName = EditingRecord.SampleName;
|
||||||
|
SelectedRecord.SampleComment = EditingRecord.SampleComment;
|
||||||
|
SelectedRecord.SampleBrand = EditingRecord.SampleBrand;
|
||||||
|
SelectedRecord.SampleMass = EditingRecord.SampleMass;
|
||||||
|
SelectedRecord.SamplingDateTime = EditingRecord.SamplingDateTime;
|
||||||
|
SelectedRecord.SamplingPlace = EditingRecord.SamplingPlace;
|
||||||
|
SelectedRecord.MixtureName = EditingRecord.MixtureName;
|
||||||
|
SelectedRecord.MixtureAmineContent = EditingRecord.MixtureAmineContent;
|
||||||
|
SelectedRecord.MixtureNormalRate = EditingRecord.MixtureNormalRate;
|
||||||
|
SelectedRecord.MixtureActualRate = EditingRecord.MixtureActualRate;
|
||||||
|
SelectedRecord.MeasuredContent = EditingRecord.MeasuredContent;
|
||||||
|
|
||||||
|
// Удаляем старые изображения сепаратора в записи
|
||||||
|
{
|
||||||
|
// Находим изображения которые нужно удалить -
|
||||||
|
// те которые есть в выбранной записи, но нет в редактируемой
|
||||||
|
var imagesToRemove = SelectedRecord.SeparatorImages
|
||||||
|
.Where(e => EditingRecord.SeparatorImages.All(i => i.Id != e.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Удаляем старые изображения
|
||||||
|
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища
|
||||||
|
foreach (var image in imagesToRemove)
|
||||||
|
SelectedRecord.SeparatorImages.Remove(image);
|
||||||
|
|
||||||
|
// Для каждого изображения в редактируемой записи
|
||||||
|
foreach (var editedImage in EditingRecord.SeparatorImages)
|
||||||
|
{
|
||||||
|
// Находим изображение в выбранной записи (оно будет привязано к базе данных)
|
||||||
|
var existing = SelectedRecord.SeparatorImages
|
||||||
|
.FirstOrDefault(i => i.Id == editedImage.Id);
|
||||||
|
|
||||||
|
// Если изображение не найдено
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
// Копируем изображение в выбранную запись
|
||||||
|
SelectedRecord.SeparatorImages.Add(CopyRecord(editedImage));
|
||||||
|
}
|
||||||
|
// Если изображение найдено
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Изменяем данные изображения на данные изображения из редактируемой записи
|
||||||
|
existing.ImagePath = editedImage.ImagePath;
|
||||||
|
existing.VisibleIntensity = editedImage.VisibleIntensity;
|
||||||
|
existing.Uv365Intensity = editedImage.Uv365Intensity;
|
||||||
|
existing.Uv254Intensity = editedImage.Uv254Intensity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для каждого изображения в выбранной записи
|
||||||
|
for (int i = 0; i < SelectedRecord.SeparatorImages.Count(); i++)
|
||||||
|
{
|
||||||
|
var image = SelectedRecord.SeparatorImages[i];
|
||||||
|
// Если изображение есть в базе данных, то пропускаем его
|
||||||
|
if (_context.Entry(image).State is not EntityState.Detached)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Если изображения нет в базе данных, то добавляем его в базу
|
||||||
|
var attachedImage = _context.ImageRecords.Add(image);
|
||||||
|
// Удаляем отвязанное от базы данных изображение из записи
|
||||||
|
SelectedRecord.SeparatorImages.RemoveAt(i);
|
||||||
|
// Добавляем привязанное изображение к записи
|
||||||
|
SelectedRecord.SeparatorImages.Insert(i, attachedImage.Entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Удаляем старые изображения пробы в записи
|
||||||
|
{
|
||||||
|
// Находим изображения которые нужно удалить -
|
||||||
|
// те которые есть в выбранной записи, но нет в редактируемой
|
||||||
|
var imagesToRemove = SelectedRecord.SampleImages
|
||||||
|
.Where(e => EditingRecord.SampleImages.All(i => i.Id != e.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Удаляем старые изображения
|
||||||
|
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища
|
||||||
|
foreach (var image in imagesToRemove)
|
||||||
|
SelectedRecord.SampleImages.Remove(image);
|
||||||
|
|
||||||
|
// Для каждого изображения в редактируемой записи
|
||||||
|
foreach (var editedImage in EditingRecord.SampleImages)
|
||||||
|
{
|
||||||
|
// Находим изображение в выбранной записи (оно будет привязано к базе данных)
|
||||||
|
var existing = SelectedRecord.SampleImages
|
||||||
|
.FirstOrDefault(i => i.Id == editedImage.Id);
|
||||||
|
|
||||||
|
// Если изображение не найдено
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
// Копируем изображение в выбранную запись
|
||||||
|
SelectedRecord.SampleImages.Add(CopyRecord(editedImage));
|
||||||
|
}
|
||||||
|
// Если изображение найдено
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Изменяем данные изображения на данные изображения из редактируемой записи
|
||||||
|
existing.ImagePath = editedImage.ImagePath;
|
||||||
|
existing.VisibleIntensity = editedImage.VisibleIntensity;
|
||||||
|
existing.Uv365Intensity = editedImage.Uv365Intensity;
|
||||||
|
existing.Uv254Intensity = editedImage.Uv254Intensity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для каждого изображения в выбранной записи
|
||||||
|
for (int i = 0; i < SelectedRecord.SampleImages.Count(); i++)
|
||||||
|
{
|
||||||
|
var image = SelectedRecord.SampleImages[i];
|
||||||
|
// Если изображение есть в базе данных, то пропускаем его
|
||||||
|
if (_context.Entry(image).State is not EntityState.Detached)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Если изображения нет в базе данных, то добавляем его в базу
|
||||||
|
var attachedImage = _context.ImageRecords.Add(image);
|
||||||
|
// Удаляем отвязанное от базы данных изображение из записи
|
||||||
|
SelectedRecord.SampleImages.RemoveAt(i);
|
||||||
|
// Добавляем привязанное изображение к записи
|
||||||
|
SelectedRecord.SampleImages.Insert(i, attachedImage.Entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если записи нет в базе данных
|
||||||
|
if (_context.Entry(SelectedRecord).State is EntityState.Detached)
|
||||||
|
{
|
||||||
|
// То добавляем её в базу
|
||||||
|
var attachedRecord = _context.CalibrationRecords.Add(SelectedRecord);
|
||||||
|
// Находим текущую запись в списке
|
||||||
|
var index = Records.IndexOf(SelectedRecord);
|
||||||
|
// Заменяем её в списке на привязанную к базе данных
|
||||||
|
Records[index] = attachedRecord.Entity;
|
||||||
|
// Заменяем выбранную запись на привязанную
|
||||||
|
SelectedRecord = attachedRecord.Entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем изменения в базе данных
|
||||||
|
_context.SaveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Команда изменения основной секции (информация <-> изображения)
|
||||||
|
[RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0;
|
||||||
|
|
||||||
|
// Команда навигации по информации
|
||||||
|
[RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--;
|
||||||
|
[RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++;
|
||||||
|
|
||||||
|
// Команда навигации по информации
|
||||||
|
[RelayCommand] private void ImagesSectionSwitch() => ImagesSectionCurrentIndex = ImagesSectionCurrentIndex == 0 ? 1 : 0;
|
||||||
|
|
||||||
|
// Команда навигации по изображениям сепаратора
|
||||||
|
[RelayCommand] private void SeparatorImagesSectionPrevious() => SeparatorImagesSectionCurrentIndex--;
|
||||||
|
[RelayCommand] private void SeparatorImagesSectionNext() => SeparatorImagesSectionCurrentIndex++;
|
||||||
|
|
||||||
|
// Команда навигации по изображениям пробы
|
||||||
|
[RelayCommand] private void SampleImagesSectionPrevious() => SampleImagesSectionCurrentIndex--;
|
||||||
|
[RelayCommand] private void SampleImagesSectionNext() => SampleImagesSectionCurrentIndex++;
|
||||||
|
|
||||||
|
// Команда съёмки изображений
|
||||||
|
[RelayCommand]
|
||||||
|
private void CaptureImage()
|
||||||
|
{
|
||||||
|
// Если съёмка уже начата, то ничего не делаем
|
||||||
|
if (CaptureImagesTask is not null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Создаём токен отмены задачи
|
||||||
|
_captureImagesTaskCts = new CancellationTokenSource();
|
||||||
|
var cancellationToken = _captureImagesTaskCts.Token;
|
||||||
|
|
||||||
|
// Запускаем получение изображений асинхронно
|
||||||
|
CaptureImagesTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
// Список полученных изображений
|
||||||
|
var imageRecords = new List<ImageRecord>();
|
||||||
|
|
||||||
|
// Обновляем данные в интерфейсе
|
||||||
|
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а
|
||||||
|
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||||
|
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true);
|
||||||
|
|
||||||
|
// Получаем изображения
|
||||||
|
var task = _imageCapturer.CaptureImages(async (data) =>
|
||||||
|
{
|
||||||
|
// Распаковываем данные
|
||||||
|
var (i, visible, uv365, uv254, image) = data;
|
||||||
|
// Выбрасываем исключение если задача отменена
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Обновляем данные в интерфейсе
|
||||||
|
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а
|
||||||
|
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||||
|
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {AmineContentImageCapturerService.IlluminatorIntensities.Count()}");
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
// Кодируем изображение в png
|
||||||
|
var bytes = image.ImEncode(".png");
|
||||||
|
// Сохраняем изображение в ImageStorageService
|
||||||
|
string fileName;
|
||||||
|
// Если ImagesSectionCurrentIndex == 0, то выбрана секция с изображениями сепаратора
|
||||||
|
if (ImagesSectionCurrentIndex == 0)
|
||||||
|
using (var stream = new MemoryStream(bytes))
|
||||||
|
fileName = await _imageStorage.SaveAsync(stream, ".png", "separator", cancellationToken);
|
||||||
|
// Если ImagesSectionCurrentIndex == 1, то выбрана секция с изображениями пробы
|
||||||
|
else if (ImagesSectionCurrentIndex == 1)
|
||||||
|
using (var stream = new MemoryStream(bytes))
|
||||||
|
fileName = await _imageStorage.SaveAsync(stream, ".png", "sample", cancellationToken);
|
||||||
|
else
|
||||||
|
throw new UnreachableException();
|
||||||
|
|
||||||
|
// Освобождаем память изображения
|
||||||
|
image.Release();
|
||||||
|
image = null;
|
||||||
|
bytes = null;
|
||||||
|
|
||||||
|
// Добавляем изображение в список полученных
|
||||||
|
imageRecords.Add(
|
||||||
|
new ImageRecord()
|
||||||
|
{
|
||||||
|
ImagePath = fileName,
|
||||||
|
VisibleIntensity = visible,
|
||||||
|
Uv365Intensity = uv365,
|
||||||
|
Uv254Intensity = uv254,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
_ = task.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (t.Exception is not null)
|
||||||
|
{
|
||||||
|
_logger.LogError(t.Exception, "Исключение во время получения изображений");
|
||||||
|
foreach (var ex in t.Exception.InnerExceptions ?? [])
|
||||||
|
_logger.LogError(ex, "Исключение во время получения изображений");
|
||||||
|
}
|
||||||
|
var images = imageRecords
|
||||||
|
// Для каждого изображения
|
||||||
|
.Select(i =>
|
||||||
|
{
|
||||||
|
// Находим путь через ImageStorageService
|
||||||
|
string? path;
|
||||||
|
// Если ImagesSectionCurrentIndex == 0, то выбрана секция с изображениями сепаратора
|
||||||
|
if (ImagesSectionCurrentIndex == 0)
|
||||||
|
path = _imageStorage.GetFullPath(i.ImagePath, "separator");
|
||||||
|
// Если ImagesSectionCurrentIndex == 1, то выбрана секция с изображениями пробы
|
||||||
|
else if (ImagesSectionCurrentIndex == 1)
|
||||||
|
path = _imageStorage.GetFullPath(i.ImagePath, "sample");
|
||||||
|
else
|
||||||
|
throw new UnreachableException();
|
||||||
|
// Если изображения не найдено, возвращаем null
|
||||||
|
if (path is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Создаём AsyncImageRecordViewModel
|
||||||
|
return new AsyncImageRecordViewModel(path, i);
|
||||||
|
})
|
||||||
|
// Оставляем только не null значения
|
||||||
|
.OfType<AsyncImageRecordViewModel>();
|
||||||
|
|
||||||
|
// Обновляем данные в редактируемой коллекции
|
||||||
|
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а
|
||||||
|
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||||
|
_ = Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
// Если ImagesSectionCurrentIndex == 0, то выбрана секция с изображениями сепаратора
|
||||||
|
if (ImagesSectionCurrentIndex == 0)
|
||||||
|
{
|
||||||
|
EditingRecord.SeparatorImages = imageRecords;
|
||||||
|
// Создаём новую коллекцию изображений
|
||||||
|
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой
|
||||||
|
SeparatorImages = new(images);
|
||||||
|
}
|
||||||
|
// Если ImagesSectionCurrentIndex == 1, то выбрана секция с изображениями пробы
|
||||||
|
else if (ImagesSectionCurrentIndex == 1)
|
||||||
|
{
|
||||||
|
EditingRecord.SampleImages = imageRecords;
|
||||||
|
// Создаём новую коллекцию изображений
|
||||||
|
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой
|
||||||
|
SampleImages = new(images);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw new UnreachableException();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Сбрасываем состояние задачи съёмки изображений
|
||||||
|
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а
|
||||||
|
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||||
|
_ = Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
ImagesIsCapturing = false;
|
||||||
|
ImagesCapturingProgress = "";
|
||||||
|
CaptureImagesTask = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Команда отмены задачи съёмки изображений
|
||||||
|
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Копирует <see cref="CalibrationRecord"/>, отвязывая её от базы данных
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Скопированная <see cref="CalibrationRecord"/></returns>
|
||||||
|
private static CalibrationRecord CopyRecord(CalibrationRecord source)
|
||||||
|
{
|
||||||
|
return new CalibrationRecord
|
||||||
|
{
|
||||||
|
Id = source.Id,
|
||||||
|
SampleName = source.SampleName,
|
||||||
|
SampleComment = source.SampleComment,
|
||||||
|
SampleBrand = source.SampleBrand,
|
||||||
|
SampleMass = source.SampleMass,
|
||||||
|
SamplingDateTime = source.SamplingDateTime,
|
||||||
|
SamplingPlace = source.SamplingPlace,
|
||||||
|
MixtureName = source.MixtureName,
|
||||||
|
MixtureAmineContent = source.MixtureAmineContent,
|
||||||
|
MixtureNormalRate = source.MixtureNormalRate,
|
||||||
|
MixtureActualRate = source.MixtureActualRate,
|
||||||
|
MeasuredContent = source.MeasuredContent,
|
||||||
|
SampleImages = source.SampleImages
|
||||||
|
.Select(CopyRecord)
|
||||||
|
.ToList(),
|
||||||
|
SeparatorImages = source.SeparatorImages
|
||||||
|
.Select(CopyRecord)
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Копирует <see cref="ImageRecord"/>, отвязывая её от базы данных
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Скопированная <see cref="ImageRecord"/></returns>
|
||||||
|
private static ImageRecord CopyRecord(ImageRecord source)
|
||||||
|
{
|
||||||
|
return new ImageRecord
|
||||||
|
{
|
||||||
|
Id = source.Id,
|
||||||
|
ImagePath = source.ImagePath,
|
||||||
|
VisibleIntensity = source.VisibleIntensity,
|
||||||
|
Uv365Intensity = source.Uv365Intensity,
|
||||||
|
Uv254Intensity = source.Uv254Intensity
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,853 @@
|
|||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="using:GSS2.ViewModels.AmineContent"
|
xmlns:vm="using:GSS2.ViewModels.AmineContent"
|
||||||
|
xmlns:converters="using:GSS2.UI.Core.Converters"
|
||||||
|
xmlns:avalonia_converters="using:Avalonia.Data.Converters"
|
||||||
|
xmlns:i="using:Avalonia.Xaml.Interactivity"
|
||||||
|
xmlns:ia="using:Avalonia.Xaml.Interactions.Custom"
|
||||||
|
xmlns:core="using:GSS2.UI.Core"
|
||||||
|
xmlns:core_vm="using:GSS2.UI.Core.ViewModels"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
x:DataType="vm:SampleCalibrationViewModel">
|
x:DataType="vm:SampleCalibrationViewModel">
|
||||||
|
|
||||||
<Grid>
|
<UserControl.Resources>
|
||||||
<TextBlock Text="Калибровка проб" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
<converters:NotEqualsConverter x:Key="NotEqualsConverter" />
|
||||||
|
<converters:EqualsConverter x:Key="EqualsConverter" />
|
||||||
|
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
|
||||||
|
<Style Selector="Button">
|
||||||
|
<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="FontSize" Value="20"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TextBox.singleline">
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TextBox.multiline">
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Top" />
|
||||||
|
<Setter Property="AcceptsReturn" Value="True" />
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="20"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TextBlock.mini">
|
||||||
|
<Setter Property="FontSize" Value="16"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,0" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<Grid Background="{StaticResource MainBackground}">
|
||||||
|
|
||||||
|
<!-- Основная форма -->
|
||||||
|
<Grid IsEnabled="{Binding !MenuOpened}" 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 MainSectionCurrentIndex}"
|
||||||
|
IsScrolling="{Binding MainSectionIsScrolling}"
|
||||||
|
Orientation="Vertical">
|
||||||
|
|
||||||
|
<!-- Информация -->
|
||||||
|
<Grid ColumnSpacing="5" RowSpacing="5">
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" /> <!-- Информация -->
|
||||||
|
<RowDefinition Height="Auto" /> <!-- Кнопки -->
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" /> <!-- Кнопка меню -->
|
||||||
|
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||||
|
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Кнопка меню -->
|
||||||
|
<Button Grid.Column="0" Grid.Row="0" Grid.RowSpan="3" Classes="vertical" VerticalAlignment="Stretch" Command="{Binding ToggleMenuCommand}">
|
||||||
|
<TextBlock Text="☰"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Информация -->
|
||||||
|
<core:SectionPanel Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"
|
||||||
|
CanScrollBackward="{Binding InfoSectionCanScrollBackward}"
|
||||||
|
CanScrollForward="{Binding InfoSectionCanScrollForward}"
|
||||||
|
ClipToBounds="True"
|
||||||
|
CurrentIndex="{Binding InfoSectionCurrentIndex}"
|
||||||
|
Cyclic="True"
|
||||||
|
IsScrolling="{Binding InfoSectionIsScrolling}"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
|
||||||
|
<!-- ИД / Название пробы / Марка пробы / Дата и время отбора пробы / Место отбора пробы / Масса пробы (кг) -->
|
||||||
|
<Grid ColumnSpacing="5" RowSpacing="5">
|
||||||
|
|
||||||
|
<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="*" /> <!-- Пустое пространство -->
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||||
|
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Информация -->
|
||||||
|
<!-- ИД (подпись) -->
|
||||||
|
<TextBlock Grid.Row="0" Grid.ColumnSpan="2" Grid.Column="0" Classes="mini" Text="ИД:" />
|
||||||
|
<!-- ИД (поле) -->
|
||||||
|
<TextBox Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.Id}" PlaceholderText="ИД" IsReadOnly="True" />
|
||||||
|
<!-- Название пробы (подпись) -->
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Classes="mini" Text="Название пробы:" />
|
||||||
|
<!-- Название пробы (поле) -->
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.SampleName}" PlaceholderText="Название пробы" />
|
||||||
|
<!-- Марка пробы (подпись) -->
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="1" Classes="mini" Text="Марка пробы:" />
|
||||||
|
<!-- Марка пробы (поле) -->
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.SampleBrand}" PlaceholderText="Марка пробы" />
|
||||||
|
<!-- Дата и время отбора пробы (подпись) -->
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0" Classes="mini" Text="Дата и время отбора пробы:" />
|
||||||
|
<!-- Дата и время отбора пробы (поле) -->
|
||||||
|
<TextBox Grid.Row="5" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.SamplingDateTime, Converter={StaticResource DateTimeConverter}}" PlaceholderText="Дата и время отбора пробы" />
|
||||||
|
<!-- Место отбора пробы (подпись) -->
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Место отбора пробы:" />
|
||||||
|
<!-- Место отбора пробы (поле) -->
|
||||||
|
<TextBox Grid.Row="5" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.SamplingPlace}" PlaceholderText="Место отбора пробы" />
|
||||||
|
<!-- Масса пробы (кг) (подпись) -->
|
||||||
|
<TextBlock Grid.Row="6" Grid.ColumnSpan="2" Grid.Column="0" Classes="mini" Text="Масса пробы (кг):" />
|
||||||
|
<!-- Масса пробы (кг) (поле) -->
|
||||||
|
<NumericUpDown Grid.Row="7" Grid.ColumnSpan="2" Grid.Column="0" Classes="singleline" FormatString="0.0000" Increment="0.0001" Value="{Binding EditingRecord.SampleMass}" PlaceholderText="Масса пробы (кг)" />
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Название используемой кондиционирующей смеси / Содержание аминов в кондиционирующей смеси (%) / Норма расхода кондиционирующей смеси (кг/т | гр/кг) / Фактический расход кондиционирующей смеси (кг/т | гр/кг) -->
|
||||||
|
<Grid ColumnSpacing="5" RowSpacing="5">
|
||||||
|
|
||||||
|
<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="*" /> <!-- Пустое пространство -->
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||||
|
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Информация -->
|
||||||
|
<!-- Название используемой кондиционирующей смеси (подпись) -->
|
||||||
|
<TextBlock Grid.Row="0" Grid.ColumnSpan="2" Grid.Column="0" Classes="mini" Text="Название используемой кондиционирующей смеси:" />
|
||||||
|
<!-- Название используемой кондиционирующей смеси (поле) -->
|
||||||
|
<TextBox Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.MixtureName}" PlaceholderText="Название используемой кондиционирующей смеси" />
|
||||||
|
<!-- Содержание люминофоров в кондиционирующей смеси (%) (подпись) -->
|
||||||
|
<TextBlock Grid.Row="2" Grid.ColumnSpan="2" Grid.Column="0" Classes="mini" Text="Содержание люминофоров в кондиционирующей смеси (%):" />
|
||||||
|
<!-- Содержание люминофоров в кондиционирующей смеси (%) (поле) -->
|
||||||
|
<NumericUpDown Grid.Row="3" Grid.ColumnSpan="2" Grid.Column="0" Classes="singleline" FormatString="0.0" Increment="0.1" Value="{Binding EditingRecord.MixtureAmineContent}" PlaceholderText="Содержание люминофоров в кондиционирующей смеси (%)" />
|
||||||
|
<!-- Норма расхода кондиционирующей смеси (кг/т | гр/кг) (подпись) -->
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0" Classes="mini" Text="Норма расхода (кг/т | гр/кг):" />
|
||||||
|
<!-- Норма расхода кондиционирующей смеси (кг/т | гр/кг) (поле) -->
|
||||||
|
<NumericUpDown Grid.Row="5" Grid.Column="0" Classes="singleline" FormatString="0.0000" Increment="0.0001" Value="{Binding EditingRecord.MixtureNormalRate}" PlaceholderText="Норма расхода кондиционирующей смеси (кг/т | гр/кг)" />
|
||||||
|
<!-- Фактический расход кондиционирующей смеси (кг/т | гр/кг) (подпись) -->
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Фактический расход (кг/т | гр/кг):" />
|
||||||
|
<!-- Фактический расход кондиционирующей смеси (кг/т | гр/кг) (поле) -->
|
||||||
|
<NumericUpDown Grid.Row="5" Grid.Column="1" Classes="singleline" FormatString="0.0000" Increment="0.0001" Value="{Binding EditingRecord.MixtureActualRate}" PlaceholderText="Фактический расход кондиционирующей смеси (кг/т | гр/кг)" />
|
||||||
|
<!-- Измеренное количество кондиционирующей смеси (кг/т | гр/кг) (подпись) -->
|
||||||
|
<TextBlock Grid.Row="6" Grid.ColumnSpan="2" Grid.Column="0" Classes="mini" Text="Измеренное количество кондиционирующей смеси (кг/т | гр/кг):" />
|
||||||
|
<!-- Измеренное количество кондиционирующей смеси (кг/т | гр/кг) (поле) -->
|
||||||
|
<NumericUpDown Grid.Row="7" Grid.ColumnSpan="2" Grid.Column="0" Classes="singleline" FormatString="0.0000" Increment="0.0001" Value="{Binding EditingRecord.MeasuredContent}" PlaceholderText="Измеренное количество кондиционирующей смеси (кг/т | гр/кг)" />
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Дополнительная информация о пробе -->
|
||||||
|
<Grid ColumnSpacing="5" RowSpacing="5">
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" /> <!-- Дополнительная информация о пробе (подпись) -->
|
||||||
|
<RowDefinition Height="*" /> <!-- Дополнительная информация о пробе (поле) -->
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Информация -->
|
||||||
|
<!-- Дополнительная информация о пробе (подпись) -->
|
||||||
|
<TextBlock Grid.Row="0" Classes="mini" Text="Дополнительная информация о пробе:" />
|
||||||
|
<!-- Дополнительная информация о пробе (поле) -->
|
||||||
|
<TextBox Grid.Row="1" Classes="multiline" Text="{Binding EditingRecord.SampleComment}" PlaceholderText="Дополнительная информация о пробе" />
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</core:SectionPanel>
|
||||||
|
|
||||||
|
<!-- Кнопка влево -->
|
||||||
|
<Button
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
Command="{Binding InfoSectionPreviousCommand}">
|
||||||
|
|
||||||
|
<Button.IsEnabled>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<Binding Path="InfoSectionCanScrollBackward" />
|
||||||
|
<Binding Converter="{x:Static BoolConverters.Not}" Path="InfoSectionIsScrolling" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsEnabled>
|
||||||
|
|
||||||
|
<TextBlock Text="◀"/>
|
||||||
|
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Кнопка вправо -->
|
||||||
|
<Button
|
||||||
|
Grid.Column="2"
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
Command="{Binding InfoSectionNextCommand}">
|
||||||
|
|
||||||
|
<Button.IsEnabled>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<Binding Path="InfoSectionCanScrollForward" />
|
||||||
|
<Binding Converter="{x:Static BoolConverters.Not}" Path="InfoSectionIsScrolling" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsEnabled>
|
||||||
|
|
||||||
|
<TextBlock Text="▶"/>
|
||||||
|
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Изображения -->
|
||||||
|
<Grid ColumnSpacing="5" RowSpacing="5">
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" /> <!-- Изображения -->
|
||||||
|
<RowDefinition Height="Auto" /> <!-- Кнопка "Получить изображения"/"Отменить съёмку" -->
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" /> <!-- Кнопка "Получить изображения"/"Отменить съёмку" -->
|
||||||
|
<ColumnDefinition Width="*" /> <!-- Кнопки "Изображения сепаратора"/"Изображения пробы" -->
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Изображения -->
|
||||||
|
<core:SectionPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
|
||||||
|
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||||
|
CanScrollBackward="{Binding ImagesSectionCanScrollBackward}"
|
||||||
|
CanScrollForward="{Binding ImagesSectionCanScrollForward}"
|
||||||
|
ClipToBounds="True"
|
||||||
|
CurrentIndex="{Binding ImagesSectionCurrentIndex}"
|
||||||
|
IsScrolling="{Binding ImagesSectionIsScrolling}"
|
||||||
|
Orientation="Vertical">
|
||||||
|
|
||||||
|
<!-- Изображения сепаратора -->
|
||||||
|
<Grid>
|
||||||
|
|
||||||
|
<Grid ColumnSpacing="5">
|
||||||
|
|
||||||
|
<Grid.IsVisible>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
|
||||||
|
<Binding
|
||||||
|
Converter="{StaticResource NotEqualsConverter}"
|
||||||
|
ConverterParameter="0"
|
||||||
|
Path="SeparatorImages.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 SeparatorImagesSectionPreviousCommand}">
|
||||||
|
|
||||||
|
<TextBlock Text="◀"/>
|
||||||
|
|
||||||
|
<Button.IsEnabled>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<Binding Path="SeparatorImagesSectionCanScrollBackward" />
|
||||||
|
<Binding Converter="{x:Static BoolConverters.Not}" Path="SeparatorImagesSectionIsScrolling" />
|
||||||
|
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesSectionIsScrolling" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsEnabled>
|
||||||
|
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Изображения -->
|
||||||
|
<core:SectionPanel
|
||||||
|
Grid.Column="1"
|
||||||
|
CanScrollBackward="{Binding SeparatorImagesSectionCanScrollBackward}"
|
||||||
|
CanScrollForward="{Binding SeparatorImagesSectionCanScrollForward}"
|
||||||
|
ChildrenSource="{Binding SeparatorImages}"
|
||||||
|
ClipToBounds="True"
|
||||||
|
CurrentIndex="{Binding SeparatorImagesSectionCurrentIndex}"
|
||||||
|
Cyclic="True"
|
||||||
|
IsScrolling="{Binding SeparatorImagesSectionIsScrolling}"
|
||||||
|
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" /> <!-- Интенсивность УФ 365 нм (подпись) -->
|
||||||
|
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 365 нм (поле) -->
|
||||||
|
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 254 нм (подпись) -->
|
||||||
|
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 254 нм (поле) -->
|
||||||
|
<RowDefinition Height="*" /> <!-- Пустое пространство -->
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Изображения -->
|
||||||
|
<Grid Grid.Row="0" Grid.RowSpan="12" Grid.Column="0">
|
||||||
|
|
||||||
|
<core:ZoomPanImage
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Background="Black"
|
||||||
|
x:DataType="core_vm:AsyncImageRecordViewModel"
|
||||||
|
ImageSource="{Binding Image}"
|
||||||
|
IsVisible="{Binding IsLoading, Converter={x:Static BoolConverters.Not}}" />
|
||||||
|
|
||||||
|
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" x:DataType="core_vm:AsyncImageRecordViewModel" IsVisible="{Binding IsLoading}">
|
||||||
|
<ProgressBar IsIndeterminate="True" />
|
||||||
|
<TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Информация -->
|
||||||
|
<!-- ИД (подпись) -->
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="1" Classes="mini" Text="ИД: " />
|
||||||
|
<!-- ИД (поле) -->
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.Id}" />
|
||||||
|
<!-- Интенсивность видимого (подпись) -->
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="1" Classes="mini" Text="Интенсивность видимого: " />
|
||||||
|
<!-- Интенсивность видимого (поле) -->
|
||||||
|
<TextBox Grid.Row="4" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.VisibleIntensity}" />
|
||||||
|
<!-- Интенсивность УФ 365 нм (подпись) -->
|
||||||
|
<TextBlock Grid.Row="5" Grid.Column="1" Classes="mini" Text="Интенсивность УФ 365 нм: " />
|
||||||
|
<!-- Интенсивность УФ 365 нм (поле) -->
|
||||||
|
<TextBox Grid.Row="6" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.Uv365Intensity}" />
|
||||||
|
<!-- Интенсивность УФ 254 нм (подпись) -->
|
||||||
|
<TextBlock Grid.Row="7" Grid.Column="1" Classes="mini" Text="Интенсивность УФ 254 нм: " />
|
||||||
|
<!-- Интенсивность УФ 254 нм (поле) -->
|
||||||
|
<TextBox Grid.Row="8" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" 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 SeparatorImagesSectionNextCommand}">
|
||||||
|
|
||||||
|
<TextBlock Text="▶"/>
|
||||||
|
|
||||||
|
<Button.IsEnabled>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<Binding Path="SeparatorImagesSectionCanScrollForward" />
|
||||||
|
<Binding Converter="{x:Static BoolConverters.Not}" Path="SeparatorImagesSectionIsScrolling" />
|
||||||
|
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesSectionIsScrolling" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsEnabled>
|
||||||
|
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="Изображения сепаратора отсутствуют">
|
||||||
|
|
||||||
|
<TextBlock.IsVisible>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
|
||||||
|
<Binding
|
||||||
|
Converter="{StaticResource EqualsConverter}"
|
||||||
|
ConverterParameter="0"
|
||||||
|
Path="SeparatorImages.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" Text="{Binding ImagesCapturingProgress}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Изображения пробы -->
|
||||||
|
<Grid>
|
||||||
|
|
||||||
|
<Grid ColumnSpacing="5">
|
||||||
|
|
||||||
|
<Grid.IsVisible>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
|
||||||
|
<Binding
|
||||||
|
Converter="{StaticResource NotEqualsConverter}"
|
||||||
|
ConverterParameter="0"
|
||||||
|
Path="SampleImages.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 SampleImagesSectionPreviousCommand}">
|
||||||
|
|
||||||
|
<TextBlock Text="◀"/>
|
||||||
|
|
||||||
|
<Button.IsEnabled>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<Binding Path="SampleImagesSectionCanScrollBackward" />
|
||||||
|
<Binding Converter="{x:Static BoolConverters.Not}" Path="SampleImagesSectionIsScrolling" />
|
||||||
|
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesSectionIsScrolling" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsEnabled>
|
||||||
|
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Изображения -->
|
||||||
|
<core:SectionPanel
|
||||||
|
Grid.Column="1"
|
||||||
|
CanScrollBackward="{Binding SampleImagesSectionCanScrollBackward}"
|
||||||
|
CanScrollForward="{Binding SampleImagesSectionCanScrollForward}"
|
||||||
|
ChildrenSource="{Binding SampleImages}"
|
||||||
|
ClipToBounds="True"
|
||||||
|
CurrentIndex="{Binding SampleImagesSectionCurrentIndex}"
|
||||||
|
Cyclic="True"
|
||||||
|
IsScrolling="{Binding SampleImagesSectionIsScrolling}"
|
||||||
|
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" /> <!-- Интенсивность УФ 365 нм (подпись) -->
|
||||||
|
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 365 нм (поле) -->
|
||||||
|
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 254 нм (подпись) -->
|
||||||
|
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 254 нм (поле) -->
|
||||||
|
<RowDefinition Height="*" /> <!-- Пустое пространство -->
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Изображения -->
|
||||||
|
<Grid Grid.Row="0" Grid.RowSpan="12" Grid.Column="0">
|
||||||
|
|
||||||
|
<core:ZoomPanImage
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Background="Black"
|
||||||
|
x:DataType="core_vm:AsyncImageRecordViewModel"
|
||||||
|
ImageSource="{Binding Image}"
|
||||||
|
IsVisible="{Binding IsLoading, Converter={x:Static BoolConverters.Not}}" />
|
||||||
|
|
||||||
|
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" x:DataType="core_vm:AsyncImageRecordViewModel" IsVisible="{Binding IsLoading}">
|
||||||
|
<ProgressBar IsIndeterminate="True" />
|
||||||
|
<TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Информация -->
|
||||||
|
<!-- ИД (подпись) -->
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="1" Classes="mini" Text="ИД: " />
|
||||||
|
<!-- ИД (поле) -->
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.Id}" />
|
||||||
|
<!-- Интенсивность видимого (подпись) -->
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="1" Classes="mini" Text="Интенсивность видимого: " />
|
||||||
|
<!-- Интенсивность видимого (поле) -->
|
||||||
|
<TextBox Grid.Row="4" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.VisibleIntensity}" />
|
||||||
|
<!-- Интенсивность УФ 365 нм (подпись) -->
|
||||||
|
<TextBlock Grid.Row="5" Grid.Column="1" Classes="mini" Text="Интенсивность УФ 365 нм: " />
|
||||||
|
<!-- Интенсивность УФ 365 нм (поле) -->
|
||||||
|
<TextBox Grid.Row="6" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.Uv365Intensity}" />
|
||||||
|
<!-- Интенсивность УФ 254 нм (подпись) -->
|
||||||
|
<TextBlock Grid.Row="7" Grid.Column="1" Classes="mini" Text="Интенсивность УФ 254 нм: " />
|
||||||
|
<!-- Интенсивность УФ 254 нм (поле) -->
|
||||||
|
<TextBox Grid.Row="8" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" 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 SampleImagesSectionNextCommand}">
|
||||||
|
|
||||||
|
<TextBlock Text="▶"/>
|
||||||
|
|
||||||
|
<Button.IsEnabled>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<Binding Path="SampleImagesSectionCanScrollForward" />
|
||||||
|
<Binding Converter="{x:Static BoolConverters.Not}" Path="SampleImagesSectionIsScrolling" />
|
||||||
|
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesSectionIsScrolling" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsEnabled>
|
||||||
|
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="Изображения пробы отсутствуют">
|
||||||
|
|
||||||
|
<TextBlock.IsVisible>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
|
||||||
|
<Binding
|
||||||
|
Converter="{StaticResource EqualsConverter}"
|
||||||
|
ConverterParameter="0"
|
||||||
|
Path="SampleImages.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" Text="{Binding ImagesCapturingProgress}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</core:SectionPanel>
|
||||||
|
|
||||||
|
<!-- Кнопка "Получить изображения" -->
|
||||||
|
<Button
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Command="{Binding CaptureImageCommand}"
|
||||||
|
IsEnabled="{Binding !ImagesSectionIsScrolling}"
|
||||||
|
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNull}}">
|
||||||
|
<TextBlock Text="Получить изображения"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Кнопка "Отменить съёмку" -->
|
||||||
|
<Button
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Command="{Binding CancelImageCapturingCommand}"
|
||||||
|
IsEnabled="{Binding !ImagesSectionIsScrolling}"
|
||||||
|
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
|
<TextBlock Text="Отменить съёмку"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Кнопка "Изображения сепаратора" -->
|
||||||
|
<Button
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Command="{Binding ImagesSectionSwitchCommand}"
|
||||||
|
IsEnabled="{Binding !ImagesSectionIsScrolling}"
|
||||||
|
IsVisible="{Binding ImagesSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=1}">
|
||||||
|
<TextBlock Text="Изображения сепаратора"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Кнопка "Изображения пробы" -->
|
||||||
|
<Button
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Command="{Binding ImagesSectionSwitchCommand}"
|
||||||
|
IsEnabled="{Binding !ImagesSectionIsScrolling}"
|
||||||
|
IsVisible="{Binding ImagesSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=0}">
|
||||||
|
<TextBlock Text="Изображения пробы"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</core:SectionPanel>
|
||||||
|
|
||||||
|
<!-- Кнопки главной панели -->
|
||||||
|
<!-- Удалить -->
|
||||||
|
<Button Grid.Row="1" Grid.Column="0" HorizontalAlignment="Stretch" Classes="danger" Command="{Binding DeleteRecordCommand}">
|
||||||
|
<TextBlock Text="Удалить"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Сохранить -->
|
||||||
|
<Button Grid.Row="1" Grid.Column="1" HorizontalAlignment="Stretch" Command="{Binding SaveRecordCommand}" Content="Сохранить">
|
||||||
|
<TextBlock Text="Сохранить"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- К информации -->
|
||||||
|
<Button
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="2"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Command="{Binding SwitchMainSectionCommand}"
|
||||||
|
Content="К информации"
|
||||||
|
IsVisible="{Binding MainSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=1}">
|
||||||
|
|
||||||
|
<TextBlock Text="К информации"/>
|
||||||
|
|
||||||
|
<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 MainSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=0}">
|
||||||
|
|
||||||
|
<TextBlock Text="К изображениям"/>
|
||||||
|
|
||||||
|
<Button.IsEnabled>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<Binding Path="MainSectionCanScrollForward" />
|
||||||
|
<Binding Converter="{x:Static BoolConverters.Not}" Path="MainSectionIsScrolling" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsEnabled>
|
||||||
|
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Полупрозрачный оверлей -->
|
||||||
|
<Border Background="{StaticResource Overlay}" IsEnabled="{Binding MenuOpened}" IsVisible="{Binding MenuOpened}">
|
||||||
|
<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 MenuOpened}"
|
||||||
|
IsVisible="{Binding MenuOpened}">
|
||||||
|
|
||||||
|
<Border.Transitions>
|
||||||
|
<Transitions>
|
||||||
|
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.25" />
|
||||||
|
</Transitions>
|
||||||
|
</Border.Transitions>
|
||||||
|
|
||||||
|
<Interaction.Behaviors>
|
||||||
|
|
||||||
|
<DataTriggerBehavior
|
||||||
|
Binding="{Binding MenuOpened}"
|
||||||
|
ComparisonCondition="Equal"
|
||||||
|
Value="True">
|
||||||
|
|
||||||
|
<ChangePropertyAction PropertyName="RenderTransform" Value="translateX(0px)" />
|
||||||
|
|
||||||
|
</DataTriggerBehavior>
|
||||||
|
|
||||||
|
<DataTriggerBehavior
|
||||||
|
Binding="{Binding MenuOpened}"
|
||||||
|
ComparisonCondition="Equal"
|
||||||
|
Value="False">
|
||||||
|
|
||||||
|
<ChangePropertyAction PropertyName="RenderTransform" Value="translateX(-320px)" />
|
||||||
|
|
||||||
|
</DataTriggerBehavior>
|
||||||
|
|
||||||
|
</Interaction.Behaviors>
|
||||||
|
|
||||||
|
<!-- Содержимое меню -->
|
||||||
|
<Grid RowSpacing="10" Margin="10">
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Button Grid.Row="0"
|
||||||
|
MinWidth="200"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Command="{Binding CreateRecordCommand}">
|
||||||
|
<TextBlock Text="Новая запись"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<ListBox Grid.Row="1" ItemsSource="{Binding Records}" SelectedItem="{Binding SelectedRecord}">
|
||||||
|
|
||||||
|
<ListBox.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListBox.ItemsPanel>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource MainBackground}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
</ListBox.Styles>
|
||||||
|
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border Margin="3" CornerRadius="5">
|
||||||
|
<TextBlock Classes="mini" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<Run Text="{Binding Id, StringFormat='[{0}] '}" />
|
||||||
|
<Run Text="{Binding SampleName}" />
|
||||||
|
</TextBlock>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</Border>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -435,7 +435,6 @@
|
|||||||
<Grid RowSpacing="10" Margin="10">
|
<Grid RowSpacing="10" Margin="10">
|
||||||
|
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|||||||
Reference in New Issue
Block a user