午後わてんのブログ

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

アプリのウィンドウキャプチャで、枠外のメニューウィンドウもキャプチャ

メモ帳のウィンドウ枠外にメニューウィンドウが広がっている状態

f:id:gogowaten:20210203140656p:plain
デスクトップ
この状態でスクショした結果
f:id:gogowaten:20210203140812p:plain
スクショ画像
目的のアプリのウィンドウ以外は取り去って、この画像を得るのが目的で、こうするには

f:id:gogowaten:20210203190801p:plain
Rect
こういう赤枠のようなRectを取得して切り抜けばいい
今回はこのRect収集

デスクトップ全体画像の取得
gogowaten.hatenablog.com

画像の切り抜きは
gogowaten.hatenablog.com

ホットキーの登録
gogowaten.hatenablog.com


Rect収集の流れ

アプリ自体のウィンドウ取得と、そのRect取得

GetForegroundwindowで取得

WinAPIのGetForegroundwindow()でウィンドウハンドルを取得
メニューウィンドウは対象外のよう、得られるのはアプリ自体のウィンドウで
最前面とかユーザーが操作しているアプリだと思う
Rectは得られたウィンドウハンドルをWinAPIのGetWindowRectで取得できる

メニューや右クリックメニューのウィンドウ取得

コレガワカラナイ
gogowaten.hatenablog.com
わからなかったけど、別の方法で

GetWindowのENABLEDPOPUPを使う

GetWindowのENABLEDPOPUPにアプリのウィンドウハンドルを使うと取得できるアプリがある
もし取得できた場合、表示されているメニューの最上層ウィンドウが取得されるので、下階層のウィンドウをGetWindowのNEXTで収集
これで取得できないアプリは、次の方法

マウスカーソル下のウィンドウハンドルから

これはENABLEDPOPUPで取得できないアプリの場合になる

メニューを開いたとき、マウスカーソルはメニューウィンドウの上にあるはずなので
マウスカーソル下のウィンドウハンドルを取得して、その上下階層のウィンドウハンドルを、GetWindowのPREVとNEXTで収集
ただ、これの問題点はこれで収集したウィンドウは、GetForegroundwindowで得たウィンドウのメニューウィンドウなのか、無関係のウィンドウなのかこのままでは不明なので、このあとの雑な方法で判定する必要があるところ

ウィンドウの選別、Rectの重なり判定

収集したウィンドウハンドル群には、メニューウィンドウ以外も含まれているので、これを除外していく

1. Textプロパティが""以外を除外する

メニューウィンドウの特徴としてTextを持たない(Textプロパティが""とかEmpty)ってのがある(と思う)ので、それを使って判定する
順番に調べてTextが""以外が出たら、それより下側のものは除外

f:id:gogowaten:20210203152930p:plain
Textプロパティ
この場合[4]でTextが出てきたので、4以降は除外、残すのは0~3までにする

2. Rectを取得

1.で残ったウィンドウのRectを取得する、GetWindowRectを使う

3. ドロップシャドウ用のウィンドウを除外する

f:id:gogowaten:20210203151424p:plain
ドロップシャドウ
メモ帳のメニューウィンドウの右下に伸びる影は、影用のウィンドウを用意して、メニューウィンドウの下に配置して表現しているようで、NEXTでこのウィンドウハンドルも収集される
これはいらないので除去する
このウィンドウはメニューウィンドウと同じ座標なので、2.で取得したRectを使って判定して除去
f:id:gogowaten:20210203153442p:plain
ウィンドウのRect
[1]と[3]はそれぞれ[0]と[2]の座標と同じなので、ドロップシャドウのウィンドウと判定する

f:id:gogowaten:20210204130810p:plain
ドロップシャドウウィンドウ除外
新しく作ったリストに順番に加えていくときに、前後のRectの座標が同じだった場合に、widthを比較して大きい方をリストから削除

4. Rectサイズが0のウィンドウを除外する
5. メニューウィンドウ同士の重なり判定

関連のあるメニューウィンドウは、重なり合っているはずなので、重なりがないものを除去する

f:id:gogowaten:20210203155348p:plain
メニューウィンドウの重なり
Rect同士の判定なので以前の
gogowaten.hatenablog.com
この方法で判定
ここまでの処理でメニューウィンドウのRect収集は完了で、あとはアプリのウィンドウの見た目通りのRectを加えれば、画像の切り抜きに必要なRectが全て揃うことになる
f:id:gogowaten:20210204131438p:plain
重なり判定

6. アプリのウィンドウとの重なり判定

マウスカーソル下のウィンドウから収集した場合は、それがアプリのメニューウィンドウかどうかの判定が必要

f:id:gogowaten:20210204124611p:plain
最前面アプリとマウスカーソル位置
この場合、マウスカーソル下のウィンドウはエクセルなので、最前面アプリのメモ帳と関係ないって判定にしたい

これも方法がわからない、思いついたのは、アプリのウィンドウと一部でも重なっていたらメニューウィンドウだと判定するという雑な方法

メニューウィンドウには階層があって、2枚以上開かれているある場合がある

f:id:gogowaten:20210204121736p:plain
階層のあるメニューウィンドウ

もし、マウスカーソルが1階層目のウィンドウ上にあれば

f:id:gogowaten:20210204121054p:plain
カーソル下のウィンドウが1階層目ウィンドウの場合
アプリのウィンドウと重なっているから判定できるけど、これが

f:id:gogowaten:20210204121008p:plain
カーソル下のウィンドウが2階層目ウィンドウの場合
2階層目とかでアプリのウィンドウと離れていた場合に、重なり判定すると関係ないウィンドウってことになってしまう
アプリのRect(緑枠)とマウスカーソル下のウィンドウのRect(オレンジ枠)は重なっていない

なので5.で集めたRectを合成した領域とメニューウィンドウのRectで重なり判定する
上の画像の場合だと、赤枠とオレンジ枠を合成した領域と、緑枠で重なり判定

f:id:gogowaten:20210204131925p:plain
マウスカーソル下のウィンドウから
128行目から重なり判定、集めたRectをGeometryGroupにしてこれと最前面アプリのRectのRectangleGeometryを
134行目で重なり判定
これで重なっていればメニューウィンドウとして判定なのでアプリのウィンドウRectに加える
重なっていなければ無関係のウィンドウなのですべて除外、残るのはアプリのウィンドウRectだけってことになる
これで切り抜き用のRect収集はすべて完了

今回のアプリ
ダウンロード先、ここの右下にあるのdownload
20210202_右クリックメニュー取得

動作環境


作成環境

github.com


MainWindow.xaml

<Window x:Class="_20210202_右クリックメニュー取得.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:_20210202_右クリックメニュー取得"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="600">
  <Grid UseLayoutRounding="True">
    <DockPanel>
      <Button Content="save" Click="Button_Click"/>
      <Image x:Name="MyImage" StretchDirection="DownOnly"/>
    </DockPanel>
  </Grid>
</Window>

画像保存用のボタンと画像確認用のImageを配置
IamgeのStretchDirectionにDownOnlyを指定している、こうすると表示される画像はウィンドウサイズに収まるように縮小される

WinAPIを使うためのインポート用?のクラス
前回と全く同じでコピペ
API.cs

using System;
using System.Text;

using System.Runtime.InteropServices;

namespace _20210202_右クリックメニュー取得
{
    static class API
    {
        //Rect取得用
        public struct RECT
        {
            //型はlongじゃなくてintが正解!!!!!!!!!!!!!!
            //longだとおかしな値になる
            public int left;
            public int top;
            public int right;
            public int bottom;
            public override string ToString()
            {
                return $"横:{right - left:0000}, 縦:{bottom - top:0000}  ({left}, {top}, {right}, {bottom})";
            }
        }
        //座標取得用
        public struct POINT
        {
            public int X;
            public int Y;
        }
        //ウィンドウ情報用
        public struct WINDOWINFO
        {
            public int cbSize;
            public RECT rcWindow;
            public RECT rcClient;
            public uint dwStyle;
            public uint dwExStyle;
            public uint dwWindowStatus;
            public uint cxWindowBorders;
            public uint cyWindowBorders;
            public ushort atomWindowType;
            public short wCreatorVersion;
        }
        public enum WINDOW_STYLE : uint
        {
            WS_BORDER = 0x00800000,
            WS_CAPTION = 0x00C00000,
            WS_CHILD = 0x40000000,
            WS_CHILDWINDOW = 0x40000000,
            WS_CLIPCHILDREN = 0x02000000,
            WS_CLIPSIBLINGS = 0x04000000,
            WS_DISABLED = 0x08000000,
            WS_DLGFRAME = 0x00400000,
            WS_GROUP = 0x00020000,
            WS_HSCROLL = 0x00100000,
            WS_ICONIC = 0x20000000,
            WS_MAXIMIZE = 0x01000000,
            WS_MAXIMIZEBOX = 0x00010000,
            WS_MINIMIZE = 0x20000000,
            WS_MINIMIZEBOX = 0x00020000,
            WS_OVERLAPPED = 0x00000000,
            WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX,
            //The window is an overlapped window.Same as the WS_TILEDWINDOW style.
            WS_POPUP = 0x80000000,
            WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU,
            WS_SIZEBOX = 0x00040000,
            WS_SYSMENU = 0x00080000,
            WS_TABSTOP = 0x00010000,
            WS_THICKFRAME = 0x00040000,
            WS_TILED = 0x00000000,
            WS_TILEDWINDOW = WS_OVERLAPPEDWINDOW,
            //(WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX)
            WS_VISIBLE = 0x10000000,
            WS_VSCROLL = 0x00200000,
        }

        //DWM(Desktop Window Manager)
        //見た目通りのRectを取得できる、引数のdwAttributeにDWMWA_EXTENDED_FRAME_BOUNDSを渡す
        //引数のcbAttributeにはRECTのサイズ、Marshal.SizeOf(typeof(RECT))これを渡す
        //戻り値が0なら成功、0以外ならエラー値
        [DllImport("dwmapi.dll")]
        internal static extern long DwmGetWindowAttribute(IntPtr hWnd, DWMWINDOWATTRIBUTE dwAttribute, out RECT rect, int cbAttribute);

        //ウィンドウ属性
        //列挙値の開始は0だとずれていたので1からにした
        internal enum DWMWINDOWATTRIBUTE
        {
            DWMWA_NCRENDERING_ENABLED = 1,
            DWMWA_NCRENDERING_POLICY,
            DWMWA_TRANSITIONS_FORCEDISABLED,
            DWMWA_ALLOW_NCPAINT,
            DWMWA_CAPTION_BUTTON_BOUNDS,
            DWMWA_NONCLIENT_RTL_LAYOUT,
            DWMWA_FORCE_ICONIC_REPRESENTATION,
            DWMWA_FLIP3D_POLICY,
            DWMWA_EXTENDED_FRAME_BOUNDS,//見た目通りのウィンドウのRect
            DWMWA_HAS_ICONIC_BITMAP,
            DWMWA_DISALLOW_PEEK,
            DWMWA_EXCLUDED_FROM_PEEK,
            DWMWA_CLOAK,
            DWMWA_CLOAKED,
            DWMWA_FREEZE_REPRESENTATION,
            DWMWA_LAST
        };


        //
        [DllImport("user32.dll")]
        internal static extern IntPtr GetActiveWindow();

        //ウィンドウのRect取得
        [DllImport("user32.dll")]
        internal static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

        //手前にあるウィンドウのハンドル取得
        [DllImport("user32.dll")]
        internal static extern IntPtr GetForegroundWindow();

        //ウィンドウ名取得
        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        internal static extern int GetWindowText(IntPtr hWin, StringBuilder lpString, int nMaxCount);

        //パレントウィンドウ取得
        [DllImport("user32.dll")]
        internal static extern IntPtr GetParent(IntPtr hWnd);

        [DllImport("user32.dll")]
        internal static extern IntPtr GetWindow(IntPtr hWnd, GETWINDOW_CMD uCmd);//本当のuCmdはuint型
        public enum GETWINDOW_CMD
        {
            GW_CHILD = 5,
            //指定されたウィンドウが親ウィンドウである場合、取得されたハンドルは、Zオーダーの最上位にある子ウィンドウを識別します。
            //それ以外の場合、取得されたハンドルはNULLです。この関数は、指定されたウィンドウの子ウィンドウのみを調べます。子孫ウィンドウは調べません。
            GW_ENABLEDPOPUP = 6,
            //取得されたハンドルは、指定されたウィンドウが所有する有効なポップアップウィンドウを識別します
            //(検索では、GW_HWNDNEXTを使用して最初に見つかったそのようなウィンドウが使用されます)。
            //それ以外の場合、有効なポップアップウィンドウがない場合、取得されるハンドルは指定されたウィンドウのハンドルです。
            GW_HWNDFIRST = 0,
            //取得されたハンドルは、Zオーダーで最も高い同じタイプのウィンドウを識別します。
            //指定されたウィンドウが最上位のウィンドウである場合、ハンドルは最上位のウィンドウを識別します。
            //指定されたウィンドウがトップレベルウィンドウである場合、ハンドルはトップレベルウィンドウを識別します。
            //指定されたウィンドウが子ウィンドウの場合、ハンドルは兄弟ウィンドウを識別します。

            GW_HWNDLAST = 1,
            //取得されたハンドルは、Zオーダーで最も低い同じタイプのウィンドウを識別します。
            //指定されたウィンドウが最上位のウィンドウである場合、ハンドルは最上位のウィンドウを識別します。指定されたウィンドウがトップレベルウィンドウである場合、ハンドルはトップレベルウィンドウを識別します。指定されたウィンドウが子ウィンドウの場合、ハンドルは兄弟ウィンドウを識別します。

            GW_HWNDNEXT = 2,
            //取得されたハンドルは、指定されたウィンドウの下のウィンドウをZオーダーで識別します。
            //指定されたウィンドウが最上位のウィンドウである場合、ハンドルは最上位のウィンドウを識別します。
            //指定されたウィンドウがトップレベルウィンドウである場合、ハンドルはトップレベルウィンドウを識別します。
            //指定されたウィンドウが子ウィンドウの場合、ハンドルは兄弟ウィンドウを識別します。

            GW_HWNDPREV = 3,
            //取得されたハンドルは、指定されたウィンドウの上のウィンドウをZオーダーで識別します。
            //指定されたウィンドウが最上位のウィンドウである場合、ハンドルは最上位のウィンドウを識別します。
            //指定されたウィンドウがトップレベルウィンドウである場合、ハンドルはトップレベルウィンドウを識別します。
            //指定されたウィンドウが子ウィンドウの場合、ハンドルは兄弟ウィンドウを識別します。

            GW_OWNER = 4,
            //取得されたハンドルは、指定されたウィンドウの所有者ウィンドウを識別します(存在する場合)。詳細については、「所有するWindows」を参照してください。
        }

        //ウィンドウのクライアント領域のRect取得
        [DllImport("user32.dll")]
        internal static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);

        //クライアント領域の座標を画面全体での座標に変換
        [DllImport("user32.dll")]
        internal static extern bool ClientToScreen(IntPtr hWnd, out POINT lpPoint);


        [DllImport("user32.dll")]
        internal static extern int GetWindowInfo(IntPtr hWnd, ref WINDOWINFO info);

        [DllImport("user32.dll")]
        internal static extern IntPtr GetLastActivePopup(IntPtr hWnd);

        /// <summary>
        /// 指定したWindowの一番上のChildWindowを返す
        /// </summary>
        /// <param name="hWnd">IntPtr.Zeroを指定すると一番上のWindowを返す</param>
        /// <returns>ChildWindowを持たない場合はnullを返す</returns>
        [DllImport("user32.dll")]
        internal static extern IntPtr GetTopWindow(IntPtr hWnd);

        /// <summary>
        /// 指定したWindowのメニューのハンドルを返す
        /// </summary>
        /// <param name="hWnd">Windowのハンドル</param>
        /// <returns>Windowがメニューを持たない場合はnullを返す</returns>
        [DllImport("user32.dll")]
        internal static extern IntPtr GetMenu(IntPtr hWnd);

        /// <summary>
        /// キーボードフォーカスを持つWindowのハンドルを返す
        /// </summary>
        /// <returns></returns>
        [DllImport("user32.dll")]
        internal static extern IntPtr GetFocus();

        [DllImport("user32.dll")]
        internal static extern IntPtr GetMenuBarInfo(IntPtr hWnd, MenuObjectId idObject, long idItem, MENUBARINFO pmbi);

        public struct MENUBARINFO
        {
            public long cbSize;
            public RECT rcBar;
            public IntPtr hMenu;
            public bool fBarFocused;
            public bool fFocused;
        }
        public enum MenuObjectId : long
        {
            OBJID_CLIENT = 0xFFFFFFFC,
            OBJID_MENU = 0xFFFFFFFD,
            OBJID_SYSMENU = 0xFFFFFFFF,
        }

        [DllImport("user32.dll")]
        internal static extern IntPtr GetMenuItemRect(IntPtr hWnd, IntPtr hMenu, uint uItem, out RECT rect);


        //指定座標にあるウィンドウのハンドル取得
        [DllImport("user32.dll")]
        internal static extern IntPtr WindowFromPoint(POINT pOINT);

        //祖先ウィンドウを取得
        [DllImport("user32.dll")]
        internal static extern IntPtr GetAncestor(IntPtr hWnd, AncestorType type);

        public enum AncestorType
        {
            GA_PARENT = 1,
            GA_ROOT = 2,//Parentを辿ってルートを取得
            GA_ROOTOWNER = 3,//GetParentを使ってルートを取得

        }

        [DllImport("user32.dll")]
        internal static extern IntPtr GetDesktopWindow();


        [DllImport("user32.dll")]
        internal static extern IntPtr GetShellWindow();

        [DllImport("user32.dll")]
        internal static extern IntPtr GetSubMenu(IntPtr hWnd, int nPos);

        [DllImport("user32.dll")]
        internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);




        //public delegate bool EnumWindowsDelegate(IntPtr hWnd, IntPtr lparam, List<IntPtr> intPtrs);
        //[DllImport("user32.dll")]
        //[return: MarshalAs(UnmanagedType.Bool)]
        //internal static extern bool EnumChildWindows(IntPtr hWnd, EnumWindowsDelegate enumWindows, IntPtr lparam);


        //internal static List<IntPtr> GetChildWindows(IntPtr hWnd)
        //{
        //    List<IntPtr> childList = new();
        //    EnumChildWindows(hWnd, new EnumWindowsDelegate(EnumWindowCallBack), IntPtr.Zero);
        //    return childList;
        //}
        //private static bool EnumWindowCallBack(IntPtr hWnd, IntPtr lparam, List<IntPtr> childList)
        //{
        //    childList.Add(hWnd);
        //    return true;
        //}



        //グローバルホットキー登録用
        internal const int WM_HOTKEY = 0x0312;
        [DllImport("user32.dll")]
        internal static extern int RegisterHotKey(IntPtr hWnd, int id, int modkyey, int vKey);
        [DllImport("user32.dll")]
        internal static extern int UnregisterHotKey(IntPtr hWnd, int id);

        //マウスカーソル座標
        [DllImport("user32.dll")]
        internal static extern bool GetCursorPos(out POINT lpPoint);


        //Bitmap描画関連
        //DC取得
        //nullを渡すと画面全体のDCを取得、ウィンドウハンドルを渡すとそのウィンドウのクライアント領域DC
        //失敗した場合の戻り値はnull
        //使い終わったらReleaseDC
        [DllImport("user32.dll")]
        internal static extern IntPtr GetDC(IntPtr hWnd);

        //渡したDCに互換性のあるDC作成
        //失敗した場合の戻り値はnull
        //使い終わったらDeleteDC
        [DllImport("gdi32.dll")]
        internal static extern IntPtr CreateCompatibleDC(IntPtr hdc);

        //指定されたDCに関連付けられているデバイスと互換性のあるビットマップを作成
        //使い終わったらDeleteObject
        [DllImport("gdi32.dll")]
        internal static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int cx, int cy);

        //DCにオブジェクトを指定する、オブジェクトの種類はbitmap、brush、font、pen、Regionなど
        [DllImport("gdi32.dll")]
        internal static extern IntPtr SelectObject(IntPtr hdc, IntPtr h);

        //画像転送
        [DllImport("gdi32.dll")]
        internal static extern bool BitBlt(IntPtr hdc, int x, int y, int cx, int cy, IntPtr hdcSrc, int x1, int y1, uint rop);
        internal const int SRCCOPY = 0x00cc0020;
        internal const int SRCINVERT = 0x00660046;

        ////
        //[DllImport("user32.dll")]
        //private static extern bool PrintWindow(IntPtr hWnd, IntPtr hDC, uint nFlags);
        //private const uint nFrags_PW_CLIENTONLY = 0x00000001;

        //[DllImport("user32.dll")]
        //private static extern bool DeleteDC(IntPtr hdc);

        [DllImport("user32.dll")]
        internal static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);

        [DllImport("gdi32.dll")]
        internal static extern bool DeleteObject(IntPtr ho);


    }
}



MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

using System.Windows.Interop;

//スクショテストアプリ
//ctrl + shift + PrintScreen でキャプチャ
//キャプチャされるのは最前面アプリのウィンドウとそのメニューウィンドウ
//saveボタンで実行ファイルと同じフォルダに画像ファイルとして保存
//右クリックメニューの上にマウスカーソルを置かないと枠外のメニューはキャプチャされない

namespace _20210202_右クリックメニュー取得
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        //ホットキー
        private const int HOTKEY_ID1 = 0x0001;//ID
        private IntPtr MyWindowHandle;//アプリのハンドル

        //ウィンドウ探査loopの回数上限値
        private const int LOOP_LIMIT = 10;

        public MainWindow()
        {
            InitializeComponent();


            MyInitializeHotKey();

            //ホットキーにPrintScreenキーを登録
            ChangeHotKey(Key.PrintScreen, HOTKEY_ID1);

            //アプリ終了時にホットキーの解除
            Closing += MainWindow_Closing;
        }


        //ホットキー判定
        private void ComponentDispatcher_ThreadPreprocessMessage(ref MSG msg, ref bool handled)
        {
            if (msg.message != API.WM_HOTKEY) return;

            //ホットキー(今回はPrintScreen)が押されたら
            else if (msg.wParam.ToInt32() == HOTKEY_ID1)
            {
                //Rect収集
                List<Rect> rectList = MakeForeWinndwWithMenuWindowRectList();
                //全画面画像取得
                var bmp = GetScreenBitmap();
                //収集したRectを使って切り抜き画像作成して表示
                MyImage.Source = CroppedBitmapFromRects(bmp, rectList);
            }
        }

        #region Rect取得

        /// <summary>
        /// 最前面ウィンドウと、そのメニューや右クリックメニューウィンドウ群のRectリストを作成
        /// </summary>
        /// <returns></returns>
        private List<Rect> MakeForeWinndwWithMenuWindowRectList()
        {
            List<Rect> result = new();
            //Foregroundのハンドル取得
            IntPtr fore = API.GetForegroundWindow();
            var infoFore = GetWindowRectAndText(fore);
            //ForegroundのPopupハンドルとRect取得
            IntPtr popup = API.GetWindow(fore, API.GETWINDOW_CMD.GW_ENABLEDPOPUP);
            Rect popupRect = GetWindowRect(popup);
            var infoPop = GetWindowRectAndText(popup);//確認用

            //Popupが存在する(Rectが0じゃない)場合
            if (popupRect != new Rect(0, 0, 0, 0))
            {
                //PopupのNEXT(下にあるウィンドウハンドル)を収集
                List<IntPtr> pops = GetCmdWindows(popup, API.GETWINDOW_CMD.GW_HWNDNEXT, LOOP_LIMIT);
                var infoPops = GetWindowRectAndTexts(pops);//確認用               

                //必要なRectだけを選別
                result = SelectRects(pops);
                ////Textを持つウィンドウ以降を除去
                ////残ったウィンドウのRect取得
                ////ドロップシャドウウィンドウのRectを除去
                ////前後のRectが重なっているところまで選択して、以降は除外

                //GetForegroundwindowの見た目通りのRectを追加
                result.Add(GetWindowRectMitame(fore));
            }
            //Popupが存在しない(Rectが0)場合
            else
            {
                //GetForegroundwindowの見た目通りのRectを追加
                Rect foreRect = GetWindowRectMitame(fore);
                result.Add(foreRect);

                //マウスカーソル下のウィンドウハンドル取得、これを基準にする
                API.GetCursorPos(out API.POINT cursorP);
                IntPtr cursor = API.WindowFromPoint(cursorP);
                //Rect cursorRect = GetWindowRectMitame(cursor);

                //カーソル下のウィンドウRectとForegroundのRect重なり判定
                //関係あるウィンドウなら、Textがない and Rectが重なっている
                //重なりはメニューウィンドウ全域と重なっていればおk判定にする
                List<Rect> rs = new();
                if (GetWindowText(cursor) == "")
                {
                    //基準の上下それぞれのウィンドウハンドル取得
                    List<IntPtr> prev = GetCmdWindows(cursor, API.GETWINDOW_CMD.GW_HWNDPREV, LOOP_LIMIT);//上
                    List<IntPtr> next = GetCmdWindows(cursor, API.GETWINDOW_CMD.GW_HWNDNEXT, LOOP_LIMIT);//下
                    //必要なRectだけを選別
                    List<Rect> rsPrev = SelectRects(prev);
                    List<Rect> rsNext = SelectRects(next);
                    //前後のRectリストを統合
                    rs = rsPrev.Union(rsNext).ToList();
                }

                //重なり判定はForegroundのRectと、それ以外のRectを結合したRectで判定する
                //Rectの結合はGeometryGroupを使う
                GeometryGroup gg = new();
                for (int i = 0; i < rs.Count; i++)
                {
                    gg.Children.Add(new RectangleGeometry(rs[i]));
                }
                //重なり判定、重なっていたらForegroundのRect+それ以外のRect
                if (IsOverlapping(gg, new RectangleGeometry(foreRect)))
                {
                    result = result.Union(rs).ToList();
                }
                //重なっていない場合はメニューウィンドウは開かれていないと判定して
                //ForegroundのウィンドウRectだけでいい
            }
            return result;
        }

        private List<Rect> SelectRects(List<IntPtr> pList)
        {
            //Textを持つウィンドウ以降を除去
            List<IntPtr> noneText = DeleteWithTextWindow(pList);
            //残ったウィンドウの見た目通りのRect取得
            List<Rect> rs = noneText.Select(x => GetWindowRectMitame(x)).ToList();
            //ドロップシャドウウィンドウのRectを除去
            var result = DeleteShadowRect(rs);
            //サイズが0のRectを除去
            result = result.Where(x => x.Size.Width != 0 && x.Size.Height != 0).ToList();
            //前後のRectが重なっているところまで選択して、以降は除外
            return SelectOverlappedRect(result);
        }

        //ウィンドウの見た目通りのRect取得はDwmGetWindowAttribute
        //https://gogowaten.hatenablog.com/entry/2020/11/17/004505
        //見た目通りのRect取得
        private Rect GetWindowRectMitame(IntPtr hWnd)
        {
            //見た目通りのWindowRectを取得
            _ = API.DwmGetWindowAttribute(
                hWnd,
                API.DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS,
                out API.RECT myRECT,
                System.Runtime.InteropServices.Marshal.SizeOf(typeof(API.RECT)));

            return MyConverterApiRectToRect(myRECT);
        }
        private Rect MyConverterApiRectToRect(API.RECT rect)
        {
            return new Rect(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
        }

        //WPFのRectの重なり判定、RectangleGeometryにしてからFillContainsWithDetailメソッドでできた
        //https://gogowaten.hatenablog.com/entry/2021/01/28/124714
        /// <summary>
        /// 前後のRectの重なりを判定、重なっていればリストに追加して返す。重なっていないRectが出た時点で終了
        /// </summary>
        /// <param name="rList"></param>
        /// <returns></returns>
        private List<Rect> SelectOverlappedRect(List<Rect> rList)
        {
            List<Rect> result = new();
            if (rList.Count == 0) return result;

            result.Add(rList[0]);

            //順番に判定
            for (int i = 1; i < rList.Count; i++)
            {
                if (IsOverlapping(rList[i - 1], rList[i]))
                {
                    //重なっていればリストに追加
                    result.Add(rList[i]);
                }
                else
                {
                    //途切れたら終了
                    return result;
                }
            }
            return result;
        }

        /// <summary>
        /// 2つのGeometryが一部でも重なっていたらTrueを返す
        /// </summary>
        /// <param name="g1"></param>
        /// <param name="g2"></param>
        /// <returns></returns>
        private bool IsOverlapping(Geometry g1, Geometry g2)
        {

            IntersectionDetail detail = g1.FillContainsWithDetail(g2);
            return detail != IntersectionDetail.Empty;
            //return (detail != IntersectionDetail.Empty || detail != IntersectionDetail.NotCalculated, detail);
        }
        /// <summary>
        /// 2つのRectが一部でも重なっていたらtrueを返す
        /// </summary>
        /// <param name="r1"></param>
        /// <param name="r2"></param>
        /// <returns></returns>        
        private bool IsOverlapping(Rect r1, Rect r2)
        {
            return IsOverlapping(new RectangleGeometry(r1), new RectangleGeometry(r2));
        }
        //IntersectionDetail列挙型
        //Empty             全く重なっていない
        //FullyContains     r2はr1の領域に完全に収まっている
        //FullyInside       r1はr2の領域に完全に収まっている
        //Intersects        一部が重なっている
        //NotCalculated     計算されません(よくわからん)


        /// <summary>
        /// Textがないものをリストに追加していって、Textをもつウィンドウが出た時点で終了、リストを返す
        /// </summary>
        /// <param name="wList"></param>
        /// <returns></returns>
        private List<IntPtr> DeleteWithTextWindow(List<IntPtr> wList)
        {
            List<IntPtr> result = new();
            for (int i = 0; i < wList.Count; i++)
            {
                if (GetWindowText(wList[i]) == "")
                {
                    result.Add(wList[i]);
                }
                else
                {
                    return result;
                }
            }

            return result;
        }


        /// <summary>
        /// ドロップシャドウ用のウィンドウを判定して、取り除いて返す。前後のRectのtopleftが同じなら後のRectはドロップシャドウと判定する
        /// </summary>
        /// <param name="rList"></param>
        /// <returns></returns>       
        private List<Rect> DeleteShadowRect(List<Rect> rList)
        {
            List<Rect> result = new();
            result.Add(rList[0]);
            Rect preRect = rList[0];//前Rect
            for (int i = 0; i < rList.Count; i++)
            {
                //リストに加えて
                Rect imaRect = rList[i];//後Rect
                result.Add(imaRect);

                //前後の座標が同じ場合は
                if (imaRect.TopLeft == preRect.TopLeft)
                {
                    //サイズが大きい方を削除
                    if (imaRect.Size.Width < preRect.Size.Width)
                    {
                        result.Remove(rList[i - 1]);
                    }
                    else
                    {
                        result.Remove(rList[i]);
                    }
                }
                preRect = imaRect;//前Rectに後Rectを入れて次へ
            }
            return result;
        }

        //指定したAPI.GETWINDOW_CMDを収集、自分自身も含む
        private List<IntPtr> GetCmdWindows
            (IntPtr hWnd, API.GETWINDOW_CMD cmd, int loopCount)
        {
            List<IntPtr> v = new();
            v.Add(hWnd);//自分自身

            IntPtr temp = API.GetWindow(hWnd, cmd);
            for (int i = 0; i < loopCount; i++)
            {
                v.Add(temp);
                temp = API.GetWindow(temp, cmd);
            }
            return v;
        }


        //ウィンドウハンドルからText(タイトル名)やRECTを取得
        private (IntPtr, Rect re, string text) GetWindowRectAndText(IntPtr hWnd)
        {
            return (hWnd, GetWindowRect(hWnd), GetWindowText(hWnd));
        }
        private (List<IntPtr> ptrs, List<Rect> rs, List<string> strs)
            GetWindowRectAndTexts(List<IntPtr> pList)
        {
            List<IntPtr> ptrs = new();
            List<Rect> rs = new();
            List<string> strs = new();
            foreach (var item in pList)
            {
                ptrs.Add(item);
                rs.Add(GetWindowRect(item));
                strs.Add(GetWindowText(item));
            }
            return (ptrs, rs, strs);
        }
        //ウィンドウハンドルからText(タイトル名)やRECTを取得
        private (IntPtr, API.RECT re, string text) GetWindowAPI_RECTAndText(IntPtr hWnd)
        {
            return (hWnd, GetWindowAPIRECT(hWnd), GetWindowText(hWnd));
        }
        private (List<IntPtr> ptrs, List<API.RECT> rs, List<string> strs)
            GetWindowAPI_RECTAndTexts(List<IntPtr> pList)
        {
            List<IntPtr> ptrs = new();
            List<API.RECT> rs = new();
            List<string> strs = new();
            foreach (var item in pList)
            {
                ptrs.Add(item);
                rs.Add(GetWindowAPIRECT(item));
                strs.Add(GetWindowText(item));
            }
            return (ptrs, rs, strs);
        }

        //ウィンドウハンドルからRect取得
        private Rect GetWindowRect(IntPtr hWnd)
        {
            _ = API.GetWindowRect(hWnd, out API.RECT re);
            return MyConverterApiRectToRect(re);
        }
        //ウィンドウハンドルからRECT取得
        private static API.RECT GetWindowAPIRECT(IntPtr hWnd)
        {
            _ = API.GetWindowRect(hWnd, out API.RECT re);
            return re;
        }

        //ウィンドウハンドルからText取得
        private static string GetWindowText(IntPtr hWnd)
        {
            StringBuilder text = new StringBuilder(65535);
            _ = API.GetWindowText(hWnd, text, 65535);
            return text.ToString();
        }

        #endregion Rect取得


        #region 画像切り抜き
        //WPF、画像から複数箇所を矩形(Rect)に切り抜いて、それぞれ位置を合わせて1枚の画像にしてファイルに保存する - 午後わてんのブログ
        //https://gogowaten.hatenablog.com/entry/2021/01/24/233657

        /// <summary>
        /// 複数Rect範囲を組み合わせた形にbitmapを切り抜く
        /// </summary>
        /// <param name="source">元の画像</param>
        /// <param name="rectList">Rectのコレクション</param>
        /// <returns></returns>
        private BitmapSource CroppedBitmapFromRects(BitmapSource source, List<Rect> rectList)
        {
            var dv = new DrawingVisual();

            using (DrawingContext dc = dv.RenderOpen())
            {
                //それぞれのRect範囲で切り抜いた画像を描画していく
                foreach (var rect in rectList)
                {
                    dc.DrawImage(new CroppedBitmap(source, RectToIntRectWith切り捨て(rect)), rect);
                }
            }

            //描画位置調整
            dv.Offset = new Vector(-dv.ContentBounds.X, -dv.ContentBounds.Y);

            //bitmap作成、縦横サイズは切り抜き後の画像全体がピッタリ収まるサイズにする
            //PixelFormatsはPbgra32で決め打ち、これ以外だとエラーになるかも、
            //画像を読み込んだbitmapImageのPixelFormats.Bgr32では、なぜかエラーになった
            var bmp = new RenderTargetBitmap(
                (int)Math.Ceiling(dv.ContentBounds.Width),
                (int)Math.Ceiling(dv.ContentBounds.Height),
                96, 96, PixelFormats.Pbgra32);

            bmp.Render(dv);
            return bmp;
        }

        //RectからInt32Rect作成、小数点以下切り捨て編
        private Int32Rect RectToIntRectWith切り捨て(Rect re)
        {
            return new Int32Rect((int)re.X, (int)re.Y, (int)re.Width, (int)re.Height);
        }



        //bitmapをpng画像ファイルで保存、アプリの実行ファイルと同じフォルダ、ファイル名は年月日_時分秒
        private void SaveImage(BitmapSource source)
        {
            PngBitmapEncoder encoder = new();
            encoder.Frames.Add(BitmapFrame.Create(source));
            string path = DateTime.Now.ToString("yyyyMMdd_HHmmss");
            path += ".png";
            using (var pp = new System.IO.FileStream(
                path, System.IO.FileMode.Create, System.IO.FileAccess.Write))
            {
                encoder.Save(pp);
            }
        }
        #endregion 画像切り抜き



        //ウィンドウDCからのキャプチャではアルファ値が変なので、画面全体をキャプチャして切り抜き
        //https://gogowaten.hatenablog.com/entry/2020/11/16/005641
        //仮想画面全体の画像取得
        private BitmapSource GetScreenBitmap()
        {
            var screenDC = API.GetDC(IntPtr.Zero);//仮想画面全体のDC、コピー元
            var memDC = API.CreateCompatibleDC(screenDC);//コピー先DC作成
            int width = (int)SystemParameters.VirtualScreenWidth;
            int height = (int)SystemParameters.VirtualScreenHeight;
            var hBmp = API.CreateCompatibleBitmap(screenDC, width, height);//コピー先のbitmapオブジェクト作成
            API.SelectObject(memDC, hBmp);//コピー先DCにbitmapオブジェクトを指定

            //コピー元からコピー先へビットブロック転送
            //通常のコピーなのでSRCCOPYを指定
            API.BitBlt(memDC, 0, 0, width, height, screenDC, 0, 0, API.SRCCOPY);
            //bitmapオブジェクトからbitmapSource作成
            BitmapSource source =
                Imaging.CreateBitmapSourceFromHBitmap(
                    hBmp,
                    IntPtr.Zero,
                    Int32Rect.Empty,
                    BitmapSizeOptions.FromEmptyOptions());

            //後片付け
            API.DeleteObject(hBmp);
            _ = API.ReleaseDC(IntPtr.Zero, screenDC);
            _ = API.ReleaseDC(IntPtr.Zero, memDC);

            //画像
            return source;
        }


        #region ホットキー関連
        //アプリのウィンドウが非アクティブ状態でも任意のキーの入力を感知、WPFでグローバルホットキーの登録
        //https://gogowaten.hatenablog.com/entry/2020/12/11/132125
        private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            //ホットキーの登録解除
            _ = API.UnregisterHotKey(MyWindowHandle, HOTKEY_ID1);
            ComponentDispatcher.ThreadPreprocessMessage -= ComponentDispatcher_ThreadPreprocessMessage;
        }

        private void MyInitializeHotKey()
        {
            MyWindowHandle = new WindowInteropHelper(this).Handle;
            ComponentDispatcher.ThreadPreprocessMessage += ComponentDispatcher_ThreadPreprocessMessage;
        }
        private void ChangeHotKey(Key Key, int hotkeyId)
        {
            ChangeHotKey(KeyInterop.VirtualKeyFromKey(Key), hotkeyId);
        }
        private void ChangeHotKey(int vKey, int hotkeyId)
        {
            //上書きはできないので、古いのを削除してから登録
            _ = API.UnregisterHotKey(MyWindowHandle, hotkeyId);

            //int mod = GetModifierKeySum();
            //int mod = 2;//ctrl
            //int mod = 1;//alt
            //int mod = 4;//shift
            //int mod = 6;//ctrl + shift
            //int mod = 0;//修飾キーなし
            int mod = 6;
            if (API.RegisterHotKey(MyWindowHandle, hotkeyId, mod, vKey) == 0)
            {
                MessageBox.Show("登録に失敗");
            }
            else
            {
                //MessageBox.Show("登録完了");
            }
        }
        #endregion ホットキー関連




        private void Button_Click(object sender, RoutedEventArgs e)
        {
            SaveImage((BitmapSource)MyImage.Source);
        }
    }
}

長いけど肝心なRect収集部分は374行目までで、それ以外はあちこちからのコピペでできている

f:id:gogowaten:20210204132721p:plain
今回のテストアプリ
ctrl + shift + PrintScreenキーで最前面のアプリのウィンドウがキャプチャされる

f:id:gogowaten:20210204133005p:plain
メモ帳

f:id:gogowaten:20210204133108p:plain
縮小表示
キャプチャした画像が大きい場合は縮小表示される、これは

<Image x:Name="MyImage" StretchDirection="DownOnly"/>

これだけなんだよねえ、ImageのStretchDirectionにDownOnly
Imageの拡大縮小表示はStretchプロパティしか知らなかったけど、StretchDirectionも便利ねえ

f:id:gogowaten:20210204133719p:plain
メニュー付きでキャプチャ
左にあるsaveボタンでアプリのファイルと同じフォルダにpng画像として保存される

f:id:gogowaten:20210204134140p:plain
png画像として保存

f:id:gogowaten:20210204134213p:plain
保存された画像
いいね

f:id:gogowaten:20210204134349p:plain
メモ帳の右クリックメニュー

f:id:gogowaten:20210204134640p:plain
Visual Studioのメニュー
これはドロップシャドウ部分が除去できない、Visual Studioのメニューウィンドウのドロップシャドウ効果は、他のアプリとは違った方法で描画されているみたい

f:id:gogowaten:20210204134944p:plain
Visual Studioのメニューウィンドウ
ドロップシャドウ部分は半透明なので、下にあるものが写っている

f:id:gogowaten:20210204135836p:plain
マウスカーソルがメニューの上にない場合
メニューウィンドウが開かれていても、その上にマウスカーソルがないと取得できないので、アプリのウィンドウ枠に限定されてしまう、残念だけどこれは仕様

f:id:gogowaten:20210204140341p:plain
エクセル
エクセル他、リボンメニューのアプリはメニューウィンドウ自体がGetForegroundwindowで取得されるので、最前面のメニューウィンドウだけがキャプチャされる

f:id:gogowaten:20210204140752p:plainf:id:gogowaten:20210204140756p:plain
リボンメニューのキャプチャ結果

僥倖

f:id:gogowaten:20210204145823p:plain
ドロップダウンリスト
なぜかコンボボックスのドロップダウンリストもキャプチャできた

いくつか残念なところはあるけど棚ぼたもあったり、なかなかいい感じでできたと思う
あとはこれをPixcrenに追加したい

今回の記事は、 staff.hatenablog.com こちらを参考に
目次を入れてみたけど、見出しのレベルとか考えるから、すごい書きにくかったw
でも自動で目次を作ってくれるのは、とっても便利

f:id:gogowaten:20210204145414p:plain
単語登録
今後も使うかは別、見出し考えるのすごい時間かかる



関連記事
次回は翌日

gogowaten.hatenablog.com 今回のをスクショアプリに取り入れた

3日後

gogowaten.hatenablog.com
エクセル専用

前回のWPF記事は一昨日
gogowaten.hatenablog.com