From 66afbc83aea605c49905c67778712919d2228523 Mon Sep 17 00:00:00 2001 From: Shatyuka Date: Mon, 12 May 2025 22:39:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=88=AA=E5=9B=BE=E4=BC=98=E5=8C=96=20(#1573)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 截图优化 * 窗口选择排除WS_EX_LAYERED 云原神 * 优化原神窗口判断 --- BetterGenshinImpact/GameTask/SystemControl.cs | 4 +- BetterGenshinImpact/View/PickerWindow.xaml | 5 +- BetterGenshinImpact/View/PickerWindow.xaml.cs | 87 ++++++++++--------- .../ViewModel/Pages/HomePageViewModel.cs | 4 +- Fischless.GameCapture/BitBlt/BitBltSession.cs | 43 +++------ .../DwmSharedSurface/SharedSurfaceCapture.cs | 7 +- .../Graphics/GraphicsCapture.cs | 25 ++++-- 7 files changed, 89 insertions(+), 86 deletions(-) diff --git a/BetterGenshinImpact/GameTask/SystemControl.cs b/BetterGenshinImpact/GameTask/SystemControl.cs index 8e20c4ab..507debbe 100644 --- a/BetterGenshinImpact/GameTask/SystemControl.cs +++ b/BetterGenshinImpact/GameTask/SystemControl.cs @@ -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 StartFromLocalAsync(string path) @@ -322,4 +322,4 @@ public class SystemControl Debug.WriteLine($"Shutdown方法执行出错: {ex.Message}"); } } -} \ No newline at end of file +} diff --git a/BetterGenshinImpact/View/PickerWindow.xaml b/BetterGenshinImpact/View/PickerWindow.xaml index 2d43c943..afe45729 100644 --- a/BetterGenshinImpact/View/PickerWindow.xaml +++ b/BetterGenshinImpact/View/PickerWindow.xaml @@ -1,6 +1,7 @@  - + @@ -180,4 +181,4 @@ - \ No newline at end of file + diff --git a/BetterGenshinImpact/View/PickerWindow.xaml.cs b/BetterGenshinImpact/View/PickerWindow.xaml.cs index 20f33cd4..c87ef4bf 100644 --- a/BetterGenshinImpact/View/PickerWindow.xaml.cs +++ b/BetterGenshinImpact/View/PickerWindow.xaml.cs @@ -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(); - + User32.EnumWindows((hWnd, lParam) => { if (!User32.IsWindowVisible(hWnd) || wih.Handle == hWnd) return true; + var exStyle = User32.GetWindowLong(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; } -} diff --git a/BetterGenshinImpact/ViewModel/Pages/HomePageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/HomePageViewModel.cs index b9da6083..48954df7 100644 --- a/BetterGenshinImpact/ViewModel/Pages/HomePageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/HomePageViewModel.cs @@ -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(); } -} \ No newline at end of file +} diff --git a/Fischless.GameCapture/BitBlt/BitBltSession.cs b/Fischless.GameCapture/BitBlt/BitBltSession.cs index 79520926..f2c68ade 100644 --- a/Fischless.GameCapture/BitBlt/BitBltSession.cs +++ b/Fischless.GameCapture/BitBlt/BitBltSession.cs @@ -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(), - 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); } } diff --git a/Fischless.GameCapture/DwmSharedSurface/SharedSurfaceCapture.cs b/Fischless.GameCapture/DwmSharedSurface/SharedSurfaceCapture.cs index 86cbfbcf..c5293ea6 100644 --- a/Fischless.GameCapture/DwmSharedSurface/SharedSurfaceCapture.cs +++ b/Fischless.GameCapture/DwmSharedSurface/SharedSurfaceCapture.cs @@ -56,7 +56,7 @@ public partial class SharedSurfaceCapture : IGameCapture /// /// /// - 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(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; diff --git a/Fischless.GameCapture/Graphics/GraphicsCapture.cs b/Fischless.GameCapture/Graphics/GraphicsCapture.cs index 2a003429..f1ac5508 100644 --- a/Fischless.GameCapture/Graphics/GraphicsCapture.cs +++ b/Fischless.GameCapture/Graphics/GraphicsCapture.cs @@ -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 /// /// /// - 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(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