WPF スクリーンショット シリーズ
第 1 章 GDI+ を使用してスクリーンショットをキャプチャする
第 2 章 DockPanel を使用してスクリーンショット フレームを作成する
第 3 章 スクリーンショット フレームのホットキー キャプチャを実装する (この章)
第 4 章 スクリーンショット フレームのリアルタイム キャプチャを実現する
第 5 章 ffmpeg コマンド ラインを使用して画面記録を実現する
記事ディレクトリ
序文
「C# wpf は DockPanel を使用してスクリーンショット ボックスを実装する」では、スクリーンショット ボックスを実装しました。次に、対応するスクリーンショット関数を実装する必要があります。スクリーンショット領域を取得し、GDI+ を使用してスクリーンショットを取得します。ここでは、ホット キーに応答してスクリーンショット インターフェイスをポップアップ表示する、スクリーンショット ボックスをクリックしてドラッグする、スクリーンショット領域を逆方向にドラッグするなど、多くの詳細を処理する必要があります。 、異なる dpi での座標位置の処理など。
1. 導入手順
1. ホットキーに応答する
win32 API の RegisterHotKey と UnregisterHotKey を直接使用できます。WindowのSourceInitializedイベントにホットキーを登録する 以下は、alt+dをホットキーとして登録するサンプルコードです。
[System.Runtime.InteropServices.DllImport("user32")]
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint controlKey, uint virtualKey);
[System.Runtime.InteropServices.DllImport("user32")]
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
HotKey は、オンラインで見つけることができる RegisterHotKey と UnregisterHotKey をカプセル化するオブジェクトです。
private void Window_SourceInitialized(object sender, EventArgs e)
{
//注册alt+d热键,0x44为d,其他虚拟键值请查看:https://learn.microsoft.com/zh-tw/windows/win32/inputdev/virtual-key-codes
HotKey k = new HotKey(this, HotKey.KeyFlags.MOD_ALT, 0x44);
k.OnHotKey += K_OnHotKey;
Visibility = Visibility.Collapsed;
}
2. スクリーンショットの表示
(1) 画面領域の取得
画面領域を取得するには win32 API を使用する必要があります。wpf メソッドを使用して取得した画面解像度は dpi に基づいています。変換に PointToScreen を使用した場合でも、プログラムの実行中にシステム dpi を変更すると、依然として不正確になります。 , そのため、画面を直接取得する必要があります。gdi+ スクリーンショットに使用される実際のピクセル解像度。
const int DESKTOPVERTRES = 117;
const int DESKTOPHORZRES = 118;
[DllImport("gdi32.dll")]
static extern int GetDeviceCaps(
IntPtr hdc, // handle to DC
int nIndex // index of capability
);
[DllImport("user32.dll")]
static extern IntPtr GetDC(IntPtr ptr);
[DllImport("user32.dll", EntryPoint = "ReleaseDC")]
static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDc);
/// <summary>
/// 获取真实设置的桌面分辨率大小
/// </summary>
static Size DESKTOP
{
get
{
IntPtr hdc = GetDC(IntPtr.Zero);
Size size = new Size();
size.Width = GetDeviceCaps(hdc, DESKTOPHORZRES);
size.Height = GetDeviceCaps(hdc, DESKTOPVERTRES);
ReleaseDC(IntPtr.Zero, hdc);
return size;
}
}
(2) 傍受と表示
上記の手順で取得した画面キャプチャ領域を使用し、「C# wpf GDI+を使用して画面キャプチャを実現する」の簡易画面キャプチャと組み合わせて完成させます。Bitmap オブジェクトを取得したら、別の記事「C# wpf Bitmap を WriteableBitmap に変換する方法 (BitmapSource)」を参照して wpf オブジェクトに変換し、ImageBrush を通じてコントロールの背景に割り当ててコントロール上に表示します。 。
//截屏并显示到窗口
void Snapshot()
{
//获取桌面实际分辨率,可以解决程序运行后修改dpi,截图区域不正常的问题
var leftTop = new Point(0, 0);
var rightBottom = new Point(DESKTOP.Width, DESKTOP.Height);
var bitmap = Snapshot((int)leftTop.X, (int)leftTop.Y, (int)(rightBottom.X - leftTop.X), (int)(rightBottom.Y - leftTop.Y));
var bmp = BitmapToWriteableBitmap(bitmap);
bitmap.Dispose();
//显示到窗口
grdGlobal.Background = new ImageBrush(bmp);
}
3. 自動キャプチャウィンドウ
qq や WeChat のスクリーンショットにはウィンドウを自動的にキャプチャする機能があり、この機能は自分で実現することもできます。
(1) システムのすべてのウィンドウを取得します
システム内のすべてのウィンドウは、win32 API を通じて列挙できます。すべてのウィンドウの位置とサイズを記録する必要があります。WindowList 関連のコードはオンラインで見つけることができるため、ここでは省略します。
//获取桌面所有窗口
_windows = WindowList.GetAllWindows();
IntPtr hwnd = new WindowInteropHelper(this).Handle;
//去除不可见窗口以及自己
_windows.RemoveAll((ele) => {
return !ele.isVisible || ele.Handle == hwnd; });
(2) マウス位置による検索ウィンドウ
//窗口是以z顺序排列的查找到第一个匹配的窗口即可
var screenPoint = grdGlobal.PointToScreen(point);
foreach (var window in _windows)
{
if (window.rect.Contains(screenPoint))
//获取在鼠标所在区域的窗口
{
try
{
if (window.rect.Right > window.rect.Left && window.rect.Bottom > window.rect.Top)
//
{
var topLeft = grdGlobal.PointFromScreen(window.rect.TopLeft);
var bottomRight = grdGlobal.PointFromScreen(window.rect.BottomRight);
Thickness thickness = new Thickness(topLeft.X, topLeft.Y, grdGlobal.ActualWidth - bottomRight.X, grdGlobal.ActualHeight - bottomRight.Y);
//修正边界
if (thickness.Left < 0) thickness.Left = 0;
if (thickness.Top < 0) thickness.Top = 0;
if (thickness.Right < 0) thickness.Right = 0;
if (thickness.Bottom < 0) thickness.Bottom = 0;
//将截屏框显示在窗口位置
leftPanel.Width = thickness.Left;
topPanel.Height = thickness.Top;
rightPanel.Width = thickness.Right;
bottomPanel.Height = thickness.Bottom;
break;
}
}
catch {
}
}
}
(3) エフェクトプレビュー
2. スクリーンショットボックスをクリックしてドラッグします。
スクリーンショット インターフェイスが表示されたら、qq または WeChat の実装を参照してください。最初のクリックでスクリーンショット ボックスをドラッグして選択できます。サンプリング描画の方法が非常に簡単な場合は、直接長方形を描画するだけです。ただし、この機能をコントロールベースで実現するにはある程度のスキルが必要であり、この機能は「C# wpf は DockPanel を使用して画面キャプチャ フレームを実現する」をベースに実現しています。
(1) クリックした位置へ移動
マウスダウンイベントまたは移動の実装時
//将截屏框移动到点击位置
leftPanel.Width = p.X;
topPanel.Height = p.Y;
rightPanel.Width = grdGlobal.ActualWidth - p.X;
bottomPanel.Height = grdGlobal.ActualHeight - p.Y;
(2) プレスイベントのシミュレーション
上記のコードに従うと、親指は右下隅のドラッグ ポイントになります。
//手动触发截屏框滑块拖动事件
MouseButtonEventArgs downEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left)
{
RoutedEvent = FrameworkElement.MouseLeftButtonDownEvent };
thumb.RaiseEvent(downEvent);
(3) 正しいオフセット
シミュレートされたクリック イベントであるため、マウスが Thumb 上にない場合があります。この場合、親指の位置を修正する必要があり、オフセットは Thumb の DragStarted イベントに記録されます。
//滑块需要的偏移量
Point? _thumbOffset;
var thumb = sender as FrameworkElement;
if (!new Rect(0, 0, thumb.ActualWidth, thumb.ActualHeight).Contains(new Point(e.HorizontalOffset, e.VerticalOffset)))
//鼠标起始位置超出了控件范围,则记录中心点偏移在拖动时修正
{
_thumbOffset = new Point(e.HorizontalOffset - thumb.ActualWidth / 2, e.VerticalOffset - thumb.ActualHeight / 2);
}
ThumbのDragDeltaイベントに修正ロジックを追加
var horizontalChange = e.HorizontalChange;
var verticalChange = e.VerticalChange;
if (_thumbOffset != null)
//修正偏移
{
horizontalChange += _thumbOffset.Value.X;
verticalChange += _thumbOffset.Value.Y;
}
(4) エフェクトプレビュー
3. 逆ドラッグ
この手順は必須ではありませんが、QQ や WeChat のスクリーンショットでは逆ドラッグがサポートされるなど、あった方が操作性が向上します。gdi または gdi+ を使用してスクリーンショット フレームを描画する場合、RECT のサイズは負の値になる可能性があるため、当然ながら逆ドラッグがサポートされます。ただし、コントロールベースなので、ある程度の難しさがあります。コントロールの幅と高さを負の値にすることはできないため、イベント転送機構を実装する必要があります。この機能は、「C# wpf は DockPanel を使用して実装する」というベースで実装しています。スクリーンショットボックス」 。
(1) 判定境界
「C# wpf は DockPanel を使用してスクリーンショット ボックスを実装する」の元のロジックは、Thumb が境界に到達すると何も操作を実行しないというものですが、境界に到達したときにイベント転送を実行するように拡張する必要があります。
水平方向の親指
if (width >= 0)
{
leftPanel.Width = left >= 0 ? left : 0;
rightPanel.Width = right >= 0 ? right : 0;
}
else{
//此处将事件转移到反方向的Thumb
}
ポートレートの親指
if (height >= 0)
{
topPanel.Height = top >= 0 ? top : 0;
bottomPanel.Height = bottom >= 0 ? bottom : 0;
}
else
{
//此处将事件转移到反方向的Thumb
}
(2) イベント転送
//当前的Thumb触发鼠标弹起事件,结束拖动
MouseButtonEventArgs upEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left)
{
RoutedEvent = FrameworkElement.MouseLeftButtonUpEvent };
thumb.RaiseEvent(upEvent);
//反方向的Thumb触发鼠标按下事件,开始拖动
MouseButtonEventArgs downEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left)
{
RoutedEvent = FrameworkElement.MouseLeftButtonDownEvent };
t.RaiseEvent(downEvent);
(3) 正しい境界線
上記の2つの手順を完了すると、逆ドラッグができるようになりますが、問題が発生します。あまりに早く移動すると、スクリーンショットの枠の位置が移動してしまいます。この問題を解決するには、イベントの境界位置を修正する必要があります。たとえ双方が一致したとしても。
水平方向の親指
if (thumb.HorizontalAlignment == HorizontalAlignment.Left)
//从左到右转移的修正
{
leftPanel.Width = grdGlobal.ActualWidth - rightPanel.Width;
}
else
//从右到左转移的修正
{
rightPanel.Width = grdGlobal.ActualWidth - leftPanel.Width;
}
ポートレートの親指
if (thumb.VerticalAlignment == VerticalAlignment.Top)
//从上到下转移的修正
{
topPanel.Height = grdGlobal.ActualHeight - bottomPanel.Height;
}
else
//从下到上转移的修正
{
bottomPanel.Height = grdGlobal.ActualHeight - topPanel.Height;
}
(4) エフェクトプレビュー
4. 写真をキャプチャする
先ほどのスクリーンショットはデスクトップ全体の画像なので、保存する際にはスクリーンショットの枠に合わせて画面をキャプチャする必要があり、これを実現するにはWriteableBitmapオブジェクトを使用します。
//获取截屏框的图片
WriteableBitmap GetClipImage()
{
var bursh = grdGlobal.Background as ImageBrush;
if (bursh != null)
{
//裁剪
//全屏图片
var screenWb = bursh.ImageSource as WriteableBitmap;
//获取截取区域
var leftTop = clipRect.PointToScreen(new Point(0, 0));
var rightBottom = clipRect.PointToScreen(new Point(clipRect.ActualWidth, clipRect.ActualHeight));
var rect = new Int32Rect((int)leftTop.X, (int)leftTop.Y, (int)(rightBottom.X - leftTop.X), (int)(rightBottom.Y - leftTop.Y));
//创建截取图片对象
var wb = new WriteableBitmap(rect.Width, rect.Height, 0, 0, screenWb.Format, null);
//写入截取区域数据
wb.WritePixels(rect, screenWb.BackBuffer, screenWb.PixelHeight * screenWb.BackBufferStride, screenWb.BackBufferStride, 0, 0);
return wb;
}
return null;
}
5. ペーストボードをセットアップする
Clipboard.SetImage を直接使用するだけです。パラメータの型は、WriteableBitmap の基本クラスである BitmapSource です。
Clipboard.SetImage(GetClipImage());
2. dpiについて
1. 異なる dpi に適応する
dpiが異なる場合もありますが、どのdpiでも正常にスクリーンショットが撮影できます。
2. dpi のリアルタイム変更をサポートしていません。
(1) 現象
プログラムの起動後、dpi がリアルタイムで変更され、表示されるスクリーンショットがぼやけてしまいます。主な理由は、異なる API 間の dpi 計算が均一ではないことです。システム dpi がリアルタイムで変更された後、wpf インターフェイスは oloaded に応じてそのサイズを自動的に調整しますが、一部のプログラム (getWindowRect など) 内の dpi は変更されません。特に、レンダリングされたイメージは依然としてプログラム起動時にdpiが高くなるため、拡大縮小して表示されるため、どうしても画像がぼやけてしまいます。
具体的なプロセス例は次のとおりです。
win11 解像度 1920x1080
1. 初期システム dpi は 120 (1.25 倍)
2. プログラムの起動
3. プログラム dpi は 120
5. 全画面ウィンドウ サイズ 1536x864、win32 API で取得した値は 1920x1080、スクリーンショット 1920x1080 表示、スクリーンショットはロスレスです
6. システム dpi は 96 (1 倍) に設定されています 7.
この時点で、プログラム dpi は 120 です
8. 全画面ウィンドウ サイズは 1920x1080 で、win32 API を通じて取得されます。 2400x1350。スクリーンショットは 1920x1080 で表示され、スクリーンショットはぼやけています。
ピクセル単位で描画するため、左上隅に表示される画像はウィンドウ全体に表示されません。
(2) 試した解決策
著者は、この問題を解決するためにさまざまな方法を試しました
。 1. 事前に画像を拡大縮小してから表示します。
2. dpi の問題を解決するためのMicrosoftの方法を参照してください。
3. GDI+ グラフィックスを使用して、HDC を通じてピクセル単位で直接描画します。
4. gdi の bitblt を使用して hdc をコピーします。
上記の方法は効果がなく、画像は依然としてぼやけています。
3. 提案
dpiのリアルタイム変更をサポートする必要がある場合、スクリーンショット機能を別のプログラムとして使用し、ホットキーに応答した後に起動できます。
3. 完全なコード
https://download.csdn.net/download/u013113678/88308050
説明: スクリーンショットを撮る操作方法は QQ や WeChat と同様で、現在設定されているホットキーは alt+d です。
4. エフェクトのプレビュー
1. スクリーンショットを QQ に貼り付けます
2. スクリーンショットをファイルに保存します
要約する
今回は以上が今回の内容ですが、この記事ではWPFのスクリーンショットボックスのホットキーを使ってスクリーンショットを撮る方法を紹介します。まだまだ実装すべき機能は多く、機能も小さくありませんが、何度か試した結果、適切な実装方法を見つけましたが、dpiブラーをリアルタイムに変更する問題については、現時点では解決できないという結論に至りました。これは wpf の制限ではありません。プログラムのグローバル dpi を設定する win32 API インターフェイスがない限り、c++ mfc では動作しません。私はそれを見つけられませんでした。したがって、この問題は現時点ではスタンドアロン プログラムを起動することによってのみ解決できます。しかし、一般的に、達成される効果は非常に良好で、特にイベント転送を通じて実装されるリバース ドラッグは非常に優れており、インターフェイスの操作も非常にスムーズです。