C#で拡大率が違うマルチディスプレイ1画面キャプチャ

2022年05月19日
複数の異なる解像度、異なる拡大率のディスプレイを使っているときに、一つの画面のスクリーンキャプチャを頻繁に保存したかったのですが、良い感じのツールがなかったので自分で作った時のメモです。

環境
・マルチディスプレイ
 メイン:3840 x 2160 150%
 サブ:1920 x 1080 100%
・Windows10
・Visual Studio 2017、または2022
・C#

スポンサーサイト

きっかけ


最近は、いろいろなライブイベントがオンラインで開催されるようになりました。
オンラインライブの場合、録音録画はNGですが、スクショしてTwitter等で共有するのはOKの場合があります。
なので、ライブを見ながらどんどん画面キャプチャして、保存したいです。

私の場合、デュアルディスプレイにしているので、ライブ中は大きなディスプレイにライブ映像を映し、サブモニターにTwitter等を表示していますので、ライブ映像のディスプレイの画面だけのスクリーンショットを撮りたいです。

画面キャプチャして保存する方法と言えば、「Windowsキー + PrtScキー」ですが、マルチディスプレイの環境だと両方のディスプレイを統合したような画面が「ピクチャ」の「スクリーンショット」フォルダに保存されてしまいます。
↓こんな感じに2画面分が結合された画像になります。
ss1.png

いくつかフリーのキャプチャツールを探してみたのですが、私の環境では以下の現象が発生してしまいました。
・やはり両方のディスプレイが結合されたキャプチャになる
・ディスプレイ設定で拡大率を変えていると、キャプチャした画像サイズがおかしい
(もっと探せば、良いツールもあると思いますが・・・)

というわけで、自分に合ったものを作ろうと思って試行錯誤した話です。
今回はC#で作ることにしました。

C#でのディスプレイのキャプチャと保存


調べてみると、キャプチャして保存するだけなら、やり方はすぐ見つかりました。
ざっくり書くとこんな感じです。(※ 下記のtargetScreenオブジェクトは、キャプチャしたいディスプレイのScreenオブジェクトです)
Bitmap bm = new Bitmap(targetScreen.Bounds.Width, targetScreen.Bounds.Height);
Graphics gr = Graphics.FromImage(bm);

gr.CopyFromScreen(new Point(0, 0), new Point(0, 0), bm.Size);

string saveFolderPath = Path.Combine(Application.StartupPath, "Capture");
string ext = ".jpeg";
string filepath = Path.Combine(saveFolderPath, DateTime.Now.ToString("yyyyMMdd_HHmmssfff") + ext);

ImageFormat imageFormat = ImageFormat.Jpeg;
bm.Save(filepath, imageFormat);

gr.Dispose();


しかし、やはり拡大率が100%以外がディスプレイが混ざっている場合、キャプチャした画像のサイズがおかしくなります・・・

高DPI対応


ひとつの原因は、下記の高DPI対応をしていないことでした。
高 DPI サポート - Windows Forms .NET Framework | Microsoft Docs

私のVisual Studio 2017の環境の場合、Windows 10以降向けだけなら、
app.configの<configuration>の中に
<System.Windows.Forms.ApplicationConfigurationSection>
<add key="DpiAwareness" value="PerMonitorV2" />
</System.Windows.Forms.ApplicationConfigurationSection>
を記載

で、良いようです。
以前は、
マニュフェストファイル(app.manifest)を作成して
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
だったようですが、app.config ファイルの設定が上書きされるため、現在では推奨されないそうです。
また、Visual Studio 2022だと、この設定を明示的に設定しなくても、デフォルトでDPI対応になっているようです。

ただ、上記設定をしてもマルチディスプレイで、それぞれの解像度、拡大率が異なる場合、やはりうまくキャプチャできませんでした。

EnumDisplaySettingsAによる実解像度の取得


明確なことはわかりませんが、メインディスプレイの拡大率をベースにScreenオブジェクトのサイズが設定されている気がします。
なので、拡大率の異なるサブディスプレイでは、解像度をうまく取得できてないっぽいです。

そこで、実際の物理ディスプレイの解像度を取得するため、Windows APIのEnumDisplaySettingsAを利用することにしました。
EnumDisplaySettingsA function (winuser.h) - Win32 apps | Microsoft Docs

C#でEnumDisplaySettingsAを利用するためには、まずEnumDisplaySettingsAを宣言し、引数で必要なDEVMODE構造体を定義します。
const int ENUM_CURRENT_SETTINGS = -1;

[DllImport("user32.dll")]
public static extern bool EnumDisplaySettingsA(string lpszDeviceName, int iModeNum, ref DEVMODE lpDevMode);

[StructLayout(LayoutKind.Sequential)]
public struct DEVMODE
{
private const int CCHDEVICENAME = 0x20;
private const int CCHFORMNAME = 0x20;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
public string dmDeviceName;
public short dmSpecVersion;
public short dmDriverVersion;
public short dmSize;
public short dmDriverExtra;
public int dmFields;
public int dmPositionX;
public int dmPositionY;
public ScreenOrientation dmDisplayOrientation;
public int dmDisplayFixedOutput;
public short dmColor;
public short dmDuplex;
public short dmYResolution;
public short dmTTOption;
public short dmCollate;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
public string dmFormName;
public short dmLogPixels;
public int dmBitsPerPel;
public int dmPelsWidth;
public int dmPelsHeight;
public int dmDisplayFlags;
public int dmDisplayFrequency;
public int dmICMMethod;
public int dmICMIntent;
public int dmMediaType;
public int dmDitherType;
public int dmReserved1;
public int dmReserved2;
public int dmPanningWidth;
public int dmPanningHeight;
}


このEnumDisplaySettingsAで引数にディスプレイのデバイス名を指定し、取得できたDEVMODEのdmPelsWidthとdmPelsHeightが実際のディスプレイのサイズです。

また、dmPositionXとdmPositionYが、仮想的な(?)ディスプレイの中での物理ディスプレイの原点のようです。
(EnumDisplaySettingsAのAPI仕様には、書かれてないですけどなぜか取得できてる)

で、これを呼び出すように書き換えます。
DEVMODE dm = new DEVMODE();
dm.dmSize = (short)Marshal.SizeOf(typeof(DEVMODE));
EnumDisplaySettingsA(targetScreen.DeviceName, ENUM_CURRENT_SETTINGS, ref dm);

Bitmap bm = new Bitmap(dm.dmPelsWidth, dm.dmPelsHeight);
Graphics gr = Graphics.FromImage(bm);

gr.CopyFromScreen(new Point(dm.dmPositionX, dm.dmPositionY), new Point(0, 0), bm.Size);

string saveFolderPath = Path.Combine(Application.StartupPath, "Capture");
string ext = ".jpeg";
string filepath = Path.Combine(saveFolderPath, DateTime.Now.ToString("yyyyMMdd_HHmmssfff") + ext);

ImageFormat imageFormat = ImageFormat.Jpeg;
bm.Save(filepath, imageFormat);

gr.Dispose();


これで特定のディスプレイのみのスクリーンをちゃんとしたサイズでキャプチャできるようになりました。

できたツール


他の環境で動くかまったくわかりませんが、一応、できたツールを置いておきます。
全然テストしてないので、何が起こっても自己責任でお願いします。
ScreenCapture.zip
解凍して「ScreenCaptureWithMultiDisplay.exe」を実行してください。

以上

スポンサーサイト

タグ:C#
posted at 00:56 | Comment(0) | プログラミング
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: