プログラミング C# - 翔ソフトウェア (Sho's)

C#/.NET/ソフトウェア開発など

.NET アプリケーション (WPF/Windows フォーム) で多重起動を禁止し、単一のプロセスで動作させる

■ 概要

.NET アプリケーション (WPF/Windows フォーム) で多重起動を禁止し、単一のプロセスで動作するようにする方法を C# で示す。

■ 解説

WPF アプリケーションや Windows フォーム アプリケーションにおいて、多重起動を禁止してみよう。

ここでは、既にアプリケーションが起動していているときに、そのアプリケーションでドキュメントを開こうとした場合 (*1) に、

  • 他のプロセスが起動していなければ、普通にドキュメントを開く。
  • 他のプロセスが既に起動していれば、その起動中のアプリケーションでドキュメントを開く。

という動作を実現する。

(*1) アプリケーションでドキュメントを開くのには、次のようなケースが考えられる。

  • 実行ファイルに、ドキュメント ファイルをドラッグ&ドロップ
    例. 実行ファイルを a.exe とすると、ファイル エクスプローラーで a.png を a.exe にドラッグ&ドロップ
  • コマンド プロンプトで、コマンドライン引数にドキュメント ファイルを指定して実行ファイルを起動
    例. 実行ファイルのパスを c:\demo\a.exe、ドキュメント ファイル c:\demo\a.png とすると、コマンド プロンプトを開き、c:\demo\a.exe c:\demo\a.png[Enter] と入力
  • ファイルをアプリケーションに関連付けし、開く。

■ 実現方法

すでに起動しているかのチェック方法

ここでは Mutex を用いる。詳細はサンプル コードを参照のこと。

すでに起動しているアプリケーションとの通信方法

ここでは Ipc を用いる。詳細はサンプル コードを参照のこと。

■ サンプル コード

WPF と Windows フォームの両方のサンプルを示そうと思う。

■ 共通部 (WPF、Windows フォーム非依存)

先ずは共通部から。

SingleDocument.cs
// 多重起動を禁止する
// - 他のプロセスが起動していなければ、普通にドキュメントを開く。
// - 他のプロセスが既に起動していれば、その起動中のアプリケーションでドキュメントを開く。
// WPF、Windows フォーム非依存
// 参照設定 System.Runtime.Remoting が必要

using System;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Threading;

namespace SingleDocumentSample
{
    // .NET で多重起動しないアプリケーションを作成するためのクラス (WPF、Windows フォーム非依存)
    public class SingleDocumentHelper
    {
        // 複数のプロセスで一つしかないことを保証する
        class Singleton : IDisposable
        {
            readonly Mutex mutex = null; // 複数のプロセスから参照するために Mutex を使用

            public bool IsExist // 既に存在しているかどうか
            {
                get
                {
                    return !mutex.WaitOne(0, false); // Mutex が既に作成されていたら true
                }
            }

            public Singleton(string uniqueName)
            {
                mutex = new Mutex(false, uniqueName); // コンストラクターで、このアプリケーション独自の名前で Mutex を作成
            }

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

        // Ipc によって複数のプロセスで共有するアイテムを格納する
        class IpcRemoteObject : MarshalByRefObject
        {
            public object Item { get; set; }

            // IpcRemoteObject に複数のプロセスで共有するアイテムを格納
            public static void Send(string channelName, string itemName, object item)
            {
                var channel = new IpcClientChannel(); // IPC (プロセス間通信) クライアント チャンネルの生成
                ChannelServices.RegisterChannel(channel, true); // チャンネルを登録
                var remoteObject = Activator.GetObject(typeof(IpcRemoteObject), string.Format("ipc://{0}/{1}", channelName, itemName)) as IpcRemoteObject; // リモートオブジェクトを取得
                if (remoteObject != null)
                    remoteObject.Item = item;
            }
        }

        // 指定された時間毎に Tick イベントが起きるオブジェクトのインタフェイス
        // アプリケーション側で実装する
        public interface ITicker : IDisposable
        {
            event Action Tick;

            void Start();
        }

        // 指定された時間毎に IpcRemoteObject をチェックし、空でなければ Receive イベントが起きる
        class Receiver : IDisposable
        {
            public event Action<object> Receive;

            IpcRemoteObject remoteObject;
            readonly ITicker ticker;

            public Receiver(string uniqueName, string itemName, ITicker ticker)
            {
                Initialize(uniqueName, itemName);
                this.ticker = ticker;
                ticker.Tick += OnTick;
                ticker.Start();
            }

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

            void Initialize(string uniqueName, string itemName)
            {
                var channel = new IpcServerChannel(uniqueName); // このアプリケーション独自の名前で IPC (プロセス間通信) サーバー チャンネルを作成
                ChannelServices.RegisterChannel(channel, true); // チャンネルを登録
                remoteObject = new IpcRemoteObject(); // リモートオブジェクトを生成
                RemotingServices.Marshal(remoteObject, itemName, typeof(IpcRemoteObject)); // リモートオブジェクトを公開
            }

            void OnTick()
            {
                if (remoteObject.Item != null) { // リモートオブジェクトがあれば
                    if (Receive != null)
                        Receive(remoteObject.Item); // Receive イベントを起こす
                    remoteObject.Item = null;
                }
            }
        }

        // 多重起動しないアプリケーション (他のプロセスが起動済みの場合は、その起動済みのプロセスにドキュメントを開かせ、終了する)
        public class Application : IDisposable
        {
            const string itemName = "DocumentPathName";

            Singleton singleton = null;
            Receiver receiver = null;

            // 初期化 (戻り値が偽の場合は終了)
            public bool Initialize(ITicker ticker, string documentPath = "", Action<string> open = null)
            {
                var applicationProductName = *1 // documentPath が空でなければ
                        open(documentPath); // documentPath を開く
                }
                return true; // 終了しない
            }

            public void Dispose()
            {
                if (receiver != null) {
                    receiver.Dispose();
                    receiver = null;
                }
                if (singleton != null) {
                    singleton.Dispose();
                    singleton = null;
                }
            }
        }
    }
}
WPF の場合

上記 SingleDocumentHelper クラスを WPF で使う場合は次のようになる。

SingleDocumentWpfApplication.cs

App.xaml.cs のアプリケーション クラスのベース クラス。

タイマーに System.Windows.Threading.DispatcherTimer クラスを使う。

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

namespace SingleDocumentSample.Wpf
{
    // WPF 多重起動防止アプリケーション
    public class SingleDocumentApplication : Application
    {
        // WPF 用の ITicker の実装
        // 指定された時間毎に Tick イベントが起きる
        class Ticker : SingleDocumentHelper.ITicker
        {
            public event Action Tick;

            public const int DefaultInterval = 1000;
            readonly DispatcherTimer timer;

            public Ticker(int interval = DefaultInterval)
            { timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(interval) }; }

            public void Start()
            {
                // DispatcherTimer を使用して定期的に Tick イベントを起こす
                timer.Tick += (sender, e) => RaiseTick();
                timer.Start();
            }

            public void Dispose()
            { timer.Stop(); }

            void RaiseTick()
            {
                if (Tick != null)
                    Tick();
            }
        }

        public event Action<string> OpenDocument;

        readonly SingleDocumentHelper.Application singleDocumentApplication = new SingleDocumentHelper.Application();
        string documentPath = null;

        protected string DocumentPath
        {
            get { return documentPath; }
            set {
                documentPath = value;
                if (OpenDocument != null)
                    OpenDocument(value); // ドキュメントを開く
            }
        }

        public SingleDocumentApplication(Window mainWindow)
        { MainWindow = mainWindow; }

        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            if (e.Args.Length > 0) // コマンドライン引数があれば
                DocumentPath = e.Args[0]; // ドキュメント ファイルのパスとする

            if (singleDocumentApplication.Initialize(new Ticker(), DocumentPath, sourcePath => DocumentPath = sourcePath))
                MainWindow.Show();
            else
                Shutdown(); // 他のプロセスが既に起動していれば終了
        }

        protected override void OnExit(ExitEventArgs e)
        {
            singleDocumentApplication.Dispose();
            base.OnExit(e);
        }
    }
}
App.xaml
<l:SingleDocumentApplication x:Class="SingleDocumentSample.Wpf.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:l="clr-namespace:SingleDocumentSample.Wpf" />
App.xaml.cs

上記 SingleDocumentApplication クラスから派生。

using System.Windows;

namespace SingleDocumentSample.Wpf
{
    partial class App : SingleDocumentApplication
    {
        public App() : base(new MainWindow())
        { OpenDocument += sourcePath => *2 {
                if (singleDocumentApplication.Initialize(new Ticker(), DocumentPath, sourcePath => DocumentPath = sourcePath)) {
                    Application.Run(mainForm);
                    return true;
                }
                return false; // 他のプロセスが既に起動していれば終了
            }
        }
    }
}
Program.cs

Main メソッド。

using System;

namespace SingleDocumentSample.WinForm
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            var application = new SingleDocumentApplication();
            var mainForm    = new MainForm();
            application.OpenDocument += documentPath => mainForm.DocumentPath = documentPath;
            application.Run(mainForm);
        }
    }
}
MainForm.cs (MainForm.Designer.cs は不要)

WebBrowser コントロールが貼ってある。

指定されたパスや URL を WebBrowser コントロールで表示する。

using System;
using System.Drawing;
using System.Windows.Forms;

namespace SingleDocumentSample.WinForm
{
    class MainForm : Form
    {
        readonly WebBrowser browser;

        public string DocumentPath
        {
            set {
                browser.Navigate(new Uri(value, UriKind.RelativeOrAbsolute));
                Text = value;
                ToTop();
            }
        }

        public MainForm()
        {
            SuspendLayout();
            ClientSize = new Size(500, 500);
            Text       = "SingleDocumentSampleWinForm";
            browser    = new WebBrowser { Dock = DockStyle.Fill };
            Controls.Add(browser);
            ResumeLayout(false);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
                browser.Dispose();
            base.Dispose(disposing);
        }

        // ウィンドウを最前面に表示
        void ToTop()
        {
            if (WindowState == FormWindowState.Minimized)
                WindowState = FormWindowState.Normal;
            Activate();
        }
    }
}

■ 実行方法

  1. WPF 版または Windows フォーム版をビルドする。
  2. ファイル エクスプローラーで実行ファイル (例えば、SingleDocumentSample.Wpf.exe や SingleDocumentSample.WinForm.exe) のあるフォルダーを開き、実行ファイルのアイコンに画像ファイル (JPEG ファイルや PNG ファイル) 等をドラッグ&ドロップする。 (または、コマンドプロンプトで実行ファイルのバスにコマンドライン引数で画像ファイルのパスやURLを渡す)
  3. サンプル プログラムが起動し、その画像ファイルが表示される。
  4. サンプル プログラムを最小化し、再度ファイル エクスプローラーで別の画像ファイルをドラッグ&ドロップする。 (または、コマンドプロンプトで実行ファイルのバスにコマンドライン引数で別の画像ファイルのパスやURLを渡す)
  5. 今度はプログラムは起動せず、最小化されていた先程のサンプル プログラムが最前面に戻り、新たな画像ファイルが表示される。
実行イメージ
実行イメージ

*1:AssemblyProductAttribute)Attribute.GetCustomAttribute(Assembly.GetExecutingAssembly(), typeof(AssemblyProductAttribute))).Product; singleton = new Singleton(applicationProductName); if (singleton.IsExist) { // 既に他のプロセスが起動していたら IpcRemoteObject.Send(applicationProductName, itemName, documentPath); // その起動済みのプロセスにドキュメントのパスを送信して return false; // 終了する } receiver = new Receiver(applicationProductName, itemName, ticker); if (open != null) { receiver.Receive += item => open((string)item); // Receive イベントが起きたらアイテムを open するように設定 if (!string.IsNullOrWhiteSpace(documentPath

*2:MainWindow)MainWindow).DocumentPath = sourcePath; } } }

MainWindow.xaml

WebBrowser コントロールが貼ってあるだけ。

<Window x:Class="SingleDocumentSample.Wpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="SingleDocumentSample.Wpf" Height="500" Width="500">
    <Grid>
        <WebBrowser x:Name="browser" />
    </Grid>
</Window>
MainWindow.xaml.cs

指定されたパスや URL を WebBrowser コントロールで表示する。

using System;
using System.Windows;

namespace SingleDocumentSample.Wpf
{
    partial class MainWindow : Window
    {
        public string DocumentPath
        {
            get { return browser.Source == null ? null : browser.Source.AbsolutePath; }
            set {
                browser.Source = new Uri(value, UriKind.RelativeOrAbsolute);
                Title          = value;
                ToTop();
            }
        }

        public MainWindow()
        { InitializeComponent(); }

        // ウィンドウを最前面に表示
        void ToTop()
        {
            if (WindowState == WindowState.Minimized)
                WindowState = WindowState.Normal;
            Activate();
        }
    }
}
■ Windows フォームの場合

そして、SingleDocumentHelper クラスを Windows フォームで使う場合は次のようになる。

SingleDocumentWindowsFormApplication.cs

Main メソッドで使用するためのクラス。

タイマーに System.Windows.Forms.Timer クラスを使う。

using System;
using System.Windows.Forms;

namespace SingleDocumentSample.WinForm
{
    class SingleDocumentApplication
    {
        // ウィンドウズ フォーム用の ITicker の実装
        // 指定された時間毎に Tick イベントが起きる
        class Ticker : SingleDocumentHelper.ITicker
        {
            public event Action Tick;

            public const int DefaultInterval = 1000;
            readonly Timer timer;

            public Ticker(int interval = DefaultInterval)
            { timer = new Timer { Interval = interval }; }

            public void Start()
            {
                // Timer を使用して定期的に Tick イベントを起こす
                timer.Tick += (sender, e) => RaiseTick();
                timer.Start();
            }

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

            void RaiseTick()
            {
                if (Tick != null)
                    Tick();
            }
        }

        public event Action<string> OpenDocument;

        string documentPath = null;

        protected string DocumentPath
        {
            get { return documentPath; }
            set {
                documentPath = value;
                if (OpenDocument != null)
                    OpenDocument(value); // ドキュメントを開く
            }
        }

        public bool Run(Form mainForm)
        {
            var commandLineArgs = Environment.GetCommandLineArgs();
            if (commandLineArgs.Length > 1) // コマンドライン引数があれば
                DocumentPath = commandLineArgs[1]; // ドキュメント ファイルのパスとする

            using (var singleDocumentApplication = new SingleDocumentHelper.Application(