diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb95ec3..7db9d43 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# CHANGELOG
+## v0.15.16
+
+- 修改模型添加顺序, 每次向顶层添加
+- 添加模型后自动选中最近添加的模型S
+- 点击预览画面或者选中项发生变化时转移焦点至列表
+- 增加移除全部菜单项
+- 增加单例模式和命令行文件参数
+- 增加文件关联设置
+
## v0.15.15
- 增加报错信息
diff --git a/Spine/Spine.csproj b/Spine/Spine.csproj
index 5c50778..b3637bc 100644
--- a/Spine/Spine.csproj
+++ b/Spine/Spine.csproj
@@ -7,7 +7,7 @@
net8.0-windows
$(SolutionDir)out
false
- 0.15.15
+ 0.15.16
diff --git a/SpineViewer/App.xaml.cs b/SpineViewer/App.xaml.cs
index 0184348..bbd70a0 100644
--- a/SpineViewer/App.xaml.cs
+++ b/SpineViewer/App.xaml.cs
@@ -1,10 +1,13 @@
using NLog;
+using SpineViewer.Natives;
using SpineViewer.Views;
using System.Collections.Frozen;
using System.Configuration;
using System.Data;
using System.Diagnostics;
using System.Globalization;
+using System.IO;
+using System.IO.Pipes;
using System.Reflection;
using System.Windows;
@@ -15,9 +18,18 @@ namespace SpineViewer
///
public partial class App : Application
{
- public static string Version => Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion;
+ public const string ProgId = "SpineViewer.skel";
+
+ public static readonly string ProcessPath = Environment.ProcessPath;
+ public static readonly string ProcessDirectory = Path.GetDirectoryName(Environment.ProcessPath);
+ public static readonly string ProcessName = Process.GetCurrentProcess().ProcessName;
+ public static readonly string Version = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion;
+
+ private const string MutexName = "SpineViewerInstance";
+ private const string PipeName = "SpineViewerPipe";
private static readonly Logger _logger;
+ private static readonly Mutex _instanceMutex;
static App()
{
@@ -35,6 +47,17 @@ namespace SpineViewer
_logger.Error("Unobserved task exception: {0}", e.Exception.Message);
e.SetObserved();
};
+
+ // 单例模式加 IPC 通信
+ _instanceMutex = new Mutex(true, MutexName, out var createdNew);
+ if (!createdNew)
+ {
+ ShowExistedInstance();
+ SendCommandLineArgs();
+ Environment.Exit(0); // 不再启动新实例
+ return;
+ }
+ StartPipeServer();
}
private static void InitializeLogConfiguration()
@@ -50,7 +73,9 @@ namespace SpineViewer
ArchiveNumbering = NLog.Targets.ArchiveNumberingMode.Rolling,
ArchiveAboveSize = 1048576,
MaxArchiveFiles = 5,
- Layout = "${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${level:uppercase=true} - ${callsite-filename:includeSourcePath=false}:${callsite-linenumber} - ${message}"
+ Layout = "${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${level:uppercase=true} - ${processid} - ${callsite-filename:includeSourcePath=false}:${callsite-linenumber} - ${message}",
+ ConcurrentWrites = true,
+ KeepFileOpen = false,
};
config.AddTarget(fileTarget);
@@ -58,20 +83,113 @@ namespace SpineViewer
LogManager.Configuration = config;
}
+ private static void ShowExistedInstance()
+ {
+ try
+ {
+ // 2. 遍历同名进程
+ var processes = Process.GetProcessesByName(ProcessName);
+ foreach (var p in processes)
+ {
+ // 跳过当前进程
+ if (p.Id == Process.GetCurrentProcess().Id)
+ continue;
+
+ IntPtr hWnd = p.MainWindowHandle;
+ if (hWnd != IntPtr.Zero)
+ {
+ // 3. 显示并置顶窗口
+ if (User32.IsIconic(hWnd))
+ {
+ User32.ShowWindow(hWnd, User32.SW_RESTORE);
+ }
+ User32.SetForegroundWindow(hWnd);
+ break; // 找到一个就可以退出
+ }
+ }
+ }
+ catch
+ {
+ // 忽略异常,不影响当前进程退出
+ }
+ }
+
+ private static void SendCommandLineArgs()
+ {
+ var args = Environment.GetCommandLineArgs().Skip(1).ToArray();
+ if (args.Length <= 0)
+ return;
+
+ _logger.Info("Send command line args to existed instance, \"{0}\"", string.Join(", ", args));
+ try
+ {
+ // 已有实例在运行,把参数通过命名管道发过去
+ using (var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out))
+ {
+ client.Connect(10000); // 10 秒超时
+ using (var writer = new StreamWriter(client))
+ {
+ foreach (var v in args)
+ {
+ writer.WriteLine(v);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Trace(ex.ToString());
+ _logger.Error("Failed to pass command line args to existed instance, {0}", ex.Message);
+ }
+ }
+
+ private static void StartPipeServer()
+ {
+ var t = new Task(() =>
+ {
+ while (Current is null) Thread.Sleep(10);
+ while (true)
+ {
+ var windowCreated = false;
+ Current.Dispatcher.Invoke(() => windowCreated = Current.MainWindow is MainWindow);
+ if (windowCreated)
+ break;
+ else
+ Thread.Sleep(100);
+ }
+ while (true)
+ {
+ using (var server = new NamedPipeServerStream(PipeName, PipeDirection.In))
+ {
+ server.WaitForConnection();
+ using (var reader = new StreamReader(server))
+ {
+ var args = new List();
+ string? line;
+ while ((line = reader.ReadLine()) != null)
+ args.Add(line);
+
+ if (args.Count > 0)
+ {
+ Current.Dispatcher.Invoke(() => ((MainWindow)Current.MainWindow).OpenFiles(args));
+ }
+ }
+ }
+ }
+ }, default, TaskCreationOptions.LongRunning);
+ t.Start();
+ }
+
protected override void OnStartup(StartupEventArgs e)
{
+ // 正式启动窗口
base.OnStartup(e);
-
- var dict = new ResourceDictionary();
-
var uiCulture = CultureInfo.CurrentUICulture.Name.ToLowerInvariant();
_logger.Info("Current UI Culture: {0}", uiCulture);
if (uiCulture.StartsWith("zh")) { } // 默认就是中文, 无需操作
else if (uiCulture.StartsWith("ja")) Language = AppLanguage.JA;
else Language = AppLanguage.EN;
-
- Resources.MergedDictionaries.Add(dict);
}
private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
diff --git a/SpineViewer/Models/PreferenceModel.cs b/SpineViewer/Models/PreferenceModel.cs
index afe9d3f..da94a82 100644
--- a/SpineViewer/Models/PreferenceModel.cs
+++ b/SpineViewer/Models/PreferenceModel.cs
@@ -76,6 +76,9 @@ namespace SpineViewer.Models
[ObservableProperty]
private bool _renderSelectedOnly;
+ [ObservableProperty]
+ private bool _associateFileSuffix;
+
[ObservableProperty]
private AppLanguage _appLanguage;
diff --git a/SpineViewer/Natives/Gdi32.cs b/SpineViewer/Natives/Gdi32.cs
new file mode 100644
index 0000000..6e97ae2
--- /dev/null
+++ b/SpineViewer/Natives/Gdi32.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace SpineViewer.Natives
+{
+ ///
+ /// gdi32.dll 包装类
+ ///
+ public static class Gdi32
+ {
+ [DllImport("gdi32.dll", SetLastError = true)]
+ public static extern nint CreateCompatibleDC(nint hdc);
+
+ [DllImport("gdi32.dll", SetLastError = true)]
+ public static extern bool DeleteDC(nint hdc);
+
+ [DllImport("gdi32.dll", SetLastError = true)]
+ public static extern nint SelectObject(nint hdc, nint hgdiobj);
+
+ [DllImport("gdi32.dll", SetLastError = true)]
+ public static extern bool DeleteObject(nint hObject);
+ }
+}
diff --git a/SpineViewer/Natives/Shell32.cs b/SpineViewer/Natives/Shell32.cs
new file mode 100644
index 0000000..a53286c
--- /dev/null
+++ b/SpineViewer/Natives/Shell32.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace SpineViewer.Natives
+{
+ ///
+ /// shell32.dll 包装类
+ ///
+ public static class Shell32
+ {
+ public const uint SHCNE_ASSOCCHANGED = 0x08000000;
+ public const uint SHCNF_IDLIST = 0x0000;
+
+ [DllImport("shell32.dll")]
+ public static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
+ }
+}
diff --git a/SpineViewer/Natives/Win32.cs b/SpineViewer/Natives/User32.cs
similarity index 94%
rename from SpineViewer/Natives/Win32.cs
rename to SpineViewer/Natives/User32.cs
index 58ad050..06d9714 100644
--- a/SpineViewer/Natives/Win32.cs
+++ b/SpineViewer/Natives/User32.cs
@@ -10,9 +10,9 @@ using System.Windows;
namespace SpineViewer.Natives
{
///
- /// Win32 Sdk 包装类
+ /// user32.dll 包装类
///
- public static class Win32
+ public static class User32
{
public const int GWL_STYLE = -16;
public const int WS_SIZEBOX = 0x40000;
@@ -178,17 +178,11 @@ namespace SpineViewer.Natives
[DllImport("user32.dll", SetLastError = true)]
public static extern bool ShowWindow(nint hWnd, int nCmdShow);
- [DllImport("gdi32.dll", SetLastError = true)]
- public static extern nint CreateCompatibleDC(nint hdc);
+ [DllImport("user32.dll")]
+ public static extern bool IsIconic(IntPtr hWnd);
- [DllImport("gdi32.dll", SetLastError = true)]
- public static extern bool DeleteDC(nint hdc);
-
- [DllImport("gdi32.dll", SetLastError = true)]
- public static extern nint SelectObject(nint hdc, nint hgdiobj);
-
- [DllImport("gdi32.dll", SetLastError = true)]
- public static extern bool DeleteObject(nint hObject);
+ [DllImport("user32.dll")]
+ public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
diff --git a/SpineViewer/Resources/AppResource.cs b/SpineViewer/Resources/AppResource.cs
index 0f076b0..8768b8d 100644
--- a/SpineViewer/Resources/AppResource.cs
+++ b/SpineViewer/Resources/AppResource.cs
@@ -19,6 +19,8 @@ namespace SpineViewer.Resources
public static string Str_GeneratePreviewsTitle => Get("Str_GeneratePreviewsTitle");
public static string Str_DeletePreviewsTitle => Get("Str_DeletePreviewsTitle");
public static string Str_AddSpineObjectsTitle => Get("Str_AddSpineObjectsTitle");
+ public static string Str_OpenSkelFileTitle => Get("Str_OpenSkelFileTitle");
+ public static string Str_OpenAtlasFileTitle => Get("Str_OpenAtlasFileTitle");
public static string Str_ReloadSpineObjectsTitle => Get("Str_ReloadSpineObjectsTitle");
public static string Str_CustomFFmpegExporterTitle => Get("Str_CustomFFmpegExporterTitle");
diff --git a/SpineViewer/Resources/Images/skel.ico b/SpineViewer/Resources/Images/skel.ico
new file mode 100644
index 0000000..1bc0a1d
Binary files /dev/null and b/SpineViewer/Resources/Images/skel.ico differ
diff --git a/SpineViewer/Resources/Images/skel.png b/SpineViewer/Resources/Images/skel.png
new file mode 100644
index 0000000..c8b006a
Binary files /dev/null and b/SpineViewer/Resources/Images/skel.png differ
diff --git a/SpineViewer/appicon.ico b/SpineViewer/Resources/Images/spineviewer.ico
similarity index 57%
rename from SpineViewer/appicon.ico
rename to SpineViewer/Resources/Images/spineviewer.ico
index f269c18..a74705d 100644
Binary files a/SpineViewer/appicon.ico and b/SpineViewer/Resources/Images/spineviewer.ico differ
diff --git a/SpineViewer/Resources/Images/spineviewer.png b/SpineViewer/Resources/Images/spineviewer.png
new file mode 100644
index 0000000..c1baefc
Binary files /dev/null and b/SpineViewer/Resources/Images/spineviewer.png differ
diff --git a/SpineViewer/Resources/Strings/en.xaml b/SpineViewer/Resources/Strings/en.xaml
index 8ad9ddf..f77be99 100644
--- a/SpineViewer/Resources/Strings/en.xaml
+++ b/SpineViewer/Resources/Strings/en.xaml
@@ -37,8 +37,11 @@
Show
{0} items, {1} selected
Add...
- Remove
+ Select Skeleton File (skel)
+ Select Atlas File (atlas)
Add from Clipboard
+ Remove
+ Remove All
Reload
Move Up
Move Down
@@ -232,6 +235,7 @@
Preview Options
Application Options
+ Associate File Extension
Language
diff --git a/SpineViewer/Resources/Strings/ja.xaml b/SpineViewer/Resources/Strings/ja.xaml
index 0ff33d6..6a84772 100644
--- a/SpineViewer/Resources/Strings/ja.xaml
+++ b/SpineViewer/Resources/Strings/ja.xaml
@@ -37,8 +37,11 @@
表示
全{0}件、選択中{1}件
追加...
- 削除
+ スケルトンファイルを選択(skel)
+ アトラスファイルを選択(atlas)
クリップボードから追加
+ 削除
+ すべて削除
再読み込み
上へ移動
下へ移動
@@ -232,6 +235,7 @@
プレビュー画面オプション
アプリケーションプション
+ ファイル拡張子を関連付ける
言語
diff --git a/SpineViewer/Resources/Strings/zh.xaml b/SpineViewer/Resources/Strings/zh.xaml
index 75eb715..f138b4c 100644
--- a/SpineViewer/Resources/Strings/zh.xaml
+++ b/SpineViewer/Resources/Strings/zh.xaml
@@ -37,8 +37,11 @@
显示
共 {0} 项,已选择 {1} 项
添加...
- 移除
+ 选择骨骼文件(skel)
+ 选择图集文件(atlas)
从剪贴板添加
+ 移除
+ 移除全部
重新加载
上移
下移
@@ -232,6 +235,7 @@
预览画面选项
应用程序选项
+ 关联文件后缀
语言
\ No newline at end of file
diff --git a/SpineViewer/Services/DialogService.cs b/SpineViewer/Services/DialogService.cs
index 25ac263..6e11135 100644
--- a/SpineViewer/Services/DialogService.cs
+++ b/SpineViewer/Services/DialogService.cs
@@ -61,6 +61,18 @@ namespace SpineViewer.Services
return dialog.ShowDialog() ?? false;
}
+ public static bool ShowOpenFileDialog(out string? fileName, string title = null, string filter = "")
+ {
+ var dialog = new OpenFileDialog() { Title = title, Filter = filter };
+ if (dialog.ShowDialog() is true)
+ {
+ fileName = dialog.FileName;
+ return true;
+ }
+ fileName = null;
+ return false;
+ }
+
///
/// 获取用户选择的文件夹
///
diff --git a/SpineViewer/SpineViewer.csproj b/SpineViewer/SpineViewer.csproj
index 4e75716..0376878 100644
--- a/SpineViewer/SpineViewer.csproj
+++ b/SpineViewer/SpineViewer.csproj
@@ -7,19 +7,22 @@
net8.0-windows
$(SolutionDir)out
false
- 0.15.15
+ 0.15.16
WinExe
true
$(NoWarn);NETSDK1206
- appicon.ico
+ Resources\Images\spineviewer.ico
app.manifest
-
+
-
+
+ PreserveNewest
+
+
diff --git a/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs b/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs
index 19ce571..950c304 100644
--- a/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs
+++ b/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs
@@ -1,13 +1,16 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
+using Microsoft.Win32;
using NLog;
using Spine.SpineWrappers;
using SpineViewer.Models;
+using SpineViewer.Natives;
using SpineViewer.Services;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
@@ -23,6 +26,10 @@ namespace SpineViewer.ViewModels.MainWindow
///
public static readonly string PreferenceFilePath = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "preference.json");
+ private static readonly string SkelFileDescription = "SpineViewer File";
+ private static readonly string SkelIconFilePath = Path.Combine(App.ProcessDirectory, "Resources\\Images\\skel.ico");
+ private static readonly string ShellOpenCommand = $"\"{App.ProcessPath}\" \"%1\"";
+
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private readonly MainWindowViewModel _vmMain;
@@ -63,8 +70,19 @@ namespace SpineViewer.ViewModels.MainWindow
///
public void LoadPreference()
{
- if (JsonHelper.Deserialize(PreferenceFilePath, out var obj, true))
- Preference = obj;
+ if (JsonHelper.Deserialize(PreferenceFilePath, out var obj, true))
+ {
+ try
+ {
+ Preference = obj;
+ }
+ catch (Exception ex)
+ {
+
+ _logger.Trace(ex.ToString());
+ _logger.Error("Failed to load some prefereneces, {0}", ex.Message);
+ }
+ }
}
///
@@ -94,6 +112,7 @@ namespace SpineViewer.ViewModels.MainWindow
DebugClippings = DebugClippings,
RenderSelectedOnly = RenderSelectedOnly,
+ AssociateFileSuffix = AssociateFileSuffix,
AppLanguage = AppLanguage,
};
}
@@ -118,6 +137,7 @@ namespace SpineViewer.ViewModels.MainWindow
DebugClippings = value.DebugClippings;
RenderSelectedOnly = value.RenderSelectedOnly;
+ AssociateFileSuffix = value.AssociateFileSuffix;
AppLanguage = value.AppLanguage;
}
}
@@ -230,6 +250,71 @@ namespace SpineViewer.ViewModels.MainWindow
set => SetProperty(_vmMain.SFMLRendererViewModel.RenderSelectedOnly, value, v => _vmMain.SFMLRendererViewModel.RenderSelectedOnly = v);
}
+ public bool AssociateFileSuffix
+ {
+ get
+ {
+ try
+ {
+ // 检查 .skel 的 ProgID
+ using (var key = Registry.CurrentUser.OpenSubKey(@"Software\Classes\.skel"))
+ {
+ var progIdValue = key?.GetValue("") as string;
+ if (!string.Equals(progIdValue, App.ProgId, StringComparison.OrdinalIgnoreCase))
+ return false;
+ }
+
+ // 检查 command 指令是否相同
+ using (var key = Registry.CurrentUser.OpenSubKey($@"Software\Classes\{App.ProgId}\shell\open\command"))
+ {
+ var command = key?.GetValue("") as string;
+ if (string.IsNullOrWhiteSpace(command))
+ return false;
+ return command == ShellOpenCommand;
+ }
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ set
+ {
+ SetProperty(AssociateFileSuffix, value, v =>
+ {
+ if (v)
+ {
+ // 文件关联
+ using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Classes\.skel"))
+ {
+ key?.SetValue("", App.ProgId);
+ }
+
+ using (var key = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{App.ProgId}"))
+ {
+ key?.SetValue("", SkelFileDescription);
+ using (var iconKey = key?.CreateSubKey("DefaultIcon"))
+ {
+ iconKey?.SetValue("", $"\"{SkelIconFilePath}\"");
+ }
+ using (var shellKey = key?.CreateSubKey(@"shell\open\command"))
+ {
+ shellKey?.SetValue("", ShellOpenCommand);
+ }
+ }
+ }
+ else
+ {
+ // 删除关联
+ Registry.CurrentUser.DeleteSubKeyTree(@"Software\Classes\.skel", false);
+ Registry.CurrentUser.DeleteSubKeyTree($@"Software\Classes\{App.ProgId}", false);
+ }
+
+ Shell32.SHChangeNotify(Shell32.SHCNE_ASSOCCHANGED, Shell32.SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
+ });
+ }
+ }
+
public AppLanguage AppLanguage
{
get => ((App)App.Current).Language;
diff --git a/SpineViewer/ViewModels/MainWindow/SpineObjectListViewModel.cs b/SpineViewer/ViewModels/MainWindow/SpineObjectListViewModel.cs
index b17bcda..4ed4a70 100644
--- a/SpineViewer/ViewModels/MainWindow/SpineObjectListViewModel.cs
+++ b/SpineViewer/ViewModels/MainWindow/SpineObjectListViewModel.cs
@@ -107,7 +107,12 @@ namespace SpineViewer.ViewModels.MainWindow
private void AddSpineObject_Execute()
{
- MessagePopupService.Info("Not Implemented, please drag files into here or add them from clipboard :)");
+ if (!DialogService.ShowOpenFileDialog(out var skelFileName, AppResource.Str_OpenSkelFileTitle))
+ return;
+ if (!DialogService.ShowOpenFileDialog(out var atlasFileName, AppResource.Str_OpenAtlasFileTitle))
+ return;
+ AddSpineObject(skelFileName, atlasFileName);
+ _logger.LogCurrentProcessMemoryUsage();
}
///
@@ -144,6 +149,34 @@ namespace SpineViewer.ViewModels.MainWindow
return true;
}
+ ///
+ /// 移除全部模型
+ ///
+ public RelayCommand Cmd_RemoveAllSpineObject => _cmd_RemoveAllSpineObject ??= new(RemoveAllSpineObject_Execute, RemoveAllSpineObject_CanExecute);
+ private RelayCommand? _cmd_RemoveAllSpineObject;
+
+ private void RemoveAllSpineObject_Execute(IList? args)
+ {
+ if (!RemoveAllSpineObject_CanExecute(args)) return;
+
+ if (!MessagePopupService.Quest(string.Format(AppResource.Str_RemoveItemsQuest, args.Count)))
+ return;
+
+ lock (_spineObjectModels.Lock)
+ {
+ foreach (var sp in _spineObjectModels)
+ sp.Dispose();
+ _spineObjectModels.Clear();
+ }
+ }
+
+ private bool RemoveAllSpineObject_CanExecute(IList? args)
+ {
+ if (args is null) return false;
+ if (args.Count <= 0) return false;
+ return true;
+ }
+
///
/// 从剪贴板文件列表添加模型
///
@@ -463,7 +496,7 @@ namespace SpineViewer.ViewModels.MainWindow
{
if (ct.IsCancellationRequested) break;
- var skelPath = paths[i];
+ var skelPath = paths[totalCount - 1 - i]; // 从后往前添加, 每次插入到列表的第一个
reporter.ProgressText = $"[{i}/{totalCount}] {skelPath}";
if (AddSpineObject(skelPath))
@@ -486,7 +519,7 @@ namespace SpineViewer.ViewModels.MainWindow
}
///
- /// 安全地在末尾添加一个模型, 发生错误会输出日志
+ /// 安全地在列表头添加一个模型, 发生错误会输出日志
///
/// 是否添加成功
private bool AddSpineObject(string skelPath, string? atlasPath = null)
@@ -494,7 +527,7 @@ namespace SpineViewer.ViewModels.MainWindow
try
{
var sp = new SpineObjectModel(skelPath, atlasPath);
- lock (_spineObjectModels.Lock) _spineObjectModels.Add(sp);
+ lock (_spineObjectModels.Lock) _spineObjectModels.Insert(0, sp);
if (Application.Current.Dispatcher.CheckAccess())
{
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
@@ -518,35 +551,6 @@ namespace SpineViewer.ViewModels.MainWindow
return false;
}
- private bool AddSpineObject(SpineObjectWorkspaceConfigModel cfg)
- {
- try
- {
- var sp = new SpineObjectModel(cfg);
- lock (_spineObjectModels.Lock) _spineObjectModels.Add(sp);
- if (Application.Current.Dispatcher.CheckAccess())
- {
- RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
- RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
- }
- else
- {
- Application.Current.Dispatcher.Invoke(() =>
- {
- RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
- RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
- });
- }
- return true;
- }
- catch (Exception ex)
- {
- _logger.Trace(ex.ToString());
- _logger.Error("Failed to load: {0}, {1}", cfg.SkelPath, ex.Message);
- }
- return false;
- }
-
public List LoadedSpineObjects
{
get
@@ -609,7 +613,7 @@ namespace SpineViewer.ViewModels.MainWindow
{
if (ct.IsCancellationRequested) break;
- var cfg = models[i];
+ var cfg = models[totalCount - 1 - i]; // 从后往前添加, 每次插入到列表的第一个
reporter.ProgressText = $"[{i}/{totalCount}] {cfg}";
if (AddSpineObject(cfg))
@@ -637,5 +641,38 @@ namespace SpineViewer.ViewModels.MainWindow
sp.ResetAnimationsTime();
}
}
+
+ ///
+ /// 安全地在列表头添加一个模型, 发生错误会输出日志
+ ///
+ /// 是否添加成功
+ private bool AddSpineObject(SpineObjectWorkspaceConfigModel cfg)
+ {
+ try
+ {
+ var sp = new SpineObjectModel(cfg);
+ lock (_spineObjectModels.Lock) _spineObjectModels.Insert(0, sp);
+ if (Application.Current.Dispatcher.CheckAccess())
+ {
+ RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
+ RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
+ }
+ else
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
+ RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
+ });
+ }
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.Trace(ex.ToString());
+ _logger.Error("Failed to load: {0}, {1}", cfg.SkelPath, ex.Message);
+ }
+ return false;
+ }
}
}
diff --git a/SpineViewer/Views/MainWindow.xaml b/SpineViewer/Views/MainWindow.xaml
index 33d1779..bda9156 100644
--- a/SpineViewer/Views/MainWindow.xaml
+++ b/SpineViewer/Views/MainWindow.xaml
@@ -147,13 +147,16 @@
+
-
+