仕様を策定する人って絶対暇人ですよね。
前回の続きと言うことで、ITask に関するいくつかの実装を見ていきましょう。
まずは TaskScheduler クラスと同じく、Task クラスの基礎実装を。まあ ComInterfaceWrapper を継承して、内部で使用するインターフェイスを private プロパティにするだけですから同じですけど。ただ、コンストラクタは構成からして外部に公開する必要はないので、internal/Friend としておきます。ITask は単独で作成できますが、それは認めない方向で。
// C# namespace HongliangSoft.Utilities.TaskSchedulers { public sealed class Task : ComInterfaceWrapper { private ITask ITask { get { return (ITask)base.Instance; } } internal Task(ITask iTask) : base(iTask) { } } } ' VB.NET Namespace HongliangSoft.Utilities.TaskSchedulers Public NotInheritable Class Task Inherits ComInterfaceWrapper Private ReadOnly Property ITask() As ITask Get Return DirectCast(MyBase.Instance, ITask) End Get End Property Friend Sub New(ByVal iTask As ITask) MyBase.New(iTask) End Sub End Class End Namespace
あとはこれにメソッド・プロパティを作っていくだけですが、特にプロパティの方はほとんどが単純なものなので、そのままインターフェイスのメソッドを公開するのはちょっと……というのだけ書きましょう。
まず、GetMaxRunTime / SetMaxRunTime 。引数が Int32 です。実に分かりにくい。単位は分なのか秒なのか、そもそも回数なのではないか? 正解はミリ秒。このパラメータの意味は「実行される時間」ですから、.NET では TimeSpan 構造体で扱うべきだろうってことで、プロパティの型は TimeSpan と言うことにして、内部で変換することにしましょう。問題は 0xFFFFFFFF の時だけは無効であるという点ですが、これは負の TimeSpan 値ということにします。.NET 2.0 限定なら nullable も良いんですけどねー。
// C# public TimeSpan MaxRunTime { get { const int infini = -1; int time; base.CheckHR(this.ITask.GetMaxRunTime(out time)); if (time == infini) return new TimeSpan(infini); uint milli = unchecked((uint)time); return TimeSpan.FromMilliseconds(milli); } set { const int infini = -1; if (value < TimeSpan.Zero) base.CheckHR(this.ITask.SetMaxRunTime(infini)); else { long time = (long)value.TotalMilliseconds; if (time > 0xffffffffL) { throw new ArgumentOutOfRangeException( "value", value, "4294967295 ミリ秒(およそ 49 日 17 時間 2 分 47 秒) " + "以上に設定することはできません。"); } base.CheckHR(this.ITask.SetMaxRunTime(unchecked((int)time))); } } } ' VB.NET Public Property MaxTunTime() As TimeSpan Get Const inifini As Integer = -1 Dim time As Integer MyBase.CheckHR(Me.ITask.GetMaxRunTime(time)) If time = inifini Then Return New TimeSpan(-1) End If Dim milli As Long = time If time < 0 Then milli += &H100000000L End If Return TimeSpan.FromMilliseconds(milli) End Get Set Const infini As Integer = -1 If value.Ticks < 0 Then MyBase.CheckHR(Me.ITask.SetMaxRunTime(infini)) Else Dim time As Long = CLng(Math.Floor(value.TotalMilliseconds)) If time > &HffffffffL Then Throw New ArgumentOutOfRangeException( _ "value", value, _ "4294967295 ミリ秒(およそ 49 日 17 時間 2 分 47 秒) " _ & "以上に設定することはできません。") End If If time >= &H80000000L Then time -= &H100000000L End If MyBase.CheckHR(Me.ITask.SetMaxRunTime(CInt(time)) End If End Set End Property
unchecked 構文がない VB.NET ではやはりどうしてもこの辺記述が面倒ですね。
あとは、GetIdleWait/SetIdleWait を紹介しましょう。このメソッドはそれぞれ二つずつ引数を取って、アイドル時間を取得・設定します。そのまま実装するならメソッド、しかも Get の方は最低一つは out(ByRef) を使うことになるでしょうが、こういう場合この二つをまとめて構造体にし、それをやりとりするプロパティと言うことにすればすっきりします。なお、全体に言えることですが、全体にタスクスケジューラ周りの COM インターフェイスは 16bit の値を要求することが多いです。では Int16 でいいかというと、Int16 では 32767 までしか扱えないため表現力不足。かといって UInt16 は CLS 非準拠ですし、VB7/7.1 からは非常に扱いにくくなります。ですから外に見せる値は大抵 Int32 で公開しています。値を取得・設定するのに分数を使うのと TimeSpan を使うのは、今回はどちらもありと思ったので両方プロパティで実装。構造体全部を書くのも面倒なので、TimeSpan と分数との変換を行うプロパティの実装を一つ書いてみましょう。
// C# public struct IdleWait { private int idleMinutes; private int deadlineMinutes; public TimeSpan IdleTime { get { return TimeSpan.FromMinutes(this.idleMinutes); } set { long minutes = (long)value.TotalMinutes; if (minutes < 0 || minutes > 65535) { throw new ArgumentOutOfRangeException( "value", value, "マイナスの値、または 65535 分間を表す値よりも" + 大きい値にすることはできません。"); } this.idleMinutes = (int)minutes; } } /* 略 */ } ' VB.NET Public Structure IdleWait Private _idleMinutes As Integer Private _deadlineMinutes As Integer Public Property TimeSpan() As IdleTime Get Return TimeSpan.FromMinutes(Me._idleMinutes) End Get Set Dim minutes As Long = CLng(Math.Floor(value.TotalMinutes)) If (minutes < 0) OrElse (minutes > 65535) Then Throw New ArgumentOutOfRangeException( _ "value", value, _ "マイナスの値、または 65535 分間を表す値よりも" _ & 大きい値にすることはできません。") End If Me._idleMinutes = CInt(minutes) End Set End Property ' 略 End Structure
前述の通りこの構造体は分数での値そのまま出しているので、IdleWait プロパティは比較的単純な実装なんで省略。まあやっぱり VB.NET では面倒なところがありますが。
メソッドも二つ紹介しましょう。まず CreateTrigger です。前々回述べたように、ITask.CreateTrigger は作成した ITaskTrigger を返し、そして ITaskTrigger.SetTrigger にてTASK_TIGGER 構造体をセットすることでトリガをセットしました。ちなみにトリガを変更した後 IPersistFile.Save メソッドを使用しないと保存されません。保存は必ずしもトリガを設定したときにするとは限らないので取り入れず、Task.CreateTrigger では ITask.CreateTrigger と ITaskTrigger.SetTrigger の二つをまとめてこなすメソッドにすることにしましょう。
// C# public int CreateTrigger(TaskTrigger trigger) { short triggerIndex; ITaskTrigger iTrigger; base.CheckHR(this.ITask.CreateTrigger(out triggerIndex, out iTrigger)); try { base.CheckHR(iTrigger.SetTrigger(trigger)); } finally { if (iTrigger != null) Marshal.ReleaseComObject(iTrigger); } return triggerIndex; } Public Function CreateTrigger(ByVal trigger As TaskTrigger) As Integer Dim triggerIndex As Short Dim iTrigger As ITaskTrigger MyBase.CheckHR(Me.ITask.CreateTrigger(triggerIndex, iTrigger)) Try MyBase.CheckHR(iTrigger.SetTrigger(trigger)) Finally If Not(iTrigger Is Nothing) Then Marshal.ReleaseComObject(iTrigger) End If End Try Return triggerIndex End Function
TaskTrigger は次回解説する予定です。さて、特徴らしい特徴と言えば Marshal.ReleaseComObject くらいでしょうか? 返値の Int32 は追加されたトリガのインデックスですが、これがまた使いづらい値でして。途中のトリガを Delete するとその後のそれぞれのトリガのインデックスが自動的に一つずれるんで、保存しとくわけにはいきません。
もう一つ、GetRunTimes の方は、トリガの起動予定日時の配列を返すメソッドです。ITask.GetRuntimes は返値を受ける分を除いて三つパラメータを要求します。取得の先頭日時、取得の最終日時、最大取得数です。しかし毎回これらを指定するのは面倒ですし、オーバーロードで引数を手抜きできるようにしましょう。基本は「今後どういうスケジュールなのか」でしょうから、できる限り先頭日時を省略するオーバーロードが基本でしょうか。また、ITask.GetRunTimes は SYSTEM_TIME(SystemTimeClass)の配列を返してきますが、これは DateTime に変換しといた方が直感的でしょう。
// C# // 一度に取得できる要素数の上限 public static readonly int RunTimesMax = 1440; // 現在から将来にわたって、最大取得数を指定して予定を取得する public DateTime[] GetRunTimes(int maxCount) { return this.GetRunTimes(DateTime.Now, DateTime.MaxValue, maxCount); } // 現在から将来にわたって、指定日時までの予定を取得する public DateTime[] GetRunTimes(DateTime endTime) { return this.GetRunTimes(DateTime.Now, endTime); } // 指定した日時から日時までの間の予定を取得する public DateTime[] GetRunTimes(DateTime beginTime, DateTime endTime) { return this.GetRunTimes(beginTime, endTime, Task.RunTimesMax); } // 全部の値を指定して予定を取得する public DateTime[] GetRunTimes(DateTime beginTime, DateTime endTime, int maxCount) { if (maxCount < 1 || maxCount > Task.RunTimesMax) { throw new ArgumentOutOfRangeException( "maxCount", maxCount, "0 以下、または Task.RunTimesMax " + より大きい値を指定することはできません。"); } SystemTimeClass begin = new SystemTimeClass(beginTime); SystemTimeClass end = new SystemTimeClass(endTime); short count = (short)maxCount; IntPtr runTimes; HResult result = this.ITask.GetRunTimes(begin, end, ref count, out runTimes); if (result != HResult.OK && result != HResult.False) { // 致命的なエラーをチェック。 CheckHR(result); // 致命的ではない場合(トリガを持ってないとか) return new DateTime[0]; } #if !V10 && !V11 List<DateTime> dates = new List<DateTime>(); #else ArrayList dates = new ArrayList(); #endif int size = Marshal.SizeOf(typeof(SystemTimeClass)); using (CoTaskMem array = runTimes) { for (int i = 0; i < count; i++) { SystemTimeClass runs; #if !V10 && !V11 runs = array.ReadStructure<SystemTimeClass>(size * i); #else runs = mem.ReadStructure(typeof(SystemTimeClass), size * i) as SystemTimeClass; #endif if (! runs.IsInvalid) { dates.Add(runs.ToDateTime()); } } } #if !V10 && !V11 return dates.ToArray(); #else return (DateTime[])dates.ToArray(typeof(DateTime)); #endif } ' VB.NET ' 一度に取得できる要素数の上限 Public Shared ReadOnly RunTimesMax As Integer = 1440 ' 現在から将来にわたって、最大取得数を指定して予定を取得する Public Function GetRunTimes(ByVal maxCount As Integer) As DateTime() Return Me.GetRunTimes(DateTime.Now, DateTime.MaxValue, maxCount) End Function ' 現在から将来にわたって、指定日時までの予定を取得する Public Function GetRunTimes(ByVal endTime As DateTime) As DateTime() Return Me.GetRunTimes(DateTime.Now, endTime) End Function ' 指定した日時から日時までの間の予定を取得する Public Function GetRunTimes(ByVal beginTime As DateTime, ByVal endTime As DateTime) As DateTime() Return Me.GetRunTimes(beginTime, endTime, Task.RunTimesMax) End Function ' 全部の値を指定して予定を取得する Public Function GetRunTimes(ByVal beginTime As DateTime, ByVal endTime As DateTime, ByVal maxCount As Integer) As DateTime() If maxCount < 1 OrElse maxCount > Task.RunTimesMax Then Throw New ArgumentOutOfRangeException( _ "maxCount", maxCount, _ "0 以下、または Task.RunTimesMax " _ & "より大きい値を指定することはできません。") End If Dim begin As New SystemTimeClass(beginTime) Dim _end As New SystemTimeClass(endTime) Dim count As Short = CShort(maxCount) Dim runTimes As IntPtr Dim result As HResult = Me.ITask.GetRunTimes(begin, _end, count, runTimes) If Not(result = HResult.OK OrElse result = HResult.False) Then ' 致命的なエラーをチェック。 CheckHR(result) ' 致命的ではない場合(トリガを持ってないとか) Return New DateTime(0){} End If Dim size As Integer = Marshal.SizeOf(GetType(SystemTimeClass)) #If Not(V = 10 OrElse V = 11) Then Dim dates As New List(Of DateTime) Using _array As New CoTaskMem(runTimes) { For i As Integer = 0 To count -1 Dim runs As SystemTimeClass runs = _array.ReadStructure(Of SystemTimeClass)(size * i) If Not(runs.IsInvalid) Then dates.Add(runs.ToDateTime()) End If Next End Using Return dates.ToArray() #Else Dim dates As New ArrayList() Dim i As Integer Dim _array As CoTaskMem Try _array = New CoTaskMem(runTimes) For i = 0 To count - 1 Dim runs As SystemTimeClass runs = DirectCast(_array.ReadStructure(GetType(SystemTimeClass), _ size * i), _ SystemTimeClass) If Not(runs.IsInvalid) Then dates.Add(runs.ToDateTime()) End If Next Finally _array.Free() End Try Return DirectCast(dates.ToArray(GetType(DateTime)), DateTime()) #End If End Function
次回は TASK_TRIGGER とそのラッパ TaskTrigger ですが……だらだら解説を続けるのもそろそろやめて、どうラップしたかを簡単に解説するだけにしましょう。