CSharp

コンテキストメニュー制御いろいろ(C# / WPF / MVVM)

メインウィンドウWPF でコンテキストメニューを表示すること自体は簡単ですが、制御しようと思うと幅広い考え方をする必要があり分かりづらいので、まとめて整理しました。

テストプログラムは GitHub に上げてあります。MVVM ライブラリは Livet を使用しています。Visual Studio 2022 + .NET 6 にて動作確認しています。


シンプルなコンテキストメニュー

素の状態のサンプルとして、ラベルにコンテキストメニューを付けています。ContextMenu タグを付けるだけの簡単なお仕事です。

<Label Content="シンプルなコンテキストメニュー:右クリックでオープンする(左クリックではオープンしない)" >
<Label.ContextMenu>
    <ContextMenu>
        <MenuItem Header="シンプルラベルメニュー 1" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="シンプルラベルメニュー 1" />     
(省略...)
        </MenuItem>
    </ContextMenu>
</Label.ContextMenu>
</Label>

ラベル右クリックラベルを右クリックするとコンテキストメニューが表示されます。

テストプログラムでは、コンテキストメニューを捕捉したことが分かるようにするため、コンテキストメニューをクリックするとステータスバーにメッセージを表示します。

左クリックでもオープンするコンテキストメニュー

通常、コンテキストメニューは右クリックで表示されますが、EventTrigger でイベントを捕捉することにより、ボタン左クリックでもコンテキストメニューを表示することができます。

<Button Name="ButtonLeftSample" Content="左クリックでもオープンするコンテキストメニュー(コントロールのイベントを捕捉してオープンする)" Margin="0,10,0,0" >
<behaviors:Interaction.Triggers>
    <behaviors:EventTrigger EventName="Click">
        <behaviors:ChangePropertyAction TargetObject="{Binding ContextMenu, ElementName=ButtonLeftSample}" PropertyName="IsOpen" Value="True"/>
        <behaviors:ChangePropertyAction TargetObject="{Binding ContextMenu, ElementName=ButtonLeftSample}" PropertyName="PlacementTarget" Value="{Binding ElementName=ButtonLeftSample, Mode=OneWay}"/>
    </behaviors:EventTrigger>
</behaviors:Interaction.Triggers>
<Button.ContextMenu>
    <ContextMenu >
        <MenuItem Header="左クリックメニュー 1" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="左クリックメニュー 1" />
        <MenuItem Header="左クリックメニュー 2" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="左クリックメニュー 2" />
    </ContextMenu>
</Button.ContextMenu>
</Button>

ラベルで同様のことをやろうと思った場合、ラベルには Click イベントが無いので、代わりに MouseLeftButtonUp イベントを捕捉するとそれっぽくなるのではないでしょうか(ラベル以外でマウスボタンを押して、ドラッグでラベルまで移動してきて、そこでマウスボタンを離した場合もコンテキストメニューが表示されてしまいますが……)。

テキストボックスデフォルトのコンテキストメニューを非表示にする

テキストボックスには、デフォルトでコンテキストメニュー(コピー、貼り付け等)が付いてきます。

このコンテキストメニューを表示したくない場合は、コンテキストメニューを null にします。

<TextBox Width="100" Text="非表示" VerticalAlignment="Center" ContextMenu="{x:Null}" />

リストボックスのアイテムのみ右クリックでオープンするコンテキストメニュー

ListBoxリストボックスにコンテキストメニューを付けると、リストボックス内のどこをクリックしてもコンテキストメニューが表示されます。

リストボックスのアイテムがある部分を右クリックした場合のみコンテキストメニューを表示したい場合は、ItemContainerStyle で ListBoxItem に対してコンテキストメニューを付けます。

この時、DataContext を指定しないとコマンドを捕捉できません。これは、コンテキストメニューの VisualTree が変な感じになっているからのようです。WPF の嫌なところです……。

<ListBox ItemsSource="{Binding NarrowListBoxItems}" Width="290" >
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem" >
            <Setter Property="Tag" Value="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext}"/>
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                        <MenuItem Header="アイテムのみ ListBox メニュー 1" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="アイテムのみ ListBox メニュー 1" />
                        <MenuItem Header="アイテムのみ ListBox メニュー 2" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="アイテムのみ ListBox メニュー 2" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>
DataGrid についても同様のやり方で、アイテムがある部分のみでのコンテキストメニュー表示ができます。

DataGrid の場合、TargetType を DataGridRow にします。

動的に状態を制御するコンテキストメニュー

コンテキストメニューのヘッダー(ラベル)等を動的に制御したい場合は、ContextMenuOpening イベントを捕捉すると簡単です。ContextMenuOpening イベントハンドラー内で、ヘッダーや IsEnabled などをいじることで、コンテキストメニューの状態を動的に制御できます。

Visibility をいじることで、ある程度の項目の増減もできます。

例えば、ファイルを右クリックでファイル系メニュー、フォルダーを右クリックでフォルダー系メニューを表示したい場合、両方のメニューを記述しておいて、不要なメニューの Visibility を Collapsed にすることで対応できます。

<Label Content="動的に状態を制御するコンテキストメニュー" Margin="0,10,0,0" >
<behaviors:Interaction.Triggers>
    <behaviors:EventTrigger EventName="ContextMenuOpening">
        <l:LivetCallMethodAction MethodName="PrepareDynamicStateContextMenu" MethodTarget="{Binding}" />
    </behaviors:EventTrigger>
</behaviors:Interaction.Triggers>
<Label.ContextMenu>
    <ContextMenu>
        <MenuItem Header="{Binding MenuItemHeaderDynamicState}" HeaderStringFormat="{}{0} 1" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="動的状態制御メニュー 1" />
        <MenuItem Header="{Binding MenuItemHeaderDynamicState}" HeaderStringFormat="{}{0} 2" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="動的状態制御メニュー 2" IsEnabled="{Binding IsDynamicStateMenuItemEnabled}" />
        <MenuItem Header="{Binding MenuItemHeaderDynamicState}" HeaderStringFormat="{}{0} 3" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="動的状態制御メニュー 3" Visibility="{Binding DynamicStateMenuItemVisibility}"/>
    </ContextMenu>
</Label.ContextMenu>
</Label>

動的にアイテム数を制御するコンテキストメニュー

Visibility の制御ではまかなえないほどフリーダムにコンテキストメニューのアイテムを変更したい場合は、ItemsSource をバインドし、ContextMenuOpening イベントで ItemsSource を変更します。

<Label Content="動的にアイテム数を制御するコンテキストメニュー" >
<behaviors:Interaction.Triggers>
    <behaviors:EventTrigger EventName="ContextMenuOpening">
        <l:LivetCallMethodAction MethodName="PrepareDynamicItemsContextMenu" MethodTarget="{Binding}" />
    </behaviors:EventTrigger>
</behaviors:Interaction.Triggers>
<Label.ContextMenu>
    <ContextMenu ItemsSource="{Binding DynamicMenuItems}" />
</Label.ContextMenu>
</Label>

コンテキストメニューでサブフォルダーを列挙して表示するようなケースでは、こちらの方法が良いかと思います。

C# で SQLite を便利に使うサンプルコード(LINQ to SQLite)【改訂版】

.NET 6 C# で SQLite3 を使う方法について整理し、サンプルプログラムにまとめました。

C# では、文字列ベースのクエリよりも遙かに効率的かつミスが少なくプログラムすることができます。

サンプルプログラムは GitHub に上げてあります。

(補足)
本記事は、以前の記事を時代に合わせて改訂したものです。


使用するパッケージ

C# で SQLite を使うためには、Microsoft.EntityFrameworkCore.Sqlite パッケージを使用します。

以前はどのパッケージ(ライブラリ)を使うか少し迷うところもありましたが、現在は Entity Framework Core (EF Core) 一択でいいのではないでしょうか。

Microsoft.EntityFrameworkCore.Sqlite のインストール

NuGetインストールは簡単です。

  1. Visual Studio を起動し、SQLite を使いたいプロジェクトを開きます。
  2. メニューの[ツール|NuGet パッケージマネージャー|ソリューションの NuGet パッケージの管理]をクリック。
  3. 検索窓に「Microsoft.EntityFrameworkCore.Sqlite」と入力して SQLite パッケージを検索。
  4. 検索結果の Microsoft.EntityFrameworkCore.Sqlite を選択して、SQLite を使いたいプロジェクトにチェックを入れ、インストールボタンをクリック。関連するパッケージも含めて自動でインストールが終わります。
以上でインストール完了です。

テーブル構造定義クラスの作成

データへのアクセスを簡単・分かりやすくするために、テーブル構造を定義するクラスを作成します。

簡易名簿テーブル「t_test」が以下のような構造になっているとします。

フィールド名NULL備考
test_id整数不可連番、主キー
test_name文字列不可氏名、インデックス作成、ユニーク制約
test_height浮動小数身長

この場合、テーブル構造定義クラスは以下のようになります。

[Table("t_test")]
[Index(nameof(Name), IsUnique = true)]
internal class TTestData
{
    // ID
    [Key]
    [Column("test_id")]
    public Int32 Id { get; set; }

    // 氏名
    [Column("test_name")]
    public String Name { get; set; } = String.Empty;

    // 身長
    [Column("test_height")]
    public Double? Height { get; set; }
}

基本的には、カラムをプロパティーとして宣言するだけです。その際、[Column] 属性でデータベースファイルのフィールド名を指定しておきます。SQLite の実際の型についてはフレームワーク側で良きに計らってくれますので、コードにする必要はありません。

主キーとなるカラムには [Key] 属性を付けておきます。

クラスには [Table] 属性を付けてテーブル名を指定します。

インデックスやユニーク制約を付けたい場合は、クラスに [Index] 属性を付けます。インデックスを複数個作成したい場合は、[Index] 属性を複数個記述します。

複合インデックス(複数カラムで 1 つのインデックス)を作成したい場合は [Index(nameof(Name), nameof(Height))] のように 1 つの [Index] 属性の中に複数のカラムを記述します。

インデックスの詳細は EF Core のドキュメントにまとめられています。

このサンプルプログラムでは、コード記述 → コードの通りにデータベース作成、の流れですが、逆に、既にデータベースファイルがある場合は、それを解析してコードを自動生成してくれるスキャフォールディングも用意されているようです。

コンテキストクラスの作成

テーブル構造定義クラスに対応したテーブルを持つデータベースにアクセスするためには、コンテキストクラスを用います。テーブル構造定義クラスが 1 つのテーブル(のレコード)、コンテキストクラスが 1 つのデータベースファイルのイメージです。

internal class TestContext : DbContext
{
    // テストテーブル
    public DbSet<TTestData> TestData { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        SqliteConnectionStringBuilder stringBuilder = new()
        {
            DataSource = "Test.sqlite3",
        };
        using SqliteConnection sqliteConnection = new(stringBuilder.ToString());
        optionsBuilder.UseSqlite(sqliteConnection);
    }
}

テーブルを DbSet<T> のプロパティーとして宣言します。1 つのデータベースファイルに複数のテーブルがある場合は、DbSet<T> なプロパティーを複数宣言します。

OnConfiguring() でデータベースファイルと対応づけます。

データベースファイルの作成

以上 2 つのクラスを作成したことで準備は整いました。

これから、実際にデータベースを操作します。

まずは空のデータベースファイルを作成するところからですが、コンテキストクラスから EnsureCreated() するだけです。

using TestContext testContext = new();
testContext.Database.EnsureCreated();

ファイルがまだ無い場合はこれでファイルが作成され、逆に、既にデータベースが存在している場合は特に何も起こりません。データベースがクリアされたりはしませんので、とりあえず EnsureCreated() しておけばデータベースにアクセスできるようになります。

レコードの挿入

1 レコードが、初めに作成した TTestData の 1 インスタンスです。レコードの挿入はテーブルプロパティーの Add() で行います。

testContext.TestData.Add(new TTestData { Name = "Fukada Kyoko" });
testContext.TestData.Add(new TTestData { Name = "Eda Ha", Height = 159.0 });
testContext.SaveChanges();

主キーが整数の場合は自動インクリメントになりますので、Id は設定する必要ありません。Height は NULL 可なので、指定しないと NULL になります。

Add() した時点では実際にはデータベースファイルには追加されず、SaveChanges() でまとめて追加されます。

文字列で SQL を記述する必要がないため、タイプミスによるエラーが発生せず、また、Visual Studio の各種サジェストによる支援も受けられます。確実・簡単にデータベースを操作できます。

レコードの検索

レコードの検索は、テーブルプロパティーから LINQ で行います。

IQueryable<TTestData> queryResult = testContext.TestData.Where(x => x.Name == "Eda Ha" || x.Height < 150.0).OrderBy(x => x.Height);

結果は foreach で回せるので、簡単に扱えます。

検索で注意が必要なのは、文字列を「含む」検索(部分一致検索)です。大文字小文字を区別して含む検索をしたい場合(小文字の a を含む)は、String.Contains() を用いて

Where(x => x.Name.Contains("a"))

のようにします。

一方で、大文字小文字を区別せずに含む(a でも A でも含む)検索をしたい場合、

Where(x => x.Name.Contains("a", StringComparison.OrdinalIgnoreCase))

とすると、「The LINQ expression '~' could not be translated」というエラーになります(InvariantCultureIgnoreCase や CurrentCultureIgnoreCase も同様です)。代わりに、

Where(x => EF.Functions.Like(x.Name, $"%a%"))

のように Like() 関数を用います。

レコードの削除

レコードを削除する場合、削除したいレコードを検索し、それを削除する、という流れになります。

検索については前節と同様で、その結果を、RemoveRange() するだけです。

IQueryable<TTestData> queryResult = testContext.TestData.Where(x => x.Height == null || x.Height < 150.0);
testContext.TestData.RemoveRange(queryResult);
testContext.SaveChanges();

SaveChanges() をお忘れなく。

レコードの更新

レコードの削除と同様、更新したいレコードを検索し、そのプロパティーを更新する、という流れになります。

こちらも更新後の SaveChanges() をお忘れなく。

サンプルコードでは、更新の時のみ、検索結果を IQueryable<TTestData> ではなく List<TTestData> で受けています。

IQueryable で SQLite を使う場合は遅延評価されるようで、foreach 等で実際に TTestData を取得して初めて検索が行われます。大量にデータがあり途中で foreach を打ち切った場合など、後半の不要な検索が行われず効率的です。

しかし、サンプルコードの場合、身長 155 cm の人は検索対象なのできちんと 145 cm に更新されるのですが、結果表示の際は「身長 >= 150」を満たさなくなり、結果には表示されなくなってしまいます。

これを防止するために、List で検索結果を受けています。ToList() の際に検索が行われるので、後で身長が変化しても、List には引き続き対象レコードが存在することになり、きちんと結果表示も行われます。

サンプルコードについて

Runサンプルコード(Test LINQ to SQLite Gen 2)は GitHub に上げてあります。.NET 6 + Visual Studio 2022 + Windows 10 で動作確認しています。Visual Studio でソリューションを開き F5 を押せば実行できます。

本記事に対応するのは、サンプルプログラム実行時に表示されるウィンドウの左半分、「基本操作」のところです。

「検索」「削除」「更新」いずれのボタンをクリックしても、まず最初にデータベースとサンプルレコードが作成されます。削除によりレコードが 0 になっていた場合もサンプルレコードが再作成されます。

「検索」ボタンをクリックすると、名前が "Eda Ha" または、身長が 150 cm 未満の人が検索されます。

「削除」ボタンをクリックすると、身長未登録、または身長が 150 cm 未満の人が削除されます。

「更新」ボタンをクリックすると、身長 150 cm 以上の人の身長を 10 cm 下げます。

「更新」してから「検索」や「削除」すると、以前と結果が変わります。

なお、ウィンドウの右半分、「ジェネリック」については、LINQ to SQLite で共通カラム部分をジェネリックで運用するをご覧ください。

インメモリデータベース

EF Core の SQLite3 でもインメモリデータベースは使用可能です。

サンプルコードでは、TestContext.OnConfiguring() のプリプロセッサ部分を true にすることでインメモリデータベースにできます。

インメモリデータベースにする場合は、OnConfiguring() で SqliteOpenMode.Memory を用いますが、その際は接続を開いておく必要があるようです。開いておかないとエラーが出ます。

インメモリデータベースは接続が切れると消滅するので、コンテキストを MainWindow クラスのメンバとして保持し続けるなどの対応が必要になるかと思います。

ジャーナルモード

EF Core はデフォルトでジャーナルモードが WAL(Write-Ahead Logging:先行書き込みログ)になっています。

ジャーナルモードを DELETE 等他のものにしたい場合は、コマンドを実行することで可能です。

サンプルコードでは、TestContext.OnConfiguring() のプリプロセッサ部分を true にすることで DELETE にできます。ジャーナルモードを変更した場合、一度データベースファイルを削除するのが確実です。

更新履歴

  • 2022/01/01 初版。
  • 2022/01/01 ジャーナルモードについて記載。
  • 2022/01/03 インメモリデータベースについて記載。



WPF アプリを Microsoft Store に申請・登録する(アプリ更新編)

前回配布したアプリのバージョンアップをします。

アプリ更新編 目次


更新版アプリのパッケージ作成

V2Srcアプリのソースコード(XAML)の「Ver 1」の部分を「Ver 2」に変更します。

Run動作を確認したら、更新版のアプリで再度パッケージを作成します。

パッケージ作成前に、ソリューションのクリーンをしておくほうが良いようです。

UploadErr2後でパッケージをアップロードする際、「お客様の申請には、パッケージのコンテンツは異なっているが、提供されている他のパッケージと同じフルネームを持つパッケージが含まれています」というエラーになり、パッケージのバージョンは上げているのに何故だろうと悩んだのですが、クリーンすることでエラーが出なくなりました。

SelectHaifuパッケージ作成時の処理で初回と異なるのは、配布方法選択時に、既存のアプリ名を選択することです。

また、バージョン番号も上げておきます(デフォルトでは自動で末尾が上がります)。

パッケージ作成後、WACK が合格になることを確認するのは初回と変わりません。

更新申請

AppGaiyouMicrosoft パートナーセンターのアプリケーションの概要ページで更新リンクをクリックします。

更新申請の場合は、変更のあるページのみ処理すれば構いません。

Package「パッケージ」ページで、Ver 2 のパッケージをアップロードします(スクリーンショットは Ver 4→Ver 5 の時のものですが、Ver 1→Ver 2 でも操作は同様です)。

初回のパッケージ(Ver 1)も引き続き存続しているので、パッケージが 2 つになります。新しいほうが 1(青色)、古いほうが 2(橙色)で示されます。新しいパッケージを利用できない環境では古いパッケージにフォールバックするようですが、フォールバックが不要なら古いパッケージは削除しても構わないと思います。

古いパッケージを存続させても、表示されるアプリサイズは増えません。Ver 1 も 2 も 140 MB ほどのサイズが表示されました。

StoreTourokuUp「Store 登録情報 - 日本語(日本)」ページで、「このバージョンの最新情報」欄に更新内容を記入します。

必要に応じてスクリーンショットも更新しておきます。

ShinseiUp申請ページが更新したページだけ「更新済み」となるので、そのまま「Microsoft Store に提出」ボタンをクリックすれば、更新版を申請できます。

ストアからの更新

StoreV2認定が終わった後、Ver 1 をインストールしている環境で再度ストアを開くと更新ボタンが表示されるので、そこからアプリを Ver 2 に更新できます。

MSIX の差分更新機能が活きDownloadDiff、表示されているアプリサイズは 140 MB ですが、ダウンロードは 400 KB 程度でした。

ユーザーが自分でストアで更新しなくても、自動更新も行われます。ただし、タイムラグはかなりある印象です。

旧バージョンを起動した際、バックグラウンドで更新が行われ、次回起動時には新バージョンになるようなのですが、自動更新されるまでに丸 1 日近く(23 時間程度)かかった場合もありました。

所感

開発者登録~更新までの一連の作業をしてみての感想としては、
  • やはり労力は今までより多くかかる……が、許容できないほどではない。
  • Microsoft パートナーサイトでの手続き自体は分かりやすい。
  • しかし謎のエラーに悩まされることもある(同じフルネームエラー以外にも、ストアページから何回やっても更新できない現象にも遭遇、最終的には再度更新申請した)。
  • パートナーサイトの応答が全体的にもっさりしている。
  • やむを得ないが、各所でタイムラグがある(申請~認定、自動更新等)のでまどろっこしい。
という感じです。

こちらで整理したように、開発者としてはデメリットも多いものの、逆にメリットもありますし、また、ユーザー側から見ればメリットは多いので、本番アプリのストア配布も前向きに検討しようかと思います。

WPF アプリを Microsoft Store に申請・登録する(全 4 回)

  1. 開発者登録編
  2. アプリ作成編
  3. ストア配布編
  4. アプリ更新編(←今ここ)

関連記事



WPF アプリを Microsoft Store に申請・登録する(ストア配布編)

前回アプリのパッケージを作成したので、いよいよストア配布の申請をします。

ストア配布編 目次


申請

DashboardMicrosoft パートナーセンターのホームで「アプリとゲーム」をクリックします。

AppAndGame予約したアプリ名(ストアテストアプリ)が表示されているのでクリック。

Gaiyou「申請を開始する」ボタンをクリック。

Shinsei1未開始になっているすべてのステップのページ内容を埋めていきます。

Kakakutoteikyou「価格と提供の状況」ページ。

今回はテストアプリなので、表示範囲をプライベートユーザーにしました。これにすると、グループ(今回は「ストアテスト」)として指定するメアドのユーザーにだけアプリが表示されます。

グループ作成ページで自分のメアドだけを指定すれば、自分だけがストアからダウンロードできるようになります。なお、グループ作成ページに遷移すると、価格と提供の状況ページで入力途中の内容は失われるようなので、最初にグループを作っておく方が良いようです。

価格は無料にしています。

Property「プロパティ」ページ。

カテゴリとサブカテゴリは、テストアプリなので、開発者ツール・開発者キットにしておきました。

問題になってくるのがプライバシーポリシーです。

画面にバージョンを表示するだけのアプリなので、当然個人情報の収集はしていないのですが、後続のパッケージアップロードあたりまで進んだ段階でこのページに戻ってくると、個人情報の収集が勝手に「はい」にされていました。そうなると、プライバシーポリシー URL の入力が必須になります。

つまり、どんなアプリでもプライバシーポリシーを書いたページを用意しておく必要があるということです。

とはいえ、こちらの記事にあるように、ページの内容としては、ごく簡単に「収集してません」程度のもので良いようです。今回は GitHub のアプリページの末尾に記述をしておきました。念のために英語にしてみましたが、日本語で構わないようです。

システム要件もそれっぽいのを入れておきます。

Rating「年齢別レーティング」ページ。

質問に答えていくと、各種レーティングシステムでのレーティングを生成してくれます。

年齢制限すべき内容ではないので、全年齢にしておきました。

Package「パッケージ」ページ。

作成したアプリパッケージ(パッケージプロジェクトフォルダーの中の AppPackages フォルダーにある msixupload ファイル)をドラッグ&ドロップしてアップロードします。

DeviceFamilyデバイスファミリの利用可否のところで、どの Windows で使えるかを指定します。今回はノーマルな Windows のみにしておきました。

StoreTouroku「Store 登録情報」ページ。

ストアでのアプリ配布時の表示(アプリ説明)を管理します。

StoreTourokuJpn「日本語(日本)」をクリックすると、日本語でのアプリ説明を編集できます。

用意されている入力項目は多いのですが、必須なのは
  • 製品名(アプリ名)
  • 説明
  • スクリーンショット
の 3 つだけです。

アプリ名を予約する際、英数名と日本語名の両方で予約した場合は、製品名でどちらかを選べます。

Shinsei1Doneここまで入力すると、申請に必要な内容がすべて揃いました。

右側のマークがすべて緑色の完了になっています。

申請オプションは特に変更しなくて大丈夫です(タイミングを遅らせることもできます)。

「Microsoft Store に提出」ボタンをクリックすると、申請が行われます。

認定待ち

Ninteichu申請すると審査が行われますので、認定されるまでにはしばらく時間がかかります。

NInteichu2プログレスバーはリアルタイムの状況を反映しているわけではないようです。「この申請の確認」ページに遷移してから再度「進行状況を確認」すると、いつの間にかプログレスバーが進んでいます。

再確認があったのかバグなのかはわかりませんが、プログレスバーが「公開処理中」から「認定」に逆戻りしたこともありました。

今回は、およそ 45 分ほどで認定されました。

しかし、長いときは丸 7 日近くかかったこともありました。

Gaiyou2認定されるとメールが届き、アプリケーションの概要ページでも「Microsoft Store で取扱い中」になります。

コラム:認定作業は平日のみか?

認定作業は土日も行われるのでしょうか?

ストア自体は、土日だろうが夜中だろうが、アプリのダウンロード等はできます。物理的なアイテムの出荷については「営業日は、月曜日 ~ 金曜日の午前 8:00 ~ 午後 5:00 (祝日を除く)」と記載があります。

パートナーサイトでは認定について「最大 3 営業日かかることがあります」と表記されています。「3 日」ではなく「3 営業日」なので、お休みの日もありそうな雰囲気で記載されています。

しかし、これまで 7 回ほど認定されたメールの送信日時を見る限りでは、土日や夜中も認定作業は行われているようです。日本時間でも 23 時台にメールが来ましたし、土曜日もメールが来ました。マイクロソフト本社があるレドモンドは太平洋時間ですが、太平洋時間の 21 時台や、日曜日にもメールが来ました。

ストアからのダウンロード

認定されたアプリがきちんとストアで公開・配布されているか確認します。

Search今回はプライベートユーザーのみの配布ですが、検索可能なオプションにはしました。しかしながら、実際には検索しても表示されません。タイムラグの問題かなとも思いましたが、3 日経った今も検索できません……。

仕方がないので、URL 直打ちします。

SeihinIdMicrosoft パートナーセンターのアプリのページの中に「製品 ID」ページがあります。

StoreWebプライベートユーザーのみの配布の場合、「特定の人だけにアプリが表示される場合の URL (認証が必要)」の URL をブラウザに打ち込めばストアのアプリページにアクセスできます。

StoreApp入手ボタンをクリックするとストアアプリが起動し、そこから配布したアプリをダウンロードできます。

StoreApp2ストアに表示されているアプリのサイズは 143 MB なのですが、ダウンロードは 60 MB 程でした。MSIX の差分更新機能により、既に持っているパーツは除いてインテリジェントにダウンロードしてくれているようです。

StartMenuスタートメニューにもきちんと登録されました。

RunWin11無事に起動。

AppAndFuncWindows 設定のアプリと機能にも登録されていて、ここからアンインストールもできました。

WPF アプリを Microsoft Store に申請・登録する(全 4 回)

  1. 開発者登録編
  2. アプリ作成編
  3. ストア配布編(←今ここ)
  4. アプリ更新編

WPF アプリを Microsoft Store に申請・登録する(アプリ作成編)

前回開発者登録を終えたので、今回は WPF アプリを作ります。Windows 10/11、Visual Studio 2022、.NET 6 で動作確認しています。

アプリ作成編 目次


準備

Installストアでアプリを配布するには、作成したアプリを MSIX パッケージにする必要があります。

Visual Studio にパッケージツールが入っているかを確認しておきます。

Visual Studio のインストーラーを起動して変更モードに入り、「.NET デスクトップ開発」ワークロードのインストール詳細に「MSIX Packaging Tools」がオンになっていることを確認します。もしオフになっていたら、オンにしてインストールします。

なお、MSIX については WPFアプリのmsixによるweb配布、自動更新方法という記事が分かりやすくまとまっていて、大変参考になりました(自前サイトでの配布向けのため、後半のやり方は異なります)。

(メモ)
非 UWP アプリを UWP 風にしてストア配布できるようにする仕組みを Desktop Bridge と呼ぶようです。
Desktop Bridge のやり方はいくつかあり、昔は Desktop App Converter(DAC) を使っていましたが、その後継が MSIX Packaging Tools のようです。

WPF アプリ作成

WPF適当に WPF アプリを作成します。

AppRun上記のサイトに倣い、ウィンドウにバージョンを表記するアプリです。

MainWindow.xaml の中身をほんの少しだけ追加するくらいのものです。最初は Ver 1 にしておきます。

    <Grid>
        <Label Content="Ver 1" FontSize="50" HorizontalAlignment="Center" VerticalAlignment="Center" />
    </Grid>

一応、ソースコードは GitHub に上げてあります。

高 DPI 対応宣言

ディスプレイが高 DPI なものでもちゃんとした表示になりますよ、という宣言をしておきます。

これをしておかないと、後でアプリを検証する際に警告が出ます。あくまでも警告であって不合格ではないので、宣言しなくても審査は通るかもしれませんが。

NewItemソリューションエクスプローラーのプロジェクトを右クリックし、[追加 → 新しい項目]を選びます。

AppManifestアプリケーションマニフェストを選んで追加します。

プロジェクトに app.manifest ファイルが追加されるので開き、<assembly> セクションの中に以下を追記します。

  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
    </windowsSettings>
  </application>

DPI のためのマニフェストについては、こちらに解説があります。

64 ビット設定

64 ビットのアプリとしてパッケージングしたい場合は、パッケージプロジェクトを作る前に予め 64 ビットの設定をしておく必要があります。そうしないと、似非 64 ビットパッケージになってしまいます

PlatformTargetプロジェクトのプロパティーで、Platform target を x64 にします。

また、念のため、ターゲット OS バージョンとサポートされている OS バージョンを、後述のパッケージターゲットバージョンと揃えて 10.0.19041.0 にしておきます。

Kousei[ビルド → 構成マネージャー]メニューで、プラットフォームを新規作成して x64 にします。

既に作成されていますという旨のエラーが出て x64 を新規作成できない場合は、いったん Visual Studio を閉じて、ソリューションファイルをバックアップしたうえで、テキストエディタで開き、preSolution の欄にある Any CPU 以外のものを削除してから、再度 Visual Studio で開くと、x64 を新規作成できるようになるかと思います。

MSIX パッケージプロジェクトの追加

パッケージを作る時、Visual Studio のログインは前回登録した開発者アカウントと同じアカウントでのログインが必要です。

ソリューションエクスプローラーのソリューションを右クリックし、[追加 → 新しいプロジェクト]を選びます。

NewPkgWindows アプリケーションパッケージプロジェクトを選んで追加します。

PkgProjNameウィザードが走ります。プロジェクト名は適当で構いません。

TargetVersionターゲットバージョンは 2004 にしました。こちらによれば、2004 では最新の MSIX アプリ パッケージ形式がさらにサポートされるようになったとのことです。

Izonウィザード完了後、パッケージプロジェクトの依存関係を右クリックしてプロジェクト参照の追加を選びます。

AddRefWPF アプリを追加します。

AddRef2依存関係に WPF アプリが追加されました。

ロゴの作成

パッケージプロジェクトの Images フォルダーの中に、ストア公開時にロゴとなる StoreLogo.png が作成されています。

StoreLogoこのロゴがデフォルトのままだと、後でアプリを検証する際に不合格となるので、適当にロゴ画像を作成して上書きします。推奨サイズは 400x400 ピクセル以上です。

ストア向けのロゴは様々なサイズが必要なので、Visual Studio にリサイズしてもらいます。

VisualShisanパッケージプロジェクトの中にある Package.appxmanifest をダブルクリックし、ビジュアル資産タブを開きます。

ソースとして作成したロゴ画像を指定し、資産はバッジロゴ以外のすべて、スケールはすべてのスケールを選択します。

生成ボタンをクリックすると、サイズ違いのロゴが山のように生成されます。

ロゴについてはこちらが詳しいです。

アプリケーション名の予約(簡単な方法)

(補足)
こちらは Visual Studio から簡単に予約する方法です。
パッケージ名にこだわりたい場合は次節をご覧ください。

AppAndStoreソリューションエクスプローラーのパッケージプロジェクトを右クリックし、[公開 → アプリケーションをストアと関連付ける]を選びます。

SelectAppNameアプリケーション名を選択の画面の「アプリケーション名を予約」のところに、公開したいアプリの名前を入れます。この名前はストアで表示される名前になります。

SelectAppName2予約した名前を選択して次へをクリックします。

AppAndStore2関連付けボタンをクリックして関連付けを完了します。

アプリケーション名の予約(パッケージ名にこだわる方法)

前節の方法で予約すると、特にアプリ名が日本語の場合に、製品 ID の「パッケージ/ID/名前」(パッケージ名)がアルファベットの羅列になります。パッケージ名は通常は利用者の目に触れないので、アルファベットの羅列でも全く問題無いと思いますが、Get-AppxPackage コマンドレットでパッケージ名を取得した時などに気になるということであれば、読みやすいパッケージ名を付けることもできます。

NewAppMicrosoft パートナーセンターのホームから「アプリとゲーム」に進み、新しい製品のアプリを選びます。

NameReserve2名前を予約する画面になりますが、必ず半角英数で入力します。そうすることでそのままパッケージ名になります。

例えば、日本語のアプリ名が「ほげふが」なら、「Hoge Fuga」で予約します。

スペース等はパッケージ名から除外されます。また、パッケージ名の先頭には開発者名が付与されます。「DevName.HogeFuga」のようなパッケージ名になります。

予約後、パートナーセンターの製品 ID ページでパッケージ名(パッケージ/ID/名前)を確認できるので、意図しているパッケージ名になっているか確認します。

NameReserve3しかしこのままでは、アプリ名が英数になってしまいます。日本語のアプリ名を付けたい場合は、アプリ名の管理ページで、追加で日本語名も予約します。新規アプリとして予約するのではなく、Hoge Fuga のその他の名前として予約します。

AppName再度アプリ名の管理ページを開くと、ページ下方に「Hoge Fuga」と「ほげふが」が併記されています。日本語のほうをダッシュボード名に設定しておくと分かりやすくなります。

これで、アプリ名「ほげふが」のパッケージ名が「DevName.HogeFuga」となり、読みやすいパッケージ名になります。

その後、前節と同様のやり方で、Visual Studio でアプリケーションをストアと関連付けますが、アプリケーション名選択の画面では、新しいアプリケーション名を予約するのではなく、先ほど予約した「Hoge Fuga」を使用します。Visual Studio からは常に最初に予約した英数名で表記されますが、ストアで公開する際は日本語名も選べます。

MSIX パッケージ作成

CreatePkgソリューションエクスプローラーのパッケージプロジェクトを右クリックし、[公開 → アプリパッケージの作成]を選びます。

HaifuHouhou「新しいアプリ名で Microsoft Store に」を選びます(先ほど予約したアプリ名が表示されている場合はそちらを選びます)。

SelectAppName3先ほど予約した名前を選択して次へ進みます。

HaifuHouhou2予約した名前で Microsoft Store に配布する方法を選択します。

Kouseiパッケージの選択と構成で、最低 1 つ以上のアーキテクチャを選びます。

今回は x64 にしました。

PkgDone作成ボタンをクリックしてしばらく待つと、パッケージが作成されます。

パッケージの検証

作成したパッケージに問題がないか、Windows アプリ認定キット(Windows App Certification Kit:WACK)で検証します。

WACKパッケージ作成完了画面のボタンから WACK を起動します。

WACK2次へボタンをクリックすると検証が行われます。しばらく時間がかかります。

WACK3結果が合格となれば問題ありません。

不合格の場合は、詳細を確認して原因を取り除きます。

作成したストア向けパッケージを、ストアに登録される前に動作確認したい場合、やり方はこちらにまとめてあります。

WPF アプリを Microsoft Store に申請・登録する(全 4 回)

  1. 開発者登録編
  2. アプリ作成編(←今ここ)
  3. ストア配布編
  4. アプリ更新編

WPF アプリを Microsoft Store に申請・登録する(開発者登録編)

Microsoft Store(旧 Windows ストア)で UWP 以外のアプリ(以降「非 UWP アプリ」)も公開できるようになったので、WPF(Win32)で作成したアプリを公開してみました。その時のやり方を整理し、以下にまとめました。画像はクリックすると拡大します。

ちなみに、Electron アプリについては ElectronアプリのMicrosoft Storeアプリ申請という記事が既にあります。

開発者登録編 目次


メモ:なぜ Microsoft Store?

HP今まで私が作ったアプリは私のホームページで公開してきたのですが、Microsoft Store(以降、ストア)に興味を持ったのは、ユーザーから見るとアプリの扱いが簡単になるかもしれない、と思ったからです。

私のアプリをユーザーが利用する場合、zip をダウンロードして解凍すればいいだけなので、もともと難しくはありません。しかし、どのフォルダーに解凍しようかちょっと迷ったり、あるいはうっかり Program Files に解凍してしまうと Virtual Store が悪さをする可能性も否定できません。

また、そのようなフリーソフトが 1 つ 2 つならまだしも、たくさんあると Windows 再インストールの時などは大変です。ストアでアカウントに紐付いていると、まとめて再インストールできるので便利です(スマホと同様の動き)。

それから、WPF アプリは現状ならまだなんとか 1 つの exe ファイルにまとめられますが、Windows App SDK(WinUI3)あたりになってくるとまとめられなくなってきそうな感じがして、ファイルの多さにユーザーが辟易してしまうかも、というのも気がかりです。

初回起動時、SmartScreen による邪魔が入るのも鬱陶しいところです。

ストアでの配布であれば、ユーザーは「入手」ボタンをクリックするだけで良く、スタートメニューにも自動的に登録されたりして、迷わず簡単です。また、あまりして欲しくはありませんが、アンインストールもボタン 1 つで簡単です……。

メリット・デメリットの詳細な検討はこちら

必要なもの

ストアでのアプリ配布に必要なものは、
  • 2,000 円程度の費用(初回のみ)
  • (自前配布と比較して)少し多めの労力
です。

ストアでのアプリを配布にあたり、Microsoft パートナーとしての開発者登録が必要で、登録料がかかります。あくまでも「開発者」としての登録料であり、アプリごとの登録料はかかりません。維持費も無料です(2021 年 12 月時点)。

懸念事項のトップクラスに挙げられる証明書問題については、ストア側で良きに取り計らってくれるので、開発者側での対応は不要です(オレオレ証明書を作る必要はありません)。

労力については、やはり今までよりは多少かかります。アプリ自体に手を加える必要もありますし、ストア申請時にはいろいろな項目に入力しなければなりません。申請すればすぐに配布に至るというわけではなく、審査があって不合格になると修正を余儀なくされます。

パートナー登録

SignUp何はともあれ、パートナー登録しないことには始まらないので、パートナー登録を進めていきます。

ストアウェブサイトの下の方にある「Microsoft 開発者プログラム」リンクから登録ができます。

既に持っている、あるいは新規に作成した Microsoft アカウントでログインすると Microsoft パートナーセンターに遷移するので、開発者として登録します。

Accountアカウント情報ページでアカウントの種類を選びます。個人であれば「個別」を選べば登録料は 2,000 円程度ですし、また、登録作業はオンラインですぐに終わります。法人の場合は登録料も高く、また、登録時に書類が必要というウワサです。

ちなみに公式には個別の登録料は「約 19 米国ドル」と表記されています。リアルタイムの為替だと 1 ドル 113 円程で、19 ドルだと 2,147 円程度になるのですが、表示された金額は税込 1,847 円でした(後日カード明細に記載された金額も 1,847 円でした)。

ここで登録する「パブリッシャーの表示名」がストアで表示される開発者名です。

Contactその他に住所や電話番号なども入れる必要がありますが、こちらはどうやらストアで晒されるというわけではないようです(もし晒されるということでしたら教えてください)。

Pay2入力を終え、次へボタンをクリックすると、支払いページになります。

課金方法(実質クレジット払いのみ)を選び、クレジットカード情報を保存します。

なぜか再び住所の入力を求められます。アメリカ仕様なのでしょうか。

Confirm最後の確認ページで完了ボタンを押すと、支払いと登録が完了します。

WPF アプリを Microsoft Store に申請・登録する(全 4 回)

  1. 開発者登録編(←今ここ)
  2. アプリ作成編
  3. ストア配布編
  4. アプリ更新編

XAML を動的に読み込むサンプルプログラム 【C#/WPF/MVVM】

Runプログラム実行時に XAML を読み込んで UI コントロールを生成する方法のまとめ。

「タブを増やす」ボタンをクリックするたびにタブが増えていく。

サンプルプログラムは GitHub にて:
Visual Studio 2022 / .NET 6 で動作確認。

大まかな手順

STEP 1:XAML 作成

UserControl動的に生成したいコントロール(DynamicTabItem.xaml)をユーザーコントロールとして作成する。

以下の 3 つの作業を行う。

[A]
UmekomiDynamicTabItem.xaml ファイルのプロパティーでビルドアクションを「埋め込みリソース」にする。

[B]
csDynamicTabItem.xaml に紐付いている DynamicTabItem.xaml.cs を削除する。

[C]
xClassDynamicTabItem.xaml の先頭に記述されている x:Class の記述を削除する。

STEP 2:動的読み込み

リソースから DynamicTabItem.xaml を読み込み、XamlReader でコントロール化する。ButtonAddTabItemClicked() 参照。

注意点

動的に読み込む XAML 内の名前空間は、アセンブリを指定する必要がある。指定が無いと XamlReader が例外を吐く。

例えば、ビューモデルの名前空間は
xmlns:vm="clr-namespace:TestXamlReader.ViewModels"
ではなく
xmlns:vm="clr-namespace:TestXamlReader.ViewModels;assembly=TestXamlReader"
とする。

(補足)
別のプロジェクトで、assembly を指定するとビルド時に MC3074 エラー「タグ 'XXX' は、XML 名前空間 'clr-namespace:YYY;assembly=ZZZ' にありません。」が発生したことがあった。
対処方法が分からず、xaml 上では assembly を指定せず、実行時に文字列置換で assembly 指定を追加することで凌いだことがある。

サードパーティーの名前空間を使いたい時も同様で、サンプルプログラムでは GongSolutions.WPF.DragDrop の添付プロパティーを使っているが、
xmlns:dd="urn:gong-wpf-dragdrop"
ではなく
xmlns:dd="clr-namespace:GongSolutions.Wpf.DragDrop;assembly=GongSolutions.Wpf.DragDrop"
とする。


async / await の実行タイミングを追う 【C#】

C# のお手軽非同期プログラミング、async / await。

概念なり捉え方なりについては Taskを極めろ!async/await完全攻略を初めとした親切丁寧な記事があって、ふむふむなんて思っていたのだが、たまに、async / await してるのに妙に UI がフリーズしてないか、みたいな時があった。

そこで、async / await の実行タイミングを追うためのテストプログラムを作ってみた。
async な関数の中でさらに async な関数を呼びだして、それをいくつかのパターンでやってみた結果、たぶん実行タイミングのイメージは以下のような感じなのかなと。
Matome


SomeAsync() の中では Task.Run() しているわけではないので、直ちに非同期処理になるわけではなく、Code P~R は順次実行される。ここが重いと UI がフリーズする。

await に到達した段階で、それ以降 Code U までをタスクとして返す。仮に AnotherAsync() の中で Task.Run() していると、その部分は非同期に処理されるので、重くても UI はフリーズしない(テストプログラムでは Web から文字列を読み込む部分が非同期に処理される)。

メインルーチンに注目すると、よくあるのは
await SomeAsync();
で直ちに await するパターンだが、上図のように await を後ろまで引っ張ると、await まではすぐに実行される。await に到達した段階で、Code U までのタスクが終了するのを待つ。

~Async() の中ではひたすら Task.Run() すれば UI はフリーズしないが、他の async を呼ぶために async にしている場合は、その他のコードが重くないか考える必要がある。

ちなみに Code S~U が重い場合は、メインルーチンで await task している時に UI がフリーズする。

……っていう感じで合っているだろうか?

ActualWidth のバインド&取得と、ビヘイビアの紐解き 【C#/WPF/MVVM】

Windowビヘイビアについて少し考えてみた。

サンプルプログラムは GitHub に置いてある。Livet 使用。

ActualWidth はバインドできない

C# WPF MVVM での開発において、FrameworkElement.ActualWidth は XAML からのバインドができない(VM で ActualWidth を取得できない)。

例えば Window.Title なら「Title="{Binding Hoge}"」のようにバインドできるが、「ActualWidth="{Binding Hoge}"」はエラーとなる。Title も ActualWidth も依存プロパティー(依存関係プロパティー)だが、ActualWidth は読み取り専用のため、「Setter が無い」旨のエラーとなる。

書き込みできないだけなら、「ActualWidth="{Binding ActualWidth, Mode=OneWayToSource}"」のような片方向バインドはさせてくれ、と思うのだが、それもできない。標準コントロールの全プロパティーはバインド可能にしておいて欲しいのだが、そんな日は来るのだろうか。

Livet で楽ちん

バインドできないものをバインドさせるには、通常はビヘイビアを自作する。

が、ActualWidth 程度であれば、ビヘイビアを自作するまでもなく Livet の XXXXSetStateToSourceAction を使うのが簡単。サンプルプログラムでは、同類の ActualHeight について、Livet の WindowSetStateToSourceAction を使用してバインドすることにより、VM で ActualHeight が取得できるようになっている。
<behaviors:Interaction.Triggers>
    <behaviors:EventTrigger EventName="SizeChanged">
        <l:WindowSetStateToSourceAction Source="{Binding ActualHeight, Mode=TwoWay}" Property="ActualHeight" />
    </behaviors:EventTrigger>
</behaviors:Interaction.Triggers>

今回はビヘイビアについて少し考えてみるのが目的なので、ActualWidth についてビヘイビアを自作する。

ビヘイビアで ActualWidth をバインド可能にする

ビヘイビアの作り方については「ビヘイビア WPF」あたりでググればたくさん出てくるが、コードの内容についての解説があまり見当たらないので、少し考えてみた(コード全体については冒頭のサンプルプログラム参照)。

XAML からバインド可能にするには依存プロパティーを作る必要があり、それが
public static readonly DependencyProperty ActualWidthProperty =
        DependencyProperty.Register(nameof(ActualWidth), typeof(Double), typeof(WindowBindingSupportBehavior),
        new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
の部分。そして、依存プロパティーとリンクする普通のプロパティーが
public Double ActualWidth
{
    get => (Double)GetValue(ActualWidthProperty);
    set => SetValue(ActualWidthProperty, value);
}
の部分。

依存プロパティーの作成と登録を行うのが DependencyProperty.Register() で、その第一引数 nameof(ActualWidth) がプロパティー名。依存プロパティー名 = プロパティー名 + "Property" となっている必要がある。なっていないと、実行時にエラーになる。ビルド時にエラーにしてほしいところだ。

多くのサイトで「DependencyProperty.Register("ActualWidth"...」のようにプロパティー名を即値で書いているが、「nameof(ActualWidth)」と書く方が多少安全になるかと思う。

DependencyProperty.Register() の第二引数で変数の型、第三引数でビヘイビアクラスを指定する。第四引数の FrameworkPropertyMetadata 内でプロパティーのデフォルト値を指定する。

依存プロパティー登録により ActualWidth のバインドが可能となったので、次は、ActualWidth の値を更新して有用な値にするところ。
ウィンドウのサイズが変更された際に ActualWidth を変化させれば、常に ActualWidth を最新の値に設定できる。

OnAttached() で SizeChanged イベントハンドラー(ControlSizeChanged)を登録し、そのイベントハンドラー内で ActualWidth を更新する。

以上で、ActualWidth をバインドして使えるようになる。

サンプルプログラムを起動してウィンドウの横幅を変化させると、ActualWidth の値も変化する。

ビヘイビアで IsActive をバインド可能にする

Window.IsActive もバインドできないが、これもバインド可能にしてみる。

IsActive は本来読み取り専用だが、ここでは書き込みも可能にして、true にされたらウィンドウをアクティブ化する、ということをしてみる。

基本的なやり方は ActualWidth の時と同じだが、ViewModel から書き込まれた時に処理を行うために、DependencyProperty.Register() で、書き込まれた時のイベントハンドラー SourceIsActiveChanged を指定している。

private static void SourceIsActiveChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    if ((obj is not WindowBindingSupportBehavior thisObject) || thisObject.AssociatedObject == null)
    {
        return;
    }

    if ((Boolean)args.NewValue)
    {
        thisObject.AssociatedObject.Activate();
    }
}

イベントハンドラーの引数 obj にビヘイビアクラスが DependencyObject 型として格納されているので、WindowBindingSupportBehavior 型にキャストして thisObject とする。

thisObject.AssociatedObject がウィンドウ自体なので、これでウィンドウに対する操作がやりたい放題できる。Activate() を呼べばウィンドウがアクティブ化される。

サンプルプログラムでは、チェックボックスでウィンドウのアクティブ状態を表示する。他のウィンドウをアクティブにすると、チェックが外される。

「5 秒後にアクティブ」ボタンをクリックすると、5 秒後にサンプルプログラムのウィンドウをアクティブ化する。ボタンクリック後、他のウィンドウをアクティブにして 5 秒待つと、サンプルプログラムがアクティブになる。


.NET Core 3 系 → .NET 5 でのプロジェクト設定変更点まとめ

.NET 5 を導入して最初の一歩で躓いたところを整理しておく。Visual Studio 2019 16.8.1。

対象のフレームワーク

Framework新しいプロジェクトの作成時、「WPF App (.NET)」を選択しても、対象のフレームワークは .NET Core 3.1 となっており、そもそも .NET 5 になっていない。

[プロジェクト→プロパティ]メニューで対象のフレームワークを .NET 5.0 にする必要がある。

NETSDK1137 警告

.NET 5 でアプリケーションをビルドすると、
NETSDK1137 Microsoft.NET.Sdk.WindowsDesktop SDK を使用する必要はなくなりました。
ルート プロジェクト要素の SDK 属性を 'Microsoft.NET.Sdk' に変更することをご検討ください。
という警告が発生する。

警告で指定されるファイルは Program Files 配下のファイルだが、Program Files 配下のファイルを変更すると影響が大きそうなので、プロジェクトファイル(.csproj)を変更することで対応可能。

CsProjソリューションエクスプローラーでプロジェクト右クリック、[プロジェクトファイルの編集]メニュー、または、ソリューションエクスプローラーでプロジェクトダブルクリックで csproj を開く。

CsProjEdit1 行目の
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

<Project Sdk="Microsoft.NET.Sdk">
に変更すると、警告が表示されなくなる。

単一ファイルの作成

TanitsuSeiseiプロジェクト発行時に「単一ファイルの作成」を有効にしても単一のファイルにならない。

Files10 個ほどのファイルが作成されてしまう。

こちらもプロジェクトファイル(csproj)を編集することで対応可能。

プロジェクトファイルの <PropertyGroup> のところに
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
CsProjEditを追記することで、.NET 3 系と同様の具合の単一ファイルが作成される。

ついでに
<DebugType>embedded</DebugType>
1も追記すると pdb も単一になる。



C# 8.0 の null 安全はいろんな意味で革命だった

null 安全とは

「イマドキのプログラミングは null 安全がトレンドらしい」(参考記事:null 安全でない言語は、もはやレガシー言語だ)ということで、null 安全初心者が null 安全なコードを書いてみた。

そもそも null 安全とは、「null 参照例外(NullReferenceException)が発生しえるコードをコンパイル時にエラーにしてくれる」仕組み。つまり、コンパイルが通った時点で null 参照例外は起こりえないということになる。革命的に素晴らしいのでは?

C# においては、最新の言語バージョン 8.0 で「null 許容参照型」が導入され、これを使うと null 安全になる。

「null 許容参照型」という名前を聞いて、「参照型なんだから null が許容されるのは当たり前じゃないか」と思ったのだが、その常識が覆る。null 許容参照型を導入することにより、「普通の参照型には null を代入できなくなる」。つまり、C# という言語レベルで過去との互換性が失われる! 革命的に影響が大きい。

あまりにも影響が大きいので C# 8.0 では null 許容参照型はオプション扱いとなり、かなり限定的な環境でないと有効にならない。Visual Studio 16.3 の時点では、プラットフォームを .NET Core にしたうえで(.NET Framework でも一部機能は使えるらしい)、プロジェクトファイルを直接編集して null 許容参照型(Nullable)を有効にして、かつ、関連する警告をエラー扱いにする(参考記事:null 許容参照型)。

コード例

今までの C# のコード
String str = null;
String low = str.ToLower();
があったとして、ここまでシンプルであれば NullReferenceException を予見してコードを修正できるが、str の初期化がどこでされてるか分かりづらい場合は見逃してしまって NullReferenceException が発生するという事態に陥る。

しかし、null 許容参照型を有効にすると「普通の参照型には null を代入できなくなる」ので、str = null の時点でコンパイルエラーとなる。従って、
String str = "HOGE";
String low = str.ToLower();
のようなコードに修正せざるを得なくなり、NullReferenceException が発生しない安全なコードになる。これが null 安全か!

とはいえ、「str が null になることもある」という場合に、null 許容参照型を使う。型名の後ろに「?」を付けるだけだ(null 許容値型と使い勝手は似ている)。
String? str = null;
String? low = str?.ToLower();
str?.ToLower() の部分は、str が null であれば null が返り、null でない場合のみ ToLower() が実行されるので、NullReferenceException が発生しない。str が null の時は low も null になるので、low も null 許容参照型にする必要がある。? を忘れるコンパイルエラーになるので、やはり NullReferenceException は発生しない。null 安全すごい。

いくつかコードを書いてみた感想

null 許容参照型を有効にすると、変数が null になり得るのか否かを常に意識するようになる(というか、不適切なコードはコンパイルエラーになるので意識せざるを得ない)。

特にクラスメンバーを null 許容にしなかった場合は、コンストラクターでの初期化が必要となるので、「null 許容があるべき姿なのかどうか」をしっかり考えて選択するようになった。

関数の書き方も変わってくる。

Boolean TryParse (String s, out Hoge? result) のような関数は、返値が true の場合は result は null にはならず、それをコンパイラに教えるためには [NotNullWhen(true)] のように属性を指定する。が、そんな面倒なことをするくらいなら Hoge? TryParse(String s) のような形式に変更するほうがいいと思う。

また、型キャストの際に as 演算子を使うとキャストできなかった場合に null が返ってきてしまうので、if 文の中で is 演算子を使うスタイルに変わってきた。
private void Func(Object? sender)
{
    if (sender is UIElement element)
    {
        element.AllowDrop = true;
    }
}
sender が null ではなく、かつキャストできる場合のみ element に代入されるため、element は null 非許容となり、element のプロパティーにアクセスする際に ? は不要。

一方で、NullReferenceException にならない安全な世界ではあるものの、使い方を間違えるとエラーが埋もれるだけのより深刻な状況にもなりかねない。

呼ばれること自体に意味があるような関数、例えばユーザーへの警告表示で、null 安全ではない世界であれば、
hoge.ShowWarning();
で hoge が null の場合は例外が発生してマズいことになったのが表面化するが、null 安全な世界だと
hoge?.ShowWarning();
のようなコードとなり、例外は発生しないが、ユーザーに警告が表示されず、しかも表示されないままスルーされて問題が埋もれてしまう。

革命は 1 日にして成らず

null 安全は C# にとってあまりにも影響が大きく、ライブラリやコンパイラの対応もまだ発展途上のようだ。

例えば、メンバー変数をコンストラクターで初期化する際、コンストラクターから関数を呼びだしてそこで初期化していても、初期化していないと判定されてしまう。

また、
String[] strs = new String[3];
はコンパイルが通ってしまうが、実際には String[0] などは null であり、本来であればコンパイルエラーにしなければならないコードだ。

環境が整うまでにはまだまだ時間がかかりそうである。

とはいえ、null 安全は非常に素晴らしい仕組みだと思うので、少なくとも今後新規作成するプロジェクトでは積極的に使っていきたいと思う。

気になったこと

マルチスレッドで null 許容参照型にアクセスするとどうなるんだろうか。

想像だが、str?.ToLower() のようなコードはコンパイル時に

if (str != null)
{
    str.ToLower();
}
のように解釈されるのではないだろうか。だとすると、str が null ではないと判定した直後に別のスレッドが str を null にしたら、ToLower() 呼出の際に NullReferenceException が発生してしまわないだろうか。

ところで、Nullable の読み方って「ぬらぶる」?

WPF で動画に文字や図形を合成して再生する方法

C# 動画プログラミングの 1 つです。

WPF の MediaElement や MediaPlayer で動画を再生する方法を紹介したページはいくつか見たのですが、通常再生のみが紹介されており、加工しながらの再生について言及されているページが見当たらなかったので、ここにまとめてみます。

目次


やりたいこと

overlay2動画再生時に、再生している動画の映像に、テキスト等を重ねます。

例として、映像の左上に灰色の長方形を描き、フレームレートや動画再生位置(秒数)といった文字を重ねてみます。

サンプルコード

GitHub にアップしてありますので、そちらからダウンロードしてください。

動作環境については GitHub リポジトリ内のヘルプファイルを参照して下さい。

実装の基本方針

WPF には、(動画に限らず)表示の更新を行うタイミング(つまりフレームごと)で発生するイベントがあり、それが CompositionTarget.Rendering イベントです。

このイベント内で再生中の動画をキャプチャした上で、テキスト等を書き込んでいく、というのが基本的な考え方です。

プログラムの大まかな動作としては、ドラッグ&ドロップされた動画を再生(もちろん音声も)しつつ、上記の方法で映像にテキスト等を重ねています。

プログラムの解説

MainWindow.xaml.cs の解説です。

ファイルをドロップされた際に発生するイベント Window_Drop() において、動画の再生 Play() と、重ね合わせの準備 PrepareOverlay() を行っています。

Play() は単純に、ドロップされたファイルを MediaPlayer で再生しているだけです。

今回の 1 つのポイントは PrepareOverlay() で、ここでフレーム描画イベントハンドラー CompositionTarget.Rendering の設定を行っています。

ここで設定されたイベントハンドラーは WPF での描画が必要なタイミングで常に呼びだされるため、仮に動画が再生されていなくても定期的に呼びだされます。呼びだされる頻度は環境によって異なるとのことで、私の環境では通常 60 fps 程度で呼びだされていますが、100 fps 程度に上がることもあります。あくまでも「WPF 描画のフレームレート」であって、動画のフレームレートではないことに留意してください。

イベントハンドラーとして設定された CompositionTargetRendering() の後半部分で合成を行っています。

DrawingVisual の DrawingContext に対して、動画、図形(背景の灰色四角形)、テキスト(フレームレートや再生位置)を書き込み、最後にそれらを表示用のビットマップ(mBmp)に書き込みます。

ImagemBmp は PrepareOverlay() で Image コントロールのソースに指定されているため、mBmp の内容がユーザーの目に見える形で表示されることになります。

WPF で動画を扱うメリット

WPF で動画を扱うメリットは、なんといっても手軽なことです。

加工しない再生だけなら MediaElement コントロール 1 つで簡単に動画を再生できますし、加工しても今回のサンプルコードのようにわずかなコード量で実現できます。

標準の C# 開発環境のみで実現できるため、追加のサードパーティーライブラリを用意したり、それをユーザーにインストールしてもらう(または一緒に配布する)という手間もありません。

GPULoadGPU ハードウェア支援が使える場合は、自動的に活用してくれているようです。

また、詳しくは未検証ですが、インストールされているコーデックライブラリ(K-Lite 等)も WPF 側で自動的に利用してくれているようで、扱える動画の形式も無限に広がっていきます。

WPF で動画を扱うデメリット

WPF で動画を扱うデメリットは、(動画の)フレームを厳密に扱えないのではないかということです(扱える方法をご存じの方は教えてください)。

まず、根本的な課題として、動画のフレームレートが取得できません。動画の縦横サイズ等はプロパティーで取得できるのですが、フレームレートを取得するプロパティーはありません。

また、WPF で動きのあるものを処理する際はアニメーションという仕組み(Animatable クラスないしは IAnimatable インターフェース)に則っており、動画(MediaElement / MediaPlayer)も例外ではありませんが、動きの管理が時間単位で行われています。

何らかの方法で動画のフレームレートを取得できたとしても、厳密な意味でのフレーム送りはできず、1 フレームは○○ミリ秒だから○○ミリ秒進める、というような形でのフレーム送りになるのではないかと思います(この辺りは未検証ですが)。30 fps だと 1 フレーム当たり 33.3333.... ミリ秒ですが、小数点の丸めによって微妙に進まない(または 2 フレーム進んでしまう)瞬間が発生したり、そもそも可変フレームレートだとどうしようもないのではないかと思います。

スクリーンショットの動画

このページ冒頭のスクリーンショットに映っている動画は、ダブルレンさんの「お願い☆EternalSummer!」という曲です。素敵な曲なので、是非聴いてみて下さい!


歌いたい時はニコカラバージョンでどうぞ。








フォームと WPF でコードメトリックスを計算・比較してみた

MVVM
同等の機能を持つソフトでフレームワークが異なる場合、Visual Studio 2017 で計算できるコードメトリックスはどのくらい変わるのか、というのが気になったので、あまり意味はないけど測定してみた。

対象プロジェクトはゆかりすたー。バージョン間で細かな違いはもちろんあるものの、大まかに見れば機能的にはほとんど同等。結果は以下の通り。

フォーム
ゆかりすたー
Ver 7.51
WPF / コードビハインド
ゆかりすたー METEOR
Ver 1.50
WPF / MVVM
ゆかりすたー METEOR
Ver 2.02
保守容易性指数828486
サイクロマティック複雑度388839334480
継承の深さ799
クラス結合431466465
コード行1249896859333

保守容易性指数

公式解説によれば保守容易性指数の値が大きいほど保守性が高く、0~9 がダメ、10~19 がまぁまぁ、20~100 が OK とのこと。

いずれのフレームワークでも 80 以上なのでかなり良好と言えると思うが、WPF のほうが少し高い数値になっている。

ただ気になるのは、WPF の XAML 部分はあまり考慮されていないのではないかということ。WPF / MVVM で試しにメインウィンドウの XAML からコントロールをさくっと削り、メインウィンドウ部分の評価を比較したのが以下。

元の WPF / MVVMコントロール削除
保守容易性指数91
91
サイクロマティック複雑度11
継承の深さ99
クラス結合72
コード行22

保守容易性指数(というかクラス結合以外)に変化がない。XAML に複雑な表示ロジックをべた書きして、代わりに ViewModel からコードを減らしたら、保守容易性指数はあがってしまうのではないか。ちょっとインチキ臭い。XAML を考慮に入れると、フォームと WPF で保守容易性はほとんど変わらないと言えるかもしれない。

さて、総合的には保守容易性指数が 86 の WPF / MVVM でも、細かく見ていくと値が低い部分もある。最低は 0 で、ViewTFoundsWindowViewModel.cs の DataGridListSorting()。ソート基準項目によってひたすら switch するので低い評価になっている。

次に低いのが CsvOutputWriter.cs の Output() で 28。こちらも出力項目ごとに switch していて、switch の case が並ぶのは低い評価になりやすいのかもしれない。

サイクロマティック複雑度

Wikipedia を読むと、サイクロマティック複雑度(循環的複雑度)はプログラム実行時の経路数に関係する値で、大きいほど経路・分岐が多くなる。プログラムの動作を理解するのが大変になったり、テストケースが多くなって大変になったりするのだろう。

プログラムの規模が大きくなるにつれてサイクロマティック複雑度も大きくなっていく性質があるので、単純に数値だけを見て良い悪いは言えないが、相対的に WPF よりフォームの方が値が小さいので、フォームの方が理解しやすいという見方ができるのではないか。体感としても、フォームの方が簡単だと思う。

継承の深さ

使用しているクラスが、根源となるクラスである Object から何段階派生しているか。WPF のほうがクラス階層が深いが、最大の 9 となっているクラスはすべてウィンドウだった。WPF でウィンドウを使うと必ず 9 以上になるということからすれば、結果が 9 であるというのは少ない値と言えるだろう。

クラス結合

低い方が良い数値。フォームの方が低い。

コード行

ソースコードの行数ではなく、IL の数。ソースコードのコメント行や空行は数値に影響しないと思われるため、単純にソースコードの行数を数えるよりも実際的。

ソースコード行数より少ない数値となる傾向があり、例えば 最大値を記録した SyncClient はソースコード上は 2058 行だが、IL では 882 行と、半分以下になっている。

フレームワーク間で比較すると、WPF のほうがずっと少なくなっている。フレームワークに関係ないところで最適化してるところが(ソースコード行数ベースで)1000 行ほどあるが、それを差し引いても WPF のほうが少ない。

ただし、WPF は使用しているライブラリが多く、それらまで含めると、総合的なコード量はフォームのほうが少なくなるのではないだろうか。

まとめ

元々あまり意味のある比較とは思えないものの、傾向としては、フォームの方がシンプル・コンパクトということになるだろう。元々 WPF / MVVM はシンプルさで売ってないし……。


MVVM ダメ Tips

使ってはいけない Tips。コードは Livet 環境前提。

ViewModel から直接 View を参照してしまう

最早 MVVM ではないが、添付ビヘイビアとか作るの面倒くさい時に。
Initialize2() の引数に Window オブジェクトが渡される。

XAML 側コード

<i:EventTrigger EventName="ContentRendered">
    <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
    <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize2" MethodParameter="{Binding ElementName=MyWindow}"/>
</i:EventTrigger>

ViewModel 側コード

public void Initialize2(Object oParam)
{
    Debug.WriteLine("Initialize2() param type: " + oParam?.GetType().ToString());
}


LINQ to SQLite で共通カラム部分をジェネリックで運用する

LINQ to SQLite を使っている中で、複数のテーブルに共通するカラム(列)がある場合、ヘルパー関数などをそれぞれのテーブルごとに手書きするのは大変だしメンテナンス性も悪い。ひとまとめにできないだろうか、というのを試行錯誤した結果、インターフェース+ジェネリックでいけることが分かったので整理しておく。

なお、LINQ to SQLite の基本的な事柄については「C# で SQLite を便利に使うサンプルコード(LINQ to SQLite)【改訂版】」を参照。

テーブル構造

フルーツの一覧を格納するフルーツテーブルと、肉の一覧を格納する肉テーブルがあるとする。フルーツテーブルと肉テーブルそれぞれのカラムのうち、ID と名前については両者共にカラムがあるものとする。

この場合、テーブル構造を定義するクラスは以下のようにする。

まず、共通カラム部分をインターフェースとしてまとめる。
public interface IFoodData
{
    // ID
    Int32 Id { get; set; }

    // 名前
    String Name { get; set; }
}

そのうえで、テーブル構造を定義するクラスにインターフェースを実装するようにする。例えばフルーツテーブルなら
[Table(Name = "t_fruit")]
public class TFruitData : IFoodData
{
    // --------------------------------------------------------------------
    // IFoodData 実装
    // --------------------------------------------------------------------

    // ID
    [Column(Name = "fruit_id", DbType = LinqUtils.DB_TYPE_INT32, CanBeNull = false, IsPrimaryKey = true)]
    public Int32 Id { get; set; }

    // 名前
    [Column(Name = "fruit_name", DbType = LinqUtils.DB_TYPE_STRING, CanBeNull = false)]
    public String Name { get; set; }

    // --------------------------------------------------------------------
    // TFruitData 独自項目
    // --------------------------------------------------------------------

    // 色
    [Column(Name = "fruit_color", DbType = LinqUtils.DB_TYPE_STRING, CanBeNull = true)]
    public String Color { get; set; }
}

のようにする。

共通カラムの操作

フルーツテーブルと肉テーブルに共通するカラム(ID、名前)について、操作をジェネリックでまとめることができる。例えば、名前でレコードを検索・表示する関数は以下のようになる。
private void QueryFoodByName<T>(String oKeyword) where T : class, IFoodData
{
    Console.WriteLine(LinqUtils.TableName(typeof(T)) + " 内で名前に「" + oKeyword + "」を含むレコードを検索");
    using (SQLiteConnection aConnection = CreateDatabaseConnection(DB_NAME_GENERIC))
    using (DataContext aContext = new DataContext(aConnection))
    {
        Table<T> aTableTest = aContext.GetTable<T>();
        IQueryable<T> aQueryResult =
                from x in aTableTest
                where x.Name.Contains(oKeyword)
                select x;
        Console.WriteLine("検索結果:" + aQueryResult.Count() + " 件");
        foreach (T aRecord in aQueryResult)
        {
            Console.WriteLine(aRecord.Name);
        }
    }
}

関数を呼びだす際は、
QueryFoodByName<TFruitData>("hoge");
QueryFoodByName<TMeatData>("fuga");

のようにする。

以上により、テーブル名が異なる複数のテーブルに共通するカラムの操作を、1 つのコードにまとめることができた。

継承でやってはどうか?

テーブル構造を定義するクラスを、インターフェースではなく継承で作るのはどうだろうか。

つまり、IFoodData をインターフェースではなく通常のクラスとして宣言してそれを基底クラスとし、TFruitData や TMeatData をそこから派生させる形にする。

しかし残念ながらそれはうまくいかなかった。

データベース内にテーブルを作成する段階で、「型 'TestLinqToSqlite.IFoodData' のデータ メンバー 'Int32 Id' は型 'TFruitData' のマッピングの一部ではありません。メンバーは継承階層のルートより上のメンバーですか?」というようなエラーが発生してしまう。

サンプルコード

サンプルコードは GitHub にて。

サンプルコードアプリの使い方

  • TestLinqToSqlite_Genericジェネリックの「DB 作成」ボタンをクリックすると、フルーツテーブルと肉テーブルを持つデータベースが作成される。
  • 「検索」ボタンをクリックすると、フルーツテーブルと肉テーブルそれぞれから検索が行われ、結果がコンソールに表示される。なお、検索はジェネリックを使用した 1 つのコードで行われている。
なお、サンプルコードアプリには基本操作の欄もあるが、こちらについては「C# で SQLite を便利に使うサンプルコード(LINQ to SQLite)【改訂版】」を参照。

参考資料

更新履歴

  • 2019/04/09 初版。
  • 2022/01/03 参考資料のリンクを改訂版のものに更新。


















フォームから WPF に移行して分かったメリット・デメリット

.NET Framework(C#)での開発のベースとなるフォーム(Windows Forms)と WPF(Windows Presentation Foundation)。後発の WPF の方が優れていてしかるべきなのに、登場からずいぶん経ったわりにはイマイチ普及が進んでないようにも見受けられる。使いづらいの?

というわけで、実際に 1 つのプロジェクト(ゆかりすたー METEOR)をフォームから WPF に移行したり、色々実験したりして分かった、フォームと WPF それぞれの良いところを整理してみた。

ゆかりすたー METEOR のフォームと WPF でのソースコード上の差分は GitHub の diff で確認できる。

WPF のほうが良いところ

フォームのほうが良いところ

まとめ

UI コントロールのフロー配置

まずは WPF の良いところから。

WPF では、ウィンドウにボタンなどのコントロールを配置する際の考え方が根本的にフォームとは違う。フォームではマウスでポチポチコントロールを置いていく(手動で位置決めをする)が、WPF は「流し込む」ようなイメージに近い。

Buttons例えばボタンを 3 つ配置する場合、右の図のように、1 つ目のボタンはウィンドウの上に自動的に吸い付く(もちろんマージン(隙間)は自由に設定できる)。2 つ目のボタンは 1 つ目のボタンに吸い付き、3 つ目のボタンは 2 つ目のボタンに吸い付く。

ボタンのサイズは自動調整される。ラベルの長さに合わせて良い感じのサイズになるのだが、そのままだと 2 つ目のボタンだけ横幅が広くて格好悪い。しかし、2 つ目のボタンに合わせて、1 つ目と 3 つ目のボタンの横幅が自動的に広がってくれるので、良い感じのレイアウトになる。

例えば多言語対応したとして、日本語だと 2 が幅広だけど英語だと 1 が幅広、なんて場合でも、言語に関わらず 3 つともサイズが揃ってくれるので便利。WPF は高 DPI 環境でもレイアウトが崩れないと言われるのも、この自動調整機能をうまく使った場合だと思う。

この考え方は非常に良いと感じた。……考え方「は」。

現実はかなり不便。WPF もフォーム同様ビジュアル開発ができるのだが、マウスでポチポチコントロールを置いても、上記は実現されない。XML(風の XAML)を手打ちしてコントロールを作っていかなければならない。ビジュアル部分はプレビュー用途で使うのが関の山。しかもエディタが重い。

グリッドへの配置をしようものなら、グリッドの何マス目なのかを地道に手打ちしていくという地獄のような作業が待っている。

タブコントロールでは、アクティブなタブページに合わせてタブコントロールのサイズが変わるので、ページを切り替えるごとにウィンドウサイズも変わるという妙な動きになってしまう。ウィンドウサイズ固定にもできるが、そうすると自動調整の恩恵を受けられず、Windows 10 では良い感じのレイアウトなのに Windows 7 だとコントロールがはみ出したりする、なんていう事態になる。

また、考え方についても、CSS でいう Flexbox に近いのではないかと思うが、Flexbox ほどオプションが豊富でもなく(均等割り付けとかはできない)、発展途上の印象がある(しかも発展が止まってる)。

テーマで見た目を変えられる

ある意味一番分かりやすいメリットがこれ。

Placeデフォルトではフォームと見た目はほとんど同じだが、例えば MaterialDesignInXamlToolkit を使えば、今風のマテリアルデザインになる。テキストボックスにプレースホルダー(入力欄に事前に表示する灰色のヒント)を入れたりとか、フォーカスが当たった際の境界線のアニメーションとか、お手軽に実現できる。

データと UI の分離と関連付け

リストボックスやコンボボックスのような選択系のコントロールにおいて、フォームではリストのアイテムを 1 つ 1 つ追加していき、リストボックスそのものがデータの塊のようになっていた。

一方で WPF では、バックデータとなるデータ群(例えば List<String>)を予め用意しておき、リストボックスに丸投げすると、全データを一気に投入できる。さらに、List<String> の内容を変更(追加・削除・入れ替え等)すると、それがちゃんとリストボックスに反映される(Items.Refresh() 関数を呼ぶ)。

データはデータ、UI は UI と分けた上でそれらを関連付けることで、考え方がすっきりする。

データグリッドがスマート

前項とも密接に関わるが、データグリッドコントロール(System.Windows.Controls.DataGrid)がフォームの時(System.Windows.Forms.DataGridView)よりもスマートになっている。

フォームの時は、行数が多いと空行を追加するだけでものすごく時間がかかっていた。それを回避するには CellValueNeeded イベントなどを駆使して必要な時にだけデータを表示する仕掛けを組まねばならなかった。

WPF では、全データを投入しておいても、行が無駄に生成されることはなく、CellValueNeeded に相当する作業を自動でやってくれる。また、条件によって行の色を自動的に変えることも可能。

ただし、フォームより大幅に後退した部分もある。代表格が「クリックされたセルの位置が(簡単には)分からない」。フォームの時は DataGridViewCellEventArgs で行列位置を知れたが、WPF ではかなり複雑な手続き(こちらの GetDataGridCellPosition() 参照)を経ないと知ることができず、まったく直感的ではない。絶対必要な事なのに、なぜこんな仕組みにしたし……。

動画系の機能が強化されている

動画からサムネイル画像を作成することが比較的簡単に行える。こちらの CreateThumb() は 100 行くらいのコード。動画のコンテナやコーデックに関わらず、扱えるものなら何でもこれでいける。

WPF の描画関連の基本思想をまだ理解できておらず(分かりやすくまとめてあるところが見当たらず……)、また、動画から画像への転写が非同期に行われるようで、その捕捉に無駄なコードを書いてしまっているので、できれば改善したい。

UI コントールの配置が簡単・速い

ここからはフォームの良いところ。

フォームは UI を作るのが簡単で速い。先に述べたように、WPF は実質 XML 手打ちなのに比べて、マウスでどんどんポチポチするだけでいい。

EditSongWindowまた、例えば右のようなウィンドウを作るとして、WPF では「どのように流し込むか」完成形を予めイメージして(もしくはどこかに書いて)から考えて、それからでないと UI を作り始められないが、フォームはもっと気軽だ。「あ、これも追加しなきゃ」と場当たり的に追加しても、後からタブオーダーを簡単に変更できたりする。

また、ラベルにアクセラレータキー(&A とか)を設定しておけば、Alt+A を押したときに隣のテキストボックスにフォーカスがちゃんと当たるなど、長年の作り込みが素晴らしい。WPF はそんなことしてくれない。

非ビジュアルコントロールもマウスで配置可能

タイマーや各種ダイアログボックスなど、ウィンドウ上に配置されるわけではないコントロール(非ビジュアルコントロール)も、フォームはマウスでポチポチ配置したり、ダブルクリックでイベントハンドラーを作ったりできる。WPF はできない。

基本的な機能がきちんと揃っている

当たり前のことが当たり前にできる。そう、フォームならね。

しかし、WPF には基本的な機能すら欠けている。

例えば、フォルダー選択ダイアログが WPF には無い。かなり高頻度で使うパーツであるが、無いものは無い。仕方が無いので、別途ライブラリを使ったりしてしのぐことになる。

また、ウィンドウの右上のボタン群のうち、最小化だけさせない、というのも、フォームならプロパティー 1 つで簡単にできる。一方で WPF は、P/Invoke で Windows API を直接叩かなくてはならない。

マイナーな機能も結構揃ってる

フォームは、マイナーめな機能も意外と揃っている。

例えば、アプリケーション独自のメッセージを処理するための機構。フォームの場合、Form.WndProc() をオーバーライドするだけで良い。一方で WPF の Window クラスにはその仕組みはないため、いくつかのパーツを組み合わせて実現する必要がある。

マルチディスプレイ環境で、ディスプレイごとの領域もフォームなら取得できる。WPF はできない(全領域なら WPF でも取得できる)。例えば、ウィンドウが 2 枚目のディスプレイにいる場合、そのディスプレイの左端に吸い付く、というような動作を、WPF ではどうやって実現するのだろうか?

勉強量が WPF より少なくて済む

シンプルに考えて、WPF でアプリケーションを作ろうとする場合、プログラミング言語である C# に加えて、画面を作るための XML である XAML を勉強する必要がある。フォームは C# だけでいい。

日本語の情報が豊富

フォームに関する日本語の情報は豊富で、DOBON.NET などのウェブサイトを見れば多くの疑問は解決する。

一方で WPF のまとまった日本語情報というのは少ない。網羅的なウェブサイトが無いのが痛い。Tips も散在はしているが数が少ないのと、せっかく見つけても「とりあえずやってみたら動いた」というような感じで汎用性に疑問のあるページも多い。一番困るのは、WPF の考え方や概念について分かりやすくまとまった情報が無いこと。一番最初に挙げた「流し込む」ような UI 配置も理解するのに時間がかかったし、描画の仕組みなどはまだまったく理解できていない。

WPF では 1 つのことをやるために C# でコードを書くやり方と XAML でコードを書くやり方の 2 通りの実装方法があるが、片方しか掲載されていないこともしばしば。

WPF は英語含めても情報が見当たらない

フォームは日本語の世界で充足するが、日本語情報の少ない WPF は海外サイトも探す必要がある。……が、海外の情報もちゃんと見つけられていない……。

まとめ

後発の WPF は、フォームよりも洗練された考え方を元に設計されていると思う。そしてその「見どころ」には確かに、WPF を使いたくなるものである。

しかし、設計が煮詰まっていないのか、あるいは実装が追いついていないのか、実装する気が失せたのかは分からないが、「見どころ」以外の部分は実用レベルに達していないと言っても過言では無い。

現状は、「見どころ」を味わうために WPF を使って地獄を見るか、それとも堅実にフォームを使って「見どころ」を楽しめない残念さをかみしめるか、の 2 択になっている気がする。

しっかりと WPF の基本思想が周知されたうえで、細部にわたり WPF の実用性が高まると良いのだが……。10 年以上前に登場したのに今がこのような状況と言うことは、今後も改善しない可能性が高いのかもしれない……。

いろんな意味で手軽に開発を進められるのは、フォームのほうだと思う。

追記:MVVM について

WPF で本格的に開発するのであれば、MVVM(Model View ViewModel)パターンと呼ばれるソフトウェアアーキテクチャの習得・活用が必須となってくる。MVVM WPF ではプログラムの考え方・構造が全く異なるため、フォームとの単純な比較はできなくなる。

本記事で使用している、フォームから WPF に移行した直後(diff 時点)でのゆかりすたー METEOR は MVVM を採用しておらず、それゆえフォームのコードとの 1 対 1 での比較がしやすくなっている。

ゆかりすたー METEOR も後期になると MVVM を採用しており、また、ゆかりすたー METEOR の後継であるゆかりすたー NEBULA は当初から MVVM WPF で開発されている。

MVVM の習得にも時間を要するため、それも含めて、手軽に開発を進められるのは、フォームのほうだと思う。

一方で、MVVM WPF では UI とそれ以外を区別してプログラムの保守性が高まるため、一定規模以上のプログラムではメリットが大きくなってくる。

実装が不十分であるところから生じる地獄についても、外部ライブラリ(Livet など)の活用によりある程度軽減される。

蛇足:個人的な好み

個人的な好みでいえば、好きな順に、
MVVM WPF > フォーム >>> MVVM ではない WPF

メインは MVVM WPF で行っており、使い捨てのツールをサッと作る時などはフォーム。

C# で extended-length パス

Windows ではパスの長さは MAX_PATH(260)文字までということになっていて、C#(というか .NET Framework)のファイル系ライブラリも昔は 260 文字までのパスしか使えなかった。

260 文字を超えるとても長いパスを扱えるようにするのが extended-length パスで、単純に、通常のパスの先頭に "\\?\" を付与するだけで良い。

使い方


NET462extended-length パスは .NET Framework 4.6.2 でサポートされたので、プロジェクトのプロパティーでターゲットフレームワークを .NET Framework 4.6.2 以上にしたうえで、以下のコードを実行すると、正しく結果が得られる。

const String EXTENDED_LENGTH_PATH_PREFIX = @"\\?\";
String aPath = EXTENDED_LENGTH_PATH_PREFIX + @"C:\Tmp\とても長いパス\123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789A123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890B\1234567890123456789Z\1234567890123456789Y\VeryVeryLongPath";
MessageBox.Show("Len: " + aPath.Length.ToString() + "\nExists: " + Directory.Exists(aPath).ToString());

"\\?\" を付与しないと、フォルダーが存在する場合でも False が返されてしまう。

互換性

ターゲットフレームワークを .NET Framework 4.6.2 とすると動作する環境が限られてしまう。

.NET Framework 4.6.2 未満の環境でも、extended-length パスはサポートされないにしても、短いパスである限りは動くようにしたい場合は、ターゲットフレームワークを例えば .NET Framework 4.5 としたうえで、まず App.config の <runtime> に <AppContextSwitchOverrides> を追加する。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
  </startup>
  <runtime>
    <AppContextSwitchOverrides value="Switch.System.IO.UseLegacyPathHandling=false;Switch.System.IO.BlockLongPaths=false"/>
  </runtime>
</configuration>

実行時に .NET のバージョンを確認し、4.6.2 以上であれば extended-length パスを使うようにする。
private Boolean GetClrVersionRegistryNumber(out Int32 oClrVersion)
{
    using (RegistryKey aKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"))
    {
        if (aKey != null && aKey.GetValue("Release") != null)
        {
            oClrVersion = (Int32)aKey.GetValue("Release");
            return true;
        }
        else
        {
            oClrVersion = 0;
            return false;
        }
    }
}

private Boolean Is462()
{
    Int32 aClrVer;
    GetClrVersionRegistryNumber(out aClrVer);
    return aClrVer >= 394802;
}

private String ExtendPath(String oPath)
{
    // extended-length パスが使えない環境や、既に extended-length パスになっている場合はそのまま返る
    if (!Is462() || oPath.StartsWith(EXTENDED_LENGTH_PATH_PREFIX))
    {
        return oPath;
    }

    // extended-length パスにする
    // MAX_PATH 文字以上のフォルダー名をダイアログから取得した場合など、短いファイル名形式になっていることがあるため、
    // Path.GetFullPath() で長いファイル名形式に変換する
    return EXTENDED_LENGTH_PATH_PREFIX + Path.GetFullPath(oPath);
}

private void button1_Click(object sender, EventArgs e)
{
    try
    {
        String aPath = ExtendPath(@"C:\Tmp\短いパス");
        //String aPath = ExtendPath(@"C:\Tmp\とても長いパス\123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789A123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890B\1234567890123456789Z\1234567890123456789Y\VeryVeryLongPath");
        MessageBox.Show("4.6.2 or higher: " + Is462().ToString() + "\nLen: " + aPath.Length.ToString() + "\nExists: " + Directory.Exists(aPath).ToString());
    }
    catch (Exception oExcep)
    {
        MessageBox.Show(oExcep.Message);
    }
}

このようにすると、.NET 4.6.2 以上の環境で動作させると長いパス含めて正しく動作するし、.NET 4.6.2 未満の環境では長いパスは動作しないが短いパスなら動作する。


C# で SQLite を便利に使うサンプルコード(LINQ to SQLite)

【ご案内】
改訂版を公開しましたので、改訂版をご覧ください。

------------------------------
.NET Framework C# で SQLite3 を使う方法について、すでにいくつか情報はあるものの、数が少なかったりするので、自分がハマった罠なども含めて、ここにまとめておく。合わせて、サンプルプログラムも公開する。

どの SQLite ライブラリを使うか?

C# で SQLite を使うための方法はいくつかあり、自分が把握している範囲では以下のようになる。
  • C 言語用 DLL を P/Invoke で使う……本家本元の DLL を使い、従来型の文字列によるクエリでプログラミング。最新のライブラリを使える(本稿執筆時点で 3.9.2)。しかし、C 言語レベルの生産性しかなく、労多くして功少なしなので、どうしても最新のバージョンを使いたい時以外はお薦めしない。
  • System.Data.SQLite……SQLite 本家による .NET 用ライブラリ。データベースを便利に使う ADO.NET に対応しており、生産性が向上。C 言語用 DLL よりは多少バージョンが古い(3.8.11.1 ベース)ものの、総合的に一番お薦め。
  • Mono.Data.SqliteClient…….NET のオープン・クロスプラットフォーム実装である Mono プロジェクトが作成したライブラリ。ADO.NET 対応。文字コードが Unicode(恐らく UTF-16)と記載があるのが気になる(本家は UTF-8)。別途本家 C 言語用 DLL が必要な模様。
  • csharp-sqlite……マネージドコードのみで作成されたライブラリ。ADO.NET 対応。2011 年で開発停止の模様。バージョンは 3.7.7.1。マネージドコードしか使えない制約がある場合に重宝しそう。
  • sqlite-net……コンパクトなライブラリ。ADO.NET 非対応。できる限り実行ファイルサイズを小さくしたい場合に活躍しそう。2012 年あたりで開発が停止しているようだ。
以降では、System.Data.SQLite を使う。

ADO.NET とは?

System.Data.SQLite は ADO.NET に対応しているが、そもそも ADO.NET とは何か。ADO.NET の詳しい解説は @IT の記事などに記載があるが、プログラマーから見たメリットを三行で書くとすれば、
  • エディタのコード補完で楽ちんかつ安全にクエリを書ける(LINQ)
  • 設計時からデータ構造をビジュアル化できる(Entity Framework)
  • 使うデータベース(SQLite/Oracle……)によらず同じコードを使い回せる
あたりになるのではと理解している。

これにより、C 言語で SQLite をいじるよりもはるかに効率的にデータベースプログラミングが行える。

なお、本稿では、LINQ は使うが Entity Framework は使わない。

System.Data.SQLite のインストール

NuGetインストールは非常に簡単である。ここでは、Visual Studio 2015 Community / .NET Framework 4.5 の環境で実行しているものとする。
  1. Visual Studio を起動し、SQLite を使いたいプロジェクトを開く
  2. メニューの[ツール|NuGet パッケージマネージャー|ソリューションの NuGet パッケージの管理]をクリック
  3. 検索窓に「SQLite」と入力して SQLite パッケージを検索
  4. 検索結果の System.Data.SQLite を選択して、インストールボタンをクリック。関連するライブラリも含めて自動でインストールが終わる
たったこれだけ。

以前は、NuGet をコマンドラインで使う必要があった時代もあったようだが、現在は GUI で使える。

NuGet の欠点は、プロジェクトごとにインストールの必要があるため(プロジェクトに最適なバージョンを選んでインストールしてくれる)、SQLite を使うプロジェクトが複数ある場合は、ディスクをどんどん消費していく。その場合は、ダミープロジェクトでインストールしたバイナリを使い回したり、手動でバイナリをダウンロードして共用するといいかもしれない。

System.Data.SQLite の基本

データベースファイル(*.db とか *.sqlite3 とか)を開くには、SQLiteConnection クラスを使う。

SQLiteConnection を new する際にパラメーターを文字列で渡すのだが、SQLiteConnectionStringBuilder というお助けクラスを使うと、パラメーター文字列をミスなく生成できる。

SQLiteConnectionStringBuilder aConnectionString = new SQLiteConnectionStringBuilder
{
    DataSource = @"R:\Test.db"
};
using (SQLiteConnection aConnection = new SQLiteConnection(aConnectionString.ToString()))
{
    aConnection.Open();

    // ここにデータベース処理コードを書く
}


従来型の文字列によるコマンドを発行したい場合は、SQLiteCommand クラスを使う。

using (SQLiteCommand aCmd = new SQLiteCommand(aConnection))
{
    aCmd.CommandText = "CREATE TABLE IF NOT EXISTS t_test (test_id INTEGER NOT NULL PRIMARY KEY, test_name NVARCHAR NOT NULL, test_height REAL);";
    aCmd.ExecuteNonQuery();
}


テーブル構造定義クラスの導入

データへのアクセスを簡単・分かりやすくするために、テーブル構造を定義するクラスを作成する。

簡易名簿テーブル「t_test」が以下のような構造になっているとする。
フィールド名NULL備考
test_idINTEGER不可連番
test_nameNVARCHAR不可氏名
test_heightREAL身長

この場合、テーブル構造定義クラスは以下のようになる。

[Table(Name = "t_test")]
public class TTestData
{
    // ID
    [Column(Name = "test_id", DbType = "INT", CanBeNull = false, IsPrimaryKey = true)]
    public Int32 Id { get; set; }

    // 氏名
    [Column(Name = "test_name", DbType = "NVARCHAR", CanBeNull = false, UpdateCheck = UpdateCheck.Never)]
    public String Name { get; set; }

    // 身長
    [Column(Name = "test_height", DbType = "REAL", CanBeNull = true)]
    public Double? Height { get; set; }
}


フィールドをプロパティーとして定義しているシンプルなクラスである。

CREATE TABLE で作成した際のテーブル情報に合わせて、クラスメンバーの属性を記述している。例えば ID フィールドであれば、Name = "test_id" でデータベース上のフィールド名、DbType = "INT" でデータ型、CanBeNull = false で NULL 不可であることを表している。

DbType の型について注意点が 2 つ。
  • 整数型は DbType = "INT" とする。INTEGER ではエラーになる。
  • 文字列型は DbType = "NVARCHAR" とする。TEXT ではエラーになる。
特に、文字列型を TEXT としてしまうと、原因が分かりづらいエラーが発生し、ハマることになる。

身長フィールドは、NULL を許可している。CanBeNull = true にすると共に、プロパティーの型を「Double?」というように「?」を付けて nullable にしている。

レコードの挿入

テーブル構造定義クラスを導入したことにより、データベースの取扱が非常に簡単になる。

レコードの挿入を行うコードは、以下のようになる。SQLiteCommand クラスを使わなくて済むので全然 SQL っぽくなく、普通のコードのように書ける。

using (DataContext aConText = new DataContext(aConnection))
{
    Table<TTestData> aTableTest = aConText.GetTable<TTestData>();
    aTableTest.InsertOnSubmit(new TTestData { Id = 1, Name = "Fukada Kyoko" });
    aTableTest.InsertOnSubmit(new TTestData { Id = 2, Name = "Eda Ha", Height = 180.0 });
    aTableTest.InsertOnSubmit(new TTestData { Id = 3, Name = "Dan Gerou", Height = 150.5 });
    aTableTest.InsertOnSubmit(new TTestData { Id = 4, Name = "Baba Takashi" });
    aTableTest.InsertOnSubmit(new TTestData { Id = 5, Name = "Aikawa Ai", Height = 145.6 });
    aConText.SubmitChanges();
}


ID 1 や 4 では身長を設定していないので、データベース上 NULL として格納される。

レコードの検索

レコードの検索は LINQ to SQLite と呼ばれる手法で行う。ちょっと変わった書き方だが、エディタのコード補完が効くのでミスが減る。

using (DataContext aConText = new DataContext(aConnection))
{

    Table<TTestData> aTableTest = aConText.GetTable<TTestData>();
    IQueryable<TTestData> aQueryResult =
            from x in aTableTest
            where x.Name == "Eda Ha" || x.Height < 150.0
            orderby x.Height
            select x;
    foreach(TTestData aData in aQueryResult)
    {
        Debug.WriteLine(aData.Name);
    }
}


from のところが SQL の発行に相当する部分。なんとなく SQL に似ているので、読めば理解はできると思う。

結果は foreach で回せるので、非常に簡単に扱える。

なお、文字列の部分一致(LIKE)を検索したい場合は String.Contains() を用いて where 句を
where x.Name.Contains("hoge")
のようにすれば良い。

レコードの削除

レコードを削除する場合、削除したいレコードを検索し、それを削除する、という流れになる。

検索については前節と同じで、その結果を、DeleteAllOnSubmit() メソッドにぶち込むだけ、とこれまた簡単。

using (DataContext aConText = new DataContext(aConnection))
{
    Table<TTestData> aTableTest = aConText.GetTable<TTestData>();
    IQueryable<TTestData> aDelTargets =
            from x in aTableTest
            where 140 < x.Height && x.Height < 160
            select x;
    aTableTest.DeleteAllOnSubmit(aDelTargets);
    aConText.SubmitChanges();
}


サンプルコード

以上をまとめたものを、サンプルコードとして公開する。
LinqUtils.cs は LINQ に関する補助クラス。CREATE TABLE や CREATE INDEX は少々コーディングが面倒くさいので、テーブル構造定義クラスの情報から自動的にテーブルを作成するためにとりまとめており、こちらも GitHub で公開している。

サンプルコードアプリの使い方

  • TestLinqToSqlite_Basic基本操作の「DB 作成」ボタンをクリックするとデータベースが作成され、「検索」ボタンをクリックすると、条件に合うレコードがコンソールに表示される(以降、表示はすべてコンソールで行われる)。
  • 「削除」ボタンをクリックするといくつかのレコードが削除されるので、その後「検索」ボタンをクリックすると、先ほどの検索結果とは異なる結果が得られる。
  • データベースをリセットするために再度「DB 作成」をクリックしてから、今度は「更新」ボタンをクリックすると、レコード内容が更新される。その後「検索」ボタンをクリックすると、先ほどの検索結果とは異なる結果が得られる。
なお、サンプルコードアプリにはジェネリックの欄もあるが、こちらについては「LINQ to SQLite で共通カラム部分をジェネリックで運用する」を参照。

参考資料

更新履歴

  • 2015/11/21 初版。
  • 2019/04/09 サンプルコードを Dropbox から GitHub に移管。
  • 2019/04/09 サンプルコードアプリの使い方を記載。
改訂版を公開しましたので、改訂版をご覧ください。



System.Data.Linq.dll で NotSupportedException(解決)

SQLite3 を C# で使いたくて、いろいろ試していたら、文字列の比較でエラーが発生。

やったことは、System.Data.SQLite(SQLite の公式 ADO.NET プロバイダー)をインストールし、LINQ to SQLite でデーターベースにアクセス。ソースコードはおよそ以下のような感じ。

    Table<TTestData> aTableTest = aConText.GetTable<TTestData>();
    IQueryable<TTestData> aQueryResult =
            from x in aTableTest
            where x.Name == "Eda Ha"
            select x;


ここで、aContext は DataContext 型のインスタンス。TTestData は以下の定義。

[Table(Name = "t_test")]
public class TTestData
{
    // ID
    [Column(Name = "test_id", DbType = "INT", CanBeNull = false, IsPrimaryKey = true)]
    public Int32 Id { get; set; }

    // 氏名
    [Column(Name = "test_name", DbType = "TEXT", CanBeNull = false, UpdateCheck = UpdateCheck.Never)]
    public String Name { get; set; }
}


NotSupportedExceptionこのようなコードを実行すると、IQueryable の結果を使う段階で、NotSupportedException が発生する。

    例外がスローされました: 'System.NotSupportedException' (System.Data.Linq.dll の中)
    追加情報:SQL Server では、NText、Text、Xml、または Image の各データ型の比較はハンドルされません。

where x.Name == "Eda Ha" の箇所が where x.ID == 1 のような数値型の比較だと例外は発生しない。

原因がわからずさんざん悩んだ。「SQL Server では~」のエラーメッセージで検索しても情報が見つからない。

結論としては、Name の DbType がまずかった。

DbType を NVARCHAR にして、CREATE TABLE する際の型も NVARCHAR にしたら、うまく動作するようになった。

どうやら、SQLite を .NET で使う場合は、文字列型は NVARCHAR にしないといけないようだ。

なお、エラーが発生した環境は、
  • Visual Studio Community 2015
  • .NET 4.5
  • System.Data.SQLite 1.0.98.0(sqlite-netFx45-setup-x86-2012-1.0.98.0.exe)
関連記事

C# による SAPI 5 TTS Engine COM コンポーネントの実装(不完全)

.NET Framework 4.5 C# / Visual Studio 2013 にて、Microsoft Speech API (SAPI) 5.4 の TTS エンジンのサンプルコード(テストコード)「TestUtaYomiEngine」を作成した。

不完全ではあるものの、SAPI 音声合成エンジン側の C# コードはあまり見かけないので、参考になるかもしれないと思い公開する。SAPI に限らず、C# の COM コンポーネントは少ないので、そちらの方面でも参考になるかもしれない。

逆に、アドバイス等あれば、是非とも教えて頂きたい。

本サンプルコードの挙動

本サンプルコードは、既に公開している音声合成エンジン「唄詠(うたよみ)」(Ver 6 系)の代替エンジンとして動作する。
入力された文字にかかわらず、「C:\Test.wav」(44.1kHz、16 ビット、モノラル限定)を再生する。

本サンプルコードで動く部分

以下のテキストスピーチソフトとの組み合わせで動作する。これらのアプリは恐らく、SpXXXX 系の純粋な SAPI を使用しているものと思われる。
  • SofTalk
  • TextToWav
  • 棒読みちゃん(フォーマット変更エラーが表示されるが再生は可能)

本サンプルコードで動かない部分

.NET Framework の SpeechSynthesizer クラスを使用しているテキストスピーチソフト(ゆっくり MovieMaker など)との組み合わせでは動作しない。

使い方

  • Ver 6 系の唄詠をインストールし、テキストスピーチソフトと組み合わせて正しく動作することを確認する。
  • サンプルコードを管理者権限でビルドする。Ver 6 系の唄詠エンジンの代わりに、ビルドしたコードが登録される。
  • 44.1kHz、16 ビット、モノラルの WAVE ファイル(UTAU 音源の原音 WAVE を使うと良い)を C:\Test.wav に保存する。
  • テキストスピーチソフトで唄詠音源でしゃべらせると、サンプルコードが動作し、上記 WAVE が再生される。
  • 唄詠以外のソフトのベースとして使う際は、必ず Guid を変更すること。

ダウンロード


ライセンス


関連記事


更新情報

  • 2015/11/21 ダウンロードリンクが切れていたので、サルベージして再公開

C# プラグイン DLL の呼出でキャストができずハマった

C# でプラグインとやり取りしたいと思ってテストコードを作っていたのだが、思わぬ所で躓いた。

やりたいことはごく普通で、
  • プラグインのインターフェースを定義
  • インターフェースを実装したプラグイン DLL を作成
  • ホスト(EXE)側から、プラグイン DLL を動的に呼びだし
というもの。

プラグインのインターフェースは以下のようなシンプルなもの(PluginInterface.cs)。
namespace TestPlugin
{
    public interface ITestPlugin
    {
        Int32 Diff(Int32 oNum1, Int32 oNum2);
    }
}

このインターフェースを実装した TestDLL.dll を作成しておき、ホスト(EXE)側で以下のコードで呼びだした。
private void button12_Click(object sender, EventArgs e)
{
    ITestPlugin aPI = null;
    try
    {
        Assembly aAsm = Assembly.LoadFrom(@"C:\TestDLL.dll");
        foreach (Type aType in aAsm.GetTypes())
        {
            if (aType.IsInterface)
            {
                continue;
            }

            Object aObj = Activator.CreateInstance(aType);
            aPI = aObj as ITestPlugin;
            if (aPI != null)
            {
                MessageBox.Show("プラグインの計算結果:" + aPI.Diff(5, 3).ToString());
                break;
            }
        }
    }
    catch (Exception oExcep)
    {
        MessageBox.Show(oExcep.Message);
    }
    if (aPI == null)
    {
        MessageBox.Show("プラグインをロードできませんでした。");
    }
}

実行してみると、Activator.CreateInstance() でオブジェクトが生成され、aObj に値が入っているのが(デバッガで)確認できた。また、別途コードを追加して aType.GetInterfaces() でインターフェース名を取得すると、ITestPlugin を有していることも確認できた。

しかし!

次の行のキャストは失敗し、aPI は null のままであった。

いろいろ悩んだが、結果的には、「プラグインインターフェースを定義している PluginInterface.cs を、DLL とホストの双方で直接プロジェクトに追加していた」のが原因だった。

解決策としては、
  1. PluginInterface.cs を直接追加したプロジェクトで予め Interface.dll を作っておき、プラグイン DLL とホストの双方から、参照設定で Interface.dll を参照する。
  2. ホスト側のプロジェクトに PluginInterface.cs を直接追加してビルドし、プラグイン DLL ではホストのバイナリを参照設定で参照する。
のいずれかを行えば良い。解決策 1 だとバイナリファイルが 3 つになるので、ファイルの数で言えば、解決策 2 の方がすっきりする。

しかし、双方で直接プロジェクトに追加するとなぜうまく動かないのかは不明。GUID の問題なのかと思って ITestPlugin に Guid 属性を付けてみたりもしたが、改善しなかった。


カンパのお願い
Amazon でお買い物の際は、下記で検索して頂けたら幸いです。
記事検索
最新コメント
月別アーカイブ
  • ライブドアブログ