feat: SampleCalibration View and ViewModel

This commit is contained in:
2026-04-14 22:30:26 +03:00
parent 106bc74cbb
commit 93bcc6c692
11 changed files with 1543 additions and 1310 deletions
@@ -1,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 System.Diagnostics;
namespace GSS2.ViewModels.AmineContent;
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: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:SampleCalibrationViewModel">
<Grid>
<TextBlock Text="Калибровка проб" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<UserControl.Resources>
<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>
</UserControl>
@@ -435,7 +435,6 @@
<Grid RowSpacing="10" Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>