Доработан winui

This commit is contained in:
2026-02-01 09:26:13 +03:00
parent 584df249f6
commit e8b4cb9881
26 changed files with 1842 additions and 2373 deletions

View File

@@ -1,22 +1,32 @@
using Lattice.Themes.Core.Tokens;
using Lattice.Themes.Core;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
namespace Lattice.Themes;
/// <summary>
/// Менеджер тем для Lattice Framework.
/// Менеджер тем для Lattice Framework. Управляет регистрацией, применением и переключением тем оформления.
/// Предоставляет доступ к токенам темы и поддерживает динамическое обновление UI при смене темы.
/// </summary>
public sealed class ThemeManager
{
public static ThemeManager Current { get; } = new();
private static readonly ThemeManager _instance = new();
/// <summary>
/// Получает текущий экземпляр менеджера тем (синглтон).
/// </summary>
public static ThemeManager Current => _instance;
private ThemePack? _currentTheme;
private readonly Dictionary<string, ThemePack> _registeredThemes = new();
/// <summary>
/// Получает текущую активную тему.
/// </summary>
public ThemePack? CurrentTheme => _currentTheme;
/// <summary>
/// Происходит при изменении текущей темы.
/// </summary>
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
private ThemeManager() { }
@@ -24,6 +34,8 @@ public sealed class ThemeManager
/// <summary>
/// Регистрирует тему в менеджере.
/// </summary>
/// <param name="theme">Тема для регистрации.</param>
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="theme"/> равен null.</exception>
public void RegisterTheme(ThemePack theme)
{
if (theme == null)
@@ -35,6 +47,8 @@ public sealed class ThemeManager
/// <summary>
/// Получает зарегистрированную тему по имени.
/// </summary>
/// <param name="name">Имя темы.</param>
/// <returns>Зарегистрированная тема или null, если тема не найдена.</returns>
public ThemePack? GetTheme(string name)
{
_registeredThemes.TryGetValue(name, out var theme);
@@ -44,17 +58,18 @@ public sealed class ThemeManager
/// <summary>
/// Получает список всех зарегистрированных тем.
/// </summary>
/// <returns>Неизменяемая коллекция зарегистрированных тем.</returns>
public IReadOnlyCollection<ThemePack> GetRegisteredThemes()
{
return _registeredThemes.Values.ToList();
}
/// <summary>
/// Получение информации о теме.
/// Получает информацию о зарегистрированной теме.
/// </summary>
/// <param name="themeName"></param>
/// <returns></returns>
public ThemeInfo GetThemeInfo(string themeName)
/// <param name="themeName">Имя темы.</param>
/// <returns>Информация о теме или null, если тема не зарегистрирована.</returns>
public ThemeInfo? GetThemeInfo(string themeName)
{
if (!_registeredThemes.TryGetValue(themeName, out var theme))
return null;
@@ -72,6 +87,9 @@ public sealed class ThemeManager
/// <summary>
/// Применяет тему по имени.
/// </summary>
/// <param name="themeName">Имя темы для применения.</param>
/// <exception cref="ArgumentException">Выбрасывается, если тема с указанным именем не зарегистрирована.</exception>
/// <exception cref="InvalidOperationException">Выбрасывается, если не удалось применить тему.</exception>
public void ApplyTheme(string themeName)
{
if (!_registeredThemes.TryGetValue(themeName, out var theme))
@@ -85,6 +103,9 @@ public sealed class ThemeManager
/// <summary>
/// Применяет указанную тему.
/// </summary>
/// <param name="theme">Тема для применения.</param>
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="theme"/> равен null.</exception>
/// <exception cref="InvalidOperationException">Выбрасывается, если не удалось применить тему.</exception>
public void ApplyTheme(ThemePack theme)
{
if (theme == null)
@@ -93,21 +114,21 @@ public sealed class ThemeManager
if (_currentTheme == theme)
return;
var old = _currentTheme;
var oldTheme = _currentTheme;
_currentTheme = theme;
try
{
ReplaceApplicationResources(theme);
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(old!, theme));
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(oldTheme!, theme));
}
catch (Exception ex)
{
// В случае ошибки возвращаемся к старой теме
_currentTheme = old;
if (old != null)
// Восстанавливаем предыдущую тему при ошибке
_currentTheme = oldTheme;
if (oldTheme != null)
{
ReplaceApplicationResources(old);
ReplaceApplicationResources(oldTheme);
}
throw new InvalidOperationException($"Failed to apply theme '{theme.Name}'.", ex);
}
@@ -116,22 +137,24 @@ public sealed class ThemeManager
/// <summary>
/// Загружает ресурсы темы в указанный словарь ресурсов.
/// </summary>
/// <param name="targetDictionary">Целевой словарь ресурсов.</param>
/// <param name="theme">Тема, ресурсы которой нужно загрузить.</param>
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="targetDictionary"/> или <paramref name="theme"/> равны null.</exception>
public void LoadThemeIntoDictionary(ResourceDictionary targetDictionary, ThemePack theme)
{
if (targetDictionary == null)
throw new ArgumentNullException(nameof(targetDictionary));
if (theme == null)
throw new ArgumentNullException(nameof(theme));
// Очищаем старые словари Lattice
// Удаляем все ThemeDictionary из словаря
for (int i = targetDictionary.MergedDictionaries.Count - 1; i >= 0; i--)
{
if (targetDictionary.MergedDictionaries[i] is ThemeDictionary)
targetDictionary.MergedDictionaries.RemoveAt(i);
}
// Добавляем новые словари темы
// Добавляем словари темы
foreach (var uri in theme.GetResourceUris())
{
try
@@ -146,6 +169,11 @@ public sealed class ThemeManager
}
}
/// <summary>
/// Подсчитывает количество токенов в теме.
/// </summary>
/// <param name="theme">Тема для подсчета токенов.</param>
/// <returns>Количество токенов в теме. Возвращает 0 при возникновении ошибки.</returns>
private int CountTokensInTheme(ThemePack theme)
{
try
@@ -160,6 +188,10 @@ public sealed class ThemeManager
}
}
/// <summary>
/// Заменяет ресурсы приложения на ресурсы указанной темы.
/// </summary>
/// <param name="theme">Тема, ресурсы которой нужно применить.</param>
private void ReplaceApplicationResources(ThemePack theme)
{
var app = Application.Current;
@@ -171,45 +203,32 @@ public sealed class ThemeManager
ForceUpdateUI();
}
/// <summary>
/// Принудительно обновляет пользовательский интерфейс после смены темы.
/// Использует легковесный подход без рекурсивного обхода дерева элементов.
/// </summary>
private void ForceUpdateUI()
{
foreach (var window in WindowTracker.Windows)
{
if (window.Content is FrameworkElement root)
RefreshElement(root);
}
}
private void RefreshElement(FrameworkElement element)
{
var stack = new Stack<FrameworkElement>();
stack.Push(element);
while (stack.Count > 0)
{
var current = stack.Pop();
// Пересоздаём Template только у Control
if (current is Control control)
{
var template = control.Template;
control.Template = null;
control.Template = template;
}
else if (current is ContentPresenter contentPresenter)
{
// Обновляем ContentPresenter
var content = contentPresenter.Content;
contentPresenter.Content = null;
contentPresenter.Content = content;
}
// Перезагружаем ресурсы корневого элемента
var resources = root.Resources;
var currentTheme = _currentTheme;
if (currentTheme != null)
{
LoadThemeIntoDictionary(resources, currentTheme);
}
// Добавляем детей в стек
int count = VisualTreeHelper.GetChildrenCount(current);
for (int i = 0; i < count; i++)
{
if (VisualTreeHelper.GetChild(current, i) is FrameworkElement child)
stack.Push(child);
// Принудительное обновление стилей через перезагрузку ResourceDictionary
var mergedDictionaries = resources.MergedDictionaries;
if (mergedDictionaries.Count > 0)
{
var temp = mergedDictionaries[mergedDictionaries.Count - 1];
mergedDictionaries.RemoveAt(mergedDictionaries.Count - 1);
mergedDictionaries.Add(temp);
}
}
}
}
@@ -217,6 +236,7 @@ public sealed class ThemeManager
/// <summary>
/// Проверяет, что все необходимые токены определены в текущей теме.
/// </summary>
/// <returns>true, если все токены присутствуют; иначе false.</returns>
public bool ValidateThemeTokens()
{
if (_currentTheme == null)
@@ -249,6 +269,8 @@ public sealed class ThemeManager
/// <summary>
/// Получает значение токена из текущей темы.
/// </summary>
/// <param name="tokenKey">Ключ токена.</param>
/// <returns>Значение токена или null, если токен не найден или приложение не инициализировано.</returns>
public object? GetTokenValue(string tokenKey)
{
var app = Application.Current;
@@ -266,6 +288,9 @@ public sealed class ThemeManager
/// <summary>
/// Получает значение токена с приведением к указанному типу.
/// </summary>
/// <typeparam name="T">Тип, к которому приводится значение токена.</typeparam>
/// <param name="tokenKey">Ключ токена.</param>
/// <returns>Значение токена или значение по умолчанию для типа T, если токен не найден.</returns>
public T? GetTokenValue<T>(string tokenKey)
{
object? value = GetTokenValue(tokenKey);
@@ -278,16 +303,32 @@ public sealed class ThemeManager
}
/// <summary>
/// Информация о теме.
/// Предоставляет информацию о теме оформления.
/// </summary>
public class ThemeInfo
{
/// <summary>
/// Название темы.
/// Получает или задает название темы.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Получает или задает описание темы.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Получает или задает версию темы.
/// </summary>
public string Version { get; set; } = string.Empty;
/// <summary>
/// Получает или задает значение, указывающее, является ли тема темной.
/// </summary>
public string Name { get; set; }
public string Description { get; set; }
public string Version { get; set; }
public bool IsDark { get; set; }
/// <summary>
/// Получает или задает количество токенов в теме.
/// </summary>
public int TokenCount { get; set; }
}