今さらながらQiitaに登録したので、お試し投稿。
こんなので良いのだろうか・・・?
はじめに
C#にてバックグラウンドで動作する常駐アプリケーションを開発する際に、Program.csのMainメソッド内にDoEventsとSleepでメインループさせるコードをよく目にするけど、もう少し綺麗に書きたい。
もっと言えば、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クラスを制御するクラス
/// <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();
}
}
/// <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クラスを継承して、必要な処理を書くだけ。
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の引数を変えるだけ。
/// <summary>
/// アプリケーションのメイン エントリ ポイントです。
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new FormlessApplicationContext(new TestFormlessApplication()));
}
これなら標準の処理を極力活かしつつループ処理ができる。
ポイント
初期化
// アプリケーション初期化
if (!this.App.Initialize())
{
// アプリケーション終了
Application.Idle += (e, sender) => this.ExitThread();
return;
}
FormlessApplicationクラスのコンストラクタで初期化処理を行うと、エラーなどで終了したいときに困るので、初期化関数を別で用意した。(上手くやればコンストラクタで初期化する形でもいける気がする)
またコンストラクタ内でExitThread()を呼んでも終了できないようなので、Idleイベントで呼ばれるように工夫。
モーダルの罠
// モーダル状態を捕捉
Application.EnterThreadModal += this.Application_EnterThreadModal;
Application.LeaveThreadModal += this.Application_LeaveThreadModal;
ループ処理中にダイアログをShowDialogすると、更にIdleイベントが呼ばれてダイアログが無限に表示される現象が発生。
モーダル状態に入ったときは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);
}
}
溜まったメッセージを処理し終わったときにIdleイベントが「1度だけ」発生する。逆に言うとメッセージが溜まって処理されないとIdleイベントが発生しない。
それを利用して、起動時のメッセージ処理後のIdleイベントでループ処理を行い、メッセージが来たら処理を返してあげて、そのメッセージ処理後のIdleイベントで再びループ処理を行い、メッセージが来たら・・・ry(無限ループ)
メッセージが溜まってるかどうかを判断するためにWinAPIのPeekMessage関数を使用。(メッセージの処理自体は標準の処理にお任せ)
#さいごに
メインループほどの頻度が必要なければ、TimerとNotifyIconを利用するようなComponentクラスを作って、ApplicationContextクラスから使えば、WinAPIやIdleイベントを使うことなく、お手軽に通知領域に表示する常駐アプリが作れたりもする。