diff --git a/SpineViewer/App.xaml.cs b/SpineViewer/App.xaml.cs index 0184348..62672fc 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,15 @@ namespace SpineViewer /// public partial class App : Application { - public static string Version => Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion; + public static readonly string ExeFilePath = 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 +44,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 +70,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 +80,107 @@ 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(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? 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) { + // 正式启动窗口 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/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/Views/MainWindow.xaml.cs b/SpineViewer/Views/MainWindow.xaml.cs index 8379888..3688730 100644 --- a/SpineViewer/Views/MainWindow.xaml.cs +++ b/SpineViewer/Views/MainWindow.xaml.cs @@ -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; } @@ -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); }