截图优化 (#1573)

* 截图优化

* 窗口选择排除WS_EX_LAYERED

云原神

* 优化原神窗口判断
This commit is contained in:
Shatyuka
2025-05-12 22:39:02 +08:00
committed by GitHub
parent abd823c4b5
commit 66afbc83ae
7 changed files with 89 additions and 86 deletions

View File

@@ -12,7 +12,7 @@ public class SystemControl
{
public static nint FindGenshinImpactHandle()
{
return FindHandleByProcessName("YuanShen", "GenshinImpact", "Genshin Impact Cloud Game");
return FindHandleByProcessName("YuanShen", "GenshinImpact", "Genshin Impact Cloud Game", "Genshin Impact Cloud");
}
public static async Task<nint> StartFromLocalAsync(string path)
@@ -322,4 +322,4 @@ public class SystemControl
Debug.WriteLine($"Shutdown方法执行出错: {ex.Message}");
}
}
}
}

View File

@@ -1,6 +1,7 @@
<Window x:Class="BetterGenshinImpact.View.PickerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:view="clr-namespace:BetterGenshinImpact.View"
Title="选择捕获窗口"
Width="800"
Height="450"
@@ -143,7 +144,7 @@
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<DataTemplate DataType="{x:Type view:PickerWindow+CapturableWindow}">
<Grid Margin="12,8" Height="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
@@ -180,4 +181,4 @@
</ListBox>
</ScrollViewer>
</Grid>
</Window>
</Window>

View File

@@ -10,27 +10,32 @@ using Vanara.PInvoke;
using System.Windows.Media;
using System.Collections.Generic;
using System.Windows.Media.Imaging;
using System.Runtime.InteropServices;
namespace BetterGenshinImpact.View;
public partial class PickerWindow : Window
{
private static readonly string[] _ignoreProcesses = ["applicationframehost", "shellexperiencehost", "systemsettings", "winstore.app", "searchui"];
private bool _isSelected = false;
public PickerWindow()
private bool _isSelected;
private readonly bool _captureTest;
private const User32.WindowStylesEx IgnoreExStyle = User32.WindowStylesEx.WS_EX_TOOLWINDOW |
User32.WindowStylesEx.WS_EX_NOREDIRECTIONBITMAP |
User32.WindowStylesEx.WS_EX_LAYERED;
public PickerWindow(bool captureTest = false)
{
InitializeComponent();
this.InitializeDpiAwareness();
Loaded += OnLoaded;
_captureTest = captureTest;
}
public class CapturableWindow
{
public string Name { get; set; }
public string ProcessName { get; set; }
public IntPtr Handle { get; set; }
public ImageSource Icon { get; set; }
public class CapturableWindow(IntPtr handle, string name, string processName, ImageSource? icon)
{
public IntPtr Handle { get; } = handle;
public string Name { get; } = name;
public string ProcessName { get; } = processName;
public ImageSource? Icon { get; } = icon;
}
private void OnLoaded(object sender, RoutedEventArgs e)
@@ -38,29 +43,33 @@ public partial class PickerWindow : Window
FindWindows();
}
public bool PickCaptureTarget(IntPtr hWnd,out IntPtr PickedWindow)
public bool PickCaptureTarget(IntPtr hWnd, out IntPtr pickedWindow)
{
new WindowInteropHelper(this).Owner = hWnd;
ShowDialog();
if(!_isSelected)
{
PickedWindow = IntPtr.Zero;
pickedWindow = IntPtr.Zero;
return false;
}
PickedWindow = ((CapturableWindow?)WindowList.SelectedItem)?.Handle ?? IntPtr.Zero;
pickedWindow = ((CapturableWindow?)WindowList.SelectedItem)?.Handle ?? IntPtr.Zero;
return true;
}
private unsafe void FindWindows()
private void FindWindows()
{
var wih = new WindowInteropHelper(this);
var windows = new List<CapturableWindow>();
User32.EnumWindows((hWnd, lParam) =>
{
if (!User32.IsWindowVisible(hWnd) || wih.Handle == hWnd)
return true;
var exStyle = User32.GetWindowLong<User32.WindowStylesEx>(hWnd, User32.WindowLongFlags.GWL_EXSTYLE);
if ((exStyle & IgnoreExStyle) != 0)
return true;
var title = new StringBuilder(1024);
_ = User32.GetWindowText(hWnd, title, title.Capacity);
if (string.IsNullOrWhiteSpace(title.ToString()))
@@ -68,26 +77,22 @@ public partial class PickerWindow : Window
_ = User32.GetWindowThreadProcessId(hWnd, out var processId);
var process = Process.GetProcessById((int)processId);
if (_ignoreProcesses.Contains(process.ProcessName.ToLower()))
return true;
// 获取窗口图标
var icon = GetWindowIcon((IntPtr)hWnd);
windows.Add(new CapturableWindow
{
Handle = (IntPtr)hWnd,
Name = title.ToString(),
ProcessName = process.ProcessName,
Icon = icon
});
windows.Add(new CapturableWindow((IntPtr)hWnd, title.ToString(), process.ProcessName, icon));
return true;
}, IntPtr.Zero);
WindowList.ItemsSource = windows;
var sortedWindows = windows.OrderByDescending(IsGenshinWindow)
.ThenByDescending(x => x.Handle).ToList();
WindowList.ItemsSource = sortedWindows;
}
private ImageSource GetWindowIcon(IntPtr hWnd)
private ImageSource? GetWindowIcon(IntPtr hWnd)
{
try
{
@@ -126,19 +131,23 @@ public partial class PickerWindow : Window
// 如果获取失败返回一个默认图标或null
return null;
}
private bool IsGenshinWindow(string windowName)
private static bool IsGenshinWindow(CapturableWindow window)
{
// 判断是否包含原神相关的进程名 TODO更加健壮的判断
return windowName == "原神";
return window is
{Name: "原神", ProcessName: "YuanShen"} or
{Name: "云·原神", ProcessName: "Genshin Impact Cloud Game"} or
{Name: "Genshin Impact", ProcessName: "GenshinImpact"} or
{Name: "Genshin Impact · Cloud", ProcessName: "Genshin Impact Cloud"};
}
private bool AskIsThisGenshinImpact(string windowName)
private static bool AskIsThisGenshinImpact(CapturableWindow window)
{
var res = MessageBox.Question(
$"""
这看起来不像是原神,确定要选择这个窗口吗?
当前选择的窗口:{windowName}
当前选择的窗口:{window.Name} ({window.ProcessName})
""",
"确认选择",
MessageBoxButton.YesNo,
@@ -149,13 +158,13 @@ public partial class PickerWindow : Window
private void WindowsOnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var selectedWindow = WindowList.SelectedItem as CapturableWindow;
if (selectedWindow == null) return;
if (WindowList.SelectedItem is not CapturableWindow selectedWindow)
return;
// 如果不是原神窗口,询问用户是否确认
if (!IsGenshinWindow(selectedWindow.Name))
if (!_captureTest && !IsGenshinWindow(selectedWindow))
{
if (!AskIsThisGenshinImpact(selectedWindow.Name))
if (!AskIsThisGenshinImpact(selectedWindow))
{
return;
}
@@ -164,9 +173,3 @@ public partial class PickerWindow : Window
Close();
}
}
public struct CapturableWindow
{
public string Name { get; set; }
public IntPtr Handle { get; set; }
}

View File

@@ -168,7 +168,7 @@ public partial class HomePageViewModel : ViewModel
[RelayCommand]
private void OnStartCaptureTest()
{
var picker = new PickerWindow();
var picker = new PickerWindow(true);
if (picker.PickCaptureTarget(new WindowInteropHelper(UIDispatcherHelper.MainWindow).Handle, out var hWnd))
{
@@ -453,4 +453,4 @@ public partial class HomePageViewModel : ViewModel
};
var result = dialogWindow.ShowDialog();
}
}
}

View File

@@ -13,10 +13,6 @@ public class BitBltSession : IDisposable
private readonly object _lockObject = new();
// 大小计算好的宽高,截图用这个不会爆炸
private readonly int _width;
private readonly int _height;
// 位图句柄
private Gdi32.SafeHBITMAP _hBitmap;
@@ -71,15 +67,15 @@ public class BitBltSession : IDisposable
if (_hdcSrc.IsInvalid) throw new Exception($"Failed to get DC for {_hWnd}");
var hdcRasterCaps = Gdi32.GetDeviceCaps(_hdcSrc, Gdi32.DeviceCap.RASTERCAPS);
if ((hdcRasterCaps | 0x800) == 0) // RC_STRETCHBLT
if ((hdcRasterCaps | 1) == 0) // RC_BITBLT
// 设备不支持 BitBlt
throw new Exception("BitBlt not supported");
var hdcSrcPixel = Gdi32.GetDeviceCaps(_hdcSrc, Gdi32.DeviceCap.BITSPIXEL);
// 颜色位数
if (hdcSrcPixel != 32)
// 目前只考虑支持32位RGBA,HDR什么的先放放
throw new Exception("BitBlt only support 32 bit RGBA pixel color");
if (hdcSrcPixel != 32 && hdcSrcPixel != 24)
// 目前只考虑支持24/32位像素格式
throw new Exception("BitBlt only support 24 or 32 bit pixel color");
var hdcSrcPlanes = Gdi32.GetDeviceCaps(_hdcSrc, Gdi32.DeviceCap.PLANES);
// 颜色平面数
@@ -99,27 +95,16 @@ public class BitBltSession : IDisposable
throw new Exception($"Failed to create CompatibleDC for {_hWnd}");
}
var maxW = Gdi32.GetDeviceCaps(_hdcDest, Gdi32.DeviceCap.HORZRES);
var maxH = Gdi32.GetDeviceCaps(_hdcDest, Gdi32.DeviceCap.VERTRES);
if (maxW <= 0 || maxH <= 0)
// 显示器不见啦
throw new Exception("Can not get display size");
// 避免截取全屏窗口的时候超出CompatibleDC范围
_width = Math.Min(maxW, Width);
_height = Math.Min(maxH, Height);
var bmi = new Gdi32.BITMAPINFO
{
bmiHeader = new Gdi32.BITMAPINFOHEADER
{
biSize = (uint)Marshal.SizeOf<Gdi32.BITMAPINFOHEADER>(),
biWidth = _width,
biHeight = -_height, // Top-down image
biPlanes = (ushort)hdcSrcPlanes,
biBitCount = (ushort)(hdcSrcPixel - 8), //RGBA->RGB
biCompression = Gdi32.BitmapCompressionMode.BI_RGB,
biWidth = Width,
biHeight = -Height, // Top-down image
biPlanes = 1,
biBitCount = 24,
biCompression = Gdi32.BitmapCompressionMode.BI_RGB, // 内存里是BGR
biSizeImage = 0
}
};
@@ -172,25 +157,25 @@ public class BitBltSession : IDisposable
lock (_lockObject)
{
// 截图
var success = Gdi32.StretchBlt(_hdcDest, 0, 0, _width, _height,
_hdcSrc, 0, 0, _width, _height, Gdi32.RasterOperationMode.SRCCOPY);
var success = Gdi32.BitBlt(_hdcDest, 0, 0, Width, Height,
_hdcSrc, 0, 0, Gdi32.RasterOperationMode.SRCCOPY);
if (!success || !Gdi32.GdiFlush()) return null;
// 新Mat
var buffer = AcquireBuffer();
var step = _width * 3;
var step = Width * 3;
if (_stride == step)
{
Buffer.MemoryCopy(_bitsPtr.ToPointer(), buffer.ToPointer(), _bufferSize, _bufferSize);
}
else
{
for (var i = 0; i < _height; i++)
for (var i = 0; i < Height; i++)
{
Buffer.MemoryCopy((void*)(_bitsPtr + _stride * i), (void*)(buffer + step * i), step, step);
}
}
return BitBltMat.FromPixelData(this, _height, _width, MatType.CV_8UC3, buffer, step);
return BitBltMat.FromPixelData(this, Height, Width, MatType.CV_8UC3, buffer, step);
}
}

View File

@@ -56,7 +56,7 @@ public partial class SharedSurfaceCapture : IGameCapture
/// </summary>
/// <param name="hWnd"></param>
/// <returns></returns>
private ResourceRegion? GetGameScreenRegion(nint hWnd)
private static ResourceRegion? GetGameScreenRegion(nint hWnd)
{
var exStyle = User32.GetWindowLong(hWnd, User32.WindowLongFlags.GWL_EXSTYLE);
if ((exStyle & (int)User32.WindowStylesEx.WS_EX_TOPMOST) != 0)
@@ -67,7 +67,7 @@ public partial class SharedSurfaceCapture : IGameCapture
ResourceRegion region = new();
User32.GetWindowRect(hWnd, out var windowWithShadowRect);
DwmApi.DwmGetWindowAttribute<RECT>(hWnd, DwmApi.DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS, out var windowRect);
User32.GetClientRect(_hWnd, out var clientRect);
User32.GetClientRect(hWnd, out var clientRect);
region.Left = windowRect.Left - windowWithShadowRect.Left;
// 标题栏 windowRect.Height - clientRect.Height 上阴影 windowRect.Top - windowWithShadowRect.Top
@@ -106,6 +106,9 @@ public partial class SharedSurfaceCapture : IGameCapture
if (_stagingTexture == null || _surfaceWidth != surfaceTexture.Description.Width ||
_surfaceHeight != surfaceTexture.Description.Height)
{
if (User32.IsIconic(_hWnd))
return null;
_stagingTexture?.Dispose();
_stagingTexture = null;
_surfaceWidth = surfaceTexture.Description.Width;

View File

@@ -40,6 +40,10 @@ public class GraphicsCapture(bool captureHdr = false) : IGameCapture
// 用于获取帧数据的临时纹理和暂存资源
private Texture2D? _stagingTexture;
// Surface 大小
private int _surfaceWidth;
private int _surfaceHeight;
private long _lastFrameTime;
private readonly Stopwatch _frameTimer = new();
@@ -65,6 +69,8 @@ public class GraphicsCapture(bool captureHdr = false) : IGameCapture
throw new InvalidOperationException("Failed to create capture item.");
}
_surfaceWidth = _captureItem.Size.Width;
_surfaceHeight = _captureItem.Size.Height;
// 创建D3D设备
_d3dDevice = Direct3D11Helper.CreateDevice();
@@ -124,7 +130,7 @@ public class GraphicsCapture(bool captureHdr = false) : IGameCapture
/// </summary>
/// <param name="hWnd"></param>
/// <returns></returns>
private ResourceRegion? GetGameScreenRegion(nint hWnd)
private static ResourceRegion? GetGameScreenRegion(nint hWnd)
{
var exStyle = User32.GetWindowLong(hWnd, User32.WindowLongFlags.GWL_EXSTYLE);
if ((exStyle & (int)User32.WindowStylesEx.WS_EX_TOPMOST) != 0)
@@ -135,9 +141,9 @@ public class GraphicsCapture(bool captureHdr = false) : IGameCapture
ResourceRegion region = new();
DwmApi.DwmGetWindowAttribute<RECT>(hWnd, DwmApi.DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS,
out var windowRect);
User32.GetClientRect(_hWnd, out var clientRect);
User32.GetClientRect(hWnd, out var clientRect);
POINT point = default;
User32.ClientToScreen(_hWnd, ref point);
User32.ClientToScreen(hWnd, ref point);
region.Left = point.X > windowRect.Left ? point.X - windowRect.Left : 0;
region.Top = point.Y > windowRect.Top ? point.Y - windowRect.Top : 0;
@@ -199,21 +205,26 @@ public class GraphicsCapture(bool captureHdr = false) : IGameCapture
}
_lastFrameTime = _frameTimer.ElapsedMilliseconds;
var frameSize = _captureItem!.Size;
var captureSize = _captureItem!.Size;
// 检查帧大小是否变化
if (frameSize.Width != frame.ContentSize.Width || frameSize.Height != frame.ContentSize.Height)
if (captureSize.Width != _surfaceWidth || captureSize.Height != _surfaceHeight)
{
frameSize = frame.ContentSize;
if (User32.IsIconic(_hWnd))
return;
_captureFramePool!.Recreate(
_d3dDevice,
_pixelFormat,
2,
frameSize
captureSize
);
_stagingTexture?.Dispose();
_stagingTexture = null;
_surfaceWidth = captureSize.Width;
_surfaceHeight = captureSize.Height;
_region = GetGameScreenRegion(_hWnd);
return;
}
try