2015/04/29

VBのループ中のローカル変数

今更という気はしますが、VBで引っ掛かってしまったので、記録として。

VBとC#は兄弟言語ですが、C#から見ると引っ掛かりやすい点があって、ぱっと思いつく限り以下のようなものがあります。
  • Nothingの意味(必ずしもnullではない)
  • 配列のコンストラクタの要素数
  • 整数と実数の自動変換(とくに除算時)
  • オーバーロード解決の優先順
これらとは別に、あまり意識してなかった違いとして、メソッド中のローカル変数の初期値の扱いがあります。C#ではローカル変数を宣言後、値を与えないまま使おうとするとエラーとなってビルドできないのに対し、VBでは値を与えなくてもその型の既定値を使う形でビルドできます。

以下のメソッド中のローカル変数valueについて、LocalVariableCase0では初期値としてFalseを代入しているのに対し、LocalVariableCase1では初期値を代入していませんが、Boolean型の既定値がFalseなので同じ結果になります。
ここまでは問題ありませんが、これをループにすると違いが出ます。

以下のLocalVariableCase2ではループが回る度にvalueはFalseに初期化されますが、LocalVariableCase3ではループしても前回のループで与えられたTrueが残ってしまいます。
ここで試しにLocalVariableCase3のループ中の処理を別メソッドに切り出すと、valueのスコープはそのメソッド中に限定されてLocalVariableCase2と同じ結果になります。
つまり、同一メソッド内のループ中であればループの度にローカル変数を新たに宣言したつもりでも、そうはならず前回の値が維持されます。

これは少しトリッキーというか、予想とは違っていて驚いたわけですが、そういえばVBを勉強し始めたときに変数に初期値を与えておかないと予期しない動作になって危ないと読んだような記憶がありますが、すっかり忘れてました。

というか、何でもかんでも初期値を与えるのもカーゴカルトみたいで無駄だなと思って削っていたら引っ掛かってしまったわけですが、また忘れそうなので書いておきます。

[追記] IL

これだけでは何がどうなっているか明瞭でないので、LocalVariableCase3のILをIL DASMで見ると、こうなっています。


これも今更ですが、ローカル変数はVBでのメソッド中の位置に関わらずILでは冒頭で宣言される形になっています。そこでVBでもローカル変数を冒頭で宣言するように変えたメソッドを作り、そのILを見たのが以下です。


見ての通り、ILは全く同じになります。つまり、ローカル変数の宣言はその位置で変数が初めて確保されることを意味しないので、初期値にリセットするにはきちんと代入しないとダメということですね。

2015/04/15

WinRTでの設定の保存

WinRTでの設定については、どこに、どのタイミングで保存するか考える必要があるわけですが、
  1. 設定が変更される都度、保存するようにした方が確実。
  2. 永続的にするにはApplicationDataのLocalSettingsかRoamingSettingsに保存するのが便利。
  3. 設定用クラスの設定用プロパティのアクセサー内でこれらにアクセスするようにすると管理がすっきりする。
  4. そもそもLocalSettingsかRoamingSettingsを設定用プロパティのバッキングストアにしてしまえばいい。
ということになって、@tmytさんがそういう例を出されてます。
この方法をベースに自作列挙型や自作クラスも保存できるように考えて、以下のようになりました。まずは設定用の基本クラス。
自作列挙型は基になる型に変換した上でそれを保存するように、自作クラスはDataContract属性を付ける前提でJSONにシリアライズした形で保存するようにしています。

これを継承した設定用クラスの例。
頻繁に参照されるプロパティでコストが気になる場合はアクセサー内でキャッシュするようにすればいいかと思います。

とくに目新しいことはないですが、一つの定型的方法として。

[修正]

GetValueメソッド中で自作クラスの値がまだ存在しなかった場合の処理を修正。

2015/02/22

WPFでカラープロファイルを添付プロパティで取得する

2/21のめとべや東京#7での@veigrさんのセッション「カラーマネジメントシステムの概要とカラマネプログラミング初歩」はもやっと感じていたカラーマネジメントの基礎がようやく理解でき、かつ実践的なコードもあるという、自分的にとても有意義なものでした。
セッション中でも触れられていたWPFの関連APIの説明も参考になります。Microsoftからの説明があまりないので、こういう実際の動作まで調べたものは有り難いです。
WPFに関しては、モニターのカラープロファイルを取得する部分を除けば標準APIの枠内で対応でき、さほどハードルは高くないので(Windowsフォトビューアーのように同じ画像内で同時に違うカラープロファイルを適用するなどという変態的なことを目指さなければ)、画像の色を真面目に表示するアプリを作る場合は必見だと思います。

で、モニターのカラープロファイルについては、マルチモニター対応のためには各Windowが現在属しているモニターのものが必要になるので、これをWindowの添付プロパティで取得するものを書いてみました。
この添付プロパティを持つクラスはColorProfilePropertyですが、
  • Freezableを継承しているのは、バインディングターゲットとなれるようにするため。DependencyObjectだとバインディングソースにしかなれないので、バインディングを張るときの自由度が落ちる。
  • 添付プロパティの値を自分自身のインスタンスとしているのは、このクラス中の依存関係プロパティを複数、自由に参照できるようにするため。"AttachedProperty"という名前にとくに意味はない。
モニターのカラープロファイルのファイルパスを取得するメソッドは以下のとおりです。
private string GetColorProfilePath(Visual sourceVisual)
{
  var source = PresentationSource.FromVisual(sourceVisual) as HwndSource;
  if (source == null)
    return null;

  var monitorHandle = NativeMethod.MonitorFromWindow(
    source.Handle,
    NativeMethod.MONITOR_DEFAULTTO.MONITOR_DEFAULTTONEAREST);

  var monitorInfo = new NativeMethod.MONITORINFOEX
  {
    cbSize = (uint)Marshal.SizeOf(typeof(NativeMethod.MONITORINFOEX))
  };

  if (!NativeMethod.GetMonitorInfo(monitorHandle, ref monitorInfo))
    return null;

  IntPtr deviceContext = IntPtr.Zero;

  try
  {
    deviceContext = NativeMethod.CreateDC(
      monitorInfo.szDevice,
      monitorInfo.szDevice,
      null,
      IntPtr.Zero);

    if (deviceContext == IntPtr.Zero)
      return null;

    // First, get the length of file path.
    var lpcbName = 0U;
    NativeMethod.GetICMProfile(deviceContext, ref lpcbName, null);

    // Second, get the file path using StringBuilder which has the same length. 
    var sb = new StringBuilder((int)lpcbName);
    NativeMethod.GetICMProfile(deviceContext, ref lpcbName, sb);

    return sb.ToString();
  }
  finally
  {
    if (deviceContext != IntPtr.Zero)
      NativeMethod.DeleteDC(deviceContext);
  }
}
この中でもコアなのはGetICMProfile関数を使う部分ですが、ファイルパスを格納するlpszFilename(第3引数)のサイズを決めるのに少しややこしいことをしていて、初めにこれにnullを入れて実行するとlpcbName(第2引数)にlpszFilenameのサイズが入ってくるので、これを使ってStringBuilderを用意し、lpszFilenameに入れて再度実行することで取得しています(@veigrさんのサンプルのとおり)。

2回実行するのはどうも……という場合は、lpcbNameを通常あり得るパスの最大長である260に決め打ちしても動きます。
var lpcbName = 260U;
var sb = new StringBuilder((int)lpcbName);
NativeMethod.GetICMProfile(deviceContext, ref lpcbName, sb);
万一サイズが足りなかった場合を考えて戻り値を取って処理を重ねることもできます……細かい話ですが。

サンプルアプリを実行すると、カラープロファイルのファイルパスが表示されます。

モニター間をまたいでWindowを移動かリサイズするとファイルパスが変わり、画像がColorConvertedBitmapで再描画されます。ただ、この処理が結構重いので、滑らかに切り替わるというわけでもないです。まあ移動かリサイズを止めたタイミングで反映させるようにすれば、目立たないだろうと思いますが。

実はこれまでセカンダリモニターのカラープロファイルは適当で済ませてましたが、きちんと設定して試してみるとプライマリモニターとはっきり分かる違いがあるのを発見して、意外と馬鹿にならないです。