mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-05-21 09:45:48 +08:00
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user