HeyCHのブログ

慢性疲労のへいちゃんです

【C#】自動シャットダウンツールの作成

前回の続きで、タスクトレイに常駐するアプリに自動シャットダウン等の機能を追加していきます。
heych.hatenablog.com
の続きです。

機能

  • 指定した時間にシャットダウンする
  • シャットダウンする際、起動中のアプリをできるだけ終了する
  • シャットダウンする前に、シャットダウンをキャンセルできる画面を表示する(20秒経過後自動で閉じる)
  • シャットダウンする時刻を設定できる画面を表示する
  • タスクトレイのアイコンの右クリックメニューに機能を設定する

シャットダウンする関数

shutdown.exeを使用してPCをシャットダウンします。コマンドラインオプションは下記リンク参照
shutdown | Microsoft Docs

        static void shutdown() {
            ProcessStartInfo psi = new ProcessStartInfo();
            psi.FileName = "shutdown.exe";
            psi.Arguments = "/s /f";
            psi.UseShellExecute = false;
            psi.CreateNoWindow = true;
            Process.Start(psi);
        }

起動中のアプリをできるだけ終了する関数

ここではメインウィンドウを持っているアプリを閉じる処理を行い、最大5分待機します

        static void waitForCloseWindows() {
            var ignoreProcesses = new[] { "TextInputHost", "ApplicationFrameHost", "SystemSettings" };//何故か起動してないCalcurator(電卓)が出てくるが放置
            var closeList = new List<Process>();
            foreach (var p in Process.GetProcesses().Where(pr=>!ignoreProcesses.Contains(pr.ProcessName))) {
                if (p.MainWindowHandle != IntPtr.Zero && !string.IsNullOrEmpty(p.MainWindowTitle)) {
                    //Debug.WriteLine(p.ProcessName);
                    p.CloseMainWindow();
                    closeList.Add(p);
                }
            }
            //5分待っても終了しない場合kill←killは良くないっぽい
            var st = DateTime.Now;
            while (closeList.Any(p => p.HasExited)) {
                if (DateTime.Now - st > new TimeSpan(0, 5, 0)) break;
                Task.Delay(1000);
            }
            //foreach (var p in closeList) {
            //    if (!p.HasExited) p.Kill();
            //}
        }

設定時刻を記憶する設定の追加

  • ソリューションエクスプローラーのプロジェクト(Shutdowner)を右クリックしてプロパティを表示
  • 設定を選択し、「ここをクリックしてください。」的なところをクリックして設定を追加
  • 「Hours」「Minutes」「SettingTime」をそれぞれ、「string」「string」「System.DateTime」で追加し、「Hours」には「5」「Minutes」には「10」と適当な初期値を入れておく

時刻設定画面の追加

  • ソリューションエクスプローラーのプロジェクトを右クリックし、追加>Windowsフォームを選択(元からあるForm1を使ってもOK)し、「SettingForm」と名前を付ける
f:id:HeyCH:20210710145005p:plain
SettingForm
  • 上図のような感じでデザインし、「Icon」をリソースの画像に設定し、「FormBorderStyle」を「FixedDialog」に設定
  • また、両方のTextBox(時間の方がtextBox1)に「TextChanged」イベントを追加し、OKボタンにも「Click」イベントを追加
        public SettingForm() {
            InitializeComponent();

            textBox1.Text = Properties.Settings.Default.Hours;
            textBox2.Text = Properties.Settings.Default.Minutes;
        }
        private void button1_Click(object sender, EventArgs e) {
            var t0 = DateTime.Now;
            try {
                var t1 = new DateTime(t0.Year, t0.Month, t0.Day, int.Parse(textBox1.Text), int.Parse(textBox2.Text), 0);
                Properties.Settings.Default.Hours = textBox1.Text;
                Properties.Settings.Default.Minutes = textBox2.Text;
                Properties.Settings.Default.SettingTime = DateTime.Now;
                Properties.Settings.Default.Save();
                this.Close();
            } catch {
                MessageBox.Show("Could not set time.");
            }
        }

        Regex r = new Regex("[^\\d]");
        private void textBox1_TextChanged(object sender, EventArgs e) {
            if (r.IsMatch(textBox1.Text)) textBox1.Text = r.Replace(textBox1.Text, "");
            if (string.IsNullOrEmpty(textBox1.Text)) textBox1.Text = "0";
            var val = int.Parse(textBox1.Text);
            if (val < 0) val = 0;
            else if (val > 23) val = 23;
            textBox1.Text = val.ToString();
        }

        private void textBox2_TextChanged(object sender, EventArgs e) {
            if (r.IsMatch(textBox2.Text)) textBox2.Text = r.Replace(textBox2.Text, "");
            if (string.IsNullOrEmpty(textBox2.Text)) textBox2.Text = "0";
            var val = int.Parse(textBox2.Text);
            if (val < 0) val = 0;
            else if (val > 59) val = 59;
            textBox2.Text = val.ToString();
        }

20秒待機してキャンセルされたかどうかを判断する画面の追加

  • 先ほどと同じようにして「ConfirmForm」を追加
f:id:HeyCH:20210710150546p:plain
ConfirmForm
  • 上図のようなデザイン(20sは「label2」というラベル)で、先ほどと同じように「Icon」と「FormBorderStyle」を設定し、Formの「Activated」イベントを追加
  • Cancelボタンに「Click」イベントを追加
        int waitTime = 20;
        public ConfirmForm() {
            InitializeComponent();

            this.DialogResult = DialogResult.Cancel;
        }

        private void button1_Click(object sender, EventArgs e) {
            this.Close();
        }

        private void ConfirmForm_Activated(object sender, EventArgs e) {
            //コンストラクタでやるとカウントが開始されない場合がある
            //20秒待ってCancelボタンを押さなければDialogResult.OKを返す
            Task.Run(async () => {
                for (int t = waitTime; t >= 0; t--) {
                    this.Invoke((MethodInvoker)delegate () { label2.Text = t + "s"; });
                    await Task.Delay(1000);
                }
                this.Invoke((MethodInvoker)delegate () {
                    this.DialogResult = DialogResult.OK;
                    this.Close();
                });
            });
        }

こんな風にしておけば、「ShowDialog」したときの戻り値が、何もしない場合「DialogResult.OK」、キャンセルボタンや×を押した場合「DialogResult.Cancel」となる

終了処理

        static bool executeShutdownProcess() {
            //確認画面表示
            var cform = new ConfirmForm();
            if (cform.ShowDialog() == DialogResult.OK) {
                //メイン画面を持つプロセスの終了
                waitForCloseWindows();
                //Shutdown処理
                shutdown();
                //終了
                return true;
            } else {
                return false;
            }
        }

右クリックメニューの追加

            //右クリックメニュー
            ContextMenuStrip cms = new ContextMenuStrip();
            ToolStripMenuItem tsmi1 = new ToolStripMenuItem("Exit");//終了
            tsmi1.Click += (s, e) => {
                survive = false;
                nicon.Dispose();
                Application.Exit();
            };
            ToolStripMenuItem tsmi2 = new ToolStripMenuItem("Shutdown now!");//今すぐシャットダウン
            tsmi2.Click += (s, e) => {
                executeShutdownProcess();
            };
            ToolStripMenuItem tsmi3 = new ToolStripMenuItem("Setting");//設定画面
            tsmi3.Click += (s, e) => {
                if (sform == null) {
                    sform = new SettingForm();
                    sform.FormClosed += (s, e) => { sform = null; };
                }
                sform.Show();
            };
            cms.Items.Add(tsmi3);
            cms.Items.Add(tsmi2);
            cms.Items.Add(tsmi1);
            nicon.ContextMenuStrip = cms;

シャットダウン待機タスク

            Task.Run(async () => {
                var t0 = DateTime.Now;
                DateTime t1 = new(0);
                while (t1.Year < t0.Year) {
                    try {
                        t1 = new DateTime(t0.Year, t0.Month, t0.Day, int.Parse(Properties.Settings.Default.Hours), int.Parse(Properties.Settings.Default.Minutes), 0);
                    } catch {
                        //設定時間でエラーが出たら設定画面を出す(設定ファイルを直接いじったりしたとき用)
                        if (sform == null) {
                            sform = new SettingForm();
                            sform.FormClosed += (s, e) => { sform = null; };
                        }
                        sform.ShowDialog();
                    }
                }
                while (survive) {
                    t0 = DateTime.Now;
                    //設定時間変更確認
                    var t2= new DateTime(t0.Year, t0.Month, t0.Day, int.Parse(Properties.Settings.Default.Hours), int.Parse(Properties.Settings.Default.Minutes), 0);
                    if (t2.Hour != t1.Hour || t2.Minute != t1.Minute) {
                        t1 = t2;
                        if (Properties.Settings.Default.SettingTime > t1) t1 = t1.AddDays(1);//設定時刻を設定した時刻より早ければ、明日実行する
                    }
                    //設定時間を現在時間が超えていても、1時間以上離れていた場合は明日にする
                    if (t0 > t1 && (t0 - t1) > new TimeSpan(1, 0, 0)) t1 = t1.AddDays(1);
                    //設定時間を超えたらシャットダウン
                    if (t0 >= t1) {
                        if (executeShutdownProcess()) break;
                        else t1 = t1.AddDays(1);
                    }
                    await Task.Delay(60000);
                }
            });

Program.cs全部

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Shutdowner {
    static class Program {
        static NotifyIcon nicon;
        static SettingForm sform;
        static bool survive = true;
        /// <summary>
        ///  The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main() {
            Application.SetHighDpiMode(HighDpiMode.SystemAware);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            //Application.Run(new Form1());

            nicon = new NotifyIcon();
            nicon.Icon = Shutdowner.Properties.Resources.Shutdowner;
            nicon.Text = "Shutdowner";
            nicon.Visible = true;
            //コンテキストメニュークリックしたときにも表示されたので止め
            //nicon.MouseClick += (s,e) => {
            //    if (e.Button == MouseButtons.Left) {
            //        if (sform == null) {//form1をcloseしてもnullにはならないのでイベントでnullにしてやる
            //            sform = new SettingForm();
            //            sform.FormClosed += (s, e) => { sform = null; };
            //        }
            //        sform.Show();
            //    }
            //};

            //右クリックメニュー
            ContextMenuStrip cms = new ContextMenuStrip();
            ToolStripMenuItem tsmi1 = new ToolStripMenuItem("Exit");//終了
            tsmi1.Click += (s, e) => {
                survive = false;
                nicon.Dispose();
                Application.Exit();
            };
            ToolStripMenuItem tsmi2 = new ToolStripMenuItem("Shutdown now!");//今すぐシャットダウン
            tsmi2.Click += (s, e) => {
                executeShutdownProcess();
            };
            ToolStripMenuItem tsmi3 = new ToolStripMenuItem("Setting");//設定画面
            tsmi3.Click += (s, e) => {
                if (sform == null) {
                    sform = new SettingForm();
                    sform.FormClosed += (s, e) => { sform = null; };
                }
                sform.Show();
            };
            cms.Items.Add(tsmi3);
            cms.Items.Add(tsmi2);
            cms.Items.Add(tsmi1);
            nicon.ContextMenuStrip = cms;

            Task.Run(async () => {
                var t0 = DateTime.Now;
                DateTime t1 = new(0);
                while (t1.Year < t0.Year) {
                    try {
                        t1 = new DateTime(t0.Year, t0.Month, t0.Day, int.Parse(Properties.Settings.Default.Hours), int.Parse(Properties.Settings.Default.Minutes), 0);
                    } catch {
                        //設定時間でエラーが出たら設定画面を出す(設定ファイルを直接いじったりしたとき用)
                        if (sform == null) {
                            sform = new SettingForm();
                            sform.FormClosed += (s, e) => { sform = null; };
                        }
                        sform.ShowDialog();
                    }
                }
                while (survive) {
                    t0 = DateTime.Now;
                    //設定時間変更確認
                    var t2= new DateTime(t0.Year, t0.Month, t0.Day, int.Parse(Properties.Settings.Default.Hours), int.Parse(Properties.Settings.Default.Minutes), 0);
                    if (t2.Hour != t1.Hour || t2.Minute != t1.Minute) {
                        t1 = t2;
                        if (Properties.Settings.Default.SettingTime > t1) t1 = t1.AddDays(1);//設定時刻を設定した時刻より早ければ、明日実行する
                    }
                    //設定時間を現在時間が超えていても、1時間以上離れていた場合は明日にする
                    if (t0 > t1 && (t0 - t1) > new TimeSpan(1, 0, 0)) t1 = t1.AddDays(1);
                    //設定時間を超えたらシャットダウン
                    if (t0 >= t1) {
                        if (executeShutdownProcess()) break;
                        else t1 = t1.AddDays(1);
                    }
                    await Task.Delay(60000);
                }
            });

            Application.Run();
        }

        static bool executeShutdownProcess() {
            //確認画面表示
            var cform = new ConfirmForm();
            if (cform.ShowDialog() == DialogResult.OK) {
                //メイン画面を持つプロセスの終了
                waitForCloseWindows();
                //Shutdown処理
                shutdown();
                //終了
                return true;
            } else {
                return false;
            }
        }

        static void waitForCloseWindows() {
            var ignoreProcesses = new[] { "TextInputHost", "ApplicationFrameHost", "SystemSettings" };//何故か起動してないCalcurator(電卓)が出てくるが放置
            var closeList = new List<Process>();
            foreach (var p in Process.GetProcesses().Where(pr=>!ignoreProcesses.Contains(pr.ProcessName))) {
                if (p.MainWindowHandle != IntPtr.Zero && !string.IsNullOrEmpty(p.MainWindowTitle)) {
                    //Debug.WriteLine(p.ProcessName);
                    p.CloseMainWindow();
                    closeList.Add(p);
                }
            }
            //5分待っても終了しない場合kill←killは良くないっぽい
            var st = DateTime.Now;
            while (closeList.Any(p => p.HasExited)) {
                if (DateTime.Now - st > new TimeSpan(0, 5, 0)) break;
                Task.Delay(1000);
            }
            //foreach (var p in closeList) {
            //    if (!p.HasExited) p.Kill();
            //}
        }
        static void shutdown() {
            ProcessStartInfo psi = new ProcessStartInfo();
            psi.FileName = "shutdown.exe";
            psi.Arguments = "/s /f";
            psi.UseShellExecute = false;
            psi.CreateNoWindow = true;
            Process.Start(psi);
        }
    }
}

GitHub - Hey-CH/Shutdowner: 指定した時間にPCをシャットダウンするツール(常駐)