---
tags: 視窗程式設計
---
# 碼表功能
利用計時器我們製作簡單的時鐘與鬧鐘功能,接下來我們讓程式完整一些,增加幾種的操作功能。
## 第一步:進行介面基本設計
我們選擇另一個頁籤,然後把頁籤名稱改成「碼表」,並且加入以下畫面的控制項。**特別提醒同學,碼表時間紀錄使用的是「ListBox」。**

也請記得,將每一個控制項設定一個名稱,例如以下的範例名稱:
:::info
1. 碼表時間文字顯示:txtStopWatch
2. 碼表時間紀錄:listStopWatchLog
3. 開始按鍵:btnStart
4. 暫停按鍵:btnPause
5. 歸零按鍵:btnReset
6. 紀錄按鍵:btnLog
7. 停止並歸零按鍵:btnStop
:::
### 計時器元件
同樣的我們需要加入「**計時器 Timer**」,「Timer」控制項也是放在工具箱中,主要是在「元件」的清單中。置放的方式很簡單,直接拖到視窗即可。
:::info
1. 碼表計時器:timerStopWatch
:::
請在屬性視窗中設定它的名稱,再將「Interval」設定為數字 1,這個是指每一毫秒執行一次。如果是設定 100,代表每一秒執行一次。

:::success
之前的時鐘功能就是設定100,你可以回去確認一下。
:::
計時器也有事件綁定,請選擇他們的「Tick」事件,點兩下,在程式碼裡面做好綁定。

## 第二部分:程式碼撰寫
完成基本介面設計後,我們就要把程式寫進來,讓程式可以運作。
碼表其實可以利用「**系統診斷**」函式庫的「**Stopwatch(碼表)**」物件,一些程式語言會有內建用來計算程式效率的程式庫,幫助程式設計師來計算程式執行的效率,例如碼表物件就能計算程式的執行時間,所以我們利用碼表物件來實作碼表功能。
```csharp
using System.Diagnostics; // 引用「系統診斷」的函式庫
```
請將以下的程式內容放進你的程式碼之中。請思考看看這些程式應該放在甚麼地方
```csharp=
List<string> StopWatchLog = new List<string>(); // 碼表紀錄清單
Stopwatch sw = new Stopwatch(); // 宣告一個碼表物件
// timerStopWatch_tick:每毫秒執行一次,所以更新的速度會比較快
private void timerStopWatch_Tick(object sender, EventArgs e)
{
txtStopWatch.Text = sw.Elapsed.ToString("hh':'mm':'ss':'fff"); // 顯示碼表時間
}
```
這裡的程式主要是設定一個「**StopWatchLog 清單變數**」,會用來記錄碼表的紀錄。同樣的,我們要讓時間顯示可以自動更新,所以也宣告了一個「**timerStopWatch_tick 倒數計時**」計時器。
並且每次更新的事件處理(timerStopWatch_tick),很單純就是顯示碼表物件的時間,並且我們顯示的時間到達毫秒的精準度。
```csharp
sw.Elapsed.ToString("hh':'mm':'ss':'fff");
// hh':'mm':'ss':'fff 會顯示的時間格式大致是:01:30:23:203
```
### 事件綁定
以下則是每一個按鍵的事件綁定程式,也請同學做好事件綁定後,將程式加入。
```csharp=
// 啟動碼表
private void btnStart_Click(object sender, EventArgs e)
{
sw.Start(); // 啟動碼表
timerStopWatch.Start(); // 開始讓碼表文字顯示
}
// 停止並歸零碼表
private void btnStop_Click(object sender, EventArgs e)
{
sw.Reset(); // 停止並歸零碼表
timerStopWatch.Stop(); // 停止讓碼表文字顯示
txtStopWatch.Text = "00:00:00:000"; // 讓碼表文字「歸零」
listStopWatchLog.Items.Clear(); // 清空 ListBox 中的元素
StopWatchLog.Clear(); // 清除暫存碼表紀錄清單
}
// 歸零按鍵會判斷你是否先按下暫停?來決定是否記錄碼表時間
private void btnReset_Click(object sender, EventArgs e)
{
// 如果碼表還在跑,就紀錄目前的時間,最後歸零再啟動碼錶
if (sw.IsRunning)
{
logRecord();
sw.Restart(); // 歸零碼表,碼表仍繼續進行
}
else
{
sw.Reset(); // 如果碼表沒在跑,停止並歸零碼表
txtStopWatch.Text = "00:00:00:000"; // 讓碼表文字「歸零」
}
}
// 停止碼表
private void btnPause_Click(object sender, EventArgs e)
{
sw.Stop(); // 停止碼表,但不歸零
timerStopWatch.Stop(); // 停止讓碼表文字顯示
}
// 碼表時間紀錄
private void btnLog_Click(object sender, EventArgs e)
{
logRecord();
}
// 碼表時間紀錄
private void logRecord()
{
listStopWatchLog.Items.Clear(); // 清空 ListBox 中的元素
StopWatchLog.Add(txtStopWatch.Text); // 將碼表時間增加到暫存碼表紀錄清單裡
// 依照碼表紀錄清單「依照最新時間順序」顯示
int i = StopWatchLog.Count;
while (i > 0)
{
listStopWatchLog.Items.Add(String.Format("第 {0} 筆紀錄:{1}", i.ToString(), StopWatchLog[i - 1] + "\n"));
i--;
}
}
```
### 碼表物件
首先說明碼表物件的使用,大致上分為啟動(Start)、停止但不歸零(Stop)、停止並歸零(Reset)、歸零重新啟動(Restart)。
```csharp
Stopwatch sw = new Stopwatch(); // 宣告碼表物件
sw.Start(); // 啟動
sw.Stop(); // 停止但不歸零
sw.Reset(); // 停止並歸零
sw.Restart(); // 歸零重新啟動
```
因此五個按鍵的事件綁定處理,都是圍繞在碼表物件的使用。開始按鍵的程式如下:
### 開始按鍵
```csharp=
private void btnStart_Click(object sender, EventArgs e)
{
sw.Start(); // 啟動碼表
timerStopWatch.Start(); // 開始讓碼表文字顯示
}
```
亦即當按下開始按鍵之後,碼表就開始啟動,並且碼表文字(計時器)也會進行更新。
### 歸零按鍵
```csharp=
private void btnReset_Click(object sender, EventArgs e)
{
// 如果碼表還在跑,就紀錄目前的時間,最後歸零再啟動碼錶
if (sw.IsRunning)
{
logRecord();
sw.Restart(); // 歸零碼表,碼表仍繼續進行
}
else
{
sw.Reset(); // 如果碼表沒在跑,停止並歸零碼表
txtStopWatch.Text = "00:00:00:000"; // 讓碼表文字「歸零」
}
}
// 碼表時間紀錄
private void logRecord()
{
listStopWatchLog.Items.Clear(); // 清空 ListBox 中的元素
StopWatchLog.Add(txtStopWatch.Text); // 將碼表時間增加到暫存碼表紀錄清單裡
// 依照碼表紀錄清單「依照最新時間順序」顯示
int i = StopWatchLog.Count;
while (i > 0)
{
listStopWatchLog.Items.Add(String.Format("第 {0} 筆紀錄:{1}", i.ToString(), StopWatchLog[i - 1] + "\n"));
i--;
}
}
```
另外我們設計了一個可以記錄碼表時間的小功能,很多碼表都有這種功能,如果碼表還在跑的時候,按下歸零按鍵,數字就會重新跑,不過會把你之前的上一筆給記錄起來。
:::info
例如你想記錄同學跑操場每一圈所耗費的時間,就可以用這樣的功能來記錄。
:::
你可以注意到我們紀錄碼表時間,是寫成一個方法(函式)「**logRecord()**」,這樣就可以重複使用。
我們用了一個「清單變數」來記錄「碼表紀錄清單」,之前有宣告過一個「StopWatchLop」變數,就是用來儲存時間紀錄,你可以看到,要對一個清單變數新增一個紀錄,需要使用「Add」方法。
「碼表紀錄清單」會在24-28行的迴圈「依照最新時間順序」加到「listStopWatchLog」清單方塊之中,這樣你就可以看到最新的一筆記錄在最上方,最舊的則是最下方。
現在執行程式,你點下開始,碼表就會啟動,按下歸零按鍵,就會一直記錄每一次歸零的時間點。

然後使用「歸零重新啟動(Restart)」,讓碼表歸零後馬上又繼續開始計時。
不過你也會發現到,其實這個事件綁定有一個特點,會先判斷是不是碼表還在跑「sw.IsRunning」?我們這個設計邏輯:如果碼表還在跑,代表要記錄碼表時間,並且歸零重新繼續跑;但是如果沒有在跑,亦即使用者是按暫停按鍵之後,要歸零,因此使用「停止並歸零(Reset)」。
### 停止並歸零按鍵
```csharp=
private void btnStop_Click(object sender, EventArgs e)
{
sw.Reset(); // 停止並歸零碼表
timerStopWatch.Stop(); // 停止讓碼表文字顯示
txtStopWatch.Text = "00:00:00:000"; // 讓碼表文字「歸零」
listStopWatchLog.Items.Clear(); // 清空 ListBox 中的元素
StopWatchLog.Clear(); // 清除暫存碼表紀錄清單
}
```
按下停止並歸零按鍵,則是「停止並歸零」碼表,也停止更新碼表文字的計時器,因為是停止且歸零,所以我們也重新設定了碼表文字。並且清除記錄表,同時也清除掉所有過去的記錄。
### 暫停按鍵
```csharp=
private void btnPause_Click(object sender, EventArgs e)
{
sw.Stop(); // 停止碼表,但不歸零
timerStopWatch.Stop(); // 停止讓碼表文字顯示
}
```
暫停按鍵也很簡單,只是暫時讓碼表停止,也讓文字停止更新。但是如果使用者繼續按下開始按鍵,碼表還會繼續往下跑。
### 記錄按鍵
```csharp=
private void btnLog_Click(object sender, RoutedEventArgs e)
{
logRecord();
}
```
最後則是記錄按鍵,其實你可以發現它非常簡單,就是執行方法(函式)「**logRecord()**」而已。
跟之前歸零按鍵不同,紀錄按鍵則是會記錄「**累計時間**」。

## 完整程式碼
:::spoiler
```csharp=
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using NAudio.Wave; // 音效檔播放器函式庫
using System.IO; // 檔案讀取的IO函式庫
using System.Diagnostics; // 引用「系統診斷」的函式庫
namespace SimpleClock
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
comboboxInitialzation(); // 下拉選單初始化
timerClcok.Start(); // 啟動時鐘
}
List<string> hours = new List<string>(); // 小時清單
List<string> minutes = new List<string>(); // 分鐘清單
string strSelectTime = ""; // 用來記錄鬧鐘設定時間
private WaveOutEvent waveOut; // 音效檔播放器
private AudioFileReader audioFileReader; // 音效檔讀取器
List<string> StopWatchLog = new List<string>(); // 碼表紀錄清單
Stopwatch sw = new Stopwatch(); // 宣告一個碼表物件
// 下拉選單初始化
private void comboboxInitialzation()
{
// 設定小時下拉選單的選單內容,建立小時的清單,數字範圍為00-23
for (int i = 0; i <= 23; i++)
cmbHour.Items.Add(string.Format("{0:00}", i));
cmbHour.SelectedIndex = 0;
// 設定分鐘下拉選單的選單內容,建立分鐘的清單,數字範圍為00-59
for (int i = 0; i <= 59; i++)
cmbMin.Items.Add(string.Format("{0:00}", i));
cmbMin.SelectedIndex = 0;
}
// 時鐘timer1_Tick事件:每一秒執行一次
private void timerClcok_Tick(object sender, EventArgs e)
{
txtTime.Text = DateTime.Now.ToString("HH:mm:ss"); // 顯示時間
txtDate.Text = DateTime.Now.ToString("yyyy-MM-dd"); // 顯示日期
txtWeekDay.Text = DateTime.Now.ToString("dddd"); // 顯示星期幾
}
// 鬧鐘計時器timerAlert_tick事件:每一秒執行一次
private void timerAlert_Tick(object sender, EventArgs e)
{
// 判斷現在時間是不是已經是鬧鐘設定時間?如果時間到了,就要播放鬧鐘聲音
if (strSelectTime == DateTime.Now.ToString("HH:mm"))
{
try
{
stopWaveOut();
// 指定聲音檔的相對路徑,可以使用MP3
string audioFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "alert.wav");
// 使用 AudioFileReader 來讀取聲音檔
audioFileReader = new AudioFileReader(audioFilePath);
// 初始化 WaveOutEvent
waveOut = new WaveOutEvent();
waveOut.Init(audioFileReader);
// 播放聲音檔
waveOut.Play();
}
catch (Exception ex)
{
MessageBox.Show("無法播放聲音檔,錯誤資訊: " + ex.Message);
}
finally
{
timerAlert.Stop(); // 停止鬧鐘計時器
}
}
}
private void stopWaveOut()
{
// 停止之前的播放
if (waveOut != null)
{
waveOut.Stop();
waveOut.Dispose();
waveOut = null;
}
}
// 啟動鬧鐘
private void btnSetAlert_Click(object sender, EventArgs e)
{
timerAlert.Start(); // 啟動鬧鐘計時器
btnSetAlert.Enabled = false;
btnCancelAlert.Enabled = true;
strSelectTime = cmbHour.SelectedItem.ToString() + ":" + cmbMin.SelectedItem.ToString(); // 擷取小時和分鐘的下拉選單文字,用來設定鬧鐘時間
}
// 停止鬧鐘
private void btnCancelAlert_Click(object sender, EventArgs e)
{
stopWaveOut(); // 停止之前的播放
timerAlert.Stop(); // 停止鬧鐘計時器
btnSetAlert.Enabled = true;
btnCancelAlert.Enabled = false;
}
// timerStopWatch_tick:每毫秒執行一次,所以更新的速度會比較快
private void timerStopWatch_tick(object sender, EventArgs e)
{
txtStopWatch.Text = sw.Elapsed.ToString("hh':'mm':'ss':'fff"); // 顯示碼表時間
}
// 啟動碼表
private void btnStart_Click(object sender, EventArgs e)
{
sw.Start(); // 啟動碼表
timerStopWatch.Start(); // 開始讓碼表文字顯示
}
// 停止並歸零碼表
private void btnStop_Click(object sender, EventArgs e)
{
sw.Reset(); // 停止並歸零碼表
timerStopWatch.Stop(); // 停止讓碼表文字顯示
txtStopWatch.Text = "00:00:00:000"; // 讓碼表文字「歸零」
listStopWatchLog.Items.Clear(); // 清空 ListBox 中的元素
StopWatchLog.Clear(); // 清除暫存碼表紀錄清單
}
// 歸零按鍵會判斷你是否先按下暫停?來決定是否記錄碼表時間
private void btnReset_Click(object sender, EventArgs e)
{
// 如果碼表還在跑,就紀錄目前的時間,最後歸零再啟動碼錶
if (sw.IsRunning)
{
logRecord();
sw.Restart(); // 歸零碼表,碼表仍繼續進行
}
else
{
sw.Reset(); // 如果碼表沒在跑,停止並歸零碼表
txtStopWatch.Text = "00:00:00:000"; // 讓碼表文字「歸零」
}
}
// 停止碼表
private void btnPause_Click(object sender, EventArgs e)
{
sw.Stop(); // 停止碼表,但不歸零
timerStopWatch.Stop(); // 停止讓碼表文字顯示
}
// 碼表時間紀錄
private void btnLog_Click(object sender, EventArgs e)
{
logRecord();
}
// 碼表時間紀錄
private void logRecord()
{
listStopWatchLog.Items.Clear(); // 清空 ListBox 中的元素
StopWatchLog.Add(txtStopWatch.Text); // 將碼表時間增加到暫存碼表紀錄清單裡
// 依照碼表紀錄清單「依照最新時間順序」顯示
int i = StopWatchLog.Count;
while (i > 0)
{
listStopWatchLog.Items.Add(String.Format("第 {0} 筆紀錄:{1}", i.ToString(), StopWatchLog[i - 1] + "\n"));
i--;
}
}
}
}
```
:::