--- tags: 視窗程式設計 --- # 實作:簡單時鐘 現在我們實作一個簡單的數位時鐘,然後還有一個鬧鐘功能,你可以用來顯示時間,還能設定鬧鐘。 我們用這個範例,讓同學更進一步瞭解「計時器(Timer)」的使用,讓你可以用來做出自動化的功能。 ## 程式功能 這次我們會設計一個簡單時鐘,可以同時顯示時間、日期與星期幾,還可以設定與關閉一個鬧鐘,時間到了會發出鬧鐘聲響。 ## 程式試用 你可以到課堂雲端硬碟連結,下載完成的程式檔,了解裡面的內容。 [課堂雲端硬碟連結](https://fgu365-my.sharepoint.com/:u:/g/personal/chlu_vdi_fgu_edu_tw/Ee2ANVS5i6hCmoocWW7WjmcBEnf_UaJDvBPVeFfwxg9vzg?e=uvkpU0) ## 開啟新專案 開啟新專案之前有教學過,如果你忘記了或不太熟悉,請點選[這裡](https://hackmd.io/sFFV6T2oT0-ysHotbIybhw?view#WPF-%E7%9A%84%E5%B0%88%E6%A1%88%E8%A8%AD%E5%AE%9A)。 :::success 我們的專案名稱可以叫做「**SimpleClock**」,簡單就好。 ::: ## 開始吧! 還記得第二部分的基本視窗程式設計嗎?同樣的,我們依照之前說過的兩個主要程式設計流程來設計: 1. 介面基本設計 2. 程式撰寫 因此,我們也是先初步設計介面,再來進行程式撰寫。 ## 第一步:進行介面基本設計 和過去的範例程式都類似,請你先從工具箱拉控制項元件進來,這次我們使用一種可以製作頁籤的控制項,叫做「**標籤頁面 TabControl**」,這個控制項就像是瀏覽的頁籤,你可以在同一個視窗中整合好幾個頁面。 你可以在以下的工具箱,在「容器」項目中找到它。 ![](https://imgur.com/H9lEiKb.png) 你可以把它拖到視窗中,再調整大小,它預設會有兩個頁籤。 ![](https://imgur.com/m19EPv4.png) 你一定會嫌它的字太小,所以我們來調整一下,這時要請你注意一下,因為「TabControl」會包含頁籤,你要設定每一個頁籤最好要選擇到正確的位置,這樣在「屬性視窗」才能找到正確的設定「TabPages」來調整字體大小。 ![](https://imgur.com/sczlEmW.png) 下圖使用「TabPages」來調整字體大小,請點選那個有三個小點點的按鍵。 ![](https://imgur.com/OI9NVB8.png) 點下按鍵後。你就可以新增與移除頁籤,再針對不同的頁籤設定標題文字,也可以改變字型、字體大小與文字樣式。你可以試試看,最後按下確定就可以設定完成。 ![](https://imgur.com/TpIUPl4.png) 接下來我們先製作時鐘的頁籤,你可以參考下圖來安排各個控制項 ![](https://imgur.com/eq72qUD.png) 需要三個輸入文字框、兩個按鍵、兩個下拉選單。也請記得,將每一個控制項設定一個名稱,例如以下的範例名稱: :::info 1. 時間顯示輸入文字框:txtTime 2. 日期顯示輸入文字框:txtDate 3. 星期顯示輸入文字框:txtWeekDay 4. 設定鬧鐘按鍵:btnSetAlert 5. 取消鬧鐘按鍵:btnCancelAlert 6. 小時下拉選單:cmbHour 7. 分鐘下拉選單:cmbMin ::: 你可以依據自己的喜好,將元件大小、字體大小和字型設定好。 再來可以參考以下的屬性設定,其中「Enabled」設定是用來設定使用者能不能改裡面的內容,因為我們希望時鐘文字顯示都可以不要被使用者修改,建議可以設定成「False」。再來則是文字置中,可以設定「TextAlign」為「Center」,將文字置中。 ![](https://imgur.com/momCjit.png) ### 設定視窗左上角小圖示與文字 現在我們調整一下視窗設定,我們其實可以設定一個簡單的小圖示放到視窗左上角,用來做辨識。 圖示其實很多網站都可以下載,例如這個[連結](https://www.flaticon.com/uicons?word=clock),你可以在上面找到你喜歡的圖示,建議你下載 PNG 格式的,解析度不要太低,至少256px左右的。或者,你也可以到這裡下載我們這次簡單時鐘所需的[素材檔](https://fgu365-my.sharepoint.com/:f:/g/personal/chlu_vdi_fgu_edu_tw/ErAp2L7jZPFLtKxozH19ldgBUePawwFRqANx_wo7HsQj9Q?e=fWApLK),其中就有一張鬧鐘的圖示檔(clock.png)。 不過 Windows Form 不支援 PNG 格式檔案,只支援 ICO 格式,這個圖形格式其實有點老舊,所以你需要做轉換,你可以用以下的網址來轉,請使用 32x32 尺寸。 ![](https://imgur.com/sD3OHHE.png)https://www.hipdf.com/zh-tw/png-to-ico 接下來請選擇視窗上面的標題列,然後選擇屬性的「Icon」,點下三個小點的按鍵,你就可以指定它的圖示檔。 ![](https://imgur.com/TytdcPh.png) 完成之後,你可以看到視窗的左上角就有一個小時鐘圖示。標題你也可以修改,請到屬性的「Text」來修改就可以了。 ![](https://imgur.com/2bqfD7b.png) ### 計時器元件 現在我們再加入兩個「**計時器 Timer**」,假如你要做跟時間相關的應用,譬如說顯示現在時間,並且要能不斷地更新,或者希望可以定時執行特定程式碼,那這個東西就很好用。 「Timer」控制項也是放在工具箱中,主要是在「元件」的清單中,如下圖。置放的方式很簡單,直接拖到視窗即可。 ![](https://imgur.com/zWfJCcm.png) 特別需要注意一下,計時器不會顯示在視窗之中,置放進去後,會在下方列出你放進來的計時器,這樣你就能確定有沒有置放完成。 ![](https://imgur.com/c5YDadQ.png) 兩個計時器我們都需要設定名稱,你可以依照以下名稱來設定,名稱也是在屬性視窗中來設定。 ![](https://imgur.com/SoHC5QB.png) :::info 1. 時鐘計時器:timerClcok 2. 鬧鐘計時器:timerAlert ::: 計時器也有事件綁定,請選擇他們的「Tick」事件,點兩下,在程式碼裡面做好綁定。 ![](https://imgur.com/Ynue8iA.png) ![](https://imgur.com/i1LrOed.png) ### 匯入鬧鐘聲音 由於我們要設計鬧鐘功能,所以需要匯入一個鬧鐘的聲音檔,同樣的請將上面的[素材檔](https://fgu365-my.sharepoint.com/:f:/g/personal/chlu_vdi_fgu_edu_tw/ErAp2L7jZPFLtKxozH19ldgBUePawwFRqANx_wo7HsQj9Q?e=fWApLK),裡面有一個「alert.wav」,這是一個電子鬧鐘的聲音,你可以試著播放聽聽看。 請將這個檔案放到你的專案檔中,請打開專案檔,找尋一個「bin」->「Debug」資料夾,在裡面新增一個資料夾,請將檔案名稱改為「Resources」,大小寫有差,請確認名稱是正確的,再將聲音檔放到「Resources」資料中。 ## 第二部分:程式碼撰寫 完成基本介面設計後,我們就要把程式寫進來,讓程式可以運作。首先我們要先初始化兩個下拉選單裡面的內容,讓這兩個下拉選單一個可以選擇小時,另一個可以選擇分鐘。 ```csharp= public partial class Form1 : Form { public Form1() { InitializeComponent(); comboboxInitialzation(); // 下拉選單初始化 } List<string> hours = new List<string>(); // 小時清單 List<string> minutes = new List<string>(); // 分鐘清單 // 下拉選單初始化 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; } } ``` ### 清單(List) 這裡我們使用一種資料型態變數,叫做「==**清單(List)**==」,他和陣列很像,也是一連串的資料的組合,不過差別在於可以很自由的增加、刪除與改變資料項目。不過方便的代價就是會比較消耗記憶體,但我們的程式很簡單,這點就可以忽略掉。 清單的宣告很簡單,就像以下的語法: ```csharp List<資料型態> 變數名稱 = new List<資料型態>(); ``` 你可以注意到,只要你的資料型態指定好,清單可以放的資料型態很多種,甚至是一個陣列都可以,在這裡,我們都是放簡單的字串。小時的清單放的是「00、01、02 ... 22、23」,用來代表小時;分鐘的清單則是「00、01、02 ... 57、58、59」,用來代表分鐘。 我們可以直接用一連串的字串直接指定給清單,不過這樣太麻煩了,我們直接就寫迴圈將字串放進清單中,你可以看到11、12行我們就是這樣做,並且使用Add方法,將字串逐一放進字串中。附帶一提,由於我們想要的數字一律要格式化為兩位數,所以使用「string.Format("{0:00}", i)」來做格式化。 最後,把兩個清單指定到兩個下拉選單,成為選單的內容。現在你可以測試程式,應該就可以看到兩個下拉選單都可以拉出一連串的項目可以選擇。 ![](https://imgur.com/n5OavC7.png) ![](https://imgur.com/Pf7yyt6.png) ### 程式初始化函式MainWindow() 如果你有仔細觀察,你可以發現,上一個下拉選單的設計是寫在一個專案一開始就有的「**MainWindow()**」函式中,這個函式很特別,只要你一開始執行程式,這一段一定會被執行到,所以當你需要開始一些基本的設定,可以將程式寫在這邊。 因為我們希望程式一開始執行,就必須要讓兩個下拉選單都有選項,所以我們選擇將兩個下拉選單的初始化程式都寫在這邊。 我們還有一些項目也要初始化,因此之後還會再進一步說明。 ### 顯示時間、日期與星期幾 現在我們要進一步讓時間、日期與星期幾可以顯示在「txtTime」輸入文字框之中。請將以下程式碼也放進到你的程式碼之中。 其中請特別注意,「Form1()」中的程式,要新增「timerClcok.Start(); 」這段語法。 ```csharp= public Form1() { InitializeComponent(); comboboxInitialzation(); // 下拉選單初始化 timerClcok.Start(); // 啟動時鐘 } // 時鐘timer1_Tick事件:每0.1秒執行一次 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"); // 顯示星期幾 } ``` 你可以初步先執行程式看看,應該就能讓時間、日期與星期幾顯示在程式上面。如下圖: ![](https://i.imgur.com/eq72qUD.png) ### 計時器概念 為了要讓時間和日期出現,我們需要一個東西可以一直執行,直到我們把它停止或者程式關閉為止。 這個東西就是我們之前提及的「**計時器 Timer**」,假如你要做跟時間相關的應用,譬如說顯示現在時間,並且要能不斷地更新,或者希望可以定時執行特定程式碼,就需要使用計時器。 你可以計時器設定多久更新一次,例如一秒鐘更新一次,然後指定給它一個程式定期執行,讓它更新一次時同時也執行你設定的程式內容。基本的流程如下。 ``` mermaid graph TD; 宣告計時器-->設定多久更新一次; 設定多久更新一次-->設定程式事件; 設定程式事件-->啟動; 啟動-->依照你設定的更新時間重複執行程式事件; 依照你設定的更新時間重複執行程式事件-->停止或程式關閉; 停止或程式關閉-->計時器停止; ``` 使用計時器的方式如下,你可以仔細觀察,程式內容也剛好就像上圖所說明的順序。 ```csharp= timer.Start(); // 啟動這個計時器 timer.Stop(); // 停止這個計時器 // timer_tick事件:每0.1秒執行一次 private void timer_tick(object sender, EventArgs e) { // 你要這個計時器每次更新要執行的程式內容... } ``` 我們在程式中設定每0.1秒就更新一次,因此在「timer_tick事件」中,每一秒鐘都顯示目前的時間(**DateTime.Now**),然後我們將時間格式化成為「時間」、「日期」與「星期幾」。 ```csharp txtTime.Text = DateTime.Now.ToString("HH:mm:ss"); // 顯示時間 txtDate.Text = DateTime.Now.ToString("yyyy-MM-dd"); // 顯示日期 txtWeekDay.Text = DateTime.Now.ToString("dddd"); // 顯示星期幾 ``` 其中以下的格式化可以顯示不同的時間格式,如果你還想要知道其他的格式,可以參考這個[連結](https://gogo1119.pixnet.net/blog/post/28140411),你也可以自己設定喜好的時間格式。 :::info HH:mm:ss -> 可以顯示「13:33:40」這樣的時間 yyyy-MM-dd -> 可以顯示「2022-05-20」這樣的日期 dddd -> 可以顯示星期幾 ::: 附帶一提,因為我們希望一直更新三個輸入文字框的內容,所以這個計時器不需要停止,但是有一個東西我們希望可以停止,就是鬧鐘功能。接下來我們就來設計鬧鐘功能。 ### 鬧鐘功能 其實鬧鐘功能也是需要使用計時器,差別在於我們要在事件處理中,每一秒做一次判斷:「**判斷現在的時間是不是和設定的鬧鐘時間一樣?如果是,就開啟鬧鐘聲音,並且停止鬧鐘計時器**」。 不過你需要讓程式發出聲音,如何讓視窗程式有聲音呢,這裡我們使用「**音效檔播放器函式庫 NAudio**」,它是一種 .NET 所使用的第三方函式庫,可以用來播放常見音效與音樂檔案格式。 你需要另外安裝,不過很簡單,只要用「**NuGet套件管理員**」就可以找到它來安裝。NuGet套件管理員的位置在「工具」、「NuGet套件管理員」選單中,請選擇下圖所示的「管理方案的NuGet套件」。 ![](https://imgur.com/cpAFTcl.png) 按下「管理方案的NuGet套件」就會出現以下視窗,你可以點選左上角的「瀏覽」,在下方的搜尋列中輸入「NAudio」,通常第一個出現的項目就是我們要找的「NAudio」,選擇起來後,請在你的專案中勾選要安裝的項目,再按「安裝」按鍵。 ![](https://imgur.com/IXmRtQz.png) 按下去之後可能看起來沒啥反應,不過在輸出視窗中會出現訊息,你可以等一下。 ![](https://imgur.com/wBOaMPU.png) 等一下之後就會看到以下視窗,請點選確定就可以等它安裝完成。 ![](https://imgur.com/dJyBq7X.png =500x) 請在下圖程式碼的位置。 ![](https://imgur.com/tW5YInX.png) 接下來,請在程式中最上方的引用函式庫的位置,再加入以下這兩行程式碼。 ```csharp= using NAudio.Wave; // 音效檔播放器函式庫 using System.IO; // 檔案讀取的IO函式庫 ``` 然後在程式碼中加入以下程式碼。你可以思考一下程式應該加在甚麼地方? ```csharp= private string strSelectTime; // 鬧鐘時間設定 private WaveOutEvent waveOut; // 音效檔播放器 private AudioFileReader audioFileReader; // 音效檔讀取器 // 鬧鐘計時器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; } } ``` 仔細閱讀程式碼,你應該也會發現,其實原理跟顯示時間類似,一樣也要一個計時器,不過在事件處理的地方(timerAlert_tick),每一秒鐘會比對一次時間,如果相同代表時間到了,要讓鬧鐘發出聲音。 ### 聲音檔播放 在聲音檔播放的部分,我們使用「音效檔播放器函式庫」來實現,因為聲音檔式檔案,因此需要指定檔案路徑,再讀取聲音檔。 然後接下來我們要建立一個 WaveOutEvent 物件變數,簡單的以「Play」函式來啟動聲音。 由於聲音檔也是檔案,如果沒有要播放了,就要釋放它,避免佔據聲音檔的使用權,所以也要從記憶體刪除 WaveOutEvent 物件變數。以下是簡單的程式碼說明。 ```csharp= using NAudio.Wave; // 音效檔播放器函式庫 using System.IO; // 檔案讀取的IO函式庫 private WaveOutEvent waveOut; // 宣告音效檔播放器 private AudioFileReader audioFileReader; // 宣告音效檔讀取器 // 指定聲音檔的相對路徑,可以使用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(); // 停止播放聲音檔 waveOut.Stop(); // 將WaveOutEvent 物件變數從記憶體中刪除 waveOut.Dispose(); waveOut = null; ``` ### 按鍵事件處理 接下來我們要設定鬧鐘的開啟與關閉按鍵,請先對它們做Click事件綁定,再置放以下的程式碼。 ```csharp= 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; } ``` 你可以仔細觀察,程式碼也不難,啟動鬧鐘的程式碼首先會開啟鬧鐘的計時器,並且將自己設定為不能按下,讓取消按鍵可以按,最後就是設定時間,主要是從小時與分鐘下拉選單建立字串,存到strSelectTime變數中。 亦即每次你選好時間,strSelectTime變數會存類似「13:31」這樣的字串,用來在timerAlert_tick事件處理做時間的比對。 如果按下取消鬧鐘的按鍵,則是會停止鬧鐘的聲響,如果鬧鐘正在發出聲音,就會關閉聲音,並且關閉鬧鐘計時器,將自己設定為不能按下,讓啟動鬧鐘按鍵可以按。 你可以測試看看,並且看看時間到了,是否發出鬧鐘聲音? ![](https://imgur.com/AR7YI2V.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函式庫 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; // 音效檔讀取器 // 下拉選單初始化 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; } } } ``` :::