LoginSignup
9
3

Delphi11でDesktop Duplicationを使ってモニタをキャプチャする(VCL版)

Last updated at Posted at 2022-12-15

はじめに

Desktop Duplication API はWindows8から利用できるモニタ単位で画面をキャプチャするAPIです。
これをDelphiで利用したいと思います。
このAPIではマウスカーソルを取得するには手数がかかります。今回はマウスカーソルなしでのキャプチャをやります。
今回はVCLで実装します。

開発環境

  • Delphi 11.2
    インライン変数宣言を使っていますので,10.3以降で利用できると思います。
  • Windows 10(21H2)

準備するもの

Desktop Duplication APIを利用するためのヘッダーファイルをダウンロードしておきます。

より,ダウンロードします。このヘッダーファイルのライセンスはApache License 2.0です。

プロジェクトを作成

新規プロジェクトの作成

ファイル-新規作成ーWindows VCL アプリケーション - Delphi で新規プロジェクトを作成し,保存します。
ターゲットプラットフォームはデフォルトでは32bitですが,64bitでも作成可能です。

DX12ヘッダーファイルの追加

GitHubよりダウンロードしたDelphiDX12ヘッダーのZIPファイルにあるUnitフォルダから以下のファイルを新規プロジェクトと同じフォルダにコピーし,新規プロジェクトに追加します。

  • DX12.D3D11.pas
  • DX12.D3DCommon.pas
  • DX12.DXGI.pas
  • DX12.DXGI1_2.pas

DX12ヘッダーファイルの修正

IDEで追加したファイルを開くと,E2026エラーが8か所で出ますので修正します。
Snap0011.png

このエラーの詳しい回避方法については Delphi 10.3以降: 列挙型で順序値を割り当てたときに起きるE2026エラーを回避する でご確認ください。
以下の3つのファイルについて以下のように修正します。

DX12.D3D11.pas
1790:        D3D11_STANDARD_MULTISAMPLE_PATTERN = int32($ffffffff),
1791:        D3D11_CENTER_MULTISAMPLE_PATTERN = int32($fffffffe)
2339:        D3D11_VIDEO_PROCESSOR_ITELECINE_CAPS_OTHER = int32($80000000)
2703:        D3D11_BUS_IMPL_MODIFIER_NON_STANDARD = int32($80000000)
DX12.DXGI.pas
260:    DXGI_FORMAT_FORCE_UINT = INT32($ffffffff));
433:    DXGI_COLOR_SPACE_CUSTOM = INT32($FFFFFFFF));
684:    DXGI_ADAPTER_FLAG_FORCE_DWORD = INT32($ffffffff)
DX12.DXGI1_2.pas
109:        DXGI_ALPHA_MODE_FORCE_DWORD = INT32($ffffffff)

修正が済んだら,構文チェックをして修正がうまくできたか確認してください。

ヘッダファイルを利用するクラスを作成

ファイルー新規作成ーユニット - Delphi でユニットを作成し,uVCLDesktopDuplication.pasとして保存します。
以下のように編集してください。

uVCLDesktopDuplication.pas
unit uVCLDesktopDuplication;

interface

uses
  System.Classes, System.Types,
  Winapi.Windows,
  DX12.DXGI, DX12.DXGI1_2, DX12.D3DCommon, DX12.D3D11,
  Vcl.Graphics;

type
  TVCLDesktopDuplication = class(TObject)
  private
    { Private 宣言 }
    FFeatureLevel:TD3D_FEATURE_LEVEL;
    FDuplicate:   IDXGIOutputDuplication;
    FDevice:      ID3D11Device;
    FContext:     ID3D11DeviceContext;
    FisCreated:   boolean;
    FBitmap:      TBitmap;
  public
    constructor CreateParamMonitor(AMonitorNum:integer);
    destructor Destroy; override;
    function  FrameGet: Boolean;
    property isCreated:boolean read FisCreated;
    property Bitmap:TBitmap read FBitmap;
  end;

implementation

{ TThreadDesktopDuplication }

constructor TVCLDesktopDuplication.CreateParamMonitor(AMonitorNum: integer);
var
  LDevice:  IDXGIDevice;
  LAdapter: IDXGIAdapter;
  LOutput:  IDXGIOutput;
  LOutput1: IDXGIOutput1;
begin
  FisCreated:=False;
  if Failed(D3D11CreateDevice(               // https://learn.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-d3d11createdevice
    nil,                                     // *pAdapter,
    D3D_DRIVER_TYPE_HARDWARE,                // DriverType,
    0,                                       // Software,
    Ord(D3D11_CREATE_DEVICE_SINGLETHREADED), // Flags,
    nil,                                     // *pFeatureLevels,
    0,                                       // FeatureLevels,
    D3D11_SDK_VERSION,                       // SDKVersion,
    FDevice,                                 // **ppDevice,
    FFeatureLevel,                           // *pFeatureLevel,
    @FContext)                               // **ppImmediateContext
  ) then Exit;
  if Failed(FDevice.QueryInterface(          // System.IInterface.QueryInterface
    IID_IDXGIDevice,                         // const IID: TGUID;
    LDevice)                                 // out Obj
  ) then Exit;
  if Failed(LDevice.GetParent(               // DX12.DXGI.IDXGIObject.GetParent
    IID_IDXGIAdapter,                        // const riid: TGUID
    Pointer(LAdapter))                       // out ppParent: Pointer
  ) then Exit;
  if Failed(LAdapter.EnumOutputs(            // DX12.DXGI.IDXGIAdapter.EnumOutputs
    AMonitorNum,                             // Output: UINT  ( Monitor )
    LOutput)                                 // out ppOutput: IDXGIOutput
  ) then Exit;
  if Failed(LOutput.QueryInterface(          // System.IInterface.QueryInterface
    IID_IDXGIOutput1,                        // const IID: TGUID;
    LOutput1)                                // out Obj
  ) then Exit;
  if Failed(LOutput1.DuplicateOutput(        // DX12.DXGI1_2.IDXGIOutput1.DuplicateOutput
    FDevice,                                 // pDevice: IUnknown
    FDuplicate)                              // out ppOutputDuplication: IDXGIOutputDuplication
  ) then Exit;
  FBitmap:=TBitmap.Create;
  FBitmap.PixelFormat:=pf32Bit;
  FisCreated:=True;
end;

destructor TVCLDesktopDuplication.Destroy;
begin
  FBitmap.Free;
  inherited;
end;

function TVCLDesktopDuplication.FrameGet: Boolean;
var
  LResource: IDXGIResource;
  LFrameInfo:TDXGI_OUTDUPL_FRAME_INFO;
  LTx2D:     ID3D11Texture2D;
begin
  Result:=False;
  if Failed(FDuplicate.AcquireNextFrame(0, LFrameInfo, LResource)) then Exit;
  if LFrameInfo.TotalMetadataBufferSize > 0 then begin
    if Failed(LResource.QueryInterface(IID_ID3D11Texture2D, LTx2D)) then Exit;
    // FBitmapに描画
    var LDesc:     TD3D11_TEXTURE2D_DESC;
    var LTx2DTemp: ID3D11Texture2D;
    var LMappedRes:TD3D11_MAPPED_SUBRESOURCE;
    var i:         Integer;
    var LPByte:    PByte;
    var LWidth:    UInt;
    LTx2D.GetDesc(LDesc);
    LDesc.BindFlags := 0;
    LDesc.CPUAccessFlags := Ord(D3D11_CPU_ACCESS_READ) or Ord(D3D11_CPU_ACCESS_WRITE);
    LDesc.Usage := D3D11_USAGE_STAGING;
    LDesc.MiscFlags := 0;
    if Failed(FDevice.CreateTexture2D(LDesc, nil, LTx2DTemp)) then Exit;
    FContext.CopyResource(LTx2DTemp, LTx2D);
    FContext.Map(LTx2DTemp, 0, D3D11_MAP_READ_WRITE, 0, LMappedRes);
    LPByte := LMappedRes.pData;
    FBitmap.SetSize(LDesc.Width, LDesc.Height);
    LWidth := 4*LDesc.Width;
    for i := 0 to LDesc.Height-1 do begin
      Move(LPByte^, FBitmap.ScanLine[i]^, LWidth);
      Inc(LPByte, LWidth);
    end;
    Result := True;
  end;
  FDuplicate.ReleaseFrame;
end;

end.

Form1のデザイン編集

以下のように,Form1にTComboBox, TButton, TImage, TTimerを追加します。
Snap0014.png

ComboBox1のデザイン編集

ComboBox1のオブジェクトインスペクタでStyleをcsDropDownListに変更します。

Image1のデザイン編集

Image1のオブジェクトインスペクタで
 AnchorsのakRightとakBotomをTrueに変更します。
 StretchをTrueに変更します。

Timer1のデザイン編集

Timer1のオブジェクトインスペクタでEnabledをFalseに変更します。

Form1でuVCLDesktopDupulicationを利用する

uVCLDesktopDuplicationinterfaceusesに追加し,TForm1のprivateFDuplicationを追加します。
FDuplicationを追加のコードは以下のようになります。

Unit1.pas
type
  TForm1 = class(TForm)
    ComboBox1: TComboBox;
    Button1: TButton;
    Image1: TImage;
    Timer1: TTimer;
  private
    { Private 宣言 }
    DesktopDuplication: TVCLDesktopDuplication;
  public
    { Public 宣言 }
  end;

Form1にイベントを追加する

Form1にOnCreateとOnDestroyを追加

Form1のオブジェクトインスペクタでOnCreateイベントを追加して以下のように編集します。

Unit1.pas
procedure TForm1.FormCreate(Sender: TObject);
var
  i:integer;
begin
  ComboBox1.Items.Clear;
  for i:=0 to Screen.MonitorCount-1 do begin
    ComboBox1.Items.Add(
      Screen.Monitors[i].MonitorNum.ToString+': '
     +Screen.Monitors[i].Width.ToString+'x'
     +Screen.Monitors[i].Height.ToString
    );
  end;
  ComboBox1.ItemIndex:=0;
  DesktopDuplication:=nil;
end;

Form1のオブジェクトインスペクタでOnDestroyイベントを追加して以下のように編集します。

Unit1.pas
procedure TForm1.FormDestroy(Sender: TObject);
begin
  DesktopDuplication.Free;
end;

Button1にOnClickを追加

Button1のオブジェクトインスペクタでOnClickイベントを追加して以下のように編集します。

Unit1.pas
procedure TForm1.Button1Click(Sender: TObject);
begin
  DesktopDuplication.Free;
  DesktopDuplication := TVCLDesktopDuplication.CreateParamMonitor(ComboBox1.ItemIndex);
  Timer1.Enabled := DesktopDuplication.isCreated;
end;

Timer1にOnTimerイベントを追加

Timer1のオブジェクトインスペクタでOnTimerイベントを追加して以下のように編集します。

Unit1.pas
procedure TForm1.Timer1Timer(Sender: TObject);
begin
  if DesktopDuplication.FrameGet then begin
    Image1.Picture.Bitmap.Assign(DesktopDuplication.Bitmap);
  end;
end;

ここまでのUnit1.pasの編集結果

Unit1.pasは以下のようになります。このコードだけを張り付けてもフォームのイベントとつながらないので,必ずフォームのオブジェクトインスペクタのイベントタブからイベントのメソッド書くようにしてください。

Unit1.pas
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls,
  uVCLDesktopDuplication;

type
  TForm1 = class(TForm)
    ComboBox1: TComboBox;
    Button1: TButton;
    Image1: TImage;
    Timer1: TTimer;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure FormShow(Sender: TObject);
  private
    { Private 宣言 }
    DesktopDuplication: TVCLDesktopDuplication;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  DesktopDuplication.Free;
  DesktopDuplication := TVCLDesktopDuplication.CreateParamMonitor(ComboBox1.ItemIndex);
  Timer1.Enabled := DesktopDuplication.isCreated;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  i:integer;
begin
  ComboBox1.Items.Clear;
  for i:=0 to Screen.MonitorCount-1 do begin
    ComboBox1.Items.Add(
      Screen.Monitors[i].MonitorNum.ToString+': '
     +Screen.Monitors[I].Width.ToString+'x'
     +Screen.Monitors[I].Height.ToString
    );
  end;
  ComboBox1.ItemIndex:=0;
  DesktopDuplication:=nil;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  DesktopDuplication.Free;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  if DesktopDuplication.FrameGet then begin
    Image1.Picture.Bitmap.Assign(DesktopDuplication.Bitmap);
  end;
end;

end.

実行して,ComboBox1でキャプチャしたいモニターを選択し,Button1をクリックするとキャプチャが始まります。1秒間隔で更新されます。
Snap0015.png
Timer1Intervalを短くするとより細かい動きを表示できます。
Windows8より古いOSで起動した場合動作しないので,その処理を追加したほうが良いと思います。

今回はマウスカーソルは取得しませんでしたがそれ以外についてもLearn Microsoftの記事を読めばできるのではないかと思います。

謝辞(引用元および参考文献)

以下のリンクが参考になりました。

貴重な情報を提供してくださった皆様,ありがとうございます。感謝します。

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3