午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

表示しているマウスカーソル画像を取得表示してみた、WinAPIとWPF

f:id:gogowaten:20201120150039g:plain
テスト中
f:id:gogowaten:20201120150533p:plain
テスト用アプリ

0.1秒ごとにマウスカーソル画像をいくつかの方法で取得して表示している
WinAPIを使ってカーソルのハンドルやカーソル画像のハンドルを取得して
WPF(.NET)の
System.Windows.Interop.Imaging.CreateBitmapSourceFromHIcon
System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap
を使ってBitmapSourceを作成している

マウスカーソルのハンドルから画像作成
[DllImport("user32.dll")]
private static extern IntPtr GetCursor();
GetCursor()で取得したハンドルを
Imaging.CreateBitmapSourceFromHIconにわたす
例えば
Imaging.CreateBitmapSourceFromHIcon(GetCursor(), Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
これだけでカーソル画像が取得できる、けど
f:id:gogowaten:20201120153355p:plain
形が違う
形が違う、タイミングによっては同じになることもあるけど、当てにならない

アイコンの情報を取得する

f:id:gogowaten:20201120153804p:plain
GetIconInfo()
マウスカーソルのハンドルをGetIconInfoに渡すと、いろいろ情報が得られる
その中の、マスク画像のハンドルhbmMaskと、カラー画像?のハンドルhbmColorを
System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmaに渡すとBitmapSourceが得られた
f:id:gogowaten:20201120154816p:plain
GetIconInfo()より
でも、これも形が違う

GetCursorInfo()を使う

f:id:gogowaten:20201120155230p:plain
GetCursorInfo()
いろいろカーソルの情報が得られる
その中のhCursorはカーソルのハンドルは、さっきのGetCursor()で得られるカーソルのハンドルとは別のようで、結果は
f:id:gogowaten:20201120162454p:plain
GetCursorInfo()より
表示されているカーソルと同じのが取得できた!
さらに
f:id:gogowaten:20201120162951p:plain
エクセルのシート上でのカーソル
エクセルのシート上での十字形のカーソルも正しく取得できた

f:id:gogowaten:20201120163701p:plain
図形の上のカーソル
いいね

f:id:gogowaten:20201120163922p:plain
自作したマウスカーソル
自作した大きめなマウスカーソルも取得できている

f:id:gogowaten:20201120165231p:plain
カラー+アニメーションなカーソル
0.1秒ごとの取得だから少しラグがあるけど、アニメーションするカーソルも取得できてたのは意外だった

マスクが必要なカーソル?

f:id:gogowaten:20201120165638p:plain
テキストエディタとかのカーソル
テキストエディタとかテキスト編集中のI型カーソルは残念ながら取得できていない、影も形もない完全透明画像が取得されている
それっぽいのが取得できたのは
GetCursorInfo()で得たマウスカーソルのハンドルを、GetIconInfo()に渡して得られるマスク画像のハンドルから作成する方法だった
f:id:gogowaten:20201120170459p:plain
マウスカーソルのマスク画像?
白背景に白画像はわかりにくかったので

f:id:gogowaten:20201120204432p:plain
マウスカーソルのマスク画像

32x64ピクセルの縦長の画像の下半分にマウスカーソルが表示されている
マスク画像ってのは他の画像と重ね合わせて使うみたいなんだけどねえ、まだわかってない

今回試したことでわかったのは
GetCursorInfoを使えばいい
これで取得できないカーソルはGetCursorInfoとIconInfoで得られるマスク画像を使ってなんとかする

参照したところ
C# - Capturing the Mouse cursor image - Stack Overflow
https://stackoverflow.com/questions/918990/c-sharp-capturing-the-mouse-cursor-image
c# - j_C#_j-Mousecursorイメージのキャプチャ
https://python5.com/q/ukmbkppc
C# - マウスカーソルイメージのキャプチャ
https://www.366service.com/jp/qa/24498664dd5f63c42b15614906b07ff2
このあたりを見ると、マスク画像の上半分をAND、下半分をXORで使うとあって、その処理はAPIBitBlt()を使っている

API 関数解説
https://www.tokovalue.jp/function/BitBlt.htm
こちらを見ると、BitBltにはオプションがあって、SRCERASEでAND演算、SRCINVERTでXOR演算できるみたいなんだけど、BitBltはDCとかビットマップオブジェクトとか、よくわからんから普通の画像処理としてWPFで行いたいんだよねえ

カーソルと重なった部分の色

f:id:gogowaten:20201120185336p:plain
文字とカーソルが重なった部分
ここを拡大してみる

f:id:gogowaten:20201120185446p:plain
文字とカーソルが重なった部分を拡大
文字がない部分は白が黒に変化している、つまり色が反転している、これはわかる、白地に白のカーソルのままじゃ見分けがつかない

一方、文字の紫の部分は緑色になっている
それぞれのRGB値を確認したら
167, 70, 255 紫
88, 185, 000 緑
だった
この辺のマスク処理は次回


ScreenToGifでスクショ

f:id:gogowaten:20201120211024p:plain
ScreenToGif

今まであるの気づかなかった
メニューのホームのコピーをクリック、もしくはCtrl+C押すと、表示しているフレームの画像がクリップボードに格納されるってあったんだけど、実際にはクリップボードには格納されずに、ScreenToGifのClipboardフォルダに1枚毎別々のフォルダが作成されてそこに保存された
f:id:gogowaten:20201120211616p:plain
png形式で保存された
…1ファイルごとに1フォルダはあかんやろwどっかに設定があるのか新しいバージョンでは直ってるのかな、でもマウスカーソルつきのスクリーンショットが撮れるのはとても便利

ベンチマークの結果

f:id:gogowaten:20201120213424p:plain
ベンチマーク結果
f:id:gogowaten:20201120213439p:plain
Rewrite ちはやローリングWE (Ver.1.10)結果
普通だ!

ステキ!!?な環境




2020WPF/20201120_マウスカーソル画像取得
github.com

MainWindow.xaml

<Window x:Class="_20201120_マウスカーソル画像取得.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_20201120_マウスカーソル画像取得"
        mc:Ignorable="d"
        Title="MainWindow" Height="380" Width="300">
  <Grid UseLayoutRounding="True">
    <Grid.Resources>
      <Style TargetType="StackPanel">
        <Setter Property="Margin" Value="8,2,8,10"/>
        <Setter Property="Background" Value="LightSteelBlue"/>
        <Setter Property="Orientation" Value="Horizontal"/>
      </Style>
      <Style TargetType="Image">
        <Setter Property="Stretch" Value="None"/>
      </Style>
      <Style TargetType="Border">
        <Setter Property="BorderBrush" Value="SteelBlue"/>
        <Setter Property="BorderThickness" Value="1"/>
      </Style>
    </Grid.Resources>

    
      <StackPanel Orientation="Vertical">
        <StackPanel>
          <Border>
            <Image x:Name="MyImageCursor"/>
          </Border>
          <TextBlock Text="Cursor"/>
        </StackPanel>
        <StackPanel>
          <Border>
            <Image x:Name="MyImageCursorMask"/>
          </Border>
          <TextBlock Text="CursorMask"/>
        </StackPanel>
        <StackPanel>
          <Border>
            <Image x:Name="MyImageCursorColor"/>
          </Border>
          <TextBlock Text="CursorColor"/>
        </StackPanel>
        <StackPanel>
          <Border>
            <Image x:Name="MyImageCursorInfo"/>
          </Border>
          <TextBlock Text="CursorInfo"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <Border>
            <Image x:Name="MyImageCursorInfoMask"/>
          </Border>
          <TextBlock Text="CursorInfoMask"/>
        </StackPanel>
        <StackPanel>
          <Border>
            <Image x:Name="MyImageCursorInfoColor"/>
          </Border>
          <TextBlock Text="CursorInfoColor"/>
        </StackPanel>
      </StackPanel>
    
  </Grid>
</Window>




MainWindow.xaml.cs

using System;
using System.Windows;
using System.Windows.Media.Imaging;

using System.Runtime.InteropServices;//APIのインポートで使っている
using System.Windows.Interop;//Imaging.CreateBitmapSourceFromHBitmapで使っている
using System.Windows.Threading;//DispatcherTimerで使っている

namespace _20201120_マウスカーソル画像取得
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        #region WindowsAPI^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       
        //座標取得用
        private struct POINT
        {
            public int X;
            public int Y;
        }

      
        #region マウスカーソル系API
        //マウスカーソル関係

        [DllImport("user32.dll")]
        private static extern IntPtr GetCursor();
        [DllImport("user32.dll")]
        private static extern bool GetCursorPos(out POINT lpPoint);
        [DllImport("user32.dll")]
        private static extern IntPtr DrawIcon(IntPtr hDC, int x, int y, IntPtr hIcon);
        [DllImport("user32.dll")]
        private static extern IntPtr DrawIconEx(IntPtr hDC,
                                                int x,
                                                int y,
                                                IntPtr hIcon,
                                                int cxWidth,
                                                int cyWidth,
                                                int istepIfAniCur,
                                                IntPtr hbrFlickerFreeDraw,
                                                int diFlags);
        private const int DI_DEFAULTSIZE = 0x0008;//cxWidth cyWidthが0に指定されている場合に規定サイズで描画する
        private const int DI_NORMAL = 0x0003;//通常はこれを指定する、IMAGEとMASKの組み合わせ
        private const int DI_IMAGE = 0x0002;//画像を使用して描画
        private const int DI_MASK = 0x0001;//マスクを使用して描画
        private const int DI_COMPAT = 0x0004;//このフラグは無視の意味
        private const int DI_NOMIRROR = 0x0010;//ミラーリングされていないアイコンとし描画される
        [DllImport("user32.dll")]
        private static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO pIconInfo);
        struct ICONINFO
        {
            public bool fIcon;
            public int xHotspot;
            public int yHotspot;
            public IntPtr hbmMask;
            public IntPtr hbmColor;
        }

        [DllImport("user32.dll")]
        private static extern bool GetCursorInfo(out CURSORINFO pci);
        [StructLayout(LayoutKind.Sequential)]
        struct CURSORINFO
        {
            public int cbSize;
            public int flags;
            public IntPtr hCursor;
            public POINT ptScreenPos;
        }
        [DllImport("user32.dll")]
        private static extern IntPtr CopyIcon(IntPtr hIcon);
        [DllImport("user32.dll")]
        private static extern bool DestroyIcon(IntPtr hIcon);//CopyIcon使ったあとに使う
        #endregion マウスカーソル系


        #endregion コピペ呪文ここまで^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^



        //タイマー用
        private DispatcherTimer MyTimer;


        public MainWindow()
        {
            InitializeComponent();


            //タイマー初期化
            MyTimer = new DispatcherTimer();
            MyTimer.Interval = new TimeSpan(0, 0, 0, 0, 100);//0.1秒間隔
            MyTimer.Tick += MyTimer_Tick;
            MyTimer.Start();

            //アプリ終了時、タイマーストップ
            this.Closing += (s, e) => { MyTimer.Stop(); };
        }
        private void MyTimer_Tick(object sender, EventArgs e)
        {
            //カーソル画像
            //CURSORINFO:カーソルの表示の有無、カーソルのハンドル、座標
            //ICONINFO:trueでアイコン falseでカーソルの識別、ホットスポット座標、アイコンマスク画像のハンドル、アイコンカラー画像のハンドル

            //GetCursorから取得
            IntPtr hCursor = GetCursor();
            MyImageCursor.Source = FromIconHandle(hCursor);
            //GetCursor+GetIconInfoから取得            
            GetIconInfo(hCursor, out ICONINFO iconInfo);
            MyImageCursorMask.Source = FromBitmapHandle(iconInfo.hbmMask);
            MyImageCursorColor.Source = FromBitmapHandle(iconInfo.hbmColor);


            //GetCursorInfoを使って取得
            var cInfo = new CURSORINFO();
            cInfo.cbSize = Marshal.SizeOf(typeof(CURSORINFO));
            GetCursorInfo(out cInfo);
            MyImageCursorInfo.Source = FromIconHandle(cInfo.hCursor);
            //GetCursorInfo+GetIconInfoから取得
            GetIconInfo(cInfo.hCursor, out ICONINFO corsorInfoIconInfo);
            MyImageCursorInfoMask.Source = FromBitmapHandle(corsorInfoIconInfo.hbmMask);
            MyImageCursorInfoColor.Source = FromBitmapHandle(corsorInfoIconInfo.hbmColor);

        }

        //カーソルやアイコンのハンドルからBitmapSourceを適当に作成
        //5~10分くらい経つと「値が期待された範囲内にありません」エラーが出ることがあるのでtry使った
        //原因がわからん
        private BitmapSource FromIconHandle(IntPtr hIcon)
        {
            BitmapSource source = null;
            if (hIcon == IntPtr.Zero) return source;
            try
            {
                source = Imaging.CreateBitmapSourceFromHIcon(hIcon, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
            }
            catch (Exception e)
            {
                MyTimer.Stop();                
                Console.WriteLine(e.Message);
                System.Threading.Thread.Sleep(1000);
                MyTimer.Start();
            }
           
            return source;
        }

        //Bitmapのハンドルから適当にBitmapSourceを作成
        private BitmapSource FromBitmapHandle(IntPtr hBitmap)
        {
            //hbmColorは存在しないことがあるので、そのときはnull            
            BitmapSource source = null;
            if (hBitmap == IntPtr.Zero) return source;
            try
            {
                source = Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
            }
            catch (Exception e)
            {
                MyTimer.Stop();
                Console.WriteLine(e);
                System.Threading.Thread.Sleep(1000);
                MyTimer.Start();
            }

            return source;
            //return Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
        }

    }
}

関連記事
次回は3日後

gogowaten.hatenablog.com

前回は実質一昨日

gogowaten.hatenablog.com