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 @@ + - + { vm.CanvasMouseButtonPressed(s, e); _spinesListView.Focus(); }; // 用户点击画布后强制转移焦点至列表 _renderPanel.CanvasMouseMove += vm.CanvasMouseMove; _renderPanel.CanvasMouseButtonReleased += vm.CanvasMouseButtonReleased; @@ -75,6 +76,16 @@ public partial class MainWindow : Window LoadLastState(); } + private void MainWindow_ContentRendered(object? sender, EventArgs e) + { + string[] args = Environment.GetCommandLineArgs(); + if (args.Length > 1) + { + string[] filePaths = args.Skip(1).ToArray(); + _vm.SpineObjectListViewModel.AddSpineObjectFromFileList(filePaths); + } + } + private void MainWindow_Closed(object? sender, EventArgs e) { SaveLastState(); @@ -83,6 +94,14 @@ public partial class MainWindow : Window vm.StopRender(); } + /// + /// 给管道通信提供的打开文件外部调用方法 + /// + public void OpenFiles(IEnumerable filePaths) + { + _vm.SpineObjectListViewModel.AddSpineObjectFromFileList(filePaths); + } + /// /// 初始化窗口日志器 /// @@ -193,6 +212,9 @@ public partial class MainWindow : Window default: break; } + + // 如果选中项发生变化也强制转移焦点 + _spinesListView.Focus(); } private void SpinesListView_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) @@ -302,7 +324,7 @@ public partial class MainWindow : Window if (_fullScreenLayout.Visibility == Visibility.Visible) return; IntPtr hwnd = new WindowInteropHelper(this).Handle; - if (Win32.GetScreenResolution(hwnd, out var resX, out var resY)) + if (User32.GetScreenResolution(hwnd, out var resX, out var resY)) { _vm.SFMLRendererViewModel.SetResolution(resX, resY); } diff --git a/SpineViewer/Views/PreferenceDialog.xaml b/SpineViewer/Views/PreferenceDialog.xaml index b3c7dfb..37723e4 100644 --- a/SpineViewer/Views/PreferenceDialog.xaml +++ b/SpineViewer/Views/PreferenceDialog.xaml @@ -143,13 +143,17 @@ +