5
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-13

今さらながら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イベントを使うことなく、お手軽に通知領域に表示する常駐アプリが作れたりもする。

5
10
0

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
5
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?