Avatar ·

Как правильно создать однократно запускаемое WPF-приложение?

Всем привет! Я пишу WPF-приложение на C# и столкнулся с проблемой: если пользователь запускает приложение несколько раз, открывается несколько окон, что приводит к путанице и лишней нагрузке. Я хочу сделать так, чтобы при повторном запуске приложение либо фокусировалось на уже открытом окне, либо выводило сообщение, что программа уже запущена. Я пробовал использовать `Process.GetProcessesByName`, но это ненадёжно, так как может быть несколько процессов с одинаковым именем. Пытался искать в интернете, но нашёл много разных подходов — через `Mutex`, через `EventWaitHandle`, через `NamedPipe`. Я новичок и не понимаю, какой способ считается правильным и безопасным. Вот пример моего кода (App.xaml.cs), который я пытаюсь модифицировать: ```csharp public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { // Тут я хочу добавить проверку на одиночный экземпляр base.OnStartup(e); } } ``` Подскажите, пожалуйста, как правильно реализовать single-instance WPF-приложение с учётом современных рекомендаций? Важно, чтобы решение работало на Windows и не вызывало проблем с потоками. Заранее спасибо! <<>>

Решение через Mutex с передачей фокуса существующему окну

Самый надёжный и распространённый способ создания однократно запускаемого WPF-приложения — использование Mutex (мьютекса) в сочетании с механизмом отправки сообщений для фокусировки уже открытого окна. Этот подход гарантирует, что даже при одновременном запуске нескольких процессов только один из них продолжит работу.

Вот полный пример реализации в файле App.xaml.cs:

using System;
using System.Threading;
using System.Windows;

public partial class App : Application
{
    private Mutex _mutex;
    private const string AppMutexName = "UniqueAppName-{8E4F5A6B-1C2D-3E4F-5A6B-7C8D9E0F1A2B}"; // GUID для уникальности

    [STAThread]
    public static void Main()
    {
        bool createdNew;
        using (_mutex = new Mutex(true, AppMutexName, out createdNew))
        {
            if (createdNew)
            {
                // Первый запуск — создаём приложение
                var app = new App();
                app.InitializeComponent();
                app.Run();
            }
            else
            {
                // Повторный запуск — фокусируем существующее окно
                // Используем User32 API для отправки сообщения
                var handle = FindWindow(null, "MainWindowTitle"); // Замените на заголовок вашего окна
                if (handle != IntPtr.Zero)
                {
                    ShowWindow(handle, SW_RESTORE);
                    SetForegroundWindow(handle);
                }
            }
        }
    }

    [System.Runtime.InteropServices.DllImport("user32.dll")]
    private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

    [System.Runtime.InteropServices.DllImport("user32.dll")]
    private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

    [System.Runtime.InteropServices.DllImport("user32.dll")]
    private static extern bool SetForegroundWindow(IntPtr hWnd);

    private const int SW_RESTORE = 9;
}

Пояснение работы

  • Mutex (мьютекс) — системный объект синхронизации. При создании с флагом true первый запуск получает право на выполнение, а последующие — нет.
  • GUID в имени мьютекса гарантирует уникальность и предотвращает конфликты с другими приложениями.
  • API User32 (FindWindow, ShowWindow, SetForegroundWindow) используется для поиска и активации уже открытого окна.
  • Метод Main с атрибутом STAThread обязателен для WPF-приложений.

Альтернативный способ с передачей аргументов через канал

Если вам нужно передать аргументы командной строки в уже запущенное приложение (например, открыть файл), используйте NamedPipeServerStream или WindowsFormsApplicationBase из Microsoft.VisualBasic.dll:

// Добавьте ссылку на Microsoft.VisualBasic
using Microsoft.VisualBasic.ApplicationServices;

public class SingleInstanceApp : WindowsFormsApplicationBase
{
    public SingleInstanceApp()
    {
        this.IsSingleInstance = true;
        this.StartupNextInstance += OnStartupNextInstance;
    }

    private void OnStartupNextInstance(object sender, StartupNextInstanceEventArgs e)
    {
        // Обрабатываем аргументы (например, открываем файл)
        var mainWindow = Application.Current.MainWindow;
        if (mainWindow != null)
        {
            mainWindow.Show();
            mainWindow.Activate();
        }
    }
}

// В App.xaml.cs
public partial class App : Application
{
    [STAThread]
    public static void Main()
    {
        var app = new SingleInstanceApp();
        app.Run(Environment.GetCommandLineArgs());
    }
}

Рекомендации:

  • Всегда обрабатывайте исключения при работе с мьютексом (например, AbandonedMutexException).
  • Для отладки используйте уникальное имя мьютекса, отличное от финального.
  • Не забывайте вызывать _mutex.ReleaseMutex() при завершении приложения.
<<>>

Простое решение через EventWaitHandle

Если вам не нужна передача фокуса на существующее окно, а достаточно просто блокировать повторный запуск с выводом сообщения, можно использовать EventWaitHandle в режиме EventResetMode.AutoReset. Этот подход проще и не требует P/Invoke.

public partial class App : Application
{
    private static EventWaitHandle _waitHandle;
    private const string EventName = "UniqueAppEvent-{B3C4D5E6-F7A8-9B0C-1D2E-3F4A5B6C7D8E}";

    protected override void OnStartup(StartupEventArgs e)
    {
        bool createdNew;
        _waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, EventName, out createdNew);

        if (!createdNew)
        {
            MessageBox.Show("Приложение уже запущено!", "Ошибка", 
                MessageBoxButton.OK, MessageBoxImage.Warning);
            Application.Current.Shutdown();
            return;
        }

        base.OnStartup(e);
    }

    protected override void OnExit(ExitEventArgs e)
    {
        _waitHandle?.Set(); // Сигнализируем о завершении
        _waitHandle?.Dispose();
        base.OnExit(e);
    }
}

Плюсы и минусы

  • + Простая реализация без WinAPI.
  • + Автоматическая очистка ресурсов при завершении процесса.
  • - Не позволяет передать фокус на существующее окно.
  • - Менее надёжен при аварийном завершении (может остаться "зависший" дескриптор).

Этот вариант подходит для простых утилит, где достаточно предотвратить множественный запуск без необходимости взаимодействия с уже работающим экземпляром.