午後わてんのブログ

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

ウィンドウDCからのキャプチャではアルファ値が変なので、画面全体をキャプチャして切り抜き

f:id:gogowaten:20201115191705p:plain
メモ帳ウィンドウをAlt+PrintScreenでのスクショ、これが期待する結果なんだけど

f:id:gogowaten:20201115191931p:plain
ウィンドウDC(デバイスコンテキスト)から作成する方法だと、スクロールバーしかキャプチャできない
水色のところは背景色なので殆どが透明になっている

ウィンドウDC(デバイスコンテキスト)からウィンドウ画像取得

//一番手前のウィンドウ
IntPtr hWindowForeground = GetForegroundWindow();//ハンドル取得
RECT rectWindowForeground;
GetWindowRect(hWindowForeground, out rectWindowForeground);//Rect取得

//ウィンドウDCからコピー
var screenDC = GetDC(hWindowForeground);//ウィンドウのDC
var memDC = CreateCompatibleDC(screenDC);//コピー先DC作成
int width = rectWindowForeground.right - rectWindowForeground.left;
int height = rectWindowForeground.bottom - rectWindowForeground.top;
var hBmp = CreateCompatibleBitmap(screenDC, width, height);//コピー先のbitmapオブジェクト作成
SelectObject(memDC, hBmp);//コピー先DCにbitmapオブジェクトを指定
//ビットブロック転送、コピー元からコピー先へ
BitBlt(memDC, 0, 0, width, height, screenDC, 0 ,0 , 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;


どうやらアルファ値がほとんど0になっているみたいで、これは以前にも似たようなことがあった
gogowaten.hatenablog.com 色々方法があるけど、今回は見た目通りになればいいので、取得した画像をピクセルフォーマットのBgr24に変換することにした。Bgr24はアルファ値が存在しないので完全不透明画像になる

//ピクセルフォーマットをBgr24に変換することでアルファ値を255に見立てている
source = new FormatConvertedBitmap(source, PixelFormats.Bgr24, source.Palette, 0);
MyImage.Source = source;

結果は
f:id:gogowaten:20201115194714p:plain
キャプチャの位置がずれているけど、映った!

エクセルウィンドウの場合
f:id:gogowaten:20201115195134p:plain
これがもとで

ウィンドウDCからそのままだと
f:id:gogowaten:20201115195144p:plain
やっぱりあちこち透明

アルファ値修正すると
f:id:gogowaten:20201115195152p:plain
タイトルバーの色がないけど、だいぶまともになった
それでも期待するもには程遠いので別の方法

画面全体のDCから画面全体をコピーして、そこから切り出す方法

//一番手前のウィンドウ
IntPtr hWindowForeground = GetForegroundWindow();//ハンドル取得
RECT rectWindowForeground;
GetWindowRect(hWindowForeground, out rectWindowForeground);//Rect取得

var screenDC = GetDC(IntPtr.Zero);//画面全体のDC、コピー元
var memDC = CreateCompatibleDC(screenDC);//コピー先DC作成
int width = rectWindowForeground.right - rectWindowForeground.left;
int height = rectWindowForeground.bottom - rectWindowForeground.top;
var hBmp = CreateCompatibleBitmap(screenDC, width, height);//コピー先のbitmapオブジェクト作成
SelectObject(memDC, hBmp);//コピー先DCにbitmapオブジェクトを指定
//ビットブロック転送、コピー元からコピー先へ
BitBlt(memDC, 0, 0, width, height, screenDC, rectWindowForeground.left, rectWindowForeground.top, 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;

違うのは2行だけで
DCを取得するGetDCに引数にIntPtr.Zeroを渡して画面全体(プライマリウィンドウ?)のDCを取得するのと
BitBltでコピーするときに切り出す位置を指定しているところで
BitBlt(memDC, 0, 0, width, height, screenDC, rectWindowForeground.left, rectWindowForeground.top, SRCCOPY);
この太字のところ
これでキャプチャすると

メモ帳
f:id:gogowaten:20201115205621p:plain

エクセル
f:id:gogowaten:20201115205630p:plain
どちらもかなり良くなった、サイズと位置が微妙にずれているけど、これはDwmGetWindowAttribute()を使えば解決できるのは、次回



今回のアプリ
f:id:gogowaten:20201115211024p:plain
右Ctrlキー押しながら右Shiftキー押すと、そのときに最前面になってるウィンドウをキャプチャする

2020WPF/20201115_ウィンドウキャプチャ時のアルファ値
github.com

MainWindow.xaml

<Window x:Class="_20201115_ウィンドウキャプチャ時のアルファ値.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:_20201115_ウィンドウキャプチャ時のアルファ値"
        mc:Ignorable="d"
        Title="MainWindow" Height="420" Width="540">
  <Window.Resources>
    <Style TargetType="RadioButton">
      <Setter Property="Margin" Value="8,2"/>
    </Style>
  </Window.Resources>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="80"/>
      <RowDefinition/>
    </Grid.RowDefinitions>
    <StackPanel Grid.Row="0">
      <TextBlock x:Name="MyTextBlock1" Text="text1" Margin="8"/>
      <GroupBox Header="取得方法">
        <StackPanel Orientation="Horizontal">
          <RadioButton x:Name="rbScreen" Content="画面全体DCから" IsChecked="True"/>
          <RadioButton x:Name="rbWindow" Content="ウィンドウDCから"/>
          <RadioButton x:Name="rbWindowAlpah255" Content="ウィンドウDCから+アルファ値255に修正"/>
        </StackPanel>
      </GroupBox>
    </StackPanel>
    <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" UseLayoutRounding="True" Background="cyan">
      <Image x:Name="MyImage" Stretch="None"/>
    </ScrollViewer>
  </Grid>
</Window>


MainWindow.xaml.cs

using System;
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Runtime.InteropServices;//Imagingで使っている
using System.Windows.Interop;//CreateBitmapSourceFromHBitmapで使っている
using System.Windows.Threading;//DispatcherTimerで使っている

namespace _20201115_ウィンドウキャプチャ時のアルファ値
{
    /// <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;
        }


        //ウィンドウのRect取得
        [DllImport("user32.dll")]
        private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

        //手前にあるウィンドウのハンドル取得
        [DllImport("user32.dll")]
        private static extern IntPtr GetForegroundWindow();

     

        //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 Regionのことレギオンって読んでたけど、リージョンだったwwwwwwwwwwwwwwwwwwwwwwwwwwww



        //タイマー用
        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();//ハンドル取得
                RECT rectWindowForeground;
                GetWindowRect(hWindowForeground, out rectWindowForeground);//Rect取得
                MyTextBlock1.Text = $"{rectWindowForeground} 一番手前(アクティブ)ウィンドウRect";

                //画面全体の画像からウィンドウRect範囲を切り出す
                if (rbScreen.IsChecked == true)
                {
                    var screenDC = GetDC(IntPtr.Zero);//画面全体のDC、コピー元
                    var memDC = CreateCompatibleDC(screenDC);//コピー先DC作成
                    int width = rectWindowForeground.right - rectWindowForeground.left;
                    int height = rectWindowForeground.bottom - rectWindowForeground.top;
                    var hBmp = CreateCompatibleBitmap(screenDC, width, height);//コピー先のbitmapオブジェクト作成
                    SelectObject(memDC, hBmp);//コピー先DCにbitmapオブジェクトを指定
                    //ビットブロック転送、コピー元からコピー先へ
                    BitBlt(memDC, 0, 0, width, height, screenDC, rectWindowForeground.left, rectWindowForeground.top, 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;
                }

                //ウィンドウDCからコピー
                else if (rbWindow.IsChecked == true)
                {
                    var screenDC = GetDC(hWindowForeground);//ウィンドウのDC
                    var memDC = CreateCompatibleDC(screenDC);
                    int width = rectWindowForeground.right - rectWindowForeground.left;
                    int height = rectWindowForeground.bottom - rectWindowForeground.top;
                    var hBmp = CreateCompatibleBitmap(screenDC, width, height);
                    SelectObject(memDC, hBmp);
                    BitBlt(memDC, 0, 0, width, height, screenDC, 0, 0, SRCCOPY);
                    var source = Imaging.CreateBitmapSourceFromHBitmap(hBmp, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());

                    DeleteObject(hBmp);
                    ReleaseDC(IntPtr.Zero, screenDC);
                    ReleaseDC(IntPtr.Zero, memDC);

                    MyImage.Source = source;
                }

                //ウィンドウDCからコピーしてアルファ値を255にする
                else if (rbWindowAlpah255.IsChecked == true)
                {
                    var screenDC = GetDC(hWindowForeground);//ウィンドウのDC
                    var memDC = CreateCompatibleDC(screenDC);
                    int width = rectWindowForeground.right - rectWindowForeground.left;
                    int height = rectWindowForeground.bottom - rectWindowForeground.top;
                    var hBmp = CreateCompatibleBitmap(screenDC, width, height);
                    SelectObject(memDC, hBmp);
                    BitBlt(memDC, 0, 0, width, height, screenDC, 0, 0, SRCCOPY);
                    var source = Imaging.CreateBitmapSourceFromHBitmap(hBmp, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());

                    DeleteObject(hBmp);
                    ReleaseDC(IntPtr.Zero, screenDC);
                    ReleaseDC(IntPtr.Zero, memDC);

                    //ピクセルフォーマットをBgr24に変換することでアルファ値を255に見立てている
                    source = new FormatConvertedBitmap(source, PixelFormats.Bgr24, source.Palette, 0);
                    MyImage.Source = source;
                }

            }
        }


    }
}


やたらDC、DC言ってるけど、DCがなんなのかよくわかっていない



参照したところ
stackoverflow.com 今回のコードはここからのコピペなかんじ

qiita.com WindowsFormsなら、ウィンドウのDCからキャプチャでも、透明にはならないみたいねえ

docs.microsoft.com

[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;//普通にコピー

hdc コピー先(宛先)のdc
x コピー先の左上x座標
y コピー先の左上y座標
cx コピー元Rectの幅
cy コピー元Rectの高さ
hdcSrc コピー元のDC
x1 コピー元Rectの左上x座標(切り出す位置)
y1 コピー元Rectの左上y座標
rop 転送時の加工方法を指定

ropはいろいろな指定ができて、コピー元と先でand、or、xor合成の実例が
www.tokovalue.jp 面白い



関連記事
次回は翌日、ズレを解消した

gogowaten.hatenablog.com

前回は4日前
gogowaten.hatenablog.com