DiscordチャットをC# WPFで流れるテロップとして表示する

C#(WPF)

Discord

Discordに投稿されたチャットをリアルタイムでC#に連携し、その内容をWPFで透明化させたウィンドウに表示させ、まるで流れるテロップのように画面に表示させます。

Discord連携

Discordのチャットをどうやって連携させるのか?

DiscordAPIと連携するためのライブラリをDiscordが提供してくれています。「Discord.Net.Core」を使用することで簡単に連携できます。

NuGetパッケージマネージャを表示する

NuGet(ニューゲット)を使用します。NuGetはVisualStudioのパッケージマネージャで、PHPだとComposer、Node.jsのnpmなどと同じ位置付けのものです。

ツール
 └ NuGetパッケージマネージャ
    └ ソリューションのNuGetパッケージの管理

Discord.Net.Coreを追加する

Discord.Net.Coreを追加します。

プログラムソース

DiscordBot.cs

Discordでチャンネルを作成し、そのチャンネルIDを指定してください。このあたりは他のサイトでもよく見られる記述ですので割愛します。

using DesktopSubtitles;
using Discord;
using Discord.WebSocket;
using System.Diagnostics;
using System.Threading.Tasks;

public class DiscordBot
{
    private DiscordSocketClient _client;
    public event DiscordEventHandler eventHandlerObj;

    public async Task StartAsync()
    {
        var socketConfig = new DiscordSocketConfig {
            GatewayIntents = GatewayIntents.All
        };

        _client = new DiscordSocketClient(socketConfig);
        _client.Log += LogAsync;
        _client.Ready += ReadyAsync;
        _client.MessageReceived += messageReceivedAsync;

        // DISCORDのチャンネルID
        await _client.LoginAsync(TokenType.Bot, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
        await _client.StartAsync();
    }

    private Task LogAsync(LogMessage log) {
        return Task.CompletedTask;
    }

    private Task ReadyAsync() {
        return Task.CompletedTask;
    }

    private async Task messageReceivedAsync(SocketMessage message) {
        receive(new DiscordEventArgs(message));
    }

    private void receive(DiscordEventArgs args) {
        if (eventHandlerObj != null) {
            eventHandlerObj(this, args);
        }
    }
}

MainWindow.xaml.cs

Discordからメッセージを受け取ると、WPFウィンドウにユーザーコントロールを1つ作成し、受信したメッセージを設定します。その後、タイマーを動作させ、メッセージが左から右へ流れ、メッセージがウィンドウ外に到達した時にタイマーを破棄しています。

メッセージが流れている途中にDiscordで新しいメッセージを作成すると、テロップの文字も更新されます。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Timers;
using System.Windows;
using System.Windows.Media;

namespace DesktopSubtitles {
    public delegate void DiscordEventHandler(object sender, DiscordEventArgs e);

    public partial class MainWindow : Window {
        private DiscordBot _bot;
        private Dictionary timers = new Dictionary();
        private Timer watchTimer = new Timer();

        public MainWindow() {
            InitializeComponent();
            _bot = new DiscordBot();
            _bot.eventHandlerObj += new DiscordEventHandler(discordMessageReceive);
            _bot.StartAsync();
        }

        // メッセージ受信
        private void discordMessageReceive(object sender, DiscordEventArgs e)
        {
            this.Dispatcher.Invoke((Action)(() => {
                // 表示用コントロールを取得
                var target = (MessageControl)this.FindName("txt" + e.message.Author.Id.ToString());

                // コントロールが存在する
                if (null != target) {
                    target.AuthorName = e.message.Author.GlobalName;
                    target.Content = e.message.Content;
                }
                // コントロールが存在しない
                else {
                    // タイマー作成(100ミリ秒)
                    var timer = new SafelyDisposableTimer(e.message.Author.Id.ToString(), 100, timerElapsed);

                    // 表示用コントロール作成
                    var tg = new TransformGroup();
                    tg.Children.Add(new TranslateTransform(0, 100));

                    var msg = new MessageControl();
                    msg.RenderTransform = tg;
                    msg.Name = "txt" + e.message.Author.Id.ToString();
                    msg.AuthorName = e.message.Author.GlobalName;
                    msg.Content = e.message.Content;
                    msg.movementX = msg.movementX + 10;
                    msg.Foreground = new SolidColorBrush(Colors.Red);

                    this.area.Children.Add(msg);
                    this.area.RegisterName(msg.Name, msg);

                    // タイマー
                    TimerSubtitle ts = new TimerSubtitle();
                    ts.timer = timer;
                    ts.message = e.message;

                    // タイマー一覧をコレクションで管理
                    this.timers.Add(e.message.Author.Id.ToString(), ts);

                    // タイマー開始
                    timer.Start();
                }
            }));
        }

        // タイマー変化
        private void timerElapsed(String id) {
            bool timerStopFlg = false;

            try {
                this.Dispatcher.Invoke((Action)(() => {
                    // 表示用コントロールを取得
                    var target = (MessageControl)this.FindName("txt" + id);

                    if (null != target) {
                        // 表示コントロールの座標取得
                        double pixelWidth = SystemParameters.WorkArea.Width;

                        // 並行移動
                        target.positionX = target.positionX + target.movementX;
                        var tg = new TransformGroup();
                        tg.Children.Add(new TranslateTransform(target.positionX, target.RenderTransform.Value.OffsetY));
                        target.RenderTransform = tg;

                        if (pixelWidth < target.positionX) {
                            // 表示用コントロール破棄
                            this.area.Children.Remove(target);
                            this.area.UnregisterName(target.Name);
                            target = null;
                            timerStopFlg = true;
                        }
                    }
                }));

                if (timerStopFlg) {
                    // タイマー破棄
                    this.timers[id].timer.Dispose();
                    this.timers.Remove(id);
                }
            }
            catch (Exception ex) {
              // 処理してね
            }
        }
    }
}

MessageControl.xaml.cs

流れるテロップのユーザーコントロールです。

using System.Windows;
using System.Windows.Controls;

namespace DesktopSubtitles {
    public partial class MessageControl : UserControl {
        // X座標
        public int positionX = 0;

        // X座標移動量
        public int movementX = 10;

        public MessageControl() {
            InitializeComponent();
        }

        public static DependencyProperty AuthorNameProperty = DependencyProperty.Register("AuthorName", typeof(string), typeof(MessageControl), new PropertyMetadata(""));
        public static DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(string), typeof(MessageControl), new PropertyMetadata(""));

        public string AuthorName {
            get => (string)GetValue(AuthorNameProperty);
            set {
                SetValue(AuthorNameProperty, value);
            }
        }

        public string Content {
            get => (string)GetValue(ContentProperty);
            set {
                SetValue(ContentProperty, value);
            }
        }
    }
}

SafelyDisposableTimer.cs

タイマークラスです。こちらのサイトを参考にさせていただきました。

C#, System.Timers.Timerクラスの安全な破棄方法 - Qiita
目的定期的にバックグランドスレッドで何か処理するようなSystem.Timers.Timerを破棄するとき、破棄したタイミングでは絶対に処理が終わっていて欲しい、という要件を満たすラッパークラスを…
using System;
using System.Diagnostics;
using System.Timers;

namespace DesktopSubtitles {
    /// 
    /// 繰り返し処理専用のTimerラッパー
    /// Dispose後は実行中の処理が必ず無い状態となる
    /// 開始後は破棄のみ可能
    /// IDisposableのsnippestはほぼ、そのまま残してある
    /// 
    internal class SafelyDisposableTimer : IDisposable {
        private bool disposedValue;
        // 定期的に実行したい処理
        private Action callbackAction;
        private string id;
        private Timer timer = new Timer();
        private object lockObject = new object();

        /// 
        /// コンストラクタ
        /// 
        /// タイマーのユニークID
        /// インターバル(ミリ秒)
        /// 定期的に実行したい処理
        internal SafelyDisposableTimer(string id, int interlval, Action callbackAction) {
            this.callbackAction = callbackAction;
            this.id = id;
            this.timer.Elapsed += this.TimerElapsedEventHandler;
            this.timer.Interval = interlval;
            // 繰り返し処理する
            this.timer.AutoReset = true;
        }

        /// 
        /// タイマーを開始する
        /// 
        internal void Start() {
            this.timer.Start();
        }

        /// 
        /// 破棄
        /// 
        /// 
        protected virtual void Dispose(bool disposing) {
            if (!disposedValue) {
                if (disposing) {
                    // TODO: マネージド状態を破棄します (マネージド オブジェクト)
                    this.timer.Stop();
                    this.timer.Dispose();
                }

                // TODO: アンマネージド リソース (アンマネージド オブジェクト) を解放し、ファイナライザーをオーバーライドします
                // TODO: 大きなフィールドを null に設定します
                lock (this.lockObject) {
                    disposedValue = true;
                }
            }
        }

        /// 
        /// 破棄
        /// 
        public void Dispose() {
            // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }

        private void TimerElapsedEventHandler(object sender, ElapsedEventArgs args) {
            lock (this.lockObject) {
                if (this.disposedValue == true) {
                    return;
                }

                // 処理を実行
                this.callbackAction(this.id);
            }
        }
    }
}

TimerSubtitle.cs

受信したメッセージとタイマーをセットで持つクラスです。これをコレクションに持たせています。

using Discord.WebSocket;

namespace DesktopSubtitles {
    internal class TimerSubtitle {
        public SafelyDisposableTimer timer;
        public SocketMessage message;
    }
}

DiscordEventArgs.cs

using System;
using Discord.WebSocket;

namespace DesktopSubtitles {
    public class DiscordEventArgs : EventArgs {
        public SocketMessage message;

        public DiscordEventArgs(SocketMessage message) {
            this.message = message;
        }
    }
}

MainWindow.xaml

WPF側の透明ウィンドウ。親です。

<Window x:Class="DesktopSubtitles.MainWindow"
        xmlns:local="clr-namespace:DesktopSubtitles"
        AllowsTransparency="True"
        WindowStyle="None"
        WindowState="Maximized"
        Background="Transparent"
        Topmost="True">
    <Canvas Name="area">
        <Button Background="red" Opacity="0.5" Width="50" Margin="20" Click="exit_Click">EXIT</Button>
    </Canvas>
</Window>

MessageControl.xaml

WPF側のユーザーコントロール。

<UserControl x:Class="DesktopSubtitles.MessageControl"
             xmlns:local="clr-namespace:DesktopSubtitles"
             mc:Ignorable="d">
    <Grid>
        <StackPanel>
            <TextBlock VerticalAlignment="Center" TextAlignment="Left"
                   Text="{Binding AuthorName, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MessageControl}}}">
            </TextBlock>

            <TextBlock VerticalAlignment="Center" TextAlignment="Left"
                   Text="{Binding Content, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MessageControl}}}">
            </TextBlock>
        </StackPanel>
    </Grid>
</UserControl>

札幌在住エンジニア。JavaやPHPやWordPressを中心とした記事が中心です。

【SE歴】四半世紀以上
【Backend】php / java(spring) / c# / AdobeFlex / c++ / VB / cobol
【Frontend】 vue.js / jquery他 / javascript / html / css
【DB】oracle / mysql / mariadb / sqlite
【infrastructure】aws / oracle / gcp
【license】aws(saa-c03) / oracle master / XML Master / Sun Certified Programmer for the Java 2 Platform 1.4

Nobelをフォローする
C#(WPF)
Nobelをフォローする

コメント

タイトルとURLをコピーしました