WPFとNAudioで音楽プレイヤーを作る~第8回:スライダーを実装する~

第8回:スライダーを実装する

今回のポイント

  • Sliderの使い方

完成形

例によって先に完成形を載せておきます。

MainWindow.xaml

<Window x:Class="MusicPlayer.MVVM.View.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:MusicPlayer"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        xmlns:viewmodels="clr-namespace:MusicPlayer.MVVM.ViewModel"
        xmlns:ui="http://schemas.modernwpf.com/2019" 
        ui:WindowHelper.UseModernWindowStyle="True"
        d:DataContext="{d:DesignInstance Type=viewmodels:MainWindowViewModel}"
        Background="#262626"
        Foreground="White"
        MinHeight="600" 
        MinWidth="950"
        xmlns:ctrl="clr-namespace:Itenso.Windows.Controls.ListViewLayout;assembly=Itenso.Windows.Controls.ListViewLayout"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:cmd="http://www.galasoft.ch/mvvmlight"
        >
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="80"/>
        </Grid.RowDefinitions>

        <Grid Grid.Column="0" Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>

            <Button 
                Grid.Column="0"
                Width="30" Height="30"
                HorizontalAlignment="Center"
                Content="■"
                Command="{Binding OnStopButtonClickCommand}"
                />

            <Button 
                Grid.Column="1"
                Width="30" Height="30"
                HorizontalAlignment="Center"
                Content="{Binding PlayButtonContent}"
                Command="{Binding OnPlayButtonClickCommand}"
                />
        </Grid> 

        <ui:NavigationView  Background="#131313" 
                            Grid.Column="0" Grid.Row="0" 
                            x:Name="NaviView"
                            IsBackButtonVisible="Collapsed"
                            IsSettingsVisible="False"
                            IsTitleBarAutoPaddingEnabled="False"
                            IsPaneToggleButtonVisible="False"
                            PaneDisplayMode="Left"
                            Width="150"
                            >
            <ui:NavigationView.MenuItems>
                <ui:NavigationViewItem
                    Content="Music"
                    Icon="Audio"
                    IsSelected="True" />
            </ui:NavigationView.MenuItems>
        </ui:NavigationView>

        <Grid Background="#131313" 
              Grid.Column="1" Grid.Row="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="80"/>
                <RowDefinition />
            </Grid.RowDefinitions>

            <TextBlock Grid.Row="0"
                       Text="Music"
                       Margin="30,30,30,0"
                       FontWeight="Bold"
                       FontSize="24" />
            <ListView Grid.Row="1"
                      Margin="30,0,0,0"
                      x:Name="MusicList" 
                      SelectionMode="Single"
                      ctrl:ListViewLayoutManager.Enabled="true"
                      ItemsSource="{Binding MusicList}"
                      >
                <ListView.View>
                    <GridView ScrollViewer.VerticalScrollBarVisibility="Auto">
                        <GridViewColumn Header="Title" 
                                        DisplayMemberBinding="{Binding Title}"
                                        ctrl:ProportionalColumn.Width="8"/>
                        <GridViewColumn Header="Artist" 
                                        DisplayMemberBinding="{Binding Artist}"
                                        ctrl:ProportionalColumn.Width="4"/>
                        <GridViewColumn Header="Album" 
                                        DisplayMemberBinding="{Binding Album}"
                                        ctrl:ProportionalColumn.Width="5"/>
                        <GridViewColumn Header="Track" 
                                        DisplayMemberBinding="{Binding Track}"
                                        ctrl:ProportionalColumn.Width="2"/>
                        <GridViewColumn Header="Time" 
                                        DisplayMemberBinding="{Binding Time}"
                                        ctrl:ProportionalColumn.Width="3"/>
                    </GridView>
                </ListView.View>
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="SelectionChanged">
                        <cmd:EventToCommand Command="{Binding OnSelectionChangedCommand}" PassEventArgsToCommand="True"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </ListView>
        </Grid>
        <Grid Grid.Column="1" Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="4*" />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>

            <Grid Grid.Column="0"  VerticalAlignment="Center">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="1*"/>
                    <ColumnDefinition Width="8*"/>
                    <ColumnDefinition Width="1*"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="5*" />
                    <RowDefinition Height="4*"/>
                </Grid.RowDefinitions>

                <TextBlock Grid.Column="0"
                           x:Name="CurrentPosition"
                           Margin="10,5,0,0"
                           TextAlignment="Left"
                           Text="{Binding CurrentPosition}"/>
                <TextBlock Grid.Column="1"
                           TextAlignment="Center"
                           Margin="0,5,0,0"
                           Text="{Binding SelectedTrackAndArtist}"/>
                <TextBlock Grid.Column="2"
                           x:Name="CurrentMusicTimeString"
                           TextAlignment="Right"
                           Margin="0,5,20,0"
                           Text="{Binding CurrentMusicTimeString}"/>

                <Slider Grid.Row="1" Grid.ColumnSpan="3"
                        x:Name="Seekbar"
                        Foreground="AntiqueWhite"
                        Margin="10,0,10,0"
                        VerticalAlignment="Center"
                        Maximum="{Binding SeekbarMax}"
                        HorizontalAlignment="Stretch"
                        Width="Auto"
                        Value="{Binding SeekbarCurrentPosition}" 
                        >
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="ValueChanged">
                            <cmd:EventToCommand Command="{Binding OnSeekbarValueChangedCommand}" PassEventArgsToCommand="True"/>
                        </i:EventTrigger>
                        <i:EventTrigger EventName="PreviewMouseDown">
                            <cmd:EventToCommand Command="{Binding OnSeekbarMouseLeftButtonDownCommand}"/>
                        </i:EventTrigger>
                        <i:EventTrigger EventName="PreviewMouseUp">
                            <cmd:EventToCommand Command="{Binding OnSeekbarMouseLeftButtonUpCommand}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Slider>
            </Grid>

            <Grid Grid.Column="1" VerticalAlignment="Center">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="1*"/>
                    <ColumnDefinition Width="8*"/>
                    <ColumnDefinition Width="1*"/>
                </Grid.ColumnDefinitions>

                <TextBlock Grid.Column="0" Grid.Row="1"
                           Margin="-5,5,0,0"
                           TextAlignment="Center"
                           VerticalAlignment="Center"
                           Text="🔊"/>
                <Slider Grid.Column="1"
                        x:Name="VolumeSlider"
                        Minimum="0"
                        Maximum="100"
                        Foreground="AntiqueWhite"
                        VerticalAlignment="Center"
                        Value="{Binding Volume}"
                        Margin="5,0,0,0"
                        >
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="ValueChanged">
                            <cmd:EventToCommand Command="{Binding OnVolumeValueChangedCommand}" PassEventArgsToCommand="True"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Slider>
            </Grid>
        </Grid>
    </Grid>
</Window>

MainWindowViewModel.cs

using GalaSoft.MvvmLight.Command;
using MusicPlayer.MVVM.Common;
using MusicPlayer.MVVM.Model;
using NAudio.Wave;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;

namespace MusicPlayer.MVVM.ViewModel
{
    class MainWindowViewModel : INotifyPropertyChanged
    {
        // 変数の更新通知用
        public event PropertyChangedEventHandler PropertyChanged;

        public MainWindowModel _model = new MainWindowModel();

        // 音声読み込み
        private AudioFileReader _afr;

        // 音楽再生用の出力デバイス
        private WaveOutEvent _outputDevice = new WaveOutEvent();

        // 選択された楽曲
        private Music _currentMusic;

        // 再生ボタンに表示する文字
        private string _playButtonContent = "▶";
        public string PlayButtonContent
        {
            get { return this._playButtonContent; }
            set
            {
                this._playButtonContent = value;
                NotifyPropertyChanged("PlayButtonContent");
            }
        }

        // 音楽の一覧
        private List<Music> _musicList;
        public List<Music> MusicList
        {
            get { return this._musicList; }
            set 
            { 
                this._musicList = value;
                NotifyPropertyChanged("MusicList");
            }
        }

        // 選択された楽曲
        private Music _selectedMusic;
        public Music SelectedMusic
        {
            get { return this._selectedMusic; }
            set { this._selectedMusic = value; }
        }

        // 音量
        private int _volume = 80;
        /// <summary>音量</summary>
        public int Volume
        {
            get { return _volume; }
            set
            {
                _volume = value;
                NotifyPropertyChanged("Volume");
            }
        }

        // シークバーの最大値
        private long _seekbarMax = 100;
        /// <summary>シークバーの最大値</summary>
        public long SeekbarMax
        {
            get { return _seekbarMax; }
            set
            {
                _seekbarMax = value;
                NotifyPropertyChanged("SeekbarMax");
            }
        }

        // シークバーの現在位置
        private long _seekbarCurrentPosition = 0;
        /// <summary>シークバーの現在位置</summary>
        public long SeekbarCurrentPosition
        {
            get { return _seekbarCurrentPosition; }
            set
            {
                _seekbarCurrentPosition = value;
                NotifyPropertyChanged("SeekbarCurrentPosition");
            }
        }

        /// <summary>シークバードラッグ用フラグ</summary>
        private bool _mouseDownFlg = false;

        // 曲名と作曲者名を合わせた文字列
        private string _selectedTrackAndArtist = "";
        /// <summary>再生中の曲名とアーティスト名を連結した文字列</summary>
        public string SelectedTrackAndArtist
        {
            get { return _selectedTrackAndArtist; }
            set
            {
                _selectedTrackAndArtist = value;
                NotifyPropertyChanged("SelectedTrackAndArtist");
            }
        }

        // 再生中の曲の位置の文字列
        private string _currentPosition = "00:00";
        /// <summary>再生中の曲の位置の文字列</summary>
        public string CurrentPosition
        {
            get { return _currentPosition; }
            set
            {
                _currentPosition = value;
                NotifyPropertyChanged("CurrentPosition");
            }
        }

        // 再生中の曲の長さの文字列
        private string _currentMusicTimeString;
        /// <summary>再生中の曲の長さの文字列</summary>
        public string CurrentMusicTimeString
        {
            get { return _currentMusicTimeString; }
            set
            {
                _currentMusicTimeString = value;
                NotifyPropertyChanged("CurrentMusicTimeString");
            }
        }

        // タイマー
        private DispatcherTimer _timer;

        private void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(info));
        }

        /// <summary>再生ボタンが押されたときのコマンド</summary>
        public ICommand OnPlayButtonClickCommand { get; set; }

        /// <summary>停止ボタンが押されたときのコマンド</summary>
        public ICommand OnStopButtonClickCommand { get; set; }

        /// <summary>ListViewで選択された曲が変わったときのコマンド</summary>
        public ICommand OnSelectionChangedCommand { get; set; }

        /// <summary>音量スライダーの値が変わったときのコマンド</summary>
        public ICommand OnVolumeValueChangedCommand { get; set; }

        /// <summary>シークバーが押されたときのコマンド</summary>
        public ICommand OnSeekbarMouseLeftButtonDownCommand { get; set; }
        /// <summary>シークバーを動かしたときのコマンド</summary>
        public ICommand OnSeekbarValueChangedCommand { get; set; }
        /// <summary>シークバーが離されたときのコマンド</summary>
        public ICommand OnSeekbarMouseLeftButtonUpCommand { get; set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainWindowViewModel() 
        {
            // フォルダ内の音楽の読込
            MusicList = this._model.LoadMusicFiles();
            // 再生ボタン用のコマンド
            OnPlayButtonClickCommand = new MyRelayCommand(OnPlayButtonClick);
            // 停止ボタン用のコマンド
            OnStopButtonClickCommand = new MyRelayCommand(OnStopButtonClick);
            // ListView用のコマンド
            OnSelectionChangedCommand = new RelayCommand<SelectionChangedEventArgs>(OnSelectionChanged);
            // 音量スライダー用のコマンド
            OnVolumeValueChangedCommand = new RelayCommand<RoutedPropertyChangedEventArgs<double>>(OnVolumeValueChanged);
            // シークバー用のコマンド
            OnSeekbarMouseLeftButtonDownCommand = new RelayCommand<RoutedPropertyChangedEventArgs<double>>(OnSeekbarPreviewMouseDown);
            OnSeekbarValueChangedCommand = new RelayCommand<RoutedPropertyChangedEventArgs<double>>(OnSeekbarValueChanged);
            OnSeekbarMouseLeftButtonUpCommand = new RelayCommand<RoutedPropertyChangedEventArgs<double>>(OnSeekbarPreviewMouseUp);
            // タイマーの初期化
            SetupTimer();
        }

        /// <summary>
        /// 再生ボタンが押されたときの処理
        /// </summary>
        private void OnPlayButtonClick()
        {
            // 起動直後で何も選択されていないときは何もしない
            if (SelectedMusic == null) return;
            // 曲の再生状況に応じて処理を分岐
            switch (this._outputDevice.PlaybackState)
            {
                case PlaybackState.Stopped:
                    // 状態①:停止→再生への変更
                    // 再生ボタンのテキストを一時停止に変更
                    PlayButtonContent = "||";
                    // 選択された曲を現在再生中の曲として設定
                    this._currentMusic = SelectedMusic;
                    // 出力デバイスに曲を設定
                    this._outputDevice.Init(InitializeStream(this._currentMusic));
                    // シークバーの初期化処理
                    SetupSeekbar();
                    // ステータスバーの初期化処理
                    InitializeCommonInfo(this._currentMusic);
                    // 再生
                    this._outputDevice.Play();
                    // タイマースタート
                    this._timer.Start();
                    break;
                case PlaybackState.Paused:
                    // 状態④:一時停止→再生への変更
                    // 再生ボタンのテキストを一時停止に変更
                    PlayButtonContent = "||";
                    if (_model.IsSelectedMusicChanged(_musicList,_currentMusic,_selectedMusic))
                    {
                        // 一時停止中に選択楽曲が変更されていた場合
                        // 停止処理
                        OnStopButtonClick();
                        // 再生開始処理
                        OnPlayButtonClick();
                        return;
                    }
                    // 再生
                    this._outputDevice.Play();
                    break;
                case PlaybackState.Playing:
                    // 状態②:再生→一時停止への変更
                    // 再生ボタンのテキストを再生に変更
                    PlayButtonContent = "▶";
                    this._outputDevice.Pause();
                    break;
                default:
                    break;
            }
        }

        /// <summary>
        /// 停止ボタンが押されたときの処理
        /// </summary>
        private void OnStopButtonClick()
        {
            // 曲の再生状況に応じて処理を分岐
            switch (this._outputDevice.PlaybackState)
            {
                case PlaybackState.Playing:
                    // 状態③:再生→停止への変更
                    // 再生ボタンのテキストを再生に変更
                    PlayButtonContent = "▶";
                    break;
                case PlaybackState.Paused:
                    // 状態⑤:一時停止→停止への変更
                    // 再生ボタンのテキストを再生に変更
                    PlayButtonContent = "▶";
                    break;
            }
            // タイマーストップ
            this._timer.Stop();
            // シークバーの位置を一番左に戻す
            SeekbarCurrentPosition = 0;
            // 現在位置の文字列を初期化
            CurrentPosition = "00:00";
            // 停止
            _outputDevice.Stop();
        }

        /// <summary>
        /// ListViewで選択されたものが変わったときの処理
        /// </summary>
        /// <param name="e"></param>
        private void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            if (e.AddedItems.Count != 0) this.SelectedMusic = (Music)e.AddedItems[0];
        }

        /// <summary>
        /// AudioFileReaderを初期化する
        /// </summary>
        /// <param name="music"></param>
        /// <returns></returns>
        private AudioFileReader InitializeStream(Music music)
        {
            // サウンドファイルを読み込む
            this._afr = new AudioFileReader(music.Path);
            // 音量の設定
            _afr.Volume = ConvertVolume(Volume);
            return _afr;
        }

        /// <summary>
        /// タイマーの初期化処理
        /// </summary>
        private void SetupTimer()
        {
            this._timer = new DispatcherTimer(DispatcherPriority.Normal)
            {
                // インターバル
                Interval = TimeSpan.FromMilliseconds(500),
            };
            // タイマーメソッド
            this._timer.Tick += (sender, e) =>
            {
                // シークバーがクリックされていない通常時の処理
                // 再生中の曲の現在の位置を取得してシークバーに反映
                if (_mouseDownFlg == false) SeekbarCurrentPosition = (long)_afr.Position;
                // 再生中の曲の現在の時間をテキストに反映
                CurrentPosition = _afr.CurrentTime.Minutes.ToString("D2") + ":" + _afr.CurrentTime.Seconds.ToString("D2");

                // 再生中に停止した(1曲の再生が終わった)とき
                if (this._outputDevice.PlaybackState == PlaybackState.Stopped)
                {
                    // シークバーの位置を一番左に戻す
                    SeekbarCurrentPosition = 0;
                    // 次の曲がある場合は再生、無いならタイマーを止める
                    PlayNextOrStopTimer(this._currentMusic);
                }
            };
        }

        /// <summary>
        /// 次に再生する曲があるなら再生する、無ければタイマーを止める
        /// </summary>
        /// <param name="currentMusic"></param>
        private void PlayNextOrStopTimer(Music currentMusic)
        {
            // 再生中の曲のインデックスを取得
            int index = MusicList.IndexOf(currentMusic);
            if (MusicList.Count != index + 1)
            {
                // 次の曲がまだある時の処理
                // 再生中の曲を更新
                this._currentMusic = MusicList[++index];
                // outputDeviceに曲をセット
                this._outputDevice.Init(InitializeStream(this._currentMusic));
                // シークバーの初期化処理
                SetupSeekbar();
                // ステータスバーの初期化処理
                InitializeCommonInfo(this._currentMusic);
                // 再生ボタンのテキストを一時停止に変更
                PlayButtonContent = "||";
                // 再生
                this._outputDevice.Play();
            }
            else
            {
                // 再生ボタンのテキストを変える
                PlayButtonContent = "▶";
                // 次に再生できる曲がないのでタイマーを停止
                _timer.Stop();
            }
        }

        /// <summary>
        /// 音量スライダーの値が変わったときの処理
        /// </summary>
        /// <param name="e"></param>
        private void OnVolumeValueChanged(RoutedPropertyChangedEventArgs<double> e)
        {
            Volume = (int)e.NewValue;
            if (_afr != null) _afr.Volume = ConvertVolume(Volume);
        }

        /// <summary>
        /// 引数を100で割ったfloat値に変換する
        /// </summary>
        /// <param name="param"></param>
        /// <returns>引数を100で割ったfloat値</returns>
        private float ConvertVolume(int param)
        {
            return (float)param / (float)100;
        }

        /// <summary>
        /// シークバーを押したときの処理
        /// </summary>
        /// <param name="e"></param>
        private void OnSeekbarPreviewMouseDown(RoutedPropertyChangedEventArgs<double> e)
        {
            if (_afr == null) return;
            _mouseDownFlg = true;
            _outputDevice.Pause();
        }

        /// <summary>
        /// シークバーを動かしたときの処理
        /// </summary>
        /// <param name="e"></param>
        private void OnSeekbarValueChanged(RoutedPropertyChangedEventArgs<double> e)
        {
            // 曲の再生位置を更新
            if (_mouseDownFlg == true)
            {
                _afr.Position = (long)e.NewValue;
            }
        }

        /// <summary>
        /// シークバーを離したときの処理
        /// </summary>
        /// <param name="e"></param>
        private void OnSeekbarPreviewMouseUp(RoutedPropertyChangedEventArgs<double> e)
        {
            if (_mouseDownFlg == false) return;
            _mouseDownFlg = false;
            if (!PlayButtonContent.Equals("▶")) _outputDevice.Play();
        }

        /// <summary>
        /// シークバーの初期化処理
        /// </summary>
        private void SetupSeekbar()
        {
            SeekbarMax = (long)_afr.Length;
        }

        /// <summary>
        /// 再生する楽曲の基本情報を初期化
        /// </summary>
        /// <param name="music"></param>
        private void InitializeCommonInfo(Music music)
        {
            // 曲名とアーティストの表示
            SelectedTrackAndArtist = "♪ " + music.Title + " / " + music.Artist;
            // 曲の長さの設定
            CurrentMusicTimeString = music.Time.Minutes.ToString("D2") + ":" + music.Time.Seconds.ToString("D2");
        }
    }
}

スライダーを実装する前に

今回はスライダーで音量と曲の再生位置を操作できるようにします。
また、再生中の楽曲名と作曲者名を表示してステータスバーの表示を充実させていきたいと思います。
早速やっていきましょう

音量スライダーを作る

まずは右下の区画をいくつかのGridで分割します。
外側のGridでは2列に分割しています。
分割したうちの左側にWidth="4*"を指定して幅を広くしています。
この区画には後ほど再生位置用のスライダーを実装します。
右側の区画はさらに3列に分割し、真ん中の列にWidth="8*"を指定しています。

MainWindow.xaml

<Grid>
    省略
    <Grid Grid.Column="1" Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="4*" />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            
            <Grid Grid.Column="1" VerticalAlignment="Center">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="1*"/>
                    <ColumnDefinition Width="8*"/>
                    <ColumnDefinition Width="1*"/>
                </Grid.ColumnDefinitions>

                    次の説明で解説
            </Grid>
        </Grid>
    省略
</Grid>

3列に分割したGridにTextBlockSliderを置きます。
TextBlockにはスピーカーのマークを表示するようにしました。
Sliderにはいくつかの設定を入れています。

  • MinimumMaximum
    スライダーの最小値、最大値を表します。
    今回は音量を操作したいので0から100の値を操作できるようにしました。

  • Value
    スライダーハンドル(ドラッグして操作する丸い部分)がある部分の値です。
    この値をViewModelで使うために変数Volumeをバインドしています。
    またEventTriggerValueChangedイベントに対してコマンドをバインドしています。
    ValueChangedイベントはスライダーハンドルが動いたときに発火するイベントです。
    PassEventArgsToCommandをtrueにして引数を渡します。

<Grid Grid.Column="1" VerticalAlignment="Center">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*"/>
        <ColumnDefinition Width="8*"/>
        <ColumnDefinition Width="1*"/>
    </Grid.ColumnDefinitions>

    <TextBlock Grid.Column="0" Grid.Row="1"
                Margin="-5,5,0,0"
                TextAlignment="Center"
                VerticalAlignment="Center"
                Text="🔊"/>
    <Slider Grid.Column="1"
            x:Name="VolumeSlider"
            Minimum="0"
            Maximum="100"
            Foreground="AntiqueWhite"
            VerticalAlignment="Center"
            Value="{Binding Volume}"
            Margin="5,0,0,0"
            >
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="ValueChanged">
                <cmd:EventToCommand Command="{Binding OnVolumeValueChangedCommand}" PassEventArgsToCommand="True"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </Slider>
</Grid>

MainWindowViewModel.cs

まずは一通り必要なものを用意します。
スライダーのValueにバインドするためのVolumeプロパティを用意しています。
起動時の初期値をとりあえず80に設定しています。
その他はコマンドの設定と初期化を行っています。
引数を利用するためRelayCommandを使用しています。
また、AudioFileReaderの初期化処理に音量を設定する処理を追加しています。

// 音量
private int _volume = 80;
/// <summary>音量</summary>
public int Volume
{
    get { return _volume; }
    set
    {
        _volume = value;
        NotifyPropertyChanged("Volume");
    }
}

/// <summary>音量スライダーの値が変わったときのコマンド</summary>
public ICommand OnVolumeValueChangedCommand { get; set; }

/// <summary>
/// コンストラクタ
/// </summary>
public MainWindowViewModel() 
{
    省略
    // 音量スライダー用のコマンド
    OnVolumeValueChangedCommand = new RelayCommand<RoutedPropertyChangedEventArgs<double>>(OnVolumeValueChanged);
    省略
}

 /// <summary>
/// AudioFileReaderを初期化する
/// </summary>
/// <param name="music"></param>
/// <returns></returns>
private AudioFileReader InitializeStream(Music music)
{
    // サウンドファイルを読み込む
    this._afr = new AudioFileReader(music.Path);
    // 音量の設定
    _afr.Volume = ConvertVolume(Volume);// 次に解説
    return _afr;
}

コマンド用の処理ではスライダーから受け取った値をAudioFileReaderの音量に設定しています。
_afr.Volumeプロパティには0~1.0のfloat値を設定する必要があるため、 ConvertVolumeメソッドで値を変換しています。

/// <summary>
/// 音量スライダーの値が変わったときの処理
/// </summary>
/// <param name="e"></param>
private void OnVolumeValueChanged(RoutedPropertyChangedEventArgs<double> e)
{
    Volume = (int)e.NewValue;
    if (_afr != null) _afr.Volume = ConvertVolume(Volume);
}

/// <summary>
/// 引数を100で割ったfloat値に変換する
/// </summary>
/// <param name="param"></param>
/// <returns>引数を100で割ったfloat値</returns>
private float ConvertVolume(int param)
{
    return (float)param / (float)100;
}

ここまでで一度実行してみて、音楽の再生中に右下のスライダーを動かすと音量が変わることを確認してみましょう。

シークバーを作る

次はシークバー(楽曲の再生位置を指定するスライダー)を実装します。
基本的には音量スライダーと同じように実装できますが、いくつか追加で処理が増えている箇所もあるのでそちらに注目してみてください。

MainWindow.xaml

まずはスライダーを配置します。
場所は音量スライダーの左側の区画です。
HorizontalAlignment="Stretch"Width="Auto"の設定を入れることでウィンドウサイズの収縮に対応しています。
スライダーのValueにはSeekbarCurrentPositionをバインドしています。
これはシークバーの現在の位置をやり取りするために使います。
また、音量スライダーにはなかったMaximumに対してSeekbarMaxをバインドしています。
これはスライダーを右端まで動かしたとき(=最大値)の値を指定しています。
楽曲によってこの最大値が変わるため、ViewModelでこの値を指定できるようにバインドしています。
EventTriggerで検知するイベントは3種類あります。
ValueChangeは音量スライダーのときにも説明したようにスライダーサムが動いたときに発生するイベントです。
PreviewMouseDownPreviewMouseUpはスライダーハンドル上でマウスのボタンが押された時、離れた時にそれぞれ発生します。
各イベントにバインドしているコマンドについてはこのあと説明します。

<Grid Grid.Column="1" Grid.Row="1">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="4*" />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <Grid Grid.Column="0"  VerticalAlignment="Center">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="8*"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="5*" />
            <RowDefinition Height="4*"/>
        </Grid.RowDefinitions>

        <Slider Grid.Row="1" Grid.ColumnSpan="3"
                x:Name="Seekbar"
                Foreground="AntiqueWhite"
                Margin="10,0,10,0"
                VerticalAlignment="Center"
                Maximum="{Binding SeekbarMax}"
                HorizontalAlignment="Stretch"
                Width="Auto"
                Value="{Binding SeekbarCurrentPosition}" 
                >
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="ValueChanged">
                    <cmd:EventToCommand Command="{Binding OnSeekbarValueChangedCommand}" PassEventArgsToCommand="True"/>
                </i:EventTrigger>
                <i:EventTrigger EventName="PreviewMouseDown">
                    <cmd:EventToCommand Command="{Binding OnSeekbarMouseLeftButtonDownCommand}"/>
                </i:EventTrigger>
                <i:EventTrigger EventName="PreviewMouseUp">
                    <cmd:EventToCommand Command="{Binding OnSeekbarMouseLeftButtonUpCommand}"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Slider>
    </Grid>

    <Grid Grid.Column="1" VerticalAlignment="Center">
        省略(音量スライダーのあるGrid)
    </Grid>
</Grid>

MainWindowViewModel.cs

必要なプロパティ、ローカル変数を用意しています。
コンストラクタでコマンドの初期化を行っています。

// シークバーの最大値
private long _seekbarMax = 100;
/// <summary>シークバーの最大値</summary>
public long SeekbarMax
{
    get { return _seekbarMax; }
    set
    {
        _seekbarMax = value;
        NotifyPropertyChanged("SeekbarMax");
    }
}

// シークバーの現在位置
private long _seekbarCurrentPosition = 0;
/// <summary>シークバーの現在位置</summary>
public long SeekbarCurrentPosition
{
    get { return _seekbarCurrentPosition; }
    set
    {
        _seekbarCurrentPosition = value;
        NotifyPropertyChanged("SeekbarCurrentPosition");
    }
}

/// <summary>シークバードラッグ用フラグ</summary>
private bool _mouseDownFlg = false;

/// <summary>シークバーが押されたときのコマンド</summary>
public ICommand OnSeekbarMouseLeftButtonDownCommand { get; set; }
/// <summary>シークバーを動かしたときのコマンド</summary>
public ICommand OnSeekbarValueChangedCommand { get; set; }
/// <summary>シークバーが離されたときのコマンド</summary>
public ICommand OnSeekbarMouseLeftButtonUpCommand { get; set; }

/// <summary>
/// コンストラクタ
/// </summary>
public MainWindowViewModel() 
{
    省略

    // シークバー用のコマンド
    OnSeekbarMouseLeftButtonDownCommand = new RelayCommand<RoutedPropertyChangedEventArgs<double>>(OnSeekbarPreviewMouseDown);
    OnSeekbarValueChangedCommand = new RelayCommand<RoutedPropertyChangedEventArgs<double>>(OnSeekbarValueChanged);
    OnSeekbarMouseLeftButtonUpCommand = new RelayCommand<RoutedPropertyChangedEventArgs<double>>(OnSeekbarPreviewMouseUp);
    // タイマーの初期化
    SetupTimer();
}

シークバーの初期化処理です。
シークバーの最大値に対して読み込まれた曲の長さを設定しています。
AudioFileReaderLengthプロパティはLong型になります。

/// <summary>
/// シークバーの初期化処理
/// </summary>
private void SetupSeekbar()
{
    SeekbarMax = (long)_afr.Length;
}

SetupSeekbar()が呼ばれるタイミングは 初めて曲が再生される時次の曲が再生され始める時 になるのでそのタイミングでこのメソッドを呼び出します。
具体的にはOnPlayButtonClick()で停止→再生になる時と、タイマーの中で次に再生する曲を決めているPlayNextOrStopTimerの中です。(20行目,46行目)

/// <summary>
/// 再生ボタンが押されたときの処理
/// </summary>
private void OnPlayButtonClick()
{
    // 起動直後で何も選択されていないときは何もしない
    if (SelectedMusic == null) return;
    // 曲の再生状況に応じて処理を分岐
    switch (this._outputDevice.PlaybackState)
    {
        case PlaybackState.Stopped:
            // 状態①:停止→再生への変更
            // 再生ボタンのテキストを一時停止に変更
            PlayButtonContent = "||";
            // 選択された曲を現在再生中の曲として設定
            this._currentMusic = SelectedMusic;
            // 出力デバイスに曲を設定
            this._outputDevice.Init(InitializeStream(this._currentMusic));
            // シークバーの初期化処理
            SetupSeekbar();
            // 再生
            this._outputDevice.Play();
            // タイマースタート
            this._timer.Start();
            break;
        省略
    }
}

/// <summary>
/// 次に再生する曲があるなら再生する、無ければタイマーを止める
/// </summary>
/// <param name="currentMusic"></param>
private void PlayNextOrStopTimer(Music currentMusic)
{
    // 再生中の曲のインデックスを取得
    int index = MusicList.IndexOf(currentMusic);
    if (MusicList.Count != index + 1)
    {
        // 次の曲がまだある時の処理
        // 再生中の曲を更新
        this._currentMusic = MusicList[++index];
        // outputDeviceに曲をセット
        this._outputDevice.Init(InitializeStream(this._currentMusic));
        // シークバーの初期化処理
        SetupSeekbar();
        // 再生ボタンのテキストを一時停止に変更
        PlayButtonContent = "||";
        // 再生
        this._outputDevice.Play();
    }
    else
    {
        省略
    }
}

シークバーのスライダーサムが押されている時の処理です。
押されていることを保持するフラグをtrueに設定します。(8行目)
シークバーを動かしている間は曲を一時停止するように_outputDevice.Pause()を呼んでいます。

/// <summary>
/// シークバーを押したときの処理
/// </summary>
/// <param name="e"></param>
private void OnSeekbarPreviewMouseDown(RoutedPropertyChangedEventArgs<double> e)
{
    if (_afr == null) return;
    _mouseDownFlg = true;
    _outputDevice.Pause();
}
```

スライダーサムが動いたとき(ドラッグして位置を決めている)の処理です。
スライダーサムがある地点の値を`_afr.Position`に設定しています。
Long型に変換してから設定します。
``` csharp {.line-numbers}
/// <summary>
/// シークバーを動かしたときの処理
/// </summary>
/// <param name="e"></param>
private void OnSeekbarValueChanged(RoutedPropertyChangedEventArgs<double> e)
{
    // 曲の再生位置を更新
    if (_mouseDownFlg == true)
    {
        _afr.Position = (long)e.NewValue;
    }
}

スライダーサムの移動が終わってマウスボタンから指を離したときの処理です。
_mouseDownFlgをfalseに戻します。
9行目ではシークバーを動かし始めるときに曲が再生中だったらシークバーを動かした位置から再生を再開するように判定を入れています。

/// <summary>
/// シークバーを離したときの処理
/// </summary>
/// <param name="e"></param>
private void OnSeekbarPreviewMouseUp(RoutedPropertyChangedEventArgs<double> e)
{
    if (_mouseDownFlg == false) return;
    _mouseDownFlg = false;
    if (!PlayButtonContent.Equals("▶")) _outputDevice.Play();
}

シークバーの現在位置を再生状況に合わせて動かす

MainWindowViewModel
次に曲の再生が進むに連れてシークバーがじわじわと右に動くような処理を作ります。
タイマーメソッドの中で、_afr.Positionの値を取得してSeekbarCurrentPositionに設定しています。
ただしシークバーがクリックされている(スライダーサムを動かして再生位置を決めている)ときには設定しません。
また、曲が停止したときにはシークバーの位置を一番左に戻す処理も入れています。

/// <summary>
/// タイマーの初期化処理
/// </summary>
private void SetupTimer()
{
    this._timer = new DispatcherTimer(DispatcherPriority.Normal)
    {
        // インターバル
        Interval = TimeSpan.FromMilliseconds(500),
    };
    // タイマーメソッド
    this._timer.Tick += (sender, e) =>
    {
        // シークバーがクリックされていない通常時の処理
        // 再生中の曲の現在の位置を取得してシークバーに反映
        if (_mouseDownFlg == false) SeekbarCurrentPosition = (long)_afr.Position;

        // 再生中に停止した(1曲の再生が終わった)とき
        if (this._outputDevice.PlaybackState == PlaybackState.Stopped)
        {
            // シークバーの位置を一番左に戻す
            SeekbarCurrentPosition = 0;
            // 次の曲がある場合は再生、無いならタイマーを止める
            PlayNextOrStopTimer(this._currentMusic);
        }
    };
}

/// <summary>
/// 停止ボタンが押されたときの処理
/// </summary>
private void OnStopButtonClick()
{
    // 曲の再生状況に応じて処理を分岐
    省略
    // タイマーストップ
    this._timer.Stop();
    // シークバーの位置を一番左に戻す
    SeekbarCurrentPosition = 0;
    // 停止
    _outputDevice.Stop();
}

ステータスバーに再生時間と作曲者の情報を表示する

最後にステータスバーの表示を追加します。
今回は

  • 曲の今再生されている場所の時間

  • 曲名と作曲者名

  • 曲の総再生時間を表示したいと思います。

MainWindow.xaml
先程実装したスライダーの上側に3つのTextBlockを追加しました。

<Grid Grid.Column="0"  VerticalAlignment="Center">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*"/>
        <ColumnDefinition Width="8*"/>
        <ColumnDefinition Width="1*"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="5*" />
        <RowDefinition Height="4*"/>
    </Grid.RowDefinitions>

    <TextBlock Grid.Column="0"
                x:Name="CurrentPosition"
                Margin="10,5,0,0"
                TextAlignment="Left"
                Text="{Binding CurrentPosition}"/>
    <TextBlock Grid.Column="1"
                TextAlignment="Center"
                Margin="0,5,0,0"
                Text="{Binding SelectedTrackAndArtist}"/>
    <TextBlock Grid.Column="2"
                x:Name="CurrentMusicTimeString"
                TextAlignment="Right"
                Margin="0,5,20,0"
                Text="{Binding CurrentMusicTimeString}"/>

    <Slider Grid.Row="1" Grid.ColumnSpan="3"
            省略
            >
    </Slider>
</Grid>

MainWindowViewModel.cs

バインドするためのプロパティを用意しました。

// 曲名と作曲者名を合わせた文字列
private string _selectedTrackAndArtist = "";
/// <summary>再生中の曲名とアーティスト名を連結した文字列</summary>
public string SelectedTrackAndArtist
{
    get { return _selectedTrackAndArtist; }
    set
    {
        _selectedTrackAndArtist = value;
        NotifyPropertyChanged("SelectedTrackAndArtist");
    }
}

// 再生中の曲の位置の文字列
private string _currentPosition = "00:00";
/// <summary>再生中の曲の位置の文字列</summary>
public string CurrentPosition
{
    get { return _currentPosition; }
    set
    {
        _currentPosition = value;
        NotifyPropertyChanged("CurrentPosition");
    }
}

// 再生中の曲の長さの文字列
private string _currentMusicTimeString;
/// <summary>再生中の曲の長さの文字列</summary>
public string CurrentMusicTimeString
{
    get { return _currentMusicTimeString; }
    set
    {
        _currentMusicTimeString = value;
        NotifyPropertyChanged("CurrentMusicTimeString");
    }
}

SelectedTrackAndArtistCurrentMusicTimeStringを初期化するためのメソッドを用意しました。

/// <summary>
/// 再生する楽曲の基本情報を初期化
/// </summary>
/// <param name="music"></param>
private void InitializeCommonInfo(Music music)
{
    // 曲名とアーティストの表示
    SelectedTrackAndArtist = "♪ " + music.Title + " / " + music.Artist;
    // 曲の長さの設定
    CurrentMusicTimeString = music.Time.Minutes.ToString("D2") + ":" + music.Time.Seconds.ToString("D2");
}

InitializeCommonInfoメソッドが呼ばれるのはシークバーの初期化処理をしたときと同じタイミングになります。
引数には_currentMusicを指定します。

/// <summary>
/// 再生ボタンが押されたときの処理
/// </summary>
private void OnPlayButtonClick()
{
    // 起動直後で何も選択されていないときは何もしない
    if (SelectedMusic == null) return;
    // 曲の再生状況に応じて処理を分岐
    switch (this._outputDevice.PlaybackState)
    {
        case PlaybackState.Stopped:
            // 状態①:停止→再生への変更
            // 再生ボタンのテキストを一時停止に変更
            PlayButtonContent = "||";
            // 選択された曲を現在再生中の曲として設定
            this._currentMusic = SelectedMusic;
            // 出力デバイスに曲を設定
            this._outputDevice.Init(InitializeStream(this._currentMusic));
            // シークバーの初期化処理
            SetupSeekbar();
            // ステータスバーの初期化処理
            InitializeCommonInfo(this._currentMusic);
            // 再生
            this._outputDevice.Play();
            // タイマースタート
            this._timer.Start();
            break;
            省略
    }
}

/// <summary>
/// 次に再生する曲があるなら再生する、無ければタイマーを止める
/// </summary>
/// <param name="currentMusic"></param>
private void PlayNextOrStopTimer(Music currentMusic)
{
    // 再生中の曲のインデックスを取得
    int index = MusicList.IndexOf(currentMusic);
    if (MusicList.Count != index + 1)
    {
        // 次の曲がまだある時の処理
        // 再生中の曲を更新
        this._currentMusic = MusicList[++index];
        // outputDeviceに曲をセット
        this._outputDevice.Init(InitializeStream(this._currentMusic));
        // シークバーの初期化処理
        SetupSeekbar();
        // ステータスバーの初期化処理
        InitializeCommonInfo(this._currentMusic);
        // 再生ボタンのテキストを一時停止に変更
        PlayButtonContent = "||";
        // 再生
        this._outputDevice.Play();
    }
    else
    {
        省略
    }
}

タイマーメソッドと停止ボタンにもCurrentPosition文字列を変更する処理を入れます。
タイマーメソッドでは再生中の曲の再生位置を取得して文字列に変換しています。

/// <summary>
/// タイマーの初期化処理
/// </summary>
private void SetupTimer()
{
    this._timer = new DispatcherTimer(DispatcherPriority.Normal)
    {
        // インターバル
        Interval = TimeSpan.FromMilliseconds(500),
    };
    // タイマーメソッド
    this._timer.Tick += (sender, e) =>
    {
        // シークバーがクリックされていない通常時の処理
        // 再生中の曲の現在の位置を取得してシークバーに反映
        if (_mouseDownFlg == false) SeekbarCurrentPosition = (long)_afr.Position;
        // 再生中の曲の現在の時間をテキストに反映
        CurrentPosition = _afr.CurrentTime.Minutes.ToString("D2") + ":" + _afr.CurrentTime.Seconds.ToString("D2");

        // 再生中に停止した(1曲の再生が終わった)とき
        if (this._outputDevice.PlaybackState == PlaybackState.Stopped)
        {
           省略
        }
    };
}
/// <summary>
/// 停止ボタンが押されたときの処理
/// </summary>
private void OnStopButtonClick()
{
    // 曲の再生状況に応じて処理を分岐
    switch (this._outputDevice.PlaybackState)
    {
        省略
    }
    // タイマーストップ
    this._timer.Stop();
    // シークバーの位置を一番左に戻す
    SeekbarCurrentPosition = 0;
    // 現在位置の文字列を初期化
    CurrentPosition = "00:00";
    // 停止
    _outputDevice.Stop();
}

それでは実行してみましょう。
再生中にシークバーが動いたり、スライダーを操作して曲の再生位置を変えたりできるようになったでしょうか。
これで一通りの機能を実装し終わりました!

おわりに

今回解説したのはあくまで音楽プレイヤーとしての最低限の機能になります。
ここからさらに機能を加えるとしたら

  • シャッフル機能

  • プレイリスト機能

  • 次へ/前へボタン

などになるでしょうか。

また、今回はほとんどの機能をViewModel上に記載しました。
MVVMについては自分自身もよく理解しきれていないところがあり、もっと勉強しないとなと思います。

ありがとうございました。


いいなと思ったら応援しよう!