--- tags: 視窗程式設計 --- # 碼表功能 利用計時器我們製作簡單的時鐘與鬧鐘功能,接下來我們讓程式完整一些,增加幾種的操作功能。 ## 第一步:進行介面基本設計 我們選擇另一個頁籤,然後把頁籤名稱改成「碼表」,並且加入以下畫面的控制項。**特別提醒同學,碼表時間紀錄使用的是「ListBox」。** ![](https://imgur.com/v1CNYv2.png) 也請記得,將每一個控制項設定一個名稱,例如以下的範例名稱: :::info 1. 碼表時間文字顯示:txtStopWatch 2. 碼表時間紀錄:listStopWatchLog 3. 開始按鍵:btnStart 4. 暫停按鍵:btnPause 5. 歸零按鍵:btnReset 6. 紀錄按鍵:btnLog 7. 停止並歸零按鍵:btnStop ::: ### 計時器元件 同樣的我們需要加入「**計時器 Timer**」,「Timer」控制項也是放在工具箱中,主要是在「元件」的清單中。置放的方式很簡單,直接拖到視窗即可。 :::info 1. 碼表計時器:timerStopWatch ::: 請在屬性視窗中設定它的名稱,再將「Interval」設定為數字 1,這個是指每一毫秒執行一次。如果是設定 100,代表每一秒執行一次。 ![](https://imgur.com/8hsRItH.png) :::success 之前的時鐘功能就是設定100,你可以回去確認一下。 ::: 計時器也有事件綁定,請選擇他們的「Tick」事件,點兩下,在程式碼裡面做好綁定。 ![](https://imgur.com/O2SlcqJ.png) ## 第二部分:程式碼撰寫 完成基本介面設計後,我們就要把程式寫進來,讓程式可以運作。 碼表其實可以利用「**系統診斷**」函式庫的「**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」清單方塊之中,這樣你就可以看到最新的一筆記錄在最上方,最舊的則是最下方。 現在執行程式,你點下開始,碼表就會啟動,按下歸零按鍵,就會一直記錄每一次歸零的時間點。 ![](https://imgur.com/s2c2Inn.png) 然後使用「歸零重新啟動(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()**」而已。 跟之前歸零按鍵不同,紀錄按鍵則是會記錄「**累計時間**」。 ![](https://imgur.com/sVi1Qyn.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(); // 宣告一個碼表物件 // 下拉選單初始化 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--; } } } } ``` :::