午後わてんのブログ

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

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

f:id:gogowaten:20201116193352g:plain
メモ帳とエクセルのウィンドウでテストしてるところ
GetForegroundWindow()で得られる最前面のウィンドウをキャプチャ

環境



今回のアプリ

f:id:gogowaten:20201116233538p:plain
今回の
右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;
        }
    }
}


f:id:gogowaten:20201116195637p:plain
GetWindowRect
GetWindowRect()で取得できたRectは(237, 248, 722, 430)
左から順に、left, top, right, bottom
横幅はright - left、722-237=485
縦幅はbottom - top、430-248=182
これに従って切り抜くと、左右と下に余分なところが入る

f:id:gogowaten:20201116200428p:plain
DwmGetWindowAttribute()
DwmGetWindowAttribute()で取得できるRectをもとに切り抜くと、ウィンドウ枠ぴったりになった。求めていたのはこれ

f:id:gogowaten:20201116201105p:plain
GetClientRect()
GetClientRect()はクライアント領域のRectを取得できる、といってもleftとtopは常に0を返してくるから、実質は縦横のサイズかな。
leftとtopはClientToScreen()で取得できる

エクセルウィンドウの場合

f:id:gogowaten:20201116202010p:plain
GetWindowRect
メモ帳と同じように余計なところまで入る

f:id:gogowaten:20201116202046p:plain
DwmGetWindowAttribute
これもメモ帳と同じで、枠ピッタリ、いいね

f:id:gogowaten:20201116202116p:plain
GetClientRect
これはメモ帳とぜんぜん違う
さっきのDwmGetWindowAttributeのRectと同じように見えるけど、左右と下が1ピクセル小さくなっている、つまり枠なし
エクセルはタイトルバーを持たないアプリで、ウィンドウ全体がクライアント領域になっているみたいねえ。このタイプのアプリは他にもVisual Studio、電卓、google chromeエクスプローラーがあった

右クリックメニューやメニューを開いた状態でのキャプチャ
メモ帳の場合

f:id:gogowaten:20201116204008p:plain
メモ帳の右クリックメニュー
期待通り、右クリックメニューを開いた状態でもウィンドウの表示範囲だけをキャプチャ

メニューバーのメニューを開いた状態

f:id:gogowaten:20201116204029p:plain
メモ帳のメニュー
これも期待通り
エクセルでもこうなってほしいんだけど
f:id:gogowaten:20201116204942p:plain
エクセルの右クリックメニュー
この状態でのキャプチャで期待する結果は
f:id:gogowaten:20201116205112p:plain
期待する結果
この画像なんだけど
残念なことに今回の結果は

f:id:gogowaten:20201116205410p:plain
結果
右クリックメニューだけがキャプチャされてしまった

リボンのメニューを開いた状態でキャプチャ
f:id:gogowaten:20201116210106p:plain
エクセルのリボンのメニューを開いた状態
このとき期待する結果は
f:id:gogowaten:20201116210311p:plain
期待する結果
こうなんだけど
結果は
f:id:gogowaten:20201116210404p:plain
結果
これも、メニューだけになってしまった
これはWindowから見ると、エクセルの右クリックメニューやリボンからのポップアップメニューが、1つのウィンドウとして認識されているみたい。なので階層があるメニューだと最後に開かれたものが最前面のウィンドウってことになるから、メニューだけがキャプチャされる、ってことかなあ
他にもこういう仕様のアプリがあるのかと、普段使うアプリを試したけど見つからなかった、Wordはそうだったけど普段使わない。 タイトルバーを持たないVisual Studiogoogle chromeもメニューは普通だった
f:id:gogowaten:20201116211632p:plain
Visual Studioのメニュー開いたところをキャプチャ





参照したところ
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

f:id:gogowaten:20201116223324p:plain
ClipDesk
このアプリを使った、Ctrl+F11で最前面のウィンドウキャプチャ

フリー(無料)かつインストール無しで使えるスクリーンショット、スクリーンキャプチャと言われるアプリを
スクリーンショット・キャプチャ - k本的に無料ソフト・フリーソフト

www.gigafree.net

無料画面キャプチャーソフト一覧 - フリーソフト100

freesoft-100.com

こちらを参考にして
10個くらい試した中で、エクセルのメニューを開いた状態でキャプチャ+マウスカーソルも描画できたのは、ClipDeskだけだった。ClipDeskはエクセルのことわかっている
他はメニューだけのキャプチャになってしまうものが多くて、ウィンドウごとキャプチャできてもマウスカーソルは描画できなかった。

GIFアニメーション作成でメモリ5GB使用

f:id:gogowaten:20201116223712p:plain
ScreenToGif
今年の5月から使っているアプリのScreenToGif

ScreenToGif - Record your screen, edit and save as a gif, video or other formats
www.screentogif.com

とても便利

f:id:gogowaten:20201117003025p:plain
ファイル保存の設定
GIFアニメーションエンコード設定で、FFmpegを使ってディザリング方式を指定できるのが特にいい
こんなかんじで作っていて、今回できあがったgifファイルサイズ自体は840KBと、それほど大きくはないんだけど、30fpsで録画時間が2分、画像サイズが604x797(797?4の倍数じゃない?)と大きかったせいか、エンコード中のメモリ使用量が
f:id:gogowaten:20201116224658p:plain
エンコード中のメモリ使用量
5GB超えてたwww
1回目のエンコード中は常用アプリを起動したままだったので、メモリが足りなかったらしく途中でエラーになってしまった。なので2GB以上使っているgoogle chromeと1GB使っているVisual Studioを終了してから再エンコードしたわけなんだけど
f:id:gogowaten:20201116230759p:plain
メモリ使用量
PCのメモリは16GBじゃ足りないわ、今の8GBx2に16GBx2を増設で合計48GBにしたいなあ



関連記事
次回は2日後

gogowaten.hatenablog.com

前回は昨日

gogowaten.hatenablog.com



利用して作ったアプリ
gogowaten.hatenablog.com