建立Windows Service 下卷

  • 3462
  • 0

如果你的程式需要系統沒有登入時就執行,也希望避免被誤關,就需要寫Windows Service.本篇介紹除錯,資源檔和如何在系統欄做一個狀態程式去控制你的服務.

接上一篇建立Windows Service上卷,現在我們來看如何加入設定,資源檔和控制服務.

設定檔

App.config是.NET 1.0就有的設定檔,其實一直都.NET3.5都還在用,只是加入了.settings和其讀取類別更換成ConfigurationManager.在這裡先解釋一下App.config和.settings的差別在那裡.如果有適當的權限,兩者都是可以透過程式去做存取的.其實.settings是由Visual Studio幫你在App.config建立SectionGroup和Section,並且把相關的程式碼都設定好.而App.config寫入則需要自己動手處理,也是可以建立自訂的SectionGroup和Section.所以.settings其實是Visual Studio提供的一個工具.

自訂Section會放在另一篇講解.這裡會解釋一下.settings.其實每個application會有預設的一個.settings.在方案總管->專案->Right Click選屬性,然後在主頁選擇設定,點中間的建立設定檔就可以.當然你也可以在專案Right Click加入其他.settings

接下來看到的畫面,名稱就是該設定值的名稱,型別是值的強型別,值就是設定值.這裡最重要的就是範圍,如果範圍設定成Application,那麼同一台電腦上的不同使用者,就會使用相同的這個設定值,而且不能用程式修改.如果範圍是User的話,就可以用程式修改值,並且每個使用者有自己的設定,這個部份的.config會放在每個使用者的Application Data資料夾裡.所以如果要修改所有User的設定值時,就要對每位使用者一個一個.config的改,又或者按左上方的[同步處理].

要使用該設定值時就用下面的程式碼,其他.settings也是用類似的方法

//讀取
ProjectName.Properties.Settings.Default.SettingName;
//寫入
ProjectName.Properties.Settings.Default.SettingName = xxx;
ProjectName.Properties.Settings.Default.Save();

資源檔

通常我會用.resx去放一些整個程式會共用到的字串,也可以用來做多國語言,這樣修改時才不會造成只有部份有改到的情況,在專案Right Click加入一個資源檔.資源檔基本上只是一個key-value的對應.讀取時就用下面的程式碼

ResourceManager manager = new ResourceManager("Namespace.ResxFileName", Assembly.GetExecutingAssembly());
manager.GetString("key");

小結
設定檔和資源檔都包含在安裝專案的內容檔裡,不用另外設定,如果有多國語言的話,可能要在安裝專案加入輸出時一併加入.

控制程式

接下來就是本篇的主餐,要寫另一個程式來對該服務做控制,因為常用的服務通常不會打開控制台來設定這麼麻煩,而且這裡還會用IPC去要求服務把供一些其他資訊.

基本上要達成這些的需求: 啟動時在System Tray顯示圖示,可以直接控制服務,Double Click要有介面對服務下指令,按[X]時會回到System Tray.

需要元件: ServiceController, NotifyIcon, ImageList, ContextMenuStrip, MenuStrip

ServiceController是用來控制服務的,要把ServiceName設定成你想控制的服務的名稱.

NotifyIcon是用來在System Tray右邊顯示小圖示的,但不用設定,我會把圖片集中在ImageList,然後在程式裡設定.

ImageList是用來存放圖片資源的,分別用在NotifyIcon和Menu等.

ContextMenuStrip讓使用者在小圖示上Right Click就可以控制服務

MenuStrip在顯示介面的模式提供跟ContextMenuStrip同樣的功能

1. 最小化

先把NotifyIcon的Visible設成True,加入NotifyIcon_DoubleClick的處理方法

        private void nicSystemTray_DoubleClick(object sender, EventArgs e)
        {
            ShowFormMain();
        }

        private void ShowFormMain()
        {
            this.Show();
            this.WindowState = FormWindowState.Normal;
            //this.ShowInTaskbar = true;
        }

這裡有一個重點,千萬不能用ShowInTaskbar = true,因為這個值是唯讀的,但不知道為何compile會過,記得不能用.接下來把Form的WindowState設定成Minimized,並加入Form_Resize和Form_Closing的處理方法

        private void FormMain_Resize(object sender, EventArgs e)
        {
            if (this.WindowState == FormWindowState.Minimized)
            {
                this.Hide();
                //ShowInTaskbar is read-only
                //this.ShowInTaskbar = false;
            }
        }

        //從介面模式按[x]會縮小到System Tray,在縮小模式才真的Close
        private void FormMain_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (this.WindowState != FormWindowState.Minimized)
            {
                e.Cancel = true;
                this.Hide();
                this.WindowState = FormWindowState.Minimized;
            }
        }

        private void FormMain_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (nicSystemTray != null)
            {
                nicSystemTray.Visible = false;
                nicSystemTray.Dispose();
                nicSystemTray = null;
            }
        }

這個FormClosed的方法是用來處理程式已關閉可是小圖示還是留在System Tray的奇怪現像的.

2. 控制服務

加入開始,停止,關閉三個Item到MenuStrip和ContextMenuStrip,使用相同的Click處理方法.我這裡用同步方式去控制Service,你可以用非同步的.

private void startToolStripMenuItem_Click(object sender, EventArgs e)
        {
            try
            {
                TimeSpan l_timeout = TimeSpan.FromMilliseconds(30 * 1000);
                svcReadLogger.Start();
                svcReadLogger.WaitForStatus(System.ServiceProcess.ServiceControllerStatus.Running, l_timeout);
            }
            catch
            {
                MessageBox.Show("服務無法啟動,請檢查相關設定!");
            }
            svcReadLogger.Refresh();
            UpdateContextMenuStatus(svcReadLogger.Status);
        }

        private void stopToolStripMenuItem_Click(object sender, EventArgs e)
        {
            try
            {
                TimeSpan l_timeout = TimeSpan.FromMilliseconds(60 * 1000);
                if (svcReadLogger.Status != System.ServiceProcess.ServiceControllerStatus.Stopped
                    && svcReadLogger.Status != System.ServiceProcess.ServiceControllerStatus.StopPending)
                {
                    svcReadLogger.Stop();
                    svcReadLogger.WaitForStatus(System.ServiceProcess.ServiceControllerStatus.Stopped, l_timeout);
                }
            }
            catch
            {
                MessageBox.Show("服務無法停用,建議您可以先利用「工作管理員」將 Service1.exe 程序結束,再進行解除安裝。");
            }
            svcReadLogger.Refresh();
            UpdateContextMenuStatus(svcReadLogger.Status);
        }

這裡的svcReadLogger就是我的ServiceController物件.先定好一個timeout的TimeSpan,Windows預設是30秒,我們這裡也用30秒,如果等了30秒後服務沒有啟動成功,就會放棄.WaitForStatus會用同步方式等到服務啟動成功為止.UpdateContextMenuStatus方法是用來更新圖示的,下面會再詳細講.停止服務基本上跟啟動服務一模一樣.

3. 圖示

對不同服務狀態顯示不同圖示和Disable不同Menu Item.先在ImageList的集合加入一些小圖示.然後我每次變更服務狀態都會呼叫一次UpdateContextMenuStatus,你也可以用Event來做

        private void UpdateContextMenuStatus(ServiceControllerStatus status)
        {
            switch (status)
            {
                case ServiceControllerStatus.Running:
                    nicSystemTray.Icon = Icon.FromHandle(((Bitmap)imlTrayIcon.Images[0]).GetHicon());
                    功能ToolStripMenuItem.DropDownItems[0].Enabled = cmsShortCutMenu.Items[0].Enabled = false;
                    功能ToolStripMenuItem.DropDownItems[1].Enabled = cmsShortCutMenu.Items[1].Enabled = true;
                    break;
                case ServiceControllerStatus.Stopped:
                    nicSystemTray.Icon = Icon.FromHandle(((Bitmap)imlTrayIcon.Images[1]).GetHicon());
                    功能ToolStripMenuItem.DropDownItems[0].Enabled = cmsShortCutMenu.Items[0].Enabled = true;
                    功能ToolStripMenuItem.DropDownItems[1].Enabled = cmsShortCutMenu.Items[1].Enabled = false;
                    break;
                default:  //error
                    break;
            }
        }

這裡用Icon.FromHandle()方法從ImageList取得圖示.MenuItem[0]為啟動服務的選項,MenuItem[1]為停止服務的選項.最後加入一個ContextMenuStrip的MenuItem為小圖示Right Click時選退出才會真正的退出服務控制程式.

        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            this.WindowState = FormWindowState.Minimized;
            this.Close();
        }

4. 捷徑

一般安裝程式都會加入程式捷徑,用上一篇介紹的方式建立好安裝專案,最後在主要輸出的設計畫面,選擇使用者桌面或使用者程式功能表,或者其他你自建的資料夾,在空白的地方Right Click->建立新捷徑,記得這裡是選目標程式,所以要選應用程式資料夾,選主要輸出,按確定就可以了.

總結

程式很簡單但加上圖示後效果還不錯的.大家可以試一試.

已停止

已啟用

控制服務

My WP Blog with english technical docs.