CSharp

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' のマッピングの一部ではありません。メンバーは継承階層のルートより上のメンバーですか?」というようなエラーが発生してしまう。

サンプルコード

以上をまとめたものを、サンプルコードとして公開する。

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

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

参考資料

更新履歴

  • 2019/04/09 初版。






















フォームから 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 年以上前に登場したのに今がこのような状況と言うことは、今後も改善しない可能性が高いのかもしれない……。

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

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 でお買い物の際は、下記で検索して頂けたら幸いです。
記事検索
最新コメント
  • ライブドアブログ