前回作成したUDP通信コンポーネントでチャットソフトを作ってみる

前回作ったUDP通信コンポーネントの覚書として、簡単なチャットソフトを作ってみた

前回はUDP通信コンポーネントを作りツールボックスへ登録した訳だが、それを使って今度は簡単なチャットソフトを作ってみよう。

作り方ですが、新しいプロジェクトの作成でWindowsフォームアプリケーション(.NETFramework) C#を選んで新規プロジェクトを作成してください。

出来たフォームに以下のようにコントロールを配置します。

コントロール名は何でも良いのですが説明の都合上以下のように命名します。

RichTextBoxがふたつ、CheckBoxをひとつ、特に名前はそのままで配置。
ボタンは3つ、StartBtnとTXbtnとRingBtnという名前に変更します。
TextBoxは5つ用意します。名前はそれぞれRevUDP,MyUDP,URLtextBox,SendIP,SendUDP。

このフォームに前回作成したUDP通信コントロールをUDPCompo1という名前で配置します。

後は実際のコードを見れば理解し易いと思うのでそのまま記載します。

using System;
using System.Drawing;
using System.Net.Sockets;
using System.Net;
using System.Windows.Forms;

namespace WindowsFormsApp3
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //UDPポート設定 環境に合わせて変更しよう
            MyUDP.Text = "2030";
            RevUDP.Text = "2020";
            SendUDP.Text = "2020";
            // 見本用初期HOST URL
            URLtextBox.Text = "localhost";
            // URL→IP変換
            SendIP.Text = ConvertIP(URLtextBox.Text);
        }

        /// <summary>
        /// HOST名をIPAdressに変換します UDPCompoではIPAdressでの指定が必要です
        /// </summary>
        /// <param name="host"></param>
        /// <returns>adress文字列</returns>
        private string ConvertIP(string host)
        {
            try
            {
                string adr = "";
                IPHostEntry ip = Dns.GetHostEntry(host);
                foreach (IPAddress address in ip.AddressList)
                {
                    if (address.AddressFamily == AddressFamily.InterNetwork)
                        adr = address.ToString();
                }
                return adr;
            }
            catch
            {
                return "正しくないホスト名";
            }
        }

        /// <summary>
        /// 正しいIPかチェック
        /// </summary>
        /// <param name="IPStr"></param>
        /// <returns>正true, 非false</returns>
        private bool VerifyIP(string IPStr)
        {
            string ipstr = SendIP.Text;
            IPAddress ipaddr;
            return IPAddress.TryParse(ipstr, out ipaddr);
        }

        private void SendText()
        {
            //! テキストボックスから、送信するテキストを取り出す.
            String strSend = richTextBox2.Text;
            //! 送信するテキストがない場合、データ送信は行わない.
            if (string.IsNullOrEmpty(strSend) == true) return;
            // 開始されていないなら
            if (UDPCompo1.udpForReceive == null)
            {
                MessageBox.Show("まだ開始ボタンが押されていません!!", "確認!");
                return;
            }

            // IPチェック
            if (VerifyIP(SendIP.Text))
            {
                // 送信パラメーター(相手IP、自分UDP、相手UDP、送信テキストデータ)
                UDPCompo1.Send(SendIP.Text, int.Parse(MyUDP.Text), int.Parse(SendUDP.Text), strSend);
                // 送信したので入力をクリアし次の入力を待つ.
                richTextBox2.Clear();
                richTextBox2.Focus();
            }
            else
            {
                MessageBox.Show("IPが正しくありません!!", "確認!");
            }

        }

        private void StartBtn_Click(object sender, EventArgs e)
        {
            if (UDPCompo1.udpForReceive != null)
            {
                //通信中なので停止
                UDPCompo1.udpForReceive.Close();
                UDPCompo1.udpForReceive = null;
                StartBtn.BackColor = SystemColors.Control;
                StartBtn.Text = "開始する";
                StartBtn.ForeColor = SystemColors.ControlText;
            }
            else
            {
                //停止しているので開始
                StartBtn.BackColor = SystemColors.GradientActiveCaption;
                StartBtn.Text = "受信停止する";
                StartBtn.ForeColor = Color.Red;
                // MyUDP初期化 (ref RichTextBox,追加モード,IPアドレス,送信ポート,宛先ポート,受信ポート)
                if (checkBox1.Checked)
                {
                    UDPCompo1.Init(ref richTextBox1, ref richTextBox2, false, int.Parse(RevUDP.Text));
                }
                else
                {
                    UDPCompo1.Init(ref richTextBox1, ref richTextBox2, true, int.Parse(RevUDP.Text));
                }
            }

            ActiveControl = richTextBox2;

        }

        private void TextBox_KeyPress(object sender, KeyPressEventArgs e)
        {
            //EnterやEscapeキーでビープ音が鳴らないようにする
            //すべてのテキストボックスのKeyPressにこれを指定しましょう

            if (e.KeyChar == (char)Keys.Enter || e.KeyChar == (char)Keys.Escape)
            {
                e.Handled = true;
            }

        }

        private void URLtextBox_Leave(object sender, EventArgs e)
        {
            SendIP.Text = ConvertIP(URLtextBox.Text);
        }

        private void richTextBox2_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
        {
            if (e.KeyCode == Keys.Enter)
            {
                if (e.Shift)
                {
                    //shift + Enterのときは送信
                    SendText();
                }
                // ただのEnterは改行するので何もしない
            }
        }

        private void checkBox1_CheckedChanged(object sender, EventArgs e)
        {
            if (UDPCompo1.udpForReceive != null)
            {
                //通信中なので停止
                UDPCompo1.udpForReceive.Close();
                UDPCompo1.udpForReceive = null;
            }
            //表示変更
            StartBtn.BackColor = SystemColors.GradientActiveCaption;
            StartBtn.Text = "受信停止する";
            StartBtn.ForeColor = Color.Red;
                // MyUDP初期化 (ref 受信RichTextBox,ref 送信RichTextBox,追加モード,受信ポート)
            if (checkBox1.Checked)
            {
                UDPCompo1.Init(ref richTextBox1, ref richTextBox2, false, int.Parse(RevUDP.Text));
            }
            else
            {
                UDPCompo1.Init(ref richTextBox1, ref richTextBox2, true, int.Parse(RevUDP.Text));
            }
        }

        private void RingBtn_Click(object sender, EventArgs e)
        {
            // ”SND_LOGON”を送り 呼び出し音を鳴らす
            UDPCompo1.Send(SendIP.Text, int.Parse(MyUDP.Text), int.Parse(SendUDP.Text), "SEND_LOGON");
            UDPCompo1.ShowSoundOn();
        }

        private void TXbtn_Click(object sender, EventArgs e)
        {
            SendText();
        }
    }

UDPCompo1.Init(..)で開始すれば自分のUDP宛に届いたものはRichTextBoxに書き込まれます。
Initで指定するRichTextBox名は頭に必ずrefが必要なのでお忘れなく!!

送信はUDPCompo1.Send(相手IP、自分UDP、相手UDP、送信テキストデータ) で送ります。
相手のIPが正しく入力されているかのチェックをVerifyIPでチェックしてます。URLではそのまま送れないのでIPアドレスに変換もしています。

シンプルに記述も少なくて済むのでUDPコンポの使用の参考にどうぞ!!

C# Windowsショートカットをキーボード入力ではなくボタンのクリックで送る方法

キャプチャー

Windowsのスクリーンショット[Win]+[shift]+S をプログラムからボタンのクリックで実行しようとSendKeys.Sendを使おうとしたのだけど、Windowsキー+コンビネーション・キー(Shift, Ctrl, Alt)は、Windowsキー専用の定義がない??と困っていた。
Windowsキー + ← だったら、SendKeys.Send(“^{ESC}{LEFT}”) とできるので同じ様にすればと思っていたが、Windowsキーが内部的に Ctrl+ESC のコンビネーションでしか表現できないようだった。
結局、以下のような方法で対処した。

        private void button1_Click(object sender, EventArgs e)
        {
            // キャプチャするためにWindowsにキーボードショートカット( [Win]+{shift}+S )を送る方法
            InputSimulator sim = new InputSimulator();
            sim.Keyboard.ModifiedKeyStroke(new[] { VirtualKeyCode.SHIFT, VirtualKeyCode.LWIN }, VirtualKeyCode.VK_S );
        }

こうすることでスクリーンショットは可能になったが、可能になったらクリップボードの画像データを取得したい。どうするか・・

クリップボードを監視してクリップボードに変化があれば知る方法

AddClipboardFormatListener関数とRemoveClipboardFormatListener関数を使い、WM_CLIPBOARDUPDATEメッセージを捉えて予め用意したイメージボックスにデータを読み込めば良い。
実際の方法としては、フォームにpictureBox1とbutton1を配置して

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
 
    [DllImport("user32.dll", SetLastError = true)]
    private extern static void AddClipboardFormatListener(IntPtr hwnd);
 
    [DllImport("user32.dll", SetLastError = true)]
    private extern static void RemoveClipboardFormatListener(IntPtr hwnd);
 
    private const int WM_CLIPBOARDUPDATE = 0x31D;
 
    private void Form1_Load(object sender, EventArgs e)
    {
        AddClipboardFormatListener(Handle);
    }
 
    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_CLIPBOARDUPDATE)
        {
            OnClipboardUpdate();
            m.Result = IntPtr.Zero;
        }
        else
            base.WndProc(ref m);
    }
 
    private void Form1_FormClosed(object sender, FormClosedEventArgs e)
    {
        RemoveClipboardFormatListener(Handle);
    }
 
    void OnClipboardUpdate()
    {
        // クリップボードのデータが変更された時の処理
        //   PictureBoxに読み込む
        
        //クリップボードのデータを取得する
        IDataObject data = Clipboard.GetDataObject();
        //クリップボードにデータが無いときはnullを返すので
        if (data != null)
        {
            //DataFormats.Bitmapに関連付けられたデータがあれば読み込む
            if (data.GetDataPresent(DataFormats.Bitmap))
            {
                pictureBox1.Image = (Bitmap)Clipboard.GetDataObject().GetData(DataFormats.Bitmap);
            }
        }
    }
    private void button1_Click(object sender, EventArgs e)
    {
        // キャプチャするためにWindowsにキーボードショートカット( [Win]+{shift}+S )を送る方法
        InputSimulator sim = new InputSimulator();
        sim.Keyboard.ModifiedKeyStroke(new[] { VirtualKeyCode.SHIFT, VirtualKeyCode.LWIN }, VirtualKeyCode.VK_S );
    }
}

で簡単に出来るので便利だ!!

C# UDP通信のためのコンポーネントの作成

.NET Freamwork4.7を使ったUDP通信をするためのコンポーネント作成

UDP通信をするために、お決まり文句を毎回書くのは面倒で嫌なので、ツールボックスから簡単にフォームにドロップするだけで使えるようコンポーネントとして作成しました。

作成方法

<< Visual Studio 2022にて新しいプロジェクトを作成する

新しいプロジェクトの作成でWindows フォームコントロールライブラリ(.NET Framework)を選ぶ。
プロジェクト名はとりあえずNonFmUDPControlとして作成する。
Timerコンポのようにデザイナの非表示領域に追加されるコントロールにする。(GUIはいらない)
そのために、最初からあるComponentクラスは使わないことにして新たに作成する。

    << 新しいコンポーネントクラスを作成する

    ソリューションエクスプローラーのプロジェクト名の所で「右クリック」→「追加」→「新しい項目」→「コンポーネントクラス」を選んでCS名をUDPCompo.csを作成する。
    プロジェクト作成のときに最初からあるUserControl.csファイルは削除する。
    新規作成したUDPCompo.cs[デザイン]も使わないのだが何もしないでそのままにしてよい。

    << 実際のUDPCompoクラスの記述

    以下本体。最初はコピペで結構です。まずは記述して動作確認してみましょう。

    using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Windows.Forms;
    
    namespace NonFmUDPControl
    {
        public partial class UDPCompo : Component
        {
            // 各種定義
    
            //受信メッセージ格納用
            public string rcvMsg = "";
    
            //送信用クライアント
            private UdpClient udpForSend = null;
            //受信用クライアント
            public UdpClient udpForReceive = null;
            // 文末追加かどうか
            private bool bottomAppend;
            //送信フラグ
            private bool SendBool = false;
    
            // デフォルト定数
            private const int rcvPort = 2020;//とりあえず用意する受信ポート番号
    
            //コンポと外部とのデータのやり取りに使うリッチテキストボックスを用意
            // 受信文字の出力先コンポ
            public RichTextBox rcvRichText;
            // 送信用入力のTextBox
            public RichTextBox sndRichText;
    
    
    
            // 以下 コンポ本体
    
            public UDPCompo()
            {
                InitializeComponent();
            }
    
            public UDPCompo(IContainer container)
            {
                container.Add(this);
    
                InitializeComponent();
            }
    
            /// <summary>
            /// 
            /// 初期化
            /// 
            /// コンポ内と外のリッチテキストボックスを結びつけて、UDP通信初期化 と 受信開始
            /// RichTextBoxはrefを付けて参照渡しにする必要がある
            /// 
            /// </summary>
            /// <param name="rv_textBox"> 受信の吐き出し先のリッチテキストボックス名 </param>
            /// <param name="tx_textBox"> 送信文字記入用リッチテキストボックス名 </param>
            /// <param name="btmAppend">  受信の吐き出しはリッチテキストボックス 最下行に追加(true)
            ///                                                               最上部に追加(false) </param>
            /// <param name="port_rcv"> 受信ポート デフォルトはrcvPort</param>
            /// 
            public bool Init(ref RichTextBox rv_textBox,
                             ref RichTextBox tx_textBox,
                             Boolean btmAppend = true,
                             int port_rcv = rcvPort)
            {
                //UDP設定(送受信用ポートを開きつつ受信用スレッドを生成)
                try
                {
                    rcvRichText = rv_textBox;
                    sndRichText = tx_textBox;
                    bottomAppend = btmAppend;
    
                    if (udpForReceive != null)
                    {
                        udpForReceive.Close();
                        udpForReceive = null;
                    }
                    udpForReceive = new UdpClient(port_rcv); //受信用ポート
                    udpForReceive.BeginReceive(ReceiveCallback, udpForReceive);//非同期的なデータ受信を開始する
    
                    return true;
                }
                catch
                {
                    return false;
                }
            }
    
            /// <summary>
            /// 
            /// 終了処理
            ///     FormClosing等で実施すること
            ///     
            /// </summary>
            public void End()
            {
                if (udpForReceive != null) udpForReceive.Close();
                if (udpForSend != null) udpForSend.Close();
            }
    
    
            public void Send(string remoteHost, int send_Port, int send_To, string sendMsg) //文字列を送信用ポートから送信先ポートに送信
            {
                try
                {
                    SendBool = true;
                    // 送信した文字RichTextBoxに送って表示する
                    ShowReceivedString("<<" + sendMsg);
    
                    byte[] sendBytes = Encoding.UTF8.GetBytes(sendMsg);
    
                    // 送信先の作成
                    IPEndPoint Ep = new IPEndPoint(IPAddress.Parse(remoteHost), send_To);
    
                    udpForSend = new UdpClient(send_Port);// 送信元のPORT指定
    
                    // send_Toへ送信!!
                    udpForSend.Send(sendBytes, sendBytes.Length, remoteHost, send_To);
                    // 送信閉じる
                    udpForSend.Close();
                }
                catch
                {
                    MessageBox.Show("通信開始がされていないため送信出来ません!", "お知らせ");
                }
    
            }
    
            //データを受信した時
            private void ReceiveCallback(IAsyncResult ar)
            {
                //非同期受信を一旦終了する
                IPEndPoint remoteEP = null;
    
                byte[] rcvBytes;
                try
                {
                    if (ar.AsyncState is UdpClient udp)
                    {
                        rcvBytes = udp.EndReceive(ar, ref remoteEP);
    
                        //データを文字列に変換する
                        string rcvMsgStr = System.Text.Encoding.UTF8.GetString(rcvBytes);
    
                        //受信したデータと送信者の情報をRichTextBoxに表示する
                        if (remoteEP != null)
                        {
                            string displayMsg = $"[{remoteEP.Address} ({remoteEP.Port})] > \r\n{rcvMsgStr}";
                            if (rcvRichText != null)
                            {
                                SendBool = false;
                                rcvRichText.BeginInvoke(
                                    new Action<string>(ShowReceivedString), displayMsg);
                            }
                        }
                        // 呼び出し文字列が送信されていたら音を出す
                        if (rcvMsgStr == "SEND_LOGON") ShowSoundOn();
    
                        //再びデータ受信を開始する
                        udp.BeginReceive(ReceiveCallback, udp);
    
                    }
                }
                catch (SocketException)
                {
                    MessageBox.Show("受信エラー");
                    return;
                }
                catch (ObjectDisposedException)
                {
                    //すでに閉じている時は何もしない
                    return;
                }
    
            }
    
            //RichTextBox1にメッセージを表示する
            public void ShowReceivedString(string str)
            {
                // それ以外の文字は表示する
                rcvRichText.SelectionLength = 0;//選択状態を解除しておく
    
                if (bottomAppend)// 最下行に追加なら
                {
                    if (SendBool)
                    {
                        rcvRichText.SelectionColor = Color.MediumSlateBlue;
                    }
                    else
                    {
                        rcvRichText.SelectionColor = Color.Black;
                    }
    
                    rcvRichText.Focus();
                    rcvRichText.AppendText(str + "\r\n");
                    sndRichText.Focus();
                }
                else// 先頭に追加なら
                {
                    rcvRichText.Text = rcvRichText.Text.Insert(0, str + "\r\n");
    
                    if (SendBool)
                    {
                        int found = -1;
                        found = rcvRichText.Find(str, found + 1, RichTextBoxFinds.MatchCase);
                        rcvRichText.SelectionStart = found;
    
                        rcvRichText.SelectionLength = str.Length;
    
                        // そのまま表示では芸が無いので色を変える
                        rcvRichText.SelectionColor = Color.MediumSlateBlue;
                    }
                    else
                    {
                        rcvRichText.SelectionColor = Color.Black;
                    }
                }
            }
    
    
            /// <summary>
            /// 
            /// 以後、呼び出し音を再生する仕組み
            /// UDP通信とは関係ないが、相手の呼び出しに使う音源。いらないなら以下を削除して OK
            /// 
            /// 音源を用意するのが手間なのでWindowsのシステム音を使っている
            /// Flagsを使いビット演算できるよう、数値は被らないように値を設定してある。
            /// </summary>
    
            [Flags]
            public enum PlaySoundFlags : int
            {
                SND_SYNC = 0x0000,
                SND_ASYNC = 0x0001,
                SND_NODEFAULT = 0x0002,
                SND_MEMORY = 0x0004,
                SND_LOOP = 0x0008,
                SND_NOSTOP = 0x0010,
                SND_NOWAIT = 0x00002000,
                SND_ALIAS = 0x00010000,
                SND_ALIAS_ID = 0x00110000,
                SND_FILENAME = 0x00020000,
                SND_RESOURCE = 0x00040004,
                SND_PURGE = 0x0040,
                SND_APPLICATION = 0x0080
            }
            // DLL読み込み指定
            [System.Runtime.InteropServices.DllImport("winmm.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
    
            private static extern bool PlaySound(string pszSound, IntPtr hmod, PlaySoundFlags fdwSound);
    
            // 呼び出し音を鳴らす
            public void ShowSoundOn()
            {
                //Windowsログオン音を鳴らす
                PlaySound("WindowsLogon", IntPtr.Zero,
                    PlaySoundFlags.SND_ALIAS | PlaySoundFlags.SND_NODEFAULT);
    
                /* <他の音の見本 参考まで>
                //メッセージ(情報)音を鳴らす
                PlaySound("SystemAsterisk", IntPtr.Zero,
                    PlaySoundFlags.SND_ALIAS | PlaySoundFlags.SND_NODEFAULT);
                //一般の警告音を鳴らす
                PlaySound(".Default", IntPtr.Zero,
                    PlaySoundFlags.SND_ALIAS | PlaySoundFlags.SND_NODEFAULT);
                //メッセージ(警告)音を鳴らす
                PlaySound("SystemExclamation", IntPtr.Zero,
                    PlaySoundFlags.SND_ALIAS | PlaySoundFlags.SND_NODEFAULT);
                //システムエラー音を鳴らす
                PlaySound("SystemHand", IntPtr.Zero,
                    PlaySoundFlags.SND_ALIAS | PlaySoundFlags.SND_NODEFAULT);
                //メッセージ(問い合わせ)音を鳴らす
                PlaySound("SystemQuestion", IntPtr.Zero,
                    PlaySoundFlags.SND_ALIAS | PlaySoundFlags.SND_NODEFAULT);
                //Windowsの起動音を鳴らす
                PlaySound("SystemStart", IntPtr.Zero,
                    PlaySoundFlags.SND_ALIAS | PlaySoundFlags.SND_NODEFAULT);
                //Windowsの終了音を鳴らす
                PlaySound("SystemExit", IntPtr.Zero,
                    PlaySoundFlags.SND_ALIAS | PlaySoundFlags.SND_NODEFAULT);
                //Windowsログオン音を鳴らす
                PlaySound("WindowsLogon", IntPtr.Zero,
                    PlaySoundFlags.SND_ALIAS | PlaySoundFlags.SND_NODEFAULT);
                //Windowsログオフ音を鳴らす
                PlaySound("WindowsLogoff", IntPtr.Zero,
                    PlaySoundFlags.SND_ALIAS | PlaySoundFlags.SND_NODEFAULT);
                */
    
            }
    
            //
            // ここまで音を再生する仕組み
            //
    
        }
    }
    

    記述が終了したら

    << ビルドしてDLLを作成

    ビルド実施。エラーが無ければツールボックスに登録出来るDLLが作成された。

    << ツールボックスへ登録

    登録の仕方は「ツール」→「ツールボックス アイテムの選択」を選び
    「.NET FreameWorkコンポーネント」のタブで同じ名前のコンポーネントが無いことをかくにんしてから「参照」ボタンを押し、先程作成したNonFmUDPControl.dllを指定すればツールボックスに表示するようになる。

    使い方

    UDP通信をするフォームに、このコンポーネントをドロップしてください。

    フォームに送信用と受信用の2つのRichTextBoxを貼り付け
    nonIF_UDPCompo1.Init(ref richTextBox1, ref richTextBox2, false, int.Parse(RevUDPName.Text));
    のようにコンポのInitにrichTextBoxを参照渡し(ref 必須)で初期化してください。
    richTextBox1は受信用のrichTextBox
    richTextBox2は送信用のrichTextBox
    次のfalseはrichTextBoxの先頭に受信文字が追加される、trueなら末尾に受信文字が追加される
    次の数値は受信用のポート番号です。無指定で2020になります。
    送信用のrichTextBoxは、内部フォーカスの移動に使い、それ以外では何もしていません。
    送信はSend(string remoteHost, int send_Port, int send_To, string sendMsg)を使って明示的に送信してください。
    End();で終了します。

    簡単なアプリは機会があれば後日掲載します。

    あとがき

    コードはGitHubあたりに載せれば良いのだろうけれど、特別な教育を受けているわけでも無い素人には何となく敷居が高いですねぇ….気まぐれで偶然来た方の参考になれば程度で良いのです。