---
tags: 110-2視窗程式設計
---
# 倒數計時器
同樣利用計時器,我們製作倒數計時器功能。
我們也是利用時鐘小程式,並且搭配計時器,來實現倒數計時器功能。我們也利用一個新的控制項「**TabControl(頁籤)**」來整合各種功能的畫面。
## 第一步:進行介面基本設計
我們可以選擇另一個頁籤,然後把頁籤名稱改成「倒數」,並且加入以下畫面的控制項。

也請記得,將每一個控制項設定一個名稱,例如以下的範例名稱:
:::info
1. 倒數時間文字顯示:txtCountDown
2. 小時下拉選單:cmbCountHour
3. 分鐘下拉選單:cmbCountMin
4. 秒下拉選單:cmbCountSecond
5. 開始倒數按鍵:btnCountStart
6. 暫停按鍵:btnCountPause
7. 停止按鍵:btnCountStop
:::
### 計時器元件
同樣的我們需要加入「**計時器 Timer**」,「Timer」控制項也是放在工具箱中,主要是在「元件」的清單中。置放的方式很簡單,直接拖到視窗即可。
:::info
1. 碼表計時器:timerCountDown
:::
請在屬性視窗中設定它的名稱,再將「Interval」設定為數字 1,這個是指每一毫秒執行一次。如果是設定 100,代表每一秒執行一次。

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

## 第二部分:程式碼撰寫
完成基本介面設計後,我們就要把程式寫進來,讓程式可以運作。請找出一個「**MainWindow()**」的函式片段,將以下的程式內容放進你的程式碼之中。不是直接將原有的函式內容蓋過去哦,要將以下程式內容加入。
```csharp=
bool isCountDownReset = true; // 用來紀錄是不是重新設定
TimeSpan ts; // 宣告一個時間間隔變數
// 下拉選單初始化
private void comboboxInitialzation()
{
// 設定小時下拉選單的選單內容,建立小時的清單,數字範圍為00-23
for (int i = 0; i <= 23; i++)
{
cmbHour.Items.Add(string.Format("{0:00}", i));
cmbCountHour.Items.Add(string.Format("{0:00}", i));
}
// 設定分鐘下拉選單的選單內容,建立分鐘的清單,數字範圍為00-59
for (int i = 0; i <= 59; i++)
{
cmbMin.Items.Add(string.Format("{0:00}", i));
cmbCountMin.Items.Add(string.Format("{0:00}", i));
cmbCountSecond.Items.Add(string.Format("{0:00}", i));
}
cmbHour.SelectedIndex = 0;
cmbMin.SelectedIndex = 0;
cmbCountHour.SelectedIndex = 0;
cmbCountMin.SelectedIndex = 0;
cmbCountSecond.SelectedIndex = 0;
}
// timerCountDown_tick:每一秒執行一次
private void timerCountDown_Tick(object sender, EventArgs e)
{
txtCountDown.Text = ts.ToString("hh':'mm':'ss"); // 顯示時間
ts = ts.Subtract(TimeSpan.FromSeconds(1)); // 每一秒鐘將顯示時間減掉一秒
if (txtCountDown.Text == "00:00:00")
{
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
{
timerCountDown.Stop(); // 停止鬧鐘計時器
}
}
}
```
### 程式說明
如果你已經對計時器物件有概念了,你應該看了程式,大概可以猜的出來,倒數計時的計時器,每一秒鐘都會將一個「**ts時間間隔變數**」顯示在「txtCountDown」輸入文字框中。
然後將「ts時間間隔變數」的時間減掉一秒,讓時間逐漸一秒一秒的減少,然後每一秒鐘都做一次判斷,如果時間等於「00:00:00」等同時間結束,就會發出鬧鐘聲音。
不過要在什麼地方設定那個「ts時間間隔變數」,才能設定我們希望倒數的時間呢?請先將所有按鍵都綁定事件,然後將以下程式碼置入。
```csharp=
// timerCountDown_tick:每一秒執行一次
private void btnCountStart_Click(object sender, EventArgs e)
{
// 進行判斷,判斷是不是有按過停止計時器按鍵
if (isCountDownReset == true)
{
int Hour = int.Parse(cmbCountHour.SelectedItem.ToString());
int Min = int.Parse(cmbCountMin.SelectedItem.ToString());
int Sec = int.Parse(cmbCountSecond.SelectedItem.ToString());
ts = new TimeSpan(Hour, Min, Sec); // 設定倒數時間
}
isCountDownReset = false;
timerCountDown.Start();
}
// 暫停倒數計時器按鍵
private void btnCountPause_Click(object sender, EventArgs e)
{
timerCountDown.Stop();
}
// 停止計時器按鍵
private void btnCountStop_Click(object sender, EventArgs e)
{
stopWaveOut(); // 關閉鬧鐘聲音
isCountDownReset = true;
timerCountDown.Stop();
txtCountDown.Text = "00:00:00";
cmbCountHour.SelectedIndex = 0;
cmbCountMin.SelectedIndex = 0;
cmbCountSecond.SelectedIndex = 0;
}
```
以上的程式碼都是在設定三個操作按鍵:
1. 啟動倒數計時器按鍵:首先進行一個簡易的判斷,判斷是不是有按過停止計時器按鍵,如果有,才是真正的重新設定倒數計時器,並且開始導數即時。
2. 暫停倒數計時器按鍵:單純只是停止計時器。
3. 停止計時器按鍵:將「isCountDownReset」變數設定為「true」,並且將所有的控制項重新設定。

現在你可以測試看看,應該設定一個倒數時間,你可以讓程式慢慢倒數,最後時間到達「00:00:00」就會發出鬧鐘聲音了。
### 將播放聲音檔的程式獨立成為函式
這裡我們將播放聲音檔的程式獨立出來,並且修改鬧鐘計時器 timerAlert_tick 與倒數計時器 timerCountDown_Tick 事件的內容,你應該會發現,程式碼會再一次的有結構感。
```csharp=
// 鬧鐘計時器timerAlert_tick事件:每一秒執行一次
private void timerAlert_tick(object sender, EventArgs e)
{
// 判斷現在時間是不是已經是鬧鐘設定時間?如果時間到了,就要播放鬧鐘聲音
if (strSelectTime == DateTime.Now.ToString("HH:mm"))
playBeep(timerAlert);
}
// 倒數計時器timerCountDown_Tick事件:每一秒執行一次
private void timerCountDown_Tick(object sender, EventArgs e)
{
txtCountDown.Text = ts.ToString("hh':'mm':'ss"); // 顯示時間
ts = ts.Subtract(TimeSpan.FromSeconds(1)); // 每一秒鐘將顯示時間減掉一秒
if (txtCountDown.Text == "00:00:00")
playBeep(timerCountDown);
}
// 播放鬧鐘聲音檔函式
private void playBeep(System.Windows.Forms.Timer timer)
{
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
{
timer.Stop(); // 停止鬧鐘計時器
}
}
```
### 程式碼整理
這時你應該會發現到程式碼不太容易閱讀,倒不是程式碼很長,而是有點混亂。
通常我們開發程式碼到一個程度的時候,就會試著去利用函式或類別對程式碼做整理,讓程式碼更加結構化。
但是你也可以透過排版的方式整理,例如「**#region 和 #endregion**」可以用來組織和管理程式碼,讓程式碼更易讀和更易維護。這些指令通常用來折疊和展開程式碼區塊,特別是在大型專案中非常有用。
用途有以下幾點:
1. 組織程式碼:使用 #region 和 #endregion 可以將相關的程式碼組織在一起,形成邏輯區塊,使程式碼結構更加清晰。
2. 折疊功能:在 Visual Studio 等開發環境中,可以折疊和展開這些區塊,這有助於集中注意力在特定部分的程式碼上,而不會被其他部分干擾。
3. 增加可讀性:通過給區塊命名,可以更直觀地瞭解區塊內的程式碼用途和功能。
它們的語法很簡單,就像以下的程式碼。其中老師會建議區塊名稱的部分,可以前後加上橫線,這樣可以讓文字看起來更好閱讀。
```csharp=
#region 區塊名稱
// 這裡是區塊內的程式碼
#endregion
// 範例
#region -- 變數宣告 --
int a = 10;
int b = 20;
#endregion
#region -- 計算 --
int sum = a + b;
Console.WriteLine($"Sum: {sum}");
#endregion
#region -- 顯示結果 --
Console.WriteLine("計算完成!");
#endregion
```
加了「**#region 和 #endregion**」的程式碼就會像以下這樣子,你還可以收起來。

收起來的樣子就像這樣,你可以將沒有要調整的程式碼先收起,並且可以透過簡單的分類,讓程式碼更好閱讀。

如果把所有區間的程式碼收起來,就可以讓程式碼看起來更加簡潔。

最後,以下的完整程式碼你可以參考,並且實際更新到你的程式碼之中。
### 完整程式碼
:::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(); // 宣告一個碼表物件
bool isCountDownReset = true; // 用來紀錄是不是重新設定
TimeSpan ts; // 宣告一個時間間隔變數
#region -- Tick事件 --
// 時鐘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"); // 顯示星期幾
}
// timerStopWatch_tick:每毫秒執行一次,所以更新的速度會比較快
private void timerStopWatch_Tick(object sender, EventArgs e)
{
txtStopWatch.Text = sw.Elapsed.ToString("hh':'mm':'ss':'fff"); // 顯示碼表時間
}
// 鬧鐘計時器timerAlert_tick事件:每一秒執行一次
private void timerAlert_Tick(object sender, EventArgs e)
{
// 判斷現在時間是不是已經是鬧鐘設定時間?如果時間到了,就要播放鬧鐘聲音
if (strSelectTime == DateTime.Now.ToString("HH:mm"))
playBeep(timerAlert);
}
// 倒數計時器timerCountDown_Tick事件:每一秒執行一次
private void timerCountDown_Tick(object sender, EventArgs e)
{
txtCountDown.Text = ts.ToString("hh':'mm':'ss"); // 顯示時間
ts = ts.Subtract(TimeSpan.FromSeconds(1)); // 每一秒鐘將顯示時間減掉一秒
if (txtCountDown.Text == "00:00:00")
playBeep(timerCountDown);
}
// timerCountDown_tick:每一秒執行一次
private void btnCountStart_Click(object sender, EventArgs e)
{
// 進行判斷,判斷是不是有按過停止計時器按鍵
if (isCountDownReset == true)
{
int Hour = int.Parse(cmbCountHour.SelectedItem.ToString());
int Min = int.Parse(cmbCountMin.SelectedItem.ToString());
int Sec = int.Parse(cmbCountSecond.SelectedItem.ToString());
ts = new TimeSpan(Hour, Min, Sec); // 設定倒數時間
}
isCountDownReset = false;
timerCountDown.Start();
}
#endregion
#region -- 自訂函式 --
// 下拉選單初始化
private void comboboxInitialzation()
{
// 設定小時下拉選單的選單內容,建立小時的清單,數字範圍為00-23
for (int i = 0; i <= 23; i++)
{
cmbHour.Items.Add(string.Format("{0:00}", i));
cmbCountHour.Items.Add(string.Format("{0:00}", i));
}
// 設定分鐘下拉選單的選單內容,建立分鐘的清單,數字範圍為00-59
for (int i = 0; i <= 59; i++)
{
cmbMin.Items.Add(string.Format("{0:00}", i));
cmbCountMin.Items.Add(string.Format("{0:00}", i));
cmbCountSecond.Items.Add(string.Format("{0:00}", i));
}
cmbHour.SelectedIndex = 0;
cmbMin.SelectedIndex = 0;
cmbCountHour.SelectedIndex = 0;
cmbCountMin.SelectedIndex = 0;
cmbCountSecond.SelectedIndex = 0;
}
// 碼表時間紀錄
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--;
}
}
// 播放鬧鐘聲音檔函式
private void playBeep(Timer timer)
{
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
{
timer.Stop(); // 停止鬧鐘計時器
}
}
private void stopWaveOut()
{
// 停止之前的播放
if (waveOut != null)
{
waveOut.Stop();
waveOut.Dispose();
waveOut = null;
}
}
#endregion
#region -- 時鐘介面 --
// 啟動鬧鐘
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;
}
#endregion
#region -- 碼表介面 --
// 啟動碼表
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();
}
#endregion
#region -- 倒數計時器介面 --
// 暫停倒數計時器按鍵
private void btnCountPause_Click(object sender, EventArgs e)
{
timerCountDown.Stop();
}
// 停止計時器按鍵
private void btnCountStop_Click(object sender, EventArgs e)
{
stopWaveOut(); // 關閉鬧鐘聲音
isCountDownReset = true;
timerCountDown.Stop();
txtCountDown.Text = "00:00:00";
cmbCountHour.SelectedIndex = 0;
cmbCountMin.SelectedIndex = 0;
cmbCountSecond.SelectedIndex = 0;
}
#endregion
}
}
```
:::