forked from amkovkov/GranuSightSoftware2
feat: amine content separator calibration db editor
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
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()
|
||||
// {
|
||||
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
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;
|
||||
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 _currentRecord;
|
||||
[ObservableProperty] private bool _isMenuOpen = false;
|
||||
[ObservableProperty] private bool _isNewRecord = false;
|
||||
|
||||
[ObservableProperty] private int _mainSectionsCurrentIndex = 0;
|
||||
[ObservableProperty] private string _switchMainSectionButtonText = "К изображениям";
|
||||
|
||||
[ObservableProperty] private Task? _captureImagesTask = null;
|
||||
|
||||
[ObservableProperty] private ObservableCollection<KeyValuePair<IImage, ImageRecord>> _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;
|
||||
|
||||
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));
|
||||
|
||||
if (Records.Count() > 0)
|
||||
{
|
||||
CurrentRecord = Records.First();
|
||||
IsNewRecord = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentRecord = new SeparatorRecord()
|
||||
{
|
||||
Type = "",
|
||||
Batch = "",
|
||||
Images = new()
|
||||
};
|
||||
IsNewRecord = true;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Initialized");
|
||||
}
|
||||
|
||||
partial void OnRecordsChanged(ObservableCollection<SeparatorRecord>? oldValue, ObservableCollection<SeparatorRecord> newValue)
|
||||
{
|
||||
oldValue?.CollectionChanged -= OnRecordsCollectionChanged;
|
||||
newValue?.CollectionChanged += OnRecordsCollectionChanged;
|
||||
}
|
||||
partial void OnCurrentRecordChanged(SeparatorRecord value)
|
||||
{
|
||||
var images = value.Images
|
||||
.ToAsyncEnumerable()
|
||||
.Select(i => new KeyValuePair<string?, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "separator"), i))
|
||||
.Where(kv => kv.Key is not null)
|
||||
.Select(kv => new KeyValuePair<IImage, ImageRecord>(new Bitmap(kv.Key!), kv.Value));
|
||||
|
||||
Images = new(images.ToBlockingEnumerable());
|
||||
|
||||
IsNewRecord = _context.Entry(value).State == EntityState.Detached;
|
||||
}
|
||||
|
||||
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 SeparatorRecord()
|
||||
{
|
||||
Type = "",
|
||||
Batch = "",
|
||||
Images = new()
|
||||
};
|
||||
CurrentRecord = record;
|
||||
IsNewRecord = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DeleteRecord()
|
||||
{
|
||||
var deletedRecord = CurrentRecord;
|
||||
var deletedIsNewRecord = IsNewRecord;
|
||||
IsNewRecord = true;
|
||||
CreateRecord();
|
||||
|
||||
if (!deletedIsNewRecord)
|
||||
{
|
||||
Records.Remove(deletedRecord);
|
||||
_context.SeparatorRecords.Remove(deletedRecord);
|
||||
_context.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SaveRecord()
|
||||
{
|
||||
for (int i = 0; i < CurrentRecord.Images.Count(); i++)
|
||||
{
|
||||
var image = CurrentRecord.Images[i];
|
||||
if (_context.Entry(image).State == EntityState.Detached)
|
||||
{
|
||||
var attachedImage = _context.ImageRecords.Add(image);
|
||||
CurrentRecord.Images.RemoveAt(i);
|
||||
CurrentRecord.Images.Insert(i, attachedImage.Entity);
|
||||
}
|
||||
}
|
||||
if (_context.Entry(CurrentRecord).State == EntityState.Detached)
|
||||
{
|
||||
var attachedRecord = _context.SeparatorRecords.Add(CurrentRecord);
|
||||
CurrentRecord = attachedRecord.Entity;
|
||||
}
|
||||
if (IsNewRecord)
|
||||
Records.Add(CurrentRecord);
|
||||
|
||||
_context.SaveChanges();
|
||||
IsNewRecord = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SwitchMainSection()
|
||||
{
|
||||
MainSectionsCurrentIndex = MainSectionsCurrentIndex == 0 ? 1 : 0;
|
||||
SwitchMainSectionButtonText = MainSectionsCurrentIndex == 0 ?
|
||||
"К изображениям" :
|
||||
"К информации";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CaptureImage()
|
||||
{
|
||||
if (CaptureImagesTask is not null)
|
||||
return;
|
||||
|
||||
_captureImagesTaskCts = new CancellationTokenSource();
|
||||
var cancellationToken = _captureImagesTaskCts.Token;
|
||||
|
||||
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(() => ImagesIsCapturing = true);
|
||||
|
||||
try
|
||||
{
|
||||
var imageRecords = new List<ImageRecord>();
|
||||
|
||||
foreach (var (i, intensity) in intensities.Enumerate())
|
||||
{
|
||||
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
|
||||
.ToAsyncEnumerable()
|
||||
.Select(i => new KeyValuePair<string?, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "separator"), i))
|
||||
.Where(kv => kv.Key is not null)
|
||||
.Select(kv => new KeyValuePair<IImage, ImageRecord>(new Bitmap(kv.Key!), kv.Value));
|
||||
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
CurrentRecord.Images.Clear();
|
||||
CurrentRecord.Images.AddRange(imageRecords);
|
||||
|
||||
Images.Clear();
|
||||
Images = new(images.ToBlockingEnumerable());
|
||||
});
|
||||
}
|
||||
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);
|
||||
|
||||
CaptureImagesTask.Start();
|
||||
}
|
||||
|
||||
[RelayCommand] private void CencelImageCapturing() => _captureImagesTaskCts?.Cancel();
|
||||
[RelayCommand] private void ImagesSectionPrevious() => ImagesSectionCurrentIndex--;
|
||||
[RelayCommand] private void ImagesSectionNext() => ImagesSectionCurrentIndex++;
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
<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 CurrentRecord.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"
|
||||
ClipToBounds="True"
|
||||
CurrentIndex="{Binding MainSectionsCurrentIndex}"
|
||||
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 CurrentRecord.Type}"
|
||||
Watermark="Тип" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Classes="mini"
|
||||
Text="Партия:" />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="3"
|
||||
Classes="singleline"
|
||||
Text="{Binding CurrentRecord.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="◀"
|
||||
IsEnabled="{Binding ImagesSectionCanScrollBackward}" />
|
||||
|
||||
<core:SectionPanel
|
||||
Grid.Column="1"
|
||||
CanScrollBackward="{Binding ImagesSectionCanScrollBackward}"
|
||||
CanScrollForward="{Binding ImagesSectionCanScrollForward}"
|
||||
ChildrenSource="{Binding Images}"
|
||||
ClipToBounds="True"
|
||||
CurrentIndex="{Binding ImagesSectionCurrentIndex}"
|
||||
Cyclic="True"
|
||||
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>
|
||||
|
||||
<core:ZoomPanImage
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="11"
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Black"
|
||||
ImageSource="{Binding Key}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Text="Id: " />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Classes="singleline"
|
||||
IsReadOnly="True"
|
||||
Text="{Binding Value.Id}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Text="Имя: " />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Classes="singleline"
|
||||
IsReadOnly="True"
|
||||
Text="{Binding Value.ImagePath}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Text="Интенсивность видимого: " />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="5"
|
||||
Grid.Column="1"
|
||||
Classes="singleline"
|
||||
IsReadOnly="True"
|
||||
Text="{Binding Value.VisibleIntensity}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="6"
|
||||
Grid.Column="1"
|
||||
Text="Интенсивность УФ 365 нм: " />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="7"
|
||||
Grid.Column="1"
|
||||
Classes="singleline"
|
||||
IsReadOnly="True"
|
||||
Text="{Binding Value.Uv365Intensity}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="8"
|
||||
Grid.Column="1"
|
||||
Text="Интенсивность УФ 254 нм: " />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="9"
|
||||
Grid.Column="1"
|
||||
Classes="singleline"
|
||||
IsReadOnly="True"
|
||||
Text="{Binding Value.Uv254Intensity}" />
|
||||
|
||||
</Grid>
|
||||
|
||||
</DataTemplate>
|
||||
</core:SectionPanel.ChildrenTemplate>
|
||||
|
||||
</core:SectionPanel>
|
||||
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Classes="vertical"
|
||||
Command="{Binding ImagesSectionNextCommand}"
|
||||
Content="▶"
|
||||
IsEnabled="{Binding ImagesSectionCanScrollForward}" />
|
||||
|
||||
</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>
|
||||
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
IsVisible="{Binding ImagesIsCapturing}"
|
||||
Text="{Binding ImagesCapturingProgress}" />
|
||||
|
||||
</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 CencelImageCapturingCommand}"
|
||||
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="{Binding SwitchMainSectionButtonText}" />
|
||||
|
||||
</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 CurrentRecord}">
|
||||
|
||||
<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>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace GSS2.UI.Core.Views;
|
||||
|
||||
public partial class AmineContentSeparatorCalibrationView : UserControl
|
||||
{
|
||||
public AmineContentSeparatorCalibrationView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user