UWP/XAMLのHello,worldアプリ(+任意の内容読み上げ機能)を作ってみた

作ってみたもの

Microsoft ディベロッパーセンターで紹介されている、Hello, world(XAML)アプリ。

docs.microsoft.com

多少機能を加えて、テキストボックスに入力した内容を読み上げてくれるアプリにしました。

機能概要

f:id:Tiratom:20190119081200p:plain
起動画面

から始まり、

f:id:Tiratom:20190119082121p:plain
メイン画面

になり、

f:id:Tiratom:20190119082254p:plain
テキストボックスに文字を入力

テキストボックスに文字を入れた状態で、「Speak!」ボタンを押すと、入力した内容を読み上げてくれるというアプリです。

ちなみに、デスクトップアイコンは

f:id:Tiratom:20190119082453p:plain
デスクトップアイコン

となっています。  

実装概要

※ここではどうやってUWPアプリの作成を開始するかという手順については割愛します。(上述の Microsoftディベロッパーセンターの記事が参考になります)

UWPのアプリを作成した際に、以下のようなフォルダ構成になると思います。

f:id:Tiratom:20190119083244p:plain
ソリューションエクスプローラ

この中の、
MainPage.xaml,
MainPage.xaml.cs,
SplashScreen.scale-200.png
Square44x44Logo.targetsize-24_altform-unplated.png
の4つのファイルをいじります。
(UWPなどGUIを持つアプリについて、MVVMモデルで構成するやり方等もあるそうですが、まだ内容を理解できていないため、今回はソースを書くことだけに焦点を当てたいと思います。そのうち勉強して適切な書き方で書けるように頑張ります。)

実装詳細

・デスクトップアイコン(Square44x44Logo.targetsize-24_altform-unplated.png) の作成

f:id:Tiratom:20190119084644p:plain
VisualStudioで該当ファイルを開いた状態

これは編集後の画面なので、ウインク顔のアイコンがすでにいます。
一番最初に開いたときは、正方形にばつが書かれた図になっていると思うので、消しゴムf:id:Tiratom:20190119084852p:plainで消して、ペンf:id:Tiratom:20190119084949p:plainなどを用いて好きな絵を描きましょう。

背景の緑色のチェック模様は、方眼紙のような役割となっています。
位置をそれで確認しながら描きましょう。(実際の画面では、緑色のチェック模様は表示されません)

ペンの太さや色は、VisualStudioの右側などに表示される、以下のプロパティタブの中身をいじれば変更できます。

f:id:Tiratom:20190119085033p:plain

ここで描いた絵はデスクトップアイコンとなるので、あまり大きくない領域で表示されます。

それを踏まえて絵を描きましょう。

・起動画面(SplashScreen.scale-200.png) の作成

これも先ほどのデスクトップアイコンと同様に、お絵かきしましょう。
起動画面なので、デスクトップアイコンよりも大きく表示されます。
そのため、先ほどよりも細かく書いても見えると思います。

・メイン画面、機能の作成

MainPage.xaml.cs
namespace XAML_HelloWorld
{
    /// <summary>
    /// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    public sealed partial class MainPage : Page
    {
        public MainPageDataClass MainPageData = new MainPageDataClass();

        public MainPage()
        {
            this.InitializeComponent();

        }
    }

    public class MainPageDataClass
    {
        public string Libretto { get; set; } = string.Empty;
        public string TitleLabel { get; set; } = "しゃべらせてみる?";
        public string ButtonLabel { get; set; } = "Speak!";



        public bool IsEmptyLibretto() => string.IsNullOrWhiteSpace(this.Libretto) ? false : true;

        public async void Button_Click(object sernder, RoutedEventArgs e)
        {
            if (e == null)
            {
                throw new ArgumentNullException(nameof(e));
            }

            if (!IsEmptyLibretto())
            {
                Libretto = "文章を 入れてよ!";
            }

            MediaElement mediaElement = new MediaElement();
            var synth = new Windows.Media.SpeechSynthesis.SpeechSynthesizer();
            Windows.Media.SpeechSynthesis.SpeechSynthesisStream stream = await synth.SynthesizeTextToStreamAsync(Libretto);
            mediaElement.SetSource(stream, stream.ContentType);
            mediaElement.Play();

        }
    }
}

以下でMainPage.xaml.cs内の各要点を見ていきたいと思います。  
 

① MainPageクラス

 public sealed partial class MainPage : Page
    {
        public MainPageDataClass MainPageData = new MainPageDataClass();

        public MainPage()
        {
            this.InitializeComponent();
        }
    }

 
MainPageDataClass型のMainPageDataの初期化の部分は、僕が書き加えました。
この初期化を行っている理由は、XAMLファイルから変数にアクセスできるようにするためです。
少し詳しく書くと、XAMLファイル(MainPage.xaml)のx:Classという箇所で、XAML_HelloWorld名前空間のMainPageクラスを、このXAMLファイルと結びつきを持ったクラスとして設定しています。そのため、MainPageクラス内のプロパティにはXAMLファイルから直接アクセスできるのですが、MainPageDataClassクラスのプロパティにはそのままではアクセスできません。そのため、MainPageクラス内でMainPageDataClassクラスの初期化をしています。 このように初期化することで、XAMLファイルからは {x:Bind MainPageData.Libretto} というような形式で、MainPageDataClassのプロパティにアクセスすることができるようになります。(と僕の中では理解しています・・・。) あと、理屈はわかっていないのですが、XAMLと結びつきを持っているMainPageクラスがPageクラス(XAMLファイルの一番外側で使われているタグの種類と対応)を継承していることも、XAMLC#の結び付けのためには重要みたいです。
 
他はデフォルトのままです。

 

② MainPageDataClassクラス

データやらを扱っていまるクラスです。
(前述の通り、MVVMモデルがわかっていないため、クラスの構成は適切ではないかもしれないです)

 public class MainPageDataClass
    {
        public string Libretto { get; set; } = string.Empty;
        public string TitleLabel { get; set; } = "しゃべらせてみる?";
        public string ButtonLabel { get; set; } = "Speak!";

        public bool IsValidLibretto() => string.IsNullOrWhiteSpace(this.Libretto) ? false : true;

        public async void Button_Click(object sernder, RoutedEventArgs e)
        {
            if (e == null)
            {
                throw new ArgumentNullException(nameof(e));
            }

            if (!IsValidLibretto())
            {
                Libretto = "文章を 入れてよ!";
            }

            MediaElement mediaElement = new MediaElement();
            var synth = new Windows.Media.SpeechSynthesis.SpeechSynthesizer();
            Windows.Media.SpeechSynthesis.SpeechSynthesisStream stream = await synth.SynthesizeTextToStreamAsync(Libretto);
            mediaElement.SetSource(stream, stream.ContentType);
            mediaElement.Play();
        }
    }

 
 
Libretto, TitleLabel, ButtoLabelプロパティ
・・・それぞれ、「読み上げる文章・テキストボックスからの入力値」、「メイン画面の見出し」、「ボタンの文字」の値を保持します。
 
 
・IsValidLibrettoメソッド
・・・Librettoの値が空かどうかを判断するメソッドです。
 
・Button_Clickメソッド ・・・ボタンをクリックした際に呼び出されるメソッドです。(どのボタンを押した際にどのメソッドを呼び出すかは、XAMLファイルに設定します。)
MadiaElement mediaElement = new MediaElement().... からmediaElement.Play(); までの行では、
①オーディオや動画を操作するためのコントロールを持つクラスを初期化 
② インストール済みの音声合成エンジン機能へのアクセスができるクラスを初期化 
③引数で与えた文字列を基に、出力する音声を非同期で作成してくれるメソッドの呼び出し 
④1のクラスの音声のソースとなるファイルとして、3のメソッドの戻り値を設定 
⑤音声を再生 
といったものを行っていると理解しています。
 
 

MainPage.xaml
<Page
    x:Class="XAML_HelloWorld.MainPage"
    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:local="using:XAML_HelloWorld"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    mc:Ignorable="d">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="2*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="3*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="3*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <TextBlock
            Grid.Row="1"
            Grid.Column="1"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="25"
            FontWeight="ExtraBlack"
            Text="{x:Bind MainPageData.TitleLabel}" />

        <TextBox
            Grid.Row="3"
            Grid.Column="1"
            VerticalAlignment="Stretch"
            AcceptsReturn="True"
            Text="{x:Bind MainPageData.Libretto, Mode=TwoWay}"
            TextWrapping="Wrap" />
        <Button
            Grid.Row="5"
            Grid.Column="1"
            HorizontalAlignment="Stretch"
            VerticalAlignment="Top"
            Click="{x:Bind MainPageData.Button_Click}"
            Content="{x:Bind MainPageData.ButtonLabel}" />

    </Grid>
</Page>

では、ソースを詳しく見ていきたいと思います。
     
② Pageプロパティ

<Page
    x:Class="XAML_HelloWorld.MainPage"
    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:local="using:XAML_HelloWorld"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    mc:Ignorable="d">

ここではいろいろと設定を行っています。
x:Classで指定されているクラスが、このXAMLファイルに紐づいているクラスとなります。

     
② Grid

<Grid>
  (中略)
</Grid>

<Page></Page>の直下に、画面のベースとなる部品を1つ記述します。
たいていの場合は、以下サイトで説明されているようなレイアウトパネルを設置し、その中に様々な部品(LabelやButtonなど)や、さらにレイアウトパネルを入れ子にして設置して多様な画面構成を行います。
ユニバーサル Windows プラットフォーム (UWP) アプリのレイアウト パネル - UWP app developer | Microsoft Docs
 
なお、Page直下に部品を複数置いてしまうと、「プロパティContentが複数回指定されています」というエラーが発生するはずです。その際には、部品の階層構造を見直してみる必要があります。

 
 
② Grid.XXXDefinitions(ページの枠組みの定義部分)

     <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="2*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="3*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

<Grid.RowDefinitions>の中にを複数置くことで、その個数分ページ上に行ができます。
Height="*" と指定すると、行の高さを自動で設定してくれます。
Height="2*" と指定すると、その行は="*"で指定した行の2倍の高さになります。
つまり、上記のソースでは、ウィンドウの高さを(1+2+1+3+1+1+1=)10で割った高さをベースとして、1行目はその高さ、2行目はその2倍の高さ、3行目はベースの高さ・・・というようにしてくれます。

     <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="3*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

<Grid.ColumDefinitions>では、RowDefinitionと同様のやり方で、列を定義します。
これらのRowDefinitionsとColumnDefinitionsにより、UWPの画面が7×3のマス目に分割することができます。(枠線は設定していないので、画面上に線が現れるわけではありません)
 

② テキストブロック

     <TextBlock
            Grid.Row="1"    
            Grid.Column="1"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="25"
            FontWeight="ExtraBlack"
            Text="{x:Bind MainPageData.TitleLabel}" />

Grid.Rowでは、RowDefinitionsで定義した行のどの部分に位置するかを設定します。
0始まりなので注意してください。
Grid.ColumnもGrid.Rowと同様の使い方で、列の位置を決めます。
このGrid.RowとGrid.Columnの設定だと、下図の位置にTextBoxは表示されます。

f:id:Tiratom:20190127213011p:plain
TextBoxの位置(黄色部分)

 
・HorizontalAlignment・VerticalAlignment
…このTextBoxが所属するGridの枠線内のどの位置にTextBoxを表示させるか、を設定します。(=親要素内における位置を設定します。)
HorizontalAlignmentでは、水平方向のどの位置か(Left:左寄せ、Right:右寄せ、Center:真ん中寄せ、Stretch:左右枠いっぱいに広げる)を決めます。
VerticalAlignmentでは、垂直方向のどの位置か(Top:上寄せ、Bottom:下寄せ、Center:中央寄せ、Stretch:上下枠いっぱいに広げる)を決めます。
 

・FontSize
…その名の通り文字の大きさを数字で設定します。
・FontWeight
…値を既定の選択肢から選ぶことで、文字の太さを設定することができます。
Textでは、 表示する文字列を設定することができます。 ここでは、{x:Bind MainPageData.TitleLabel}" と記述することで、前述したMainPage.xaml.csファイル内のMainPageクラス内で初期化したMainPageDataのTitleLabelの値との紐づけを行っています。  
 
② テキストボックス

     <TextBox
            Grid.Row="3"
            Grid.Column="1"
            VerticalAlignment="Stretch"
            AcceptsReturn="True"
            Text="{x:Bind MainPageData.Libretto, Mode=TwoWay}"
            TextWrapping="Wrap" />

 
・AcceptsReturn
…trueに設定することで、テキストボックス内での改行を行えるようにします。
・Text
…テキストボックス内の文字を設定します。
ここでは、MainPageクラス内の変数であるMainPageDataが持つLibrettoプロパティとの紐づけを行っています。
Modeは、画面上の値とソースコード内における値がどう影響を及ぼすかを設定します。
OneWayだと、ソースコードにおける値(Librettoプロパティの値)が変化した際に、画面(XAMLファイル)の値も変更されます。
TwoWayだと、OneWayに加えて画面(XAMLファイル)の値が変化した際に、ソースコードにおける値も変更されます。画面入力をもとに何かプログラムを動かす際は、TwoWayに設定する必要があります。
TextWrappingでは、テキストボックスの端まで文章が伸びた際に、そのまま横につなげていくか、下の段に折り返して表示するかを設定します。 Wrapにすれば、下の段に折り返されます。
 
 
② ボタンボックス

     <Button
            Grid.Row="5"
            Grid.Column="1"
            HorizontalAlignment="Stretch"
            VerticalAlignment="Top"
            Click="{x:Bind MainPageData.Button_Click}"
            Content="{x:Bind MainPageData.ButtonLabel}" />

 
・Click
…このボタンをクリックした際に呼び出すメソッドを指定します。{x:Bind MainPageData.Button_Click}と書いていることからわかるように、MainPageクラス内の変数MainPageDataが持つ、Button_Clickメソッドが、呼び出されるメソッドとして設定されています。
・Content
…ボタンに表示する文字を設定します。ここもバインディングが設定してあり、MainPageDataのButtonLableの値が実際には表示されることになります。

 
 
 

まとめ

以上、XAMLのHello,worldアプリについて僕なりの理解をまとめてみました。
UWPとSQLiteの接続について現在は勉強中で、まだ理解ができていないのですが、また自分なりに内容が理解できれば整理してみようと思います。
   

参考サイト