前回の前提を踏まえ、それでは比較的簡単な音声合成を使うプログラムからやりましょうか。

 

今回のお題である「音声合成」をwebであさっていたら、2019年のIlia SmirnovさんによるMicrosoft Learnの英文記事(注)

に小ネタが取り上げられており、その4番目のものを、英語+ロシア語から英語+日本語にしてウィンドウプログラムに改造してみました。

注:Speech - Text-To-Speech Synthesis in .NET | Microsoft Learn

また、ウィンドウ関係は今までWPFを学んできたのでその流れでプログラミングしてみます。(但し、Visual Studio+Xamlを使わないので、C#のコードだけでやります。)

 

プログラムを起動すると、3つのWPFボタンが付いたウィンドウが現れます。

//解説:↑ではマウスが"Exit"ボタンに掛かっているので背景色が変わっています。

英語ボタンを押すと英語で、日本語ボタンを押すと日本語で、(↑のMicrosoft Learnの記事ではフィンランドのVantaa空港での)スペイン行き乗り換え便案内をしゃべります。又、Exitボタンまたは「✖」システムボタンでプログラムを終了させる際にも(私の記憶にある)「(コロナのおかげで)懐かしいセリフ」をしゃべってくれます。

但し、

私的評価では「英語(Zira)は完璧。日本語(Haruka)は乗換案内は(『お進みください』がちょっと危なっかしいが)何とかクリア。しかし、最後のHarukaのセリフ(『誠に』)は完全に訛ってます!!!」です。

 

【Speech.Synthesizer04.cs】

 

///////////////////////////
// Speech.Synthesizer04.cs

///////////////////////////
using System;
using System.Windows;            //PresentationFramework.dllにある
using System.Windows.Controls;    //PresentationFramework.dllにある
using System.Collections.Generic;    //解説:IDictionaryというキーと値がペアの配列を使うので
using System.Globalization;    //解説:CultureInfoを使うので(何故C++等アンマネージドコードの開発では "ロケール" と呼ぶのでしょう?分からない...)
using System.Speech.Synthesis;    //System.Speech.dllにある(Ref:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Speech\v4.0_4.0.0.0__31bf3856ad364e35\System.Speech.dll)

namespace Speech.Synthesizer04
{
    public partial class MainWindow : Window
    {
        private const string en = "en-US";
        //private const string ru = "ru-RU";    //解説:オリジナルは英語とロシア語でした。
        private const string jp = "jp-JP";    //解説:日本語を追加しています。
        private readonly IDictionary<string, string> _messagesByCulture = new Dictionary<string, string>();
        //解説:今回はキーと値のペアが両方とも文字列です。
        public MainWindow()
        {
            this.Title = "SpeechSynthesizer サンプル";
            this.Width = 320;            //ウィンドウ幅
            this.Height = 280;            //ウィンドウ高
            this.MinWidth = 320;        //ウィンドウ最小幅
            this.MinHeight = 280;        //ウィンドウ最小高
            /* ダイアログタイプ */
            //this.WindowStyle = WindowStyle.ThreeDBorderWindow;
            //this.ResizeMode = ResizeMode.NoResize;
            /* サイズ変更ウィンドウタイプ */
            this.WindowStyle = WindowStyle.SingleBorderWindow;
            this.ResizeMode = ResizeMode.CanResizeWithGrip;    //解説:サイズ変更用の「グリップ」が付いています。
            this.WindowState = WindowState.Normal;
            this.WindowStartupLocation = WindowStartupLocation.CenterScreen;    //Manual, CenterOwnerもある
            this.Background = SystemColors.ControlBrush;
            InitControls();
            PopulateMessages();
            this.Closing += Window_Closing;
        }

        public void InitControls()
        {
            //スタックパネル(StackPanel)を作成
            StackPanel stackPanel = new StackPanel();
            stackPanel.Width = 300;
            stackPanel.Height = 260;
            //ボタン1
            Button Btn1 = new Button();
            Btn1.Width = 240;
            Btn1.Height = 60;
            Btn1.HorizontalAlignment = HorizontalAlignment.Center;
            Btn1.VerticalAlignment = VerticalAlignment.Center;
            //Btn1.HorizontalContentAlignment = HorizontalAlignment.Left;    //ボタン内のコンテントの位置
            //Btn1.VerticalContentAlignment = VerticalAlignment.Bottom;        //ボタン内のコンテントの位置
            Btn1.Margin = new Thickness(10, 10, 10, 10);    //構造体 Thickness(左, 上, 右, 下)
            Btn1.Content = "Englich";
            Btn1.Click += PromptInEnglish;
            stackPanel.Children.Add(Btn1);
            //ボタン2ボタン
            Button Btn2 = new Button();
            Btn2.Width = 240;
            Btn2.Height = 60;
            Btn2.HorizontalAlignment = HorizontalAlignment.Center;
            Btn2.VerticalAlignment = VerticalAlignment.Center;
            Btn2.Margin = new Thickness(10, 10, 10, 10);    //構造体 Thickness(左, 上, 右, 下)
            Btn2.Content = "Japanese";    //解説:もともとはロシア語でしたが...
            Btn2.Click += PromptInJapanese;
            stackPanel.Children.Add(Btn2);
            //終了ボタン
            Button extBtn = new Button();
            extBtn.Width = 240;
            extBtn.Height = 60;
            extBtn.HorizontalAlignment = HorizontalAlignment.Center;
            extBtn.VerticalAlignment = VerticalAlignment.Center;
            extBtn.Margin = new Thickness(10, 10, 10, 10);    //構造体 Thickness(左, 上, 右, 下)
            extBtn.Content = "Exit";
            extBtn.Click += ExitBtn;
            stackPanel.Children.Add(extBtn);
            //ウィンドウにスタックパネルを登録
            this.Content = stackPanel;
        }

        [STAThread]
        public static void Main()
        {
            MainWindow mw = new MainWindow();
            Application ap = new Application();    //WPFでは明示的にインスタンスを作らなければならない
            ap.Run(mw);
        }

        //終了処理
        void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            _messagesByCulture[en] = "Thank you for flying with us today.";
            DoPrompt(en);
            _messagesByCulture[jp] = "ご搭乗、誠に有難うございました。";
            DoPrompt(jp);
        }

        private void PromptInEnglish(object sender, RoutedEventArgs e)
        {
            DoPrompt(en);
        }

/*        private void PromptInRussian(object sender, RoutedEventArgs e)
        {
            DoPrompt(ru);       //解説:オリジナルはロシア語でした。
        }
*/

        private void PromptInJapanese(object sender, RoutedEventArgs e)
        {
            DoPrompt(jp);
        }

        private void ExitBtn(object sender, RoutedEventArgs e)
        {
            Close();
        }

        private void DoPrompt(string culture)
        {
            SpeechSynthesizer synthesizer = new SpeechSynthesizer();    //解説:音声合成インスタンスを作ります。
            synthesizer.SetOutputToDefaultAudioDevice();    //解説:標準出力を設定します。

            //解説:若しなければ致命的エラーになります...汗; ここはusingかtry~catchを使うともっと良いでしょうが。
            PromptBuilder builder = new PromptBuilder();    //解説:「プロンプト作成」クラスとは「プロンプト(IT分野では文字や音声で操作情報等を提供するものをいいます)」作成するクラスです。
            builder.StartVoice(new CultureInfo(culture));    //解説:カルチャー(理系は「カルチャ」、localeのこと)を設定して、科白作りを始めます。
            builder.AppendText(_messagesByCulture[culture]);    //解説:あらかじめ用意していた科白を使います。
            builder.EndVoice();    //解説:科白の終了です。
            synthesizer.Speak(builder);    //解説:科白をしゃべらせます。
            synthesizer.Dispose();    //解説:オリジナルのプログラムではメモリー開放を明示的に行わず、ガーベージコレクターに任せていましたが、SpeechSynthesizerクラスの注意書きがあったので追加してみました。
        }

        private void PopulateMessages()
        {
            _messagesByCulture[en] = "For the connection flight 123 to Saint Petersburg, please, proceed to gate A1";
            //_messagesByCulture[ru] = "Для пересадки на рейс 123 в    Санкт-Петербург, пожалуйста, пройдите к выходу A1";    //解説:オリジナルのロシア語です。
            _messagesByCulture[jp] = "スペインのピーターズバーグへのお乗り継ぎは、A1番ゲートへお進みください。";
        }
    }
}

 

テキストなどの読み上げを行う場合にSAPIの音声合成機能を使いますが、その大前提として音声合成と認識が行えるローカルエクスペリエンスパックが導入されていることが前提となります。私の場合はデフォルトの日本語の他、英語(米語)を入れています。

 

更に増やしたいという時はどうするか?と考え、Webで調べるとローカル エクスペリエンス パック - Microsoft サポートというサイトにダウンロードのリンクがありましたが、参照先が自分自身(従って何の役にも立ちません!怒)になっています。

 

では、

ということで、矢張りWindows(11ですが)の「設定」を開くと、「時刻と言語」に「音声認識」があり、こっちの方が早いことが分かりました。

導入されているローカルエクスペリエンスパッケージと同じ「音声パッケージ」として日本語と英語がありますね。増やすには「音声を追加」ボタンを押せばダイアログが現れ、多様な言語をダウンロードできるようです。

(不要なメモリー消費はしないようにしているので、私は2言語だけでOKです。)

 

なお、私のPC環境ではデフォルト音声はAyumiになっており、その他HarukaとIchiroが選択できるのですが、音声合成プログラムを組んで「日本語」(カルチャーを"ja-JP")を選択すると、元のHarukaになってしまうようです。(

:この点について、(当時Windows 10でしたが)私のBCCSkeltonサンプルプログラム、"TextToSpeech"のヘルプファイルで「(レジストリーの) HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices ...というところに、..."Microsoft Haruka Desktop”という名前の成人女性の音声であることが分かります。このPCのSAPI-TTSで使用できる音声(デフォルト音声トークン)は、HARUKAの他に米英語を話すDaividとZiraの3つしかありません。」「実はこのPCの中には上記以外にも、同じマイクロソフトのソフトウェアとしてレジストリーに登録されている”OneCore”と呼ばれるカテゴリーに、より多くの音声トークンがありますが、...SAPIISpVoiceオブジェクトが直ちに利用することができません。SAPIはこの HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens にアクセスできず、プログラマーが強引にOneCoreの音声トークンにアクセスし、SAPIのISpVoiceオブジェクトに与えるしかないのが現状です。」と書いています。(私のTextToSpeechは強引にそこへアクセスしていますが、何か?)

 

今後はこの前提(と私のPC環境)で解説します。

 

【WPF】シリーズで動画再生をやってきましたが、それはC#で「昔のCOMの動画再生や文章読み上げ(TextToSpeech)をどうやっているか」確認する為であり、動画再生をする為にはWPFを学ぶ必要があったからです。しかし、動画再生は一応前回までで終了したので、今後は【WPF】から離れて、「音声合成」(と「音声認識」)をやってみようかと思います。C#でこれらをどうまとめるかが分からず、ヘッダーを【Speech】にしてもう良かったのですが、矢張り歴史に敬意を表して【SAPI】としてみました。

 

ここで話を整理しておくと、SAPIというのは「Windowsアプリケーションで音声認識や音声合成を使うためにマイクロソフトが開発したAPI」で、初めはプリミティブなAPIだったようですが、2000年のSAPI 5.0の段階ではラッパーライブラリーとしてsapi.dllに纏められ、これを通じて利用するようになったようです。(「新APIは当初 COM API のみであり、C/C++ からのみ使えるようになっていた。VB やスクリプト言語のサポートは後から追加された。OSとしては、Windows 98 と NT 4.0 以降でサポートされた。」by wiki

私の初遭遇は2003年に(当時持っていた)Borland C++ Builder 5.0(BCB)でSAPIのCOM(TTS-TextToSpeech)をBCBがコンポーネント(Windows コントロール)に変換し、ウィンドウに貼り付けて使った時でした。この時はコントロールに紅い唇が現れ、話す英語に合わせて動きました。(注)

注:この時の印象が強烈だったので、私がBCCSkeltonでCVOICEというSAPI COMのラッパークラスを作り、そのテストプログラムとしてTextToSpeachというアプリを作った時のプログラムアイコンを↓の様にしました。(絵心、無いですねぇ。なお、TextToSpeechはBCCForm and BCCSkeltonのサンプルに入っています。)

 

現在のC#でどうなっているかということで調べてみると、先ず「音声読み上げ」機能は(SAPI 5.0では既に入っていた)「音声認識機能」と共に、System.Speech.dll(注)というライブラリーに纏められており、

System.Speech.Synthesis(音声合成用SpeechSynthesizerクラス等が入っています。)

System.Speech.Recognition(音声認識用SpeechRecognitionEngineクラス等が入っています。)

という名前空間を持っています。

注:"C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Speech\v4.0_4.0.0.0__31bf3856ad364e35"というWindows 10/11のフォールダーにあります。

 

これらは既にラッパークラスになっているのでWinFormでも、WPFでも使用できます。使い方はMicrosoft Learningにも出ていますが、次回以降WPFの学習の際にMicrosoft Learningのサンプルを参考に(私のPC環境に合わせて作った)自作サンプルで解説してみます。

 

前回コントロールとしてMediaElementを使いました。今回は一味違うMediaPlayerを使ってみます。

 

実はこのMediaPlayer、(WPFのことを何も知らない)以前にWinFormで貼り付けたことがあります。この時の結果は、

追記:アップしてから、「↑のコードでどうなったの?」という結論を書き忘れましたぁ!実は↑のコードは「正常にコンパイルされ、動くことは動き」ます。ただ、動画ファイルを読み込んで再生しても「音しか出ない」だけです。まぁ、画像処理部分が解説した通りなので、再生されるわけがないのですが...

となりました。今回はきちんとWPFで書きますので、赤字部分がどうなるか解説します。(なお、以下の色分けはコメント解説MediaPlayerが動画を表示するUIElementビデオ再生VideoDrawingクラス)、ピンク描画ブラシとなります。)

 

【Test_MediaPlayer.cs】

///////////////////////////////////////////
//Test_MediaPlayer.cs - MediaPlayer Sample
///////////////////////////////////////////

using System;
using System.Windows;            //PresentationFramework.dllにある
using System.Windows.Controls;    //PresentationFramework.dllにある
using System.Windows.Media;        //PresentationCore.dllにある(MediaPlayer, Brushes構造体使用の為)
/* 参照DLL
RefFile=C:\Windows\Microsoft.NET\Framework\v4.0.30319\WPF\PresentationFramework.dll
RefFile01=C:\Windows\Microsoft.NET\Framework\v4.0.30319\WPF\PresentationCore.dll
RefFile02=C:\Windows\Microsoft.NET\Framework\v4.0.30319\WPF\WindowsBase.dll
RefFile03=C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Xaml.dll
*/


namespace Test_MediaPlayer
{
    ///////////////////////////
    //エントリーポイントクラス
    ///////////////////////////

    class MainApp
    {
        [STAThread]
        public static void Main()
        {
            DlgWindow dlg = new DlgWindow();
            Application ap = new Application();    //明示的にインスタンスを作らなければならない
            ap.Run(dlg);
        }
    }

    public partial class DlgWindow : Window    //派生元が"Form"から"Window"となる
    {
        //コントロール
        //private MediaElement me;    //解説:今回はMediaElementを使いません。

        private MediaPlayer mp = null;    //解説:これが噂のMediaPlayerクラスインスタンスです。
        private Canvas canvas = null;    //解説:今回は動画再生画面をUIElement配置用のCanvasコントロールにしてみます。(詳細は後記)
        //private Button canvas = null;    //解説:Canvasで実験したら、Buttonクラスに入れ替えてみてください。

        private VideoDrawing vd = null;    //解説:VideoDrawingはビデオ再生機能の為のクラスです。(詳細は後記)
        private Button Btn1, Btn2, Btn3, Btn4, extBtn;
        //ファイル名
        string fName = "";
        //MediaPlayerコントロール用変数
        bool toggle = true;

        public DlgWindow()
        {
            this.Title = "MediaPlayer サンプル";
            this.Width = 640;
            this.Height = 480;
            this.WindowStyle = WindowStyle.ThreeDBorderWindow;
            this.ResizeMode = ResizeMode.NoResize;
            this.WindowState = WindowState.Normal;
            this.WindowStartupLocation = WindowStartupLocation.CenterScreen;    //Manual, CenterOwnerもある(Manyualの場合のTopとLeftで設定)
            this.Background = SystemColors.ControlBrush;
            InitControls();
        }

        public void InitControls()
        {
            //グリッド(Grid)と縦二列を作成
            Grid grid = new Grid();
            ColumnDefinition colL = new ColumnDefinition();
            ColumnDefinition colR = new ColumnDefinition();
            //列を定義
            //colL.Width = GridLength.Auto;    //解説:何も入れていないと幅0となる。

            colL.Width = new GridLength(540, GridUnitType.Pixel);
            colR.Width = GridLength.Auto;
            //colR.Width = new GridLength(80, GridUnitType.Pixel);    //解説:右列を固定幅にする場合
            grid.ColumnDefinitions.Add(colL);
            grid.ColumnDefinitions.Add(colR);

            //スタックパネル(StackPanel)を作成
            StackPanel stackPanel = new StackPanel();
            stackPanel.Width = 80;
            stackPanel.Height = 420;
            stackPanel.HorizontalAlignment = HorizontalAlignment.Right;

            //Canvas    //解説:Canvasコントロールを単にGridの左列に貼り付けるだけです。
            canvas = new Canvas();
            //canvas = new Button();    //解説:Canvasで実験したら、Buttonクラスに入れ替えてみてください。
            //canvas.Content = "I'm a mere button.";

            canvas.Margin = new Thickness(10, 10, 10, 10);    //構造体 Thickness(左, 上, 右, 下)

            //グリッド左列に動画再生用Canvasコントロールを登録
            Grid.SetColumn(canvas, 0);
            grid.Children.Add(canvas);

            //ボタン1
            Btn1 = new Button();
            Btn1.Width = 60;
            Btn1.Height = 24;
            Btn1.HorizontalAlignment = HorizontalAlignment.Right;
            Btn1.VerticalAlignment = VerticalAlignment.Top;
            Btn1.Margin = new Thickness(10, 10, 10, 0);    //構造体 Thickness(左, 上, 右, 下)
            Btn1.Content = "Open";
            Btn1.Click += Button1_Click;
            stackPanel.Children.Add(Btn1);

            //ボタン2ボタン
            Btn2 = new Button();
            Btn2.Width = 60;
            Btn2.Height = 24;
            Btn2.HorizontalAlignment = HorizontalAlignment.Right;
            Btn2.VerticalAlignment = VerticalAlignment.Top;
            Btn2.Margin = new Thickness(10, 10, 10, 0);    //構造体 Thickness(左, 上, 右, 下)
            Btn2.Content = "Play";
            Btn2.Click += Button2_Click;
            stackPanel.Children.Add(Btn2);

            //ボタン3ボタン
            Btn3 = new Button();
            Btn3.Width = 60;
            Btn3.Height = 24;
            Btn3.HorizontalAlignment = HorizontalAlignment.Right;
            Btn3.VerticalAlignment = VerticalAlignment.Top;
            Btn3.Margin = new Thickness(10, 10, 10, 0);    //構造体 Thickness(左, 上, 右, 下)
            Btn3.Content = "Pause";
            Btn3.Click += Button3_Click;
            stackPanel.Children.Add(Btn3);

            //ボタン4ボタン
            Btn4 = new Button();
            Btn4.Width = 60;
            Btn4.Height = 24;
            Btn4.HorizontalAlignment = HorizontalAlignment.Right;
            Btn4.VerticalAlignment = VerticalAlignment.Top;
            Btn4.Margin = new Thickness(10, 10, 10, 0);    //構造体 Thickness(左, 上, 右, 下)
            Btn4.Content = "Stop";
            Btn4.Click += Button4_Click;
            stackPanel.Children.Add(Btn4);

            //終了ボタン
            extBtn = new Button();
            extBtn.Width = 60;
            extBtn.Height = 24;
            extBtn.HorizontalAlignment = HorizontalAlignment.Right;
            extBtn.VerticalAlignment = VerticalAlignment.Bottom;
            extBtn.Margin = new Thickness(10, 10, 10, 10);    //構造体 Thickness(左, 上, 右, 下)
            extBtn.Content = "終了";
            extBtn.Click += extBtn_Click;
            stackPanel.Children.Add(extBtn);

            //グリッド右列にスタックパネルを登録
            Grid.SetColumn(stackPanel, 1);
            grid.Children.Add(stackPanel);

            //ウィンドウにグリッドを登録
            this.Content = grid;

            //MediaPlayerとVideoDrawing
            mp = new MediaPlayer();    //解説:メディアプレーヤーのインスタンス実体を生成します。
            vd = new VideoDrawing();    //解説:ビデオ再生のインスタンス実体を生成します。
        }

        void Button1_Click(object sender, RoutedEventArgs e)
        {
            Microsoft.Win32.OpenFileDialog ofd = new Microsoft.Win32.OpenFileDialog();
            ofd.AddExtension = true;
            ofd.DefaultExt = "*.*";
            ofd.Filter = "Media(*.*)|*.*";
            ofd.ShowDialog();
            fName = ofd.FileName;
            //ofd.Dispose();    //WPFでは不要(https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/windows/how-to-open-common-system-dialog-box?view=netdesktop-8.0)
        }

        void Button2_Click(object sender, RoutedEventArgs e)
        {
            if(fName == "")    //解説:メディアファイルが指定されていない場合
                MessageBox.Show("メディアファイルを選択してください。", "エラー", MessageBoxButton.OK, MessageBoxImage.Exclamation);
            else    //解説:メディアファイルが指定されている場合、再生を開始します。
            {
                //解説:先ずメディアプレーヤーにソースファイルを喰わせます。

                mp.Open(new System.Uri(fName, UriKind.Absolute));    //Uri:URI(Uniform Resource Identifier) のオブジェクト表現を可能にし、URI の一部へ簡単にアクセスできるようにします。

                //解説:ビデオ再生サイズを指定します。
                vd.Rect = new System.Windows.Rect(0, 0, 200, 200);

                //解説:ビデオ再生のソースをメディアプレーヤーに指定します。
                vd.Player = mp;

                //解説:ビデオ再生を描画ブラシとして生成します。
                DrawingBrush br = new DrawingBrush(vd);
                canvas.Background = br;     //解説:Canvasコントロールの背景にビデオ再生描画ブラシを登録します。
                mp.Play();     //解説:再生を開始します。
                Btn3.Content = "Pause";     //解説:ボタン3の「一時停止」の表示(MediaElementの場合と同じ)
                toggle = true;     //解説:トグルスイッチを真にします。(MediaElementの場合と同じ。)
            }
        }

        void Button3_Click(object sender, RoutedEventArgs e)
        {
            if(toggle)
            {
                //解説:MediaPlayerになった以外は、MediaElementの場合と同じです。

                mp.Pause();
                Btn3.Content = "Resume";
            }
            else
            {
                //解説:MediaPlayerになった以外は、MediaElementの場合と同じです。

                mp.Play();
                Btn3.Content = "Pause";
            }
            toggle = !toggle;
        }

        void Button4_Click(object sender, RoutedEventArgs e)
        {
            //解説:MediaPlayerになった以外は、MediaElementの場合と同じです。

            mp.Stop();
            Btn3.Content = "Pause";
            toggle = true;
        }

        void extBtn_Click(object sender, RoutedEventArgs e)
        {
            Close();
        }
    }
}

 

いかがでしょうか?

 

MediaElementクラスは、インスタンスを生成するだけでそれ自体がコントロールとなって動画再生が出来ましたが、MediaPlayerの場合は、

 

(1)動画ソースファイルの読み込みと管制→MediaPlayer

 ↓(登録)

(2)動画データの再生→VideoDrawing

 ↓(登録)

(3)描画ブラシ→DrawingBrush

 ↓(登録)

(4)(コントロール等)UIElementの背景(Backgroundメンバー)

 

という手続きを経て動画を再生します。(

:と言っても、Microsoft Learningのサンプルには(1)→(2)までしか載っておらず、私はwebをググって英文のサイトに辿り着き、(3)→(4)を行うことで動画がUIElementに表示されることを知りました。

 

なーんだ、MediaElementの方が簡単じゃん!又は、

MediaPlayerは面倒くさい。又は、

結局動画の再生、一時停止、中止はMediaElementとおんなじじゃん!

 

と思ったあなた、正解ですっ!

 

ただしっ

MediaPlayerは、

(背景がある)どんなUIElementでも動画再生できる

というところがMediaElementと違うのです。↑のサンプルでCanvasの代わりにButton等他のコントロールを使ってテスト()してみたり、そのコントロールのContentメンバーに何か登録したらどうなるかテストしてみてください。

:↑のコードの「//解説:Canvasで実験したら、Buttonクラスに入れ替えてみてください。」というコメントが付いている行の行頭のコメント記号(//)を切り取り、Canvasクラスにしている相対する行の頭に貼り付けて、コメントを入れ替えてコンパイルしてみてください。又、ボタンにはContentメンバーに文字列を設定しています。それがどう見えるかも確認してください。(インスタンス名canvasは、Buttonクラスにしてもそのまま使っているのでいじらなくて結構です。)

 

これこそがWEBデザイン時代の動画再生なんだなぁ

 

と思いました。

(未再生)

(Canvasクラスで再生する場合)

(Buttonクラスで再生する場合)

 

まぁ、いずれにしてもWin32ではDirectShowのラッパーを作る必要があった(昔の)C++やWinFormとは異なり、WPF+C#では「MediaElementというコントロールを使う」か「どんなコントロールでも背景をMediaPlayerで描画する」ことで動画再生はとても簡単になりました。

 

と、いうことが分かったので、動画再生は一件落着です。

 

今度はもう一つ引っ掛かっていたSAPIがWPFでどう扱われているか確かめたのでご紹介しましょう。

 

前々回、WPFを利用した動画再生のためのアプローチとして、MediaElementとMediaPlayerの二つがあり、前者はUIElementなのでWin32でいうコントロールのように使え、後者は描画機能として実装することを書きました。

 

今回は前々回紹介したダイアログにMediaElementを入れて、動画再生アプリサンプルとします。その為、既にコメント解説:で紹介したところは色付けせず、MediaElement関連の部分だけ青色(コメント解説赤色(MediaElement)紫色(MediaElementのメンバー)にします。見ていただければわかりますが、極めてシンプルでユーザーの手を付けるところは殆どありません。

 

【Test_MediaElement.cs】

////////////////////////////////////////
//MediaElement.cs - MediaElement Sample
// https://learn.microsoft.com/ja-jp/windows/communitytoolkit/controls/wpf-winforms/mediaplayerelement
// https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/graphics-multimedia/multimedia-overview?view=netframeworkdesktop-4.8
// http://www.wisdomsoft.jp/467.html

//解説:↑はMediaElementについての参照先です。
////////////////////////////////////////
using System;
using System.Windows;            //PresentationFramework.dllにある
using System.Windows.Controls;    //PresentationFramework.dllにある
using System.Windows.Media;        //PresentationCore.dllにある(MediaElement, Brushes構造体使用の為)
/* 参照DLL
RefFile=C:\Windows\Microsoft.NET\Framework\v4.0.30319\WPF\PresentationFramework.dll
RefFile01=C:\Windows\Microsoft.NET\Framework\v4.0.30319\WPF\PresentationCore.dll
RefFile02=C:\Windows\Microsoft.NET\Framework\v4.0.30319\WPF\WindowsBase.dll
RefFile03=C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Xaml.dll
*/

namespace Test_MediaElement
{
    ///////////////////////////
    //エントリーポイントクラス
    ///////////////////////////
    class MainApp
    {
        [STAThread]
        public static void Main()
        {
            DlgWindow dlg = new DlgWindow();
            Application ap = new Application();    //明示的にインスタンスを作らなければならない
            ap.Run(dlg);
        }
    }

    public partial class DlgWindow : Window    //派生元が"Form"から"Window"となる
    {
        //コントロール
        private MediaElement me;
        private Button Btn1, Btn2, Btn3, Btn4, extBtn;
        //ファイル名
        string fName = "";
        //MediaElementコントロール用変数
        bool toggle = true;

        public DlgWindow()
        {
            this.Title = "MediaElement サンプル";
            this.Width = 640;
            this.Height = 480;
            this.WindowStyle = WindowStyle.ThreeDBorderWindow;
            this.ResizeMode = ResizeMode.NoResize;
            this.WindowState = WindowState.Normal;
            this.WindowStartupLocation = WindowStartupLocation.CenterScreen;    //Manual, CenterOwnerもある(Manualの場合の設定方法不明)
            this.Background = SystemColors.ControlBrush;
            InitControls();
        }

        public void InitControls()
        {
            //グリッド(Grid)と縦二列を作成
            Grid grid = new Grid();
            ColumnDefinition colL = new ColumnDefinition();
            ColumnDefinition colR = new ColumnDefinition();
            //列を定義
            //colL.Width = GridLength.Auto;
            colL.Width = new GridLength(540, GridUnitType.Pixel);
            colR.Width = GridLength.Auto;
            //colR.Width = new GridLength(80, GridUnitType.Pixel);
            grid.ColumnDefinitions.Add(colL);
            grid.ColumnDefinitions.Add(colR);

            //スタックパネル(StackPanel)を作成
            StackPanel stackPanel = new StackPanel();
            stackPanel.Width = 80;
            stackPanel.Height = 420;
            stackPanel.HorizontalAlignment = HorizontalAlignment.Right;

            //MediaElement
            me = new MediaElement();
            me.Margin = new Thickness(10, 10, 10, 10);    //構造体 Thickness(左, 上, 右, 下)
            me.LoadedBehavior = MediaState.Manual;    //解説:MediaElementの初期状態初期設定

            //グリッド左列にMediaElementを登録
            Grid.SetColumn(me, 0);    //解説:左の列を指定
            grid.Children.Add(me);    //解説:子UIElementに指定

            //ボタン1
            Btn1 = new Button();
            Btn1.Width = 60;
            Btn1.Height = 24;
            Btn1.HorizontalAlignment = HorizontalAlignment.Right;
            Btn1.VerticalAlignment = VerticalAlignment.Top;
            Btn1.Margin = new Thickness(10, 10, 10, 0);    //構造体 Thickness(左, 上, 右, 下)
            Btn1.Content = "Open";
            Btn1.Click += Button1_Click;
            stackPanel.Children.Add(Btn1);

            //ボタン2ボタン
            Btn2 = new Button();
            Btn2.Width = 60;
            Btn2.Height = 24;
            Btn2.HorizontalAlignment = HorizontalAlignment.Right;
            Btn2.VerticalAlignment = VerticalAlignment.Top;
            Btn2.Margin = new Thickness(10, 10, 10, 0);    //構造体 Thickness(左, 上, 右, 下)
            Btn2.Content = "Play";
            Btn2.Click += Button2_Click;
            stackPanel.Children.Add(Btn2);

            //ボタン3ボタン
            Btn3 = new Button();
            Btn3.Width = 60;
            Btn3.Height = 24;
            Btn3.HorizontalAlignment = HorizontalAlignment.Right;
            Btn3.VerticalAlignment = VerticalAlignment.Top;
            Btn3.Margin = new Thickness(10, 10, 10, 0);    //構造体 Thickness(左, 上, 右, 下)
            Btn3.Content = "Pause";
            Btn3.Click += Button3_Click;
            stackPanel.Children.Add(Btn3);

            //ボタン4ボタン
            Btn4 = new Button();
            Btn4.Width = 60;
            Btn4.Height = 24;
            Btn4.HorizontalAlignment = HorizontalAlignment.Right;
            Btn4.VerticalAlignment = VerticalAlignment.Top;
            Btn4.Margin = new Thickness(10, 10, 10, 0);    //構造体 Thickness(左, 上, 右, 下)
            Btn4.Content = "Stop";
            Btn4.Click += Button4_Click;
            stackPanel.Children.Add(Btn4);

            //終了ボタン
            extBtn = new Button();
            extBtn.Width = 60;
            extBtn.Height = 24;
            extBtn.HorizontalAlignment = HorizontalAlignment.Right;
            extBtn.VerticalAlignment = VerticalAlignment.Bottom;
            extBtn.Margin = new Thickness(10, 10, 10, 10);    //構造体 Thickness(左, 上, 右, 下)
            extBtn.Content = "終了";
            extBtn.Click += extBtn_Click;
            stackPanel.Children.Add(extBtn);

            //グリッド右列にスタックパネルを登録
            Grid.SetColumn(stackPanel, 1);
            grid.Children.Add(stackPanel);

            //ウィンドウにグリッドを登録
            this.Content = grid;
        }

        void Button1_Click(object sender, RoutedEventArgs e)
        {
            Microsoft.Win32.OpenFileDialog ofd = new Microsoft.Win32.OpenFileDialog();

            //解説:WPFの「ファイルを開く」ダイアログはこういう書き方になります。
            ofd.AddExtension = true;
            ofd.DefaultExt = "*.*";
            ofd.Filter = "Media(*.*)|*.*";
            ofd.ShowDialog();
            fName = ofd.FileName;    //解説:再生する動画ファイル名を記録します。
            //ofd.Dispose();    //WPFでは不要(https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/windows/how-to-open-common-system-dialog-box?view=netdesktop-8.0)
        }

        void Button2_Click(object sender, RoutedEventArgs e)
        {
            if(fName == "")    //解説:ファイルの登録がない場合
                MessageBox.Show("メディアファイルを選択してください。", "エラー", MessageBoxButton.OK, MessageBoxImage.Exclamation);    //解説:くどいですが、WinFormのMessageBoxButtons、MessageBoxIconとは違います。
            else
            {
                me.Source = new System.Uri(fName, UriKind.Absolute);    //解説:ソースファイルの登録
                me.Play();    //解説:再生します。
                Btn3.Content = "Pause";    //解説:ボタン3の表示を"Pause"にします。
                toggle = true;    //解説:トグルフラッグを真にします。
            }
        }

        void Button3_Click(object sender, RoutedEventArgs e)
        {
            if(toggle)    //解説:トグルフラッグが真なら
            {
                me.Pause();    //解説:一時中止します。
                Btn3.Content = "Resume";    //解説:ボタン3の表示を"Resume"にします。
            }
            else
            {
                me.Play();    //解説:動画再生を続けます。
                Btn3.Content = "Pause";    //解説:ボタン3の表示を"Pause"にします。
            }
            toggle = !toggle;    //解説:トグルフラッグを反転させます。
        }

        void Button4_Click(object sender, RoutedEventArgs e)
        {
            me.Stop();    //解説:動画再生を停止します。
            Btn3.Content = "Pause";    //解説:ボタン3の表示を"Pause"にします。
            toggle = true;    //解説:トグルフラッグを真にします。
        }

        void extBtn_Click(object sender, RoutedEventArgs e)
        {
            Close();    //解説:プログラムを閉じます。
        }
    }
}

 

これが「おっと!」(OOP-オブジェクトオリエンテッドプログラミング)の神髄ですね。MediaElementクラスのオブジェクトさえ作れば、後は最小限の動作(メソッド)で「動画再生、一時停止、中断」という目的が達成されます。Win32のDirectShowもC++でラッパークラス化すれば、同様にコントロールとしてインスタンスを作り、「動画再生、一時停止、中断」が簡単にできました。ご参考までにBCCSkeltonで自作(C++)したCDSHOWクラスの宣言部のみ後掲します。考えることは同じですね。

 

【CDSHOW.hの一部】

///////////////////////////
//CDSHOWクラス定義ファイル
///////////////////////////

#include <DShow.h>                    //DirectShowヘッダー
#include <wchar.h>

//フィルター定義(利用者のフィルターの環境によって適宜変更が必要)
#define    FILTER    "ビデオファイル(*.avi;*.mp4;*.mov)\0*.avi;*.mp4;*.mov\0オーディオファイル(*.mp3;*.wav;*.mid)\0*.mp3;*.wav;*.mid\0\0"

//親ウィンドウへの通知メッセージ
#define WM_GRAPHNOTIFY  WM_APP + 1

class CDSHOW {

protected:    //解説:「保護」メンバーはユーザーから見えないので無視してください。
    //DirectShow親ウィンドウ
    HWND m_hWnd;
    //DirectShow関連変数
    IGraphBuilder *m_pGraph;        //フィルターグラフビルダークラスポインター
    IMediaControl *m_pControl;        //メディアコントロールインターフェースポインター
    IMediaEventEx *m_pEvent;        //メディアイベントインターフェースポインター
    IVideoWindow *m_pVideoWindow;    //ビデオウィンドウポインター
    IMediaPosition *m_pMPos;        //メディア再生位置インターフェースポインター
    //結果判定用変数

    HRESULT m_hr;
public:    //解説:「公開」メンバーはユーザーがアクセスしたり、メソッド(関数)として使用します。
    //ファイル名取得用変数
    char m_FileName[MAX_PATH];
    WCHAR m_WFileName[MAX_PATH];
    //ファイル再生時間変数(double)
    REFTIME m_Len;
    //メンバー関数   //解説:動画の再生、一時中止、再開、中止は関数を呼ぶだけでできます。
    CDSHOW();                        //コンストラクター
    ~CDSHOW();                        //デストラクター
    void Init();                    //メンバー関数の初期化
    bool Move(int, int, int, int);    //ビデオウィンドウの位置設定(x, y, w, h)
    bool SetFileName(char*, char*);    //ビデオファイル名を設定する
    bool Show(HWND);                //ビデオウィンドウの初期化とビデオの再生
    REFTIME GetDuration();            //ビデオ再生時間を返す
    OAFilterState GetState(int);    //フィルターグラフの再生状態を取得(0-Stopped、1-Paused、2-Running)
    bool Pause();                    //ビデオの一時停止
    bool Continue();                //ビデオの再開
    void Stop();                    //ビデオの中止
    REFTIME GetCurrentPos();        //現在の再生時間を返す
    bool SetCurrentPos(REFTIME);    //再生位置(時間-秒)を設定する
    bool IsOver(long&);                //親ウィンドウに置く終了モニタリング関数
    void CleanUp();                    //ビデオウィンドウ、フィルターグラフの終了処理
};

 

次回はMediaElementと「一味違う」MediaPlayerの違いを解説してみましょう。

 

WinFormがWindows 95からのWin32 APIをベースにしたプラットフォームで、WinFormはWin32APIに依拠し、WPFはDirectXの描画に依拠した新しいプラットフォームであることは前に述べました。

 

ということで、

 

Microsoft Copilotに以下の質問をしてみました。

WinFormになくて、WPFで新たに作られたコントロール(UIElement)は何ですか?

 

これに対する答えは以下の通り。

:「WPFで新たに作られたコントロール(UIElement)は、Windows Formsで提供されていないか、または大きく改善されたものが多くあります。例えば、次のようなコントロールがあります。

  • Button: ボタンの外観や動作をカスタマイズできるようになりました。また、ボタンにはコマンドやイベントトリガーを割り当てることができます。

  • ComboBox: コンボボックスの項目には、テキストだけでなく、画像やその他の要素を含めることができます。また、コンボボックスのドロップダウン部分のレイアウトやスタイルを変更することもできます。

  • ListBox: リストボックスの項目には、テキストだけでなく、画像やその他の要素を含めることができます。また、リストボックスの選択モードやレイアウトを変更することもできます。

  • DataGrid: データグリッドは、WPFで新たに追加されたコントロールで、表形式のデータを表示や編集することができます。データグリッドは、列の自動生成や並べ替え、フィルタリング、グループ化、集計などの機能を備えています。

  • Calendar: カレンダーは、WPFで新たに追加されたコントロールで、日付を選択することができます。カレンダーは、表示モードや選択モード、ブラックアウト日などの機能を備えています。(注:見た目はMCコントロールと同じです。)

  • MediaElement: メディアエレメントは、WPFで新たに追加されたコントロールで、オーディオやビデオなどのメディアファイルを再生することができます。メディアエレメントは、再生、一時停止、停止、シークなどの操作をサポートしています。

これらは、WPFで新たに作られたコントロール(UIElement)の一部です。他にも、WPFでは、Windows Formsでは提供されていない、または大きく改善されたコントロールが多数あります。詳しくは、こちらをご覧ください。

 

↑に赤字で書いたものはWinFormでももちろんありましたが、WPFで刷新されたもの、という意味でしょう。実際これらはContentControlとしてUIElementを一つ持てますし、後に紹介するように背景(Background)の描画でWinForm時代のコントロールとは全く異なります。(Calender橙色にしているのは「WPFで新たに追加されたコントロール」と書いてあるからですが、これはの通り、WinFormのMCコントロールと全く同じ外観です。)

 

青字でかかれたコントロールはWinFormにはなかったです。なので、次回MediaElementを紹介するのは理にかなったことですね。

 

さて、今回WPFを学習する主動機であるこれのうち、動画の再生を取り上げてみます。

 

さて、この段階では

 

こんなものを見つけました。

『このページWindows メディア プレーヤーSDKに関連付けられている機能は、レガシ機能です。 MediaPlayerに置き換わりました。MediaPlayerは、Windows 10とWindows 11用に最適化されています。新しいコードでは、可能な場合は SDK ではなく MediaPlayerWindowsメディア プレーヤー使用することを強くお勧めします。Microsoftは、従来のAPIを使用する既存のコードを、可能であれば新しい API を使用するように書き直すよう提案しています。』

 

を引用しましたが、今よく見るとこれはまだまだレガシーなんですね。(Microsoft Learningの範疇が「アプリ/Win32/デスクトップ テクノロジ/音声とビデオ/Windows メディア プレーヤー SDK」になっています。)これは.NETでも.NET Frameworkでもないです。事実、自分でも気が付いて、

 

『じゃあ、MediaPlayerクラスのC#コードの記述』を調べようとしていると、またこんなものを見つけました。

(注)

注:はっきりと"Windows Presentation Foundation"とか"WPF"とか書かれていますが、私はその段階ではまだその本当の意味が分かっていませんでした。

 

と書いています。これは(今は断言できますが)正解ですね。そして、このWPFのサイトで動画再生について、

 

メディア API
....(後述)

MediaElementおよびMediaPlayerクラスは、オーディオまたはビデオのコンテンツを表示するために使用されます。これらのクラスは、対話式またはクロックで制御できます。これらのクラスは、Microsoft Windows Media Player 10 コントロールでメディアの再生に使用できます。どちらのクラスを使用するかは、シナリオによって決まります。
MediaElementは、レイアウトでサポートされている UIElement であり、多くのコントロールのコンテンツとして利用できます。また、Extensible Application Markup Language (XAML) とコードでも使用できます。それに対し、MediaPlayer は Drawing オブジェクト向けに設計されていて、レイアウトではサポートされていません。MediaPlayerを使用して読み込まれたメディアは、VideoDrawing を使用するか、DrawingContext を直接操作することでのみ表示できます。MediaPlayer は XAML では使用できません。

 

この文章の意味を当時は正確に理解していませんでしたが、今は分かります。要すれば、

 

動画再生は(Win32のコントロールのようにオブジェクト化できる)UIElementとしての("形のあるものとしてXAMLでレイアウトできる")MediaElementと、(XAMLでレイアウトする為の形を伴わない)"描画機能"としてのMediaPlayer

 

の二通りのソリューションがある、ということですね。

 

「じゃぁ、どっちを使えばいいの?」という当然の疑問が生じますが、それに対して前出のMicrosoft Learningでは,

 

メディア API
MediaElement および MediaPlayer クラスは、オーディオまたはビデオのコンテンツを表示するために使用されます。 これらのクラスは、対話式またはクロックで制御できます。 これらのクラスは、Microsoft Windows Media Player 10 コントロールでメディアの再生に使用できます。 どちらのクラスを使用するかは、シナリオによって決まります。

 

としか書いていません。

 

じゃ、どーするの?

 

ということで、

 

両方ともやっちゃいましょう!

 

というソリューションを採ることにしました!(まぁ、簡単な解法ですよね?)

 

ps. 順序はWin32でもわかりやすいUIElement(Win32なら「コントロール」と言い換えてもよいかと)のMediaElement

から行きましょう。実際、MediaPlayerは

Win32の感覚では「うへぇ、大変だねぇ」というような、WPFらしいものだったからです。(って、見た目は全く変わりありませんが...)

 

前回まででWPFで複数のコントロールを扱えることが分かりましたので、これから当初の意図にもとづいてWPFで提供されている動画再生と音声再生のガジェット(Gadget)を探索してみたいと思います。

 

しかしっ!

 

その前にテストする器(ウィンドウ)を用意しなければなりません。先ずは動画再生用のダイアログを用意することにします。

 

以下のサンプルはWPFを学習し始めた当初の者なので、コメントで備忘を書き込んでいます。これと//解説:を参考にしてください。(WinformのDialogテンプレートを改造しているので、WinformとWPFの違いに言及しているところもあります。)

 

【Dialog_Type.cs】

//////////////////////////////////////////////
//Dialog_Type.cs - Dialog type window sample
//////////////////////////////////////////////

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;            //Brushes構造体使用の為

namespace Dialog_Type
{
    ///////////////////////////
    //エントリーポイントクラス
    ///////////////////////////

    class MainApp
    {
        [STAThread]
        public static void Main()
        {
            DlgWindow dlg = new DlgWindow();
            Application ap = new Application();    //明示的にインスタンスを作らなければならない
            ap.Run(dlg);
        }
    }

    public partial class DlgWindow : Window    //派生元が"Form"から"Window"となる
    {
        //コントロール
        Button Btn1, Btn2, Btn3, Btn4, extBtn;

        public DlgWindow()
        {
        //Windowのプロパティ:https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.window?view=windowsdesktop-8.0
            this.Title = "Dialog風ウィンドウ";
            this.Width = 640;
            this.Height = 480;
            this.WindowStyle = WindowStyle.ThreeDBorderWindow;
            /*
                SingleBorderWindow    1    A window with a single border. This is the default value. --- Default
                ThreeDBorderWindow    2    A window with a 3-D border.
                ToolWindow            3    A fixed tool window.
            */

            this.ResizeMode = ResizeMode.NoResize;
            /*
                CanMinimize            1    ウィンドウは最小化および元のサイズへの復元だけが可能です。 最小化ボタンと最大化ボタンが表示されますが、最小化ボタンだけが有効になります。
                CanResize            2    ウィンドウのサイズは変更できます。 最小化ボタンと最大化ボタンはどちらも表示され、有効になります。
                CanResizeWithGrip    3    ウィンドウのサイズは変更できます。 最小化ボタンと最大化ボタンはどちらも表示され、有効になります。 サイズ変更グリップがウィンドウの右下隅に表示されます。
                NoResize            0    ウィンドウのサイズは変更できません。 最小化ボタンと最大化ボタンはタイトル バーに表示されません。
            */

            this.WindowState = WindowState.Normal;
            /*
                Maximized    2    The window is maximized.
                Minimized    1    The window is minimized.
                Normal        0    The window is restored.
            */

            this.WindowStartupLocation = WindowStartupLocation.CenterScreen;    //Manual, CenterOwnerもある(Manyualの場合の設定方法不明)
            //this.BackColor = SystemColors.Window;    //Color BackColorからBrush Backgroundになった
            this.Background = SystemColors.ControlBrush;
            //this.Load += new EventHandler(DlgWindow_Load);    //解説:WPFではLoadは無い。
            this.Loaded += new RoutedEventHandler(DlgWindow_Loaded);    //解説:LoadedもRoutedEventになる。
        }

        public void InitControls()
        {
            //参考:https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/advanced/alignment-margins-and-padding-overview?view=netframeworkdesktop-4.8
            //WPFでは、親Content プロパティに対して子要素を直接1つしか持てない。その為、GridやStackPanelを使用する。


            //グリッド(Grid)を作成
            Grid grid = new Grid();
        /*    //行を定義
            RowDefinition row1 = new RowDefinition();
            RowDefinition row2 = new RowDefinition();
            grid.RowDefinitions.Add(row1);
            grid.RowDefinitions.Add(row2);
        */    //列を定義

            ColumnDefinition colL = new ColumnDefinition();
            colL.Width = new GridLength(540, GridUnitType.Pixel);
            //colL.Width = GridLength.Auto;        //中に何か入れないと幅が出ない
            ColumnDefinition colR = new ColumnDefinition();
            //colR.Width = new GridLength(80, GridUnitType.Pixel);
            colR.Width = GridLength.Auto;
            grid.ColumnDefinitions.Add(colL);    //解説:列の定義です。行の定義はRowDefinition.Addメソッドを使います。
            grid.ColumnDefinitions.Add(colR);   //解説:同上。左から追加してゆきますので、二番目を右の列としています。 

            //スタックパネル(StackPanel)を作成
            StackPanel stackPanel = new StackPanel();
            stackPanel.Width = 80;        //絶対値を入れてみた
            stackPanel.Height = 420;    //絶対値を入れてみた
            stackPanel.HorizontalAlignment = HorizontalAlignment.Right;    //右寄せ

            //ボタン1
            Btn1 = new Button();
            Btn1.Width = 60;
            Btn1.Height = 24;
            //Btn1.Location = new Point(ClientSize.Width - Btn1.Width - 10, 10);
            //Btn1.Anchor = (AnchorStyles.Top | AnchorStyles.Right);

            //解説:↑はベースとしたWinFormの位置決めとウィンドウサイズ変更時の動作指定方法です。

            Btn1.HorizontalAlignment = HorizontalAlignment.Right;    //解説:WPFでは垂直水平方向での指定になります。
            /*
                Left    0    An element aligned to the left of the layout slot for the parent element.
                Center    1    An element aligned to the center of the layout slot for the parent element.
                Right    2    An element aligned to the right of the layout slot for the parent element.
                Stretch    3    An element stretched to fill the entire layout slot of the parent element.
            */

            Btn1.VerticalAlignment = VerticalAlignment.Top;    //上→下のStackPanelの場合すべてTopになるみたい
            /*
                Top        0    子要素を、親のレイアウト スロットの上端に揃えて配置します。
                Center    1    子要素を、親のレイアウト スロットの中央に揃えて配置します。
                Bottom    2    子要素を、親のレイアウト スロットの下端に揃えて配置します。
                Stretch    3    子要素を、親のレイアウト スロット全体に引き伸ばします。
            */

            Btn1.Margin = new Thickness(10, 10, 10, 0);        //外の余白(中の余白はPadding)

            //解説:Margin、Padding共に指定方法は厚さ構造体 Thickness(左, 上, 右, 下)が使える。

            //解説:WPFのコンテンツの配置の基本については、これを参考にしてください。
            Btn1.Content = "Open";
            Btn1.Click += Button1_Click;    //解説:ボタンクリックイベントにメソッドを追加するとRoutedEventになります。

            Btn1.PreviewMouseDown += Button1_ClickDown;
            Btn1.PreviewMouseUp += Button1_ClickUp;
            //this.Controls.Add(Btn1);    //解説:WinFormでのボタン追加方法です。
            stackPanel.Children.Add(Btn1);

            //ボタン2ボタン
            Btn2 = new Button();
            Btn2.Width = 60;
            Btn2.Height = 24;
            //Btn2.Location = new Point(ClientSize.Width - Btn2.Width - 10, Btn1.Height + 20);
            //Btn2.Anchor = (AnchorStyles.Top | AnchorStyles.Right);

            Btn2.HorizontalAlignment = HorizontalAlignment.Right;
            Btn2.VerticalAlignment = VerticalAlignment.Top;
            Btn2.Margin = new Thickness(10, 10, 10, 0);    //構造体 Thickness(左, 上, 右, 下)
            Btn2.Content = "Play";
            Btn2.Click += Button2_Click;
            //this.Controls.Add(Btn2);

            stackPanel.Children.Add(Btn2);

            //ボタン3ボタン
            Btn3 = new Button();
            Btn3.Width = 60;
            Btn3.Height = 24;
            //Btn3.Location = new Point(ClientSize.Width - Btn3.Width - 10, Btn1.Height + Btn2.Height + 30);
            //Btn3.Anchor = (AnchorStyles.Top | AnchorStyles.Right);

            Btn3.HorizontalAlignment = HorizontalAlignment.Right;
            Btn3.VerticalAlignment = VerticalAlignment.Top;
            Btn3.Margin = new Thickness(10, 10, 10, 0);    //構造体 Thickness(左, 上, 右, 下)
            Btn3.Content = "Pause";
            Btn3.Click += Button3_Click;
            //this.Controls.Add(Btn3);
            stackPanel.Children.Add(Btn3);

            //ボタン4ボタン
            Btn4 = new Button();
            Btn4.Width = 60;
            Btn4.Height = 24;
            //Btn4.Location = new Point(ClientSize.Width - Btn4.Width - 10, Btn1.Height + Btn2.Height + Btn3.Height + 40);
            //Btn4.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            Btn4.HorizontalAlignment = HorizontalAlignment.Right;
            Btn4.VerticalAlignment = VerticalAlignment.Top;
            Btn4.Margin = new Thickness(10, 10, 10, 0);    //構造体 Thickness(左, 上, 右, 下)
            Btn4.Content = "Stop";
            Btn4.Click += Button4_Click;
            //this.Controls.Add(Btn4);
            stackPanel.Children.Add(Btn4);

            //終了ボタン
            extBtn = new Button();
            extBtn.Width = 60;
            extBtn.Height = 24;
            //extBtn.Location = new Point(ClientSize.Width - extBtn.Width - 10, ClientSize.Height - extBtn.Height - 10);
            //extBtn.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right);

            extBtn.HorizontalAlignment = HorizontalAlignment.Right;    //解説:左右いずれかに寄せられます。
            extBtn.VerticalAlignment = VerticalAlignment.Bottom;    //解説:Bottomは無視され、Topの動作になります。
            extBtn.Margin = new Thickness(10, 10, 10, 10);    //構造体 Thickness(左, 上, 右, 下)
            extBtn.Content = "終了";
            extBtn.Click += extBtn_Click;
            //this.Controls.Add(extBtn);
            stackPanel.Children.Add(extBtn);

            //グリッド右列にスタックパネルを登録
            Grid.SetColumn(stackPanel, 1);    //0を指定するとグリッドの左列の右側にボタンが表示される
            grid.Children.Add(stackPanel);    //↑のようにコラムに設定しただけでは子コントロールとして表示されない

            //ウィンドウにグリッドを登録
            this.Content = grid;
        }

        private void DlgWindow_Loaded(object sender, RoutedEventArgs e)    //解説:単なるEventArgsではありません。

        {
            //コントロールの付加
            InitControls();
        }

        void Button1_ClickDown(object sender, RoutedEventArgs e)
        {
            Btn1.Background = Brushes.Red;
        }

        void Button1_Click(object sender, RoutedEventArgs e)
        {
            Btn1.Background = Brushes.Pink;
        }

        void Button1_ClickUp(object sender, RoutedEventArgs e)
        {
            Btn1.Background = Brushes.Blue;
        }

        void Button2_Click(object sender, RoutedEventArgs e)
        {
            Btn2.Background = Brushes.LightGreen;
        }

        void Button3_Click(object sender, RoutedEventArgs e)
        {
            Btn3.Background = Brushes.Blue;
        }

        void Button4_Click(object sender, RoutedEventArgs e)
        {
            Btn4.Background = Brushes.Red;
        }

        void extBtn_Click(object sender, RoutedEventArgs e)
        {
            Close();
        }
    }
}

 

このサンプルでは追加したボタンクリックイベントメソッドでボタンの色を変更するようにしてみました。(私の環境では、マウスがボタンを上にある時はライトブルーになります。)

特別な意味はありませんが、前に作ったWinFormのダイアログのイメージを載せます。Win32のGDI+で作成されたコントロールとボタンの外観が可也異なることが鮮明にわかります。

 

私自身は「29年前のWin32 APIでWIndows プログラミング(言語はC++)をスタートし、1年前にC#の学習を開始(知らなかったけどWinFormプラットフォームだった)し、最近やっとWPFプラットフォームを理解し始め、少し弄れるようになった」浅学者(せんがくもの)ですが、その関係でこの記事(2022年4月)を読む機会がありました。

 

まぁ、言葉ではWPFもUWPも知っていましたし、WinUIWinRTのファミリー(「UWP XAMLやWinRT XAML」)位に理解していましたが、WinUI 3)が何かは余り分からず、ちょっと調べてみました。(リンクはその結果です。)

:「(完全に Windows 10 SDK から切り離された) Windows App SDK(注:Windows AKDとは全く異なります)」に含まれるUIプラットフォームらしく、WinUI 2もありますが、「WinUI 3はWinUI 2のコードベースからスタートしましたが、両社(解説:「者」の誤字?)は別個の技術です。WinUI 2とWinUI 3はどちらもXAMLベースのUIフレームワークで、.NETとC++で動作します。なお、WinUI 2とWinUI 3は相互に互換性がありません。」「WinUI 2がWinUI 3と混在することはできません。」ということなので、ちょっと混乱が予想されそうです。

 

この巷で騒がれているWinUI 3。それにかかわるWindows 開発者向けFAQを覗いてみました。

 

一寸驚きです。

 

少しまとめてみると、

 

となりますか。同じく巷ではWindows10のサポートが打ち切られると(32bitベースでは)Windows11に移行できなくなるPCが山程あると騒いでますが、Windowsが32bitを捨てて、64bitベースとなる布石のようにも思えますね。まぁ、パラダイムシフトなので仕方がないような気もします。

 

気に掛かるのは32bitベースや.NETFrameworkなどの「旧い器」です。一応Microsoftは、

 

Q: UWP/WinUI 2 は非推奨なのですか?
A: いいえ。 UWP と WinUI 2 は引き続きサポートされており、バグ、信頼性、およびセキュリティの修正が行われます。 ただし、最新の .NET ランタイムのサポートを含むほとんどの新機能は、Windows App SDK/WinUI 3 だけに追加されます。
Q: WPF は非推奨ですか?
A:いいえ。 WPF はまだサポートされていて、いくつかの機能アップデートを受けることもできます。
Q: WinForms は非推奨ですか?
いいえ。 WinForms はまだサポートされていて、いくつかの機能アップデートを受けることもできます。
Q: Windows ランタイム (WinRT) は非推奨ですか?
A: いいえ。 WinRT とは、複数の言語間での相互運用を可能にするアプリケーション バイナリ インターフェイス (ABI) を指します。 WinRT は COM の進化系です。 Windows App SDK は、その機能のほとんどを WinRT API を通じて提供します。

 

ここで書いていますが、

 

不動産屋さんの物件説明

 

のように聞こえるのは私だけでしょうか?

 

ps. パラダイムや環境の変化が大きそうなので、元コンサルタントとしてMicrosoft Copilot君に次の質問をしてみました。

Q: WinUI 3になると、何が良くなるのですか?利点を教えてください。
A: Windows UIライブラリ (WinUI) 3 は、Windows 10向けのユーザーインターフェイスライブラリであり、UWPアプリケーション (およびXAML Islandsを使用するデスクトップアプリケーション) の構築に使用できます。WinUI 3は、Windows App SDK (旧称: Project Reunion) の一部であり、2021年11月17日にリリースされました。WinUI 3 には、以下のような利点があります。
Fluent Design Systemのアクセス: WinUI 3に移行する利点の1つは、Fluent Design Systemにアクセスできる点です。Fluent Design Systemは、Windows 10で導入されたデザイン言語であり、アプリケーションに美しさと洗練さをもたらします。
Windows App SDK の一部: WinUI 3は、Windows App SDKの一部であり、WinUI 3アプリでは他のWindows App SDK機能や APIも使用できます。
ダークモード、ライトモードへの対応: WinUI 3では、ダークモード、ライトモードへのUI変更が自動で行われます。基本的に WinUI 3では色の指定をしなくてもよいです。


腑に落ちたかなぁ?(素人プログラマーには無用の長物だな。)

 

ps of ps. まぁ、私の場合はもう歳で、プログラミング寿命が(生物学的寿命も)長くないですが、ITビジネスでこき使われる方々は大変ですね。

 

前回WPFにおけるコントロールの使い方作法を紹介しましたので、先ずはウィンドウに色々とコントロールを貼り付けてみようかと思いました。

しかし、

 

哀しい哉、私には絵心がない!

 

ので、Mirosoft Learningに載っているコントロールサンプルを3つ使って、それも一挙にウィンドウに張り付けることにしました。いつものようにコメントと「解説:」で紹介します。

 

【Window05.cs】

/////////////////
// Window05_1.cs
//参照:https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.controls.contentcontrol?view=windowsdesktop-8.0
// Window05_2.cs
//参照:https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/advanced/alignment-margins-and-padding-overview?view=netframeworkdesktop-4.8
// Window05_3.cs
//参照:https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.controls.canvas?view=windowsdesktop-8.0
/////////////////

using System;
using System.Windows;
using System.Windows.Controls;            //コントロールを利用する為
using System.Windows.Shapes;
using System.Windows.Media;
using System.Windows.Media.Imaging;        //BitmapFrameを使用する為

namespace Window05_1
{
    ///////////////////////////
    //エントリーポイントクラス
    ///////////////////////////

    class MainApp
    {
        [STAThread]
        public static void Main()
        {
            MainWindow mwnd = new MainWindow();
            Application ap = new Application();
            ap.Run(mwnd);
        }
    }

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            /* ウィンドウスタイル、クライアントエリア色、アイコンとZオーダーの指定 */
            this.WindowStyle = WindowStyle.ThreeDBorderWindow;    //解説:3D境界(太いやつ)
            this.Background = SystemColors.WindowBrush;            //本例ではウインドウのクライアントエリア色にする
            this.Icon = BitmapFrame.Create(new Uri("Icon.ico", UriKind.Relative));    //同じフォールダーのIcon.icoを読んで使用する
            this.Topmost = false;                                //最上位ウィンドウとするか否か
            /* 起動時の位置-上、左座標で指定または、起動時の位置で指定 */
            this.WindowStartupLocation = WindowStartupLocation.CenterScreen;
            /* ウィンドウサイズ-幅、高さで指定 */
            this.Width = 640;
            this.Height = 480;
            this.ResizeMode = ResizeMode.CanResizeWithGrip;        //解説:グリップ付きでサイズ変更可能
            this.ShowActivated = true;                            //アクティブ状態で初期表示するかどうか(規定値 true)
            this.ShowInTaskbar = true;                            //タスクバーにボタン表示するか否か
            this.Title = "Window05";
            InitControls();
        }

        /////////////////////////////////////////////
        // Microsoft Learnのサンプルを三つ一緒にする
        /////////////////////////////////////////////

        public void InitControls()
        {
            /* ---------- */
            /* Window05_1 */
            /* ---------- */
            /* 様々な様々なコンテンツの入ったボタン */

            //文字列をいれたボタン
            Button StrBtn = new Button();
            StrBtn.Height = 32;
            StrBtn.Width = 240;
            StrBtn.HorizontalContentAlignment = HorizontalAlignment.Left;    //解説:これはボタン内コンテンツの左寄せです。
            StrBtn.VerticalContentAlignment = VerticalAlignment.Top;   //解説:これはボタン内コンテンツの上寄せです。
            //StrBtn.HorizontalContentAlignment = HorizontalAlignment.Right;
            //StrBtn.VerticalContentAlignment = VerticalAlignment.Bottom;

            StrBtn.Content = "This is string content of a Button";    //解説:ボタンはContentControlで一つだけ。
            //DateTimeオブジェクトを入れたボタン
            Button ObjBtn = new Button();
            ObjBtn.Height = 32;
            ObjBtn.Width = 180;
            ObjBtn.HorizontalAlignment = HorizontalAlignment.Right;
            ObjBtn.HorizontalContentAlignment = HorizontalAlignment.Right;
            ObjBtn.VerticalContentAlignment = VerticalAlignment.Bottom;
            DateTime dateTime1 = new DateTime(2004, 3, 4, 13, 6, 55);
            ObjBtn.Content = dateTime1;
            //単一UIElementを入れたボタン(塗潰し四角形)
            Button UIEBtn = new Button();
            UIEBtn.Height = 60;
            UIEBtn.Width = 60;
            UIEBtn.HorizontalAlignment = HorizontalAlignment.Center;
            UIEBtn.HorizontalContentAlignment = HorizontalAlignment.Left;
            UIEBtn.VerticalContentAlignment = VerticalAlignment.Bottom;
            Rectangle rec = new Rectangle();
            rec.Width = 40;
            rec.Height = 40;
            rec.Fill = Brushes.Blue;
            UIEBtn.Content = rec;
            //複数オブジェクトの入ったパネルを入れたボタン(スタックパネルを使用)
            Button panelBtn = new Button();
            panelBtn.Height = 240;
            panelBtn.Width = 180;
            panelBtn.HorizontalAlignment = HorizontalAlignment.Right;
            panelBtn.HorizontalContentAlignment = HorizontalAlignment.Left;
            panelBtn.VerticalContentAlignment = VerticalAlignment.Top;
            StackPanel  StckPanel = new StackPanel();
            Ellipse ellipse = new Ellipse();    //解説:青塗りの円
            TextBlock textBlock = new TextBlock();    //解説:改行文字列ブロック
            ellipse.Width = 40;
            ellipse.Height = 40;
            ellipse.Fill = Brushes.Blue;
            textBlock.TextAlignment = TextAlignment.Center;
            textBlock.Text = "Multiple Objs Button";
            StckPanel.Children.Add(ellipse);    //解説:スタックパネルに円を追加
            StckPanel.Children.Add(textBlock);    //解説:同じく文字列ブロックを追加
            panelBtn.Content = StckPanel;    //解説:ボタンにスタックパネルを追加
            //コンテナ(スタックパネル、ドックパネル、キャンバス)に入れる
            StackPanel Container = new StackPanel();    //解説:スタックパネルに入れてみる。
            //DockPanel Container = new DockPanel();    //解説:ドックパネルでも試せます。
            //Canvas Container = new Canvas();
    //解説:キャンバスでも試せます。
            Container.HorizontalAlignment = HorizontalAlignment.Left;
            Container.VerticalAlignment = VerticalAlignment.Top;
            Container.Children.Add(StrBtn);    //解説:文字列ボタンをスタックパネルに追加。
            Container.Children.Add(ObjBtn);    //解説:DateTimeインスタンスボタンを追加。
            Container.Children.Add(UIEBtn);    //解説:図形ボタンを追加。
            Container.Children.Add(panelBtn);    //解説:複数コントロールボタンを追加。

            /* ---------- */
            /* Window05_2 */
            /* ---------- */

            //Borderを追加(解説:ボーダーはPanelではなく、Decoratorクラスです。)
            Border myBorder = new Border();
            myBorder.Background = Brushes.LightBlue;
            myBorder.BorderBrush = Brushes.Black;
            myBorder.Padding = new Thickness(15);   //解説:コントロール内側の余白ですね。
            myBorder.BorderThickness = new Thickness(2);   //解説:コントロールの外側の余白ですね。
            //StackPanelを追加
            StackPanel myStackPanel = new StackPanel();
            myStackPanel.Background = Brushes.White;
            myStackPanel.HorizontalAlignment = HorizontalAlignment.Center;
            myStackPanel.VerticalAlignment = VerticalAlignment.Top;
            //TextBlockを追加
            TextBlock myTextBlock = new TextBlock();
            myTextBlock.Margin = new Thickness(5, 0, 5, 0);
            myTextBlock.FontSize = 12;
            myTextBlock.HorizontalAlignment = HorizontalAlignment.Center;
            myTextBlock.Text = "Alignment, Margin and Padding Sample";
            //Buttonを3つ追加
            Button myButton1 = new Button();
            myButton1.HorizontalAlignment = HorizontalAlignment.Left;
            myButton1.Margin = new Thickness(20);
            myButton1.Content = "My Button1";
            Button myButton2 = new Button();
            myButton2.HorizontalAlignment = HorizontalAlignment.Right;
            myButton2.Margin = new Thickness(10);
            myButton2.Content = "My Button 2";
            Button myButton3 = new Button();
            myButton3.HorizontalAlignment = HorizontalAlignment.Stretch;
            myButton3.Margin = new Thickness(0);
            myButton3.Content = "My Button 3";
            //StackPanelにコントロールを要素として追加
            myStackPanel.Children.Add(myTextBlock);
            myStackPanel.Children.Add(myButton1);
            myStackPanel.Children.Add(myButton2);
            myStackPanel.Children.Add(myButton3);
            //BorderにStackPanelを単独のコンテンツとして追加
            myBorder.Child = myStackPanel;

            /* ---------- */
            /* Window05_3 */
            /* ---------- */

            //親Canvasの作成
            Canvas ParentCvs = new Canvas();
            //ParentCvs.Width = 210;
            //ParentCvs.Height = 210;

            double unit = this.Width / (3 * 4);    //ウィンドウクライアントエリアの1/3を4等分する
            //子Canvasの作成
            Canvas ChildCvs1 = new Canvas();
            ChildCvs1.Background = Brushes.Red;
            ChildCvs1.Height = unit;
            ChildCvs1.Width = unit;
            Canvas.SetTop(ChildCvs1, unit * 0.5);
            Canvas.SetLeft(ChildCvs1, unit);
            Canvas ChildCvs2 = new Canvas();
            ChildCvs2.Background = Brushes.Green;
            ChildCvs2.Height = unit;
            ChildCvs2.Width = unit;
            Canvas.SetTop(ChildCvs2, unit);
            Canvas.SetLeft(ChildCvs2, unit * 1.5);
            Canvas ChildCvs3 = new Canvas();
            ChildCvs3.Background = Brushes.Blue;
            ChildCvs3.Height = unit;
            ChildCvs3.Width = unit;
            Canvas.SetTop(ChildCvs3, unit * 1.5);
            Canvas.SetLeft(ChildCvs3, unit * 2);
            //親へ子を追加する
            ParentCvs.Children.Add(ChildCvs1);
            ParentCvs.Children.Add(ChildCvs2);
            ParentCvs.Children.Add(ChildCvs3);

            //グリッド(解説:いよいよ3つのサンプルをグリッドに纏めます。)
            Grid gridContainer = new Grid();
            gridContainer.ColumnDefinitions.Add(new ColumnDefinition());
            gridContainer.ColumnDefinitions.Add(new ColumnDefinition());
            gridContainer.ColumnDefinitions.Add(new ColumnDefinition());
            //Containerをグリッドに追加
            Grid.SetColumn(Container, 0);
            gridContainer.Children.Add(Container);
            //myBorderをグリッドに追加
            Grid.SetColumn(myBorder, 1);
            gridContainer.Children.Add(myBorder);
            //ParentCvsをグリッドに追加
            Grid.SetColumn(ParentCvs, 2);
            gridContainer.Children.Add(ParentCvs);

            //メインウィンドウにgridContainerをコンテンツとして入れる
            this.Content = gridContainer;
        }
    }
}

 

出来上がりがこれ↓です。右下のグリップを掴んでウィンドウサイズを変えるとコントロールの動きが分かりますよ。

 

次は当初の目的に沿った容れ物としてダイアログテンプレートみたいなものを作りましょう。