・----------------------------------------------------------------------------------------・
・ソケットでメッセージを送り、相手のデスクトップ画面に横スクロール表示します。
・「ニコニコ動画」みたいな感じです。動画1,2は物は同じです。
・----------------------------------------------------------------------------------------・
・ソケットでメッセージを送り、相手のデスクトップ画面に横スクロール表示します。
・「ニコニコ動画」みたいな感じです。動画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とポート番号を入れて「設定」ボタン。
///////////////////////////////////////
// メッセージ送信用 (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>