forked from amkovkov/GranuSightSoftware2
feat: SeparatorCalibration View and ViewModel
This commit is contained in:
@@ -775,7 +775,7 @@ public class AmineContentAnalyzerService
|
||||
p.First.VisibleIntensity != p.Second.VisibleIntensity ||
|
||||
p.First.Uv365Intensity != p.Second.Uv365Intensity ||
|
||||
p.First.Uv254Intensity != p.Second.Uv254Intensity))
|
||||
throw new Exception("Image records indencities to correlate with separator images intencities");
|
||||
throw new Exception("Image records intensities to correlate with separator images intensities");
|
||||
|
||||
var imagePaths = GetImageRecordsFilePaths(imageRecords);
|
||||
var roi = CalculateSeparatorRoi(imagePaths, 5, cancellationToken);
|
||||
@@ -867,7 +867,7 @@ public class AmineContentAnalyzerService
|
||||
return calibrationRecord;
|
||||
}
|
||||
|
||||
public async Task<(Mat result, Mat mask)> CalcaulateAmineContent(IEnumerable<ImageRecord> sampleImageRecords, IEnumerable<ImageRecord> separatorImageRecords, CalibrationRecord calibrationRecord, CancellationToken cancellationToken = default)
|
||||
public async Task<(Mat result, Mat mask)> CalculateAmineContent(IEnumerable<ImageRecord> sampleImageRecords, IEnumerable<ImageRecord> separatorImageRecords, CalibrationRecord calibrationRecord, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var sampleImagePaths = GetImageRecordsFilePaths(sampleImageRecords);
|
||||
@@ -986,10 +986,10 @@ public class AmineContentAnalyzerService
|
||||
}
|
||||
}
|
||||
|
||||
// Срезаем 99 проценталь (1% самых горячих пикселей), которые скорее всего являются шумом или загрязенением
|
||||
// Срезаем 99.9 процентиль (0.1% самых горячих пикселей), которые скорее всего являются шумом или загрязнением
|
||||
var oldMean = Cv2.Mean(result, sampleMask).Val0;
|
||||
values.Sort();
|
||||
float p99 = values[(int)(values.Length * 0.99)];
|
||||
float p99 = values[(int)(values.Length * 0.999)];
|
||||
Cv2.Threshold(result, result, p99, p99, ThresholdTypes.Trunc);
|
||||
var newMean = Cv2.Mean(result, sampleMask).Val0;
|
||||
var scale = oldMean / newMean;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Drawing;
|
||||
|
||||
using GSS2.Core.Extensions;
|
||||
using GSS2.Core.Hardware;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace GSS2.Core.Analysis.AmineContent;
|
||||
|
||||
public class AmineContentImageCapturerService
|
||||
{
|
||||
// Список интенсивностей освещения для каждого кадра
|
||||
public static readonly ReadOnlyCollection<(double visible, double uv365, double uv254)> IlluminatorIntensities =
|
||||
[
|
||||
(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)
|
||||
];
|
||||
|
||||
private readonly ILogger<AmineContentImageCapturerService> _logger;
|
||||
private readonly CameraService _camera;
|
||||
private readonly IlluminatorService _illuminator;
|
||||
private readonly LightsService _lights;
|
||||
|
||||
public AmineContentImageCapturerService(
|
||||
ILogger<AmineContentImageCapturerService> logger,
|
||||
CameraService camera,
|
||||
IlluminatorService illuminator,
|
||||
LightsService lights
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_camera = camera;
|
||||
_illuminator = illuminator;
|
||||
_lights = lights;
|
||||
}
|
||||
|
||||
public async Task CaptureImages(Action<(int i, double visible, double uv365, double uv254, Mat image)> callback, CancellationToken cancellationToken) =>
|
||||
await CaptureImages(async (data) => callback(data), cancellationToken);
|
||||
|
||||
public async Task CaptureImages(Func<(int i, double visible, double uv365, double uv254, Mat image), Task> callback, CancellationToken cancellationToken)
|
||||
{
|
||||
// Включаем осветитель
|
||||
_illuminator.TurnOn();
|
||||
|
||||
// Если ViewFinder запущен, то для съёмки изображений его нужно остановить
|
||||
// потом после съёмки снова запустить
|
||||
var needRestartViewFinder = _camera.ViewFinderStarted;
|
||||
if (needRestartViewFinder)
|
||||
_camera.StopViewFinder();
|
||||
|
||||
try
|
||||
{
|
||||
// Для каждой интенсивности
|
||||
foreach (var (i, (visible, uv365, uv254)) in IlluminatorIntensities.Enumerate())
|
||||
{
|
||||
// Выбрасываем исключение если задача отменена
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Логгируем прогресс
|
||||
_logger.LogInformation("Capturing images: {} / {} ({};{};{})", i + 1, IlluminatorIntensities.Count(), visible, uv365, uv254);
|
||||
// Отображаем прогресс на светодиодах
|
||||
_lights.Load(0.3, 1, (double)(i + 1) / IlluminatorIntensities.Count * 100, (double)i / IlluminatorIntensities.Count * 100, Color.Yellow);
|
||||
|
||||
// Устанавливаем интенсивность
|
||||
_illuminator.SetIntensity(visible, uv365, uv254);
|
||||
|
||||
// Получаем изображение
|
||||
Mat? image;
|
||||
image = await _camera.CaptureImage(cancellationToken, 2);
|
||||
if (image is null)
|
||||
throw new Exception("Image capture failed");
|
||||
|
||||
// Вызываем коллбек
|
||||
await callback.Invoke((i, visible, uv365, uv254, image));
|
||||
}
|
||||
|
||||
// Если ошибок не было, то мигаем светодиодами зелёным
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
_lights.Flash(1, 1, Color.Green);
|
||||
await Task.Delay(3000);
|
||||
_lights.Disconnect();
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ }
|
||||
catch
|
||||
{
|
||||
// Если была ошибка, то мигаем светодиодами красным
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
_lights.Flash(1, 1, Color.Red);
|
||||
await Task.Delay(3000);
|
||||
_lights.Disconnect();
|
||||
});
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// В любом случае отключаем осветитель
|
||||
_illuminator.TurnOff();
|
||||
// Запускаем обратно ViewFinder, если он был запущен и ещё не запущен обратно
|
||||
if (needRestartViewFinder && !_camera.ViewFinderStarted)
|
||||
_camera.StartViewFinder();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,32 @@ using Microsoft.IdentityModel.Protocols.Configuration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
|
||||
using GSS2.Core.Logging;
|
||||
using GSS2.Core.Hardware;
|
||||
using GSS2.Core.Analysis.AmineContent.Database.Calibration;
|
||||
using GSS2.Core.Analysis.AmineContent.Database.Results;
|
||||
using GSS2.Core.Analysis.AmineContent;
|
||||
|
||||
using ConsoleFormatter = GSS2.Core.Logging.ConsoleFormatter;
|
||||
|
||||
namespace GSS2.Core;
|
||||
|
||||
public static class DependencyInjectionHelper
|
||||
{
|
||||
public static IConfigurationBuilder AddAppSettingsJson(this IConfigurationBuilder configuration) =>
|
||||
configuration.AddJsonFile(Path.Join(Path.GetDirectoryName(Environment.ProcessPath), "appsettings.json"));
|
||||
|
||||
public static ILoggingBuilder ConfigureLogging(this ILoggingBuilder logging)
|
||||
{
|
||||
logging.ClearProviders();
|
||||
logging.AddConsole(options => options.FormatterName = nameof(ConsoleFormatter));
|
||||
logging.AddConsoleFormatter<ConsoleFormatter, ConsoleFormatterOptions>();
|
||||
logging.AddProvider(new FileLoggerProvider("full.log", true));
|
||||
logging.AddProvider(new FileLoggerProvider("last_run.log", false));
|
||||
return logging;
|
||||
}
|
||||
public static IServiceCollection AddTemperatureHumidityService(this IServiceCollection services, string section) => services
|
||||
.AddSingleton(
|
||||
serviceProvider =>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace GSS2.Core.Extensions;
|
||||
|
||||
public static class MatExtensions
|
||||
{
|
||||
public static void ImWrite(this Mat mat, string path)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
if (directory is not null && !Directory.Exists(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
mat.ImWrite(path);
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,7 @@ public class CameraService : IDisposable
|
||||
public bool IsImageCapturing { get; private set; } = false;
|
||||
public List<FrameBuffer> ViewFinderBufferQueue { get; private set; } = new List<FrameBuffer>();
|
||||
public CameraServiceConfiguration Configuration => _configuration;
|
||||
public bool ViewFinderStarted { get; private set; } = false;
|
||||
|
||||
public CameraService(ILogger<CameraService> logger, CameraServiceConfiguration? configuration = null, bool autoInitialize = false)
|
||||
{
|
||||
@@ -315,7 +316,7 @@ public class CameraService : IDisposable
|
||||
throw new Exception($"Error while camera start {result}");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Camera settings: \n{}", GetSettings(4));
|
||||
_logger.LogDebug("Camera settings: \n{}", GetSettings(4));
|
||||
|
||||
_state |= State.CameraStarted;
|
||||
}
|
||||
@@ -840,6 +841,8 @@ public class CameraService : IDisposable
|
||||
_viewFinderRequestCompletedHandler is not null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
ViewFinderStarted = true;
|
||||
|
||||
_viewFinderTimer = new System.Timers.Timer()
|
||||
{
|
||||
Interval = 1000 / _configuration.ViewFinderFps,
|
||||
@@ -919,9 +922,11 @@ public class CameraService : IDisposable
|
||||
}
|
||||
public void StopViewFinder()
|
||||
{
|
||||
ViewFinderStarted = false;
|
||||
_viewFinderTimer?.Stop();
|
||||
_viewFinderTimer?.Dispose();
|
||||
_viewFinderTimer = null;
|
||||
_viewFinderTimerElapsed = null;
|
||||
_camera?.RequestCompleted -= _viewFinderRequestCompletedHandler;
|
||||
_viewFinderRequestCompletedHandler = null;
|
||||
lock (ViewFinderBufferQueue)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
using GSS2.Core.Analysis.AmineContent.Database.Calibration;
|
||||
|
||||
namespace GSS2.UI.Core.ViewModels;
|
||||
@@ -14,14 +14,14 @@ public partial class AsyncImageRecordViewModel : ViewModelBase
|
||||
|
||||
private readonly string _path;
|
||||
|
||||
[ObservableProperty] private ImageRecord _imageRecord;
|
||||
[ObservableProperty] private IImage? _image;
|
||||
[ObservableProperty] private bool _isLoading = true;
|
||||
[ObservableProperty] public partial ImageRecord ImageRecord { get; set; }
|
||||
[ObservableProperty] public partial IImage? Image { get; set; }
|
||||
[ObservableProperty] public partial bool IsLoading { get; set; } = true;
|
||||
|
||||
public AsyncImageRecordViewModel(string path, ImageRecord imageRecord)
|
||||
{
|
||||
_path = path;
|
||||
_imageRecord = imageRecord;
|
||||
ImageRecord = imageRecord;
|
||||
_ = LoadAsync();
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
Orientation="Horizontal"
|
||||
Spacing="20">
|
||||
|
||||
<Slider Classes="horizontal" Value="{Binding WhiteIntensity}" />
|
||||
<Slider Classes="horizontal" Value="{Binding WhiteIntensity}" Maximum="0.03" />
|
||||
<Slider Classes="horizontal" Value="{Binding Uv365Intensity}" />
|
||||
<Slider Classes="horizontal" Value="{Binding Uv254Intensity}" />
|
||||
|
||||
@@ -53,8 +53,9 @@
|
||||
Margin="0,10,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Content="Включить"
|
||||
IsChecked="{Binding Enabled}" />
|
||||
IsChecked="{Binding Enabled}">
|
||||
<TextBlock Text="Включить" FontSize="18"/>
|
||||
</ToggleButton>
|
||||
|
||||
</Grid>
|
||||
|
||||
@@ -71,7 +72,7 @@
|
||||
Orientation="Vertical"
|
||||
Spacing="20">
|
||||
|
||||
<Slider Classes="vertical" Value="{Binding WhiteIntensity}" />
|
||||
<Slider Classes="vertical" Value="{Binding WhiteIntensity}" Maximum="0.03" />
|
||||
<Slider Classes="vertical" Value="{Binding Uv365Intensity}" />
|
||||
<Slider Classes="vertical" Value="{Binding Uv254Intensity}" />
|
||||
|
||||
@@ -82,8 +83,9 @@
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Content="Включить"
|
||||
IsChecked="{Binding Enabled}" />
|
||||
IsChecked="{Binding Enabled}">
|
||||
<TextBlock Text="Включить" FontSize="18"/>
|
||||
</ToggleButton>
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
+5
-3
@@ -3,12 +3,13 @@ using System.CommandLine;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.OpenGL.Egl;
|
||||
|
||||
using GSS2.Core.Logging;
|
||||
using GSS2.Core;
|
||||
using GSS2.Core.Logging;
|
||||
using GSS2.Core.Analysis.AmineContent.Database.Calibration;
|
||||
using GSS2.Core.Analysis.AmineContent.Database.Results;
|
||||
|
||||
@@ -157,7 +158,8 @@ public static class UI
|
||||
builder.Services.AddIlluminatorService("Hardware:Illuminator");
|
||||
builder.Services.AddCameraService("Hardware:Camera", true);
|
||||
builder.Services.AddComputeResourcesService();
|
||||
builder.Services.AddImageStorageService(new DirectoryInfo("images"));
|
||||
builder.Services.AddImageStorageService(new DirectoryInfo(builder.Configuration.GetValue<string>("ImageStorage") ?? "images"));
|
||||
builder.Services.AddAmineContentImageCapturerService();
|
||||
|
||||
// Добавляем элементы интерфейса как сервисы
|
||||
builder.Services.AddViewsAndModels();
|
||||
@@ -191,7 +193,7 @@ public static class UI
|
||||
.LogToTrace()
|
||||
.With(new X11PlatformOptions
|
||||
{
|
||||
RenderingMode = [X11RenderingMode.Egl]
|
||||
RenderingMode = [X11RenderingMode.Egl],
|
||||
})
|
||||
.With(new EglDisplayOptions
|
||||
{
|
||||
|
||||
@@ -1,11 +1,446 @@
|
||||
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;
|
||||
|
||||
namespace GSS2.ViewModels.AmineContent;
|
||||
|
||||
public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
{
|
||||
public SeparatorCalibrationViewModel()
|
||||
{
|
||||
private readonly ILogger<SeparatorCalibrationViewModel> _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<SeparatorRecord> Records { get; set; }
|
||||
|
||||
[ObservableProperty] public partial SeparatorRecord? SelectedRecord { get; set; }
|
||||
[ObservableProperty] public partial SeparatorRecord EditingRecord { get; set; }
|
||||
|
||||
[ObservableProperty] public partial int MainSectionsCurrentIndex { 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 Task? CaptureImagesTask { get; set; } = null;
|
||||
|
||||
[ObservableProperty] public partial ObservableCollection<AsyncImageRecordViewModel> Images { get; set; } = new();
|
||||
[ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false;
|
||||
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
|
||||
[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;
|
||||
|
||||
public SeparatorCalibrationViewModel(
|
||||
ILogger<SeparatorCalibrationViewModel> logger,
|
||||
AmineContentCalibrationContext context,
|
||||
AmineContentImageCapturerService imageCapturer,
|
||||
ImageStorageService imageStorage
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_context = context;
|
||||
_imageCapturer = imageCapturer;
|
||||
_imageStorage = imageStorage;
|
||||
_logger.LogInformation("Инициализация");
|
||||
|
||||
// Получаем записи из базы данных
|
||||
Records = new ObservableCollection<SeparatorRecord>(_context.SeparatorRecords.Include(r => r.Images));
|
||||
|
||||
// Берём первую запись
|
||||
SelectedRecord = Records.FirstOrDefault();
|
||||
// Если записей нет, то создаём пустую
|
||||
// на самом деле это излишне, потому что при изменении SelectedRecord
|
||||
// вызовется OnSelectedRecordChanged который сделает то же самое, но сделано это для избавления от предупреждений
|
||||
if (SelectedRecord is null)
|
||||
EditingRecord = new()
|
||||
{
|
||||
Type = "",
|
||||
Batch = "",
|
||||
Images = 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(SeparatorRecord? value)
|
||||
{
|
||||
// Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись)
|
||||
// то создаём пустую запись
|
||||
// TODO: Подумать над тем, чтобы сразу удалять несохранённые изображения из хранилища
|
||||
if (SelectedRecord is null)
|
||||
{
|
||||
EditingRecord = new()
|
||||
{
|
||||
Type = "",
|
||||
Batch = "",
|
||||
Images = new()
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Иначе копируем запись, отвязывая её от базы данных
|
||||
EditingRecord = CopyRecord(SelectedRecord);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вызывается при изменении <see cref="EditingRecord"/>.<br/>
|
||||
/// Заполняет <see cref="Images"/> изображениями из <see cref="EditingRecord"/>.
|
||||
/// </summary>
|
||||
partial void OnEditingRecordChanged(SeparatorRecord value)
|
||||
{
|
||||
var images = value.Images
|
||||
// Для каждого изображения
|
||||
.Select(i =>
|
||||
{
|
||||
// Находим путь через ImageStorageService
|
||||
var path = _imageStorage.GetFullPath(i.ImagePath, "separator");
|
||||
// Если изображения не найдено, возвращаем null
|
||||
if (path is null)
|
||||
return null;
|
||||
// Создаём AsyncImageRecordViewModel
|
||||
return new AsyncImageRecordViewModel(path, i);
|
||||
})
|
||||
// Оставляем только не null значения
|
||||
.OfType<AsyncImageRecordViewModel>();
|
||||
|
||||
// Создаём новую коллекцию изображений
|
||||
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой
|
||||
Images.Clear();
|
||||
Images = 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.SeparatorRecords.Remove(deletedRecord);
|
||||
_context.SaveChanges();
|
||||
}
|
||||
|
||||
// Команда сохранения записи, сохраняет новую запись или изменения в отредактированной
|
||||
[RelayCommand]
|
||||
private void SaveRecord()
|
||||
{
|
||||
// Если это новая запись
|
||||
if (SelectedRecord is null)
|
||||
{
|
||||
// Если это новая запись, то копируем её
|
||||
var newRecord = CopyRecord(EditingRecord);
|
||||
|
||||
// Для каждого изображения в записи
|
||||
for (int i = 0; i < newRecord.Images.Count(); i++)
|
||||
{
|
||||
var image = newRecord.Images[i];
|
||||
// Если изображение есть в базе данных, то пропускаем его
|
||||
if (_context.Entry(image).State is not EntityState.Detached)
|
||||
continue;
|
||||
|
||||
// Если изображения нет в базе данных, то добавляем его в базу
|
||||
var attachedImage = _context.ImageRecords.Add(image);
|
||||
// Удаляем отвязанное от базы данных изображение из записи
|
||||
newRecord.Images.RemoveAt(i);
|
||||
// Добавляем привязанное изображение к записи
|
||||
newRecord.Images.Insert(i, attachedImage.Entity);
|
||||
}
|
||||
// Если записи нет в базе данных
|
||||
if (_context.Entry(newRecord).State is EntityState.Detached)
|
||||
{
|
||||
// То добавляем её в базу
|
||||
var attachedRecord = _context.SeparatorRecords.Add(newRecord);
|
||||
// Заменяем текущую запись на привязанную к базе данных
|
||||
newRecord = attachedRecord.Entity;
|
||||
}
|
||||
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
// Добавляем запись в список
|
||||
Records.Add(newRecord);
|
||||
// Выбираем запись
|
||||
SelectedRecord = newRecord;
|
||||
}
|
||||
// Если эта запись есть в базе
|
||||
else
|
||||
{
|
||||
// Переносим данные из редактируемой записи в выбранную
|
||||
SelectedRecord.Type = EditingRecord.Type;
|
||||
SelectedRecord.Batch = EditingRecord.Batch;
|
||||
|
||||
// Находим изображения которые нужно удалить -
|
||||
// те которые есть в выбранной записи, но нет в редактируемой
|
||||
var imagesToRemove = SelectedRecord.Images
|
||||
.Where(e => EditingRecord.Images.All(i => i.Id != e.Id))
|
||||
.ToList();
|
||||
|
||||
// Удаляем старые изображения
|
||||
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища
|
||||
foreach (var image in imagesToRemove)
|
||||
SelectedRecord.Images.Remove(image);
|
||||
|
||||
// Для каждого изображения в редактируемой записи
|
||||
foreach (var editedImage in EditingRecord.Images)
|
||||
{
|
||||
// Находим изображение в выбранной записи (оно будет привязано к базе данных)
|
||||
var existing = SelectedRecord.Images
|
||||
.FirstOrDefault(i => i.Id == editedImage.Id);
|
||||
|
||||
// Если изображение не найдено
|
||||
if (existing is null)
|
||||
{
|
||||
// Копируем изображение в выбранную запись
|
||||
SelectedRecord.Images.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.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 ImagesSectionPrevious() => ImagesSectionCurrentIndex--;
|
||||
[RelayCommand] private void ImagesSectionNext() => ImagesSectionCurrentIndex++;
|
||||
|
||||
// Команда съёмки изображений
|
||||
[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;
|
||||
using (var stream = new MemoryStream(bytes))
|
||||
fileName = await _imageStorage.SaveAsync(stream, ".png", "separator", cancellationToken);
|
||||
|
||||
// Освобождаем память изображения
|
||||
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
|
||||
var path = _imageStorage.GetFullPath(i.ImagePath, "separator");
|
||||
// Если изображения не найдено, возвращаем null
|
||||
if (path is null)
|
||||
return null;
|
||||
// Создаём AsyncImageRecordViewModel
|
||||
return new AsyncImageRecordViewModel(path, i);
|
||||
})
|
||||
// Оставляем только не null значения
|
||||
.OfType<AsyncImageRecordViewModel>();
|
||||
|
||||
// Обновляем данные в редактируемой коллекции
|
||||
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
EditingRecord.Images = imageRecords;
|
||||
// Создаём новую коллекцию изображений
|
||||
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой
|
||||
Images = new(images);
|
||||
});
|
||||
|
||||
// Сбрасываем состояние задачи съёмки изображений
|
||||
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
ImagesIsCapturing = false;
|
||||
ImagesCapturingProgress = "";
|
||||
CaptureImagesTask = null;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Команда отмены задачи съёмки изображений
|
||||
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
|
||||
|
||||
/// <summary>
|
||||
/// Копирует <see cref="SeparatorRecord"/>, отвязывая её от базы данных
|
||||
/// </summary>
|
||||
/// <returns>Скопированная <see cref="SeparatorRecord"/></returns>
|
||||
private static SeparatorRecord CopyRecord(SeparatorRecord source)
|
||||
{
|
||||
return new SeparatorRecord
|
||||
{
|
||||
Id = source.Id,
|
||||
Type = source.Type,
|
||||
Batch = source.Batch,
|
||||
Images = source.Images
|
||||
.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,493 @@
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
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"
|
||||
x:DataType="vm:SeparatorCalibrationViewModel">
|
||||
|
||||
<Grid>
|
||||
<TextBlock Text="Калибровка сепаратора" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<UserControl.Resources>
|
||||
<converters:NotEqualsConverter x:Key="NotEqualsConverter" />
|
||||
<converters:EqualsConverter x:Key="EqualsConverter" />
|
||||
|
||||
<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" />
|
||||
</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 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" /> <!-- Тип (поле) -->
|
||||
<RowDefinition Height="Auto" /> <!-- Партия (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- Партия (поле) -->
|
||||
<RowDefinition Height="*" /> <!-- Пустое пространство -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" /> <!-- Кнопка меню -->
|
||||
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Кнопка меню -->
|
||||
<Button Grid.Column="0" Grid.Row="0" Grid.RowSpan="7" Classes="vertical" VerticalAlignment="Stretch" Command="{Binding ToggleMenuCommand}">
|
||||
<TextBlock Text="☰"/>
|
||||
</Button>
|
||||
|
||||
<!-- Информация -->
|
||||
<!-- ИД (подпись) -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Classes="mini" Text="ИД:" />
|
||||
<!-- ИД (поле) -->
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Id}" PlaceholderText="ИД" IsReadOnly="True" />
|
||||
<!-- Тип (подпись) -->
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" Classes="mini" Text="Тип:" />
|
||||
<!-- Тип (поле) -->
|
||||
<TextBox Grid.Row="3" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Type}" PlaceholderText="Тип" />
|
||||
<!-- Партия (подпись) -->
|
||||
<TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Партия:" />
|
||||
<!-- Партия (поле) -->
|
||||
<TextBox Grid.Row="5" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Batch}" PlaceholderText="Партия" />
|
||||
|
||||
</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}">
|
||||
|
||||
<TextBlock Text="◀"/>
|
||||
|
||||
<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" /> <!-- Интенсивность УФ 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 ImagesSectionNextCommand}">
|
||||
|
||||
<TextBlock Text="▶"/>
|
||||
|
||||
<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"
|
||||
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" Text="{Binding ImagesCapturingProgress}" />
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Кнопка "получить изображения" -->
|
||||
<Button
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding CaptureImageCommand}"
|
||||
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNull}}">
|
||||
<TextBlock Text="Получить изображения"/>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding CancelImageCapturingCommand}"
|
||||
Content="Отменить съёмку"
|
||||
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<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 MainSectionsCurrentIndex, 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 MainSectionsCurrentIndex, 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="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 Type}" />
|
||||
<Run Text=": " />
|
||||
<Run Text="{Binding Batch}" />
|
||||
</TextBlock>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
|
||||
</ListBox>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
|
||||
Reference in New Issue
Block a user