午後わてんのブログ

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

エクセルのスクショ時にウィンドウ枠外のメニュー、右クリックメニューも同時に撮りたい、WPFとWinAPI

目的

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

f:id:gogowaten:20210207130201p:plain
デスクトップ画面にメニューが開かれたエクセル
デスクトップ画面がこうだった場合にスクショして

f:id:gogowaten:20210207130327p:plain
スクショ結果
こういう画像を取得したい
エクセルのウィンドウをスクショするときに、ウィンドウ外に表示されているメニューウィンドウも一緒にキャプチャした上で、エクセルに関連するウィンドウ以外は除外

基本的な処理の流れは

WinAPIのGetForegroundで最前面のウィンドウハンドルを取得、これを基点にGetWindow系を使ってウィンドウハンドルを取得して、そこからGetWindowRectでRectを取得するのは前回までと同じ

普通のアプリとエクセルとかのリボンメニューがあるアプリの違い

メニューウィンドウが開かれている状態だとGetForegroundで得られる対象のウィンドウが違う
普通のアプリ、ここではメモ帳の場合

f:id:gogowaten:20210207125144p:plain
メモ帳の場合
メモ帳自体のウィンドウハンドルが得られる

エクセルの場合は

f:id:gogowaten:20210207125248p:plain
エクセルの場合
エクセル自体ではなくて、メニューウィンドウのハンドルが得られる
メニューウィンドウのほうが全面に表示されているから、GetForegroundって名前からすると、メニューウィンドウが得られるのが自然な感じがするけどねえ、アプリによってアプリ自体だったりメニューだったりの違いが出る
ややこしいけど、今回はエクセルだけにするので、GetForegroundで得られ基点になるのは、最前面に表示されたウィンドウってことになる

基点ウィンドウから関連ウィンドウを辿る

f:id:gogowaten:20210207131301p:plain
デスクトップ画面
エクセルのリボンメニューのスタイルから、条件付き書式、データバーと開いた状態
GetForegroundで得られるのは最前面のウィンドウなので

f:id:gogowaten:20210207131847p:plain
GetForegroundの対象
このデータバーのウィンドウが基点になる

GetParentで辿っていく方法もあるけど

APIのGetParentで順番に辿っていくと、エクセルのウィンドウまで行ける

f:id:gogowaten:20210207132103p:plain
GetParent
ついでに右下のPopupウィンドウは、ForegroundのENABLEDPOPUP属性のウィンドウってことみたいで、GetWindowのENABLEDPOPUPを使ったら取得できた

右クリックメニューはGetParentで辿れない

リボンメニューはGetParentでたどれたけど、右クリックメニューの場合は

f:id:gogowaten:20210207133257p:plain
エクセルの右クリックメニュー
これをGetParentで辿ると

f:id:gogowaten:20210207133351p:plain
取得できないウィンドウ
右クリックで同時に2つ表示されるウィンドウの小さい方は取得できない!
ってことで今回の目的にGetParentは向いていない

下層のウィンドウを取得するNEXTで辿る

これは前回と同じ方法で、GetWindowのGW_HWNDNEXTを使って、対象ウィンドウの1つ下層にあるウィンドウを取得する、これなら小さい方のウィンドウも取得できた

f:id:gogowaten:20210207133934p:plain
NEXTで辿る
ただこの方法だとエクセル自体のウィンドウまで到達するにはかなり辿ることになりそうだったので、エクセル自体はGetAncesterのRootOwnerで取得することにした

余分なウィンドウの除去

NEXTで辿ると、たくさんの余計なわけのわからんウィンドウも取得されるので、余分なウィンドウを除去する必要がある

f:id:gogowaten:20210207140437p:plain
エクセルの右クリックメニューをNEXTで
ドロップシャドウの表現の部分は、別ウィンドウなのがNEXTで辿ってわかった、いらないので除去する

右クリックメニューウィンドウのパーツ

f:id:gogowaten:20210207134941p:plain
ウィンドウパーツ?
ほしいのは[0]と[3]だけで、ドロップシャドウ部分の1,2,4,5はいらない、これの判定は

f:id:gogowaten:20210207135533p:plain
RootOwnerのTextプロパティ
ドロップシャドウ用のウィンドウのRootOwnerのTextプロパティは無い(Text = ""Empty)、対して欲しいウィンドウのTextはエクセルになっているのがわかったので、ForegroundのRootOwnerのTextプロパティと同じものだけ残すっていう処理にした

見えない(可視 = false)ウィンドウの除去

f:id:gogowaten:20210207142736p:plain
RootOwnerのTextプロパティ
これは右クリックメニューをNEXTで辿ったウィンドウの、それぞれのRootOwnerのTextプロパティ
さっきの方法で残るのは0,3,6,7,8,9ってことになる

f:id:gogowaten:20210207142547p:plain
いらない謎のウィンドウ
これも同じくNEXTで辿ったウィンドウ群で、それぞれのハンドルとRect、可視(IsVisible)、Text
この中の[8]、これが邪魔、Rectサイズは必ず(172,192)なのが特徴、座標はその時によって変わし、全く関係なさそうな場所なので除去したいんだけど、RootOwnerのTextはエクセルだから、さっきの方法では抜けてくる、サイズ0を除外する方法でも0じゃないからこれも抜けてしまう、どうしたもんかなあと思ったんだけど、いいのがあった!

IsWindowVisible
docs.microsoft.com
ウィンドウの可視状態(表示されているか否か)を取得する、これにさっきの8番渡したらfalse(見えていない)だったので、これで判定できる
だいたいこんな感じ、リボンメニューもNEXTで辿れるので、GetParentは使わずにGetWindowのNEXTで辿る方法で統一できた
大まかな流れはGetForegroundで最上層ウィンドウ取得、そこからGetWindowのNEXTで下層のウィンドウ群を20個程度取得、いらないウィンドウを除去

処理の流れ

  1. エクセルのメニュー、右クリックメニューのRect取得
  2. GetForeground、これで得たハンドル(Fore)を使ってRootOWNERを取得(ForeOWNER)
  3. さらにForeのENABLEDPOPUPをGetWindowで取得しておく(Popup)
  4. Foreの下層にあるウィンドウハンドルをGetWindowのNEXTで20個程度取得(ForeNEXT)
  5. 表示されているもの(IsWindowVisibleがTrue)だけ残す
  6. ForeNEXTのRootOWNERとForeOWNERを比較、同じものだけ残す
  7. 見た目通りのRectを取得
  8. 上から順番にRectを見て、0が見つかったらそれ以降は除外
  9. PopupのRectを追加
  10. 最後にForeOWNERの見た目通りのRectを追加で完了


テストアプリ

f:id:gogowaten:20210207162144p:plain
テスト用アプリ
PrintScreenキーでキャプチャ画像を表示する
全体画像から最前面アプリのメニューを含めた領域(Rect)で切り抜いた画像を表示する、Rectの取得が正しいか確認用のアプリ

作成環境

コード

github.com

アプリ名は20210206_エクセルのメニューRect取得.7z

MainWindow.xaml
<Window x:Class="_20210206_エクセルのメニューRect取得.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:_20210206_エクセルのメニューRect取得"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
  <Grid UseLayoutRounding="True">
    <Image x:Name="MyImage" StretchDirection="DownOnly"/>
  </Grid>
</Window>



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);

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



        //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);


    }
}

相変わらずAPIのインポート?は前回のコピペなんだけど、今回はIsWindowVisibleを追加した

f:id:gogowaten:20210207150447p:plain
IsWindowVisible


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 _20210202_右クリックメニュー取得;
using System.Windows.Interop;

//エクセルのリボンメニューや右クリックメニューがエクセルのウィンドウ枠にある場合も途切れずに取得するテスト
//リボンメニューを使ったアプリならエクセル以外でもキャプチャできるかも
//キャプチャはPrintScreenキー

namespace _20210206_エクセルのメニューRect取得
{
    /// <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収集して、それを使って切り抜き画像作成して表示
                MyImage.Source = CroppedBitmapFromRects(GetScreenBitmap(), GetExcelMenuRects());
            }
        }


        #region エクセルとかリボンメニューのアプリのRect取得      

        //エクセルの右クリックメニュー、リボンメニューのRect収集
        private List<Rect> GetExcelMenuRects()
        {
            IntPtr fore = API.GetForegroundWindow();
            
            var foreOwnder = GetWindowInfo(API.GetAncestor(fore, API.AncestorType.GA_ROOTOWNER));
            var popup = GetWindowInfo(API.GetWindow(foreOwnder.hWnd, API.GETWINDOW_CMD.GW_ENABLEDPOPUP));

            //Foreの下層にあるウィンドウハンドルをGetWindowのNEXTで20個程度取得
            List<MyStruct> foreNexts = GetWindowInfos(GetCmdWindows(fore, API.GETWINDOW_CMD.GW_HWNDNEXT, 20));
                        
            //可視状態のものだけ残す
            var noneZero = foreNexts.Where(x => x.IsVisible == true).ToList();

            //ForeNEXTのRootOWNERとForeOWNERを比較、同じものだけ残す
            List<MyStruct> nexts = noneZero.Where(x => foreOwnder.Text == GetWindowText(API.GetAncestor(x.hWnd, API.AncestorType.GA_ROOTOWNER))).ToList();

            //見た目通りのRectを取得
            List<Rect> nextRect = nexts.Select(x => GetWindowRectMitame(x.hWnd)).ToList();

            //ForeNEXTを上から順番にRectを見て、0が見つかったらそれ以降は除外
            List<Rect> nextRect2 = SelectNoneZeroRects(nextRect);

            //popupウィンドウのRectを追加
            if (popup.Rect.Width != 0)
            {
                nextRect2.Add(popup.Rect);
            }

            //最後にRootOWNERの見た目通りのRectを追加
            nextRect2.Add(GetWindowRectMitame(foreOwnder.hWnd));
            return nextRect2;

        }
     

        //RectのListを順番にwidthが0を探して、見つかったらそれ以降のRectは除外して返す
        private List<Rect> SelectNoneZeroRects(List<Rect> rl)
        {
            List<Rect> r = new();
            for (int i = 0; i < rl.Count; i++)
            {
                if (rl[i].Width == 0)
                {
                    return r;
                }
                else
                {
                    r.Add(rl[i]);
                }
            }
            return r;
        }


        #endregion エクセルとかリボンメニューのアプリのRect取得


        #region Rect取得

        //ウィンドウの見た目通りの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);
        }



        //指定した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;
        }

        private List<MyStruct> GetWindowInfos(List<IntPtr> hWnd)
        {
            List<MyStruct> l = new();
            foreach (var item in hWnd)
            {
                l.Add(GetWindowInfo(item));
            }
            return l;
        }
        private MyStruct GetWindowInfo(IntPtr hWnd)
        {
            return new MyStruct() { 
                hWnd = hWnd, 
                Rect = GetWindowRect(hWnd),
                Text = GetWindowText(hWnd),
                IsVisible = API.IsWindowVisible(hWnd) };

        }

        //ウィンドウハンドルから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 = 0;
            if (API.RegisterHotKey(MyWindowHandle, hotkeyId, mod, vKey) == 0)
            {
                MessageBox.Show("登録に失敗");
            }
            else
            {
                //MessageBox.Show("登録完了");
            }
        }
        #endregion ホットキー関連









        //ウィンドウハンドルからウィンドウの情報用
        //ウィンドウのハンドル、Rect、Text、IsVisible
        private struct MyStruct
        {
            public IntPtr hWnd;
            public Rect Rect;
            public bool IsVisible;
            public string Text;

            public override string ToString()
            {
                //x16は書式で、xが16進数で表示、16が表示桁数
                return $"IntPtr({hWnd.ToString("x16")}), Rect({Rect}), 可視{IsVisible}, Text({Text})";
            }
        }
    }


}



毎回WinAPIを使うからってコピペするのもめんどくさいので

プロジェクトに前回のAPI.csファイルを追加してみた
手順

f:id:gogowaten:20210207151449p:plain
手順1
ソリューションエクスプローラーのプロジェクトの右クリックメニューの追加→既存の項目

f:id:gogowaten:20210207151641p:plain
手順2
追加したいファイルを選ぶ、今回は以前書いたAPI.csファイルを選択

f:id:gogowaten:20210207151831p:plain
手順3
API.csが追加された

f:id:gogowaten:20210207152020p:plain
手順4
追加したAPI.csを開いたところ
ネームスペースは
namespace 20210202右クリックメニュー取得

f:id:gogowaten:20210207152356p:plain
手順5
MainWindowに戻ってusingに追加
このままだとネームスペースが違って使いづらいので、usingにさっきのネームスペースを追加すれば

f:id:gogowaten:20210207152722p:plain
手順6
APIだけで使えるし

f:id:gogowaten:20210207152820p:plain
手順7
関数もそのまま使えた
こういう使い方であっているのかはわからんけど、コピペよりラク

情報表示用

//ウィンドウハンドルからウィンドウの情報用
//ウィンドウのハンドル、Rect、Text、IsVisible
private struct MyStruct
{
    public IntPtr hWnd;
    public Rect Rect;
    public bool IsVisible;
    public string Text;

    public override string ToString()
    {
        //x16は書式で、xが16進数で表示、16が表示桁数
        return $"IntPtr({hWnd.ToString("x16")}), Rect({Rect}), 可視{IsVisible}, Text({Text})";
    }
}

struct使ってみた、これもこういう使い方であっているのかはわからん、真相ハ ソウイウコトダ

確認

エクセルで確認

f:id:gogowaten:20210207162754p:plain
右クリックメニュー
小さい方のウィンドウも取得できてる

f:id:gogowaten:20210207162848p:plain
右クリックメニューのフォントサイズのドロップダウンリスト
こういうリストのRectも取得できてるってことは、これもウィンドウなんだねえ

f:id:gogowaten:20210207163026p:plain
右クリックメニューから塗りつぶしの色の選択ウィンドウ
いいね!

f:id:gogowaten:20210207163116p:plain
リボンメニューのセルから連なるメニューウィンドウ
リボンメニューも問題ない

f:id:gogowaten:20210207163159p:plain
ポップアップウィンドウ
いいね!

f:id:gogowaten:20210207163229p:plain
ポップアップウィンドウ単体

f:id:gogowaten:20210207163257p:plain
ポップアップウィンドウ単体

f:id:gogowaten:20210207163315p:plain
シートの見出し部分の右クリックメニュー


メモ帳で確認

f:id:gogowaten:20210207163343p:plain
メモ帳のメニュー
今回のはエクセル専用みたいなのものだから、うまく取得できない

f:id:gogowaten:20210207163452p:plain
メモ帳の右クリックメニュー
これも同様、メモ帳自体のウィンドウで切れてしまう

次は

前回と今回のを合わせて、普通のアプリとエクセルどちらのメニューウィンドウを取得できるようにしたいけど、難しそうなんだよねえ
今回見つけたIsWindowVisibleがあるからなんとかできるかもってところ



関連記事

次回は2日後
アプリのスクショでウィンドウ枠外のメニューもキャプチャしたい - 午後わてんのブログ

gogowaten.hatenablog.com

テストでは両対応できた!

前回のWPF記事は2日前

gogowaten.hatenablog.com

前々回は3日前

gogowaten.hatenablog.com
今回の記事はこの記事の続きにあたる

gogowaten.hatenablog.com
ホットキー

gogowaten.hatenablog.com