CG Library SkiaSharp「ニコニコ動画風メッセージ」

<B>
<font COLOR="blue" >
・----------------------------------------------------------------------------------------・<BR>
・ソケットでメッセージを送り、相手のデスクトップ画面に横スクロール表示します。<BR>
・「ニコニコ動画」みたいな感じです。動画1,2は物は同じです。<BR>
・----------------------------------------------------------------------------------------・<BR>
</B>

:::note info
大体の仕組み
- 送信アプリで速度、色等を選んで指定したIPアドレスの端末にDGRAMで速度等のパラメータを追加した電文を送信。<BR>
- 表示アプリで画面を最大化し透過させ、受信した電文により表示データ毎に構造体の配列に入れる。メンバ変数は位置X,Y,文字サイズ,速度,色及び有効データ判定フラグ等。
- UDPなのでデータの保証は無し。その代わり数十台の端末から一斉に送信可。
- Msgが流れていない場合、殆どシステムに負荷が掛かっていません。
- システムメニュ非表示,Alt + F4無効化,右クリックメニュー?「閉じる」無効化等をしてます。(<a href="https://qiita.com/SUIMA/items/ea9faeda750248d57306">参考記事)</A>
- 別記事、<a href="https://qiita.com/inf102/items/e6cc6d8590b929f5e54e">「SkiaSharp 導入と各種サンプル」</A>も必要に応じて参照下さい。
:::
### <font  color="6495ed">1.警告動画 (WQHD 2560x1440,自作機 i5-7600K 3.80GHz)</F>

<iframe width="560" height="315" src="https://www.youtube.com/embed/AcyoP0KlUSk?si=IKYD4Sp9venvqv2b" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>



### <font  color="6495ed">2.ニコニコ動画生成中にニコニコ動画風メッセージを流してみる</F>

<iframe width="560" height="315" src="https://www.youtube.com/embed/lABLIXVfDBg?si=CgtPQNdNfQAQI6T9" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>


### <font  color="6495ed">3.拡大メッセージ版 (拡大縮小するように改変。参考動画)</F>
<iframe width="560" height="315" src="https://www.youtube.com/embed/kA7JLlIsVsc?si=XHKR9mkc7RKXkKLB" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

<HR>

### <font  color="6495ed">4.送信側 (WPF-C#)</F>
・Msgを表示する端末のIPAddrとポート番号を入れて「設定」ボタン。
![No1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821882/81b08dfa-8a0f-e80c-8cda-33991f1d0197.jpeg)

```cs
///////////////////////////////////////
// メッセージ送信用  (c)inf102 S.H 2023. 
//////////////////////////////////////
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace WPF_UDP1 {

    public partial class MainWindow : Window {
        
        public string SENDIpAddr="";
        public int    SendPort;
        public System.Net.Sockets.UdpClient SendUdp = new System.Net.Sockets.UdpClient();

        public MainWindow() {
            InitializeComponent();
           
        }

        // SEND BTN
        private void Button_Click(object sender, RoutedEventArgs e) {
            if (SENDIpAddr == "" ){
                LIST1.Items.Add ("右上の設定ﾎﾞﾀﾝを押して下さい.");
                return;
            }

            SendMsg();
        }

        // SEND MAIN
        public async void SendMsg(){

            if (TEXT1.Text=="" ) return;

            // 電文生成
            string ss="";

            // 表示設置用コマンド
            if (SIZES.IsChecked == true ) ss="S";
            if (SIZEM.IsChecked == true ) ss="M";
            if (SIZEL.IsChecked == true ) ss="L";

            if (S1.IsChecked == true ) ss+="1";
            if (S2.IsChecked == true ) ss+="2";
            if (S3.IsChecked == true ) ss+="3";

            if (C1.IsChecked == true ) ss=ss+"R"+TEXT1.Text;
            if (C2.IsChecked == true ) ss=ss+"W"+TEXT1.Text;;
            if (C3.IsChecked == true ) ss=ss+"G"+TEXT1.Text;;

            byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(ss);
            await SendUdp.SendAsync (sendBytes, sendBytes.Length, SENDIpAddr, SendPort);
            
            // リストボックスに表示するのはコマンドを除いた本文のみ
            LIST1.Items.Add (ss.Substring(3));
                    
            // LSITBOX 最終行に移動
            var border = VisualTreeHelper.GetChild(LIST1, 0) as Border;
            var listBoxScroll = border.Child as ScrollViewer;
            listBoxScroll.ScrollToEnd();

            TEXT1.Text="";
            TEXT1.Focus();

        }

        private void TEXT1_KeyDown(object sender, System.Windows.Input.KeyEventArgs e) {
            if (e.Key == Key.Return){
                SendMsg();
            }
        }

        // 設定ボタン
        private void Button_Click_1(object sender, RoutedEventArgs e) {

            SENDIpAddr=IPADDR.Text;
            SendPort=int.Parse(SENDPORT.Text);
            
            Title="[UDP SendAddr " + SENDIpAddr +"] [SendPort "+SendPort + "]";
            LIST1.Items.Add ("設定完了");
        }
    }
}
```
### <font  color="6495ed">5.送信側 (WPF-XAML)</F>

```xml
<!-- 開発元 inf102 2023 -->
<Window x:Class="WPF_UDP1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WPF_UDP1"
        mc:Ignorable="d" Title="スクロールメッセージ" Height="380" Width="720">

    <Grid Margin="1,1,1,1" >
        <Grid.RowDefinitions>
            <RowDefinition Height="28"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="42"/>
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="70*"/>
            <ColumnDefinition Width="68"/>
            <ColumnDefinition Width="359"/>
        </Grid.ColumnDefinitions>

        <TextBox x:Name="TEXT1" Grid.Column="0" Grid.Row="2" Height="33"  Margin="1,5,1,4"  KeyDown="TEXT1_KeyDown" FontSize="25"/>
        <Button Content="送信"  Grid.Column="1" Grid.Row="2"  VerticalAlignment="Center"  Height="30" Click="Button_Click"   Width="62"/>


        <ListBox x:Name="LIST1" Grid.Row="1" Grid.ColumnSpan="3" Margin="1,1,1,1" BorderBrush="#FF5D3045" Background="#FF6AA3EF" Foreground="White" FontSize="19"/>


        <StackPanel Orientation="Horizontal" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" HorizontalAlignment="Right" Width="354"  >

            <Label Content="SendPORT" Margin="-8,-5,0,0" VerticalAlignment="Center" FontSize="16" Height="28"  HorizontalAlignment="Center"  />
            <TextBox x:Name="SENDPORT" Margin="-3,-3,0,0" HorizontalAlignment="Center" TextAlignment="Center" FontSize="16" Width="60" Text="65100" VerticalAlignment="Center" Height="20"  />
            <Label Content="SendIP" Margin="0,-5,0,0" FontSize="16" VerticalAlignment="Center"  Height="28"   />
            <TextBox x:Name="IPADDR" Margin="-3,-3,0,0" TextAlignment="Center" Width="110" Text="172.31.1.128" VerticalAlignment="Center" FontSize="16" HorizontalAlignment="Center" Height="20"   />
            <Button  Width="40" Content="設定" Margin="2,3,4,4" Height="21" Click="Button_Click_1" />

        </StackPanel>

        <StackPanel Orientation="Horizontal" Grid.Row="2" HorizontalAlignment="Center"  Grid.RowSpan="2" Width="344" Grid.Column="2"  >
            <GroupBox  Header="ｻｲｽﾞ"   Height="37" Margin="-5,-6,0,0" FontSize="12" Width="115" >
                <StackPanel Orientation="Horizontal">

                    <RadioButton x:Name="SIZES" Content="小 "  FontSize="13"/>
                    <RadioButton x:Name="SIZEM" Content="中 "  FontSize="13" IsChecked="True"/>
                    <RadioButton x:Name="SIZEL" Content="大 "  FontSize="13"/>
                </StackPanel>

            </GroupBox>

            <GroupBox  Header="速度"  Height="37" Margin="0,-6,0,0" FontSize="12" Width="115">
                <StackPanel Orientation="Horizontal">

                    <RadioButton x:Name="S1" Content="遅 " HorizontalAlignment="Left" FontSize="13"/>
                    <RadioButton x:Name="S2" Content="普 "  HorizontalAlignment="Left"  FontSize="13" IsChecked="True"/>
                    <RadioButton x:Name="S3" Content="速 "  HorizontalAlignment="Left"  FontSize="13"/>
                </StackPanel>

            </GroupBox>

            <GroupBox  Header="色"  Height="37" Margin="0,-6,0,0" FontSize="12" Width="115">
                <StackPanel Orientation="Horizontal">

                    <RadioButton x:Name="C1" Content="赤 " HorizontalAlignment="Left" FontSize="13"/>
                    <RadioButton x:Name="C2" Content="白 "  HorizontalAlignment="Left"  FontSize="13" />
                    <RadioButton x:Name="C3" Content="緑 "  HorizontalAlignment="Left"  FontSize="13" IsChecked="True"/>
                </StackPanel>
            </GroupBox>
        </StackPanel>
    </Grid>
</Window>
```

<HR>

### <font  color="6495ed">6.表示側 (WPF-C#) </F>
・止める場合はタスクマネージャから。

```cs
///////////////////////////////////////////////////
// スクロールメッセージ (表示用) (c)inf102 S.H 2023.
// SkiaSharp+WPF(.NETFRAME WORK)
///////////////////////////////////////////////////

using SkiaSharp;
using SkiaSharp.Views.Desktop;
using System;
using System.Net;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;

namespace ScrMSG {

    public partial class MainWindow : Window {

        ///////////////////////////////////
        public int RcvPort=65100;
        const  int MAX=300;      // 最大表示数
        ///////////////////////////////////
        
        public System.Net.Sockets.UdpClient SendUdp = new System.Net.Sockets.UdpClient();

        // 表示データ
        struct RcvData{
            public bool EnabelFlg;  // True == 有効
            public string TEXT;     // 表示データ
            public double X,Y;      // 表示位置
            public int Speed;       // スクロール速度
            public int SIZE;        // 文字大きさ
            public string COLOR;    // 文字色

        }

        // 表示データ構造体配列
        static RcvData[] PrnMsg = new RcvData[MAX];

        // 初回メッセージ表示管理フラグ
        static bool FirstFLG=true;

        public const int GWL_STYLE = ( -16 ); // ウィンドウスタイル
        public const int GWL_EXSTYLE = ( -20 ); // 拡張ウィンドウスタイル
        public const int WS_SYSMENU = 0x00080000; // システムメニュを表示する
        public const int WS_EX_TRANSPARENT = 0x00000020; // 透過ウィンドウスタイル
        public const int WM_SYSKEYDOWN = 0x0104; // Alt + 任意のキー の入力
        public const int VK_F4 = 0x73; 

        [DllImport( "user32" )]
        protected static extern int GetWindowLong(IntPtr hWnd, int nIndex);

        [DllImport( "user32" )]
        protected static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwLong);


        public MainWindow() {

            InitializeComponent();

            this.WindowState = WindowState.Maximized;
            Rcv();
    
        }

        public void Rcv(){ 

            System.Net.IPEndPoint localEP    =new System.Net.IPEndPoint(IPAddress.Any, RcvPort);
            System.Net.Sockets.UdpClient UDP =new System.Net.Sockets.UdpClient(localEP);
            System.Net.IPEndPoint remoteEP   =null;

            // Y軸ランダム値
            Random Rnd = new Random();

            // DGRAM受信 -> 電文解析 -> 空いている(xa[vg].EnabelFlg == false)表示用構造体配列にパラメータ設定
            string rcvMsg="";
            _=Task.Run(() => {    
                while(true){
                    // 初回起動
                    if (FirstFLG == true){
                        FirstFLG = false;
                        rcvMsg ="M2R起動 Port"+RcvPort;
                    }
                    else{
                        // UDPデータ受信
                        byte[] rcvBytes = UDP.Receive(ref remoteEP);
                        rcvMsg = System.Text.Encoding.UTF8.GetString(rcvBytes);
                    }

                    for (int vg=0;vg<MAX;vg++){

                        // 構造体無効配列を探してデータ投入
                        if (PrnMsg[vg].EnabelFlg == false){
           
                            // 電文解析してパラメータ設定        
                            // 文字大きさ
                            if (rcvMsg[0] == 'S' ) PrnMsg[vg].SIZE=50;
                            if (rcvMsg[0] == 'M' ) PrnMsg[vg].SIZE=90;
                            if (rcvMsg[0] == 'L' ) PrnMsg[vg].SIZE=260;

                            // 速度
                            if (rcvMsg[1] == '1' ) PrnMsg[vg].Speed=5;
                            if (rcvMsg[1] == '2' ) PrnMsg[vg].Speed=9;
                            if (rcvMsg[1] == '3' ) PrnMsg[vg].Speed=16;

                            // 色
                            if (rcvMsg[2] == 'R' ) PrnMsg[vg].COLOR="R";
                            if (rcvMsg[2] == 'W' ) PrnMsg[vg].COLOR="W";
                            if (rcvMsg[2] == 'G' ) PrnMsg[vg].COLOR="G";

                            // 有効化
                            PrnMsg[vg].EnabelFlg = true;

                            // 画面に出すのは先頭コマンド3文字除いた後のデータ
                            PrnMsg[vg].TEXT=rcvMsg.Substring(3);
                            
                            // 0からメインディスプレイ縦(Y)サイズまでのランダム値取得
                            PrnMsg[vg].Y=Rnd.Next(0, (int)SystemParameters.FullPrimaryScreenHeight);

                            // 画面上部に表示する際、Y軸が小さいと上部が切れるのでY軸を補正(文字のY軸ベースラインは下部な為。下参照)
                            //       .
                            //      . .
                            //     .....
                            //    .     . <--- ベースライン　Y軸を10で高さが20ある文字の場合上部10が切れるので補正
                            //
                            //PrnMsg[vg].Y = 213;
                            if (PrnMsg[vg].Y <= 50  && rcvMsg[0] =='S') PrnMsg[vg].Y =50;   //SIZE-S
                            if (PrnMsg[vg].Y <= 85  && rcvMsg[0] =='M') PrnMsg[vg].Y =85;   //SIZE-M
                            if (PrnMsg[vg].Y <= 210 && rcvMsg[0] =='L') PrnMsg[vg].Y =210;  //SIZE-L
           
                            // メインディスプレイ横サイズ
                            PrnMsg[vg].X=SystemParameters.FullPrimaryScreenWidth;
                            
                            break;
                        }
                    }
                }
            });
        }

        protected override void OnSourceInitialized(EventArgs e) {

            base.OnSourceInitialized( e );

            //WindowHandle(Win32) を取得
            var handle = new WindowInteropHelper( this ).Handle;

            //システムメニュを非表示
            int windowStyle = GetWindowLong( handle, GWL_STYLE );
            windowStyle &= ~WS_SYSMENU;
            SetWindowLong( handle, GWL_STYLE, windowStyle );

            // Alt + F4を無効にする
            HwndSource source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
            source.AddHook(new HwndSourceHook(WndProc));

        }

         // Alt + F4無効 / 右クリックメニュー?「閉じる」 無効
        private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled){

            const int WM_SYSCOMMAND = 0x0112;
            const int SC_CLOSE = 0xF060;

            if ((msg == WM_SYSKEYDOWN) && (wParam.ToInt32() == VK_F4)){
                handled = true;
            }
 
            if ((msg == WM_SYSCOMMAND) && (wParam.ToInt32() == SC_CLOSE)){
                handled = true;
            }

            return IntPtr.Zero;
        
            
        }

        private async void Window_Loaded(object sender, RoutedEventArgs e) {
    
            // 有効データがあるかどうかのフラグ
            bool ka=false;

            // MAIN-LOOP
            while (true){

                // 有効データがある時のみ表示
                if (ka == true ){
                    Dispatcher.Invoke(() => { 
                        skiaCanvas2.InvalidateVisual(); 
                    });
                }

                // X軸可変 フラグがtrueの構造体配列のみX軸を可変させる
                ka=false;
                for (int vv=0;vv<MAX;vv++){
                    if (PrnMsg[vv].EnabelFlg == true ) {
                        PrnMsg[vv].X -= PrnMsg[vv].Speed; 
                        ka=true;
                    }
                }
             
                await Task.Delay(3); 
            }

        }


        // 文字列表示
		void PaintSurface(object sender, SKPaintSurfaceEventArgs args){
            
            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas;

            ///////////////////////////////////////////////////////////////////

            var skPaint = new SKPaint() {
                FilterQuality = SKFilterQuality.High,
               // IsAntialias = true, 
                Typeface = SKTypeface.FromFamilyName("LINE Seed JP_OTF")
            };

            ////////////////////////////////////////////////////////////////////

            canvas.Clear();

            // 全文字表示
            for(int vc = 0; vc < MAX; vc++) {

                // 有効データの場合表示させる
                if(PrnMsg[vc].EnabelFlg == true ){

                    // 文字サイズ
                    skPaint.TextSize = PrnMsg[vc].SIZE;

                    // 色
                    if ( PrnMsg[vc].COLOR=="R" ) skPaint.Color=SKColors.Red;
                    if ( PrnMsg[vc].COLOR=="G" ) skPaint.Color=SKColors.Green;
                    if ( PrnMsg[vc].COLOR=="W" ) skPaint.Color=SKColors.White;

                    // 最後の文字の右端が画面左に到達したか検査
                    // 1文字のX幅(skPaint.TextSize) * 文字数 が全体のX幅となるのでどこまで マイナス値にすれば良いかわかる (文字によって幅が違うが問題ない)
                    if ( PrnMsg[vc].X >= -( (PrnMsg[vc].TEXT.Length) * skPaint.TextSize) ) {
                        canvas.DrawText (PrnMsg[vc].TEXT, (float)PrnMsg[vc].X, (float)PrnMsg[vc].Y , skPaint);
                    }
                    else {
                        // 最右の文字の右側が画面左に達したので無効化
                        PrnMsg[vc].EnabelFlg=false;
                        
                    }
                }
            }
        }
    }
}
```

### <font  color="6495ed">7.表示側 (WPF-XAML)</F>
```xml
<Window x:Class="ScrMSG.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wpf="clr-namespace:SkiaSharp.Views.WPF;assembly=SkiaSharp.Views.WPF"
        Title="OverlayWindow" Topmost="True"
        WindowStyle="None" ShowInTaskbar="False" AllowsTransparency="True"
        Background="Transparent" Loaded="Window_Loaded">

    <Grid>
        <wpf:SKElement x:Name="skiaCanvas2" PaintSurface="PaintSurface" />
    </Grid>
</Window>
```