Jaki jest prawidłowy sposób utworzenia aplikacji WPF z pojedynczym wystąpieniem?


656

Używając C # i WPF w .NET (zamiast Windows Forms lub konsoli), jaki jest właściwy sposób na stworzenie aplikacji, którą można uruchomić tylko jako jedną instancję?

Wiem, że ma to coś wspólnego z mityczną rzeczą zwaną mutex, rzadko mogę znaleźć kogoś, kto będzie chciał się zatrzymać i wyjaśnić, co to jest.

Kod musi również informować działającą już instancję, że użytkownik próbował uruchomić drugą, a także może przekazać wszelkie argumenty wiersza polecenia, jeśli takie istnieją.


14
Czy CLR nie zwalnia automatycznie żadnych niewydanych muteksów, gdy aplikacja i tak się zakończy?
Cocowalla

1
@Cocowalla: finalizator powinien pozbyć się niezarządzanych muteksów, chyba że nie może wiedzieć, czy muteks został utworzony przez zarządzaną aplikację, czy dołączony do istniejącej.
Ignacio Soler Garcia

Posiadanie tylko jednego wystąpienia aplikacji jest uzasadnione. Ale przekazywanie argumentów do już istniejącej aplikacji wydaje mi się trochę głupie. Nie widzę żadnego powodu, aby to zrobić. Jeśli powiążesz aplikację z rozszerzeniem pliku, powinieneś otworzyć tyle aplikacji, ile użytkownik chce otworzyć dokumenty. Takie jest standardowe zachowanie, jakiego oczekiwałby każdy użytkownik.
Eric Ouellet,

9
@Cocowalla CLR nie zarządza rodzimymi zasobami. Jeśli jednak proces się zakończy, wszystkie uchwyty zostaną zwolnione przez system (system operacyjny, a nie CLR).
Widoczny

1
Wolę odpowiedź od @huseyint. Wykorzystuje własną klasę Microsoft „SingleInstance.cs”, więc nie musisz się martwić o Mutexes i IntPtrs. Ponadto brak zależności od VisualBasic (fuj). Zobacz codereview.stackexchange.com/questions/20871/... aby uzyskać więcej ...
Heliac

Odpowiedzi:


537

Oto bardzo dobry artykuł dotyczący rozwiązania Mutex. Podejście opisane w artykule jest korzystne z dwóch powodów.

Po pierwsze, nie wymaga zależności od zestawu Microsoft.VisualBasic. Gdyby mój projekt był już zależny od tego zestawu, prawdopodobnie zalecałbym użycie podejścia pokazanego w innej odpowiedzi . Ale w tej chwili nie używam zestawu Microsoft.VisualBasic i wolałbym nie dodawać niepotrzebnej zależności do mojego projektu.

Po drugie, artykuł pokazuje, jak przenieść istniejącą instancję aplikacji na pierwszy plan, gdy użytkownik próbuje uruchomić inną instancję. To bardzo miły akcent, którego nie opisują inne opisane tutaj rozwiązania Mutex.


AKTUALIZACJA

Na dzień 8.01.2014 artykuł, do którego odsyłam, jest nadal aktywny, ale blog nie był przez jakiś czas aktualizowany. Martwi mnie to, że ostatecznie może zniknąć, a wraz z nim zalecane rozwiązanie. Powielam tutaj treść artykułu dla potomności. Słowa należą wyłącznie do właściciela bloga w Sanity Free Coding .

Dzisiaj chciałem zmienić kod, który zabraniałby mojej aplikacji uruchamiania wielu własnych instancji.

Wcześniej korzystałem z System.Diagnostics.Process, aby wyszukać wystąpienie mojego myapp.exe na liście procesów. Podczas gdy to działa, powoduje to duże obciążenie i chciałem czegoś czystszego.

Wiedząc, że mogę do tego użyć muteksu (ale nigdy wcześniej tego nie robiłem) postanowiłem ograniczyć kod i uprościć moje życie.

W klasie mojej aplikacji głównej stworzyłem statyczny o nazwie Mutex :

static class Program
{
    static Mutex mutex = new Mutex(true, "{8F6F0AC4-B9A1-45fd-A8CF-72F04E6BDE8F}");
    [STAThread]
    ...
}

Posiadanie nazwanego muteksu pozwala nam nakładać synchronizację na wiele wątków i procesów, co jest właśnie magią, której szukam.

Mutex.WaitOne ma przeciążenie, które określa czas oczekiwania. Ponieważ tak naprawdę nie chcemy synchronizować naszego kodu (wystarczy sprawdzić, czy jest on aktualnie używany), używamy przeciążenia z dwoma parametrami: Mutex.WaitOne (Limit czasu, bool exitContext) . Poczekaj, aż zwróci się prawda, jeśli można wejść, i fałsz, jeśli tak nie było. W tym przypadku wcale nie chcemy czekać; Jeśli używany jest nasz muteks, pomiń go i przejdź dalej, więc przekazujemy TimeSpan.Zero (odczekaj 0 milisekund) i ustawiamy exitContext na true, abyśmy mogli opuścić kontekst synchronizacji, zanim spróbujemy uzyskać na nim blokadę. Korzystając z tego, zawijamy nasz kod Application.Run wewnątrz czegoś takiego:

static class Program
{
    static Mutex mutex = new Mutex(true, "{8F6F0AC4-B9A1-45fd-A8CF-72F04E6BDE8F}");
    [STAThread]
    static void Main() {
        if(mutex.WaitOne(TimeSpan.Zero, true)) {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
            mutex.ReleaseMutex();
        } else {
            MessageBox.Show("only one instance at a time");
        }
    }
}

Tak więc, jeśli nasza aplikacja jest uruchomiona, WaitOne zwróci false, a my otrzymamy okno komunikatu.

Zamiast wyświetlać okno komunikatu, zdecydowałem się użyć małego Win32, aby powiadomić moją działającą instancję, że ktoś zapomniał, że już działa (poprzez przejście na szczyt wszystkich innych okien). Aby to osiągnąć, użyłem PostMessage do wysłania niestandardowej wiadomości do każdego okna (wiadomość niestandardowa została zarejestrowana w RegisterWindowMessage przez moją uruchomioną aplikację, co oznacza, że ​​tylko moja aplikacja wie, co to jest), a następnie moja druga instancja kończy działanie. Działająca instancja aplikacji otrzyma to powiadomienie i przetworzy je. W tym celu zastąpiłem WndProc w mojej głównej formie i odsłuchałem moje niestandardowe powiadomienie. Kiedy otrzymałem to powiadomienie, ustawiłem właściwość TopMost formularza na true, aby wyświetlić ją na górze.

Oto, z czym skończyłem:

  • Program.cs
static class Program
{
    static Mutex mutex = new Mutex(true, "{8F6F0AC4-B9A1-45fd-A8CF-72F04E6BDE8F}");
    [STAThread]
    static void Main() {
        if(mutex.WaitOne(TimeSpan.Zero, true)) {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
            mutex.ReleaseMutex();
        } else {
            // send our Win32 message to make the currently running instance
            // jump on top of all the other windows
            NativeMethods.PostMessage(
                (IntPtr)NativeMethods.HWND_BROADCAST,
                NativeMethods.WM_SHOWME,
                IntPtr.Zero,
                IntPtr.Zero);
        }
    }
}
  • NativeMethods.cs
// this class just wraps some Win32 stuff that we're going to use
internal class NativeMethods
{
    public const int HWND_BROADCAST = 0xffff;
    public static readonly int WM_SHOWME = RegisterWindowMessage("WM_SHOWME");
    [DllImport("user32")]
    public static extern bool PostMessage(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam);
    [DllImport("user32")]
    public static extern int RegisterWindowMessage(string message);
}
  • Form1.cs (częściowe z przodu)
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
    protected override void WndProc(ref Message m)
    {
        if(m.Msg == NativeMethods.WM_SHOWME) {
            ShowMe();
        }
        base.WndProc(ref m);
    }
    private void ShowMe()
    {
        if(WindowState == FormWindowState.Minimized) {
            WindowState = FormWindowState.Normal;
        }
        // get our current "TopMost" value (ours will always be false though)
        bool top = TopMost;
        // make our form jump to the top of everything
        TopMost = true;
        // set it back to whatever it was
        TopMost = top;
    }
}

5
Na tej podstawie, że ta odpowiedź zużywa mniej kodu i mniej bibliotek oraz zapewnia najwyższą funkcjonalność, zamierzam uczynić tę nową akceptowaną odpowiedzią. Jeśli ktoś zna bardziej prawidłowy sposób przeniesienia formularza na górę za pomocą interfejsów API, dodaj go.
Nidonocu

11
@BlueRaja, uruchamiasz pierwszą instancję aplikacji. Po uruchomieniu drugiej instancji aplikacji wykrywa ona, że ​​inna instancja już działa i przygotowuje się do zamknięcia. Zanim to zrobi, wysyła natywną wiadomość „SHOWME” do pierwszej instancji, która przenosi pierwszą instancję na szczyt. Zdarzenia w .NET nie pozwalają na komunikację między procesami, dlatego używany jest komunikat natywny.
Matt Davis

7
Czy jest jakiś sposób na przekazanie linii poleceń z innej instancji?
gyurisc

22
@Nam, Mutexkonstruktor po prostu wymaga łańcucha, abyś mógł podać dowolną nazwę łańcucha, np. „This Is My Mutex”. Ponieważ „Mutex” jest obiektem systemowym dostępnym dla innych procesów, zwykle chcesz, aby nazwa była unikalna, aby nie kolidowała z innymi nazwami „Mutex” w tym samym systemie. W artykule tajemniczo wyglądającym ciągiem jest „Guid”. Możesz wygenerować to programowo, dzwoniąc System.Guid.NewGuid(). W przypadku tego artykułu użytkownik prawdopodobnie wygenerował go za pomocą Visual Studio, jak pokazano tutaj: msdn.microsoft.com/en-us/library/ms241442(VS.80).aspx
Matt Davis

6
Czy podejście mutex zakłada, że ​​ten sam użytkownik próbuje ponownie uruchomić aplikację? Z pewnością przeniesienie „istniejącej instancji aplikacji na pierwszy plan” nie ma sensu po „przełączeniu użytkownika”
dumbledad,

107

Możesz użyć klasy Mutex, ale wkrótce przekonasz się, że będziesz musiał zaimplementować kod, aby przekazać argumenty i tym podobne. Cóż, nauczyłem się sztuczki podczas programowania w WinForm, kiedy czytałem książkę Chrisa Sella . Ta sztuczka wykorzystuje logikę, która jest już dla nas dostępna w ramach. Nie wiem o tobie, ale kiedy dowiaduję się o rzeczach, mogę ponownie użyć w ramie, zwykle taką drogę wybieram zamiast wymyślać koło. Chyba że oczywiście nie robi wszystkiego, czego chcę.

Kiedy dostałem się do WPF, wymyśliłem sposób użycia tego samego kodu, ale w aplikacji WPF. To rozwiązanie powinno zaspokoić twoje potrzeby w oparciu o twoje pytanie.

Najpierw musimy stworzyć naszą klasę aplikacji. W tej klasie przesłonimy zdarzenie OnStartup i stworzymy metodę o nazwie Activate, która zostanie wykorzystana później.

public class SingleInstanceApplication : System.Windows.Application
{
    protected override void OnStartup(System.Windows.StartupEventArgs e)
    {
        // Call the OnStartup event on our base class
        base.OnStartup(e);

        // Create our MainWindow and show it
        MainWindow window = new MainWindow();
        window.Show();
    }

    public void Activate()
    {
        // Reactivate the main window
        MainWindow.Activate();
    }
}

Po drugie, musimy stworzyć klasę, która będzie zarządzać naszymi instancjami. Zanim to przejdziemy, faktycznie ponownie wykorzystamy kod z zestawu Microsoft.VisualBasic. Ponieważ używam C # w tym przykładzie, musiałem odwołać się do zestawu. Jeśli korzystasz z VB.NET, nie musisz nic robić. Klasa, której będziemy używać, to WindowsFormsApplicationBase i dziedziczy po niej naszego menedżera instancji, a następnie wykorzystuje właściwości i zdarzenia do obsługi pojedynczego wystąpienia.

public class SingleInstanceManager : Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase
{
    private SingleInstanceApplication _application;
    private System.Collections.ObjectModel.ReadOnlyCollection<string> _commandLine;

    public SingleInstanceManager()
    {
        IsSingleInstance = true;
    }

    protected override bool OnStartup(Microsoft.VisualBasic.ApplicationServices.StartupEventArgs eventArgs)
    {
        // First time _application is launched
        _commandLine = eventArgs.CommandLine;
        _application = new SingleInstanceApplication();
        _application.Run();
        return false;
    }

    protected override void OnStartupNextInstance(StartupNextInstanceEventArgs eventArgs)
    {
        // Subsequent launches
        base.OnStartupNextInstance(eventArgs);
        _commandLine = eventArgs.CommandLine;
        _application.Activate();
    }
}

Zasadniczo używamy bitów VB do wykrywania pojedynczych instancji i odpowiedniego przetwarzania. OnStartup zostanie uruchomiony po załadowaniu pierwszej instancji. Program OnStartupNextInstance jest uruchamiany po ponownym uruchomieniu aplikacji. Jak widać, mogę dostać się do tego, co zostało przekazane w wierszu poleceń, za pomocą argumentów zdarzenia. Ustawiam wartość na pole instancji. Możesz parsować tutaj wiersz poleceń lub przekazać go do aplikacji przez konstruktor i wywołanie metody Activate.

Po trzecie, czas stworzyć nasz EntryPoint. Zamiast odświeżać aplikację tak, jak normalnie, skorzystamy z naszego narzędzia SingleInstanceManager.

public class EntryPoint
{
    [STAThread]
    public static void Main(string[] args)
    {
        SingleInstanceManager manager = new SingleInstanceManager();
        manager.Run(args);
    }
}

Mam nadzieję, że będziesz w stanie śledzić wszystko i będziesz mógł korzystać z tej implementacji i dostosować ją do własnych potrzeb.


9
Trzymałbym się rozwiązania mutex, ponieważ nie ma ono nic wspólnego z formami.
Steven Sudit

1
Użyłem tego, ponieważ miałem problemy z innymi podejściami, ale jestem całkiem pewien, że używa on zdalnego sterowania pod maską. W mojej aplikacji wystąpiły dwa powiązane problemy - niektórzy klienci twierdzą, że próbuje zadzwonić do domu, nawet jeśli powiedzieli, że tego nie robią. Kiedy wyglądają uważniej, połączenie jest z hostem lokalnym. Mimo to początkowo tego nie wiedzą. Nie mogę też używać zdalnego do innych celów (tak myślę?), Ponieważ jest już używany do tego. Kiedy wypróbowałem podejście mutex, mogłem ponownie skorzystać z usługi zdalnej.
Richard Watson,

4
Wybacz mi, ale chyba że czegoś mi brakuje, unikałeś pisania 3 wierszy kodu, a zamiast tego użyłeś frameworka tylko po to, aby napisać dość ciężki kod. Więc gdzie są oszczędności?
greenoldman

2
czy można to zrobić w winformach?
Jack

1
Jeśli nie wywołasz funkcji InitializeComponent () w instancji aplikacji, nie będziesz w stanie rozwiązać zasobów ... _application = new SingleInstanceApplication (); _application.InitializeComponent (); _application.Run ();
Nick

84

Od tutaj .

Powszechnym zastosowaniem w wielu procesach Mutex jest zapewnienie, że jednocześnie może być uruchomiona tylko instancja programu. Oto jak to się robi:

class OneAtATimePlease {

  // Use a name unique to the application (eg include your company URL)
  static Mutex mutex = new Mutex (false, "oreilly.com OneAtATimeDemo");

  static void Main()
  {
    // Wait 5 seconds if contended – in case another instance
    // of the program is in the process of shutting down.
    if (!mutex.WaitOne(TimeSpan.FromSeconds (5), false))
    {
        Console.WriteLine("Another instance of the app is running. Bye!");
        return;
    }

    try
    {    
        Console.WriteLine("Running - press Enter to exit");
        Console.ReadLine();
    }
    finally
    {
        mutex.ReleaseMutex();
    }    
  }    
}

Dobrą cechą Mutex jest to, że jeśli aplikacja zakończy działanie bez uprzedniego wywołania ReleaseMutex, CLR automatycznie zwolni Mutex.


5
Muszę powiedzieć, że podoba mi się ta odpowiedź o wiele bardziej niż zaakceptowana, po prostu dlatego, że nie jest zależna od WinForms. Osobiście większość moich prac rozwojowych przeniosła się na WPF i nie chcę pobierać bibliotek WinForm do czegoś takiego.
Switters

5
Oczywiście, aby uzyskać pełną odpowiedź, musisz także opisać przekazywanie argumentów do drugiej instancji :)
Simon Buchan,

@Jason, dobrze, dziękuję! Ale wolę nie przekraczać limitu czasu. Jest tak subiektywny i zależy od tak wielu zmiennych. Jeśli kiedykolwiek chcesz włączyć uruchamianie innej aplikacji, po prostu zwolnij mutex szybciej. Na przykład, gdy tylko użytkownik potwierdzi zamknięcie
Eric Ouellet

@EricOuellet: robi to prawie każdy program z kartami - Photoshop, Sublime Text, Chrome .... Jeśli masz dobry powód, aby mieć proces „master” (powiedz, że masz DB w ustawieniach), możesz chcę, aby wyświetlał interfejs użytkownika tak, jakby to był nowy proces.
Simon Buchan

@ Simon, masz rację. Po prostu zadaję sobie pytanie o bardzo starą rzecz ... MDI vs SDI (interfejs wielu dokumentów vs interfejs jednego dokumentu). Kiedy mówisz o kartach, odwołujesz się do MDI. W 1998 roku książka Microsoft sugeruje wyeliminowanie każdej aplikacji MDI. Microsoft zmienił Word, Excel ... na SDI, co moim zdaniem jest prostsze i lepsze. Rozumiem, że Chrome i inni (obecnie IE) chcą wrócić do MDI. Ja osobiście (w oparciu o nic / osobiste odczucia), że nadal lepiej jest otworzyć nową aplikację po wybraniu assoc pliku. Ale rozumiem lepiej zadane teraz pytanie. Dzięki !
Eric Ouellet,

58

MSDN faktycznie ma przykładową aplikację dla C # i VB, która wykonuje dokładnie to: http://msdn.microsoft.com/en-us/library/ms771662(v=VS.90).aspx

Najpopularniejszą i niezawodną techniką opracowywania wykrywania pojedynczych instancji jest użycie infrastruktury zdalnej Microsoft .NET Framework (System.Remoting). Microsoft .NET Framework (wersja 2.0) zawiera typ WindowsFormsApplicationBase, który zawiera wymaganą funkcję zdalnego zarządzania. Aby włączyć ten typ do aplikacji WPF, typ musi z niego wywodzić i być używany jako podkładka między metodą statycznego punktu wejścia aplikacji, Main a typem aplikacji WPF. Podkładka wykrywa, kiedy aplikacja jest uruchamiana po raz pierwszy i kiedy podejmowane są kolejne próby uruchomienia, i daje kontrolę nad typem aplikacji WPF, aby określić sposób przetwarzania uruchomień.

  • Dla C # ludzie biorą głęboki oddech i zapominają o całym „Nie chcę dołączać VisualBasic DLL”. Z tego powodu i tego, co mówi Scott Hanselman oraz z faktu, że jest to najczystsze rozwiązanie problemu i zostało zaprojektowane przez ludzi, którzy wiedzą o frameworku znacznie więcej niż ty.
  • Z punktu widzenia użyteczności faktem jest, że jeśli użytkownik ładuje aplikację, która jest już otwarta i wyświetlasz komunikat o błędzie 'Another instance of the app is running. Bye', jakbyś nie był bardzo szczęśliwym użytkownikiem. Po prostu MUSISZ (w aplikacji GUI) przejść do tej aplikacji i przekazać podane argumenty - lub jeśli parametry wiersza poleceń nie mają znaczenia, musisz wyskoczyć z aplikacji, która mogła zostać zminimalizowana.

Framework ma już na to wsparcie - po prostu ten idiota nazwał DLL Microsoft.VisualBasici nie został wprowadzony Microsoft.ApplicationUtilslub coś takiego. Pokonaj go - lub otwórz Odbłyśnik.

Wskazówka: Jeśli zastosujesz to podejście dokładnie tak, jak jest, i masz już plik App.xaml z zasobami itp ., Również powinieneś się tym przyjrzeć .


Dziękujemy za dołączenie linku „spójrz na to”. Właśnie tego potrzebowałem. Nawiasem mówiąc, rozwiązanie nr 3 w twoim linku jest najlepsze.
Eternal21

Jestem także zwolenniczką delegowania do frameworku i, w miarę możliwości, specjalnie zaprojektowanych bibliotek.
Eniola,

23

Ten kod powinien przejść do głównej metody. Spójrz tutaj, aby uzyskać więcej informacji na temat głównej metody WPF.

[DllImport("user32.dll")]
private static extern Boolean ShowWindow(IntPtr hWnd, Int32 nCmdShow);

private const int SW_SHOWMAXIMIZED = 3;

static void Main() 
{
    Process currentProcess = Process.GetCurrentProcess();
    var runningProcess = (from process in Process.GetProcesses()
                          where
                            process.Id != currentProcess.Id &&
                            process.ProcessName.Equals(
                              currentProcess.ProcessName,
                              StringComparison.Ordinal)
                          select process).FirstOrDefault();
    if (runningProcess != null)
    {
        ShowWindow(runningProcess.MainWindowHandle, SW_SHOWMAXIMIZED);
       return; 
    }
}

Metoda 2

static void Main()
{
    string procName = Process.GetCurrentProcess().ProcessName;
    // get the list of all processes by that name

    Process[] processes=Process.GetProcessesByName(procName);

    if (processes.Length > 1)
    {
        MessageBox.Show(procName + " already running");  
        return;
    } 
    else
    {
        // Application.Run(...);
    }
}

Uwaga: powyższe metody zakładają, że proces / aplikacja ma unikalną nazwę. Ponieważ używa nazwy procesu, aby sprawdzić, czy istnieją istniejące procesory. Jeśli więc aplikacja ma bardzo popularną nazwę (np. Notatnik), powyższe podejście nie będzie działać.


1
Nie zadziała to również, jeśli na komputerze działa inny program o tej samej nazwie. ProcessNamezwraca nazwę pliku wykonywalnego minus exe. Jeśli utworzysz aplikację o nazwie „Notatnik”, a Notatnik systemu Windows będzie działał, wykryje ją jako działającą.
Jcl

1
Dziękuję za tę odpowiedź. Znalazłem tak wiele podobnych pytań, a odpowiedzi zawsze były tak skomplikowane i / lub mylące, że uznałem je za bezużyteczne. Ten (Metoda nr 1) jest prosty, przejrzysty, a przede wszystkim pomógł mi uruchomić kod.
ElDoRado1239

20

Cóż, mam do tego jednorazową klasę, która z łatwością działa w większości przypadków użycia:

Użyj tego w ten sposób:

static void Main()
{
    using (SingleInstanceMutex sim = new SingleInstanceMutex())
    {
        if (sim.IsOtherInstanceRunning)
        {
            Application.Exit();
        }

        // Initialize program here.
    }
}

Oto on:

/// <summary>
/// Represents a <see cref="SingleInstanceMutex"/> class.
/// </summary>
public partial class SingleInstanceMutex : IDisposable
{
    #region Fields

    /// <summary>
    /// Indicator whether another instance of this application is running or not.
    /// </summary>
    private bool isNoOtherInstanceRunning;

    /// <summary>
    /// The <see cref="Mutex"/> used to ask for other instances of this application.
    /// </summary>
    private Mutex singleInstanceMutex = null;

    /// <summary>
    /// An indicator whether this object is beeing actively disposed or not.
    /// </summary>
    private bool disposed;

    #endregion

    #region Constructor

    /// <summary>
    /// Initializes a new instance of the <see cref="SingleInstanceMutex"/> class.
    /// </summary>
    public SingleInstanceMutex()
    {
        this.singleInstanceMutex = new Mutex(true, Assembly.GetCallingAssembly().FullName, out this.isNoOtherInstanceRunning);
    }

    #endregion

    #region Properties

    /// <summary>
    /// Gets an indicator whether another instance of the application is running or not.
    /// </summary>
    public bool IsOtherInstanceRunning
    {
        get
        {
            return !this.isNoOtherInstanceRunning;
        }
    }

    #endregion

    #region Methods

    /// <summary>
    /// Closes the <see cref="SingleInstanceMutex"/>.
    /// </summary>
    public void Close()
    {
        this.ThrowIfDisposed();
        this.singleInstanceMutex.Close();
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            /* Release unmanaged ressources */

            if (disposing)
            {
                /* Release managed ressources */
                this.Close();
            }

            this.disposed = true;
        }
    }

    /// <summary>
    /// Throws an exception if something is tried to be done with an already disposed object.
    /// </summary>
    /// <remarks>
    /// All public methods of the class must first call this.
    /// </remarks>
    public void ThrowIfDisposed()
    {
        if (this.disposed)
        {
            throw new ObjectDisposedException(this.GetType().Name);
        }
    }

    #endregion
}

1
ten był dość łatwy do uruchomienia. Nie zamknie drugiej aplikacji, dopóki nie zmienię Application.Exit (); do prostego powrotu; ale poza tym jest świetny. Chociaż przyznaję, że przyjrzę się bliżej poprzedniemu rozwiązaniu, ponieważ używa ono interfejsu. blogs.microsoft.co.il/blogs/arik/archive/2010/05/28/…
hal9000

15

Nowym, który korzysta z Mutex i IPC, a także przekazuje argumenty wiersza poleceń do działającej instancji, jest aplikacja pojedynczej instancji WPF .


Używam tego z wielkim sukcesem. Jeśli włączysz do tego NamedPipe, możesz również przekazać argumenty wiersza poleceń do oryginalnej aplikacji. Klasa „SingleInstance.cs” została napisana przez Microsoft. Dodałem kolejny link do bardziej czytelnej wersji bloga Arika Poznańskiego na CodeProject.
Heliac

Link jest teraz zepsuty.
Mike Lowery

11

Kod C # .NET Aplikacja pojedynczego wystąpienia która jest odnośnikiem do zaznaczonej odpowiedzi, jest świetnym początkiem.

Stwierdziłem jednak, że nie radzi sobie zbyt dobrze z przypadkami, gdy instancja, która już istnieje, ma otwarte okno dialogowe modalne, niezależnie od tego, czy jest to okno zarządzane (jak inny formularz, np. O polu), czy niezarządzane (takie jak OpenFileDialog nawet przy użyciu standardowej klasy .NET). W przypadku oryginalnego kodu aktywowana jest główna forma, ale modalna pozostaje nieaktywna, co wygląda dziwnie, a użytkownik musi ją kliknąć, aby móc nadal korzystać z aplikacji.

Tak więc utworzyłem klasę narzędzi użytkowych SingleInstance do obsługi tego wszystkiego dość automatycznie dla aplikacji Winforms i WPF.

Winforms :

1) zmodyfikuj klasę Programu w następujący sposób:

static class Program
{
    public static readonly SingleInstance Singleton = new SingleInstance(typeof(Program).FullName);

    [STAThread]
    static void Main(string[] args)
    {
        // NOTE: if this always return false, close & restart Visual Studio
        // this is probably due to the vshost.exe thing
        Singleton.RunFirstInstance(() =>
        {
            SingleInstanceMain(args);
        });
    }

    public static void SingleInstanceMain(string[] args)
    {
        // standard code that was in Main now goes here
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}

2) zmodyfikuj klasę głównego okna w następujący sposób:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    protected override void WndProc(ref Message m)
    {
        // if needed, the singleton will restore this window
        Program.Singleton.OnWndProc(this, m, true);

        // TODO: handle specific messages here if needed
        base.WndProc(ref m);
    }
}

WPF:

1) zmodyfikuj stronę aplikacji w ten sposób (i upewnij się, że ustawiłeś akcję kompilacji na stronę, aby móc ponownie zdefiniować metodę główną):

public partial class App : Application
{
    public static readonly SingleInstance Singleton = new SingleInstance(typeof(App).FullName);

    [STAThread]
    public static void Main(string[] args)
    {
        // NOTE: if this always return false, close & restart Visual Studio
        // this is probably due to the vshost.exe thing
        Singleton.RunFirstInstance(() =>
        {
            SingleInstanceMain(args);
        });
    }

    public static void SingleInstanceMain(string[] args)
    {
        // standard code that was in Main now goes here
        App app = new App();
        app.InitializeComponent();
        app.Run();
    }
}

2) zmodyfikuj klasę głównego okna w następujący sposób:

public partial class MainWindow : Window
{
    private HwndSource _source;

    public MainWindow()
    {
        InitializeComponent();
    }

    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);
        _source = (HwndSource)PresentationSource.FromVisual(this);
        _source.AddHook(HwndSourceHook);
    }

    protected virtual IntPtr HwndSourceHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        // if needed, the singleton will restore this window
        App.Singleton.OnWndProc(hwnd, msg, wParam, lParam, true, true);

        // TODO: handle other specific message
        return IntPtr.Zero;
    }

A oto klasa użyteczności:

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;

namespace SingleInstanceUtilities
{
    public sealed class SingleInstance
    {
        private const int HWND_BROADCAST = 0xFFFF;

        [DllImport("user32.dll")]
        private static extern bool PostMessage(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam);

        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        private static extern int RegisterWindowMessage(string message);

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

        public SingleInstance(string uniqueName)
        {
            if (uniqueName == null)
                throw new ArgumentNullException("uniqueName");

            Mutex = new Mutex(true, uniqueName);
            Message = RegisterWindowMessage("WM_" + uniqueName);
        }

        public Mutex Mutex { get; private set; }
        public int Message { get; private set; }

        public void RunFirstInstance(Action action)
        {
            RunFirstInstance(action, IntPtr.Zero, IntPtr.Zero);
        }

        // NOTE: if this always return false, close & restart Visual Studio
        // this is probably due to the vshost.exe thing
        public void RunFirstInstance(Action action, IntPtr wParam, IntPtr lParam)
        {
            if (action == null)
                throw new ArgumentNullException("action");

            if (WaitForMutext(wParam, lParam))
            {
                try
                {
                    action();
                }
                finally
                {
                    ReleaseMutex();
                }
            }
        }

        public static void ActivateWindow(IntPtr hwnd)
        {
            if (hwnd == IntPtr.Zero)
                return;

            FormUtilities.ActivateWindow(FormUtilities.GetModalWindow(hwnd));
        }

        public void OnWndProc(IntPtr hwnd, int m, IntPtr wParam, IntPtr lParam, bool restorePlacement, bool activate)
        {
            if (m == Message)
            {
                if (restorePlacement)
                {
                    WindowPlacement placement = WindowPlacement.GetPlacement(hwnd, false);
                    if (placement.IsValid && placement.IsMinimized)
                    {
                        const int SW_SHOWNORMAL = 1;
                        placement.ShowCmd = SW_SHOWNORMAL;
                        placement.SetPlacement(hwnd);
                    }
                }

                if (activate)
                {
                    SetForegroundWindow(hwnd);
                    FormUtilities.ActivateWindow(FormUtilities.GetModalWindow(hwnd));
                }
            }
        }

#if WINFORMS // define this for Winforms apps
        public void OnWndProc(System.Windows.Forms.Form form, int m, IntPtr wParam, IntPtr lParam, bool activate)
        {
            if (form == null)
                throw new ArgumentNullException("form");

            if (m == Message)
            {
                if (activate)
                {
                    if (form.WindowState == System.Windows.Forms.FormWindowState.Minimized)
                    {
                        form.WindowState = System.Windows.Forms.FormWindowState.Normal;
                    }

                    form.Activate();
                    FormUtilities.ActivateWindow(FormUtilities.GetModalWindow(form.Handle));
                }
            }
        }

        public void OnWndProc(System.Windows.Forms.Form form, System.Windows.Forms.Message m, bool activate)
        {
            if (form == null)
                throw new ArgumentNullException("form");

            OnWndProc(form, m.Msg, m.WParam, m.LParam, activate);
        }
#endif

        public void ReleaseMutex()
        {
            Mutex.ReleaseMutex();
        }

        public bool WaitForMutext(bool force, IntPtr wParam, IntPtr lParam)
        {
            bool b = PrivateWaitForMutext(force);
            if (!b)
            {
                PostMessage((IntPtr)HWND_BROADCAST, Message, wParam, lParam);
            }
            return b;
        }

        public bool WaitForMutext(IntPtr wParam, IntPtr lParam)
        {
            return WaitForMutext(false, wParam, lParam);
        }

        private bool PrivateWaitForMutext(bool force)
        {
            if (force)
                return true;

            try
            {
                return Mutex.WaitOne(TimeSpan.Zero, true);
            }
            catch (AbandonedMutexException)
            {
                return true;
            }
        }
    }

    // NOTE: don't add any field or public get/set property, as this must exactly map to Windows' WINDOWPLACEMENT structure
    [StructLayout(LayoutKind.Sequential)]
    public struct WindowPlacement
    {
        public int Length { get; set; }
        public int Flags { get; set; }
        public int ShowCmd { get; set; }
        public int MinPositionX { get; set; }
        public int MinPositionY { get; set; }
        public int MaxPositionX { get; set; }
        public int MaxPositionY { get; set; }
        public int NormalPositionLeft { get; set; }
        public int NormalPositionTop { get; set; }
        public int NormalPositionRight { get; set; }
        public int NormalPositionBottom { get; set; }

        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool SetWindowPlacement(IntPtr hWnd, ref WindowPlacement lpwndpl);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool GetWindowPlacement(IntPtr hWnd, ref WindowPlacement lpwndpl);

        private const int SW_SHOWMINIMIZED = 2;

        public bool IsMinimized
        {
            get
            {
                return ShowCmd == SW_SHOWMINIMIZED;
            }
        }

        public bool IsValid
        {
            get
            {
                return Length == Marshal.SizeOf(typeof(WindowPlacement));
            }
        }

        public void SetPlacement(IntPtr windowHandle)
        {
            SetWindowPlacement(windowHandle, ref this);
        }

        public static WindowPlacement GetPlacement(IntPtr windowHandle, bool throwOnError)
        {
            WindowPlacement placement = new WindowPlacement();
            if (windowHandle == IntPtr.Zero)
                return placement;

            placement.Length = Marshal.SizeOf(typeof(WindowPlacement));
            if (!GetWindowPlacement(windowHandle, ref placement))
            {
                if (throwOnError)
                    throw new Win32Exception(Marshal.GetLastWin32Error());

                return new WindowPlacement();
            }
            return placement;
        }
    }

    public static class FormUtilities
    {
        [DllImport("user32.dll")]
        private static extern IntPtr GetWindow(IntPtr hWnd, int uCmd);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr SetActiveWindow(IntPtr hWnd);

        [DllImport("user32.dll")]
        private static extern bool IsWindowVisible(IntPtr hWnd);

        [DllImport("kernel32.dll")]
        public static extern int GetCurrentThreadId();

        private delegate bool EnumChildrenCallback(IntPtr hwnd, IntPtr lParam);

        [DllImport("user32.dll")]
        private static extern bool EnumThreadWindows(int dwThreadId, EnumChildrenCallback lpEnumFunc, IntPtr lParam);

        private class ModalWindowUtil
        {
            private const int GW_OWNER = 4;
            private int _maxOwnershipLevel;
            private IntPtr _maxOwnershipHandle;

            private bool EnumChildren(IntPtr hwnd, IntPtr lParam)
            {
                int level = 1;
                if (IsWindowVisible(hwnd) && IsOwned(lParam, hwnd, ref level))
                {
                    if (level > _maxOwnershipLevel)
                    {
                        _maxOwnershipHandle = hwnd;
                        _maxOwnershipLevel = level;
                    }
                }
                return true;
            }

            private static bool IsOwned(IntPtr owner, IntPtr hwnd, ref int level)
            {
                IntPtr o = GetWindow(hwnd, GW_OWNER);
                if (o == IntPtr.Zero)
                    return false;

                if (o == owner)
                    return true;

                level++;
                return IsOwned(owner, o, ref level);
            }

            public static void ActivateWindow(IntPtr hwnd)
            {
                if (hwnd != IntPtr.Zero)
                {
                    SetActiveWindow(hwnd);
                }
            }

            public static IntPtr GetModalWindow(IntPtr owner)
            {
                ModalWindowUtil util = new ModalWindowUtil();
                EnumThreadWindows(GetCurrentThreadId(), util.EnumChildren, owner);
                return util._maxOwnershipHandle; // may be IntPtr.Zero
            }
        }

        public static void ActivateWindow(IntPtr hwnd)
        {
            ModalWindowUtil.ActivateWindow(hwnd);
        }

        public static IntPtr GetModalWindow(IntPtr owner)
        {
            return ModalWindowUtil.GetModalWindow(owner);
        }
    }
}

10

Oto przykład, który pozwala mieć jedną instancję aplikacji. Po załadowaniu nowych instancji przekazują swoje argumenty do działającej instancji głównej.

public partial class App : Application
{
    private static Mutex SingleMutex;
    public static uint MessageId;

    private void Application_Startup(object sender, StartupEventArgs e)
    {
        IntPtr Result;
        IntPtr SendOk;
        Win32.COPYDATASTRUCT CopyData;
        string[] Args;
        IntPtr CopyDataMem;
        bool AllowMultipleInstances = false;

        Args = Environment.GetCommandLineArgs();

        // TODO: Replace {00000000-0000-0000-0000-000000000000} with your application's GUID
        MessageId   = Win32.RegisterWindowMessage("{00000000-0000-0000-0000-000000000000}");
        SingleMutex = new Mutex(false, "AppName");

        if ((AllowMultipleInstances) || (!AllowMultipleInstances && SingleMutex.WaitOne(1, true)))
        {
            new Main();
        }
        else if (Args.Length > 1)
        {
            foreach (Process Proc in Process.GetProcesses())
            {
                SendOk = Win32.SendMessageTimeout(Proc.MainWindowHandle, MessageId, IntPtr.Zero, IntPtr.Zero,
                    Win32.SendMessageTimeoutFlags.SMTO_BLOCK | Win32.SendMessageTimeoutFlags.SMTO_ABORTIFHUNG,
                    2000, out Result);

                if (SendOk == IntPtr.Zero)
                    continue;
                if ((uint)Result != MessageId)
                    continue;

                CopyDataMem = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Win32.COPYDATASTRUCT)));

                CopyData.dwData = IntPtr.Zero;
                CopyData.cbData = Args[1].Length*2;
                CopyData.lpData = Marshal.StringToHGlobalUni(Args[1]);

                Marshal.StructureToPtr(CopyData, CopyDataMem, false);

                Win32.SendMessageTimeout(Proc.MainWindowHandle, Win32.WM_COPYDATA, IntPtr.Zero, CopyDataMem,
                    Win32.SendMessageTimeoutFlags.SMTO_BLOCK | Win32.SendMessageTimeoutFlags.SMTO_ABORTIFHUNG,
                    5000, out Result);

                Marshal.FreeHGlobal(CopyData.lpData);
                Marshal.FreeHGlobal(CopyDataMem);
            }

            Shutdown(0);
        }
    }
}

public partial class Main : Window
{
    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        HwndSource Source;

        Source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
        Source.AddHook(new HwndSourceHook(Window_Proc));
    }

    private IntPtr Window_Proc(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam, ref bool Handled)
    {
        Win32.COPYDATASTRUCT CopyData;
        string Path;

        if (Msg == Win32.WM_COPYDATA)
        {
            CopyData = (Win32.COPYDATASTRUCT)Marshal.PtrToStructure(lParam, typeof(Win32.COPYDATASTRUCT));
            Path = Marshal.PtrToStringUni(CopyData.lpData, CopyData.cbData / 2);

            if (WindowState == WindowState.Minimized)
            {
                // Restore window from tray
            }

            // Do whatever we want with information

            Activate();
            Focus();
        }

        if (Msg == App.MessageId)
        {
            Handled = true;
            return new IntPtr(App.MessageId);
        }

        return IntPtr.Zero;
    }
}

public class Win32
{
    public const uint WM_COPYDATA = 0x004A;

    public struct COPYDATASTRUCT
    {
        public IntPtr dwData;
        public int    cbData;
        public IntPtr lpData;
    }

    [Flags]
    public enum SendMessageTimeoutFlags : uint
    {
        SMTO_NORMAL             = 0x0000,
        SMTO_BLOCK              = 0x0001,
        SMTO_ABORTIFHUNG        = 0x0002,
        SMTO_NOTIMEOUTIFNOTHUNG = 0x0008
    }

    [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
    public static extern uint RegisterWindowMessage(string lpString);
    [DllImport("user32.dll")]
    public static extern IntPtr SendMessageTimeout(
        IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam,
        SendMessageTimeoutFlags fuFlags, uint uTimeout, out IntPtr lpdwResult);
}

To naprawdę fajny przykład tego, co mam robić. Nathan, czy wszystkie argumenty są wysyłane przy użyciu tej metody? Mam w aplikacji około 7 i myślę, że ten kod będzie działał.
kevp

1
W moim przykładzie wysyłany jest tylko pierwszy argument, ale można go zmienić tak, aby wszystkie zostały wysłane.
Nathan Moinvaziri

8

Tylko kilka przemyśleń: zdarzają się sytuacje, w których wymaga się, aby tylko jedna instancja aplikacji nie była „kiepska”, jak niektórzy by chcieli. Aplikacje bazodanowe itp. Są o rząd wielkości trudniejsze, jeśli pozwala się na dostęp do bazy danych wielu wystąpieniom aplikacji dla jednego użytkownika (wszystko to aktualizuje wszystkie rekordy, które są otwarte w wielu wystąpieniach aplikacji na użytkownikach) maszyna itp.). Po pierwsze, w przypadku „kolizji nazw” nie używaj nazwy czytelnej dla człowieka - zamiast tego użyj identyfikatora GUID lub, nawet lepiej, identyfikatora GUID + nazwy czytelnej dla człowieka. Szanse kolizji nazwy spadły z radaru, a Mutex nie ma znaczenia Jak ktoś zauważył, atak DOS byłby do bani, ale gdyby złośliwa osoba zadała sobie trud uzyskania nazwy mutexu i włączenia jej do swojej aplikacji, i tak jesteś raczej celem i będziesz musiał zrobić DUŻO więcej, aby się bronić, niż po prostu bawić się mutexem. Ponadto, jeśli użyjesz wariantu: new Mutex (true, „GUID plus Name”, obecnie AIsFirstInstance), masz już wskaźnik tego, czy Mutex jest pierwszą instancją.


6

Tak wiele odpowiedzi na tak pozornie proste pytanie. Wystarczy trochę potrząsnąć tutaj, jest moim rozwiązaniem tego problemu.

Utworzenie Muteksu może być kłopotliwe, ponieważ JIT-er widzi, że używasz go tylko do niewielkiej części kodu i chce oznaczyć go jako gotowy do wyrzucania elementów bezużytecznych. Praktycznie chce obezwładnić cię myśląc, że nie będziesz używać tego Mutexa tak długo. W rzeczywistości chcesz trzymać się tego Mutexa tak długo, jak długo aplikacja jest uruchomiona. Najlepszym sposobem, aby powiedzieć śmieciarzowi, aby zostawił cię Mutex w spokoju, jest powiedzenie mu, aby utrzymał go przy życiu przez różne generacje kolekcji garażowych. Przykład:

var m = new Mutex(...);
...
GC.KeepAlive(m);

Podniosłem pomysł z tej strony: http://www.ai.uga.edu/~mc/SingleInstance.html


3
Czy nie byłoby łatwiej przechowywać współdzieloną kopię w klasie aplikacji?
rossisdead

6

Wygląda na to, że istnieje naprawdę dobry sposób na poradzenie sobie z tym:

Aplikacja pojedynczego wystąpienia WPF

Zapewnia to klasę, którą można dodać, która zarządza wszystkimi muteksami i wątkami komunikacyjnymi, aby uprościć implementację do tego stopnia, że ​​jest to po prostu trywialne.


Wydawało się, że to nie spowodowało przeniesienia istniejącego okna na pierwszy plan, gdy go wypróbowałem.
RandomEngy,

6

Poniższy kod to moje rozwiązanie nazwane potokami WCF do rejestracji aplikacji z pojedynczą instancją. To miłe, ponieważ wywołuje także zdarzenie, gdy inna instancja próbuje się uruchomić, i otrzymuje wiersz komendowy drugiej instancji.

Jest nastawiony na WPF, ponieważ wykorzystuje System.Windows.StartupEventHandlerklasę, ale można to łatwo zmodyfikować.

Ten kod wymaga odniesienia do PresentationFrameworki System.ServiceModel.

Stosowanie:

class Program
{
    static void Main()
    {
        var applicationId = new Guid("b54f7b0d-87f9-4df9-9686-4d8fd76066dc");

        if (SingleInstanceManager.VerifySingleInstance(applicationId))
        {
            SingleInstanceManager.OtherInstanceStarted += OnOtherInstanceStarted;

            // Start the application
        }
    }

    static void OnOtherInstanceStarted(object sender, StartupEventArgs e)
    {
        // Do something in response to another instance starting up.
    }
}

Kod źródłowy:

/// <summary>
/// A class to use for single-instance applications.
/// </summary>
public static class SingleInstanceManager
{
  /// <summary>
  /// Raised when another instance attempts to start up.
  /// </summary>
  public static event StartupEventHandler OtherInstanceStarted;

  /// <summary>
  /// Checks to see if this instance is the first instance running on this machine.  If it is not, this method will
  /// send the main instance this instance's startup information.
  /// </summary>
  /// <param name="guid">The application's unique identifier.</param>
  /// <returns>True if this instance is the main instance.</returns>
  public static bool VerifySingleInstace(Guid guid)
  {
    if (!AttemptPublishService(guid))
    {
      NotifyMainInstance(guid);

      return false;
    }

    return true;
  }

  /// <summary>
  /// Attempts to publish the service.
  /// </summary>
  /// <param name="guid">The application's unique identifier.</param>
  /// <returns>True if the service was published successfully.</returns>
  private static bool AttemptPublishService(Guid guid)
  {
    try
    {
      ServiceHost serviceHost = new ServiceHost(typeof(SingleInstance));
      NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
      serviceHost.AddServiceEndpoint(typeof(ISingleInstance), binding, CreateAddress(guid));
      serviceHost.Open();

      return true;
    }
    catch
    {
      return false;
    }
  }

  /// <summary>
  /// Notifies the main instance that this instance is attempting to start up.
  /// </summary>
  /// <param name="guid">The application's unique identifier.</param>
  private static void NotifyMainInstance(Guid guid)
  {
    NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
    EndpointAddress remoteAddress = new EndpointAddress(CreateAddress(guid));
    using (ChannelFactory<ISingleInstance> factory = new ChannelFactory<ISingleInstance>(binding, remoteAddress))
    {
      ISingleInstance singleInstance = factory.CreateChannel();
      singleInstance.NotifyMainInstance(Environment.GetCommandLineArgs());
    }
  }

  /// <summary>
  /// Creates an address to publish/contact the service at based on a globally unique identifier.
  /// </summary>
  /// <param name="guid">The identifier for the application.</param>
  /// <returns>The address to publish/contact the service.</returns>
  private static string CreateAddress(Guid guid)
  {
    return string.Format(CultureInfo.CurrentCulture, "net.pipe://localhost/{0}", guid);
  }

  /// <summary>
  /// The interface that describes the single instance service.
  /// </summary>
  [ServiceContract]
  private interface ISingleInstance
  {
    /// <summary>
    /// Notifies the main instance that another instance of the application attempted to start.
    /// </summary>
    /// <param name="args">The other instance's command-line arguments.</param>
    [OperationContract]
    void NotifyMainInstance(string[] args);
  }

  /// <summary>
  /// The implementation of the single instance service interface.
  /// </summary>
  private class SingleInstance : ISingleInstance
  {
    /// <summary>
    /// Notifies the main instance that another instance of the application attempted to start.
    /// </summary>
    /// <param name="args">The other instance's command-line arguments.</param>
    public void NotifyMainInstance(string[] args)
    {
      if (OtherInstanceStarted != null)
      {
        Type type = typeof(StartupEventArgs);
        ConstructorInfo constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
        StartupEventArgs e = (StartupEventArgs)constructor.Invoke(null);
        FieldInfo argsField = type.GetField("_args", BindingFlags.Instance | BindingFlags.NonPublic);
        Debug.Assert(argsField != null);
        argsField.SetValue(e, args);

        OtherInstanceStarted(null, e);
      }
    }
  }
}

5

Nigdy nie należy używać nazwanego muteksu do implementacji aplikacji z pojedynczą instancją (a przynajmniej nie do kodu produkcyjnego). Złośliwy kod może łatwo zrobić DoS ( Denial of Service ) tyłek ...


8
„Nigdy nie powinieneś używać nazwanego muteksu” - nigdy nie mów nigdy. Jeśli na moim komputerze działa złośliwy kod, prawdopodobnie jestem już ukryty.
Joe

W rzeczywistości nie musi to być złośliwy kod. Może to być przypadkowe zderzenie nazwy.
Matt Davison

Co powinieneś zrobić?
Kevin Berridge

Lepszym pytaniem jest, jaki jest możliwy powód takiego zachowania. Nie projektuj aplikacji jako aplikacji pojedynczej instancji =). Wiem, że to kiepska odpowiedź, ale z punktu widzenia projektu jest to prawie zawsze poprawna odpowiedź. Bez wiedzy na temat aplikacji trudno powiedzieć o wiele więcej.
Matt Davison

2
Przynajmniej w systemie Windows Mutexy mają kontrolę dostępu, więc można bawić się swoim przedmiotem. Jeśli chodzi o same kolizje nazw, właśnie dlatego UUID / GUID zostały wynalezione.
NuSkooler,

5

Spójrz na poniższy kod. Jest to świetne i proste rozwiązanie, aby zapobiec wielu wystąpieniom aplikacji WPF.

private void Application_Startup(object sender, StartupEventArgs e)
{
    Process thisProc = Process.GetCurrentProcess();
    if (Process.GetProcessesByName(thisProc.ProcessName).Length > 1)
    {
        MessageBox.Show("Application running");
        Application.Current.Shutdown();
        return;
    }

    var wLogin = new LoginWindow();

    if (wLogin.ShowDialog() == true)
    {
        var wMain = new Main();
        wMain.WindowState = WindowState.Maximized;
        wMain.Show();
    }
    else
    {
        Application.Current.Shutdown();
    }
}

4

Oto czego używam. Połączono wyliczanie procesów w celu wykonania przełączania i muteksu w celu zabezpieczenia przed „aktywnymi modułami klikającymi”:

public partial class App
{
    [DllImport("user32")]
    private static extern int OpenIcon(IntPtr hWnd);

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

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        var p = Process
           .GetProcessesByName(Process.GetCurrentProcess().ProcessName);
            foreach (var t in p.Where(t => t.MainWindowHandle != IntPtr.Zero))
            {
                OpenIcon(t.MainWindowHandle);
                SetForegroundWindow(t.MainWindowHandle);
                Current.Shutdown();
                return;
            }

            // there is a chance the user tries to click on the icon repeatedly
            // and the process cannot be discovered yet
            bool createdNew;
            var mutex = new Mutex(true, "MyAwesomeApp", 
               out createdNew);  // must be a variable, though it is unused - 
            // we just need a bit of time until the process shows up
            if (!createdNew)
            {
                Current.Shutdown();
                return;
            }

            new Bootstrapper().Run();
        }
    }

4

Znalazłem prostsze rozwiązanie, podobne do Dale Ragana, ale nieco zmodyfikowane. Robi praktycznie wszystko, czego potrzebujesz, w oparciu o standardową klasę Microsoft WindowsFormsApplicationBase.

Po pierwsze, tworzysz klasę SingleInstanceController, której możesz używać we wszystkich innych aplikacjach z pojedynczą instancją, które używają Windows Forms:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Microsoft.VisualBasic.ApplicationServices;


namespace SingleInstanceController_NET
{
    public class SingleInstanceController
    : WindowsFormsApplicationBase
    {
        public delegate Form CreateMainForm();
        public delegate void StartNextInstanceDelegate(Form mainWindow);
        CreateMainForm formCreation;
        StartNextInstanceDelegate onStartNextInstance;
        public SingleInstanceController(CreateMainForm formCreation, StartNextInstanceDelegate onStartNextInstance)
        {
            // Set whether the application is single instance
            this.formCreation = formCreation;
            this.onStartNextInstance = onStartNextInstance;
            this.IsSingleInstance = true;

            this.StartupNextInstance += new StartupNextInstanceEventHandler(this_StartupNextInstance);                      
        }

        void this_StartupNextInstance(object sender, StartupNextInstanceEventArgs e)
        {
            if (onStartNextInstance != null)
            {
                onStartNextInstance(this.MainForm); // This code will be executed when the user tries to start the running program again,
                                                    // for example, by clicking on the exe file.
            }                                       // This code can determine how to re-activate the existing main window of the running application.
        }

        protected override void OnCreateMainForm()
        {
            // Instantiate your main application form
            this.MainForm = formCreation();
        }

        public void Run()
        {
            string[] commandLine = new string[0];
            base.Run(commandLine);
        }
    }
}

Następnie możesz użyć go w swoim programie w następujący sposób:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using SingleInstanceController_NET;

namespace SingleInstance
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        static Form CreateForm()
        {
            return new Form1(); // Form1 is used for the main window.
        }

        static void OnStartNextInstance(Form mainWindow) // When the user tries to restart the application again,
                                                         // the main window is activated again.
        {
            mainWindow.WindowState = FormWindowState.Maximized;
        }
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);            
            SingleInstanceController controller = new SingleInstanceController(CreateForm, OnStartNextInstance);
            controller.Run();         
        }
    }
}

Zarówno program, jak i rozwiązanie SingleInstanceController_NET ​​powinny odwoływać się do Microsoft.VisualBasic. Jeśli chcesz ponownie aktywować działającą aplikację jako normalne okno, gdy użytkownik próbuje zrestartować uruchomiony program, drugi parametr w SingleInstanceController może mieć wartość NULL. W podanym przykładzie okno jest zmaksymalizowane.


4

Aktualizacja 2017-01-25. Po wypróbowaniu kilku rzeczy, zdecydowałem się na VisualBasic.dll, jest łatwiejszy i działa lepiej (przynajmniej dla mnie). Podaję moją poprzednią odpowiedź jako odniesienie ...

Jako odniesienie, tak zrobiłem bez przekazywania argumentów (co nie mogę znaleźć żadnego powodu, aby to zrobić ... Mam na myśli pojedynczą aplikację z argumentami, które mają być przekazywane z jednej instancji do drugiej). Jeśli wymagane jest powiązanie plików, aplikacja powinna (zgodnie ze standardowymi oczekiwaniami użytkowników) zostać utworzona dla każdego dokumentu. Jeśli musisz przekazać argumenty do istniejącej aplikacji, myślę, że użyłbym vb dll.

Nie przekazując argumentów (tylko aplikacja z pojedynczym wystąpieniem), wolę nie rejestrować nowej wiadomości Windows i nie zastępować pętli wiadomości, jak zdefiniowano w Matt Davis Solution. Chociaż dodanie dll VisualBasic nie jest niczym wielkim, ale wolę nie dodawać nowego odwołania tylko po to, aby zrobić aplikację z pojedynczą instancją. Wolę też zaimplementować nową klasę z Main zamiast wywoływać Shutdown z App.Startup override, aby zapewnić jak najszybsze wyjście.

Mam nadzieję, że każdemu się spodoba ... lub trochę zainspiruje :-)

Klasę uruchamiania projektu należy ustawić jako „SingleInstanceApp”.

public class SingleInstanceApp
{
    [STAThread]
    public static void Main(string[] args)
    {
        Mutex _mutexSingleInstance = new Mutex(true, "MonitorMeSingleInstance");

        if (_mutexSingleInstance.WaitOne(TimeSpan.Zero, true))
        {
            try
            {
                var app = new App();
                app.InitializeComponent();
                app.Run();

            }
            finally
            {
                _mutexSingleInstance.ReleaseMutex();
                _mutexSingleInstance.Close();
            }
        }
        else
        {
            MessageBox.Show("One instance is already running.");

            var processes = Process.GetProcessesByName(Assembly.GetEntryAssembly().GetName().Name);
            {
                if (processes.Length > 1)
                {
                    foreach (var process in processes)
                    {
                        if (process.Id != Process.GetCurrentProcess().Id)
                        {
                            WindowHelper.SetForegroundWindow(process.MainWindowHandle);
                        }
                    }
                }
            }
        }
    }
}

WindowHelper:

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;

namespace HQ.Util.Unmanaged
{
    public class WindowHelper
    {
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool SetForegroundWindow(IntPtr hWnd);

3

Jednak nie używając Mutex, prosta odpowiedź:

System.Diagnostics;    
...
string thisprocessname = Process.GetCurrentProcess().ProcessName;

if (Process.GetProcesses().Count(p => p.ProcessName == thisprocessname) > 1)
                return;

Umieść w środku Program.Main().
Przykład :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Diagnostics;

namespace Sample
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            //simple add Diagnostics namespace, and these 3 lines below 
            string thisprocessname = Process.GetCurrentProcess().ProcessName;
            if (Process.GetProcesses().Count(p => p.ProcessName == thisprocessname) > 1)
                return;

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Sample());
        }
    }
}

Możesz dodać MessageBox.Showdo if-statement i umieścić „Aplikacja już działa”.
To może być komuś pomocne.


4
Jeśli dwa procesy rozpoczną się w tym samym czasie, oba mogą zobaczyć dwa aktywne procesy i zakończyć się.
AT

@AT Tak, może to być również pomocne dla aplikacji działających jako Administrator lub inaczej
newbieguy

Jeśli wykonasz kopię aplikacji i zmienisz jej nazwę, możesz jednocześnie uruchomić oryginał i kopię.
Dominique Bijnens

2

Podejścia oparte na muteksach nie są wieloplatformowe, ponieważ nazwane muteksy nie są globalne w Mono. Podejścia oparte na wyliczaniu procesów nie mają żadnej synchronizacji i mogą powodować nieprawidłowe zachowanie (np. Wiele procesów uruchomionych w tym samym czasie może zakończyć się samoczynnie w zależności od czasu). Podejścia oparte na systemie okienkowym nie są pożądane w aplikacji konsolowej. To rozwiązanie, oparte na odpowiedzi Divina, rozwiązuje wszystkie te problemy:

using System;
using System.IO;

namespace TestCs
{
    public class Program
    {
        // The app id must be unique. Generate a new guid for your application. 
        public static string AppId = "01234567-89ab-cdef-0123-456789abcdef";

        // The stream is stored globally to ensure that it won't be disposed before the application terminates.
        public static FileStream UniqueInstanceStream;

        public static int Main(string[] args)
        {
            EnsureUniqueInstance();

            // Your code here.

            return 0;
        }

        private static void EnsureUniqueInstance()
        {
            // Note: If you want the check to be per-user, use Environment.SpecialFolder.ApplicationData instead.
            string lockDir = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
                "UniqueInstanceApps");
            string lockPath = Path.Combine(lockDir, $"{AppId}.unique");

            Directory.CreateDirectory(lockDir);

            try
            {
                // Create the file with exclusive write access. If this fails, then another process is executing.
                UniqueInstanceStream = File.Open(lockPath, FileMode.Create, FileAccess.Write, FileShare.None);

                // Although only the line above should be sufficient, when debugging with a vshost on Visual Studio
                // (that acts as a proxy), the IO exception isn't passed to the application before a Write is executed.
                UniqueInstanceStream.Write(new byte[] { 0 }, 0, 1);
                UniqueInstanceStream.Flush();
            }
            catch
            {
                throw new Exception("Another instance of the application is already running.");
            }
        }
    }
}

2

Używam Mutex w swoim rozwiązaniu do zapobiegania wielu wystąpieniom.

static Mutex mutex = null;
//A string that is the name of the mutex
string mutexName = @"Global\test";
//Prevent Multiple Instances of Application
bool onlyInstance = false;
mutex = new Mutex(true, mutexName, out onlyInstance);

if (!onlyInstance)
{
  MessageBox.Show("You are already running this application in your system.", "Already Running..", MessageBoxButton.OK);
  Application.Current.Shutdown();
}

1

Użyj rozwiązania mutex:

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

namespace OneAndOnlyOne
{
static class Program
{
    static String _mutexID = " // generate guid"
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        Boolean _isNotRunning;
        using (Mutex _mutex = new Mutex(true, _mutexID, out _isNotRunning))
        {
            if (_isNotRunning)
            {
                Application.Run(new Form1());
            }
            else
            {
                MessageBox.Show("An instance is already running.");
                return;
            }
        }
    }
}
}

1

Oto lekkie rozwiązanie, którego używam, które pozwala aplikacji przenieść istniejące okno na pierwszy plan bez uciekania się do niestandardowych komunikatów systemu Windows lub ślepego wyszukiwania nazw procesów.

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

static readonly string guid = "<Application Guid>";

static void Main()
{
    Mutex mutex = null;
    if (!CreateMutex(out mutex))
        return;

    // Application startup code.

    Environment.SetEnvironmentVariable(guid, null, EnvironmentVariableTarget.User);
}

static bool CreateMutex(out Mutex mutex)
{
    bool createdNew = false;
    mutex = new Mutex(false, guid, out createdNew);

    if (createdNew)
    {
        Process process = Process.GetCurrentProcess();
        string value = process.Id.ToString();

        Environment.SetEnvironmentVariable(guid, value, EnvironmentVariableTarget.User);
    }
    else
    {
        string value = Environment.GetEnvironmentVariable(guid, EnvironmentVariableTarget.User);
        Process process = null;
        int processId = -1;

        if (int.TryParse(value, out processId))
            process = Process.GetProcessById(processId);

        if (process == null || !SetForegroundWindow(process.MainWindowHandle))
            MessageBox.Show("Unable to start application. An instance of this application is already running.");
    }

    return createdNew;
}

Edycja: Możesz także przechowywać i inicjować muteks i utworzony Nowy statycznie, ale będziesz musiał jawnie pozbyć się / zwolnić muteks, gdy skończysz. Osobiście wolę utrzymywać muteks lokalny, ponieważ zostanie on automatycznie zutylizowany, nawet jeśli aplikacja zostanie zamknięta bez osiągania końca Main.



1

Dodałem metodę sendMessage do klasy NativeMethods.

Najwyraźniej metoda postmessage nie działa, jeśli aplikacja nie jest wyświetlana na pasku zadań, jednak zastosowanie metody sendmessage rozwiązuje ten problem.

class NativeMethods
{
    public const int HWND_BROADCAST = 0xffff;
    public static readonly int WM_SHOWME = RegisterWindowMessage("WM_SHOWME");
    [DllImport("user32")]
    public static extern bool PostMessage(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam);
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
    [DllImport("user32")]
    public static extern int RegisterWindowMessage(string message);
}

1

Oto ta sama rzecz zaimplementowana przez Event.

public enum ApplicationSingleInstanceMode
{
    CurrentUserSession,
    AllSessionsOfCurrentUser,
    Pc
}

public class ApplicationSingleInstancePerUser: IDisposable
{
    private readonly EventWaitHandle _event;

    /// <summary>
    /// Shows if the current instance of ghost is the first
    /// </summary>
    public bool FirstInstance { get; private set; }

    /// <summary>
    /// Initializes 
    /// </summary>
    /// <param name="applicationName">The application name</param>
    /// <param name="mode">The single mode</param>
    public ApplicationSingleInstancePerUser(string applicationName, ApplicationSingleInstanceMode mode = ApplicationSingleInstanceMode.CurrentUserSession)
    {
        string name;
        if (mode == ApplicationSingleInstanceMode.CurrentUserSession)
            name = $"Local\\{applicationName}";
        else if (mode == ApplicationSingleInstanceMode.AllSessionsOfCurrentUser)
            name = $"Global\\{applicationName}{Environment.UserDomainName}";
        else
            name = $"Global\\{applicationName}";

        try
        {
            bool created;
            _event = new EventWaitHandle(false, EventResetMode.ManualReset, name, out created);
            FirstInstance = created;
        }
        catch
        {
        }
    }

    public void Dispose()
    {
        _event.Dispose();
    }
}

1

[Podałem przykładowy kod dla aplikacji konsoli i wpf poniżej.]

Musisz tylko sprawdzić wartość createdNewzmiennej (przykład poniżej!) Po utworzeniu nazwanej instancji Mutex.

Wartość logiczna createdNewzwróci false:

jeśli instancja Mutex o nazwie „YourApplicationNameHere” została już gdzieś utworzona w systemie

Wartość logiczna createdNewzwróci wartość prawda:

jeśli jest to pierwszy Mutex o nazwie „YourApplicationNameHere” w systemie.


Aplikacja konsoli - przykład:

static Mutex m = null;

static void Main(string[] args)
{
    const string mutexName = "YourApplicationNameHere";
    bool createdNew = false;

    try
    {
        // Initializes a new instance of the Mutex class with a Boolean value that indicates 
        // whether the calling thread should have initial ownership of the mutex, a string that is the name of the mutex, 
        // and a Boolean value that, when the method returns, indicates whether the calling thread was granted initial ownership of the mutex.

        using (m = new Mutex(true, mutexName, out createdNew))
        {
            if (!createdNew)
            {
                Console.WriteLine("instance is alreday running... shutting down !!!");
                Console.Read();
                return; // Exit the application
            }

            // Run your windows forms app here
            Console.WriteLine("Single instance app is running!");
            Console.ReadLine();
        }


    }
    catch (Exception ex)
    {

        Console.WriteLine(ex.Message);
        Console.ReadLine();
    }
}

Przykład WPF:

public partial class App : Application
{
static Mutex m = null;

protected override void OnStartup(StartupEventArgs e)
{

    const string mutexName = "YourApplicationNameHere";
    bool createdNew = false;

    try
    {
        // Initializes a new instance of the Mutex class with a Boolean value that indicates 
        // whether the calling thread should have initial ownership of the mutex, a string that is the name of the mutex, 
        // and a Boolean value that, when the method returns, indicates whether the calling thread was granted initial ownership of the mutex.

        m = new Mutex(true, mutexName, out createdNew);

        if (!createdNew)
        {
            Current.Shutdown(); // Exit the application
        }

    }
    catch (Exception)
    {
        throw;
    }

    base.OnStartup(e);
}


protected override void OnExit(ExitEventArgs e)
{
    if (m != null)
    {
        m.Dispose();
    }
    base.OnExit(e);
}
}

1

Oszczędzające czas rozwiązanie dla C # Winforms ...

Program.cs:

using System;
using System.Windows.Forms;
// needs reference to Microsoft.VisualBasic
using Microsoft.VisualBasic.ApplicationServices;  

namespace YourNamespace
{
    public class SingleInstanceController : WindowsFormsApplicationBase
    {
        public SingleInstanceController()
        {
            this.IsSingleInstance = true;
        }

        protected override void OnStartupNextInstance(StartupNextInstanceEventArgs e)
        {
            e.BringToForeground = true;
            base.OnStartupNextInstance(e);
        }

        protected override void OnCreateMainForm()
        {
            this.MainForm = new Form1();
        }
    }

    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            string[] args = Environment.GetCommandLineArgs();
            SingleInstanceController controller = new SingleInstanceController();
            controller.Run(args);
        }
    }
}

1

Sprawdź proponowane rozwiązanie stąd które używa semafora, aby ustalić, czy istniejąca instancja już działa, działa dla aplikacji WPF i może przekazywać argumenty z drugiej instancji do pierwszej już działającej instancji za pomocą TcpListener i TcpClient:

Działa również dla .NET Core, nie tylko dla .NET Framework.


1

Nie mogę znaleźć tutaj krótkiego rozwiązania , więc mam nadzieję, że komuś się spodoba:

AKTUALIZACJA 2018-09-20

Umieść ten kod w swoim Program.cs:

using System.Diagnostics;

static void Main()
{
    Process thisProcess = Process.GetCurrentProcess();
    Process[] allProcesses = Process.GetProcessesByName(thisProcess.ProcessName);
    if (allProcesses.Length > 1)
    {
        // Don't put a MessageBox in here because the user could spam this MessageBox.
        return;
    }

    // Optional code. If you don't want that someone runs your ".exe" with a different name:

    string exeName = AppDomain.CurrentDomain.FriendlyName;
    // in debug mode, don't forget that you don't use your normal .exe name.
    // Debug uses the .vshost.exe.
    if (exeName != "the name of your executable.exe") 
    {
        // You can add a MessageBox here if you want.
        // To point out to users that the name got changed and maybe what the name should be or something like that^^ 
        MessageBox.Show("The executable name should be \"the name of your executable.exe\"", 
            "Wrong executable name", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return;
    }

    // Following code is default code:
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new MainForm());
}

To wprowadzi warunki wyścigu. Musisz użyć muteksu.
georgiosd,

1
nie ma gwarancji, że jeśli rozwiniesz dwa wystąpienia w tym samym czasie, to zadziała. Jak aktualizacja zmiennej z dwóch różnych wątków. Podstępny ryzykowny biznes. Użyj siły, Luke :)
georgiosd,

@georgiosd ah Rozumiem, co masz na myśli. Jak gdyby ktoś uruchomił .exe i zmienił nazwę. Tak, byłby to sposób na uruchomienie go więcej razy, ale normalnie .exe nie działa, jeśli nazwa zostanie zmieniona. Zaktualizuję swoją odpowiedź ^^ Dziękuję Luke: D za zwrócenie na to uwagi :)
Deniz,

1
Nie tylko to @Deniz. Jeśli naprawdę szybko uruchomisz dwa procesy, istnieje szansa, że ​​lista procesów lub metoda pobierająca je wykonają się, dopóki tylko jeden z nich się pojawi. To może być przypadek
skrajny,

@georgiosd Czy możesz to udowodnić? Ponieważ Iv'e przetestował to właśnie dla ciebie hehe. Ale nie było to dla mnie możliwe, nawet naprawdę „naprawdę szybko”! : P Więc nie rozumiem, dlaczego wierzysz w coś, co po prostu nie jest prawdą, a nawet nie lubię tego niewinnego kodu: D
Deniz
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.