---
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**」,這個控制項就像是瀏覽的頁籤,你可以在同一個視窗中整合好幾個頁面。
你可以在以下的工具箱,在「容器」項目中找到它。

你可以把它拖到視窗中,再調整大小,它預設會有兩個頁籤。

你一定會嫌它的字太小,所以我們來調整一下,這時要請你注意一下,因為「TabControl」會包含頁籤,你要設定每一個頁籤最好要選擇到正確的位置,這樣在「屬性視窗」才能找到正確的設定「TabPages」來調整字體大小。

下圖使用「TabPages」來調整字體大小,請點選那個有三個小點點的按鍵。

點下按鍵後。你就可以新增與移除頁籤,再針對不同的頁籤設定標題文字,也可以改變字型、字體大小與文字樣式。你可以試試看,最後按下確定就可以設定完成。

接下來我們先製作時鐘的頁籤,你可以參考下圖來安排各個控制項

需要三個輸入文字框、兩個按鍵、兩個下拉選單。也請記得,將每一個控制項設定一個名稱,例如以下的範例名稱:
:::info
1. 時間顯示輸入文字框:txtTime
2. 日期顯示輸入文字框:txtDate
3. 星期顯示輸入文字框:txtWeekDay
4. 設定鬧鐘按鍵:btnSetAlert
5. 取消鬧鐘按鍵:btnCancelAlert
6. 小時下拉選單:cmbHour
7. 分鐘下拉選單:cmbMin
:::
你可以依據自己的喜好,將元件大小、字體大小和字型設定好。
再來可以參考以下的屬性設定,其中「Enabled」設定是用來設定使用者能不能改裡面的內容,因為我們希望時鐘文字顯示都可以不要被使用者修改,建議可以設定成「False」。再來則是文字置中,可以設定「TextAlign」為「Center」,將文字置中。

### 設定視窗左上角小圖示與文字
現在我們調整一下視窗設定,我們其實可以設定一個簡單的小圖示放到視窗左上角,用來做辨識。
圖示其實很多網站都可以下載,例如這個[連結](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://www.hipdf.com/zh-tw/png-to-ico
接下來請選擇視窗上面的標題列,然後選擇屬性的「Icon」,點下三個小點的按鍵,你就可以指定它的圖示檔。

完成之後,你可以看到視窗的左上角就有一個小時鐘圖示。標題你也可以修改,請到屬性的「Text」來修改就可以了。

### 計時器元件
現在我們再加入兩個「**計時器 Timer**」,假如你要做跟時間相關的應用,譬如說顯示現在時間,並且要能不斷地更新,或者希望可以定時執行特定程式碼,那這個東西就很好用。
「Timer」控制項也是放在工具箱中,主要是在「元件」的清單中,如下圖。置放的方式很簡單,直接拖到視窗即可。

特別需要注意一下,計時器不會顯示在視窗之中,置放進去後,會在下方列出你放進來的計時器,這樣你就能確定有沒有置放完成。

兩個計時器我們都需要設定名稱,你可以依照以下名稱來設定,名稱也是在屬性視窗中來設定。

:::info
1. 時鐘計時器:timerClcok
2. 鬧鐘計時器:timerAlert
:::
計時器也有事件綁定,請選擇他們的「Tick」事件,點兩下,在程式碼裡面做好綁定。


### 匯入鬧鐘聲音
由於我們要設計鬧鐘功能,所以需要匯入一個鬧鐘的聲音檔,同樣的請將上面的[素材檔](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)」來做格式化。
最後,把兩個清單指定到兩個下拉選單,成為選單的內容。現在你可以測試程式,應該就可以看到兩個下拉選單都可以拉出一連串的項目可以選擇。


### 程式初始化函式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"); // 顯示星期幾
}
```
你可以初步先執行程式看看,應該就能讓時間、日期與星期幾顯示在程式上面。如下圖:

### 計時器概念
為了要讓時間和日期出現,我們需要一個東西可以一直執行,直到我們把它停止或者程式關閉為止。
這個東西就是我們之前提及的「**計時器 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套件」。

按下「管理方案的NuGet套件」就會出現以下視窗,你可以點選左上角的「瀏覽」,在下方的搜尋列中輸入「NAudio」,通常第一個出現的項目就是我們要找的「NAudio」,選擇起來後,請在你的專案中勾選要安裝的項目,再按「安裝」按鍵。

按下去之後可能看起來沒啥反應,不過在輸出視窗中會出現訊息,你可以等一下。

等一下之後就會看到以下視窗,請點選確定就可以等它安裝完成。

請在下圖程式碼的位置。

接下來,請在程式中最上方的引用函式庫的位置,再加入以下這兩行程式碼。
```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事件處理做時間的比對。
如果按下取消鬧鐘的按鍵,則是會停止鬧鐘的聲響,如果鬧鐘正在發出聲音,就會關閉聲音,並且關閉鬧鐘計時器,將自己設定為不能按下,讓啟動鬧鐘按鍵可以按。
你可以測試看看,並且看看時間到了,是否發出鬧鐘聲音?

## 完整程式碼
:::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;
}
}
}
```
:::