読者です 読者をやめる 読者になる 読者になる

WPFでニコニコメソッド

WPF C#

LL魂で、kogurroさんのプレゼンソフトに影響を受けたことと、WPFの勉強も兼ねて、ニコニコ動画風にデスクトップ上にコメントが流れるツールを作ってみました。(ちょっと乗り遅れた感はありますが。)
このツールには、次のような特徴があります。

  • 常に最前面で、背景を透過しているので、コメントをつけられるアプリを選びません。(PowerPointでプレゼン中でも可。)
  • コメントはtelnetで送ります。
  • キーワードにより、コメントの文字色や大きさを指定できます。

コメントをつけるためにわざわざ特別なアプリケーションが必要になると不便だろうなと思い、最初はWebサーバを立ててブラウザでコメントするのがいいかなとか考えていたのですが、それも面倒だったので、telnetでコメントをつけられるようにしました。
telnetだったらどのPCにも入っているし、社内勉強会とかだったら十分使えるんじゃないかと思います。

ダウンロード

多分、.NET Framework 3.0のラインタイムがインストールされていれば、動くと思います。

コメントの送り方

コメントを送るには、コマンドプロンプトを立ち上げて、本ソフトの起動しているPCに、telnetで接続します。

>telnet 192.168.1.12

すると、以下のような画面が出てくるので、好きなようにコメントします。(日本語も使えます。)

ニコニコプレゼンにようこそ!
>

キーワードについて

送信したメッセージの中に特定のキーワードが含まれていると、文字の大きさや色を変えることができます。
利用可能なキーワードは以下の5つです。

キーワード 効果
big コメントの文字サイズが大きくなる
small コメントの文字サイズが小さくなる
blue コメントの文字色が青色になる
red コメントの文字色が赤色になる
green コメントの文字色が緑色になる

例えば、

> big red ほげ

と入力すると、「ほげ」という文字が赤く大きく表示されます。キーワードは表示されません。

Windows1.xaml

XAMLのコードはほぼデフォルトのままです。

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Class="NicoNico.Window1"
    Height="800" Width="1024"
    Loaded="Window_Loaded"
    >
  <Grid></Grid>
</Window>

Windows1.xaml.cs

ロジックはこんな感じ。

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace NicoNico
{
    public partial class Window1 : System.Windows.Window
    {
        Dictionary<string, Brush> fontColorMap = new Dictionary<string, Brush>();
        Dictionary<string, double> fontSizeMap = new Dictionary<string, double>();
        private Canvas canvas;

        public Window1()
        {
            InitializeComponent();
            // 透明で常に前面に設定
            this.AllowsTransparency = true;
            this.Height = System.Windows.SystemParameters.PrimaryScreenHeight;
            this.Width = System.Windows.SystemParameters.PrimaryScreenWidth;
            this.Top = 0;
            this.Left = 0;
            this.WindowState = WindowState.Maximized;
            this.Background = Brushes.Transparent;
            this.WindowStyle = WindowStyle.None;
            this.Topmost = true;
            this.canvas = new Canvas();
            this.Content = this.canvas;

            fontColorMap.Add("blue", Brushes.Blue);
            fontColorMap.Add("red", Brushes.Red);
            fontColorMap.Add("green", Brushes.Green);

            fontSizeMap.Add("big", 200.0);
            fontSizeMap.Add("small", 50.0);
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // telnetサーバ起動
            Thread thread = new Thread(new ThreadStart(TelnetThread));
            thread.IsBackground = true;
            thread.Start();
        }

        private int verticalPosition = 0;
        public delegate void MoveMessageDelegate(string message);

        public void MoveMessage(string message)
        {
            if (!this.CheckAccess())
            {
                Dispatcher.BeginInvoke(DispatcherPriority.Normal, new MoveMessageDelegate(this.MoveMessage), message);
            }
            else
            {
                // フォントサイズの決定
                double fontsize = 100;
                foreach (string sizeName in fontSizeMap.Keys)
                {
                    if (message.Contains(sizeName))
                    {
                        message = message.Remove(message.IndexOf(sizeName), sizeName.Length);
                        fontsize = fontSizeMap[sizeName];
                    }
                }
                // フォントカラーの決定
                Brush fontcolor = Brushes.White;
                foreach (string colorname in fontColorMap.Keys)
                {
                    if (message.Contains(colorname))
                    {
                        message = message.Remove(message.IndexOf(colorname), colorname.Length);
                        fontcolor = fontColorMap[colorname];
                    }
                }
                // 新しいテキストを生成
                TextBlock textBlock = new TextBlock();
                textBlock.FontSize = fontsize;
                textBlock.Text = message.Trim();
                textBlock.Foreground = fontcolor;
                this.canvas.Children.Add(textBlock);
                // テキストに影をつける
                DropShadowBitmapEffect effect = new DropShadowBitmapEffect();
                effect.ShadowDepth = 4;
                effect.Direction = 330;
                effect.Color = (Color)ColorConverter.ConvertFromString("black");
                textBlock.BitmapEffect = effect;
                // テキストの位置を指定
                verticalPosition += (int)fontsize;
                if (verticalPosition >= this.Height) verticalPosition = 0;
                TranslateTransform transform = new TranslateTransform(this.Width, verticalPosition);
                // テキストのアニメーション
                textBlock.RenderTransform = transform;
                Duration duration = new Duration(TimeSpan.FromMilliseconds(5000));
                DoubleAnimation animationX = new DoubleAnimation(-1 * message.Length * fontsize, duration);
                transform.BeginAnimation(TranslateTransform.XProperty, animationX);
            }
        }

        const int PortNumber = 23;
        const int BacklogSize = 20;

        private void TelnetThread()
        {
            Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            server.Bind(new IPEndPoint(IPAddress.Any, PortNumber));
            server.Listen(BacklogSize);
            while (true)
            {
                Socket conn = server.Accept();
                new Connection(conn, this);
            }
        }
    }

    class Connection
    {
        private static object BigLock = new object();
        private static List<Connection> connections = new List<Connection>();
        private Window1 parent;
        private Socket socket;
        public StreamReader Reader;
        public StreamWriter Writer;

        public Connection(Socket socket, Window1 win)
        {
            this.parent = win;
            this.socket = socket;
            Reader = new StreamReader(new NetworkStream(socket, false), System.Text.Encoding.Default);
            Writer = new StreamWriter(new NetworkStream(socket, true), System.Text.Encoding.Default);
            Thread thread = new Thread(ClientLoop);
            thread.IsBackground = true;
            thread.Start();
        }

        void ClientLoop()
        {
            try
            {
                lock (BigLock)
                {
                    OnConnect();
                }
                while (true)
                {
                    lock (BigLock)
                    {
                        foreach (Connection conn in connections)
                        {
                            conn.Writer.Flush();
                        }
                    }
                    string line = Reader.ReadLine();

                    if (line == null)
                    {
                        break;
                    }
                    lock (BigLock)
                    {
                        ProcessLine(line);
                    }
                }
            }
            finally
            {
                lock (BigLock)
                {
                    socket.Close();
                    OnDisconnect();
                }
            }
        }

        void OnConnect()
        {
            connections.Add(this);

            Writer.WriteLine("ニコニコプレゼンにようこそ!");
            this.Writer.Write("> ");
        }

        void OnDisconnect()
        {
            connections.Remove(this);
        }

        void ProcessLine(string line)
        {
            this.parent.MoveMessage(line);
            this.Writer.Write("> ");
        }
    }
}

telnetのコードは、http://www.thescripts.com/forum/thread275416.html を参考にさせていただきました。

バグ

  • バックスペースやデリートなどの制御文字を使うと文字化けします。
  • Shift JIS以外の漢字コードで日本語のコメントを送信すると文字化けします。
  • 一度にたくさんコメントを送ると、ちょっと重たいかも。
  • アニメーションが終了したらTextBlockを解放すべきかもしれない。