forked from amkovkov/GranuSightSoftware2
feat: fix separator calibration after database reforge
+ add comments + refactoring
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Data;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -8,9 +9,10 @@ using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
using GSS2.Core;
|
||||
using GSS2.Core.Analysis.AmineContent;
|
||||
using GSS2.Core.Analysis.AmineContent.Database.Calibration;
|
||||
using GSS2.Core.Analysis.AmineContent.Database;
|
||||
using GSS2.Core.Extensions;
|
||||
|
||||
using GSS2.UI.Core.ViewModels;
|
||||
using GSS2.UI.Core.Services;
|
||||
|
||||
@@ -18,268 +20,519 @@ namespace GSS2.ViewModels.AmineContent;
|
||||
|
||||
public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Внутренняя модель элементов меню
|
||||
/// </summary>
|
||||
public partial class MenuItemViewModel : ViewModelBase
|
||||
{
|
||||
[ObservableProperty] public partial int Id { get; set; }
|
||||
[ObservableProperty] public partial string Type { get; set; }
|
||||
|
||||
public MenuItemViewModel(int id, string type)
|
||||
{
|
||||
Id = id;
|
||||
Type = type;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Логгер
|
||||
/// </summary>
|
||||
private readonly ILogger<SeparatorCalibrationViewModel> _logger;
|
||||
private readonly CalibrationContext _context;
|
||||
private readonly ImageCapturerService _imageCapturer;
|
||||
private readonly ImageStorageService _imageStorage;
|
||||
/// <summary>
|
||||
/// Контекст базы данных
|
||||
/// </summary>
|
||||
private readonly Context _context;
|
||||
/// <summary>
|
||||
/// Сервис для получения изображений с камеры
|
||||
/// </summary>
|
||||
private readonly IImageCapturerService _imageCapturer;
|
||||
/// <summary>
|
||||
/// ViewLocator для получения других View по их моделям
|
||||
/// </summary>
|
||||
private readonly DependencyInjectionViewLocator _viewLocator;
|
||||
/// <summary>
|
||||
/// Сервис навигации между экранами
|
||||
/// </summary>
|
||||
private readonly NavigationService _navigation;
|
||||
|
||||
/// <summary>
|
||||
/// Задача съёмки изображений
|
||||
/// </summary>
|
||||
private Task? _сaptureImagesTask = null;
|
||||
/// <summary>
|
||||
/// Токен отмены задачи съёмки изображений
|
||||
/// </summary>
|
||||
private CancellationTokenSource? _captureImagesTaskCts = null;
|
||||
|
||||
/// <summary>
|
||||
/// Данные <see cref="SeparatorRecord.ImageId"/> выбранного элемента
|
||||
/// </summary>
|
||||
private string? _selectedImageData = null;
|
||||
/// <summary>
|
||||
/// Данные <see cref="SeparatorRecord.FeatureIds"/> выбранного элемента
|
||||
/// </summary>
|
||||
private List<float[]> _selectedFeaturesData = new();
|
||||
|
||||
/// <summary>
|
||||
/// Меню открыто
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool MenuOpened { get; set; } = false;
|
||||
[ObservableProperty] public partial ObservableCollection<SeparatorRecord> Records { get; set; }
|
||||
/// <summary>
|
||||
/// Элементы меню
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial ObservableCollection<MenuItemViewModel> MenuItems { get; set; } = new();
|
||||
/// <summary>
|
||||
/// Текущий выбранный элемент меню. Равен -1 если элемент не выбран
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1;
|
||||
|
||||
[ObservableProperty] public partial SeparatorRecord? SelectedRecord { get; set; }
|
||||
[ObservableProperty] public partial SeparatorRecord EditingRecord { get; set; }
|
||||
/// <summary>
|
||||
/// <see cref="SeparatorRecord.Id"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial int SelectedId { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// <see cref="SeparatorRecord.Uuid"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString();
|
||||
/// <summary>
|
||||
/// <see cref="SeparatorRecord.Type"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SelectedType { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Представление <see cref="SeparatorRecord.ImageId"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial AsyncImageRecordViewModel SelectedImage { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Текущий экран главной секции
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial int MainSectionCurrentIndex { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// Можно ли переходить на следующий экран главной секции
|
||||
/// </summary>
|
||||
[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;
|
||||
/// <summary>
|
||||
/// Можно ли переходить на предыдущий экран главной секции
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = false;
|
||||
/// <summary>
|
||||
/// Происходит ли переход между экранами главной секции
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Проводится ли съёмка изображений
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false;
|
||||
/// <summary>
|
||||
/// Прогресс съёмки изображений
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
|
||||
|
||||
[ObservableProperty] public partial AsyncImageRecordViewModel? Image { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Можно ли производить перемещение между экранами
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool CanScroll { get; set; }
|
||||
|
||||
public SeparatorCalibrationViewModel(
|
||||
ILogger<SeparatorCalibrationViewModel> logger,
|
||||
CalibrationContext context,
|
||||
ImageCapturerService imageCapturer,
|
||||
ImageStorageService imageStorage,
|
||||
Context context,
|
||||
IImageCapturerService imageCapturer,
|
||||
DependencyInjectionViewLocator viewLocator,
|
||||
NavigationService navigation
|
||||
)
|
||||
{
|
||||
// Устанавливаем readonly поля
|
||||
_logger = logger;
|
||||
_context = context;
|
||||
_imageCapturer = imageCapturer;
|
||||
_imageStorage = imageStorage;
|
||||
_viewLocator = viewLocator;
|
||||
_navigation = navigation;
|
||||
_logger.LogInformation("Инициализация");
|
||||
|
||||
// Получаем записи из базы данных
|
||||
Records = new ObservableCollection<SeparatorRecord>(_context.SeparatorRecords);
|
||||
// Получаем записи меню из базы данных
|
||||
MenuItems = new ObservableCollection<MenuItemViewModel>(_context.SeparatorRecords.Select(r => new MenuItemViewModel(r.Id, r.Type)));
|
||||
if (MenuItems.Count() > 0) // Если записи есть,
|
||||
MenuSelectedIndex = 0; // Выбираем первую запись
|
||||
else // Если записей нет,
|
||||
MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана
|
||||
|
||||
// Берём первую запись
|
||||
SelectedRecord = Records.FirstOrDefault();
|
||||
// Если записей нет, то создаём пустую
|
||||
// на самом деле это излишне, потому что при изменении SelectedRecord
|
||||
// вызовется OnSelectedRecordChanged который сделает то же самое, но сделано это для избавления от предупреждений
|
||||
if (SelectedRecord is null)
|
||||
EditingRecord = new()
|
||||
// Подписываемся на изменения свойств для обновления CanScroll
|
||||
PropertyChanged += (_, ea) =>
|
||||
{
|
||||
Type = "",
|
||||
Batch = "",
|
||||
Image = "",
|
||||
VectorRecords = new()
|
||||
if (ea.PropertyName == nameof(MenuOpened) ||
|
||||
ea.PropertyName == nameof(MainSectionIsScrolling))
|
||||
UpdateCanScroll();
|
||||
};
|
||||
else
|
||||
EditingRecord = CopyRecord(SelectedRecord);
|
||||
|
||||
_logger.LogInformation("Инициализировано");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вызывается при изменении <see cref="SelectedRecord"/>.<br/>
|
||||
/// Копирует <see cref="SelectedRecord"/> в <see cref="EditingRecord"/> или издаёт пустую <see cref="EditingRecord"/>.
|
||||
/// Вызывается при изменении <see cref="MenuSelectedIndex"/>
|
||||
/// </summary>
|
||||
partial void OnSelectedRecordChanged(SeparatorRecord? value)
|
||||
partial void OnMenuSelectedIndexChanged(int value)
|
||||
{
|
||||
// Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись)
|
||||
// то создаём пустую запись
|
||||
if (SelectedRecord is null)
|
||||
// Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку
|
||||
if (value >= MenuItems.Count())
|
||||
_logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(MenuSelectedIndex), MenuSelectedIndex, nameof(MenuItems), MenuItems.Count());
|
||||
// Если значение больше или равно 0, то значит выбран один из элементов меню
|
||||
if (value >= 0 && value < MenuItems.Count())
|
||||
{
|
||||
EditingRecord = new()
|
||||
_logger.LogTrace("Выбран элемент из списка: {}", value);
|
||||
// Находим запись в базе данных
|
||||
var selectedRecord = _context.SeparatorRecords.Find(MenuItems[value].Id);
|
||||
// Если запись найдена, то заполняем поля из базы данных
|
||||
if (selectedRecord is not null)
|
||||
{
|
||||
Type = "",
|
||||
Batch = "",
|
||||
Image = "",
|
||||
VectorRecords = new()
|
||||
};
|
||||
// Заполняем простые поля
|
||||
SelectedId = selectedRecord.Id;
|
||||
SelectedUuid = selectedRecord.Uuid;
|
||||
SelectedType = selectedRecord.Type;
|
||||
|
||||
// Ищем изображение в базе данных
|
||||
var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId);
|
||||
// Если изображение не найдено в базе данных, то логгируем как ошибку
|
||||
if (imageRecord is null)
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords));
|
||||
// В любом случае заполняем изображение, оно заполнится null если запись не найдена
|
||||
_selectedImageData = imageRecord?.Base64;
|
||||
SelectedImage.Data = _selectedImageData;
|
||||
|
||||
// Ищем все признаки в базе данных
|
||||
_selectedFeaturesData = _context.FeatureRecords
|
||||
.Where(r => r.SeparatorRecordId == SelectedId)
|
||||
.Select(r => r.Values.ToArray())
|
||||
.ToList();
|
||||
|
||||
return;
|
||||
}
|
||||
// Если значение не найдено в базе данных
|
||||
else
|
||||
{
|
||||
// Иначе копируем запись, отвязывая её от базы данных
|
||||
EditingRecord = CopyRecord(SelectedRecord);
|
||||
// Логгируем как ошибку
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.BrandRecords));
|
||||
var nonExistingIndex = MenuSelectedIndex; // Сохраняем несуществующий индекс
|
||||
// Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями
|
||||
MenuSelectedIndex = -1;
|
||||
// Удаляем несуществующий элемент из списка
|
||||
MenuItems.RemoveAt(nonExistingIndex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Если элемент не выбран, заполняем поля пустыми значениями
|
||||
_logger.LogTrace("Выбран новый элемент");
|
||||
SelectedId = 0;
|
||||
SelectedUuid = Guid.NewGuid().ToString();
|
||||
SelectedType = "";
|
||||
SelectedImage.Data = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обновить значение <see cref="CanScroll"/>
|
||||
/// </summary>
|
||||
private void UpdateCanScroll()
|
||||
{
|
||||
CanScroll = !MenuOpened;
|
||||
CanScroll &= !MainSectionIsScrolling;
|
||||
CanScroll &= !ImagesIsCapturing;
|
||||
}
|
||||
|
||||
// Команды управления меню
|
||||
/// <summary>
|
||||
/// Открыть меню
|
||||
/// </summary>
|
||||
[RelayCommand] private void OpenMenu() => MenuOpened = true;
|
||||
/// <summary>
|
||||
/// Закрыть меню
|
||||
/// </summary>
|
||||
[RelayCommand] private void CloseMenu() => MenuOpened = false;
|
||||
/// <summary>
|
||||
/// Переключить видимость меню
|
||||
/// </summary>
|
||||
[RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened;
|
||||
|
||||
/// <summary>
|
||||
/// Переключить главную секцию информация <-> изображения
|
||||
/// </summary>
|
||||
[RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0;
|
||||
|
||||
/// <summary>
|
||||
/// Создать новую запись
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void CreateRecord()
|
||||
{
|
||||
_logger.LogInformation("Создание новой записи");
|
||||
// Если уже создана новая запись, то MenuSelectedIndex может быть равен -1 и
|
||||
// при простом присвоении MenuSelectedIndex = -1 событие OnMenuSelectedIndexChanged не вызовется,
|
||||
// потому что событие срабатывает только при изменении значения, а значение было -1 и привилось ему -1,
|
||||
// поэтому если SelectedRecord != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged
|
||||
if (MenuSelectedIndex != -1)
|
||||
MenuSelectedIndex = -1;
|
||||
else
|
||||
OnMenuSelectedIndexChanged(-1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удалить выбранную запись из базы данных или очистить значения, если запись не выбрана
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void DeleteRecord()
|
||||
{
|
||||
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
|
||||
try
|
||||
{
|
||||
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SeparatorRecord.Id), SelectedId, nameof(SeparatorRecord.Type), SelectedType);
|
||||
// Если запись не выбрана, то просто создаём новую запись
|
||||
if (MenuSelectedIndex < 0)
|
||||
{
|
||||
_logger.LogTrace("Запись не выбрана, создаём новую");
|
||||
CreateRecord();
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаём модель для экрана подтверждения
|
||||
var confirmViewModel = new DeleteConfirmViewModel(_navigation)
|
||||
{
|
||||
Question = "Вы уверены, что хотите удалить запись?"
|
||||
};
|
||||
|
||||
// Создаём коллбек который вызовется при экрана подтверждения
|
||||
void OnConfirmCloses(object? sender, bool result)
|
||||
{
|
||||
// Если пользователь отменил удаление, то выходим из коллбека
|
||||
if (!result)
|
||||
{
|
||||
_logger.LogTrace("Пользователь отменил удаление");
|
||||
return;
|
||||
}
|
||||
|
||||
// Если запись не выбрана, то просто создаём новую запись
|
||||
if (MenuSelectedIndex < 0)
|
||||
{
|
||||
_logger.LogTrace("Запись не выбрана, создаём новую");
|
||||
CreateRecord();
|
||||
return;
|
||||
}
|
||||
|
||||
// Сохраняем индекс удалённый записи и находим её в базе данных
|
||||
var deletedMenuIndex = MenuSelectedIndex;
|
||||
var deletedRecordId = SelectedId;
|
||||
var deletedRecord = _context.SeparatorRecords.Find(deletedRecordId);
|
||||
// Создаём новую запись
|
||||
CreateRecord();
|
||||
|
||||
// Удаляем запись из списка
|
||||
MenuItems.RemoveAt(deletedMenuIndex);
|
||||
// Если удалённая запись не найдена в базе данных, то логгируем как ошибку
|
||||
if (deletedRecord is null)
|
||||
{
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.BrandRecords));
|
||||
return;
|
||||
}
|
||||
|
||||
// Удаляем запись из базы данных и сохраняем изменения в базе данных
|
||||
_context.SeparatorRecords.Remove(deletedRecord);
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись удалена");
|
||||
|
||||
// Отписываемся от события закрытия экрана подтверждения
|
||||
confirmViewModel.Closes -= OnConfirmCloses;
|
||||
}
|
||||
|
||||
// Создаём View экрана подтверждения
|
||||
var confirmView = _viewLocator.Build(confirmViewModel);
|
||||
// Если View не найден, то логгируем как ошибку
|
||||
if (confirmView is null)
|
||||
{
|
||||
_logger.LogError("Не удалось найти View для экрана подтверждения");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Задаём вопрос");
|
||||
// Подписываемся на событие закрытия экрана подтверждения
|
||||
confirmViewModel.Closes += OnConfirmCloses;
|
||||
// Преходим на экран подтверждения, при закрытии он сам вернёт экран обратно на текущий
|
||||
_navigation.NavigateTo(confirmView);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Исключение при удалении записи");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вызывается при изменении <see cref="EditingRecord"/>.<br/>
|
||||
/// Изменяет <see cref="Image"/> изображением из <see cref="EditingRecord"/>.
|
||||
/// Сохранить текущие значения в новую запись или перезаписать значения в выдранной записи
|
||||
/// </summary>
|
||||
partial void OnEditingRecordChanged(SeparatorRecord value)
|
||||
{
|
||||
|
||||
var imagePath = _imageStorage.GetFullPath(value.Image, "calibration-sample");
|
||||
if (!File.Exists(imagePath))
|
||||
{
|
||||
Image = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Image is null)
|
||||
Image = new AsyncImageRecordViewModel(imagePath);
|
||||
else
|
||||
Image.Path = imagePath;
|
||||
}
|
||||
|
||||
// Просто команды управления меню
|
||||
[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 confirmViewModel = new DeleteConfirmViewModel(_navigation)
|
||||
{
|
||||
Question = "Вы уверены, что хотиите удаить запись?"
|
||||
};
|
||||
if (SelectedRecord is null)
|
||||
{
|
||||
CreateRecord();
|
||||
return;
|
||||
}
|
||||
confirmViewModel.Closes += (_, ea) =>
|
||||
{
|
||||
if (ea != true)
|
||||
return;
|
||||
|
||||
// Сохраняем текущую запись
|
||||
var deletedRecord = SelectedRecord;
|
||||
// Создаём новую запись
|
||||
CreateRecord();
|
||||
|
||||
// Если запись на была сохранена в базу данных, то ничего не делаем
|
||||
if (deletedRecord is null)
|
||||
return;
|
||||
|
||||
// Если запись была в без данных, то
|
||||
// она была и в Records, удаляем её из Records
|
||||
Records.Remove(deletedRecord);
|
||||
// Удаляем запись из базы данных и сохраняем изменения
|
||||
// TODO: Подумать над тем, что возможно стоит не удалять запись полностью, а добавить флаг удаления,
|
||||
// но тогда нужно будет сохранять и изображения этой записи, как в базе данных, так и на диске,
|
||||
// тогда удалённые записи будут заполнять диск
|
||||
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища
|
||||
_context.SeparatorRecords.Remove(deletedRecord);
|
||||
_context.SaveChanges();
|
||||
};
|
||||
|
||||
var convfirmView = _viewLocator.Build(confirmViewModel);
|
||||
if (convfirmView is not null)
|
||||
_navigation.NavigateTo(convfirmView);
|
||||
}
|
||||
|
||||
// // Команда сохранения записи, сохраняет новую запись или изменения в отредактированной
|
||||
[RelayCommand]
|
||||
private void SaveRecord()
|
||||
{
|
||||
// Если это новая запись
|
||||
if (SelectedRecord is null)
|
||||
try
|
||||
{
|
||||
// Копируем её
|
||||
var newRecord = CopyRecord(EditingRecord);
|
||||
|
||||
// Для каждого вектора
|
||||
for (int i = 0; i < newRecord.VectorRecords.Count(); i++)
|
||||
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SeparatorRecord.Id), SelectedId, nameof(SeparatorRecord.Type), SelectedType);
|
||||
// Если запись не выбрана
|
||||
if (MenuSelectedIndex < 0)
|
||||
{
|
||||
var vector = newRecord.VectorRecords[i];
|
||||
|
||||
// Добавляем вектор в базу
|
||||
var attachedRecord = _context.VectorRecords.Add(vector);
|
||||
// Удаляем отвязанный от базы данных вектор из записи
|
||||
newRecord.VectorRecords.RemoveAt(i);
|
||||
// Добавляем привязанный к базе данных вектор в запись
|
||||
newRecord.VectorRecords.Insert(i, attachedRecord.Entity);
|
||||
}
|
||||
|
||||
{
|
||||
// То добавляем запись в базу
|
||||
var attachedRecord = _context.SeparatorRecords.Add(newRecord);
|
||||
// Заменяем текущую запись на привязанную к базе данных
|
||||
newRecord = attachedRecord.Entity;
|
||||
}
|
||||
|
||||
// Создаём запись изображения
|
||||
_logger.LogTrace("Создаём новую запись изображения");
|
||||
// Заполняем запись изображения
|
||||
var imageRecord = new ImageRecord(base64: _selectedImageData ?? "");
|
||||
// Добавляем запись изображения в базу данных
|
||||
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
// Добавляем запись в список
|
||||
Records.Add(newRecord);
|
||||
// Выбираем запись
|
||||
SelectedRecord = newRecord;
|
||||
_logger.LogTrace("Запись изображения сохранена");
|
||||
|
||||
_logger.LogTrace("Создаём новую запись");
|
||||
// Заполняем запись
|
||||
var newRecord = new SeparatorRecord(
|
||||
uuid: SelectedUuid,
|
||||
type: SelectedType,
|
||||
imageId: attachedImageRecord.Entity.Id,
|
||||
featureIds: []
|
||||
);
|
||||
// Добавляем запись в базу данных
|
||||
var attachedRecord = _context.SeparatorRecords.Add(newRecord);
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись сохранена");
|
||||
|
||||
_logger.LogTrace("Создаём новые записи признаков");
|
||||
// Заполняем записи признаков и добавляем их в базу данных
|
||||
var attachedFeatureRecords = new List<FeatureRecord>();
|
||||
foreach (var feature in _selectedFeaturesData)
|
||||
{
|
||||
var featureRecord = new FeatureRecord(
|
||||
values: feature,
|
||||
sampleRecordId: attachedRecord.Entity.Id
|
||||
);
|
||||
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord);
|
||||
attachedFeatureRecords.Add(attachedFeatureRecord.Entity);
|
||||
}
|
||||
// Если эта запись есть в базе
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Записи признаков сохранены");
|
||||
|
||||
_logger.LogTrace("Заполняем признаки в сохранённой записи");
|
||||
// Добавляем идентификаторы признаков в запись
|
||||
attachedRecord.Entity.FeatureIds.AddRange(attachedFeatureRecords.Select(r => r.Id));
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Сохранение записи завершено");
|
||||
|
||||
// Добавляем новый элемент в меню
|
||||
MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.Type));
|
||||
// Выбираем последний элемент из меню
|
||||
MenuSelectedIndex = MenuItems.Count() - 1;
|
||||
}
|
||||
// Если запись выбрана
|
||||
else
|
||||
{
|
||||
// Переносим данные из редактируемой записи в выбранную
|
||||
SelectedRecord.Type = EditingRecord.Type;
|
||||
SelectedRecord.Batch = EditingRecord.Batch;
|
||||
SelectedRecord.Image = EditingRecord.Image;
|
||||
SelectedRecord.VectorRecords = EditingRecord.VectorRecords;
|
||||
|
||||
// Для каждого вектора в выбранной записи
|
||||
for (int i = 0; i < SelectedRecord.VectorRecords.Count(); i++)
|
||||
// Находим запись в базе данных
|
||||
var attachedRecord = _context.SeparatorRecords.Find(SelectedId);
|
||||
// Если запись не найдена, то логгируем как ошибку
|
||||
if (attachedRecord is null)
|
||||
{
|
||||
var vector = SelectedRecord.VectorRecords[i];
|
||||
// Если вектор есть в базе данных, то пропускаем его
|
||||
if (_context.Entry(vector).State is not EntityState.Detached)
|
||||
continue;
|
||||
|
||||
// Если вектора нет в базе данных, то добавляем его в базу
|
||||
var attachedRecord = _context.VectorRecords.Add(vector);
|
||||
// Удаляем отвязанный от базы данных вектор из записи
|
||||
SelectedRecord.VectorRecords.RemoveAt(i);
|
||||
// Добавляем привязанный вектор к записи
|
||||
SelectedRecord.VectorRecords.Insert(i, attachedRecord.Entity);
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.SeparatorRecords));
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Записываем изменения в базу данных");
|
||||
|
||||
// Ищем запись изображения в базе данных
|
||||
var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.ImageId);
|
||||
// Если запись не найдена
|
||||
if (attachedImageRecord is null)
|
||||
{
|
||||
// Логгируем как ошибку
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", attachedRecord.ImageId, nameof(_context.ImageRecords));
|
||||
_logger.LogTrace("Создаём новую запись изображения");
|
||||
// Заполняем запись изображения
|
||||
var imageRecord = new ImageRecord(base64: _selectedImageData ?? "");
|
||||
// Добавляем запись изображения в базу данных
|
||||
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись изображения сохранена");
|
||||
}
|
||||
|
||||
// Ищем записи признаков в базе данных
|
||||
var attachedFeatureRecords = _context.FeatureRecords.Where(r => r.SeparatorRecordId == SelectedId);
|
||||
// Если количество признаков в базе данных больше чем текущее
|
||||
if (attachedFeatureRecords.Count() > _selectedFeaturesData.Count())
|
||||
{
|
||||
// Находим количество признаков которое нужно удалить
|
||||
var featuresRecordsToRemove = attachedFeatureRecords.Count() - _selectedFeaturesData.Count();
|
||||
// Логгируем
|
||||
_logger.LogTrace("Количество записей признаков в базе данных больше чем текущее, удаление лишних записей: {} ({} - {})", featuresRecordsToRemove, attachedFeatureRecords.Count(), _selectedFeaturesData.Count());
|
||||
// Удаляем необходимое количество признаков из конца
|
||||
attachedFeatureRecords.TakeLast(featuresRecordsToRemove);
|
||||
}
|
||||
|
||||
// Заполняем все имеющиеся признаки в базе данных новыми значениями
|
||||
foreach (var (i, attachedFeatureRecord) in attachedFeatureRecords.Enumerate())
|
||||
attachedFeatureRecord.Values = _selectedFeaturesData[i];
|
||||
|
||||
// Если количество признаков в безе данных меньше чем текущее
|
||||
if (attachedFeatureRecords.Count() < _selectedFeaturesData.Count())
|
||||
{
|
||||
// Находим количество признаков которое нужно добавить
|
||||
var featuresRecordsToAdd = _selectedFeaturesData.Count() - attachedFeatureRecords.Count();
|
||||
// Логгируем
|
||||
_logger.LogTrace("Количество записей признаков в базе данных меньше чем текущее, добавление недостающих записей: {} ({} - {})", featuresRecordsToAdd, _selectedFeaturesData.Count(), attachedFeatureRecords.Count());
|
||||
// Заполняем записи недостающих признаков и добавляем их в базу данных
|
||||
for (var i = attachedFeatureRecords.Count(); i < _selectedFeaturesData.Count(); i++)
|
||||
{
|
||||
var featureRecord = new FeatureRecord(
|
||||
values: _selectedFeaturesData[i],
|
||||
sampleRecordId: attachedRecord.Id
|
||||
);
|
||||
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord);
|
||||
attachedFeatureRecords.Append(attachedFeatureRecord.Entity);
|
||||
}
|
||||
}
|
||||
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Записи признаков сохранена");
|
||||
|
||||
// Если в данных есть изменения, то записываем их
|
||||
if (attachedRecord.Type != SelectedType)
|
||||
attachedRecord.Type = SelectedType;
|
||||
if (attachedRecord.ImageId != attachedImageRecord.Id)
|
||||
attachedRecord.ImageId = attachedImageRecord.Id;
|
||||
// Для признаков особый случай, они в любом случае будут перезаписаны, потому что так быстрее
|
||||
attachedRecord.FeatureIds = attachedFeatureRecords.Select(r => r.Id).ToList();
|
||||
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись сохранена");
|
||||
|
||||
// Записываем изменения в меню
|
||||
_logger.LogTrace("Записываем изменения в меню");
|
||||
MenuItems[MenuSelectedIndex].Type = SelectedType;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Исключение при сохранении записи");
|
||||
}
|
||||
}
|
||||
|
||||
// Команда изменения основной секции (информация <-> изображения)
|
||||
[RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0;
|
||||
|
||||
// Команда навигации по информации
|
||||
[RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--;
|
||||
[RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++;
|
||||
|
||||
// Команда съёмки изображений
|
||||
/// <summary>
|
||||
/// Снять изображения
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void CaptureImage()
|
||||
{
|
||||
// Если съёмка уже начата, то ничего не делаем
|
||||
if (CaptureImagesTask is not null)
|
||||
// Если съёмка уже начата, то логгируем как ошибку
|
||||
if (_сaptureImagesTask is not null)
|
||||
{
|
||||
_logger.LogError("Задача съёмки уже запущена");
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаём токен отмены задачи
|
||||
_captureImagesTaskCts = new CancellationTokenSource();
|
||||
@@ -291,7 +544,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}");
|
||||
|
||||
// Запускаем получение изображений асинхронно
|
||||
CaptureImagesTask = Task.Run(async () =>
|
||||
_сaptureImagesTask = Task.Run(async () =>
|
||||
{
|
||||
// Список полученных изображений
|
||||
var imagesData = new List<Task<ImageData>>();
|
||||
@@ -306,7 +559,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
{
|
||||
// Распаковываем данные
|
||||
var (i, visible, uv365, uv254, image) = data;
|
||||
// Выбрасываем исключение если задача отменена
|
||||
// Выбрасываем исключение, если задача отменена
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
@@ -314,54 +567,89 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}");
|
||||
|
||||
// Создаём задачу сохранения изображения
|
||||
var imagesDataTask = Task.Run(() =>
|
||||
{
|
||||
// Сохраняем png изображение во временную папку
|
||||
var path = Path.Join(Path.GetTempPath(), $"gss2-calibration-sample-{visible}-{uv365}-{uv254}.png");
|
||||
// Выбрасываем исключение, если задача отменена
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
// Сохраняем PNG изображение во временную папку
|
||||
var path = Path.Join(Path.GetTempPath(), $"gss2-calibration-separator-{visible}-{uv365}-{uv254}.png");
|
||||
image.ImWrite(path);
|
||||
|
||||
// Освобождаем память изображения
|
||||
image.Release();
|
||||
image.Dispose();
|
||||
|
||||
// Добавляем данные изображения в список
|
||||
return new ImageData(path, visible, uv365, uv254);
|
||||
});
|
||||
}, cancellationToken);
|
||||
// Добавляем задачу сохранения изображения в список
|
||||
imagesData.Add(imagesDataTask);
|
||||
}, cancellationToken);
|
||||
|
||||
// Обрабатываем изображения
|
||||
var postTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t =>
|
||||
// Создаём задачу обработки изображений,
|
||||
// эта задача начнётся только после того как завершится
|
||||
// задача съёмки изображений и все задачи сохранения изображений
|
||||
var processTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t =>
|
||||
{
|
||||
// Выбрасываем исключение, если задача отменена
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений");
|
||||
// Проверяем исключения задачи получения изображений
|
||||
if (t.Exception is not null)
|
||||
|
||||
// Если во время выполнения задачи съёмки изображений произошло исключение,
|
||||
// то логгируем все исключения и выходим
|
||||
if (captureTask.Exception is not null)
|
||||
{
|
||||
_logger.LogError(t.Exception, "Исключение во время получения изображений");
|
||||
foreach (var ex in t.Exception.InnerExceptions ?? [])
|
||||
_logger.LogError(ex, "Исключение во время получения изображений");
|
||||
_logger.LogError(captureTask.Exception, "Исключение во время получения изображения");
|
||||
foreach (var ex in captureTask.Exception.InnerExceptions ?? [])
|
||||
_logger.LogError(ex, "Исключение во время получения изображения");
|
||||
return;
|
||||
}
|
||||
// Проверяем исключения задач сохранения изображений
|
||||
|
||||
// Если во время выполнения задач сохранения изображений произошло исключение,
|
||||
// то логгируем все исключения и выходим
|
||||
bool anyImageDataException = false;
|
||||
foreach (var imageData in imagesData)
|
||||
{
|
||||
if (imageData.Exception is not null)
|
||||
{
|
||||
_logger.LogError(imageData.Exception, "Исключение во время получения изображения");
|
||||
_logger.LogError(imageData.Exception, "Исключение во время сохранения изображения");
|
||||
foreach (var ex in imageData.Exception.InnerExceptions ?? [])
|
||||
_logger.LogError(ex, "Исключение во время получения изображения");
|
||||
_logger.LogError(ex, "Исключение во время сохранения изображения");
|
||||
anyImageDataException = true;
|
||||
}
|
||||
}
|
||||
if (anyImageDataException)
|
||||
return;
|
||||
|
||||
// Если во время выполнения произошло исключение,
|
||||
// то логгируем все исключения и выходим
|
||||
if (t.Exception is not null)
|
||||
{
|
||||
_logger.LogError(t.Exception, "Исключение во время обработки изображений");
|
||||
foreach (var ex in t.Exception.InnerExceptions ?? [])
|
||||
_logger.LogError(ex, "Исключение во время обработки изображений");
|
||||
return;
|
||||
}
|
||||
|
||||
// Вычисляем вектор признаков
|
||||
var result = AnalyzerService.GetFeatureVectors(imagesData.Select(i => i.Result));
|
||||
// Если вектор признаков вычислен
|
||||
if (result is not null)
|
||||
// Если вектор признаков не вычислен, то логгируем как ошибку
|
||||
if (result is null)
|
||||
{
|
||||
_logger.LogError("Не удалось вычислить вектор признаков");
|
||||
return;
|
||||
}
|
||||
|
||||
// Распаковываем результат
|
||||
var (avgImage, roi, features) = result.Value;
|
||||
_logger.LogInformation($"Длинна вектора признаков: {features.Count()}");
|
||||
|
||||
// Сохраняем вектор признаков
|
||||
_selectedFeaturesData = features;
|
||||
|
||||
// Обрабатываем среднее изображение
|
||||
avgImage = new OpenCvSharp.Mat(avgImage, roi);
|
||||
OpenCvSharp.Cv2.Resize(
|
||||
@@ -373,38 +661,77 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
)
|
||||
);
|
||||
|
||||
// Кодируем и сохраняем среднее изображение
|
||||
// Кодируем среднее изображение
|
||||
OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
|
||||
string avgPath;
|
||||
using (var stream = new MemoryStream(avgData))
|
||||
avgPath = await _imageStorage.SaveAsync(stream, ".png", "calibration-sample");
|
||||
_selectedImageData = Convert.ToBase64String(avgData);
|
||||
|
||||
// Заполняем поля в редактируемой записи
|
||||
EditingRecord.Image = avgPath;
|
||||
EditingRecord.VectorRecords = features.Select(f => new VectorRecord { Values = f }).ToList();
|
||||
// Очищаем память среднего изображения
|
||||
avgImage.Dispose();
|
||||
|
||||
// Обновляем данные в редактируемой записи
|
||||
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => OnEditingRecordChanged(EditingRecord));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Ошибка при вычислении векторов признаков");
|
||||
}
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => SelectedImage.Data = _selectedImageData);
|
||||
}, cancellationToken);
|
||||
|
||||
// Сбрасываем состояние задачи съёмки изображений
|
||||
_ = Task.WhenAll([captureTask, .. imagesData, postTask]).ContinueWith(t =>
|
||||
// Создаём задачу которая выполнится после завершения всех других
|
||||
_ = Task.WhenAll([captureTask, .. imagesData, processTask]).ContinueWith(t =>
|
||||
{
|
||||
// Если во время выполнения задачи съёмки изображений произошло исключение,
|
||||
// то логгируем все исключения и выходим
|
||||
if (captureTask.Exception is not null)
|
||||
{
|
||||
_logger.LogError(captureTask.Exception, "Исключение во время получения изображения");
|
||||
foreach (var ex in captureTask.Exception.InnerExceptions ?? [])
|
||||
_logger.LogError(ex, "Исключение во время получения изображения");
|
||||
return;
|
||||
}
|
||||
|
||||
// Если во время выполнения задач сохранения изображений произошло исключение,
|
||||
// то логгируем все исключения и выходим
|
||||
bool anyImageDataException = false;
|
||||
foreach (var imageData in imagesData)
|
||||
{
|
||||
if (imageData.Exception is not null)
|
||||
{
|
||||
_logger.LogError(imageData.Exception, "Исключение во время сохранения изображения");
|
||||
foreach (var ex in imageData.Exception.InnerExceptions ?? [])
|
||||
_logger.LogError(ex, "Исключение во время сохранения изображения");
|
||||
anyImageDataException = true;
|
||||
}
|
||||
}
|
||||
if (anyImageDataException)
|
||||
return;
|
||||
|
||||
// Если во время выполнения задачи обработки изображений произошло исключение,
|
||||
// то логгируем все исключения и выходим
|
||||
if (processTask.Exception is not null)
|
||||
{
|
||||
_logger.LogError(processTask.Exception, "Исключение во время обработки изображений");
|
||||
foreach (var ex in processTask.Exception.InnerExceptions ?? [])
|
||||
_logger.LogError(ex, "Исключение во время обработки изображений");
|
||||
return;
|
||||
}
|
||||
|
||||
// Если во время выполнения произошло исключение,
|
||||
// то логгируем все исключения и выходим
|
||||
if (t.Exception is not null)
|
||||
{
|
||||
_logger.LogError(t.Exception, "Исключение во время обработки изображений");
|
||||
foreach (var ex in t.Exception.InnerExceptions ?? [])
|
||||
_logger.LogError(ex, "Исключение во время обработки изображений");
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
// Сбрасываем состояние задачи съёмки изображений
|
||||
ImagesIsCapturing = false;
|
||||
ImagesCapturingProgress = "";
|
||||
CaptureImagesTask = null;
|
||||
_сaptureImagesTask = null;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -412,34 +739,4 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
|
||||
// Команда отмены задачи съёмки изображений
|
||||
[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,
|
||||
Image = source.Image,
|
||||
VectorRecords = source.VectorRecords
|
||||
.Select(CopyRecord)
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
/// <summary>
|
||||
/// Копирует <see cref="VectorRecord"/>, отвязывая её от базы данных
|
||||
/// </summary>
|
||||
/// <returns>Скопированная <see cref="VectorRecord"/></returns>
|
||||
private static VectorRecord CopyRecord(VectorRecord source)
|
||||
{
|
||||
return new VectorRecord
|
||||
{
|
||||
Id = source.Id,
|
||||
Values = (float[])source.Values.Clone()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,205 +1,264 @@
|
||||
<UserControl
|
||||
x:Class="GSS2.Views.AmineContent.SeparatorCalibrationView"
|
||||
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"
|
||||
x:DataType="vm:SeparatorCalibrationViewModel"
|
||||
xmlns:controls="using:GSS2.Controls"
|
||||
xmlns:core="using:GSS2.UI.Core"
|
||||
xmlns:i="using:Avalonia.Xaml.Interactivity"
|
||||
xmlns:ia="using:Avalonia.Xaml.Interactions.Custom"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
x:DataType="vm:SeparatorCalibrationViewModel">
|
||||
|
||||
<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>
|
||||
xmlns:vm="using:GSS2.ViewModels.AmineContent"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
>
|
||||
|
||||
<Grid Background="{StaticResource MainBackground}">
|
||||
|
||||
<!-- Основная форма -->
|
||||
<Grid IsEnabled="{Binding !MenuOpened}" ColumnSpacing="5" RowSpacing="5">
|
||||
<Grid IsEnabled="{Binding !MenuOpened}">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" /> <!-- Кнопка "Удалить" -->
|
||||
<ColumnDefinition Width="*" /> <!-- Кнопка "Сохранить" -->
|
||||
<ColumnDefinition Width="*" /> <!-- Кнопки "К информации"/"К изображению" -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" /> <!-- Главная панель -->
|
||||
<RowDefinition Height="Auto" /> <!-- Кнопки главной панели -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" /> <!-- Удалить -->
|
||||
<ColumnDefinition Width="*" /> <!-- Сохранить -->
|
||||
<ColumnDefinition Width="*" /> <!-- К информации/К изображению -->
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowSpacing>5</Grid.RowSpacing>
|
||||
|
||||
<!-- Главная панель -->
|
||||
<core:SectionPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
<core:SectionPanel
|
||||
CanScrollBackward="{Binding MainSectionCanScrollBackward}"
|
||||
CanScrollForward="{Binding MainSectionCanScrollForward}"
|
||||
ClipToBounds="True"
|
||||
CurrentIndex="{Binding MainSectionCurrentIndex}"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
IsScrolling="{Binding MainSectionIsScrolling}"
|
||||
Orientation="Vertical">
|
||||
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>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" /> <!-- Кнопка меню -->
|
||||
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
|
||||
|
||||
<!-- Кнопка меню -->
|
||||
<Button Grid.Column="0" Grid.Row="0" Grid.RowSpan="7" Classes="vertical" VerticalAlignment="Stretch" Command="{Binding ToggleMenuCommand}">
|
||||
<Button
|
||||
Classes="vertical"
|
||||
Command="{Binding ToggleMenuCommand}"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Stretch"
|
||||
>
|
||||
<TextBlock Text="☰"/>
|
||||
</Button>
|
||||
|
||||
<!-- Id / Uuid / Type -->
|
||||
<Grid Grid.Column="1">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" /> <!-- Id (подпись) | Uuid (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
|
||||
<RowDefinition Height="Auto" /> <!-- Type (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- Type (поле ) -->
|
||||
<RowDefinition Height="*" /> <!-- Пустое пространство -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.RowSpacing>5</Grid.RowSpacing>
|
||||
|
||||
<!-- Id (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Text="ИД:"
|
||||
/>
|
||||
<!-- Id (поле) -->
|
||||
<TextBox
|
||||
Classes="singleline"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
IsReadOnly="True"
|
||||
PlaceholderText="ИД"
|
||||
Text="{Binding SelectedId}"
|
||||
/>
|
||||
<!-- Uuid (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini"
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
Text="Уникальный ИД:"
|
||||
/>
|
||||
<!-- Uuid (поле) -->
|
||||
<TextBox
|
||||
Classes="singleline"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
IsReadOnly="True"
|
||||
PlaceholderText="Уникальный ИД"
|
||||
Text="{Binding SelectedUuid}"
|
||||
/>
|
||||
<!-- Type (подпись) -->
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Classes="mini"
|
||||
Text="Тип:"
|
||||
/>
|
||||
<!-- Type (поле) -->
|
||||
<controls:TouchTextBox
|
||||
Classes="singleline"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Grid.Row="3"
|
||||
PlaceholderText="Тип"
|
||||
Text="{Binding SelectedType}"
|
||||
/>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Информация -->
|
||||
<Grid Grid.Column="1">
|
||||
<!-- ИД (подпись) -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Classes="mini" Text="ИД:" />
|
||||
<!-- <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" />
|
||||
<!-- <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="Тип:" />
|
||||
<!-- <TextBlock Grid.Row="2" Grid.Column="1" Classes="mini" Text="Тип:" /> -->
|
||||
<!-- Тип (поле) -->
|
||||
<controls:TouchTextBox Grid.Row="3" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Type}" PlaceholderText="Тип" />
|
||||
<!-- <controls:TouchTextBox Grid.Row="3" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Type}" PlaceholderText="Тип" /> -->
|
||||
<!-- Партия (подпись) -->
|
||||
<TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Партия:" />
|
||||
<!-- <TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Партия:" /> -->
|
||||
<!-- Партия (поле) -->
|
||||
<controls:TouchTextBox Grid.Row="5" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Batch}" PlaceholderText="Партия" />
|
||||
<!-- <controls:TouchTextBox Grid.Row="5" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Batch}" PlaceholderText="Партия" /> -->
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Изображение -->
|
||||
<Grid ColumnSpacing="5" RowSpacing="5">
|
||||
<Grid>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" /> <!-- Изображение -->
|
||||
<RowDefinition Height="Auto" /> <!-- Кнопка "Получить изображения" / "Отменить съёмку" -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.RowSpacing>5</Grid.RowSpacing>
|
||||
|
||||
<!-- Изображение -->
|
||||
<Grid>
|
||||
|
||||
<!-- Изображение -->
|
||||
<Grid>
|
||||
|
||||
<Grid.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="Image" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
|
||||
</MultiBinding>
|
||||
</Grid.IsVisible>
|
||||
|
||||
<!-- Изображение -->
|
||||
<core:ZoomPanImage HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
ImageSource="{Binding Image.Image}"
|
||||
Background="Black"
|
||||
IsVisible="{Binding Image.IsLoading, Converter={x:Static BoolConverters.Not}}" />
|
||||
|
||||
<!-- Оверлей загрузки -->
|
||||
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" IsVisible="{Binding Image.IsLoading}">
|
||||
<ProgressBar IsIndeterminate="True" />
|
||||
<TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Оверлей "Изображения отсутствуют" -->
|
||||
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Изображения отсутствуют">
|
||||
<!-- Изображение -->
|
||||
<core:ZoomPanImage
|
||||
Background="Black"
|
||||
HorizontalAlignment="Stretch"
|
||||
ImageSource="{Binding SelectedImage.Image}"
|
||||
IsVisible="{Binding SelectedImage.IsLoading, Converter={x:Static BoolConverters.Not}}"
|
||||
VerticalAlignment="Stretch"
|
||||
>
|
||||
|
||||
<core:ZoomPanImage.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="SelectedImage.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
|
||||
</MultiBinding>
|
||||
</core:ZoomPanImage.IsVisible>
|
||||
|
||||
</core:ZoomPanImage>
|
||||
|
||||
<!-- Оверлей "Изображение загружается" -->
|
||||
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" >
|
||||
|
||||
<StackPanel.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Path="SelectedImage.IsLoading" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
|
||||
</MultiBinding>
|
||||
</StackPanel.IsVisible>
|
||||
|
||||
<ProgressBar IsIndeterminate="True" />
|
||||
|
||||
<TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- Оверлей "Изображение отсутствует" -->
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
Text="Изображение отсутствует"
|
||||
VerticalAlignment="Center"
|
||||
>
|
||||
<TextBlock.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="Image" />
|
||||
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
|
||||
</MultiBinding>
|
||||
</TextBlock.IsVisible>
|
||||
</TextBlock>
|
||||
|
||||
<!-- Оверлей прогресс -->
|
||||
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" IsVisible="{Binding ImagesIsCapturing}">
|
||||
<StackPanel
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
>
|
||||
|
||||
<StackPanel.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
|
||||
<Binding Path="ImagesIsCapturing" />
|
||||
</MultiBinding>
|
||||
</StackPanel.IsVisible>
|
||||
|
||||
<ProgressBar IsIndeterminate="True" />
|
||||
|
||||
<TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Кнопка "Получить изображения" -->
|
||||
<Button Grid.Row="1"
|
||||
<Button
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding CaptureImageCommand}"
|
||||
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNull}}">
|
||||
>
|
||||
<TextBlock Text="Получить изображения"/>
|
||||
</Button>
|
||||
|
||||
<!-- Кнопка "Отменить съёмку" -->
|
||||
<Button Grid.Row="1"
|
||||
<Button
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding CancelImageCapturingCommand}"
|
||||
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
>
|
||||
<TextBlock Text="Отменить съёмку"/>
|
||||
</Button>
|
||||
|
||||
@@ -208,22 +267,37 @@
|
||||
</core:SectionPanel>
|
||||
|
||||
<!-- Кнопки главной панели -->
|
||||
<!-- Удалить -->
|
||||
<Button Grid.Row="1" Grid.Column="0" HorizontalAlignment="Stretch" Classes="danger" Command="{Binding DeleteRecordCommand}">
|
||||
<!-- Кнопка "Удалить" -->
|
||||
<Button
|
||||
Classes="danger"
|
||||
Command="{Binding DeleteRecordCommand}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
>
|
||||
<TextBlock Text="Удалить"/>
|
||||
</Button>
|
||||
|
||||
<!-- Сохранить -->
|
||||
<Button Grid.Row="1" Grid.Column="1" HorizontalAlignment="Stretch" Command="{Binding SaveRecordCommand}" Content="Сохранить">
|
||||
<!-- Кнопка "Сохранить" -->
|
||||
<Button
|
||||
Command="{Binding SaveRecordCommand}"
|
||||
Content="Сохранить"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
>
|
||||
<TextBlock Text="Сохранить"/>
|
||||
</Button>
|
||||
|
||||
<!-- К информации -->
|
||||
<Button Grid.Row="1" Grid.Column="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
<!-- Кнопка "К информации" -->
|
||||
<Button
|
||||
Command="{Binding SwitchMainSectionCommand}"
|
||||
Content="К информации"
|
||||
IsVisible="{Binding MainSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=1}">
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding MainSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=1}"
|
||||
>
|
||||
|
||||
<TextBlock Text="К информации"/>
|
||||
|
||||
@@ -236,12 +310,15 @@
|
||||
|
||||
</Button>
|
||||
|
||||
<!-- К изображению -->
|
||||
<Button Grid.Row="1" Grid.Column="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
<!-- Кнопка "К изображению" -->
|
||||
<Button
|
||||
Command="{Binding SwitchMainSectionCommand}"
|
||||
Content="К изображению"
|
||||
IsVisible="{Binding MainSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=0}">
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding MainSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=0}"
|
||||
>
|
||||
|
||||
<TextBlock Text="К изображению"/>
|
||||
|
||||
@@ -257,7 +334,11 @@
|
||||
</Grid>
|
||||
|
||||
<!-- Полупрозрачный оверлей -->
|
||||
<Border Background="{StaticResource Overlay}" IsEnabled="{Binding MenuOpened}" IsVisible="{Binding MenuOpened}">
|
||||
<Border
|
||||
Background="{StaticResource Overlay}"
|
||||
IsEnabled="{Binding MenuOpened}"
|
||||
IsVisible="{Binding MenuOpened}"
|
||||
>
|
||||
<Interaction.Behaviors>
|
||||
<TappedTrigger>
|
||||
<InvokeCommandAction Command="{Binding CloseMenuCommand}" />
|
||||
@@ -266,72 +347,111 @@
|
||||
</Border>
|
||||
|
||||
<!-- Выезжающая боковая панель -->
|
||||
<Border HorizontalAlignment="Left"
|
||||
Background="{StaticResource MenuBackground}" BorderBrush="{StaticResource MenuBorder}" BorderThickness="0,0,1,0"
|
||||
IsEnabled="{Binding MenuOpened}" IsVisible="{Binding MenuOpened}">
|
||||
<Border
|
||||
Background="{StaticResource MenuBackground}"
|
||||
BorderBrush="{StaticResource MenuBorder}"
|
||||
BorderThickness="0,0,1,0"
|
||||
HorizontalAlignment="Left"
|
||||
IsEnabled="{Binding MenuOpened}"
|
||||
IsVisible="{Binding MenuOpened}"
|
||||
>
|
||||
|
||||
<!-- Переходы -->
|
||||
<Border.Transitions>
|
||||
<Transitions>
|
||||
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.25" />
|
||||
<TransformOperationsTransition Duration="0:0:0.25" Property="RenderTransform" />
|
||||
</Transitions>
|
||||
</Border.Transitions>
|
||||
|
||||
<!-- Поведения -->
|
||||
<Interaction.Behaviors>
|
||||
|
||||
<DataTriggerBehavior Binding="{Binding MenuOpened}" ComparisonCondition="Equal" Value="True">
|
||||
<!-- Поведение при открытии меню -->
|
||||
<DataTriggerBehavior
|
||||
Binding="{Binding MenuOpened}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True"
|
||||
>
|
||||
<ChangePropertyAction PropertyName="RenderTransform" Value="translateX(0px)" />
|
||||
</DataTriggerBehavior>
|
||||
|
||||
<DataTriggerBehavior Binding="{Binding MenuOpened}" ComparisonCondition="Equal" Value="False">
|
||||
<!-- Поведение при закрытии меню -->
|
||||
<DataTriggerBehavior
|
||||
Binding="{Binding MenuOpened}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False"
|
||||
>
|
||||
<ChangePropertyAction PropertyName="RenderTransform" Value="translateX(-320px)" />
|
||||
</DataTriggerBehavior>
|
||||
|
||||
</Interaction.Behaviors>
|
||||
|
||||
<!-- Содержимое меню -->
|
||||
<Grid RowSpacing="10" Margin="10">
|
||||
<Grid Margin="10">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/> <!-- Кнопка "Новая запись" -->
|
||||
<RowDefinition Height="*"/> <!-- Элементы меню -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Button Grid.Row="0" MinWidth="200" HorizontalAlignment="Stretch" Command="{Binding CreateRecordCommand}">
|
||||
<Grid.RowSpacing>10</Grid.RowSpacing>
|
||||
|
||||
<!-- Кнопка "Новая запись" -->
|
||||
<Button
|
||||
Command="{Binding CreateRecordCommand}"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
MinWidth="200"
|
||||
>
|
||||
<TextBlock Text="Новая запись"/>
|
||||
</Button>
|
||||
|
||||
<ListBox Grid.Row="1" Background="{StaticResource MenuBackground}" ItemsSource="{Binding Records}" SelectedItem="{Binding SelectedRecord}">
|
||||
<!-- Элементы меню -->
|
||||
<ListBox
|
||||
Background="{StaticResource MenuBackground}"
|
||||
Grid.Row="1"
|
||||
ItemsSource="{Binding MenuItems}"
|
||||
SelectedIndex="{Binding MenuSelectedIndex}"
|
||||
>
|
||||
|
||||
<!-- Шаблон списка элементов -->
|
||||
<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>
|
||||
<DataTemplate x:DataType="vm:SeparatorCalibrationViewModel+MenuItemViewModel">
|
||||
<Border Margin="3" CornerRadius="5">
|
||||
<TextBlock Classes="mini" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user