C#

カーソル(マウスポインタ)

フォーマット

カーソル・ファイル(*.cur)とアイコン・ファイル(*.ico)のフォーマットは同じです。カーソルの場合はICONDIRのidTypeが2で、ICONDIRENTRYのwPlanesとwBitCountがHotSpotのXとYになります。

アニメーションカーソル・ファイル(*.ani)はRIFFフォーマットです。正確なフォーマットはよくわかりませんが、RIFF:ACONの下にanihとLIST:framがあれば良いのかなと思います。

カーソルの影

ユーザーはカーソルに影を付けるかどうかをマウスのプロパティで設定しています。しかしプログラムでは、どうやっても影を付けられません。OSに用意されている機能をアプリケーションから呼び出せないなんて不条理ですが、しかたありません。影の付いた半透過のカーソルを用意しておくしかないでしょう。Visual Studioのリソースエディタでは、そのような半透過のカーソルを作ることはできないので、自分で何とかするしかありません。

カーソルに影を付けるかどうかの設定をSystemInformation.IsDropShadowEnabledで調べられますが、影の色や方向や距離やぼかし具合など他の設定は見当たらないので、適当な感じの影付きのカーソルを用意しておくのかなと思います。

色が反映されない

リソースからCursorクラスのコンストラクタでカーソルを作る場合、色は反映されず、白黒になります。GetIconInfo()で調べてみると、組み込みのカーソルではちゃんとhbmColorとhbmMaskにビットマップがセットされています。

cursor-fig-1
fig.1: 組み込みのカーソルのICONINFO。てっきりhbmColorに影が入っていると思っていたのに、半透過のピクセルはありませんでした。そうすると、カーソルを描画するときに影を付けているのだろうと思いますが、Cursorクラスのコンストラクタで作ったカーソルには影が付きません。

Cursorクラスのコンストラクタでリソースから作成したカーソルをGetIconInfo()で調べてみると、hbmColorがNULLで、hbmMaskしか作られていないようです。しかもicANDとicXORが上下にくっついています。何かおかしい気がしますが、ヘルプによると、白黒の場合には、これで良いらしいです。でもCursorクラスのコンストラクタでリソースからカーソルを作ると、白黒以外でもこうなってしまい、色情報が失われるので、1ビットカラーのパレットを変更した場合でも、4ビット以上のカラーでも、みな白黒になってしまいます。半透過も無理です。

cursor-fig-2
fig.2: Cursorクラスのコンストラクタでリソースから作成したカーソルのICONINFO。まったく不可解な仕様です。

ICONINFOにhbmColorとhbmMaskを設定してCreateIconIndirect()関数を使えば、カラーのカーソルを作ることができます。

cursor-fig-3
fig.3: CreateIconIndirect()関数で作成したカーソルのICONFINFO。Cursorクラスでも、こうなってほしかった。作成に使用したビットマップについては、APIのほうでコピーが作られるようで、カーソルを作った直後に削除しても大丈夫ですが、カーソルのハンドルのほうは管理が面倒です。

Cursorクラスのコンストラクタには、動作としても仕様的にも不具合があるような気がします。リソースからCursorクラスのコンストラクタでカーソルを作る場合、カラーのカーソルを使えないので、あきらめたほうが良いでしょう。

リソースエディタ

リソースエディタで1ビットカラーのカーソルを編集するときにパレットの色を指定できませんが、パレットを保存し、パレットを編集し、編集したパレットを読み込めば、パレットを変更することができます。2色だけなら、バイナリのパレット・ファイル(*.pal)を編集するのは難しくありません。でも、やはりCursorクラスのコンストラクタを使うと、色は反映されず白黒になります。

カーソル・ファイル(*.cur)には複数のカーソルを保存できますが、リソースエディタでは32ビットのイメージタイプを作ることができず、同じイメージタイプを複数保存することもできないし、Cursorクラスには、複数のイメージタイプを使い分けるような方法も用意されていません。1つのファイルにつき、1個のカーソルを保存して使うしかないでしょう。

アイコンやビットマップからカーソルを作る

アイコンのハンドルからカーソルを作ることはできます。アイコンのハンドルはBitmapからも作ることができるので、Bitmapからカーソルを作ることもできます。

// SomeBitmapというビットマップリソースがあると仮定して・・・
using (Bitmap SomeBitmap = Properties.Resources.SomeBitmap)
{
    IntPtr hIcon = SomeBitmap.GetHicon();
    this.Cursor = new Cursor(hIcon);
}

この場合、ホットスポットは画像の中心に設定されます。おそらく、ICONINFO構造体の仕様でアイコンの場合はxHotspotとyHotspotをアイコンの中心に設定することになっているためでしょう。画像が大きくなってしまいますが、中心がホットスポットと想定して画像を作っておけば、カラーのカーソルも半透過のカーソルも問題なく使えます。画像はだいたいBitmapになるので、PNGなどでも大丈夫です。

ただし、このような方法で作成したCursorクラスのインスタンスをDispose()してもアイコンのハンドルは削除されないので、要らなくなったらDestroyIcon()を使ってアイコンのハンドルを削除しておくべきです。

APIを使うのが良さそう

カーソルリソースを使ってカラーカーソルやアニメーションカーソルを扱うなら、CreateIconFromResource()やLoadCursorFromFile()を利用すると良さそうです。CreateIconFromResource()では、なぜか普通のカーソルの作成に失敗することがあるので、一旦ファイルに書き出してから、LoadCursorFromFile()で作成するほうが良いかもしれません。

using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Forms;


public static class ResourceCursors
{
    [DllImport("user32.dll")]
    private static extern IntPtr LoadCursorFromFile(string lpFileName);

    [DllImport("user32.dll")]
    private static extern int DestroyCursor(IntPtr hCursor);

    private static readonly IntPtr hAniCur;
    public static readonly Cursor AniCur;
    // ・
    // ・
    // ・

    // hAniCurはAniCur.Handleと同じなので、
    // AniCurだけ管理するほうが効率的かもしれませんが、
    // プログラムの別の箇所でAniCurがDispose()されると
    // hAniCurを削除できなくなってしまいます。

    static ResourceCursors()
    {
        // AniCurというカーソルリソースがあると仮定して・・・
        // カーソルファイルはリソースに追加されると
        // ファイルリソースになるので、バイト配列で取得します。
        AniCur = new Cursor(
            hAniCur = CreateCursor(Properties.Resources.AniCur));
        // ・
        // ・
        // ・

        Application.ApplicationExit += Application_Exit;
    }

    private static void Application_Exit(object sender, EventArgs e)
    {
        DeleteCursor(AniCur, hAniCur);
        // ・
        // ・
        // ・
    }

    private static IntPtr CreateCursor(byte[] Resource)
    {
        string TempFileName = Path.GetTempFileName();

        try
        {
            File.WriteAllBytes(TempFileName, Resource);
            return LoadCursorFromFile(TempFileName);
        }
        finally
        {
            File.Delete(TempFileName);
        }
    }

    private static void DeleteCursor(Cursor Cur, IntPtr hCursor)
    {
        try
        {
            Cur.Dispose();
        }
        catch
        {
        }

        try
        {
            DestroyCursor(hCursor);
        }
        catch
        {
        }
    }
}

結局

いまのところ、カーソルはデフォルトのやつで充分でしょう。自作のカーソルを使おうとすると影が付かなくて変になるし、影を付けようとすると要らない苦労をさせられるし、カーソルのハンドルの管理もいちいち面倒で、割に合いません。もう最初からカーソルについては、気にせず、何もせず、放っておいたほうが良いです。

(2019/03/27 初稿)