Merge pull request #101 from ww-rm/dev/wpf

v0.15.16
This commit is contained in:
ww-rm
2025-09-21 01:14:57 +08:00
committed by GitHub
22 changed files with 426 additions and 70 deletions

View File

@@ -1,5 +1,14 @@
# CHANGELOG
## v0.15.16
- 修改模型添加顺序, 每次向顶层添加
- 添加模型后自动选中最近添加的模型S
- 点击预览画面或者选中项发生变化时转移焦点至列表
- 增加移除全部菜单项
- 增加单例模式和命令行文件参数
- 增加文件关联设置
## v0.15.15
- 增加报错信息

View File

@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.15.15</Version>
<Version>0.15.16</Version>
</PropertyGroup>
<PropertyGroup>

View File

@@ -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
/// </summary>
public partial class App : Application
{
public static string Version => Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.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<AssemblyInformationalVersionAttribute>()?.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>();
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)

View File

@@ -76,6 +76,9 @@ namespace SpineViewer.Models
[ObservableProperty]
private bool _renderSelectedOnly;
[ObservableProperty]
private bool _associateFileSuffix;
[ObservableProperty]
private AppLanguage _appLanguage;

View File

@@ -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
{
/// <summary>
/// gdi32.dll 包装类
/// </summary>
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);
}
}

View File

@@ -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
{
/// <summary>
/// shell32.dll 包装类
/// </summary>
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);
}
}

View File

@@ -10,9 +10,9 @@ using System.Windows;
namespace SpineViewer.Natives
{
/// <summary>
/// Win32 Sdk 包装类
/// user32.dll 包装类
/// </summary>
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);

View File

@@ -19,6 +19,8 @@ namespace SpineViewer.Resources
public static string Str_GeneratePreviewsTitle => Get<string>("Str_GeneratePreviewsTitle");
public static string Str_DeletePreviewsTitle => Get<string>("Str_DeletePreviewsTitle");
public static string Str_AddSpineObjectsTitle => Get<string>("Str_AddSpineObjectsTitle");
public static string Str_OpenSkelFileTitle => Get<string>("Str_OpenSkelFileTitle");
public static string Str_OpenAtlasFileTitle => Get<string>("Str_OpenAtlasFileTitle");
public static string Str_ReloadSpineObjectsTitle => Get<string>("Str_ReloadSpineObjectsTitle");
public static string Str_CustomFFmpegExporterTitle => Get<string>("Str_CustomFFmpegExporterTitle");

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -37,8 +37,11 @@
<s:String x:Key="Str_Show">Show</s:String>
<s:String x:Key="Str_ListViewStatusBar">{0} items, {1} selected</s:String>
<s:String x:Key="Str_AddSpineObject">Add...</s:String>
<s:String x:Key="Str_RemoveSpineObject">Remove</s:String>
<s:String x:Key="Str_OpenSkelFileTitle">Select Skeleton File (skel)</s:String>
<s:String x:Key="Str_OpenAtlasFileTitle">Select Atlas File (atlas)</s:String>
<s:String x:Key="Str_AddSpineObjectFromClipboard">Add from Clipboard</s:String>
<s:String x:Key="Str_RemoveSpineObject">Remove</s:String>
<s:String x:Key="Str_RemoveAllSpineObject">Remove All</s:String>
<s:String x:Key="Str_Reload">Reload</s:String>
<s:String x:Key="Str_MoveUpSpineObject">Move Up</s:String>
<s:String x:Key="Str_MoveDownSpineObject">Move Down</s:String>
@@ -232,6 +235,7 @@
<s:String x:Key="Str_RendererPreference">Preview Options</s:String>
<s:String x:Key="Str_AppPreference">Application Options</s:String>
<s:String x:Key="Str_AssociateFileSuffix">Associate File Extension</s:String>
<s:String x:Key="Str_Language">Language</s:String>
</ResourceDictionary>

View File

@@ -37,8 +37,11 @@
<s:String x:Key="Str_Show">表示</s:String>
<s:String x:Key="Str_ListViewStatusBar">全{0}件、選択中{1}件</s:String>
<s:String x:Key="Str_AddSpineObject">追加...</s:String>
<s:String x:Key="Str_RemoveSpineObject">削除</s:String>
<s:String x:Key="Str_OpenSkelFileTitle">スケルトンファイルを選択skel</s:String>
<s:String x:Key="Str_OpenAtlasFileTitle">アトラスファイルを選択atlas</s:String>
<s:String x:Key="Str_AddSpineObjectFromClipboard">クリップボードから追加</s:String>
<s:String x:Key="Str_RemoveSpineObject">削除</s:String>
<s:String x:Key="Str_RemoveAllSpineObject">すべて削除</s:String>
<s:String x:Key="Str_Reload">再読み込み</s:String>
<s:String x:Key="Str_MoveUpSpineObject">上へ移動</s:String>
<s:String x:Key="Str_MoveDownSpineObject">下へ移動</s:String>
@@ -232,6 +235,7 @@
<s:String x:Key="Str_RendererPreference">プレビュー画面オプション</s:String>
<s:String x:Key="Str_AppPreference">アプリケーションプション</s:String>
<s:String x:Key="Str_AssociateFileSuffix">ファイル拡張子を関連付ける</s:String>
<s:String x:Key="Str_Language">言語</s:String>
</ResourceDictionary>

View File

@@ -37,8 +37,11 @@
<s:String x:Key="Str_Show">显示</s:String>
<s:String x:Key="Str_ListViewStatusBar">共 {0} 项,已选择 {1} 项</s:String>
<s:String x:Key="Str_AddSpineObject">添加...</s:String>
<s:String x:Key="Str_RemoveSpineObject">移除</s:String>
<s:String x:Key="Str_OpenSkelFileTitle">选择骨骼文件skel</s:String>
<s:String x:Key="Str_OpenAtlasFileTitle">选择图集文件atlas</s:String>
<s:String x:Key="Str_AddSpineObjectFromClipboard">从剪贴板添加</s:String>
<s:String x:Key="Str_RemoveSpineObject">移除</s:String>
<s:String x:Key="Str_RemoveAllSpineObject">移除全部</s:String>
<s:String x:Key="Str_Reload">重新加载</s:String>
<s:String x:Key="Str_MoveUpSpineObject">上移</s:String>
<s:String x:Key="Str_MoveDownSpineObject">下移</s:String>
@@ -232,6 +235,7 @@
<s:String x:Key="Str_RendererPreference">预览画面选项</s:String>
<s:String x:Key="Str_AppPreference">应用程序选项</s:String>
<s:String x:Key="Str_AssociateFileSuffix">关联文件后缀</s:String>
<s:String x:Key="Str_Language">语言</s:String>
</ResourceDictionary>

View File

@@ -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;
}
/// <summary>
/// 获取用户选择的文件夹
/// </summary>

View File

@@ -7,19 +7,22 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.15.15</Version>
<Version>0.15.16</Version>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);NETSDK1206</NoWarn>
<ApplicationIcon>appicon.ico</ApplicationIcon>
<ApplicationIcon>Resources\Images\spineviewer.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<Content Include="appicon.ico" />
<Content Include="Resources\Images\skel.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Images\spineviewer.ico" />
</ItemGroup>
<ItemGroup>

View File

@@ -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
/// </summary>
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;
@@ -64,7 +71,18 @@ namespace SpineViewer.ViewModels.MainWindow
public void LoadPreference()
{
if (JsonHelper.Deserialize<PreferenceModel>(PreferenceFilePath, out var obj, true))
Preference = obj;
{
try
{
Preference = obj;
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to load some prefereneces, {0}", ex.Message);
}
}
}
/// <summary>
@@ -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;

View File

@@ -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();
}
/// <summary>
@@ -144,6 +149,34 @@ namespace SpineViewer.ViewModels.MainWindow
return true;
}
/// <summary>
/// 移除全部模型
/// </summary>
public RelayCommand<IList?> Cmd_RemoveAllSpineObject => _cmd_RemoveAllSpineObject ??= new(RemoveAllSpineObject_Execute, RemoveAllSpineObject_CanExecute);
private RelayCommand<IList?>? _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;
}
/// <summary>
/// 从剪贴板文件列表添加模型
/// </summary>
@@ -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
}
/// <summary>
/// 安全地在末尾添加一个模型, 发生错误会输出日志
/// 安全地在列表头添加一个模型, 发生错误会输出日志
/// </summary>
/// <returns>是否添加成功</returns>
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<SpineObjectWorkspaceConfigModel> 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();
}
}
/// <summary>
/// 安全地在列表头添加一个模型, 发生错误会输出日志
/// </summary>
/// <returns>是否添加成功</returns>
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;
}
}
}

View File

@@ -147,13 +147,16 @@
<ContextMenu>
<MenuItem Header="{DynamicResource Str_AddSpineObject}"
Command="{Binding Cmd_AddSpineObject}"/>
<MenuItem Header="{DynamicResource Str_AddSpineObjectFromClipboard}"
InputGestureText="Ctrl+V"
Command="{Binding Cmd_AddSpineObjectFromClipboard}"/>
<MenuItem Header="{DynamicResource Str_RemoveSpineObject}"
InputGestureText="Delete"
Command="{Binding Cmd_RemoveSpineObject}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{DynamicResource Str_AddSpineObjectFromClipboard}"
InputGestureText="Ctrl+V"
Command="{Binding Cmd_AddSpineObjectFromClipboard}"/>
<MenuItem Header="{DynamicResource Str_RemoveAllSpineObject}"
Command="{Binding Cmd_RemoveAllSpineObject}"
CommandParameter="{Binding PlacementTarget.Items, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{DynamicResource Str_Reload}"
InputGestureText="Ctrl+R"
Command="{Binding Cmd_ReloadSpineObject}"

View File

@@ -49,6 +49,7 @@ public partial class MainWindow : Window
_vm.SpineObjectListViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
_vm.SFMLRendererViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
Loaded += MainWindow_Loaded;
ContentRendered += MainWindow_ContentRendered;
Closed += MainWindow_Closed;
}
@@ -56,7 +57,7 @@ public partial class MainWindow : Window
{
var vm = _vm.SFMLRendererViewModel;
_renderPanel.CanvasMouseWheelScrolled += vm.CanvasMouseWheelScrolled;
_renderPanel.CanvasMouseButtonPressed += vm.CanvasMouseButtonPressed;
_renderPanel.CanvasMouseButtonPressed += (s, e) => { 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();
}
/// <summary>
/// 给管道通信提供的打开文件外部调用方法
/// </summary>
public void OpenFiles(IEnumerable<string> filePaths)
{
_vm.SpineObjectListViewModel.AddSpineObjectFromFileList(filePaths);
}
/// <summary>
/// 初始化窗口日志器
/// </summary>
@@ -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);
}

View File

@@ -143,13 +143,17 @@
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_RenderSelectedOnly}"/>
<ToggleButton Grid.Row="0" Grid.Column="1" IsChecked="{Binding RenderSelectedOnly}"/>
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_Language}"/>
<ComboBox Grid.Row="1" Grid.Column="1"
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_AssociateFileSuffix}"/>
<ToggleButton Grid.Row="1" Grid.Column="1" IsChecked="{Binding AssociateFileSuffix}"/>
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_Language}"/>
<ComboBox Grid.Row="2" Grid.Column="1"
SelectedItem="{Binding AppLanguage}"
ItemsSource="{x:Static vm:PreferenceViewModel.AppLanguageOptions}"/>