diff --git a/SpineViewer/App.xaml.cs b/SpineViewer/App.xaml.cs index bbd70a0..73b3e78 100644 --- a/SpineViewer/App.xaml.cs +++ b/SpineViewer/App.xaml.cs @@ -1,4 +1,5 @@ -using NLog; +using Microsoft.Win32; +using NLog; using SpineViewer.Natives; using SpineViewer.Views; using System.Collections.Frozen; @@ -18,15 +19,28 @@ namespace SpineViewer /// public partial class App : Application { +#if DEBUG + public const string AppName = "SpineViewer_D"; + public const string ProgId = "SpineViewer_D.skel"; +#else + public const string AppName = "SpineViewer"; public const string ProgId = "SpineViewer.skel"; +#endif + + public const string AutoRunFlag = "--autorun"; + private const string MutexName = "__SpineViewerInstance__"; + private const string PipeName = "__SpineViewerPipe__"; 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 string AutoRunCommand = $"\"{ProcessPath}\" {AutoRunFlag}"; + + private static readonly string SkelFileDescription = $"SpineViewer File"; + private static readonly string SkelIconFilePath = Path.Combine(ProcessDirectory, "Resources\\Images\\skel.ico"); + private static readonly string ShellOpenCommand = $"\"{ProcessPath}\" \"%1\""; private static readonly Logger _logger; private static readonly Mutex _instanceMutex; @@ -87,7 +101,7 @@ namespace SpineViewer { try { - // 2. 遍历同名进程 + // 遍历同名进程 var processes = Process.GetProcessesByName(ProcessName); foreach (var p in processes) { @@ -199,6 +213,116 @@ namespace SpineViewer e.Handled = true; } + public bool AutoRun + { + get + { + try + { + using (var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run")) + { + var command = key?.GetValue(AppName) as string; + return string.Equals(command, AutoRunCommand, StringComparison.OrdinalIgnoreCase); + } + } + catch (Exception ex) + { + _logger.Error("Failed to query autorun registry key, {0}", ex.Message); + _logger.Trace(ex.ToString()); + return false; + } + } + set + { + try + { + if (value) + { + // 写入自启命令 + using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run")) + { + key?.SetValue(AppName, AutoRunCommand); + } + } + else + { + // 删除自启命令 + using (var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run")) + { + key?.DeleteValue(AppName, false); + } + } + } + catch (Exception ex) + { + _logger.Error("Failed to set autorun registry key, {0}", ex.Message); + _logger.Trace(ex.ToString()); + } + } + } + + 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 + { + if (value) + { + // 文件关联 + 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.NotifyAssociationChanged(); + } + } + /// /// 程序语言 /// @@ -221,7 +345,6 @@ namespace SpineViewer } } private AppLanguage _language = AppLanguage.ZH; - } public enum AppLanguage @@ -230,4 +353,4 @@ namespace SpineViewer EN, JA } -} \ No newline at end of file +} diff --git a/SpineViewer/Models/PreferenceModel.cs b/SpineViewer/Models/PreferenceModel.cs index 635ec10..aeebef6 100644 --- a/SpineViewer/Models/PreferenceModel.cs +++ b/SpineViewer/Models/PreferenceModel.cs @@ -1,5 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using Spine.SpineWrappers; +using SpineViewer.Services; using System; using System.Collections.Generic; using System.IO; @@ -73,6 +75,12 @@ namespace SpineViewer.Models #region 程序选项 + [ObservableProperty] + private bool _autoRun; + + [ObservableProperty] + private string _autoRunWorkspaceConfigPath; + [ObservableProperty] private bool _wallpaperView; @@ -85,6 +93,14 @@ namespace SpineViewer.Models [ObservableProperty] private AppLanguage _appLanguage; + public RelayCommand Cmd_SelectAutoRunWorkspaceConfigPath => _cmd_SelectAutoRunWorkspaceConfigPath ??= new(() => + { + if (!DialogService.ShowOpenJsonDialog(out var fileName)) + return; + AutoRunWorkspaceConfigPath = fileName; + }); + private RelayCommand? _cmd_SelectAutoRunWorkspaceConfigPath; + #endregion } } diff --git a/SpineViewer/Resources/Strings/en.xaml b/SpineViewer/Resources/Strings/en.xaml index 9d7a0d5..2099061 100644 --- a/SpineViewer/Resources/Strings/en.xaml +++ b/SpineViewer/Resources/Strings/en.xaml @@ -17,6 +17,11 @@ Preferences... Exit + + Auto Start + Auto-load Workspace File on Startup + Specifies the workspace configuration file to be automatically loaded when the program starts with Windows startup. This takes effect only if auto-startup is enabled. + Explorer Model diff --git a/SpineViewer/Resources/Strings/ja.xaml b/SpineViewer/Resources/Strings/ja.xaml index e1fb869..3612f9d 100644 --- a/SpineViewer/Resources/Strings/ja.xaml +++ b/SpineViewer/Resources/Strings/ja.xaml @@ -17,6 +17,11 @@ 設定... 終了 + + 自動起動 + 起動時にワークスペースファイルを自動読み込み + プログラムが Windows 起動と同時に自動起動した場合に、自動的に読み込むワークスペース設定ファイルを指定します。自動起動が有効な場合にのみ適用されます。 + エクスプローラー モデル diff --git a/SpineViewer/Resources/Strings/zh.xaml b/SpineViewer/Resources/Strings/zh.xaml index 05f485d..d4df843 100644 --- a/SpineViewer/Resources/Strings/zh.xaml +++ b/SpineViewer/Resources/Strings/zh.xaml @@ -16,6 +16,11 @@ 首选项 首选项... 退出 + + + 开机自启 + 自启动加载工作区文件 + 设置程序开机自启后自动加载的工作区配置文件,仅在启用开机自启时生效 浏览 diff --git a/SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs b/SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs index 044e33a..45b2486 100644 --- a/SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs +++ b/SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs @@ -29,6 +29,13 @@ namespace SpineViewer.ViewModels.MainWindow public string Title => $"SpineViewer - v{App.Version}"; + public string AutoRunWorkspaceConfigPath + { + get => _autoRunWorkspaceConfigPath; + set => SetProperty(ref _autoRunWorkspaceConfigPath, value); + } + private string _autoRunWorkspaceConfigPath; + /// /// SFML 渲染对象 /// diff --git a/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs b/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs index bcdefb7..8a7d27e 100644 --- a/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs +++ b/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs @@ -26,10 +26,6 @@ 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; @@ -111,6 +107,8 @@ namespace SpineViewer.ViewModels.MainWindow DebugPoints = DebugPoints, DebugClippings = DebugClippings, + AutoRun = AutoRun, + AutoRunWorkspaceConfigPath = AutoRunWorkspaceConfigPath, WallpaperView = WallpaperView, RenderSelectedOnly = RenderSelectedOnly, AssociateFileSuffix = AssociateFileSuffix, @@ -137,6 +135,8 @@ namespace SpineViewer.ViewModels.MainWindow DebugPoints = value.DebugPoints; DebugClippings = value.DebugClippings; + AutoRun = value.AutoRun; + AutoRunWorkspaceConfigPath = value.AutoRunWorkspaceConfigPath; WallpaperView = value.WallpaperView; RenderSelectedOnly = value.RenderSelectedOnly; AssociateFileSuffix = value.AssociateFileSuffix; @@ -248,16 +248,21 @@ namespace SpineViewer.ViewModels.MainWindow public bool AutoRun { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); + get => ((App)App.Current).AutoRun; + set => SetProperty(((App)App.Current).AutoRun, value, v => ((App)App.Current).AutoRun = v); + } + + public string AutoRunWorkspaceConfigPath + { + get => _vmMain.AutoRunWorkspaceConfigPath; + set => SetProperty(_vmMain.AutoRunWorkspaceConfigPath, value, v => _vmMain.AutoRunWorkspaceConfigPath = v); } public bool WallpaperView { - get => _wallpaperView; - set => SetProperty(ref _wallpaperView, value); + get => _vmMain.SFMLRendererViewModel.WallpaperView; + set => SetProperty(_vmMain.SFMLRendererViewModel.WallpaperView, value, v => _vmMain.SFMLRendererViewModel.WallpaperView = v); } - private bool _wallpaperView; // UI 变化通过 PropertyChanged 事件交由 View 层处理 public bool RenderSelectedOnly { @@ -267,67 +272,8 @@ namespace SpineViewer.ViewModels.MainWindow 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.NotifyAssociationChanged(); - }); - } + get => ((App)App.Current).AssociateFileSuffix; + set => SetProperty(((App)App.Current).AssociateFileSuffix, value, v => ((App)App.Current).AssociateFileSuffix = v); } public AppLanguage AppLanguage diff --git a/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs b/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs index cdfb2bb..dbf51f5 100644 --- a/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs +++ b/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs @@ -240,6 +240,13 @@ namespace SpineViewer.ViewModels.MainWindow } private Stretch _backgroundImageMode = Stretch.Uniform; + public bool WallpaperView + { + get => _wallpaperView; + set => SetProperty(ref _wallpaperView, value); + } + private bool _wallpaperView; + public bool RenderSelectedOnly { get => _renderSelectedOnly; @@ -465,10 +472,13 @@ namespace SpineViewer.ViewModels.MainWindow } using var v = _renderer.GetView(); - _wallpaperRenderer.SetView(v); - _renderer.Clear(_backgroundColor); - _wallpaperRenderer.Clear(_backgroundColor); + + if (_wallpaperView) + { + _wallpaperRenderer.SetView(v); + _wallpaperRenderer.Clear(_backgroundColor); + } // 渲染背景 lock (_bgLock) @@ -499,7 +509,11 @@ namespace SpineViewer.ViewModels.MainWindow bg.Position = view.Center; bg.Rotation = view.Rotation; _renderer.Draw(bg); - _wallpaperRenderer.Draw(bg); + + if (_wallpaperView) + { + _wallpaperRenderer.Draw(bg); + } } } @@ -539,12 +553,20 @@ namespace SpineViewer.ViewModels.MainWindow sp.EnableDebug = true; _renderer.Draw(sp); sp.EnableDebug = false; - _wallpaperRenderer.Draw(sp); + + if (_wallpaperView) + { + _wallpaperRenderer.Draw(sp); + } } } _renderer.Display(); - _wallpaperRenderer.Display(); + + if (_wallpaperView) + { + _wallpaperRenderer.Display(); + } } } catch (Exception ex) diff --git a/SpineViewer/Views/MainWindow.xaml.cs b/SpineViewer/Views/MainWindow.xaml.cs index 2e8b939..c821c24 100644 --- a/SpineViewer/Views/MainWindow.xaml.cs +++ b/SpineViewer/Views/MainWindow.xaml.cs @@ -69,7 +69,7 @@ public partial class MainWindow : Window _vm.SpineObjectListViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging; _vm.SFMLRendererViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging; - _vm.PreferenceViewModel.PropertyChanged += PreferenceViewModel_PropertyChanged; + _vm.SFMLRendererViewModel.PropertyChanged += SFMLRendererViewModel_PropertyChanged; } /// @@ -193,11 +193,26 @@ public partial class MainWindow : Window private void MainWindow_ContentRendered(object? sender, EventArgs e) { string[] args = Environment.GetCommandLineArgs(); - if (args.Length > 1) + + // 不带参数启动 + if (args.Length <= 1) + return; + + // 带一个参数启动, 允许提供一些启动选项 + if (args.Length == 2) { - string[] filePaths = args.Skip(1).ToArray(); - _vm.SpineObjectListViewModel.AddSpineObjectFromFileList(filePaths); + if (args[1] == App.AutoRunFlag) + { + var autoPath = _vm.AutoRunWorkspaceConfigPath; + if (!string.IsNullOrWhiteSpace(autoPath) && JsonHelper.Deserialize(autoPath, out var obj)) + _vm.Workspace = obj; + return; + } } + + // 其余提供了任意参数的情况 + string[] filePaths = args.Skip(1).ToArray(); + _vm.SpineObjectListViewModel.AddSpineObjectFromFileList(filePaths); } private void MainWindow_Closed(object? sender, EventArgs e) @@ -210,9 +225,9 @@ public partial class MainWindow : Window #endregion - #region PreferenceViewModel 事件处理 + #region SFMLRendererViewModel 事件处理 - private void PreferenceViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e) + private void SFMLRendererViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(PreferenceViewModel.WallpaperView)) { diff --git a/SpineViewer/Views/PreferenceDialog.xaml b/SpineViewer/Views/PreferenceDialog.xaml index 644cf8a..f90c5a5 100644 --- a/SpineViewer/Views/PreferenceDialog.xaml +++ b/SpineViewer/Views/PreferenceDialog.xaml @@ -53,115 +53,219 @@ - - - - - - - - - - + + + + + + + - + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + - + - - - - - - - - - - - + + + + + + + + + + + + + +