WPFとNAudioで音楽プレイヤーを作る~第6回:ListViewに表示した楽曲を選択して再生する~

第6回:ListViewに表示した楽曲を選択して再生する

今回のポイント

  • MvvmLightのEventToCommandでViewModelへEventArgsを渡す

完成形

先にMainWindowViewModel.csMainWindow.xamlの完成形を貼ります。
解説と合わせてご確認ください。

MainWindowViewModel.cs

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

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

        public MainWindowModel _model = new MainWindowModel();

        // 音楽の一覧
        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 void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(info));
        }

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

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

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainWindowViewModel() 
        {
            // フォルダ内の音楽の読込
            MusicList = this._model.LoadMusicFiles();
            // 再生ボタン用のコマンド
            OnPlayButtonClickCommand = new MyRelayCommand(OnPlayButtonClick);
            // ListView用のコマンド
            OnSelectionChangedCommand = new RelayCommand<SelectionChangedEventArgs>(OnSelectionChanged);
        }

        /// <summary>
        /// 再生ボタンが押されたときの処理
        /// </summary>
        private void OnPlayButtonClick()
        {
            var dialog = new OpenFileDialog();
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                WaveOutEvent outputDevice = new WaveOutEvent();
                AudioFileReader afr = new AudioFileReader(dialog.FileName);
                outputDevice.Init(afr);
                outputDevice.Play();
            }
        }

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

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>

        <Button 
            Grid.Column="0" Grid.Row="1"
            Width="30" Height="30"
            Content="▶"
            Command="{Binding OnPlayButtonClickCommand}"
            />

        <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>
</Window>

ListViewで選択されたものをViewModelで扱う

前回まででListViewにフォルダ内の楽曲を一覧表示できるようになりました。
再生ボタンの処理を変更して、ListViewで選択した楽曲を再生できるようにします。

  • MvvmLightを導入する
    後ほど必要になるのでMvvmLightを導入します。
    例によってプロジェクト > NuGetパッケージの管理からmvvmlightと検索してインストールするだけです。

  • ViewModelに選択された楽曲を保持するプロパティを用意する
    ListViewで選択された楽曲を保持するためのMusic型のプロパティを用意します。
    今回はViewModel側での変更をView側に伝える必要はないので、セッターにNotifyPropertyChangedを実装していません。

        // 選択された楽曲
        private Music _selectedMusic;
        public Music SelectedMusic
        {
            get { return this._selectedMusic; }
            set { this._selectedMusic = value; }
        }
  • ListViewで選択された楽曲が変更されたときのコマンドを作る
    ListViewで選択されたものが変わるとSelectionChangedというイベントが発火します。
    その時に飛んでくるSelectionChangedEventArgsを捕まえて、選択されたMusicをプロパティに設定しています。
    14行目ではMyRelayCommandではなく先程導入したMvvmLightRelayCommandを使ってコマンドを定義していることに注意してください。
    残念ながらMyRelayCommandではEventArgsが扱えないためこうしています。
    23行目ではSelectionChangedEventArgsから値を取得してプロパティにセットしています。
    セットする前にArgsの中身があるかをチェックしています。

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

/// <summary>
/// コンストラクタ
/// </summary>
public MainWindowViewModel() 
{
    // フォルダ内の音楽の読込
    MusicList = this._model.LoadMusicFiles();
    // 再生ボタン用のコマンド
    OnPlayButtonClickCommand = new MyRelayCommand(OnPlayButtonClick);
    // ListView用のコマンド
    OnSelectionChangedCommand = new RelayCommand<SelectionChangedEventArgs>(OnSelectionChanged);
}

/// <summary>
/// ListViewで選択されたものが変わったときの処理
/// </summary>
/// <param name="e"></param>
private void OnSelectionChanged(SelectionChangedEventArgs e)
{
    if (e.AddedItems.Count != 0) this.SelectedMusic = (Music)e.AddedItems[0];
}
  • ListViewで選択された楽曲が変わったときにコマンドを呼ぶ
    ViewModelでListViewの変更を検知する仕組みが整ったので、Viewから変更を通知するように修正します。
    まずはWindowタグ内に以下の2行を追加します。

        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:cmd="http://www.galasoft.ch/mvvmlight"

次にListView.Viewタグの次に以下の記述を追加します。
i:EventTriggerでは何のイベントが発生したときの処理を追加するかを指定しています。
今回はListViewで選択されたものが変更されたときなのでSelectionChangedを指定します。
cmd:EventToCommandMvvmLightの機能で、イベントが発生したときに呼ぶコマンドをバインドしています。
ViewModelで作成したOnSelectionChangedCommandをバインドしましょう。
ここでPassEventArgsToCommandプロパティをTrueに設定するのがポイントです。
こうすることでコマンドに引数を渡すことができます。

<i:Interaction.Triggers>
    <i:EventTrigger EventName="SelectionChanged">
        <cmd:EventToCommand Command="{Binding OnSelectionChangedCommand}" PassEventArgsToCommand="True"/>
    </i:EventTrigger>
</i:Interaction.Triggers>

これで選択された楽曲がViewModelで扱えるようになりました。

再生ボタンの処理を修正する

それでは再生ボタンの処理を修正します。
ダイアログを開いて曲を選択する処理とはここでお別れです。
まずはローカル変数をいくつか用意します。
音楽再生に必要なAudioFileReaderWaveOutEventの変数を用意しました。
_currentMusicは現在再生中の楽曲を保持するために用意しました。
SelectedMusicではListViewが操作されるたびに変わってしまうためです。

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

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

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

次にAudioFileReaderを初期化するためのメソッドを用意しました。
引数としてMusicを渡しています。

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

OnPlayButtonClickメソッドは以下のように修正しました。
まず起動直後はListViewで何も選択されていない状態なので、そのときは何もせず終了します(7行目)。
再生可能なとき(ListViewで何かが選択されているとき)はWaveOutDeviceの状態に応じて処理を分岐します。
状態は_outputDevice.PlaybackStateプロパティで確認できます。
今回は停止状態→再生状態への遷移なのでcase PlaybackState.Stopped:に記載しています。

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

case PlaybackState.Paused:case PlaybackState.Playing:のときの処理はまだありません。
ここの処理は次回追加します。

ここまでできたらListViewから楽曲を選択して、再生ボタンを押すと再生できることを確認してみましょう。

さて、再生できるようにはなりましたが以下のように問題はいくつかあります。

  • 楽曲が停止できない

  • 再生中に楽曲が変更できない

  • 連続再生ができず、1曲の再生が終わったあと止まってしまう

次回はこれらを一気に修正して、より音楽プレイヤーらしくしていきたいと思います。

参考

https://qiita.com/takanemu/items/efbe7ab1b1272251720a


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