ソフトウェア-開発全般

コンテキストメニュー制御いろいろ(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 インメモリデータベースについて記載。



MSIX パッケージが似非 64 ビットになる件

Win32 Desktop Bridge でアプリを Microsoft Store 配布用に MSIX パッケージ化した場合、デフォルトのままでは、x64 でパッケージ化しても、実際には 64 ビットなアプリにはなりません(Visual Studio 2022 17.0.4 現在)。

状況整理

Arch2パッケージプロジェクト(TestMsStoreWpf_Package)の[公開 → アプリパッケージの作成]で MSIX パッケージを作成する際、アーキテクチャを x64 にしただけでは、x64 なアプリは作成されません。

TaskManアプリを実行してタスクマネージャーを見てみると、「(32 ビット)」の表記はないので、一見すると 64 ビットアプリのように見えます。

しかし実際には、Environment.Is64BitProcess は false が返ってきますし(パッケージ化する前はちゃんと true が返ってきます)、4 GB 以上のメモリアロケートもできません。なんちゃって 64 ビットアプリになってしまっています。

解決方法

【注意】
本記事の解決方法は情報が古くなりました。
新しい解決方法については、こちらをご覧ください。
備忘として以下を残しておきます。

PlatformTarget本体プロジェクト(TestMsStoreWpf)のプロパティーで、Platform Target を Any CPU ではなく x64 に変更します。

しかしこうすると、本体プロジェクトはビルド・実行できるのですが、パッケージプロジェクトで MSIX パッケージを作成しようとすると、次のようなエラーになってしまいます。

資産ファイル '~\obj\wappublish\win-x64\project.assets.json' が見つかりません。

以降で、きちんとビルド・実行できるように対策していきます。

間違いを防止するために、いったん、本体プロジェクトの bin フォルダー・obj フォルダーを削除します。

Releasex64本体プロジェクトのプロパティーで Platform Target が x64 になっていることを確認したうえで、Release | x64 で本体プロジェクトをビルドします。

objobj フォルダーに project.assets.json を含む 5 つのファイルが作成されます。

obj フォルダーの中に wappublish フォルダーを作成し、さらにその中に win-x64 フォルダーを作成します。

作成した win-x64 の中に、project.assets.json を含む 5 つのファイルをコピーします(project.assets.json だけでもいいのかもしれませんが)。

Editコピーした win-x64 フォルダーの project.assets.json を編集します。

先頭付近にある targets セクションの、「"net6.0-windows10.0.19041": {}」の行末にカンマを追加し、その次の行として「"net6.0-windows10.0.19041/win-x64": {}」を追加します(ターゲット OS バージョンが 10.0.19041.0 の場合)。

  "targets": {
    "net6.0-windows10.0.19041": {},
    "net6.0-windows10.0.19041/win-x64": {}
  },

その後パッケージプロジェクトをビルドすると、真に 64 ビットの MSIX パッケージが作成されます。パッケージをインストールしても、Environment.Is64BitProcess が true になります。

なお、パッケージプロジェクトビルドの際に「アプリケーション マニフェストが無効なため、アプリケーション パッケージを作成できませんでした。アプリケーション マニフェストのエラーを修正してください。」というエラーになる場合がありますが、構わず再度ビルドするとビルドできます。このエラーは、x64 かどうかに関わらず発生します。

ストア向け MSIX のローカルでの動作確認方法

Win32 Desktop Bridge でアプリを Microsoft Store 配布用に MSIX パッケージ化した場合、ストア配布する前にローカルで動作確認する方法を整理しました。管理者権限が必要です。

事前準備

予め、PC の設定を変更しておきます。Windows の設定で「開発者向け」のページを開きます。

DevMode開発者モードを「オン」にします。

PowerShellScriptまた、PowerShell(署名無しスクリプトの許可)の適用ボタンをクリックします。

アプリのインストール

PowerShell を実行します。

MSIX パッケージが生成されたフォルダー(.msixupload ファイルがあるフォルダー)からさらに 1 階層深いフォルダー(Install.ps1 ファイルがあるフォルダー)に移動します。

PS> CD 目的のフォルダーパス

InstallPsInstall.ps を実行します。

PS> .\Install.ps

Shoumei新しい PowerShell が開き、証明書のインストールについて尋ねられるので、Y で回答します。

Imstalling証明書のインストールが終わると、元の PowerShell に戻り、インストールが続行します。

Done「アプリは正常にインストールされました」表示されればインストール完了です。

起動

PowerShell を実行したユーザーのスタートメニューにアプリが登録されているので、スタートメニューから起動して動作確認をします。

関連リンク




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 秒待つと、サンプルプログラムがアクティブになる。


次期ゆかりすたーの開発に着手

nebulaゆかり検索支援ツール「ゆかりすたー METEOR」の次期世代、「ゆかりすたー NEBULA」の開発に着手しました。

AutoResetEvent と CountdownEvent の速度差比較

スレッド間で繰り返し同期を取る必要がある場合、今までは AutoResetEvent を使っていたが、こちらの記事に「CountdownEvent の方が、オーバーヘッドが小さい」と書かれていた。

そうだったのか!

昔どこかのサンプルコードを見て AutoResetEvent を使っていたけど、他の手段との比較検討はしたことなかったなぁと思い、実際に CountdownEvent と処理速度を比べてみた。

処理速度の結論

処理速度について結論から言うと、AutoResetEvent より CountdownEvent のほうが速い。

そもそも

同期に使えるクラスはたくさんあり、MS 公式にまとめられている。そのうち、
  • AutoResetEvent……スレッドをシグナル状態になるまで待機させる。単一の待ちスレッドが解放された後、自動的にリセットされる。
  • CountdownEvent……スレッドをカウントダウンが 0 になるまで待機させる。複数のスレッドからのシグナルで 1 つ以上のスレッドのブロックを解除できる。
とあるので、待機させる側と待機する側のスレッド数は、AutoResetEvent は 1 対 1、CountdownEvent は複数対複数、ということになる。

また、AutoResetEvent はスレッド解放後に自動的にリセットされる(何もしなくても再び待機させられる)ので、セットし忘れてスレッドがスルーしてしまうバグや、セットのタイミングでブロックできないケースが発生するバグの軽減につながり、安心感は高い。

速度比較

機能的な違いは横に置いておいて、処理速度のみに注目して比較。コードは GitHub に上げておいた

メインスレッドとサブスレッドでそれぞれブロックし合うこと 500 万回(メインとサブがそれぞれブロックするので同期回数は 1,000 万回)、その所要時間を計測した。

result こちらの PC 環境で、.NET 5 の自己完結型リリースビルドで実行したところ、所要時間は
  • AutoResetEvent:22,031 ミリ秒
  • CountdownEvent:578 ミリ秒
となり、CountdownEvent のほうが 40 倍近く速いという結果になった。

まとめ

同期回数 1,000 万回で 20 秒程度の差が出たので、100 万回でも秒単位の差が出ることになる。短時間で 100 万回レベルの同期を行う場合は、CountdownEvent を選択する方が良いと思う。

自分のプロジェクトだとそこまで頻繁に同期するわけではなく(数分間で 1 万回程度、つまり 20 ミリ秒程度の差異)、速度差は誤差程度なので、安心感のある AutoResetEvent を使い続けるかもしれない。

.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 も単一になる。



ニコカラメーカー 2 開発状況(フォント選択)

動画にカラオケ風ワイプ字幕を付けるツール「ニコカラメーカー 2」の開発状況です。今回はフォント選択について。

字幕スタイル適用初代「ニコカラメーカー」では、「字幕スタイル適用」タブにあるコンボボックスでフォントを選択していました。プリセットされたフォント設定、または、自分の好きにカスタマイズしたフォント設定の中から選ぶだけなので簡単です。

ニコカラメーカー 2 でも根本的な考え方は一緒で、コンボボックスでフォント設定を選ぶだけです。

歌詞編集ニコカラメーカー 2 では編集画面がビジュアルになっており、選択したフォント設定の概要が視覚的に確認できます。

動画のプレビューをしなくても字幕の大まか雰囲気がわかるので、より編集がやりやすくなります。

複数行複数行の一括設定も行えるようになります。

Shift キーを押しながら行をクリックすることで複数行を選択できます。また、行の左側のチェックボックスで選択行を切り替えることもできます。

複数行が選択された状態で、選択した行のいずれかのフォント設定を変更すると、選択されているすべての行のフォント設定が同時に変更されます。

範囲選択行中の一部の文字だけフォント設定を変更したい場合は、変更したい文字を選択(Shift + クリックで複数文字を選択することも可)してからフォント設定を選ぶことで、選択した文字のみフォントを変更することができます。

初代ニコカラメーカーでは「前行追随」を活用することで文字ごとのフォントを変更できましたが、予め歌詞行を分けておく必要があり、一手間がかかっていました。ニコカラメーカー 2 では、より手間無く文字単位のフォント設定ができるようになります。

なお、ニコカラメーカー 2 が全体像として目指すところについては、以前の記事を参照ください。

【関連リンク】

フォームと 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());
}


フォームから 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 で行っており、使い捨てのツールをサッと作る時などはフォーム。

ゆかりすたーリニューアル作業中

動画ファイルリスト化ツール「ゆかりすたー」をリニューアル中。

画面デザインを今風にアップグレード。

見た目以外の部分では(というかこちらが本命なのだが)、使用しているフレームワークを WinForm から WPF (Windows Presentation Foundation) に変更。将来的な拡張への基礎としている。

YukaLister


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 未満の環境では長いパスは動作しないが短いパスなら動作する。


フォルダー追加時の待ち時間を短縮

ゆかりすたーにゆかり検索対象フォルダーを追加した際の待ち時間を短縮できるように改良中。

フォルダー追加時の動作としては、いったんサブフォルダーをすべて洗い出して一覧として表示した後、ゆかり用データベースの構築と、ゆかり用リストの出力を行っている。

時間がかかるのはゆかり用データベースの構築で、これが終わらないとキーワード検索さえ行えない。

そこで、フォルダー一覧表示後、まずファイル名だけをゆかり用データベースに追加することとした。ファイル名だけであればすぐに追加でき、その段階で、キーワード検索でファイル名が検索可能となる。その後、ファイル名以外の属性についてゆかり用データベースに追加していく。

実際に、一覧表示後にキーワード検索ができるようになるまでの待ち時間を比較した結果が以下。物量としてはおよそ、フォルダー数 1,500、ファイル数 22,000、容量 1.6 TB。

環境 1(メインマシン)

現行(Ver 7.21 β)新バージョン
ファイル名のみ検索可能となるまでの時間-15 秒
全属性で検索可能となるまでの時間5 分 43 秒
5 分 06 秒

環境 2(LAVIE)

現行(Ver 7.21 β)新バージョン
ファイル名のみ検索可能となるまでの時間-13 秒
全属性で検索可能となるまでの時間3 分 54 秒
4 分 16 秒

解説

statusいずれの環境でも、15 秒程度でファイル名検索が可能となった。今までは数分待たないと何もできなかったことに比べると、利便性は大幅に向上することになる。

ファイル名検索が可能となると、右の写真のように、状態欄に「ファイル名検索可」と表示される。

全属性で検索可能となるまでの時間は、環境によって現行より速くなったり遅くなったりしている。環境 1 では 37 秒速くなった一方で、環境 2 では22 秒遅くなった。傾向としては、HDD の速度が遅い環境ほど、現行よりも新バージョンのほうが速くなりやいかもしれない。

なお、トータルの所要時間としては、フォルダーを一覧表示するまでの所要時間 30 秒程度と、ゆかり用リストを出力する時間数秒が上乗せされる。新バージョンにおけるトータル所要時間は、環境 1 では 5 分 49 秒、環境 2 では 4 分 52 秒となった。






ニコカラ動画のご紹介(750 突破記念)

Nkm768カラオケ字幕付き動画を手軽に作れるツール「ニコカラメーカー」を活用して作成された動画の数が、750 を超えました。

501~750 番目のニコカラの中から、いくつかピックアップしてご紹介させていただきます。

すべての一覧は、以下のリンクからご覧になれます。
ニコカラメーカーを使用して作成した動画をニコニコ動画にアップロードした場合、ニコカラメーカー 簡易説明書動画をコンテンツツリーの親にご登録お願いします。上記リンクに反映されます。

アリスのお茶会 ~デキる女と(ry(Raildex さん)

東方ニコカラです。

ニコカラメーカーのインライングラフィックス機能を活用し、パート分けをアイコンで分かりやすく行っています。

NeGa/PoSi*ラブ/コール(ディレイドさん)

東方ニコカラです。

ディレイドさんの投稿ペースは速く、次々とニコカラを投稿されています。魂音泉さんや凋叶棕さんのニコカラが多いようです。

Bi☣hazard(饒速水琥白主さん)

UTAU 音源「気球音アイコ」5 周年で作曲された UTAU オリジナル曲のニコカラです。

英語歌詞ですが、ルビが振られているので分かりやすいです。

ふたりの点滅(しゅれんつさん)

CeVIO 黄咲愛里オリジナル曲のニコカラです。

動画の雰囲気に合わせた字幕配色になっています。

いちにちっ!(ver.Xmas)(徒者さん)

メドレーカラオケです。

徒者さんはニコカラ初挑戦とのことです。新しくニコカラを作り始めて下さる方が増えるのは嬉しいですね。

マグネット・ジョーに気をつけろ(Ottomo さん)

星屑スキャットのボカロカバー版のニコカラです。

Ottomo さんもニコカラ初挑戦とのことです。丸くて優しいフォントをセレクトされています。


これからもどんどんニコカラが増えていくのを楽しみにしています。


ニコカラメーカーの今後の見通し

ニコカラメーカーカラオケ字幕付き動画を手軽に作成できる「ニコカラメーカー」の今後のバージョンアップについて、見通しを整理しておきます。現時点でのものですので、今後変更の可能性は多いにあります。

2016/04/14 現在、ニコカラメーカーに頂いている要望等は 61 件で、そのうち、37 件は既に対応済みです。

今後検討が必要な大規模改修は、以下となります。
  • 半透明字幕
  • 複数歌詞ファイルの読み込み(パート分け)
  • 歌詞のアニメーション
  • ルビ入力
  • 出力形式の多様化

半透明字幕

歌詞字幕や影などを透けるようにできる機能は、比較的近い将来に対応したいと考えています。

しかしながら、内部処理が複雑なので、実現できるかどうかはまだ不透明です。

複数歌詞ファイルの読み込み(パート分け)

デュエット曲などでは、歌詞ファイルをパートごとに分けて作成される方がいらっしゃいます。現時点では、歌詞ファイルを 1 つしか読み込めませんが、将来的には複数の歌詞ファイルを読み込めるようにしたいと考えています。

単に読み込めるというだけではなく、自動レイアウトもパートごとに分かれるようにする予定です。

歌詞のアニメーション

これについては、直近での対応予定はありません。一般的な商業カラオケではアニメーションしておらず、応用的な内容となるためです。

技術的にも困難が予想され、実現できるかどうか、かなり難しい状況です。

しかし、実現できれば楽しいニコカラが作れるようになりますので、将来的には検討したいです。

ルビ入力

現時点でのニコカラメーカーは歌詞ファイルに記載されているルビを読み込むのみですが、ニコカラメーカー上で直接ルビを編集できる機能について、比較的近い将来に対応したいと考えています。

出力形式の多様化

現時点でのニコカラメーカーは無圧縮 AVI のみ出力可能ですが、将来的には、多様なフォーマットで出力できるようにしたいと考えています。

しかし、技術的には困難が予想され、実現できるかどうかは現時点で不透明です。

ニコカラメーカーを開発中

ルートスフィアニコカラ(カラオケ字幕付き動画)を手軽に作れるフリーソフト「ニコカラメーカー」を開発している。

というのも、ニコカラを作ってみたいと思ってネットで調べてみたら、手順がものすごく難しそうで、それならツールを自作した方が楽なんじゃないか、と思ったからである。

現状、ニコカラは主に以下のような手順で作るらしい(サイトによって少しずつ違うのでイマイチわからないが)。[]内は各段階での成果物のファイル拡張子、【】内は代表的な使用ツール。
  1. 素材(動画、タイムタグ付き歌詞ファイル)を揃える。 [.mp4 .txt]【RhythmicaLyrics】
  2. タイムタグ付き歌詞ファイルを字幕に変換する。 [.ass]【txt2ass】
  3. ルビを振る。 [.ass]【txt2ass】
  4. 出来映えを確認する。 [.avi]【VirtualDubMod / Vobsub / TextSub】
  5. ツールを行ったり来たりしながら 2~4 を繰り返す。保存のやり方を間違えるとデータが消えるらしい。
  6. エンコードする。 [.mp4]【AviUtl】
特に、2~5 の繰り返しが鬼のように面倒くさそうな感じである。1 つのツールでできないので、注意しないと修正した時にデータの齟齬が発生したりするようだ。

また、どのサイトもエンコードに AviUtl を使うと書いてあったが、何か理由があるのだろうか。どうにも AviUtl は敷居が高くて踏み込めない。エンコードだけなら夏蓮根の方が親切丁寧で手っ取り早いと思うのだが。

そんなこんなで、開発中のニコカラメーカーは、2~5 の作業を簡単にできるようにする予定。

現在できているところまでで、サンプル的に動画を出力してみたのが、冒頭のスクリーンショット(クリックすると拡大する)。ニコカラっぽくなっていると思う。

ニコカラメーカーニコカラメーカーの動作の様子は、右の写真(まだ機能はほとんど実現できていないが……)。

今までのニコカラ作りのやり方だと、汎用ツールを使っているので、ニコカラには不要な機能まで山ほどあり、逆にそれが分かりづらさにつながっていると思う。ニコカラメーカーはシンプルにニコカラに特化したツールにしたい。エンコードも、たぶん夏蓮根でいけると思う。

ニコカラメーカーを使った新しいニコカラの作り方は、以下のような手順になるだろう。
  1. 素材(動画、タイムタグ付き歌詞ファイル)を揃える。 [.mp4 .txt]【RhythmicaLyrics】
  2. ルビを含めた字幕を、動作を確認しながら出力する。 [.avi]【ニコカラメーカー】
  3. エンコードする。 [.mp4]【夏蓮根】

追記



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)
関連記事

Visual Studio の .lib を C++Builder にリンクするのは無理っぽい

Visual Studio (Visual C++) 2013 で作成したスタティックライブラリ(.lib)を、C++Builder XE3 のプロジェクトに組み込みたいのだが、無理のようだ。

VC は COFF というオブジェクトフォーマットを採用しており、一方で、C++Builder は OMF を採用している。このため、単純に、VC の .lib を C++Builder に組み込んでリンクしようとすると、

[ilink32 エラー] Error: '...\HOGE.LIB' には無効な OMF レコード (タイプ 0x21) が含まれています (COFF の可能性もあります)
というエラーとなってリンクできない。

形式が違うのなら変換すればいいじゃない、ということで、C++Builder に付属の coff2omf.exe を使おうとするものの、これは通常のスタティックライブラリを変換するためのツールではなく、インポートライブラリを変換するためのものなので、うまくいかない。

サードパーティーのツールはどうか。objconv を試してみた。

ojbconv -fomf in.lib out.lib
のようにして使ってみたものの、警告がたくさん表示された。結果の .lib を C++Builder に組み込むと、

[ilink32 エラー] Fatal: EXTDEF フィックスアップ インデックス (モジュール 'xxx_00.obj' 内) は無効です
というエラーになってうまくいかない。

DLL を使うしかないか……。




C++Builder XE3 で BMP/JPEG/PNG 画像を合成する

C++Builder は、様々なフォーマットの画像を扱うのも簡単だ。……が、まとまった情報が見つからなくて苦労したので、ここにまとめておく。動作確認は C++Builder XE3。

例として、JPEG 画像を背景画像として、その上に、BMP 画像と透過 PNG 画像を重ねる。合成結果を PNG 画像として保存する。

背景 JPEGBMP透過 PNG
TestBG Test1
※Web 用に PNG に変換してあるが、元は BMP
Test2


準備として、ビットマップ形式以外の画像ファイルを読み書きしたい場合は、対応するヘッダをインクルードしておく。

#include <Jpeg.hpp>
#include <PNGImage.hpp>

ヘッダをインクルードしてしまえば、読み込みに関しては、画像フォーマットを気にせずに読み込めるようになる。書き込みは、対応する画像形式を扱うクラスに任せる。コードは以下。

void __fastcall TFormTest01::Button1Click(TObject *Sender)
{
    unique_ptr<TImage>        aImageMerge(new TImage(this));
    unique_ptr<TImage>        aImageBG(new TImage(this));
    unique_ptr<TImage>        aImage1(new TImage(this));
    unique_ptr<TImage>        aImage2(new TImage(this));
    unique_ptr<TPngImage>    aPng(new TPngImage());

    // 画像ロード
    aImageBG->AutoSize = true;
    aImageBG->Picture->LoadFromFile(L"R:\\TestBG.jpg");    // JPEG
    aImage1->AutoSize = true;
    aImage1->Picture->LoadFromFile(L"R:\\Test1.bmp");    // BMP
    aImage2->AutoSize = true;
    aImage2->Picture->LoadFromFile(L"R:\\Test2.png");    // PNG

    // 合成画像のサイズを背景画像に合わせる
    aImageMerge->Width = aImageBG->Width;
    aImageMerge->Height = aImageBG->Height;

    // 合成
    aImageMerge->Canvas->Draw(0, 0, aImageBG->Picture->Graphic);
    aImageMerge->Canvas->Draw(100, 200, aImage1->Picture->Graphic);
    aImageMerge->Canvas->Draw(300, 200, aImage2->Picture->Graphic);

    // PNG 保存
    aPng->Assign(aImageMerge->Picture->Graphic);
    aPng->SaveToFile(L"R:\\Result.png");
}

Result結果は右の画像のようになる。

シンプルなコードだが、透過もきちんと自動処理されている。

ポイントは、TImage を使うこと(ここでは動的生成しているが、もちろんフォームに貼り付けても良い)。TImage のローダーは、画像形式に関わらず読み込める。TBitmap を使うと PNG を読み込めなかった。

古いバージョンの C++Builder/Delphi だと、PNG の読み込みには TPngImage で画像の読み込みを行わなければならなかったようだが、現在は、TImage を使えば良い。

逆に、書き込みは、明示的に TPngImage を使っている。

aImageBG->Picture->SaveToFile(L"R:\\Result.png");

のように拡張子で自動的に PNG に変換してくれるかなーと期待したが、そんなことは無かった。



CodeIQ の「グループを作ろう!」にチャレンジ

目次

現役のエンジニア・企業がプログラミングの問題を出してくれる CodeIQ というサイトがある。問題を解くと、正解不正解の判定や、フィードバックなどを行ってくれる。ジャンルや言語などはさまざまある。

グループを作ろう!

今回は結城浩さんの「グループを作ろう!」という問題に挑戦してみた。アルゴリズムの問題で、プログラムを使って解いても使わずに解いても構わない。プログラムを使う場合、言語は何でも良い。解答(とコメントなど)を送ると、フィードバックをもらえる。

問題の概要は以下。

Bill=Billy
Mick=Michael
Billy=William
というように、同じ人物を表すニックネームのペアが 444 個与えられる。同じ名前をすべて連結し、
Bill=Billy=Will=William=Willie=Willy
Michael=Mick=Micky=Mike=Mikey
というようにグルーピングする。ただし、各行の順番および、同一行内の名前は ASCII 順に並べなければならない。

自分の解答:STEP 1 グループ番号割り振り

C++ でプログラムを組む前提で。

どうやってグループを寄せ集めていくかと考えたとき、グループ毎の配列(vector)を用意してしまう、というのは最初に思いつくが、さすがに効率が悪い(名前を読み込む度に、すべてのグループ配列を見て、すでにグループ化されているか調べる必要がある)。

漠然と、木を使えばできるのではないかとイメージしたが、残念ながら、木を簡単に実装する方法を思いつかなかった。

CodeIQ_グループ割り振りそこで代わりに、グループ番号を割り当てていく方法を採った(右はイメージ図)。

ペアの両辺を読み込み、それぞれの名前に既にグループ番号が割り当てられているか調べる。両辺とも未割り当てであれば、新しいグループ番号を割り振る。いずれかが割り当て済みであれば、もう片方に同じグループ番号を割り当てる。

CodeIQ_グループ変更両方とも割り当て済みで、かつ、違うグループ番号であれば、今までは別のグループとして扱ってきたが、実は同じグループであることが分かったということなので、片方のグループ番号をすべて書き換える。

名前→グループ番号の対応を格納するのは、STL の map を用いた。

自分の解答:STEP 2 結合

すべてグループ分けできたら、同じグループ同士を連結していく。string 型の vector を用意しておき、1 行に 1 グループを突っ込んでいく。

CodeIQ_行番号割り振りmap を先頭から読み込んでいき、同じグループ番号の文字列が既に vector にあるか調べる。あれば、文字列の末尾に新しい名前を追加する。無ければ、新しい行の先頭に名前を追加する。

グループ番号と行番号の対応も、やはり map で覚えておく(……が、今から思えば、配列の方が高速だった)。

map を使うことにより。キーが自動的に ASCII 順になっているので、新しい行ができた時は、単純に配列の末尾に加えれば良く、ソートの必要は無い。また、1 つの行に名前を追加する際も、末尾に追加するだけで良く、ソートの必要は無い。

注意点

注意すべきはグループの統合で、最初はこれを見逃していた。できあがった解答を眺めていて偶然気づいた。グループ統合のコードを後から付け加えたので、これだけ異質な感じがする。

計算量は、入力のペア数を N として、グループ統合がなければ恐らく O(NlogN) だが、グループ統合があると最悪 O(N^2) になるのだろう。

ソースコードの抜粋

// グループ番号割り当て
while ( aInFile && getline(aInFile, aLine) ) {
    get_names(aLine, &aName1, &aName2);

    // 名前のペアをマップに登録していく
    p1 = aGroupMap.find(aName1);
    p2 = aGroupMap.find(aName2);
    if ( p1 == aGroupMap.end() && p2 == aGroupMap.end() ) {
        // 新規登録
        aGroupMap[aName1] = aGroupIndex;
        aGroupMap[aName2] = aGroupIndex;
        aGroupIndex++;
    } else if ( p1 != aGroupMap.end() && p2 == aGroupMap.end() ) {
        // aName1 が登録済み→aName2 を同じグループに登録
        aGroupMap[aName2] = p1->second;
    } else if ( p1 == aGroupMap.end() && p2 != aGroupMap.end() ) {
        // aName2 が登録済み→aName1 を同じグループに登録
        aGroupMap[aName1] = p2->second;
    } else {
        // 両方登録済み
        if ( p1->second != p2->second ) {
            // 違うグループとして登録されているのでグループを統合
            change_group(&aGroupMap, p1->second, p2->second);
        }
    }
}

// A=B=C=D というような結合行を作成していく
while ( aGItr != aGroupMap.end() ) {
    aRItr = aResultMap.find(aGItr->second);
    if ( aRItr == aResultMap.end() ) {
        // 新規に結果行に登録
        aResult[aResultIndex] = aGItr->first;
        aResultMap[aGItr->second] = aResultIndex;
        aResultIndex++;
    } else {
        // 既存の行に追加
        aResult[aRItr->second] += "="+aGItr->first;
    }
    aGItr++;
}


模範解答

CodeIQ_木模範解答では、やはりグルーピングに木を使っていた。

面白かったのは木の実装方法。木を map で実装していた(模範解答は perl と思われる言語で実装されていたので、実際には連想配列)。

map のキーに、葉っぱの名前を入れ、値に、根っこの名前を入れる。根っこの名前は、値を空文字列にでもしておく。

こうすることで、数回 map を探索すれば、一番根っこの名前にたどりつける。1 つの根っこから出てくる名前はみな同じグループで、グループの数だけ、根っこ(空文字列)があることになる。

木をこうやって実装するのは定石なのだろうか。シンプルでいいな。

木の良い点は、グループの結合が楽ちんなこと。2 つのグループの根っこのうち、片方の空文字列をやめて、もう片方の根っこの名前を入れるだけで良い。どんなにグループが巨大でも、1 つの書き換えで済む。

感想

単にグルーピングするだけというシンプルな問題なのに、やってみるといろいろ考えることがあって面白かった。Union Find アルゴリズムとか Union Find Tree データ構造などという問題らしい。

実装言語を問わないこともあり、いろんな解答があったと聞く。なかにはエクセルで解いた人もいるとか。

他の人がどんな解答をしているのか気になっているのだが、残念ながら、ググっても自分の解答を公開している人はほとんどいないようだ。

結城さんがせっかく公開 OK に設定してくれているのだから、自分の解答を公開する人が増えてくれればいいなと思う。


文字コードを指定してメモリから文字列を読み込む(C++Builder XE3/TMemoryStream)

C++Builder は、ファイルの読み込みがとても簡単で、TStringList::LoadFromFile() により、文字コードを適切に処理して読み込んでくれる。

LoadFromFile() は見ただけで使い方が分かる。しかし、その親戚である、メモリからの読み込みを行う LoadFromStream() については、まとまった説明が見当たらなかったので試行錯誤。

で、以下が結果のコード。メモリに格納されている文字列を、文字コードを指定して TStringList に読み込む。

void __fastcall TForm1::Button1Click(TObject *Sender)
{
    const char                  UTF8_STR[] = { 0xE6, 0x98, 0xA5 };
    unique_ptr<TMemoryStream>   aMemStream(new TMemoryStream());
    unique_ptr<TStringList>     aStrList(new TStringList());

    aMemStream->Write(UTF8_STR, 3);
    aMemStream->Seek(static_cast<long>(0), soBeginning);
    aStrList->LoadFromStream(aMemStream.get(), TEncoding::UTF8);
    ShowMessage(aStrList->Text);
}


UTF8_STR が文字列が格納されているメモリ領域。今回は定数にしてあるが、通常は、どこかから持ってきた内容を格納しているバッファになる。

TStringList::LoadFromStream() は、TStream 型(の派生クラス)からしかデータを読み出せない。メモリ領域をそのまま読むことはできないので、仲介役の TMemoryStream が必要になる。

TMemoryStream にメモリ領域の内容を書き込んでいるのが aMemStream->Write(UTF8_STR, 3); TMemoryStream にメモリ領域の内容を読み込むというイメージだったので、最初 Read() を使いそうになってしまったが、Write() なので注意。

Write() 後に、読み書き位置を先頭に戻しておく。

あとは、LoadFromStream() に TMemoryStream へのポインタを渡せば、文字列が読み込まれる。

文字コードの指定は、
  • UTF-8……TEncoding::UTF8
  • UNICODE(UTF-16)……TEncoding::Unicode
  • 指定無し……自動判別
など。TEncoding についてはここが詳しい。

以上、コードを実行すると、メモリ領域から 3 バイトを読み取って、「春」を取得できる。

LoadFromStream

Delphi 用 RS-232C コンポーネントを C++Builder XE で使う

Delphi(および C++Builder の旧バージョン)で手軽に RS-232C(COM ポート)を使うためのコンポーネントに ComPort Library(TComPort)がある。C++Builder XE でも何とか動作したので、やり方を整理しておく。

【実行時パッケージの作成】

  • すべてのプロジェクトを閉じた状態で、[ファイル→プロジェクトを開く]メニューにて、ComPort Library の Source フォルダにある CPortLibCB6.bpk(C++Builder 6 用)を開く。
  • 自動的にプロジェクトが C++Builder XE 用に変換される。
  • ビルド構成を Release にする(右側のプロジェクトマネージャーペインで選択)。
  • メイクすると、CPortLibCB6.bpl ができあがる。


【設計時パッケージの作成】

  • [ファイル→プロジェクトを開く]メニューにて、DsgnCPortCB6.bpk を開く。
  • 実行時パッケージと同様にメイクし、DsgnCPortCB6.bpl を得る。

【パッケージのインストール】

  • CPortLibCB6.bpl を Windows のシステムライブラリフォルダ(Windows 7 64bit なら C:\Windows\SysWOW64)にコピーする。
  • [コンポーネント→パッケージのインストール]メニューにて、追加ボタンをクリックして DsgnCPortCB6.bpl を追加する。デフォルトにチェックを入れると毎回設定しなくて良いだろう。

【ComPort Library を利用するアプリケーションのメイク】

  • ツールパレットペインに「CPortLib」タブが増えていて、その中の TComPort をフォームに貼り付ければ RS-232C にアクセスできるようになる。
  • [プロジェクト→オプション]メニューで、ディレクトリと条件定義→ライブラリパスに、CPortLibCB6.bpl/DsgnCPortCB6.bpl のパスを追加する。
  • [プロジェクト→オプション]メニューで、ディレクトリと条件定義→条件定義に DONT_USE_WINSPOOL_SETPORTA を追加する。
  • winspool.h(include\windows\sdk 等にある)を開き、SetPortA/SetPortW を宣言している箇所を、#ifndef DONT_USE_WINSPOOL_SETPORTA~#endif で挟む
  • CPort.hpp をインクルードする。
  • CPort.hpp を開き、inline じゃない方の EComPort() の宣言 2 つをコメントアウトする。

以上でビルドができる。



≪参考≫

C++Builder XE でウィンドウを透明・半透明にする

Normalウィンドウの透明化・半透明化あれこれ。

元々は右のような普通の(不透明な)ウィンドウの透過度を、いくつかの方法で変化させてみる。

動作は Windows 7 (64bit) / C++Builder XE で行っているが、別の環境では別の動作になるかも。

* クライアント領域のみ透明化

クライアント領域のみを透明化させるには、フォームの TransparentColor/TransparentColorValue プロパティを使う。
void	TFormTest01::Test01()
{
	TransparentColor = true;
	TransparentColorValue = TColor(0x000001);
	Canvas->Brush->Color = TColor(0x000001);
	Canvas->FillRect(TRect(0, 0, 300, 300));
	Panel2->Color = TColor(0x000001);
}
TransparentColorTransparentColor プロパティを true にすることで透過モードとなり、フォーム上の TransparentColorValue プロパティで指定した色(コンポーネント同士の重なりに関係なく、指定した色で塗られている部分はすべて)が完全透過となる。半透明にすることはできない。

Windows API で言えば、SetLayeredWindowAttributes() を LWA_COLORKEY でコールした状態だと思われる。ただし、Windows API の場合、透過部分ではマウスイベント等を受け取れない(透明と言うよりはウィンドウに穴が空いて、背後のデスクトップ等にイベントが行く)ようだが、C++Builder の場合はイベントを受け取れる。

* ウィンドウ全体を半透明化

ウィンドウ全体を半透明にするには、フォームの AlphaBlend/AlphaBlendValue プロパティを使う。
void	TFormTest01::Test02()
{
	AlphaBlend = true;
	AlphaBlendValue = 128;
}
AlphaBlendAlphaBlendValue = 0 で完全透明、AlphaBlendValue = 255 で完全不透明、その中間で半透明となる。

コンポーネントも含め、ウィンドウ内がすべて一律で透明になる。

Windows API で言えば、SetLayeredWindowAttributes() を LWA_ALPHA でコールした状態だと思われる。

* グラスフレーム(ガラス効果)の拡張

Windows Vista から導入された Aero において、グラスフレーム(タイトルバーのもやもやした半透明)をクライアント領域にまで広げるには、GlassFrame プロパティを使う。
void	TFormTest01::Test07()
{
	GlassFrame->Enabled = true;
	GlassFrame->SheetOfGlass = true;
}
GlassFrameGlassFrame->Enabled を true にするとグラスフレーム拡張モードとなり、GlassFrame->SheetOfGlass も true にすることで、クライアント領域全体でガラス効果を得られる。GlassFrame->SheetOfGlass を false にして、GlassFrame->Bottom などのプロパティで任意の範囲でガラス効果を得るようにすることもできる。

Windows API で言えば、Desktop Window Manager の DwmExtendFrameIntoClientArea() をコールした状態と思われる。
カンパのお願い
Amazon でお買い物の際は、下記で検索して頂けたら幸いです。
記事検索
最新コメント
月別アーカイブ
  • ライブドアブログ