SourceTree for Windows で使われているWPFライブラリ

Free Mercurial and Git Client for Windows and Mac | Atlassian SourceTreeをダウンロードしてみたら、全然知らないWPFライブラリが使われてるので、備忘録を兼ねてメモ。

64bit Windows が本格的に普及する前にネイティブ DLL を P/Invoke する .NET アプリがやっておくべきこと

以前からいろいろ調べたり質問したりしてた、ネイティブ DLL を P/Invoke する .NET アプリを 64bit Windows でも動作させる方法がようやく納得できる形で見つかったので、備忘録を兼ねてメモしておく。

P/Invoke したいネイティブDLL

  • x86用DLLとx64用DLLが用意されているネイティブDLL
  • System.Data.SQLite ※正確にはネイティブDLLではなく、ネイティブDLLを P/Invoke するアセンブリ

QA@ITでした質問

参考にしたサイト

なぜ必要か

まだまだ 32bit のサポートが必要だが、現在販売されている個人向けPCは 64bit 向けが大半を占めている。サーバーOSに至っては、 2008R2から 64bit しか用意されていない。

結論

  1. プロジェクトはターゲットプラットフォームを AnyCPU にする。
  2. x86用ネイティブDLLをx86ディレクトリに配置し、ビルドアクションを「コンテンツ」に設定する。さらに出力ディレクトリにコピーするようにしておく。
  3. x64用ネイティブDLLをx64ディレクトリに配置し、同様に設定する。
  4. SetDllDirectory でDLL検索パスを切り分ける。
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Hoge {
    /// <summary>
    /// ネイティブDLLのロード先ディレクトリを追加するユーティリティクラスです。
    /// </summary>
    public static class DllSearchPath {
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool SetDllDirectory(string lpPathName);
        /// <summary>
        /// ネイティブDLLのロード先ディレクトリを追加します。
        /// </summary>
        /// <param name="path">ロード先ディレクトリ</param>
        public static void Add(string path) {
            SetDllDirectory(path);
        }
        /// <summary>
        /// x86とx64のネイティブDLLのロード先ディレクトリを追加します。
        /// </summary>
        /// <param name="x86path">x86用ロード先ディレクトリ</param>
        /// <param name="x64path">x64用ロード先ディレクトリ</param>
        public static void Add(string x86path, string x64path) {
            int bitWidth = IntPtr.Size * 8;
            switch (bitWidth) {
                case 64:
                    Add(x64path);
                    break;
                case 32:
                    Add(x86path);
                    break;
                default:
                    throw new NotSupportedException();
            }
        }
    }
}

#Program.cs
DllSearchPath.Add("path/to/x86", "path/to/x64");

ちなみに、 x86向けネイティブDLLしか用意されていない場合、ターゲットプラットフォームを x86 にしておく方が無難だろう。

動作原理

AnyCPU に設定しておけば、64bit Windows では 64bit プロセスとして動作し、32bit Windows では 32bit プロセスとして動作する。
DllSearchPath.Add で 32bit 向けパスと 64bit 向けパスをあらかじめ指定しておけば、自動的にプロセスを判断してロードするDLL
が配置されるディレクトリを決定してくれる。

この方法であれば、わざわざプロジェクトを x86 向けと x64 向けに2つ用意する必要はなくなる。

System.Data.SQLite

参考URLにあるとおり、

  • 32bit ・・・ Precompiled Statically-Linked Binaries for 32-bit Windows
  • 64bit ・・・ Precompiled Statically-Linked Binaries for 64-bit Windows

をダウンロードしておけばいい。選定理由はこんな感じ。

  • Visual C++ 2008 SP1 runtime はstaticリンクの方が無難。当然ランタイム分容量が増えるが、今更気にするサイズではないだろう。
  • mixed-mode assembly はネイティブDLLをアセンブリに含んでしまうので、ターゲットプラットフォームが x86 または x64 になってしまう。参照アセンブリは静的に決まるため、ロードするネイティブDLLを動的に切り分けられない。

GUIテストフレームワークwhiteがすごい

http://white.codeplex.com/

たとえばこんなWindowsアプリがあったとして、

namespace WindowsFormsApplication1 {
    public partial class Form1 : Form {
        public Form1() {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e) {
            this.textBox1.Text = "Hello white";
        }
    }
}

こんなコードでbutton1のクリック結果をNUnitでテスト可能。

using System;
using NUnit.Framework;
using White.Core;
using White.Core.UIItems;

namespace WindowsFormsApplication1Test {
    [TestFixture]
    public class Class1 {
        [Test]
        public void Test() {
            var exe = @"D:\Work\WindowsFormsApplication1\WindowsFormsApplication1\bin\Debug\WindowsFormsApplication1.exe";
            using (var app = White.Core.Application.Launch(exe)) {
                var win = app.GetWindow("Form1");
                var button = win.Get<Button>("button1");
                button.Click();
                Assert.AreEqual("Hello white", win.Get<TextBox>("textBox1").Text);
            }
        }
    }
}

さすがにDataGridとかはテストできなそうだけど、これは調査しないとダメだな。WindowsFormsだけじゃなくて、WPFとかSilverlightもテストできるっぽいし。

ちなみに上記のコードをコンパイルするためには、テストアセンブリにWhite.Core.dllの参照が必要。

EnumをComboBoxに表示する。

アセンブリHogeAsmに含まれるEnum、HogeEnumをComboBoxに表示。

<UserControl
    x:Class="Hoge.HogeView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:System="clr-namespace:System;assembly=mscorlib"
    xmlns:hoge="clr-namespace:Hoge;assembly=HogeAsm">
    <UserControl.Resources>
        <ObjectDataProvider x:Key="EnumList" MethodName="GetValues" ObjectType="{x:Type System:Enum}">
            <ObjectDataProvider.MethodParameters>
                <x:Type TypeName="hoge:HogeEnum"/>
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
    </UserControl.Resources>

    <Grid>
        <ComboBox ItemsSource="{Binding Source={StaticResource EnumList}}" />
    </Grid>
</UserControl>

Windowsサービスでlog4netを使うための覚え書き

最近作っていたWindowsアプリをWindowsサービスに作り替えてみたら、log4netで出力していたイベントログが出力されなくなってしまった。最初は権限関係だろうと思っていろいろ試してみたがさっぱり解決できなくて、1週間くらい無駄にしてしまった。
原因はアプリケーション構成ファイルのパスが取得できなかったせい。プログラムフォルダのパスを取得するためにAssembly.GetCallingAssembly/GetExecutingAssemblyを使うと、「C:\Windows\System32」になり、実際のパスが取得できない(Windowsサービス特有か?)。System.Windows.Forms.Application.ExecutablePathなど、他の手段でも取得できなかった。
Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.Info.DirectoryPathはなぜか取得できるので、ほかの手段が見つかるまではMicrosoft.VisualBasicを参照に追加することにした。

string exename = Assembly.GetExecutingAssembly().GetName().Name + ".exe.config";
Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase MyApp
 = new Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase();
string configPath = Path.Combine(MyApp.Info.DirectoryPath, exename);

FileInfo info = new FileInfo(configPath);
log4net.Config.XmlConfigurator.Configure(log4net.LogManager.GetRepository(),
info);
20080508追記
AppDomain.CurrentDomain.SetupInformation.ConfigurationFileっていうのがあるのを最近知った。これで問題ないみたい。
FileInfo info = new FileInfo(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile);
log4net.Config.XmlConfigurator.Configure(log4net.LogManager.GetRepository(), info);

WPFでコントロールをアニメーションさせてみる

Expression Blend を使えばアニメーションは簡単にできるんだけど、アニメーション中は Width プロパティとかに Auto を指定できないので微妙にめんどくさい(ちなみにコードだと Auto は Double.NaN で指定できる)。
なので、細かい操作は Visual Studio を使ってコードを書いた方が楽みたい。

そんなわけで、以下アニメーションの覚え書き。

1つのコントロールの1つのプロパティをアニメーション

  1. System.Windows.Media.Animation.DoubleAnimation クラスみたいな Animation クラスを生成する。コンストラクタで開始値・目標値・アニメーション時間を指定しておく。 Animation クラスは、変化させる型に応じたものを選択すること。特殊なのは Enum で、 ObjectAnimationUsingKeyFrames クラスを使用する。
  2. 変化させるコントロールの System.Windows.Media.Animation.Animatable#BeginAnimation メソッドでアニメーションを開始する。引数は、変化させるプロパティを表す依存プロパティと、上で生成した Animation クラス。

1つのコントロールの2つ以上のプロパティをアニメーション

アプローチとしては2つ。

  • Animation クラスを複数用意して、BeginAnimation を必要なだけ呼ぶ。
  • System.Windows.Media.Animation.StoryBoard クラスを使う。

MS的には後者が推奨なようですな。具体的な方法はこんな感じ。

  1. Animation クラスを作成。
  2. StoryBoard#SetTargetProperty で、上記で生成した Animation クラスと、変化させるプロパティを表す依存プロパティを指定する。Animatable#BeginAnimation とは引数の順番が入れ替わっていて、依存プロパティを指定するのも PropertyPath 経由になっていてわかりにくい・・・。なんで依存プロパティじゃ駄目なんだろう?
  3. 変化させるすべてのプロパティに対応する Animation クラスを生成する。
  4. StoryBoard クラスを生成して、Children プロパティに生成した Animation クラスを追加する。
  5. 変化させるコントロールの Animatable#BeginStoryboard メソッドでアニメーションを開始する。引数は上記で生成した StoryBoard クラス。

2つ以上のコントロールをアニメーション

これがサンプルが見つからなくて、かなりはまった。方法としては、1つのコントロールの2つ以上のプロパティをアニメーションとほとんど同じ。違いは StoryBoard#SetTargetName で、変化させるコントロールを指定するところ。オブジェクトを指定するんじゃなくて、文字列で名前を指定するのが嫌な感じ。

  1. Animation クラスを作成。
  2. StoryBoard#SetTargetProperty で、上記で生成した Animation クラスと、変化させるプロパティを表す依存プロパティを指定する。
  3. StoryBoard#SetTargetName で、上記で生成した Animation クラスと、変化させるコントロール名を指定する。
  4. 変化させるすべてのコントロール・プロパティに対応する Animation クラスを生成する。
  5. StoryBoard クラスを生成して、Children プロパティに生成した Animation クラスを追加する。
  6. Animatable#BeginStoryboard メソッドでアニメーションを開始する。引数は上記で生成した StoryBoard クラス。メソッドを呼ぶインスタンスは何でもいいみたい。

サンプルコード

WindowにCanvasを置いて、その上にボタンを3つ(left 、 center 、 right)を置いた場合。

using System;
using System.IO;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Navigation;

namespace SampleWPF {
    public partial class Window1 {
        public Window1() {
            this.InitializeComponent();
        }
        private void left_Click(object sender, RoutedEventArgs e) {
            //1つのコントロールの1つのプロパティをアニメーション
            Duration d = new Duration(TimeSpan.FromMilliseconds(100));
            DoubleAnimation a = new DoubleAnimation(300, d);
            left.BeginAnimation(Canvas.WidthProperty, a);
        }
        private void center_Click(object sender, RoutedEventArgs e) {
            //1つのコントロールの2つ以上のプロパティをアニメーション
            Storyboard s = new Storyboard();
            Duration d = new Duration(TimeSpan.FromMilliseconds(100));

            DoubleAnimation a1 = new DoubleAnimation(50, d);
            Storyboard.SetTargetProperty(a1, new PropertyPath(Canvas.WidthProperty));
            s.Children.Add(a1);

            DoubleAnimation b1 = new DoubleAnimation(50, d);
            Storyboard.SetTargetProperty(b1, new PropertyPath(Canvas.HeightProperty));
            s.Children.Add(b1);

            center.BeginStoryboard(s);
        }
        private void right_Click(object sender, RoutedEventArgs e) {
            //2つ以上のコントロールをアニメーション
            Storyboard s = new Storyboard();
            Duration d = new Duration(TimeSpan.FromMilliseconds(100));

            DoubleAnimation a1 = new DoubleAnimation(100, d);
            Storyboard.SetTargetProperty(a1, new PropertyPath(Canvas.WidthProperty));
            Storyboard.SetTargetName(a1, "left");
            s.Children.Add(a1);

            DoubleAnimation b1 = new DoubleAnimation(100, d);
            Storyboard.SetTargetProperty(b1, new PropertyPath(Canvas.HeightProperty));
            Storyboard.SetTargetName(b1, "left");
            s.Children.Add(b1);

            DoubleAnimation a2 = new DoubleAnimation(500, d);
            Storyboard.SetTargetProperty(a2, new PropertyPath(Canvas.WidthProperty));
            Storyboard.SetTargetName(a2, "right");
            s.Children.Add(a2);

            DoubleAnimation b2 = new DoubleAnimation(500, d);
            Storyboard.SetTargetProperty(b2, new PropertyPath(Canvas.HeightProperty));
            Storyboard.SetTargetName(b2, "right");
            s.Children.Add(b2);

            this.BeginStoryboard(s);
        }
    }
}

WPF雑感

ここしばらく WPF をいじってるんだけど、情報が少なくてさっぱり進まない。GUI フレームワークが大幅に変更されたから、当然といえば当然なんだけど、より一層理解を妨げてくれるのが依存プロパティ。こいつのおかげでインスタンスメンバとクラスメンバに情報が分散しているため、思ったようにコーディングできない。
まあ依存プロパティとかは、動的言語の影響なんだろうけど。確かに必要ないプロパティを定義せずに済むのはメリットと言える。でも C# では無理やり感が漂っていて、あまり好きになれそうにない。DLR とかで少しはましになるのかな?