アプリのスクショでウィンドウ枠外のメニューもキャプチャしたい
目的
前回まで、普通のアプリとエクセル(リボンメニュー)系アプリの、それぞれではキャプチャできたので、今回は両対応版
普通系アプリ?メモ帳の場合
↑ここから、こう↓
エクセルの場合
すべてのアプリでこうするのが目的
使うWinAPI関数
ウィンドウハンドル取得系
- GetForegroundWindow
最前面ウィンドウハンドルを取得?
これで得たウィンドウハンドルを基点にするんだけど、取得できるのはアプリ自体のウィンドウだったり、メニューウィンドウだったりする - GetWindow
ウィンドウハンドルとGETWINDOW_CMD.GW_HWNDNEXTを渡すと、Z軸で1層下のウィンドウハンドルが取得できる、それを同じように渡していけば、どんどん下を辿れる - GetParent
親ウィンドウ取得 - GetAncester
祖先ウィンドウ取得、AncestorType.GA_ROOTOWNERとともに渡せば、一番元になっているウィンドウハンドルが取得できる
- GetForegroundWindow
ウィンドウの情報系?
- GetWindowRect
座標とサイズを表すRectを取得 - GetWindowText
Textを取得、たぶんタイトルバーに表示されるのと同じ、string型
- GetWindowRect
マウスカーソル下のウィンドウハンドル取得用
- ClientToScreen
マウスカーソルの座標を取得 - WindowFromPoint
指定座標にあるウィンドウハンドルを取得
- ClientToScreen
大まかな流れ
アプリのウィンドウのRectとメニューウィンドウのRectを収集して、デスクトップ画面をキャプチャした画像を、そのRectで切り抜く
Rect収集
GetForegroundWindowで得たウィンドウハンドル(以下Fore)
エクセル系なら最上層メニューウィンドウハンドルなので、そこから下層のウィンドウを取得していけば、すべてのメニューウィンドウが取得できる、あとはアプリ自体のウィンドウはForeをGetAncesterのRootOwnerで取得したのを加えれば完了
エクセル系じゃないアプリの場合Foreはアプリ自体のウィンドウになる。メニューウィンドウハンドルはマウスカーソル下のウィンドウハンドルを取得して、そこから上層と下層のウィンドウ群を取得していく方法にした、なのでマウスカーソルがメニューウィンドウの上に乗っていることが条件になる。もっといい方法があるかもしれないけど、これしか思いつかなかった
GetForegroundWindowの結果はアプリによって違う
一番いいのは
Get最前面アプリのウィンドウとか
Get最前面アプリのメニューウィンドウ
Get最前面アプリの右クリックメニューウィンドウ
こんなWinAPIがあればいいんだけど、見つけられなかった
なので、最前面アプリのウィンドウハンドルを取得するっぽいGetForegroundWindowを使って、ここから色々辿っていく
でもこれがまた、デスクトップの状態や、アプリによって変わる
GetForegroundWindowの対象になるウィンドウ(取得されるウィンドウハンドル)
アプリ名 | 状態 | GetForegroundWindow |
---|---|---|
メモ帳 | アプリ自体 | |
メモ帳 | メニュー表示 | アプリ自体 |
メモ帳 | 右クリックメニュー表示 | アプリ自体 |
Mery | アプリ自体 | |
Mery | メニュー表示 | アプリ自体 |
Mery | 右クリックメニュー表示 | アプリ自体 |
エクセル系 | アプリ自体 | |
エクセル系 | メニュー表示 | メニュー |
エクセル系 | 右クリックメニュー表示 | 右クリックメニュー |
エクセル系ってのは、メニューにリボンメニューを使っているアプリを指して勝手に言っているだけ
エクセル系以外はメニューの表示の有無に関係なく、常にアプリ自体のウィンドウが取得されるみたいで、この状態になるアプリがほとんどで、エクセルが例外というか少数派だと思う
で、そのエクセル系なんだけど、リボンメニューを使っていると思われるエクスプローラーはエクセルと違って、右クリックメニュー表示していてもアプリ自体(エクスプローラー)のウィンドウハンドルが取得される、わけわからん
でも大まかには、アプリ自体が取得されるものと、メニュー系が取得されるものの2つに分けることができる
エクセル系、普通のアプリ系の判定
アプリ自体ならタイトルバーに文字(Text)があるけど、メニューウィンドウにはないので、GetForegroundWindowで得たウィンドウハンドルをGetWindowTextに渡して、でTextプロパティを見ればわかる
エクセル系だった場合NEXTで下層ウィンドウ収集、Rectリスト作成
前回の
このときと同じ
GetForegroundWindowで最上層のメニューウィンドウが取得できているので、そこから下層のウィンドウを取得する、これにはGetWindowのGETWINDOW_CMD.GW_HWNDNEXT(以下NEXT)を使う、得られたハンドルにNEXTを使っていけば、どんどん下を辿れる、メニューが何層なのかわからないので適当に20個くらい取得
要るものだけ残して、そのRect収集
収集したウィンドウハンドルには、いらないものがいっぱいなので選別する
順番に
- 可視状態のものだけ残す
- RootOwnerがForeのRootOwnerと同じものだけ残す
- ウィンドウの見た目通りのRectを取得
- Rectリストの上から順番にRectのwidthが0を探して、見つかったところから下を除外
ここまででメニューウィンドウのRect収集は完了
追加Rect
ForeのPopupがあれば、そのRect
メニューのアイコンとかにマウスカーソルを乗せておくと、出てくる説明用の小さなウィンドウ、これのRectを追加する
これはGetWindowにENABLEDPOPUPを使うと取得できるForeのParentのTextが""ならRootOwnerのRect追加
- ""以外ならParentのRect追加
エクセル系のForeからGetParentで得られるのは、アプリ自体のウィンドウ、またはそのアプリから開いた別ウィンドウ(設定とかオプション)なのでこれを追加している
普通のアプリ系だった場合はPREVとNEXTで収集、Rectリスト作成
だいたいは
gogowaten.hatenablog.com
このときと同じ
GetForegroundWindowではメニューウィンドウのハンドルは取得できないので、カーソル下のウィンドウハンドルを取得、そこから上層と下層をPREVとNEXTそれぞれで収集後、選別した後、統合
- 可視状態のものだけ残す
- 順番にTextがあるのを探して、見つかったところ以降を除外
これは、メニューウィンドウにはTextがないはずだから - 残ったウィンドウのRectのリスト作成
- ドロップシャドウウィンドウを除外
- Rectサイズ0を除外
- 順番にRectが重なっているかを見て、途切れたところ以降を除外
- PREVとNEXTで得たRectリストを統合
Foreground(アプリ自体)との重なり判定
- 統合したRectリストを元にGeometryGroup作成、これとForegroundのRectで重なり判定
- 重なりがあればRectリストにForegroundのRect追加
- なければRectリストは破棄、ForegroundのRectだけにする
普通系アプリだった場合GetForegroundWindowで得たウィンドウハンドルはアプリ自体のウィンドウなので、これと重なっていなければ全く無関係のウィンドウってことで全破棄
結果
エクセルのセルの書式設定のウィンドウ
このウィンドウのRootOwnerにあたるエクセルのウィンドウはキャプチャされない
これが難しかった
GetForegroundWindowで取得されるのは色の一覧のウィンドウで、そのParentがセルの書式設定ウィンドウ、エクセルのウィンドウはRootOwnerだった
エクスプローラーのメニューウィンドウキャプチャ
ドロップシャドウ部分が残ってしまう、これはVisual Studioもこうなる、残念だけど解消するにはキャプチャの方法を根本的に変えないとできない感じ
普通系アプリのメニューウィンドウキャプチャ
普通系アプリの場合はメニューウィンドウの上に、マウスカーソルを置いてからキャプチャする必要がある
いいね!
Paint.NETの場合
いいね!アプリのウィンドウ外に表示されていたツールウィンドウは、キャプチャされなかった
ツールウィンドウだけがキャプチャされた
これはこれでいいのかな?
ウィンドウ外のツールウィンドウは、直前にアクティブだった「履歴」ウィンドウだけキャプチャされたけど、ドロップシャドウが残っているし、「レイヤー」ウィンドウは切れている
これはバランスが良くない気がするけど、どうかなあ
アプリによっては、ちょっと違う感じのキャプチャ結果もあるけど、いいと思う
保存とクリップボードにコピー
saveボタンで実行ファイルと同じフォルダに、png形式画像として保存される、ファイル名は日時
copyボタンは画像を普通にクリップボードにコピーしているんだけど、.NETのClipboardの仕様かなんかでアルファ値が失われる(0になる)ので、画像アプリとかに貼り付けると透明になるはずの部分が真っ黒になる
そのかわり貼り付けは、ほとんどの画像アプリならできる
copy(PNG)ボタン、こっちはアルファ値は保たれるけど、クリップボードのPNG形式画像を貼り付けることができるアプリは限られるみたいで、貼り付けることができたのは
- Paint.NET
- FireAlpaca
- Pixtack紫陽花
できなかったアプリは
- ペイント
- Pixtack紫陽花2nd
- JTrim
- IrfanView
モニター系アプリはキャプチャできない?
タスクマネージャーとか随時画面の更新しているようなアプリのウィンドウはホットキーがPrintScreenだとキャプチャできない
修飾キーをつけるとキャプチャできる
ctrl + PrintScreenならキャプチャできたけど、ctrlキーを押した瞬間からポップアップウィンドウがフェードアウトしていく
今回のテストアプリはポップアップウィンドウも撮りたかったのでホットキーはPrintScreenにしてある
コード
ダウンロード先
2021WPF/20210208_メニューキャプチャ普通のアプリとエクセル.7z at master · gogowaten/2021WPF · GitHub
作成環境
- Windows 10 Home
- Visual Studio Community 2019
- WPF
- .NET Core 5
- C#
アプリの動作には.NET Core 5が必要で、インストールされていないとアプリは動かないはず
MainWindow.xaml
<Window x:Class="_20210208_メニューキャプチャ普通のアプリとエクセル.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:_20210208_メニューキャプチャ普通のアプリとエクセル" mc:Ignorable="d" Title="20210209Test" Height="450" Width="800"> <Grid UseLayoutRounding="True"> <DockPanel> <Menu DockPanel.Dock="Top"> <MenuItem Header="save" ToolTip="実行ファイルと同じフォルダに保存" Click="MenuItem_Click"/> <MenuItem Header="copy" ToolTip="アルファ値はなくなるけど多くのアプリに貼り付け可能" Click="MenuItem_Click_1"/> <MenuItem Header="copy(PNG)" ToolTip="アルファ値は保たれるけど利用可能なアプリは限られる" Click="MenuItem_Click_2"/> </Menu> <Image x:Name="MyImage" StretchDirection="DownOnly"/> </DockPanel> </Grid> </Window>
API.cs
これは前回と全く同じ内容で、違うのはnamespaceだけ
using System; using System.Text; using System.Runtime.InteropServices; namespace _20210208_メニューキャプチャ普通のアプリとエクセル { 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); } }
MainWindow.xaml.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Windows.Interop; namespace _20210208_メニューキャプチャ普通のアプリとエクセル { /// <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 = 20; // private BitmapSource MyBitmapSource; public MainWindow() { InitializeComponent(); MyInitializeHotKey(); //ホットキーに修飾キーとPrintScreenキーを登録 //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; ChangeHotKey(mod, Key.PrintScreen, HOTKEY_ID1); //アプリ終了時にホットキーの解除 Closing += MainWindow_Closing; } //ホットキー判定 private void ComponentDispatcher_ThreadPreprocessMessage(ref MSG msg, ref bool handled) { if (msg.message != API.WM_HOTKEY) return; //ホットキーが押されたら else if (msg.wParam.ToInt32() == HOTKEY_ID1) { //画面全体をキャプチャして、Rect収集して、それを使って切り抜き画像作成 MyBitmapSource = CroppedBitmapFromRects(GetScreenBitmap(), RR()); //画像表示 MyImage.Source = MyBitmapSource; } } private List<Rect> RR() { List<Rect> R = new(); var fore = GetWindowInfo(API.GetForegroundWindow()); //エクセル系アプリ if (fore.Text == "") { MyWidndowInfo rootOwner = GetWindowInfo( API.GetAncestor(fore.hWnd, API.AncestorType.GA_ROOTOWNER)); MyWidndowInfo parent = GetWindowInfo( API.GetParent(fore.hWnd)); MyWidndowInfo popup = GetWindowInfo( API.GetWindow(rootOwner.hWnd, API.GETWINDOW_CMD.GW_ENABLEDPOPUP)); //Foreの下層にあるウィンドウハンドルをGetWindowのNEXTで収集 List<MyWidndowInfo> next = GetWindowInfos( GetCmdWindows(fore.hWnd, API.GETWINDOW_CMD.GW_HWNDNEXT, LOOP_LIMIT)); //可視状態のものだけ残す next = next.Where(x => x.IsVisible == true).ToList(); //RootOwnerがForeのRootOwnerと同じものだけ残す next = next.Where(x => rootOwner.Text == GetWindowText(API.GetAncestor(x.hWnd, API.AncestorType.GA_ROOTOWNER))).ToList(); //見た目通りのRectを取得 R = next.Select(x => GetWindowRectMitame(x.hWnd)).ToList(); //ForeNEXTを上から順番にRectを見て、width = 0が見つかったらそれ以降は除外 R = SelectNoneZeroRects(R); //popupウィンドウのRectを追加 if (popup.Rect.Width != 0) { R.Add(popup.Rect); } //ParentのTextが""ならParentは無いので、代わりにRootOwnerのRectを追加 if (parent.Text == "") { R.Add(GetWindowRectMitame(rootOwner.hWnd)); } //ParentのTextがあればダイアログボックスウィンドウが最前面なので、そのRectを追加 else { R.Add(GetWindowRectMitame(parent.hWnd)); } } //普通のアプリ else { API.GetCursorPos(out API.POINT cp); MyWidndowInfo cursor = GetWindowInfo(API.WindowFromPoint(cp)); List<MyWidndowInfo> prev = GetWindowInfos( GetCmdWindows(cursor.hWnd, API.GETWINDOW_CMD.GW_HWNDPREV, LOOP_LIMIT)); List<MyWidndowInfo> next = GetWindowInfos( GetCmdWindows(cursor.hWnd, API.GETWINDOW_CMD.GW_HWNDNEXT, LOOP_LIMIT)); R = SelectRects(prev).Union(SelectRects(next)).ToList(); //重なり判定はForegroundのRectと、それ以外のRectを結合したRectで判定する //Rectの結合はGeometryGroupを使う GeometryGroup gg = new(); for (int i = 0; i < R.Count; i++) { gg.Children.Add(new RectangleGeometry(R[i])); } //重なり判定 //重なりがなければメニューウィンドウは開かれていないと判定して //収集したRect全破棄 if (IsOverlapping(gg, new RectangleGeometry(fore.Rect)) == false) { R = new(); } //ForeのRectを追加 R.Add(GetWindowRectMitame(fore.hWnd)); //PopupのRectを追加 MyWidndowInfo popup = GetWindowInfo( API.GetWindow(fore.hWnd, API.GETWINDOW_CMD.GW_ENABLEDPOPUP)); if (popup.Rect.Width != 0) R.Add(popup.Rect); //ForegroundのウィンドウRectだけでいい } return R; } private void RRR() { var fore = GetWindowInfo(API.GetForegroundWindow()); var rootOwner = GetWindowInfo(API.GetAncestor(fore.hWnd, API.AncestorType.GA_ROOTOWNER)); //var popup = GetWindowInfos(GetCmdWindows(fore.hWnd, API.GETWINDOW_CMD.GW_ENABLEDPOPUP, LOOP_LIMIT)); //var next = GetWindowInfos(GetCmdWindows(fore.hWnd, API.GETWINDOW_CMD.GW_HWNDNEXT, LOOP_LIMIT)); //var prev = GetWindowInfos(GetCmdWindows(fore.hWnd, API.GETWINDOW_CMD.GW_HWNDPREV, LOOP_LIMIT)); //var child = GetWindowInfos(GetCmdWindows(fore.hWnd, API.GETWINDOW_CMD.GW_CHILD, LOOP_LIMIT)); API.GetCursorPos(out API.POINT cp); var cursor = GetWindowInfo(API.WindowFromPoint(cp)); var cursorrootOwner = GetWindowInfo(API.GetAncestor(cursor.hWnd, API.AncestorType.GA_ROOTOWNER)); var cursorpopup = GetWindowInfos(GetCmdWindows(cursor.hWnd, API.GETWINDOW_CMD.GW_ENABLEDPOPUP, LOOP_LIMIT)); var cursornext = GetWindowInfos(GetCmdWindows(cursor.hWnd, API.GETWINDOW_CMD.GW_HWNDNEXT, LOOP_LIMIT)); var cursorprev = GetWindowInfos(GetCmdWindows(cursor.hWnd, API.GETWINDOW_CMD.GW_HWNDPREV, LOOP_LIMIT)); var cursorchild = GetWindowInfos(GetCmdWindows(cursor.hWnd, API.GETWINDOW_CMD.GW_CHILD, LOOP_LIMIT)); } #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<MyWidndowInfo> foreNexts = GetWindowInfos(GetCmdWindows(fore, API.GETWINDOW_CMD.GW_HWNDNEXT, 20)); //可視状態のものだけ残す var noneZero = foreNexts.Where(x => x.IsVisible == true).ToList(); //ForeNEXTのRootOWNERとForeOWNERを比較、同じものだけ残す List<MyWidndowInfo> 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取得 private List<Rect> SelectRects(List<MyWidndowInfo> pList) { //可視状態のものだけ残す pList = pList.Where(x => x.IsVisible == true).ToList(); //Textを持つウィンドウ以降を除去 pList = DeleteWithTextWindow(pList); //残ったウィンドウの見た目通りのRect取得 List<Rect> rs = pList.Select(x => GetWindowRectMitame(x.hWnd)).ToList(); if (rs.Count == 0) return rs; //ドロップシャドウウィンドウのRectを除去 rs = DeleteShadowRect(rs); //サイズが0のRectを除去 rs = rs.Where(x => x.Size.Width != 0 && x.Size.Height != 0).ToList(); //前後のRectが重なっているところまで選択して、以降は除外 return SelectOverlappedRect(rs); } //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> /// ドロップシャドウ用のウィンドウを判定して、取り除いて返す。前後の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; } /// <summary> /// Textがないものをリストに追加していって、Textをもつウィンドウが出た時点で終了、リストを返す /// </summary> /// <param name="wList"></param> /// <returns></returns> private List<MyWidndowInfo> DeleteWithTextWindow(List<MyWidndowInfo> wList) { for (int i = 0; i < wList.Count; i++) { if (wList[i].Text != "") { wList.RemoveRange(i, wList.Count - i); return wList; } } return wList; } #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<MyWidndowInfo> GetWindowInfos(List<IntPtr> hWnd) { List<MyWidndowInfo> l = new(); foreach (var item in hWnd) { l.Add(GetWindowInfo(item)); } return l; } private MyWidndowInfo GetWindowInfo(IntPtr hWnd) { return new MyWidndowInfo() { 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(int mod, Key Key, int hotkeyId) { ChangeHotKey(mod, KeyInterop.VirtualKeyFromKey(Key), hotkeyId); } private void ChangeHotKey(int mod, int vKey, int hotkeyId) { //上書きはできないので、古いのを削除してから登録 _ = API.UnregisterHotKey(MyWindowHandle, hotkeyId); if (API.RegisterHotKey(MyWindowHandle, hotkeyId, mod, vKey) == 0) { MessageBox.Show("登録に失敗"); } else { //MessageBox.Show("登録完了"); } } #endregion ホットキー関連 //ウィンドウハンドルからウィンドウの情報用 //ウィンドウのハンドル、Rect、Text、IsVisible private struct MyWidndowInfo { public IntPtr hWnd; public Rect Rect; public bool IsVisible; public string Text; public override string ToString() { string visible = IsVisible == true ? "可視" : "不可視"; //x16は書式で、xが16進数で表示、16が表示桁数 return $"IntPtr({hWnd.ToString("x16")}), Rect({Rect}), {visible}, Text({Text})"; } } #region ボタンクリックイベントでの動作 //画像保存 private void MenuItem_Click(object sender, RoutedEventArgs e) { SaveImage(MyBitmapSource); } private void MenuItem_Click_1(object sender, RoutedEventArgs e) { //クリップボードのpng形式画像を読み込むことができないアプリ用 //ただし透明部分は真っ黒になる Clipboard.SetImage(MyBitmapSource); } private void MenuItem_Click_2(object sender, RoutedEventArgs e) { //png形式にして画像をクリップボードにコピー //クリップボードのpng形式画像を読み込めるアプリ用 var enc = new PngBitmapEncoder(); enc.Frames.Add(BitmapFrame.Create(MyBitmapSource)); using var ms = new System.IO.MemoryStream(); enc.Save(ms); Clipboard.SetData("PNG", ms); } #endregion ボタンクリックイベントでの動作 } }
これも前回からのコピペ改変だけど、同じようなことばかり書いていたせいか、だいぶ良くなったと思う
それでも例外処理はほとんどできていない
毎回WinAPIを使うからってコピペするのもめんどくさいので2回め
前回の記事でも書いたこれ
毎回WinAPIを使うからってコピペするのもめんどくさいので
ここの手順4までは同じなんだけど、そこから
今回はAPI.csのnamespaceをMainWindowのnamespaceに変更
namespaceとかファイルの関係とかわかってないんだよねえ、試してみたら、これでもできたので今回はそうしてみた
前回と手間は変わんないけど、usingしなくていいから1行少なくできる、あんまり変わんないな…
次は、今回のをPixcrenに取り込むというか、
キャプチャ範囲のウィンドウ特殊のところに入れる
関連記事
次回のWPF記事は翌日
前回のWPF記事は一昨日
gogowaten.hatenablog.com
今回の記事はこれ+
5日前の
gogowaten.hatenablog.com
これ