35
35

【C#】完璧で幸福にアプリケーションの二重起動を防止してすでに起動しているアプリを前面に表示する

Last updated at Posted at 2024-01-14

ごきげんよう市民!
今日はアプリケーションの二重起動の防止について学びます。

アプリケーションの二重起動の防止

あなたもご存じの通り、アプリケーションの不要な二重(多重)起動を防止しておくと、ユーザビリティが向上します。

「……あっ!?二つ起動してた。じゃあ一つ消して……うわ、設定値が初期値に上書きされちゃった!」なんて間抜けな事故を防げます(アプリが二重に起動していて、アプリ終了時に設定を保存する仕組みになっていたりすると、意図せずして上書きされてしまったりする場合がありますからね!)。

Mutexを使用した実装例

市民、あなたはMutexを使用することで、アプリ間を跨いでリソースを監視することができます。

Mutexとは、リソースへの排他的アクセスを提供するクラスです。

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

namespace WindowsFormsApp2
{
    internal static class Program
    {
        private static Mutex mutex;

        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            bool createdNew;
            mutex = new Mutex(true, "SampleApplication", out createdNew);

            try
            {
                // 二重起動をチェック
                if (!createdNew)
                {
                    Console.WriteLine("アプリケーションは既に実行中です。");
                    return;
                }

                Application.Run(new Form1());
            }
            finally
            {
                // ミューテックスが取得されている場合にのみリリース
                if (createdNew)
                {
                    mutex.ReleaseMutex();
                }
            }
        }
    }
}

とても簡単です。

private static Mutex mutex;

このようにMutexを宣言し、それを取得したり、解放したりしているだけです。

おっと、Mutexを使用した場合は、ReleaseMutexを忘れないでください。
リソースを使い終わったら、元の位置に!
基本ですよね?

すべてのアプリに二重(多重)起動の防止が必要なわけではない

全てのアプリにおいて、二重(多重)起動を防止する必要はありません。
例えばペイントなんかは二重起動オーケーです。
別々の絵を描きたいときだってあるし、画像を見比べる必要があるときだってあるでしょう。

ペイント

image.png

一個しか起動しないはずで、二つ以上あるとややこしい場合は多重起動しません。

さて、市民。アプリケーションの二重起動を防止した後、……どうしましょうか?

「こういう時どういう実装にすればいいんだっけ?」と思いましたら、適当な(=程よい)アプリケーションをぱちぱちっと起動してみるのがいいかと思います。えーっと、そうですね……。このたとえが適切か分かりませんが、Steamとか。ううーん。
Microsoft Storeなんかどうです? 私は使ったことがありませんけどね。

Microsoft Store

image.png

  1. 何もしない。何も起きない(黙って、片方を起動しないだけで終わる)。
  2. 「アプリケーションが二重起動しています。終了します。」というメッセージボックスを出す。
  3. すでに起動しているアプリを前面に出す。

ほかにも方法があるかもしれませんが、私が思いつくのは3つです。1と2は簡単ですが、3は少し工夫が要ります。

すでに起動しているアプリを前面に出す

この3つ目は、簡単そうに見えて少々厄介です。
Win32 APIを使用する必要があるためです。
……ああ、怖がらなくてもいいですよ!

このように、APIを使用するための宣言をします。
SetForegroundWindowでウィンドウを前面に出します。
IsIconicで最小化を判定し、ShowWindowAsyncで前面表示させます。

[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool ShowWindowAsync(IntPtr hWnd,int nCmdShow);
[DllImport("user32.dll")]
private static extern bool IsIconic(IntPtr hWnd);

SW_RESTOREはShowWindow 関数の定義値を参照してください。
「ウィンドウをアクティブにして表示」したいので、9です。マジックナンバーになってしまうので、定義しているのですね。

private const int SW_RESTORE = 9;

おっと、

using System.Runtime.InteropServices;

も忘れないでくださいね。

前面に表示するためのプロセスを取得する関数、GetPreviousProcessを用意しましょう。プロセス名からの検索になります。

public static Process GetPreviousProcess()
{
    Process curProcess = Process.GetCurrentProcess();
    string currentPath = curProcess.MainModule.FileName;

    Process[] allProcesses = Process.GetProcessesByName(curProcess.ProcessName);

    foreach (Process checkProcess in allProcesses)
    {
        if (checkProcess.Id != curProcess.Id)
        {
            string checkPath;

            try
            {
                checkPath = checkProcess.MainModule.FileName;
            }
            catch (System.ComponentModel.Win32Exception)
            {
                // アクセス権限がない場合は無視
                continue;
            }

            // プロセスのフルパス名を比較して同じアプリケーションか検証
            if (String.Compare(checkPath, currentPath, true) == 0)
            {
                return checkProcess;
            }
        }
    }

    return null;
}

参考にしたのはこちらです(少し改変しました)。

@IT 多重起動禁止時に実行中のWindowsアプリケーションを最前面に表示するには?

FindWindow関数は不向き

FindoWindow関数というものもあります。
しかし、市民、このシチュエーションでは、FindWindow関数が使えない(使いづらい)のには注意してください。

FindWindowは、ウィンドウタイトル(またはクラス名)から取得する関数ですが、これはウィンドウのタイトルから検索しますので、同名のタイトル……たとえば、ファイルエクスプローラーでアプリケーションの場所を開いている場合なんかにも引っ掛かります。その場合、ファイルエクスプローラーが前面に表示されてしまいます。
実現はできますが、妙なところで引っかかるので、注意が必要です。

はじめてWinAPIを使用するときは見慣れなさに驚くかもしれません。これはWindowsの機能を使って、別のアプリケーションを触るための機構です。今回は自分のアプリケーションを起動させていますが、べつに、操作できるのは自分のアプリケーションに限りません。
例えば、あなたのアプリケーションのボタンを押したら、後ろに引っ込めておいた「火山の娘」が前面に来るようなソフトを作れるんです! わくわくしませんか?(そうでもない?)

すでに起動しているアプリを前面に出す場合のコード

全体としては、このようなものになります。

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApp2
{
    internal static class Program
    {
        private static extern bool SetForegroundWindow(IntPtr hWnd);
        [DllImport("user32.dll")]
        private static extern bool SetForegroundWindow(IntPtr hWnd);
        [DllImport("user32.dll")]
        private static extern bool ShowWindowAsync(IntPtr hWnd,int nCmdShow);
        [DllImport("user32.dll")]
        private static extern bool IsIconic(IntPtr hWnd);
        private const int SW_RESTORE = 9;  // 画面を元の大きさに戻す    

        private static Mutex mutex;

        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            bool createdNew;
            mutex = new Mutex(true, "SampleApplication", out createdNew);

            if (!createdNew)
            {
                Process previousProcess = GetPreviousProcess();
                if (previousProcess != null)
                {
                    // 既存のプロセスのメインウィンドウを前面に表示
                    IntPtr hWnd = previousProcess.MainWindowHandle;
                    if (hWnd != IntPtr.Zero)
                    {
                        // 最小化されている場合は元のサイズに戻す
                        if (IsIconic(hWnd))
                        {
                            ShowWindowAsync(hWnd, SW_RESTORE);
                        }
                        SetForegroundWindow(hWnd);
                    }
                    else
                    {
                        MessageBox.Show("既に実行中のウィンドウが見つかりませんでした。");
                    }
                }
                return;
            }
            Application.Run(new Form1());
        }

        public static Process GetPreviousProcess()
        {
            Process curProcess = Process.GetCurrentProcess();
            string currentPath = curProcess.MainModule.FileName;

            Process[] allProcesses = Process.GetProcessesByName(curProcess.ProcessName);

            foreach (Process checkProcess in allProcesses)
            {
                if (checkProcess.Id != curProcess.Id)
                {
                    string checkPath;

                    try
                    {
                        checkPath = checkProcess.MainModule.FileName;
                    }
                    catch (System.ComponentModel.Win32Exception)
                    {
                        // アクセス権限がない場合は無視
                        continue;
                    }

                    // プロセスのフルパス名を比較して同じアプリケーションか検証
                    if (String.Compare(checkPath, currentPath, true) == 0)
                    {
                        return checkProcess;
                    }
                }
            }

            return null;
        }
    }
}

どうでしょうか、市民。さらなるユーザービリティの向上をはかってください。

35
35
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
35