LoginSignup
3
4

[開発] ニコニコ動画風 メッセージ (WPF+SkiaSharp)

Last updated at Posted at 2024-01-09
・----------------------------------------------------------------------------------------・
・ソケットでメッセージを送り、相手のデスクトップ画面に横スクロール表示します。
・「ニコニコ動画」みたいな感じです。動画1,2は物は同じです。
・----------------------------------------------------------------------------------------・

大体の仕組み

  • 送信アプリで速度、色等を選んで指定したIPアドレスの端末にDGRAMで速度等のパラメータを追加した電文を送信。
  • 表示アプリで画面を最大化し透過させ、受信した電文により表示データ毎に構造体の配列に入れる。メンバ変数は位置X,Y,文字サイズ,速度,色及び有効データ判定フラグ等。
  • UDPなのでデータの保証は無し。その代わり数十台の端末から一斉に送信可。
  • Msgが流れていない場合、殆どシステムに負荷が掛かっていません。
  • システムメニュ非表示,Alt + F4無効化,右クリックメニュー?「閉じる」無効化等をしてます。(参考記事)
  • 別記事、「SkiaSharp 導入と各種サンプル」も必要に応じて参照下さい。

1.警告動画 (WQHD 2560x1440,自作機 i5-7600K 3.80GHz)

2.ニコニコ動画生成中にニコニコ動画風メッセージを流してみる

3.拡大メッセージ版 (拡大縮小するように改変。参考動画)


4.送信側 (WPF-C#)

・Msgを表示する端末のIPAddrとポート番号を入れて「設定」ボタン。
No1.jpg

///////////////////////////////////////
// メッセージ送信用  (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 ("設定完了");
        }
    }
}

5.送信側 (WPF-XAML)

<!-- 開発元 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>

6.表示側 (WPF-C#)

・止める場合はタスクマネージャから。

///////////////////////////////////////////////////
// スクロールメッセージ (表示用) (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;
                        
                    }
                }
            }
        }
    }
}

7.表示側 (WPF-XAML)

<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>
3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4