某掲示板に触発されて、ついマルチメディアタイマを使ったタイマクラスを書いてしまいました。
タイマが独自スレッドで動くことから、System.Timers.Timer とほぼ同じ構成にしています。私自身は VS を滅多に使わないから割と無駄っぽいですが、既定のプロパティやイベントを設定してみたり、コンポーネントを D&D した時点で SynchronizingObject が自動的に設定されるようにしてみたり、とちょっとしたコンポーネント作成のお勉強と言った雰囲気。
ドキュメントは WinXP 対象に書いてますが、9x 系ではマルチメディアタイマの挙動が微妙に違うらしいので要注意です。
今回は、コンポーネントをデザイナ画面に D&D したときに自動的にプロパティを設定するにはどう実装するかを考察してみましょう。
追記(06/04/02)この MultimediaTimer クラスは、速攻で改訂版が出ました。こっちのは残していますが使うべきではありません。
といっても率直に言ってなにをどうすればいいのか全く糸口が分からなかったので、まずは既に実装している System.Timers.Timer の観察です。クラス自体は Component から継承、ISupportInitialize を実装。カスタム属性は DefaultProperty と DefaultEvent ぐらいのようです。それから、SynchronizingObject プロパティのカスタム属性は DefaultValue と Browsable、TimersDescription 属性くらい。プロパティの方のカスタム属性に見るところはなさそうです。とすると怪しいのは ISupportInitialize の実装メソッドである BeginInit/EndInit ですが……これはドキュメントの説明や InitializeComponent メソッドの使われ方を見るに、どうも ListView における BeginUpdate/EndUpdate みたいなメソッドみたいですね。そうするとこれもはずれです。
いきなり行き止まりです。こうなったら困ったときの ILDASM でどうやってるのか調べてみることにしましょう。クラス定義、SynchronizingObject プロパティの定義に見るべきは無し。set_SynchronizingObject は単純にフィールドにセットしてるだけ。get_SynchronizingObject は……おや、なにやらごちゃごちゃやってますよ。
掻い摘んで言うと、デザインモードの時は Component.GetService で IDesignerHost を取得。IDesignerHost の RootComponent プロパティを ISynchronizeInvoke として SynchronizingObject に入れる、と言った動作です。なるほど。
この GetService やら IDesignerHost やらをキーワードに調べると、カスタムデザイナに行き着きます。複数の値を処理する必要があったり複雑な処理を行う場合はこちらを実装することが要求されるんでしょうね、やっぱり。
これだけ短いと創意工夫もあったもんじゃありませんが、せいぜい as キャスト(VB.NET だと 2005 で追加された TryCast ですね)を使って短く効率的なコードを書きましょう。
それから、System.Timers.Timer を真似るなら、Enabled プロパティが必要ですね。実際の動作時は普通に Start/Stop を呼び出せば済む話ですが、デザイン時にまでタイマが動いちゃうのは困ってしまいます。クラスの設計上、タイマを開始する Win32API 関数である timeSetEvent が返すタイマ ID が設定されているかどうかで現在実行中かどうかを判断させているため、ここは簡単に「デザイン時には仮の ID を与えておく」という手段で行きましょう。デザイン画面で Enabled を true にしたら、プロパティグリッドでは ID がセットされているので Enabled == true と判断されますし、実際に動作するときはコンストラクタで Enabled が true に設定されるため自動的に Start() が呼ばれるようになります。
注意事項が多いので、ご使用はドキュメントをしっかり読んでどうぞ。そのままだと XML なので読みづらいですけど。
なお、今回の VB.NET のコードは、ISynchronizeObject を使う関係上、VB2005 オンリーです。カスタムイベントを使えない VB7 で Invoke させるイベントハンドラを取得する方法が思いつきませんでした。
// C# using System; using System.ComponentModel; using System.Runtime.InteropServices; using System.Diagnostics; using System.ComponentModel.Design; namespace HongliangSoft.Utilities { [DefaultProperty("Interval"), DefaultEvent("Elapsed")] public class MultimediaTimer : Component { public MultimediaTimer() { this.InitializeComponent(); } private void InitializeComponent() { this.callback = GCHandle.Alloc(new TickCallback(OnTicked)); } [DefaultValue(1000), Description("タイマの間隔。"), Category("Behavior")] public int Interval { get { return this.interval; } set { if (value <= 0 || value > MultimediaTimer.MaximumInterval) { throw new ArgumentOutOfRangeException( "value", value, "0 以下、または MultimediaTimer.MaximumInterval より" + "大きい値にすることはできません。"); } if (this.Enabled) { this.Stop(); this.interval = value; this.Start(); } else { this.interval = value; } } } [DefaultValue(true), Category("Behavior"), Description("Elapsed イベントを繰り返し発生させる場合は true。")] public bool AutoReset { get { return this.autoReset; } set { if (this.autoReset != value) { if (this.Enabled) { this.Stop(); this.autoReset = value; this.Start(); } else { this.autoReset = value; } } } } [DefaultValue(null), Category("Behavior"), Description("同期処理を行うオブジェクト")] public ISynchronizeInvoke SynchronizingObject { get { if (this.syncObj == null && this.DesignMode) { IDesignerHost host = this.GetService(typeof(IDesignerHost)) as IDesignerHost; if (host != null) this.syncObj = host.RootComponent as ISynchronizeInvoke; } return this.syncObj; } set { if (this.syncObj != value) { this.invokeArgs = (value == null) ? null : new object[] { this, EventArgs.Empty }; this.syncObj = value; } } } [DefaultValue(false), Category("Behavior"), Description("タイマを実行するかどうかを示します。"), ] public bool Enabled { get { return this.timerId != 0; } set { if (this.DesignMode) { this.timerId = value ? -2 : 0; return; } if (this.Enabled) { if (!value) this.Stop(); } else if (value) this.Start(); } } public static int MaximumInterval { get { TimeCaps caps; GetDeviceCaps(out caps, 8); return caps.Max; } } [Description("指定した間隔が経過すると発生します。"), Category("Behavior")] public event EventHandler Elapsed { add { this.Events.AddHandler(EventElapsed, value); } remove { this.Events.RemoveHandler(EventElapsed, value); } } public void Start() { if (this.DesignMode) return; if (!this.callback.IsAllocated) { throw new ObjectDisposedException( "MultimediaTimer", "破棄されたオブジェクトにアクセスできません。"); } if (this.Enabled) return; this.timerId = SetTimeEvent( this.interval, 0, (TickCallback)this.callback.Target, IntPtr.Zero, this.autoReset ? TimerEventTypes.Periodic : TimerEventTypes.OneShot); if (this.timerId == 0) { throw new InvalidOperationException( "タイマの初期化に失敗しました。"); } } public void Stop() { if (this.DesignMode) { this.timerId = 0; return; } if (!this.Enabled) return; KillTimeEvent(this.timerId); this.timerId = 0; } private void OnTicked(int id, int uiNo, IntPtr user, IntPtr reserved1, IntPtr reserved2) { if (id != this.timerId) return; if (!this.autoReset) { this.timerId = 0; } EventHandler handler = this.Events[EventElapsed] as EventHandler; if (handler != null) { if (this.syncObj != null && this.syncObj.InvokeRequired) this.syncObj.Invoke(handler, this.invokeArgs); else handler(this, EventArgs.Empty); } } protected override void Dispose(bool disposing) { this.Stop(); if (disposing) { if (this.callback.IsAllocated) this.callback.Free(); this.syncObj = null; this.invokeArgs = null; } base.Dispose(disposing); } private static readonly object EventElapsed = new object(); private int interval = 1000; private bool autoReset = true; private int timerId; private GCHandle callback; private ISynchronizeInvoke syncObj; private object[] invokeArgs; private delegate void TickCallback(int id, int uiNo, IntPtr user, IntPtr reserved1, IntPtr reserved2); [DllImport("winmm.dll", EntryPoint = "timeGetDevCaps")] private static extern int GetDeviceCaps(out TimeCaps ptc, int cbtc); [DllImport("winmm.dll", EntryPoint = "timeSetEvent")] private static extern int SetTimeEvent( int delay, int resolution, TickCallback ticked, IntPtr user, TimerEventTypes type); [DllImport("winmm.dll", EntryPoint = "timeKillEvent")] private static extern int KillTimeEvent(int id); [Flags] private enum TimerEventTypes : int { OneShot = 0x00, Periodic = 0x01, } private struct TimeCaps { public int Min; public int Max; } } } ' VB 2005 Option Strict On Imports System Imports System.ComponentModel Imports System.Runtime.InteropServices Imports System.Diagnostics Imports System.ComponentModel.Design Namespace HongliangSoft.Utilities <DefaultProperty("Interval"), DefaultEvent("Elapsed")> _ Public Class MultimediaTimer Inherits Component Public Sub New() Me.InitializeComponent() End Sub Private Sub InitializeComponent() Me.m_callback = GCHandle.Alloc(New TickCallback(AddressOf OnTicked)) End Sub <DefaultValue(1000), Description("タイマの間隔。")> _ <Category("Behavior")> _ Public Property Interval() As Integer Get Return Me.m_interval End Get Set If value <= 0 _ OrElse value > MultimediaTimer.MaximumInterval Then Throw New ArgumentOutOfRangeException( _ "value", value, _ "0 以下、または MultimediaTimer.MaximumInterval より" _ & "大きい値にすることはできません。") End If If Me.Enabled Then Me.Stop() Me.m_interval = value Me.Start() Else Me.m_interval = value End If End Set End Property <DefaultValue(True), Category("Behavior")> _ <Description("Elapsed イベントを繰り返し発生させる場合は true。")> _ Public Property AutoReset() As Boolean Get Return Me.m_autoReset End Get Set If Not(Me.m_autoReset = value) Then If Me.Enabled Then Me.Stop() Me.m_autoReset = value Me.Start() Else Me.m_autoReset = value End If End If End Set End Property <DefaultValue(CType(Nothing, Object)), Category("Behavior")> _ <Description("同期処理を行うオブジェクト")> _ Public Property SynchronizingObject() As ISynchronizeInvoke Get If Me.m_syncObj Is Nothing AndAlso Me.DesignMode Then Dim host As IDesignerHost _ = DirectCast(Me.GetService(GetType(IDesignerHost)), _ IDesignerHost) If host IsNot Nothing _ AndAlso TypeOf host.RootComponent _ Is ISynchronizeInvoke Then Me.m_syncObj = DirectCast(host.RootComponent, _ ISynchronizeInvoke) End If End If Return Me.m_syncObj End Get Set If Me.m_syncObj IsNot value Then If value Is Nothing Then Me.m_invokeArgs = Nothing Else Me.m_invokeArgs = New Object() { Me, EventArgs.Empty } End If Me.m_syncObj = value End If End Set End Property <DefaultValue(False), Category("Behavior")> _ <Description("タイマを実行するかどうかを示します。")> _ Public Property Enabled() As Boolean Get Return Not(Me.m_timerId = 0) End Get Set If Me.DesignMode Then If value Then Me.m_timerId = -2 Else Me.m_timerId = 0 End If Else If Me.Enabled Then If Not(value) Then Me.Stop() End If Else If value Then Me.Start() End If End If End Set End Property Public Shared ReadOnly Property MaximumInterval() As Integer Get Dim caps As TimeCaps GetDeviceCaps(caps, 8) Return caps.Max End Get End Property <Description("指定した間隔が経過すると発生します。")> _ <Category("Behavior")> _ Public Custom Event Elapsed As EventHandler AddHandler(ByVal value As EventHandler) Me.Events.AddHandler(EventElapsed, value) End AddHandler RemoveHandler(ByVal value As EventHandler) Me.Events.RemoveHandler(EventElapsed, value) End RemoveHandler RaiseEvent(ByVal sender As Object, ByVal e As EventArgs) Dim handler As EventHandler _ = TryCast(Me.Events(EventElapsed), _ EventHandler) If handler IsNot Nothing Then If Me.m_syncObj IsNot Nothing _ AndAlso Me.m_syncObj.InvokeRequired Then Me.m_syncObj.Invoke(handler, Me.m_invokeArgs) Else handler(sender, e) End If End If End RaiseEvent End Event Public Sub Start() If Me.DesignMode Then Return End If If Not(Me.m_callback.IsAllocated) Then Throw New ObjectDisposedException( _ "MultimediaTimer", _ "破棄されたオブジェクトにアクセスできません。") End If If Me.Enabled Then Return End If If Me.m_autoReset Then Me.m_timerId = SetTimeEvent( _ Me.m_interval, 0, _ DirectCast(Me.m_callback.Target, TickCallback), _ IntPtr.Zero, TimerEventTypes.Periodic) Else Me.m_timerId = SetTimeEvent( _ Me.m_interval, 0, _ DirectCast(Me.m_callback.Target, TickCallback), _ IntPtr.Zero, TimerEventTypes.OneShot) End If If Me.m_timerId = 0 Then Throw New InvalidOperationException( _ "タイマの初期化に失敗しました。") End If End Sub Public Sub [Stop]() If Me.DesignMode Then Me.m_timerId = 0 Return End If If Not(Me.Enabled) Then Return End If KillTimeEvent(Me.m_timerId) Me.m_timerId = 0 End Sub Private Sub OnTicked(ByVal id As Integer, ByVal uiNo As Integer, _ ByVal user As IntPtr, ByVal reserved1 As IntPtr, _ ByVal reserved2 As IntPtr) If Not(id = Me.m_timerId) Then Return End If If Not(Me.m_autoReset) Then Me.m_timerId = 0 End If RaiseEvent Elapsed(Me, EventArgs.Empty) End Sub Protected Overrides Sub Dispose(ByVal disposing As Boolean) Me.Stop() If disposing Then If Me.m_callback.IsAllocated Then Me.m_callback.Free() Me.m_syncObj = Nothing Me.m_invokeArgs = Nothing End If End If MyBase.Dispose(disposing) End Sub Private Shared ReadOnly EventElapsed As New Object() Private m_interval As Integer = 1000 Private m_autoReset As Boolean = True Private m_timerId As Integer Private m_callback As GCHandle Private m_syncObj As ISynchronizeInvoke Private m_invokeArgs As Object() Private Delegate Sub TickCallback( _ ByVal id As Integer, ByVal uiNo As Integer, ByVal user As IntPtr, _ ByVal reserved1 As IntPtr, ByVal reserved2 As IntPtr) <DllImport("winmm.dll", EntryPoint:="timeGetDevCaps")> _ Private Shared Function GetDeviceCaps( _ ByRef ptc As TimeCaps, ByVal cbtc As Integer) As Integer End Function <DllImport("winmm.dll", EntryPoint:="timeSetEvent")> _ Private Shared Function SetTimeEvent( _ ByVal delay As Integer, ByVal resolution As Integer, _ ByVal ticked As TickCallback, ByVal user As IntPtr, _ ByVal [type] As TimerEventTypes) As Integer End Function <DllImport("winmm.dll", EntryPoint:="timeKillEvent")> _ Private Shared Function KillTimeEvent(ByVal id As Integer) As Integer End Function <Flags> Private Enum TimerEventTypes As Integer OneShot = 0 Periodic = 1 End Enum Private Structure TimeCaps Public Min As Integer Public Max As Integer End Structure End Class End Namespace