ウィンドウの見た目通りのRect取得はDwmGetWindowAttribute
C#での画面キャプチャの取得方法を徹底解説! | .NETコラム
https://www.fenet.jp/dotnet/column/language/4633/
ここを見るのが早いかなあ
DwmGetWindowAttribute
//DWM(Desktop Window Manager) //見た目通りのRectを取得できる、引数のdwAttributeにDWMWA_EXTENDED_FRAME_BOUNDSを渡す //引数のcbAttributeにはRECTのサイズ、Marshal.SizeOf(typeof(RECT))これを渡す //戻り値が0なら成功、0以外ならエラー値 [DllImport("dwmapi.dll")] private static extern long DwmGetWindowAttribute(IntPtr hWnd, DWMWINDOWATTRIBUTE dwAttribute, out RECT rect, int cbAttribute); //ウィンドウ属性 //列挙値の開始は0だとずれていたので1からにした 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 };
2番めの引数はDWMWINDOWATTRIBUTEとかいう列挙値で、たくさん種類があるけど、ウィンドウの見た目通りのRectを取得したいときに使うのは
DWMWA_EXTENDED_FRAME_BOUNDS
値にするとInt型で9
メモ帳とエクセルのウィンドウでテストしてるところ
GetForegroundWindow()で得られる最前面のウィンドウをキャプチャ
環境
- Windows 10 Home
- Visual Studio 2019 Community
- C#、WPF、.NET Core 3.1
- Excel 2007
今回のアプリ
右Ctrl+右Shiftで、そのとき最前面のウィンドウをキャプチャして表示する
2020WPF/20201116_ウィンドウキャプチャ時の見た目とのズレを修正
github.com
MainWindow.xaml
<Window x:Class="_20201116_ウィンドウキャプチャ時の見た目とのズレを修正.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:_20201116_ウィンドウキャプチャ時の見た目とのズレを修正" mc:Ignorable="d" Title="MainWindow" Height="500" Width="600"> <Window.Resources> <Style TargetType="RadioButton"> <Setter Property="Margin" Value="8,2"/> </Style> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="130"/> <RowDefinition/> </Grid.RowDefinitions> <StackPanel Grid.Row="0"> <GroupBox Header="Rect取得方法"> <StackPanel Orientation="Horizontal"> <RadioButton x:Name="rbRect" Content="GetWindowRect" IsChecked="True"/> <RadioButton x:Name="rbRectDwm" Content="DwmGetWindowAttribute"/> <RadioButton x:Name="rbRectClient" Content="GetClientRect"/> </StackPanel> </GroupBox> <TextBlock x:Name="MyTextBlock1" Text="text1" Margin="8,2"/> <TextBlock x:Name="MyTextBlock2" Text="text1" Margin="8,2"/> <TextBlock x:Name="MyTextBlock3" Text="text1" Margin="8,2"/> <Button x:Name="MyButton" Content="imageClear" Click="MyButton_Click"/> </StackPanel> <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" UseLayoutRounding="True" Background="Magenta"> <Image x:Name="MyImage" Stretch="None"/> </ScrollViewer> </Grid> </Window>
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.Runtime.InteropServices;//Imagingで使っている using System.Windows.Interop;//CreateBitmapSourceFromHBitmapで使っている using System.Windows.Threading;//DispatcherTimerで使っている namespace _20201116_ウィンドウキャプチャ時の見た目とのズレを修正 { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { #region WindowsAPI^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //キーの入力取得 [DllImport("user32.dll")] private static extern short GetAsyncKeyState(int vKey); //Rect取得用 private 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})"; } } //座標取得用 private struct POINT { public int X; public int Y; } //手前にあるウィンドウのハンドル取得 [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); //ウィンドウのRect取得 [DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); //ウィンドウのクライアント領域のRect取得 [DllImport("user32.dll")] private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); //クライアント領域の座標を画面全体での座標に変換 [DllImport("user32.dll")] private static extern bool ClientToScreen(IntPtr hWnd, out POINT lpPoint); //DWM(Desktop Window Manager) //見た目通りのRectを取得できる、引数のdwAttributeにDWMWA_EXTENDED_FRAME_BOUNDSを渡す //引数のcbAttributeにはRECTのサイズ、Marshal.SizeOf(typeof(RECT))これを渡す //戻り値が0なら成功、0以外ならエラー値 [DllImport("dwmapi.dll")] private static extern long DwmGetWindowAttribute(IntPtr hWnd, DWMWINDOWATTRIBUTE dwAttribute, out RECT rect, int cbAttribute); //ウィンドウ属性 //列挙値の開始は0だとずれていたので1からにした 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 }; //DC取得 //nullを渡すと画面全体のDCを取得、ウィンドウハンドルを渡すとそのウィンドウのクライアント領域DC //失敗した場合の戻り値はnull //使い終わったらReleaseDC [DllImport("user32.dll")] private static extern IntPtr GetDC(IntPtr hWnd); //渡したDCに互換性のあるDC作成 //失敗した場合の戻り値はnull //使い終わったらDeleteDC [DllImport("gdi32.dll")] private static extern IntPtr CreateCompatibleDC(IntPtr hdc); //指定されたDCに関連付けられているデバイスと互換性のあるビットマップを作成 //使い終わったらDeleteObject [DllImport("gdi32.dll")] private static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int cx, int cy); //DCにオブジェクトを指定する、オブジェクトの種類はbitmap、brush、font、pen、Regionなど [DllImport("gdi32.dll")] private static extern IntPtr SelectObject(IntPtr hdc, IntPtr h); [DllImport("gdi32.dll")] private static extern bool BitBlt(IntPtr hdc, int x, int y, int cx, int cy, IntPtr hdcSrc, int x1, int y1, uint rop); private const int SRCCOPY = 0x00cc0020; private const int SRCINVERT = 0x00660046; [DllImport("user32.dll")] private static extern bool DeleteDC(IntPtr hdc); [DllImport("user32.dll")] private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC); [DllImport("gdi32.dll")] private static extern bool DeleteObject(IntPtr ho); //ウィンドウ系のAPI //Windows(Windowsおよびメッセージ)-Win32アプリ | Microsoft Docs // https://docs.microsoft.com/en-us/windows/win32/winmsg/windows #endregion コピペ呪文ここまで^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //タイマー用 DispatcherTimer MyTimer; public MainWindow() { InitializeComponent(); //タイマー初期化 MyTimer = new DispatcherTimer(); MyTimer.Interval = new TimeSpan(0, 0, 0, 0, 100);//0.1秒間隔 MyTimer.Tick += MyTimer_Tick; MyTimer.Start(); //アプリ終了時、タイマーストップ this.Closing += (s, e) => { MyTimer.Stop(); }; } private void MyTimer_Tick(object sender, EventArgs e) { //キー入力取得用 //Keyを仮想キーコードに変換 int vKey1 = KeyInterop.VirtualKeyFromKey(Key.RightCtrl); int vKey2 = KeyInterop.VirtualKeyFromKey(Key.RightShift); //キーの状態を取得 short key1state = GetAsyncKeyState(vKey1); short key2state = GetAsyncKeyState(vKey2); //右Ctrlキー+右Shiftキーが押されていたら if ((key1state & 0x8000) >> 15 == 1 & ((key2state & 1) == 1)) { //一番手前のウィンドウのハンドル取得 IntPtr hWindowForeground = GetForegroundWindow(); //ウィンドウキャプチャ Capture(hWindowForeground); } } //ウィンドウキャプチャ //画面全体をキャプチャして、そこからウィンドウのRect領域を切り抜いて表示 private void Capture(IntPtr hWnd) { //通常のRECT、見た目とのズレが有る GetWindowRect(hWnd, out RECT windowRect); //見た目通りのRect DwmGetWindowAttribute(hWnd, DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS, out RECT windowExRect, Marshal.SizeOf(typeof(RECT))); //クライアント領域のRECT GetClientRect(hWnd, out RECT clientRect); MyTextBlock1.Text = $"{windowRect} 通常のWindowRect"; MyTextBlock2.Text = $"{windowExRect} 通常のWindowRectのズレ修正"; MyTextBlock3.Text = $"{clientRect} クライアント領域のRect"; int width = 1, height = 1;//切り抜きサイズ int offsetX = 0, offsetY = 0;//切り抜きの左上座標 if (rbRect.IsChecked == true) { //通常RECT width = windowRect.right - windowRect.left; height = windowRect.bottom - windowRect.top; offsetX = windowRect.left; offsetY = windowRect.top; } else if (rbRectDwm.IsChecked == true) { //見た目通りのRect width = windowExRect.right - windowExRect.left; height = windowExRect.bottom - windowExRect.top; offsetX = windowExRect.left; offsetY = windowExRect.top; } else if (rbRectClient.IsChecked == true) { //クライアント領域RECT width = clientRect.right;//GetClientRectのRECTのtopとleftは、 height = clientRect.bottom;//常に0なので引き算は不必要 //画面全体におけるクライアント領域の左上座標を取得 ClientToScreen(hWnd, out POINT cPoint); offsetX = cPoint.X; offsetY = cPoint.Y; } var screenDC = GetDC(IntPtr.Zero);//画面全体のDC、コピー元 var memDC = CreateCompatibleDC(screenDC);//コピー先DC作成 var hBmp = CreateCompatibleBitmap(screenDC, width, height);//コピー先のbitmapオブジェクト作成 SelectObject(memDC, hBmp);//コピー先DCにbitmapオブジェクトを指定 //コピー元からコピー先へビットブロック転送 //通常のコピーなのでSRCCOPYを指定 BitBlt(memDC, 0, 0, width, height, screenDC, offsetX, offsetY, SRCCOPY); //bitmapオブジェクトからbitmapSource作成 BitmapSource source = Imaging.CreateBitmapSourceFromHBitmap(hBmp, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); //後片付け DeleteObject(hBmp); ReleaseDC(IntPtr.Zero, screenDC); ReleaseDC(IntPtr.Zero, memDC); //画像表示 MyImage.Source = source; } private void MyButton_Click(object sender, RoutedEventArgs e) { MyImage.Source = null; MyTextBlock1.Text = ""; MyTextBlock2.Text = string.Empty; MyTextBlock3.Text = null; } } }
GetWindowRect()で取得できたRectは(237, 248, 722, 430)
左から順に、left, top, right, bottom
横幅はright - left、722-237=485
縦幅はbottom - top、430-248=182
これに従って切り抜くと、左右と下に余分なところが入る
DwmGetWindowAttribute()で取得できるRectをもとに切り抜くと、ウィンドウ枠ぴったりになった。求めていたのはこれ
GetClientRect()はクライアント領域のRectを取得できる、といってもleftとtopは常に0を返してくるから、実質は縦横のサイズかな。
leftとtopはClientToScreen()で取得できる
エクセルウィンドウの場合
メモ帳と同じように余計なところまで入る
これもメモ帳と同じで、枠ピッタリ、いいね
これはメモ帳とぜんぜん違う
さっきのDwmGetWindowAttributeのRectと同じように見えるけど、左右と下が1ピクセル小さくなっている、つまり枠なし
エクセルはタイトルバーを持たないアプリで、ウィンドウ全体がクライアント領域になっているみたいねえ。このタイプのアプリは他にもVisual Studio、電卓、google chrome、エクスプローラーがあった
右クリックメニューやメニューを開いた状態でのキャプチャ
メモ帳の場合
メニューバーのメニューを開いた状態
これも期待通り
エクセルでもこうなってほしいんだけど
この状態でのキャプチャで期待する結果は
この画像なんだけど
残念なことに今回の結果は
右クリックメニューだけがキャプチャされてしまった
リボンのメニューを開いた状態でキャプチャ
このとき期待する結果は
こうなんだけど
結果は
これも、メニューだけになってしまった
これはWindowから見ると、エクセルの右クリックメニューやリボンからのポップアップメニューが、1つのウィンドウとして認識されているみたい。なので階層があるメニューだと最後に開かれたものが最前面のウィンドウってことになるから、メニューだけがキャプチャされる、ってことかなあ
他にもこういう仕様のアプリがあるのかと、普段使うアプリを試したけど見つからなかった、Wordはそうだったけど普段使わない。
タイトルバーを持たないVisual Studioやgoogle chromeもメニューは普通だった
参照したところ
C# - アクティブウィンドウが正しくキャプチャされない|teratail
https://teratail.com/questions/103093
プログラマから見たWindows 10 #4 ~ 最新情報とプログラミングTips | OPTPiX Labs Blog
https://www.webtech.co.jp/blog/os/win10/8445/
見た目のウィンドウサイズの違いは、ここがわかりやすい
DwmGetWindowAttribute function (dwmapi.h) - Win32 apps | Microsoft Docs
https://docs.microsoft.com/ja-jp/windows/win32/api/dwmapi/nf-dwmapi-dwmgetwindowattribute
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/0642cb2f-2075-4469-918c-4441e69c548a
Common HRESULT Values - Win32 apps | Microsoft Docs
https://docs.microsoft.com/ja-jp/windows/win32/seccrypto/common-hresult-values
ASCII.jp:Windowsで表示されるエラーコードの見方
https://ascii.jp/elem/000/001/432/1432965/
DwmGetWindowAttributeの戻り値が0以外だったときの詳細確認
今回の記事中のエクセルのウィンドウのキャプチャで、期待する結果として使った画像の作成には
ClipDesk
http://hp.vector.co.jp/authors/VA028640/software/m_cdesk.xhtml
このアプリを使った、Ctrl+F11で最前面のウィンドウキャプチャ
フリー(無料)かつインストール無しで使えるスクリーンショット、スクリーンキャプチャと言われるアプリを
スクリーンショット・キャプチャ - k本的に無料ソフト・フリーソフト
無料画面キャプチャーソフト一覧 - フリーソフト100
こちらを参考にして
10個くらい試した中で、エクセルのメニューを開いた状態でキャプチャ+マウスカーソルも描画できたのは、ClipDeskだけだった。ClipDeskはエクセルのことわかっている
他はメニューだけのキャプチャになってしまうものが多くて、ウィンドウごとキャプチャできてもマウスカーソルは描画できなかった。
GIFアニメーション作成でメモリ5GB使用
今年の5月から使っているアプリのScreenToGif
ScreenToGif - Record your screen, edit and save as a gif, video or other formats
www.screentogif.com
とても便利
GIFアニメーションのエンコード設定で、FFmpegを使ってディザリング方式を指定できるのが特にいい
こんなかんじで作っていて、今回できあがったgifファイルサイズ自体は840KBと、それほど大きくはないんだけど、30fpsで録画時間が2分、画像サイズが604x797(797?4の倍数じゃない?)と大きかったせいか、エンコード中のメモリ使用量が
5GB超えてたwww
1回目のエンコード中は常用アプリを起動したままだったので、メモリが足りなかったらしく途中でエラーになってしまった。なので2GB以上使っているgoogle chromeと1GB使っているVisual Studioを終了してから再エンコードしたわけなんだけど
PCのメモリは16GBじゃ足りないわ、今の8GBx2に16GBx2を増設で合計48GBにしたいなあ
関連記事
次回は2日後
前回は昨日
利用して作ったアプリ
gogowaten.hatenablog.com