StreamDeckっぽいものを作る_4_コントローラーアプリ(windows版)

コントローラーはこんな感じの画面構成。とりあえずESP32で作る前にwindowsで作ってみる
StreamDeckもどきの画面(Windows版)
![123333.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/494758/e0c2670a-1153-1c7e-b8e2-3d690da6ccc6.png)
各ウィジェットは画面構成ファイルから読み取っているので編集はユーザー側で可能にしている

画面構成ファイル
```php:displaysetting.txt
<?xml version="1.0" encoding="utf-8"?>
<root>
  <item tool="button_command">
    <width>50</width>
    <height>50</height>
    <X>50</X>
    <Y>50</Y>
    <Image>.\icons\obs_scenechange.png</Image>
    <func>ButtonCommand_Click</func>
    <filepass>.\cmd1.txt</filepass>
  </item>
  <item tool="button_command">
    <width>50</width>
    <height>50</height>
    <X>150</X>
    <Y>50</Y>
    <Image>C:\Users\hoge\Desktop\dq.gif</Image>
    <func>ButtonCommand_Click</func>
    <filepass>.\cmd1.txt</filepass>
  </item>
  <item tool="slide_command">
    <width>200</width>
    <height>100</height>
    <X>50</X>
    <Y>150</Y>
    <minimum>0</minimum>
    <maximum>100</maximum>
    <orientation>horizontal</orientation>
    <requestcmd>cmd:GetActiveSpeakerVolume</requestcmd>
    <func>TrackBarCommand_Scroll</func>
    <filepass>.\cmd3.txt</filepass>
  </item>
  <item tool="folder">
    <width>50</width>
    <height>50</height>
    <X>250</X>
    <Y>50</Y>
    <Image>.\icons\twitterfolder.jpg</Image>
    <func>ButtonFolder_Click</func>
    <folderpass>folder1/</folderpass>
  </item>
</root>
```
UDPコマンドデータファイル
```php:cmd1.txt
<?xml version="1.0" encoding="utf-8"?>
<root>
	<cmd>cmd:OBSSceneChange,OBSSceneChange_Name:ps2</cmd>
</root>
```


```php:Form1.cs
using System;
using System.Diagnostics;
using System.Security.Cryptography.X509Certificates;
using System.Xml;

namespace WinFormsApp2
{
    public partial class Form1 : Form
    {
        public UDP udp = new UDP();    //UDPサーバとの送受信用
        //コマンド関数辞書オブジェクトを作成する
        delegate void Deltest(object? sender, EventArgs e);  // デリゲート宣言
        Dictionary<string, Deltest> commandfunctions = new Dictionary<string, Deltest>();

        /// <summary>
        /// フォルダ移動ボタン
        /// フォルダパスを持ったボタン
        /// </summary>
        public class ButtonFolder : Button
        {
            private string m_folderpass = ""; //フォルダパス

            public string FolderPass // m_folderpassのプロパティ
            {
                get { return m_folderpass; }
                set { m_folderpass = value; }
            }
        }

        //コントロール群
        /// <summary>
        /// コマンドボタン
        /// コマンドファイルパスを持ったボタン
        /// </summary>
        public class ButtonCommand : Button
        {
            private string m_filepass = ""; //コマンド格納ファイルパス

            public string FilePass // m_filepassのプロパティ
            {
                get { return m_filepass; }
                set { m_filepass = value; }
            }
        }

        /// <summary>
        /// コマンドトラックバー
        /// コマンドファイルパスを持ったトラックバー
        /// </summary>
        public class TrackBarCommand : TrackBar
        {
            private string m_filepass = ""; //コマンド格納ファイルパス

            public string FilePass // m_filepassのプロパティ
            {
                get { return m_filepass; }
                set { m_filepass = value; }
            }
        }

        /// <summary>
        /// コマンドボタンのクリックイベント
        /// </summary>
        /// <param name="sender">ButtonCommand</param>
        /// <param name="e">EventArgs</param>
        public void ButtonCommand_Click(object? sender, EventArgs e)
        {
            if (sender != null)
            {
                XmlReaderSettings settings = new XmlReaderSettings();
                settings.IgnoreComments = true;
                settings.IgnoreWhitespace = true;
                string filepass = ((ButtonCommand)sender).FilePass;
                XmlReader reader = XmlReader.Create(filepass, settings);

                //コマンドの読み取り
                reader.ReadStartElement("root");    //スタート位置をrootにする
                while (reader.Read())
                {
                    sendCommand(reader.Value);  //コマンド送信
                    reader.ReadToFollowing("cmd");    //次のcmdに移動する
                }
            }

            //トーストメッセージ用
            int x = this.Location.X;
            int toastwidth = 150;
            x += (this.Width / 2) - (toastwidth / 2);
            int y = this.Location.Y;
            int toastheight = 50;
            y += (this.Height) - (toastheight);

            ToastUtil.ShowToast("SendCommand",disp_y:y,disp_x:x,toast_height:toastheight,toast_width:toastwidth);
        }

        /// <summary>
        /// フォルダボタンのクリックイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void ButtonFolder_Click(object? sender, EventArgs e)
        {
            if(sender != null)
            {
                Directory.SetCurrentDirectory(Directory.GetCurrentDirectory() + "\\" + ((ButtonFolder)(sender)).FolderPass);
                showDisplay(Directory.GetCurrentDirectory() + "\\" + ConstValue.DISPLAYSETTINGFILE);
            }
        }

        /// <summary>
        /// コマンドトラックバーのスクロールイベント
        /// </summary>
        /// <param name="sender">TrackBarCommand</param>
        /// <param name="e">EventArgs</param>
        public void TrackBarCommand_Scroll(object? sender, EventArgs e)
        {
            if (sender != null)
            {
                XmlReaderSettings settings = new XmlReaderSettings();
                settings.IgnoreComments = true;
                settings.IgnoreWhitespace = true;
                string filepass = ((TrackBarCommand)sender).FilePass;
                XmlReader reader = XmlReader.Create(filepass, settings);

                //コマンドの読み取り
                reader.ReadStartElement("root");    //スタート位置をrootにする
                while (reader.Read())
                {
                    float range = ((TrackBarCommand)sender).Maximum - ((TrackBarCommand)sender).Minimum;
                    float volume = ((TrackBarCommand)sender).Value / range;
                    sendCommand(reader.Value + volume.ToString());  //コマンド送信
                    reader.ReadToFollowing("cmd");    //次のcmdに移動する
                }
            }
        }

        //メソッド群
        /// <summary>
        /// コマンドに対応したメソッドの登録
        /// コマンド関数を登録するときは定数を宣言してそれに対応する関数をここで登録する
        /// </summary>
        private void cmdFuncInit()
        {
            //コマンド関数辞書を登録
            commandfunctions.Add(ConstValue.BUTTONCOMMAND_CLICK, ButtonCommand_Click);
            commandfunctions.Add(ConstValue.BUTTONFOLDER_CLICK, ButtonFolder_Click);
            commandfunctions.Add(ConstValue.TRACKBARCOMMAND_SCROLL, TrackBarCommand_Scroll);
        }

        /// <summary>
        /// 画面の作成
        /// </summary>
        /// <param name="cnts">ボタン等のコントロール配列</param>
        private void drawDisplay(Control[] cnts)
        {
            //フォームのすべてのコントロールを削除
            this.Controls.Clear();

            //フォームにコントロールを追加
            foreach (Control cnt in cnts)
            {
                this.Controls.Add(cnt);
            }
        }

        /// <summary>
        /// コマンドをUDPで送信
        /// </summary>
        /// <param name="cmd">コマンド</param>
        private void sendCommand(string cmd)
        {
            //コマンド送信
            this.udp.Send(cmd, ConstValue.STREAMDECKLIKE_IP, ConstValue.STREAMDECKLIKE_PORT);
        }

        /// <summary>
        /// 返り値のあるコマンドをUDPで送信
        /// </summary>
        /// <param name="cmd">返り値のあるコマンド</param>
        /// <returns>コマンドの結果</returns>
        private string equestCommand(string cmd)
        {
            string result;

            //コマンド送受信
            this.udp.Send(cmd, ConstValue.STREAMDECKLIKE_IP, ConstValue.STREAMDECKLIKE_PORT);
            result = this.udp.Receive();    //タイムアウトを設定するべきな気がする！！！

            return result;
        }

        /// <summary>
        /// 構成画面を表示
        /// </summary>
        /// <param name="filepass">画面構成ファイルパス</param>
        private void showDisplay(string filepass)
        {
            Control[] ctrls;

            ctrls = xmlToControls(filepass);   //画面構成コントロールを作成
            drawDisplay(ctrls); //画面にコントロールをセット
        }

        //xml読み込み
        private Control[] xmlToControls(string filepass)
        {
            var result = new List<Control>();

            XmlReaderSettings settings = new XmlReaderSettings();
            settings.IgnoreComments = true;
            settings.IgnoreWhitespace = true;
            XmlReader reader = XmlReader.Create(filepass, settings);

            //rootエレメントまで読み飛ばす
            reader.ReadStartElement("root");    //スタート位置をrootにする

            do
            {
                if (reader.Name.Equals("item"))
                {
                    if (reader.MoveToFirstAttribute())
                    {
                        switch (reader.Value)
                        {
                            case "button_command":  //コマンドボタン
                                ButtonCommand cmdbtn = new();
                                reader.ReadToFollowing("width");
                                reader.Read();
                                cmdbtn.Width = int.Parse(reader.Value);
                                reader.ReadToFollowing("height");
                                reader.Read();
                                cmdbtn.Height = int.Parse(reader.Value);
                                reader.ReadToFollowing("X");
                                reader.Read();
                                int pos_x = int.Parse(reader.Value);
                                reader.ReadToFollowing("Y");
                                reader.Read();
                                int pos_y = int.Parse(reader.Value);
                                cmdbtn.Location = new System.Drawing.Point(pos_x, pos_y);
                                reader.ReadToFollowing("Image");
                                reader.Read();
                                cmdbtn.BackgroundImage = System.Drawing.Image.FromFile(reader.Value);
                                cmdbtn.BackgroundImageLayout = ImageLayout.Stretch;
                                reader.ReadToFollowing("func");
                                reader.Read();
                                cmdbtn.Click += new EventHandler(commandfunctions[reader.Value]);
                                reader.ReadToFollowing("filepass");
                                reader.Read();
                                cmdbtn.FilePass = reader.Value;
                                result.Add(cmdbtn);
                                break;
                            case "slide_command":   //コマンドトラックバー
                                TrackBarCommand cmdtkb = new();
                                reader.ReadToFollowing("width");
                                reader.Read();
                                cmdtkb.Width = int.Parse(reader.Value);
                                reader.ReadToFollowing("height");
                                reader.Read();
                                cmdtkb.Height = int.Parse(reader.Value);
                                reader.ReadToFollowing("X");
                                reader.Read();
                                int tkbpos_x = int.Parse(reader.Value);
                                reader.ReadToFollowing("Y");
                                reader.Read();
                                int tkbpos_y = int.Parse(reader.Value);
                                cmdtkb.Location = new System.Drawing.Point(tkbpos_x, tkbpos_y);
                                reader.ReadToFollowing("minimum");
                                reader.Read();
                                cmdtkb.Minimum = int.Parse(reader.Value);
                                reader.ReadToFollowing("maximum");
                                reader.Read();
                                cmdtkb.Maximum = int.Parse(reader.Value);
                                reader.ReadToFollowing("orientation");
                                reader.Read();
                                if (reader.Value.Equals("horizontal"))
                                    cmdtkb.Orientation = Orientation.Horizontal;
                                else
                                    cmdtkb.Orientation = Orientation.Vertical;
                                reader.ReadToFollowing("requestcmd");
                                reader.Read();
                                float range = cmdtkb.Maximum- cmdtkb.Minimum;
                                int value =(int)(float.Parse(equestCommand(reader.Value)) * range);
                                //cmdtkb.Value = int.Parse(equestCommand(reader.Value));
                                cmdtkb.Value = value;
                                reader.ReadToFollowing("func");
                                reader.Read();
                                cmdtkb.Scroll += new EventHandler(commandfunctions[reader.Value]);
                                //btn.Click += new EventHandler(eventfunctest);
                                reader.ReadToFollowing("filepass");
                                reader.Read();
                                cmdtkb.FilePass = reader.Value;
                                cmdtkb.TickStyle = TickStyle.None;  //目盛りなし
                                result.Add(cmdtkb);
                                break;
                            case "folder":  //フォルダー
                                ButtonFolder folderbtn = new();
                                reader.ReadToFollowing("width");
                                reader.Read();
                                folderbtn.Width = int.Parse(reader.Value);
                                reader.ReadToFollowing("height");
                                reader.Read();
                                folderbtn.Height = int.Parse(reader.Value);
                                reader.ReadToFollowing("X");
                                reader.Read();
                                int folderbtn_pos_x = int.Parse(reader.Value);
                                reader.ReadToFollowing("Y");
                                reader.Read();
                                int folderbtn_pos_y = int.Parse(reader.Value);
                                folderbtn.Location = new System.Drawing.Point(folderbtn_pos_x, folderbtn_pos_y);
                                reader.ReadToFollowing("Image");
                                reader.Read();
                                folderbtn.BackgroundImage = System.Drawing.Image.FromFile(reader.Value);
                                folderbtn.BackgroundImageLayout = ImageLayout.Stretch;
                                reader.ReadToFollowing("func");
                                reader.Read();
                                folderbtn.Click += new EventHandler(commandfunctions[reader.Value]);
                                reader.ReadToFollowing("folderpass");
                                reader.Read();
                                folderbtn.FolderPass = reader.Value;
                                result.Add(folderbtn);
                                break;
                        }
                    }
                }
            } while (reader.Read());

            return result.ToArray();
        }


        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            this.udp.init(ConstValue.SEND_PORT, ConstValue.RECEIVE_PORT);   //UDPクラスの初期化
            cmdFuncInit();  //コマンド関数辞書の作成

            showDisplay(Directory.GetCurrentDirectory() + "\\" + ConstValue.DISPLAYSETTINGFILE);    //画面構成の表示
        }

        private void button1_Click(object sender, EventArgs e)
        {
            this.udp.Send("cmd:GetActiveSpeakerVolume",ConstValue.STREAMDECKLIKE_IP, ConstValue.STREAMDECKLIKE_PORT);
            label1.Text = this.udp.Receive();
        }

        private void Form1_Shown(object sender, EventArgs e)
        {
            //Control[] ctrls;

            //ctrls = xmlToControls(@".\displaysetting.txt");   //画面構成コントロールを作成
            //drawDisplay(ctrls); //画面にコントロールをセット
        }
    }
}
```


アンドロイドのToastっぽい感じで動くやつ
```php:ToastUtil.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Security.Policy;
using System.Text;
using System.Threading.Tasks;


namespace WinFormsApp2
{
    /// <summary>
    /// Android風のトーストクラス
    /// </summary>
    internal class ToastUtil
    {
        private static void fadein(int msec, Form target)
        {
            System.Windows.Forms.Timer timer = new();
            timer.Interval = 25;
            double freq = (msec/2.0) / timer.Interval;

            target.Opacity = 0.0;
            target.Visible = true;

            int count = 0;
            timer.Tick += (sender, e) =>
            {
                if (count > freq)
                {
                    timer.Stop();
                    fadekeep(msec, target);
                    return;
                }
                count++;
                target.Opacity = (double)(count / freq);
            };
            timer.Start();
        }

        private static void fadekeep(int msec, Form target)
        {
            System.Windows.Forms.Timer timer = new();
            timer.Interval = 25;
            double freq = msec / timer.Interval;

            int count = 0;
            timer.Tick += (sender, e) =>
            {
                if (count > freq)
                {
                    timer.Stop();

                    fadeout(msec, target);
                    return;
                }
                count++;
            };
            timer.Start();
        }

        private static void fadeout(int msec, Form target)
        {
            System.Windows.Forms.Timer timer = new();
            timer.Interval = 25;
            double freq = (msec/2.0) / timer.Interval;

            int count = 0;
            timer.Tick += (sender, e) =>
            {
                if (count > freq)
                {
                    timer.Stop();
                    target.Close();
                    return;
                }
                count++;
                target.Opacity = 1.0 - (double)(count / freq);
            };
            timer.Start();
        }

        public static void ShowToast(string msg, int msec=1000,int toast_height=50,int toast_width=150,int disp_x=0,int disp_y=0)
        {
            Form toastform = new();
            //toastform.TopMost= true;  //常に最前へ
            toastform.AutoScaleMode = AutoScaleMode.None;
            toastform.StartPosition = FormStartPosition.Manual;
            toastform.Padding = new Padding(0);
            toastform.Opacity = 0;
            toastform.Width = toast_width;
            toastform.Height = toast_height;
            Point formpoint = new(disp_x,disp_y);
            toastform.Location = formpoint;

            Label msglabel = new();
            msglabel.AutoSize = false;
            msglabel.TextAlign = ContentAlignment.MiddleCenter;
            msglabel.Width = toastform.Width;
            msglabel.Text = msg;
            int y = (toastform.Height / 2) - (msglabel.Height / 2);
            int x = 0;
            Point p = new(x, y);
            msglabel.Location = p;

            toastform.FormBorderStyle = FormBorderStyle.None;
            toastform.Controls.Add(msglabel);

            toastform.Load += (e, args) =>
            {
                fadein(msec, toastform);
            };
            toastform.Show();
        }

    }
}
```

定数関係
```php:ConstValue.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WinFormsApp2
{
    static class ConstValue
    {
        //UDP関係
        public const int SEND_PORT = 50000;  //送信元ポート
        public const int RECEIVE_PORT = 50001;  //受信用ポート
        public const int STREAMDECKLIKE_PORT = 50007;  //サーバ側ポート
        //public const string STREAMDECKLIKE_IP = "127.0.0.1"; //サーバ側IPアドレス
        public const string STREAMDECKLIKE_IP = "192.168.1.70"; //サーバ側IPアドレス

        //コマンド関数辞書関係
        public const string BUTTONCOMMAND_CLICK = "ButtonCommand_Click";
        public const string BUTTONFOLDER_CLICK = "ButtonFolder_Click";
        public const string TRACKBARCOMMAND_SCROLL = "TrackBarCommand_Scroll";

        //
        public const string DISPLAYSETTINGFILE = @"displaysetting.txt"; //画面構成ファイル名
    }
}
```


UDP関係のクラス
```php:UDP.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading.Tasks;

namespace WinFormsApp2
{
    /****
    UDP通信クラス
    newしてinitして終わりはend
    ・UDP送信 Send
    ・UDP受信 Receive
    ・終了処理　End
    ****/
    public class UDP
    {
        private UdpClient udpForSend; //送信用クライアント
        private int remotePort;//送信先のポート

        private UdpClient udpForReceive; //受信用クライアント

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public UDP()
        {
        }

        /// <summary>
        /// 初期化処理
        /// </summary>
        /// <param name="port_snd">送信元ポート</param>
        /// <param name="port_rcv">受信用ポート</param>
        /// <returns></returns>
        public bool init(int port_snd, int port_rcv) //UDP設定（送受信用ポートを開きつつ受信用スレッドを生成）
        {
            try
            {
                udpForSend = new UdpClient(port_snd); //送信用ポート
                udpForReceive = new UdpClient(port_rcv); //受信用ポート
                return true;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// データ送信
        /// </summary>
        /// <param name="sendMsg">送信データ</param>
        /// <param name="remotehostip">送信先IP</param>
        /// <param name="remoteport">送信先ポート</param>
        public void Send(string sendmsg, string remotehostip, int remoteport) //文字列を送信用ポートから送信先ポートに送信
        {
            try
            {
                byte[] sendBytes = Encoding.ASCII.GetBytes(sendmsg);
                udpForSend.Send(sendBytes, sendBytes.Length, remotehostip, remoteport);
            }
            catch { }
        }

        /// <summary>
        /// 受信したデータを文字列で返す
        /// </summary>
        /// <returns>受信したデータを文字列に変換したもの</returns>
        public string Receive() //受信スレッドで実行される関数
        {
            string result;
            IPEndPoint remoteEP = null;//任意の送信元からのデータを受信
            byte[] rcvBytes = udpForReceive.Receive(ref remoteEP);
            result = Encoding.ASCII.GetString(rcvBytes);

            return result;
        }

        /// <summary>
        /// 終了処理
        /// </summary>
        public void End() //送受信用ポートを閉じつつ受信用スレッドも廃止
        {
            try
            {
                udpForReceive.Close();
                udpForSend.Close();
            }
            catch { }
        }
    }
}
```