--- tags: 110-2視窗程式設計 --- # 倒數計時器 同樣利用計時器,我們製作倒數計時器功能。 我們也是利用時鐘小程式,並且搭配計時器,來實現倒數計時器功能。我們也利用一個新的控制項「**TabControl(頁籤)**」來整合各種功能的畫面。 ## 第一步:進行介面基本設計 我們可以選擇另一個頁籤,然後把頁籤名稱改成「倒數」,並且加入以下畫面的控制項。 ![](https://imgur.com/edAMN8r.png) 也請記得,將每一個控制項設定一個名稱,例如以下的範例名稱: :::info 1. 倒數時間文字顯示:txtCountDown 2. 小時下拉選單:cmbCountHour 3. 分鐘下拉選單:cmbCountMin 4. 秒下拉選單:cmbCountSecond 5. 開始倒數按鍵:btnCountStart 6. 暫停按鍵:btnCountPause 7. 停止按鍵:btnCountStop ::: ### 計時器元件 同樣的我們需要加入「**計時器 Timer**」,「Timer」控制項也是放在工具箱中,主要是在「元件」的清單中。置放的方式很簡單,直接拖到視窗即可。 :::info 1. 碼表計時器:timerCountDown ::: 請在屬性視窗中設定它的名稱,再將「Interval」設定為數字 1,這個是指每一毫秒執行一次。如果是設定 100,代表每一秒執行一次。 ![](https://imgur.com/cJ1U1DD.png) :::success 之前的時鐘功能就是設定100,你可以回去確認一下。 ::: 計時器也有事件綁定,請選擇他們的「Tick」事件,點兩下,在程式碼裡面做好綁定。 ![](https://imgur.com/hwj8Kb6.png) ## 第二部分:程式碼撰寫 完成基本介面設計後,我們就要把程式寫進來,讓程式可以運作。請找出一個「**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」,並且將所有的控制項重新設定。 ![](https://imgur.com/N3Eu0Yy.png) 現在你可以測試看看,應該設定一個倒數時間,你可以讓程式慢慢倒數,最後時間到達「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**」的程式碼就會像以下這樣子,你還可以收起來。 ![](https://imgur.com/86q1AMd.png =400x) 收起來的樣子就像這樣,你可以將沒有要調整的程式碼先收起,並且可以透過簡單的分類,讓程式碼更好閱讀。 ![](https://imgur.com/B2ZzHgq.png =300x) 如果把所有區間的程式碼收起來,就可以讓程式碼看起來更加簡潔。 ![](https://imgur.com/yjlAtW9.png) 最後,以下的完整程式碼你可以參考,並且實際更新到你的程式碼之中。 ### 完整程式碼 :::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 } } ``` :::