增加单实例模式和命令行参数

This commit is contained in:
ww-rm
2025-09-20 23:11:47 +08:00
parent 3d649e36cc
commit 3a3691bcca
5 changed files with 197 additions and 20 deletions

View File

@@ -1,10 +1,13 @@
using NLog; using NLog;
using SpineViewer.Natives;
using SpineViewer.Views; using SpineViewer.Views;
using System.Collections.Frozen; using System.Collections.Frozen;
using System.Configuration; using System.Configuration;
using System.Data; using System.Data;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO;
using System.IO.Pipes;
using System.Reflection; using System.Reflection;
using System.Windows; using System.Windows;
@@ -15,9 +18,15 @@ namespace SpineViewer
/// </summary> /// </summary>
public partial class App : Application public partial class App : Application
{ {
public static string Version => Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion; public static readonly string ExeFilePath = 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 Logger _logger;
private static readonly Mutex _instanceMutex;
static App() static App()
{ {
@@ -35,6 +44,17 @@ namespace SpineViewer
_logger.Error("Unobserved task exception: {0}", e.Exception.Message); _logger.Error("Unobserved task exception: {0}", e.Exception.Message);
e.SetObserved(); e.SetObserved();
}; };
// 单例模式加 IPC 通信
_instanceMutex = new Mutex(true, MutexName, out var createdNew);
if (!createdNew)
{
ShowExistedInstance();
SendCommandLineArgs();
Environment.Exit(0); // 不再启动新实例
return;
}
StartPipeServer();
} }
private static void InitializeLogConfiguration() private static void InitializeLogConfiguration()
@@ -50,7 +70,9 @@ namespace SpineViewer
ArchiveNumbering = NLog.Targets.ArchiveNumberingMode.Rolling, ArchiveNumbering = NLog.Targets.ArchiveNumberingMode.Rolling,
ArchiveAboveSize = 1048576, ArchiveAboveSize = 1048576,
MaxArchiveFiles = 5, 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); config.AddTarget(fileTarget);
@@ -58,20 +80,107 @@ namespace SpineViewer
LogManager.Configuration = config; 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(1000); // 等待 1 秒
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 (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(() =>
{
if (Current?.MainWindow is MainWindow mainWindow)
mainWindow.OpenFiles(args);
});
}
}
}
}
}, default, TaskCreationOptions.LongRunning);
t.Start();
}
protected override void OnStartup(StartupEventArgs e) protected override void OnStartup(StartupEventArgs e)
{ {
// 正式启动窗口
base.OnStartup(e); base.OnStartup(e);
var dict = new ResourceDictionary();
var uiCulture = CultureInfo.CurrentUICulture.Name.ToLowerInvariant(); var uiCulture = CultureInfo.CurrentUICulture.Name.ToLowerInvariant();
_logger.Info("Current UI Culture: {0}", uiCulture); _logger.Info("Current UI Culture: {0}", uiCulture);
if (uiCulture.StartsWith("zh")) { } // 默认就是中文, 无需操作 if (uiCulture.StartsWith("zh")) { } // 默认就是中文, 无需操作
else if (uiCulture.StartsWith("ja")) Language = AppLanguage.JA; else if (uiCulture.StartsWith("ja")) Language = AppLanguage.JA;
else Language = AppLanguage.EN; else Language = AppLanguage.EN;
Resources.MergedDictionaries.Add(dict);
} }
private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)

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 namespace SpineViewer.Natives
{ {
/// <summary> /// <summary>
/// Win32 Sdk 包装类 /// user32.dll 包装类
/// </summary> /// </summary>
public static class Win32 public static class User32
{ {
public const int GWL_STYLE = -16; public const int GWL_STYLE = -16;
public const int WS_SIZEBOX = 0x40000; public const int WS_SIZEBOX = 0x40000;
@@ -178,17 +178,11 @@ namespace SpineViewer.Natives
[DllImport("user32.dll", SetLastError = true)] [DllImport("user32.dll", SetLastError = true)]
public static extern bool ShowWindow(nint hWnd, int nCmdShow); public static extern bool ShowWindow(nint hWnd, int nCmdShow);
[DllImport("gdi32.dll", SetLastError = true)] [DllImport("user32.dll")]
public static extern nint CreateCompatibleDC(nint hdc); public static extern bool IsIconic(IntPtr hWnd);
[DllImport("gdi32.dll", SetLastError = true)] [DllImport("user32.dll")]
public static extern bool DeleteDC(nint hdc); public static extern bool SetForegroundWindow(IntPtr hWnd);
[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")] [DllImport("user32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags); private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);

View File

@@ -49,6 +49,7 @@ public partial class MainWindow : Window
_vm.SpineObjectListViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging; _vm.SpineObjectListViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
_vm.SFMLRendererViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging; _vm.SFMLRendererViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
Loaded += MainWindow_Loaded; Loaded += MainWindow_Loaded;
ContentRendered += MainWindow_ContentRendered;
Closed += MainWindow_Closed; Closed += MainWindow_Closed;
} }
@@ -75,6 +76,16 @@ public partial class MainWindow : Window
LoadLastState(); 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) private void MainWindow_Closed(object? sender, EventArgs e)
{ {
SaveLastState(); SaveLastState();
@@ -83,6 +94,14 @@ public partial class MainWindow : Window
vm.StopRender(); vm.StopRender();
} }
/// <summary>
/// 给管道通信提供的打开文件外部调用方法
/// </summary>
public void OpenFiles(IEnumerable<string> filePaths)
{
_vm.SpineObjectListViewModel.AddSpineObjectFromFileList(filePaths);
}
/// <summary> /// <summary>
/// 初始化窗口日志器 /// 初始化窗口日志器
/// </summary> /// </summary>
@@ -193,6 +212,9 @@ public partial class MainWindow : Window
default: default:
break; break;
} }
// 如果选中项发生变化也强制转移焦点
_spinesListView.Focus();
} }
private void SpinesListView_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) private void SpinesListView_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
@@ -302,7 +324,7 @@ public partial class MainWindow : Window
if (_fullScreenLayout.Visibility == Visibility.Visible) return; if (_fullScreenLayout.Visibility == Visibility.Visible) return;
IntPtr hwnd = new WindowInteropHelper(this).Handle; 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); _vm.SFMLRendererViewModel.SetResolution(resX, resY);
} }