正確なタイマーがほしい
プログラムを組んでいると、時々正確なタイマーが欲しくなる。
VB6はほぼシングルタスクなので、APIの正確な時間を取得しても、
正確なタイマーを作ることは難しい。
VB5の頃はマルチメディア・タイマーなるものが有って、タイマーが別スレッドで走っていた、
しかしVB6になってネイテブコードのコンパイルでは、殆どマルチスレッドは無理になってしまった。
ところが、VB.NET、C#になってこのマルチスレッドが使用できるようになった。
ただし使い方に色々制約があり結構面倒である。
所がである、VB2005、C#にはBackgroundWorkerなるものが出来、マルチスレッドが簡単に使えるのだと言う、
これはかなり耳よりな話だ、さっそくこのBackgroundWorkerで今まで欲しかったタイマーを作ってみよう。
概観と仕様
下のイメージがその概観である、チョッと欲張って時計とストップウォッチを入れておこう。
ストップウォッチはLapの機能を付ける。
Lap中はタイマーの表示の更新はしないが、其の横に黄色のドットが1秒毎に点滅する。
コードのサンプルと説明
画面デザイン
ラベルの一つには時間を表示させ、もう一つににはストップウォッチの経過時間を表示させる。
ボタンは、スタート、ラップ、ストップの計3つである。
さて画像を良く見てほしいのだが、時間と経過時間の表示の中に、H、M、S、mSの表示がある、
実はこれらも小さいラベルを貼り付けてある。
時間表示用のラベルと、Lap時に点滅するPictureBoxを置いてある。
それとツールボックスからBackgroundWorker1(backgroundWorker1)を貼り付け
其のプロパティWindowのイベント表示メニューからDoWorkをクリックして、コードに
BackgroundWorker1_DoWork(backgroundWorker1_DoWork)イベントを追加しておこう。
もちろんボタンのイベントも追加しておく。
デリゲートの宣言と共通変数
先ずストップウオッチの状態を表す定数を宣言する。
更にこの定数を保持する、変数「States」を宣言する。
この変数はボタンが押されて、Start、Stop、Lapのいずれかの状態を保持する。
又秒の変化を知るために前の秒を保持する変数「Second」を宣言している。
後は、Labelに書き込むためのデリゲートとドット表示用のPictureBoxを
表示非表示にするデリゲートを宣言する。
BackgroundWorkerの実行は別スレッドで行なわれる為にメインスレッドのLabelの表示を
変えたりPictureBoxの表示を切り替える為にはデリゲートが必要である。
C#のコード
//状態を表す定数
const int STOP = 0;
const int START = 1;
const int LAP = 2;
//上の3つ状態を保持します
int States = 0 ;
//秒を保持します
int Second = 0;
//現在の時刻の表示と、タイマーの表示に使用されるデリゲート
delegate void dlgSetString(object lbl,string text);
//ラップ時のドットの表示非表示に使用されます
delegate void dlgGene();
//ストップウオッチクラス
System.Diagnostics.Stopwatch MyStopWatch = new System.Diagnostics.Stopwatch();
VB2005のコード
'状態を表す定数
Const sSTOP As Integer = 0
Const sSTART As Integer = 1
Const sLAP As Integer = 2
'上の3つ状態を保持します
Dim States As Integer = 0
'秒を保持します
Dim Second As Integer = 0
'現在の時刻の表示と、タイマーの表示に使用されるデリゲート
Delegate Sub dlgSetString(ByVal lbl As Object, ByVal text As String)
'ラップ時のドットの表示非表示に使用されます
Delegate Sub dlgGene()
'ストップウオッチクラス
Dim MyStopWatch As System.Diagnostics.Stopwatch = New System.Diagnostics.Stopwatch()
フォームロード時の処理
フォーム起動時にBacgroundWorkerが立ち上がる様になっている。
RunWorkerAsync()
驚くことに、この一行で第2スレッドが立ち上がり、この中にコードが書けるのである。
このセカンドスレッドの中のコードは、BackgroundWorker1_DoWork
の中にに書かれている。
C#のコード フォームのロード時のコード
//コンストラクタ
public FomStopwatch()
{
InitializeComponent();
//BackgroundWorkerを起動します
backgroundWorker1.RunWorkerAsync();
}
VB2005のコード フォームのロード時のコード
Private Sub ForStopwatch_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
'BackgroundWorkerを起動します()
BackgroundWorker1.RunWorkerAsync()
End Sub
フォームアンロード時の処理
フォームアンロード時は第2スレッドも閉じる必要が有る
WorkerSupportsCancellation = True
CancelAsync()
この2行で第2スレッドを閉じている。
C# フォームアンロード時のコード
//フォームが閉じられるとき発生します
private void FomStopwatch_FormClosing(object sender, FormClosingEventArgs e)
{
//キャンセルを有効にする
backgroundWorker1.WorkerSupportsCancellation = true;
backgroundWorker1.CancelAsync();
backgroundWorker1.Dispose();
}
VB2005のコード フォームアンロード時のコード
Private Sub FomStopwatch_FormClosing(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.FormClosingEventArgs) _
Handles MyBase.FormClosing
'キャンセルを有効にする
BackgroundWorker1.WorkerSupportsCancellation = True
BackgroundWorker1.CancelAsync()
BackgroundWorker1.Dispose()
End Sub
ボタン押下の処理
ボタン押下時はそれぞれ前の状態(States)を見て処理を変えている。
Statesは別スレッドの中からも参照され、其の状態を判断して、何を表示するかを決めている。
注意する点は、LapからStart又はStopに移行した時に、ドットの点滅がタイミングにより残るので
これを非表示にする点と、LapからStopに変わった時に最終の時間を表示する点である。
コードは全体のコードを参照願いたい。
BackgroundWorkerの別スレッドから呼ばれるメソド。
BackgroundWorkerの別スレッドBackgroundWorker1_DoWorkからは2つのメソドが呼ばれる。
1つ目はラベルに経過時間を表示するメソドであり、これは時間表示のラベルの表示用も兼ねている。
C#のコード
private void ShowTime(object sender,string strTime)
VB2005のコード
Private Sub ShowTime(ByVal sender As Object, ByVal strTime As String)
第一引数はobject(Object)にしてあるが、これはLabelでも良くLabelの場合はキャストの必要は無い。
2つ目のメソドはLap表示の為のドットの表示非表示を切り替えるメソドである。
C#のコード BackgroundWorkerの別スレッドから呼ばれるメソド。
//デリゲートで別スレッドから呼ばれてラベルに現在の時間又は
//ストップウオッチの時間を表示する
private void ShowTime(object sender,string strTime)
{
Label lbl = (Label)sender; //objectをLabelにキャストする
lbl.Text = strTime;
}
//Lapの場合一秒ごとに別スレッドからデリゲートに呼ばれ
//ドットの表示非表示を行なう
private void ChangePicColor()
{
picDot.Visible = !picDot.Visible;
}
VB2005のコード BackgroundWorkerの別スレッドから呼ばれるメソド。
'デリゲートで別スレッドから呼ばれてラベルに現在の時間又は
'ストップウオッチの時間を表示する
Private Sub ShowTime(ByVal sender As Object, ByVal strTime As String)
Dim lbl As Label = DirectCast(sender, Label) 'objectをLabelにキャストする
lbl.Text = strTime
End Sub
'Lapの場合一秒ごとに別スレッドからデリゲートに呼ばれ
'ドットの表示非表示を行なう
Private Sub ChangePicColor()
PicDot.Visible = Not PicDot.Visible
End Sub
セカンドスレッドの処理
BackgroundWorkerの別スレッドの中はループになっていて、秒が更新されるとびょの表示を、
ストップウオッチが作動してると経過時間の表示を、又Lap時はドットの点滅を行なう。
ただしこれらの表示はいずれもメインスレッドで行なう為に、デリゲートを使って、メソドを
呼び出している。
C#のコード
this.Invoke(new dlgGene(ChangePicColor), new object[] { });
VB2005のコード
Me.Invoke(New SetLabelTextDelegate(AddressOf SetLabelTime), strTime)
上の方法は匿名デリゲートを呼ばれる方法であり
C#のコード
dlgSetString SetString = new dlgSetString(ShowTime);
this.Invoke(SetString,lblTimer, eTime);
VB2005のコード
Dim SetString As dlgSetString = New dlgSetString(AddressOf ShowTime)
Me.Invoke(SetString, LblTimer, eTime)
という書き方と比較してデリゲートのインスタンスの名前を付けずにデリゲートを呼び出す為に匿名と呼ばれているのだろう。
更に、CPUの使用率を下げる為に下のコードを追加している。
C#のコード
System.Threading.Thread.Sleep(1);
VB2005のコード
System.Threading.Thread.Sleep(1)
セカンドスレッドでの処理のコード
C#のコード
//別スレッド上で行なわれる処理
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
//スレッドが停止状態でなければ
while (!backgroundWorker1.CancellationPending)
{
//1msecのウエイト、CPUの使用率を下げる
System.Threading.Thread.Sleep(1);
try
{
//秒が変更されたら
if (Second != DateTime.Now.Second)
{
//時間を文字列に直す
string strTime = stringFormat("{0:00}", DateTime.Now.Hour)
+ ":" + stringFormat("{0:00}", DateTime.Now.Minute)
+ ":" + stringFormat("{0:00}", DateTime.Now.Second) + ":"
+ stringFormat("{0:000}", DateTime.Now.Millisecond);
//匿名デリゲートで現在の時間をラベルに表示する
this.Invoke(new dlgSetString(ShowTime), new object[]
{ lblTime, strTime });
if (States == LAP)
{
//匿名デリゲートでドットの表示非表示情報を渡す
this.Invoke(new dlgGene(ChangePicColor),
new object[] { });
}
//秒の保持
Second = DateTime.Now.Second;
}
//ストップウオッチが動作していたら時間を表示する
if (States == START)
{
//ストッップウオッチの時間を文字に直す
string eTime = string.Format("{00:00:00:00:000}",
MyStopWatch.ElapsedMilliseconds);
//匿名デリゲートで時間を渡す
this.Invoke(new dlgSetString(ShowTime),
new object[] { lblTimer, eTime });
//dlgSetString SetString = new dlgSetString(ShowTime);
//this.Invoke(SetString,lblTimer, eTime);
}
}
catch
{
//このエラーはフォームを閉じたときに発生します
}
}
}
VB2005のコード
'別スレッド上で行なわれる処理
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) _
Handles BackgroundWorker1.DoWork
'スレッドが停止状態でなければ
While (Not BackgroundWorker1.CancellationPending)
'1msecのウエイト、CPUの使用率を下げる
System.Threading.Thread.Sleep(1)
Try
If Second <> DateTime.Now.Second Then
'時間を文字列に直す
Dim strTime As String = String.Format("{0:00}", DateTime.Now.Hour) _
+ ":" + String.Format("{0:00}", DateTime.Now.Minute) _
+ ":" + String.Format("{0:00}", DateTime.Now.Second) + ":" _
+ String.Format("{0:000}", DateTime.Now.Millisecond)
'匿名デリゲートで現在の時間をラベルに表示する
Me.Invoke(New dlgSetString(AddressOf ShowTime), _
New Object() {LblTime, strTime})
If States = sLAP Then
'匿名デリゲートでドットの表示非表示情報を渡す
Me.Invoke(New dlgGene(AddressOf ChangePicColor), _
New Object() {})
End If
'秒の保持
Second = DateTime.Now.Second
End If
'ストップウオッチが動作していたら時間を表示する
If States = sSTART Then
'ストッップウオッチの時間を文字に直す
Dim eTime As String = String.Format("{00:00:00:00:000}", _
MyStopWatch.ElapsedMilliseconds)
'匿名デリゲートで時間を渡す
Me.Invoke(New dlgSetString(AddressOf ShowTime), _
New Object() {LblTimer, eTime})
'Dim SetString As dlgSetString = New dlgSetString(AddressOf ShowTime)
'Me.Invoke(SetString, LblTimer, eTime)
End If
Catch
'このエラーはフォームを閉じたときに発生します
End Try
End While
End Sub
|