Edited at

【Windows/C#】フォームなしのアプリケーションをなるべく綺麗に書いてみる

今さらながらQiitaに登録したので、お試し投稿。

こんなので良いのだろうか・・・?


はじめに

C#にてバックグラウンドで動作する常駐アプリケーションを開発する際に、Program.csのMainメソッド内にDoEventsSleepでメインループさせるコードをよく目にするけど、もう少し綺麗に書きたい。

もっと言えば、Program.csの中身は・・・


Program.cs

/// <summary>

/// アプリケーションのメイン エントリ ポイントです。
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}

この形を維持して、メッセージループは標準のものを利用したい。

環境:Visual Studio 2017 / .Net 4.6 / C# 7.3


こんなクラスをこさえてみた

Application.RunメソッドにFormクラス以外で渡せるApplicationContextクラスを使って、便利なクラスを作ってみた。

FormlessApplicationクラス: 実際に行う処理を実装する基底クラス

FormlessApplicationContextクラス: FormlessApplicationクラスを制御するクラス


FormlessApplication.cs

/// <summary>

/// メインフォームを利用しないアプリケーションの処理を実装します。
/// </summary>
public abstract class FormlessApplication : IDisposable
{
/// <summary>
/// このアプリケーションを実行しているコンテキストを取得します。
/// </summary>
protected internal FormlessApplicationContext Context { get; internal set; }

/// <summary>
/// アプリケーションを初期化します。
/// </summary>
/// <returns>処理を継続する場合は、true。それ以外は、false。</returns>
public virtual bool Initialize()
{
return true;
}

/// <summary>
/// アプリケーションの処理を実行します。
/// </summary>
public virtual void DoWork()
{
}

/// <summary>
/// 全てのリソースを破棄します。
/// </summary>
public void Dispose()
{
Dispose(true);
}

/// <summary>
/// 全てのリソースを破棄します。
/// </summary>
/// <param name="disposing">マネージドリソースを破棄する場合は、true。それ以外は、false。</param>
protected virtual void Dispose(bool disposing)
{
}

/// <summary>
/// アプリケーションを終了します。
/// </summary>
protected void ExitApp()
{
this.Context.ExitThread();
}
}



FormlessApplicationContext.cs

/// <summary>

/// メインフォームを利用しないアプリケーションの終了を定義します。
/// </summary>
public class FormlessApplicationContext : ApplicationContext
{
static class SafeNativeMethods
{
[StructLayout(LayoutKind.Sequential)]
internal struct POINT
{
public int x;
public int y;
}

[StructLayout(LayoutKind.Sequential)]
internal struct MSG
{
public IntPtr hwnd;
public int message;
public IntPtr wParam;
public IntPtr lParam;
public int time;
public POINT pt;
}

[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool PeekMessage(
out MSG msg, IntPtr hWnd,
int wMsgFilterMin, int wMsgFilterMax, int wRemoveMsg);
}

/// <summary>
/// 実行しているアプリケーションを取得します。
/// </summary>
public FormlessApplication App { get; }

/// <summary>
/// FormlessApplicationContext クラスを初期化し、
/// 実行するアプリケーションを設定します。。
/// </summary>
/// <param name="app">実行するアプリケーション</param>
public FormlessApplicationContext(FormlessApplication app)
: base()
{
this.App = app;
this.App.Context = this;

// アプリケーション初期化
if (!this.App.Initialize())
{
// アプリケーション終了
Application.Idle += (e, sender) => this.ExitThread();
return;
}

// モーダル状態を捕捉
Application.EnterThreadModal += this.Application_EnterThreadModal;
Application.LeaveThreadModal += this.Application_LeaveThreadModal;
// Idleイベント登録
Application.Idle += this.Application_Idle;
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
// App破棄
this.App.Dispose();
}

base.Dispose(disposing);
}

private void Application_EnterThreadModal(object sender, EventArgs e)
{
// Idleイベント一時解除
Application.Idle -= this.Application_Idle;
}

private void Application_LeaveThreadModal(object sender, EventArgs e)
{
// Idleイベント再登録
Application.Idle += this.Application_Idle;
}

private void Application_Idle(object sender, EventArgs e)
{
// ウィンドウメッセージを受信するまでループ処理
while (!SafeNativeMethods.PeekMessage(
out SafeNativeMethods.MSG _, IntPtr.Zero, 0, 0, 0))
{
// アプリケーションの処理を実行
this.App.DoWork();
System.Threading.Thread.Sleep(1);
}
}
}



使い方

FormlessApplicationクラスを継承して、必要な処理を書くだけ。


TestFormlessApplication.cs

class TestFormlessApplication : FormlessApplication

{
/// <summary>
/// アプリケーションを初期化します。
/// </summary>
public override bool Initialize()
{
// 初期化処理を書く
// エラーなどで終了したい場合は、return false;
}

/// <summary>
/// アプリケーションの処理を実行します。
/// </summary>
public override void DoWork()
{
// メイン処理を書く
// アプリケーションを終了するときは、this.ExitApp();
}

/// <summary>
/// 全てのリソースを破棄します。
/// </summary>
/// <param name="disposing">マネージドリソースを破棄する場合は、true。それ以外は、false。</param>
protected override void Dispose(bool disposing)
{
// リソースの解放処理を書く

base.Dispose(disposing);
}
}


Program.csの中身はシンプルにApplication.Runの引数を変えるだけ。


Program.cs

/// <summary>

/// アプリケーションのメイン エントリ ポイントです。
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new FormlessApplicationContext(new TestFormlessApplication()));
}

これなら標準の処理を極力活かしつつループ処理ができる。


ポイント


初期化


FormlessApplicationContext.cs

// アプリケーション初期化

if (!this.App.Initialize())
{
// アプリケーション終了
Application.Idle += (e, sender) => this.ExitThread();
return;
}

FormlessApplicationクラスのコンストラクタで初期化処理を行うと、エラーなどで終了したいときに困るので、初期化関数を別で用意した。(上手くやればコンストラクタで初期化する形でもいける気がする)

またコンストラクタ内でExitThread()を呼んでも終了できないようなので、Idleイベントで呼ばれるように工夫。


モーダルの罠


FormlessApplicationContext.cs

// モーダル状態を捕捉

Application.EnterThreadModal += this.Application_EnterThreadModal;
Application.LeaveThreadModal += this.Application_LeaveThreadModal;

ループ処理中にダイアログをShowDialogすると、更にIdleイベントが呼ばれてダイアログが無限に表示される現象が発生。

モーダル状態に入ったときはIdleイベントを一時的に解除することで回避。


ループ処理


FormlessApplicationContext.cs

private void Application_Idle(object sender, EventArgs e)

{
// ウィンドウメッセージを受信するまでループ処理
while (!SafeNativeMethods.PeekMessage(
out SafeNativeMethods.MSG _, IntPtr.Zero, 0, 0, 0))
{
// アプリケーションの処理を実行
this.App.DoWork();
System.Threading.Thread.Sleep(1);
}
}

溜まったメッセージを処理し終わったときにIdleイベントが「1度だけ」発生する。逆に言うとメッセージが溜まって処理されないとIdleイベントが発生しない。

それを利用して、起動時のメッセージ処理後のIdleイベントでループ処理を行い、メッセージが来たら処理を返してあげて、そのメッセージ処理後のIdleイベントで再びループ処理を行い、メッセージが来たら・・・ry(無限ループ)

メッセージが溜まってるかどうかを判断するためにWinAPIのPeekMessage関数を使用。(メッセージの処理自体は標準の処理にお任せ)


さいごに

メインループほどの頻度が必要なければ、TimerとNotifyIconを利用するようなComponentクラスを作って、ApplicationContextクラスから使えば、WinAPIやIdleイベントを使うことなく、お手軽に通知領域に表示する常駐アプリが作れたりもする。