自己写一个Setting类,支持jason,ini格式。直接写入和读取对象,ISettingBackend用于配置加密,压缩等自定义。
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace Shares
{
public interface ISettingBackend
{
// 用户可配置:按添加顺序执行
IList<Func<byte[], byte[]>> EncodePipeline { get; }
IList<Func<byte[], byte[]>> DecodePipeline { get; }
void Load(string text);
string Save();
void Set<T>(string sectionName, T value);
T Get<T>(string sectionName, T defaultValue);
}
public sealed class Setting
{
private static readonly ConcurrentDictionary<string, object> fileLocks =
new(StringComparer.OrdinalIgnoreCase);
private static object Gate(string path) => fileLocks.GetOrAdd(path, _ => new object());
private static readonly Dictionary<string, Func<ISettingBackend>> factories =
new(StringComparer.OrdinalIgnoreCase)
{
[".json"] = () => new JsonSettingBackend(),
[".ini"] = () => new IniSettingBackend(),
[".key"]= () => new IniSettingBackend(),
};
public static void RegisterBackend(string extension, Func<ISettingBackend> factory)
{
if (string.IsNullOrWhiteSpace(extension))
throw new ArgumentException(nameof(extension));
if (!extension.StartsWith('.'))
extension = "." + extension;
factories[extension] = factory ?? throw new ArgumentNullException(nameof(factory));
}
private readonly string filepath;
private readonly ISettingBackend backend;
public Setting(string filePath,
IEnumerable<Func<byte[], byte[]>>? encodePipeline = null,
IEnumerable<Func<byte[], byte[]>>? decodePipeline = null)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException(nameof(filePath));
filepath = Path.GetFullPath(filePath);
var ext = Path.GetExtension(filepath);
if (!factories.TryGetValue(ext, out var factory))
throw new NotSupportedException($"Unsupported format: {ext}");
backend = factory();
if (encodePipeline != null)
foreach (var f in encodePipeline) backend.EncodePipeline.Add(f);
if (decodePipeline != null)
foreach (var f in decodePipeline) backend.DecodePipeline.Add(f);
lock (Gate(filepath))
backend.Load(ReadText(filepath, backend.DecodePipeline));
}
public void Set<T>(string sectionName, T value)
{
if (string.IsNullOrWhiteSpace(sectionName))
throw new ArgumentException(nameof(sectionName));
lock (Gate(filepath))
{
backend.Set(sectionName, value);
AtomicWrite(filepath, backend.Save(), backend.EncodePipeline);
}
}
public T Get<T>(string sectionName) => Get(sectionName, default(T)!);
public T Get<T>(string sectionName, T defaultValue)
{
if (string.IsNullOrWhiteSpace(sectionName))
throw new ArgumentException(nameof(sectionName));
lock (Gate(filepath))
return backend.Get(sectionName, defaultValue);
}
private static string ReadText(string path, IList<Func<byte[], byte[]>> decodePipeline)
{
if (!File.Exists(path))
return "";
var bytes = File.ReadAllBytes(path);
if (decodePipeline.Count > 0)
bytes = ApplyPipeline(bytes, decodePipeline);
return Encoding.UTF8.GetString(bytes);
}
private static void AtomicWrite(string path, string text, IList<Func<byte[], byte[]>> encodePipeline)
{
EnsureDir(path);
var bytes = Encoding.UTF8.GetBytes(text);
if (encodePipeline.Count > 0)
bytes = ApplyPipeline(bytes, encodePipeline);
var temp = path + ".tmp";
File.WriteAllBytes(temp, bytes);
if (File.Exists(path))
{
try { File.Replace(temp, path, null); return; }
catch { }
}
File.Move(temp, path, true);
}
private static byte[] ApplyPipeline(byte[] data, IList<Func<byte[], byte[]>> pipeline)
{
for (int i = 0; i < pipeline.Count; i++)
data = pipeline[i](data);
return data;
}
private static void EnsureDir(string path)
{
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
}
}
internal abstract class SettingBackendBase : ISettingBackend
{
public IList<Func<byte[], byte[]>> EncodePipeline { get; } = [];
public IList<Func<byte[], byte[]>> DecodePipeline { get; } = [];
public abstract void Load(string text);
public abstract string Save();
public abstract void Set<T>(string sectionName, T value);
public abstract T Get<T>(string sectionName, T defaultValue);
}
internal sealed class JsonSettingBackend : SettingBackendBase
{
private readonly JsonSerializerOptions options = new()
{
WriteIndented = true,
PropertyNameCaseInsensitive = true,
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private JsonObject root = [];
public override void Load(string text)
{
root = [];
if (string.IsNullOrWhiteSpace(text))
return;
try
{
var node = JsonNode.Parse(text, documentOptions: new JsonDocumentOptions
{
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip
});
if (node is JsonObject obj)
root = obj;
}
catch
{
root = [];
}
}
public override string Save() => root.ToJsonString(options);
public override void Set<T>(string sectionName, T value)
{
root[sectionName] = JsonSerializer.SerializeToNode(value, options);
}
public override T Get<T>(string sectionName, T defaultValue)
{
if (!root.TryGetPropertyValue(sectionName, out var node) || node is null)
return defaultValue;
try { return node.Deserialize<T>(options) ?? defaultValue; }
catch { return defaultValue; }
}
}
internal sealed class IniSettingBackend : SettingBackendBase
{
private Dictionary<string, Dictionary<string, string>> root =
new(StringComparer.OrdinalIgnoreCase);
public override void Load(string text)
{
root = ParseIni(text ?? "");
}
public override string Save() => BuildIni(root);
public override void Set<T>(string sectionName, T value)
{
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (value != null) FlattenAny(value, value.GetType(), dict, "");
root[sectionName] = dict;
}
public override T Get<T>(string sectionName, T defaultValue)
{
if (!root.TryGetValue(sectionName, out var dict))
return defaultValue;
try
{
var t = typeof(T);
if (t.IsValueType)
{
object boxed = Activator.CreateInstance(t)!;
UnflattenInto(boxed, t, dict, "");
return (T)boxed;
}
var instance = Activator.CreateInstance<T>();
if (instance == null) return defaultValue;
UnflattenInto(instance, t, dict, "");
return instance;
}
catch
{
return defaultValue;
}
}
private static Dictionary<string, Dictionary<string, string>> ParseIni(string text)
{
var result = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
string? current = null;
using var reader = new StringReader(text);
while (true)
{
var line = reader.ReadLine();
if (line == null) break;
line = line.Trim();
if (line.Length == 0 || line.StartsWith(';') || line.StartsWith('#'))
continue;
if (line.StartsWith('[') && line.EndsWith(']'))
{
var name = line[1..^1].Trim();
current = name.Length == 0 ? null : name;
if (current != null && !result.ContainsKey(current))
result[current] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
continue;
}
if (current == null)
continue;
var idx = line.IndexOf('=');
if (idx <= 0) continue;
result[current][line[..idx].Trim()] = UnescapeIni(line[(idx + 1)..].Trim());
}
return result;
}
private static string BuildIni(Dictionary<string, Dictionary<string, string>> data)
{
var sb = new StringBuilder();
foreach (var sec in data)
{
sb.Append('[').Append(sec.Key).AppendLine("]");
foreach (var kv in sec.Value)
sb.Append(kv.Key).Append('=').AppendLine(EscapeIni(kv.Value));
sb.AppendLine();
}
return sb.ToString();
}
private static string EscapeIni(string value)
{
if (value == null) return "";
return value.Replace("\\", "\\\\").Replace("\r", "\\r").Replace("\n", "\\n");
}
private static string UnescapeIni(string value)
{
if (value == null) return "";
return value.Replace("\\n", "\n").Replace("\\r", "\r").Replace("\\\\", "\\");
}
private static string EscapeToken(string token)
{
if (token == null) return "";
return token.Replace("\\", "\\\\").Replace("]", "\\]");
}
private static string UnescapeToken(string token)
{
if (token == null) return "";
return token.Replace("\\]", "]").Replace("\\\\", "\\");
}
private static bool IsSimple(Type type)
{
type = Nullable.GetUnderlyingType(type) ?? type;
return type.IsEnum
|| type.IsPrimitive
|| type == typeof(string)
|| type == typeof(decimal)
|| type == typeof(DateTime)
|| type == typeof(DateTimeOffset)
|| type == typeof(Guid)
|| type == typeof(TimeSpan);
}
private static string ToStringInvariant(object value, Type type)
{
type = Nullable.GetUnderlyingType(type) ?? type;
if (type == typeof(DateTime))
return ((DateTime)value).ToString("O", CultureInfo.InvariantCulture);
if (type == typeof(DateTimeOffset))
return ((DateTimeOffset)value).ToString("O", CultureInfo.InvariantCulture);
if (type == typeof(TimeSpan))
return ((TimeSpan)value).ToString("c", CultureInfo.InvariantCulture);
if (type == typeof(bool))
return (bool)value ? "true" : "false";
return Convert.ToString(value, CultureInfo.InvariantCulture) ?? "";
}
private static object? FromStringInvariant(string raw, Type targetType)
{
var underlying = Nullable.GetUnderlyingType(targetType);
var type = underlying ?? targetType;
if (type == typeof(string)) return raw;
if (type == typeof(bool)) return raw.Equals("true", StringComparison.OrdinalIgnoreCase) || raw == "1";
if (type.IsEnum) return Enum.Parse(type, raw, true);
if (type == typeof(DateTime))
return DateTime.Parse(raw, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
if (type == typeof(DateTimeOffset))
return DateTimeOffset.Parse(raw, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
if (type == typeof(TimeSpan))
return TimeSpan.Parse(raw, CultureInfo.InvariantCulture);
if (type == typeof(Guid))
return Guid.Parse(raw);
return Convert.ChangeType(raw, type, CultureInfo.InvariantCulture);
}
private static bool IsListOrArray(Type type, out Type elementType, out bool isArray)
{
if (type.IsArray)
{
elementType = type.GetElementType()!;
isArray = true;
return true;
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
{
elementType = type.GetGenericArguments()[0];
isArray = false;
return true;
}
elementType = typeof(object);
isArray = false;
return false;
}
private static bool IsStringKeyDictionary(Type type, out Type valueType)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
var args = type.GetGenericArguments();
if (args[0] == typeof(string))
{
valueType = args[1];
return true;
}
}
valueType = typeof(object);
return false;
}
private static void FlattenAny(object obj, Type objType, Dictionary<string, string> dict, string prefix)
{
if (IsStringKeyDictionary(objType, out var valueType) && obj is IDictionary map)
{
foreach (DictionaryEntry e in map)
{
var key = EscapeToken(e.Key?.ToString() ?? "");
var baseKey = prefix + "[" + key + "]";
var val = e.Value;
if (val == null) { dict[baseKey] = ""; continue; }
var vt = Nullable.GetUnderlyingType(valueType) ?? valueType;
if (IsSimple(vt))
dict[baseKey] = ToStringInvariant(val, vt);
else
FlattenAny(val, val.GetType(), dict, baseKey);
}
return;
}
if (IsListOrArray(objType, out var elementType, out _) && obj is IEnumerable en && objType != typeof(string))
{
var i = 0;
foreach (var item in en)
{
var baseKey = prefix + "[" + i.ToString(CultureInfo.InvariantCulture) + "]";
if (item == null) { dict[baseKey] = ""; i++; continue; }
var et = Nullable.GetUnderlyingType(elementType) ?? elementType;
if (IsSimple(et))
dict[baseKey] = ToStringInvariant(item, et);
else
FlattenAny(item, item.GetType(), dict, baseKey);
i++;
}
return;
}
foreach (var prop in objType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (!prop.CanRead) continue;
if (prop.GetIndexParameters().Length != 0) continue;
var value = prop.GetValue(obj);
var key = prefix.Length == 0 ? prop.Name : prefix + "." + prop.Name;
if (value == null) { dict[key] = ""; continue; }
var pt = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
if (IsSimple(pt))
dict[key] = ToStringInvariant(value, pt);
else
FlattenAny(value, value.GetType(), dict, key);
}
}
private static void UnflattenInto(object obj, Type objType, Dictionary<string, string> dict, string prefix)
{
foreach (var prop in objType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (!prop.CanWrite) continue;
if (prop.GetIndexParameters().Length != 0) continue;
var key = prefix.Length == 0 ? prop.Name : prefix + "." + prop.Name;
var pType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
if (IsSimple(pType))
{
if (dict.TryGetValue(key, out var raw))
{
try { prop.SetValue(obj, FromStringInvariant(raw, prop.PropertyType)); } catch { }
}
continue;
}
if (IsStringKeyDictionary(pType, out var valueType))
{
var keys = dict.Keys.Where(k => k.StartsWith(key + "[", StringComparison.OrdinalIgnoreCase)).ToArray();
if (keys.Length == 0) continue;
object? mapObj;
try { mapObj = Activator.CreateInstance(pType); } catch { mapObj = null; }
if (mapObj is not IDictionary map) continue;
foreach (var token in keys.Select(k => ExtractBracketToken(k, key)).Where(t => t != null).Distinct(StringComparer.OrdinalIgnoreCase)!)
{
var realKey = UnescapeToken(token!);
var baseKey = key + "[" + token + "]";
var vt = Nullable.GetUnderlyingType(valueType) ?? valueType;
if (IsSimple(vt))
{
if (dict.TryGetValue(baseKey, out var raw))
{
try { map[realKey] = FromStringInvariant(raw, valueType); } catch { }
}
}
else
{
object child = Activator.CreateInstance(valueType)!;
UnflattenInto(child, valueType, dict, baseKey);
map[realKey] = child;
}
}
try { prop.SetValue(obj, mapObj); } catch { }
continue;
}
if (IsListOrArray(pType, out var elementType, out var isArray))
{
var idxs = dict.Keys
.Where(k => k.StartsWith(key + "[", StringComparison.OrdinalIgnoreCase))
.Select(k => ExtractBracketIndex(k, key))
.Where(i => i >= 0)
.Distinct()
.OrderBy(i => i)
.ToArray();
if (idxs.Length == 0) continue;
var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType))!;
var et = Nullable.GetUnderlyingType(elementType) ?? elementType;
foreach (var i in idxs)
{
var baseKey = key + "[" + i.ToString(CultureInfo.InvariantCulture) + "]";
if (IsSimple(et))
{
if (dict.TryGetValue(baseKey, out var raw))
{
try { list.Add(FromStringInvariant(raw, elementType)); }
catch { list.Add(elementType.IsValueType ? Activator.CreateInstance(elementType) : null); }
}
else
{
list.Add(elementType.IsValueType ? Activator.CreateInstance(elementType) : null);
}
}
else
{
object child = Activator.CreateInstance(elementType)!;
UnflattenInto(child, elementType, dict, baseKey);
list.Add(child);
}
}
try
{
if (isArray)
{
var arr = Array.CreateInstance(elementType, list.Count);
list.CopyTo(arr, 0);
prop.SetValue(obj, arr);
}
else
{
prop.SetValue(obj, list);
}
}
catch { }
continue;
}
var childPrefix = key + ".";
if (!dict.Keys.Any(k => k.StartsWith(childPrefix, StringComparison.OrdinalIgnoreCase)))
continue;
if (prop.PropertyType.IsValueType)
{
object boxed = Activator.CreateInstance(prop.PropertyType)!;
UnflattenInto(boxed, prop.PropertyType, dict, key);
try { prop.SetValue(obj, boxed); } catch { }
continue;
}
object? childObj;
try { childObj = prop.GetValue(obj) ?? Activator.CreateInstance(prop.PropertyType); }
catch { continue; }
if (childObj == null) continue;
try { prop.SetValue(obj, childObj); } catch { continue; }
UnflattenInto(childObj, prop.PropertyType, dict, key);
}
}
private static string? ExtractBracketToken(string fullKey, string baseKey)
{
var start = baseKey.Length;
if (fullKey.Length <= start + 2) return null;
if (fullKey[start] != '[') return null;
var end = FindBracketEnd(fullKey, start);
if (end < 0) return null;
return fullKey.Substring(start + 1, end - start - 1);
}
private static int ExtractBracketIndex(string fullKey, string baseKey)
{
var token = ExtractBracketToken(fullKey, baseKey);
if (token == null) return -1;
return int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : -1;
}
private static int FindBracketEnd(string s, int bracketStartIndex)
{
for (var i = bracketStartIndex + 1; i < s.Length; i++)
{
if (s[i] == ']' && s[i - 1] != '\\')
return i;
}
return -1;
}
}
}
SavePosition.axaml代码
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="300" Width="300" x:Class="AvaloniaUI.SavePosition" Title="SavePosition"> <StackPanel Margin="10" HorizontalAlignment="Center" Spacing="5"> <Button Click="cmdSave_Click">Save Position and Size</Button> <Button Click="cmdRestore_Click">Restore Position and Size</Button> </StackPanel> </Window>
SavePosition.axaml.cs代码
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Shares;
using System;
using System.IO;
namespace AvaloniaUI;
public sealed class WindowPlacement
{
public int X { get; set; }
public int Y { get; set; }
public double Width { get; set; } = 300;
public double Height { get; set; } = 300;
public bool IsMaximized { get; set; }
}
public partial class SavePosition : Window
{
private readonly Setting setting;
private const string sectionName = "MainWindow";
public SavePosition()
{
InitializeComponent();
var path = Path.Combine(AppContext.BaseDirectory, "settings.json");
//Console.WriteLine(path);
setting = new Setting(path);
}
private void cmdSave_Click(object? sender, RoutedEventArgs e)
{
var placement = CapturePlacement();
setting.Set(sectionName, placement);
}
private void cmdRestore_Click(object? sender, RoutedEventArgs e)
{
var placement = setting.Get<WindowPlacement>(sectionName);
ApplyPlacement(placement);
}
private WindowPlacement CapturePlacement()
{
// 最大化时保存当前位置和 ClientSize 意义不大,但可以保存一个 IsMaximized 供恢复使用
var pos = Position;
var size = ClientSize;
return new WindowPlacement
{
X = pos.X,
Y = pos.Y,
Width = Math.Max(200, size.Width),
Height = Math.Max(200, size.Height),
IsMaximized = WindowState == WindowState.Maximized
};
}
private void ApplyPlacement(WindowPlacement placement)
{
// 先还原,避免最大化状态下设置尺寸/位置无效
WindowState = WindowState.Normal;
Width = placement.Width > 200 ? placement.Width : 300;
Height = placement.Height > 200 ? placement.Height : 300;
Position = new PixelPoint(placement.X, placement.Y);
if (placement.IsMaximized)
WindowState = WindowState.Maximized;
}
}
运行效果

浙公网安备 33010602011771号