2006年04月02日

マルチメディアタイマ

某掲示板に触発されて、ついマルチメディアタイマを使ったタイマクラスを書いてしまいました。

タイマが独自スレッドで動くことから、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
posted by Hongliang at 00:01| Comment(0) | TrackBack(1) | .NET | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック

S1 X2 Simulator/Csharp
Excerpt: S1 X2 SimulatorでC#を使う。 目次 Visual StudioをSubversionリポジトリと連携する ImageList Timer System.T..
Weblog: Appli Wiki (PukiWiki/TrackBack 0.3)
Tracked: 2008-04-25 17:24

ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。

ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。

あ、こんなのに使ったってコメントを頂ければ嬉しいです。

×

この広告は1年以上新しい記事の投稿がないブログに表示されております。