Linq to P/Invoke


たいそうなタイトルにしてみましたが、やろうとしていることは以下の点です。

EnumWindows 関数で効率的に Linq を使いたい。

背景

そもそも、Win32API には IEnumerable<T> や Linq といった概念がないいので、
EnumWindows 関数はコールバックによる列挙を行います。

例えばすべてのトップレベルウィンドウを取得するためにはこのような処理になります。

public static List<IntPtr> EnumWindow()
{
    var list = new List<IntPtr>();

    User32.EnumWindows(
        (hwnd, _) =>
        {
            list.Add(hwnd);
            return true;
        },
        IntPtr.Zero);

    return list;
}

しかし、この方法ではすでに一度列挙が確定しているため、以下のような処理では無駄が発生します。Take() や First() のようなクエリは最後まで列挙する必要がないため特に差が出ます。

var hwndArray = EnumWindow()
    .Where(x => User32.IsWindowVisible(x))
    .Take(5)
    .ToArray();

複雑かつ効率的な処理をしようとすると、どんどんネストが深くなってしまいます。

var list = new List<IntPtr>();
var count = 5;
User32.EnumWindows(
    (x, _) =>
    {
        if (User32.IsWindowVisible(x))
        {
            list.Add(x);
            return (count-- > 0);
        }
        else
        {
            return true;
        }
    },
    IntPtr.Zero);

そこで、コールバックを Linq 的に組み上げる方法を考えました。

実装

IObservable<T> っぽいインターフェースを設けます。
(いい名前が思いつきませんでした。)

public interface IGetResult<out TResult>
{
    TResult GetResult();
}

public interface IEnumObservaber<in TIn>
{
    bool OnNext(TIn item);
}

public interface IEnumObservable<out TOut> : IGetResult<IEnumerable<TOut>>
{
    void Subscribe(IEnumObservaber<TOut> observer);
}

このインターフェースを使って EnumWindow をこのように定義します。

public static T EnumWindows<T>(Func<IEnumObservable<IntPtr>, IGetResult<T>> func)
{
    var start = new StartQuery<IntPtr>();
    var end = func(start);
    User32.EnumWindows((hwnd, _) => start.OnNext(hwnd), IntPtr.Zero);
    return end.GetResult();
}

ポイントは IObservable<T> と違い OnNext メソッドに bool の戻り値を持たせ、それをコールバックの戻り値にします。
この戻り値が Take() や First() のように列挙を途中で終了させたいときに必要となります。

細かい実装は省きますが、クエリのインターフェースはこの通りです。

public static class EnumObservableEx
{
    public static IEnumObservable<T> Where<T>(this IEnumObservable<T> source, Func<T, bool> func);

    public static IEnumObservable<T2> Select<T1, T2>(this IEnumObservable<T1> source, Func<T1, T2> func);

    public static IEnumObservable<T> Take<T>(this IEnumObservable<T> source, int count);

    public static IGetResult<T> First<T>(this IEnumObservable<T> source);
}

使い方

以上のことを使って EnumWindows() で効率的な Linq を構築することができました。

var procId = Process.GetCurrentProcess().Id;
string titile = User32
    .EnumWindows(x => x
        .Where(y =>
        {
            User32.GetWindowThreadProcessId(y, out var id);
            return id == procId;
        })
        .Where(y => User32.IsWindowVisible(y))
        .Where(y => User32.GetWindowTextLength(y) > 0)
        .Select(y => User32.GetWindowText(y))
        .First());

var dic = User32
    .EnumWindows(x => x
        .Where(y =>
        {
            User32.GetWindowThreadProcessId(y, out var id);
            return id == procId;
        })
        .Where(y => User32.IsWindowVisible(y)))
    // ここから下は Linq to object
    .ToDictionary(x => x, x => User32.GetWindowText(x));

ここでは EnumWindow() について話しましたが、Win32API には似たような関数がほかにもあると思うので、応用できると思います。