From bbd8ed506701c5496695ce68014cd9bc2fb5b94c Mon Sep 17 00:00:00 2001 From: huiyadanli Date: Mon, 2 Oct 2023 18:00:14 +0800 Subject: [PATCH] refactor: use Fischless --- BetterGenshinImpact.sln | 24 +++ .../BetterGenshinImpact.csproj | 112 +++++------ .../Core/Config/ApplicationConfiguration.cs | 4 +- .../Core/Simulator/MouseEventSimulator.cs | 20 +- .../Core/Simulator/PostMessageSimulator.cs | 26 +-- .../Core/Simulator/Simulator.cs | 4 +- .../AutoFishing/AutoFishingTrigger.cs | 22 +-- .../GameTask/AutoSkip/AutoSkipTrigger.cs | 177 +++++++++--------- .../GameTask/CaptureContent.cs | 2 + .../GameTask/Model/SystemInfo.cs | 8 +- BetterGenshinImpact/GameTask/SystemControl.cs | 26 ++- .../GameTask/TaskDispatcher.cs | 12 +- BetterGenshinImpact/Helpers/DpiHelper.cs | 2 - .../Helpers/Extensions/PointExtension.cs | 4 +- BetterGenshinImpact/Helpers/PrimaryScreen.cs | 48 ++--- .../View/CaptureTestWindow.xaml.cs | 8 +- .../ViewModel/MainWindowViewModel.cs | 12 +- .../BitBlt/BitBltCapture.cs | 113 +++++++++++ Fischless.WindowCapture/BitmapExtensions.cs | 14 ++ Fischless.WindowCapture/CaptionHelper.cs | 28 +++ .../CaptureModeExtensions.cs | 9 + Fischless.WindowCapture/CaptureModes.cs | 7 + Fischless.WindowCapture/DpiHelper.cs | 27 +++ .../Fischless.WindowCapture.csproj | 22 +++ .../Graphics/GraphicsCapture.cs | 152 +++++++++++++++ .../Graphics/Helpers/CaptureHelper.cs | 49 +++++ .../Graphics/Helpers/Direct3D11Helper.cs | 115 ++++++++++++ .../Graphics/Helpers/Texture2DExtensions.cs | 51 +++++ .../Graphics/Helpers/WinRT.cs | 111 +++++++++++ Fischless.WindowCapture/IWindowCapture.cs | 14 ++ .../WindowCaptureBenchmark.cs | 44 +++++ .../WindowCaptureFactory.cs | 19 ++ 32 files changed, 1033 insertions(+), 253 deletions(-) create mode 100644 Fischless.WindowCapture/BitBlt/BitBltCapture.cs create mode 100644 Fischless.WindowCapture/BitmapExtensions.cs create mode 100644 Fischless.WindowCapture/CaptionHelper.cs create mode 100644 Fischless.WindowCapture/CaptureModeExtensions.cs create mode 100644 Fischless.WindowCapture/CaptureModes.cs create mode 100644 Fischless.WindowCapture/DpiHelper.cs create mode 100644 Fischless.WindowCapture/Fischless.WindowCapture.csproj create mode 100644 Fischless.WindowCapture/Graphics/GraphicsCapture.cs create mode 100644 Fischless.WindowCapture/Graphics/Helpers/CaptureHelper.cs create mode 100644 Fischless.WindowCapture/Graphics/Helpers/Direct3D11Helper.cs create mode 100644 Fischless.WindowCapture/Graphics/Helpers/Texture2DExtensions.cs create mode 100644 Fischless.WindowCapture/Graphics/Helpers/WinRT.cs create mode 100644 Fischless.WindowCapture/IWindowCapture.cs create mode 100644 Fischless.WindowCapture/WindowCaptureBenchmark.cs create mode 100644 Fischless.WindowCapture/WindowCaptureFactory.cs diff --git a/BetterGenshinImpact.sln b/BetterGenshinImpact.sln index 0958d77a..38db720a 100644 --- a/BetterGenshinImpact.sln +++ b/BetterGenshinImpact.sln @@ -9,24 +9,48 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vision.WindowCapture", "Vis EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterGenshinImpact.Win32", "BetterGenshinImpact.Win32\BetterGenshinImpact.Win32.csproj", "{75F4541B-9624-4AFB-BAEA-3EAFD3300EE1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fischless.WindowCapture", "Fischless.WindowCapture\Fischless.WindowCapture.csproj", "{6B0A3D96-D88D-48DD-8112-4CD5BA5D27CE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {75EC89E2-413D-4725-BCEA-AFAC57708F07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {75EC89E2-413D-4725-BCEA-AFAC57708F07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75EC89E2-413D-4725-BCEA-AFAC57708F07}.Debug|x64.ActiveCfg = Debug|Any CPU + {75EC89E2-413D-4725-BCEA-AFAC57708F07}.Debug|x64.Build.0 = Debug|Any CPU {75EC89E2-413D-4725-BCEA-AFAC57708F07}.Release|Any CPU.ActiveCfg = Release|Any CPU {75EC89E2-413D-4725-BCEA-AFAC57708F07}.Release|Any CPU.Build.0 = Release|Any CPU + {75EC89E2-413D-4725-BCEA-AFAC57708F07}.Release|x64.ActiveCfg = Release|Any CPU + {75EC89E2-413D-4725-BCEA-AFAC57708F07}.Release|x64.Build.0 = Release|Any CPU {C9080C1D-1B26-46CB-A494-A5E7FE3FCEBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C9080C1D-1B26-46CB-A494-A5E7FE3FCEBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9080C1D-1B26-46CB-A494-A5E7FE3FCEBA}.Debug|x64.ActiveCfg = Debug|Any CPU + {C9080C1D-1B26-46CB-A494-A5E7FE3FCEBA}.Debug|x64.Build.0 = Debug|Any CPU {C9080C1D-1B26-46CB-A494-A5E7FE3FCEBA}.Release|Any CPU.ActiveCfg = Release|Any CPU {C9080C1D-1B26-46CB-A494-A5E7FE3FCEBA}.Release|Any CPU.Build.0 = Release|Any CPU + {C9080C1D-1B26-46CB-A494-A5E7FE3FCEBA}.Release|x64.ActiveCfg = Release|Any CPU + {C9080C1D-1B26-46CB-A494-A5E7FE3FCEBA}.Release|x64.Build.0 = Release|Any CPU {75F4541B-9624-4AFB-BAEA-3EAFD3300EE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {75F4541B-9624-4AFB-BAEA-3EAFD3300EE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75F4541B-9624-4AFB-BAEA-3EAFD3300EE1}.Debug|x64.ActiveCfg = Debug|Any CPU + {75F4541B-9624-4AFB-BAEA-3EAFD3300EE1}.Debug|x64.Build.0 = Debug|Any CPU {75F4541B-9624-4AFB-BAEA-3EAFD3300EE1}.Release|Any CPU.ActiveCfg = Release|Any CPU {75F4541B-9624-4AFB-BAEA-3EAFD3300EE1}.Release|Any CPU.Build.0 = Release|Any CPU + {75F4541B-9624-4AFB-BAEA-3EAFD3300EE1}.Release|x64.ActiveCfg = Release|Any CPU + {75F4541B-9624-4AFB-BAEA-3EAFD3300EE1}.Release|x64.Build.0 = Release|Any CPU + {6B0A3D96-D88D-48DD-8112-4CD5BA5D27CE}.Debug|Any CPU.ActiveCfg = Debug|x64 + {6B0A3D96-D88D-48DD-8112-4CD5BA5D27CE}.Debug|Any CPU.Build.0 = Debug|x64 + {6B0A3D96-D88D-48DD-8112-4CD5BA5D27CE}.Debug|x64.ActiveCfg = Debug|x64 + {6B0A3D96-D88D-48DD-8112-4CD5BA5D27CE}.Debug|x64.Build.0 = Debug|x64 + {6B0A3D96-D88D-48DD-8112-4CD5BA5D27CE}.Release|Any CPU.ActiveCfg = Release|x64 + {6B0A3D96-D88D-48DD-8112-4CD5BA5D27CE}.Release|Any CPU.Build.0 = Release|x64 + {6B0A3D96-D88D-48DD-8112-4CD5BA5D27CE}.Release|x64.ActiveCfg = Release|x64 + {6B0A3D96-D88D-48DD-8112-4CD5BA5D27CE}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BetterGenshinImpact/BetterGenshinImpact.csproj b/BetterGenshinImpact/BetterGenshinImpact.csproj index a520563a..b8beda8d 100644 --- a/BetterGenshinImpact/BetterGenshinImpact.csproj +++ b/BetterGenshinImpact/BetterGenshinImpact.csproj @@ -1,63 +1,65 @@  - - WinExe - net7.0-windows10.0.22000.0 - enable - true - + + WinExe + net7.0-windows10.0.22621.0 + enable + true + True + 11.0 + true + - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + - - - + + + + + - - - - - + + + - - - Code - - + + + Code + + - - - Always - - - Always - - - Always - - - Always - - - Always - - + + + Always + + + Always + + + Always + + + Always + + + Always + + - + \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Config/ApplicationConfiguration.cs b/BetterGenshinImpact/Core/Config/ApplicationConfiguration.cs index 36eec6e6..5b73b99a 100644 --- a/BetterGenshinImpact/Core/Config/ApplicationConfiguration.cs +++ b/BetterGenshinImpact/Core/Config/ApplicationConfiguration.cs @@ -1,7 +1,7 @@ using BetterGenshinImpact.GameTask.AutoFishing; using BetterGenshinImpact.GameTask.AutoSkip; using System; -using Vision.WindowCapture; +using Fischless.WindowCapture; namespace BetterGenshinImpact.Core.Config { @@ -14,7 +14,7 @@ namespace BetterGenshinImpact.Core.Config /// /// 窗口捕获的方式 /// - public string CaptureMode { get; set; } = CaptureModeEnum.BitBlt.ToString(); + public string CaptureMode { get; set; } = CaptureModes.BitBlt.ToString(); /// /// 窗口捕获帧数/触发器触发频率 diff --git a/BetterGenshinImpact/Core/Simulator/MouseEventSimulator.cs b/BetterGenshinImpact/Core/Simulator/MouseEventSimulator.cs index 5cac39b1..f8341927 100644 --- a/BetterGenshinImpact/Core/Simulator/MouseEventSimulator.cs +++ b/BetterGenshinImpact/Core/Simulator/MouseEventSimulator.cs @@ -1,8 +1,7 @@ -using System.Threading; +using BetterGenshinImpact.Helpers; +using System.Threading; using System.Windows; -using Windows.Win32.UI.Input.KeyboardAndMouse; -using BetterGenshinImpact.Helpers; -using static Windows.Win32.PInvoke; +using Vanara.PInvoke; namespace BetterGenshinImpact.Core.Simulator; @@ -10,22 +9,19 @@ public class MouseEventSimulator { public static void Move(int x, int y) { - mouse_event( - MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE | MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE, - x * 65535 / PrimaryScreen.DESKTOP.Width, - y * 65535 / PrimaryScreen.DESKTOP.Height, - 0, - 0); + User32.mouse_event(User32.MOUSEEVENTF.MOUSEEVENTF_ABSOLUTE | User32.MOUSEEVENTF.MOUSEEVENTF_MOVE, + x * 65535 / PrimaryScreen.DESKTOP.Width, y * 65535 / PrimaryScreen.DESKTOP.Height, + 0, 0); } public static void LeftButtonDown() { - mouse_event(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0); + User32.mouse_event(User32.MOUSEEVENTF.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0); } public static void LeftButtonUp() { - mouse_event(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0); + User32.mouse_event(User32.MOUSEEVENTF.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0); } public static bool Click(int x, int y) diff --git a/BetterGenshinImpact/Core/Simulator/PostMessageSimulator.cs b/BetterGenshinImpact/Core/Simulator/PostMessageSimulator.cs index ecaf4101..24c96eb2 100644 --- a/BetterGenshinImpact/Core/Simulator/PostMessageSimulator.cs +++ b/BetterGenshinImpact/Core/Simulator/PostMessageSimulator.cs @@ -1,16 +1,20 @@ -using System.Threading; -using Windows.Win32.Foundation; -using static Windows.Win32.PInvoke; +using System; +using System.Threading; +using Vanara.PInvoke; namespace BetterGenshinImpact.Core.Simulator; public class PostMessageSimulator { - private readonly HWND _hWnd; + public static readonly uint WM_LBUTTONDOWN = 0x201; //按下鼠标左键 - public PostMessageSimulator(HWND hWnd) + public static readonly uint WM_LBUTTONUP = 0x202; //释放鼠标左键 + + private readonly IntPtr _hWnd; + + public PostMessageSimulator(IntPtr hWnd) { - _hWnd = hWnd; + this._hWnd = hWnd; } /// @@ -20,10 +24,10 @@ public class PostMessageSimulator /// public void LeftButtonClick(int x, int y) { - LPARAM p = y << 16 | x; - PostMessage(_hWnd, WM_LBUTTONDOWN, default, p); + IntPtr p = (y << 16) | x; + User32.PostMessage(_hWnd, WM_LBUTTONDOWN, IntPtr.Zero, p); Thread.Sleep(100); - PostMessage(_hWnd, WM_LBUTTONUP, default, p); + User32.PostMessage(_hWnd, WM_LBUTTONUP, IntPtr.Zero, p); } /// @@ -31,7 +35,7 @@ public class PostMessageSimulator /// public void LeftButtonDown() { - PostMessage(_hWnd, WM_LBUTTONDOWN, default, default); + User32.PostMessage(_hWnd, WM_LBUTTONDOWN, IntPtr.Zero); } /// @@ -39,6 +43,6 @@ public class PostMessageSimulator /// public void LeftButtonUp() { - PostMessage(_hWnd, WM_LBUTTONUP, default, default); + User32.PostMessage(_hWnd, WM_LBUTTONUP, IntPtr.Zero); } } \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Simulator/Simulator.cs b/BetterGenshinImpact/Core/Simulator/Simulator.cs index 019eb389..5639d1a5 100644 --- a/BetterGenshinImpact/Core/Simulator/Simulator.cs +++ b/BetterGenshinImpact/Core/Simulator/Simulator.cs @@ -1,10 +1,10 @@ -using Windows.Win32.Foundation; +using System; namespace BetterGenshinImpact.Core.Simulator; public class Simulator { - public static PostMessageSimulator PostMessage(HWND hWnd) + public static PostMessageSimulator PostMessage(IntPtr hWnd) { return new PostMessageSimulator(hWnd); } diff --git a/BetterGenshinImpact/GameTask/AutoFishing/AutoFishingTrigger.cs b/BetterGenshinImpact/GameTask/AutoFishing/AutoFishingTrigger.cs index b0bc03d3..dbcd4bf9 100644 --- a/BetterGenshinImpact/GameTask/AutoFishing/AutoFishingTrigger.cs +++ b/BetterGenshinImpact/GameTask/AutoFishing/AutoFishingTrigger.cs @@ -1,7 +1,6 @@ using BetterGenshinImpact.Core.Recognition; using BetterGenshinImpact.Core.Recognition.OCR; using BetterGenshinImpact.Core.Recognition.OpenCv; -using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.GameTask.AutoFishing.Assets; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.View; @@ -12,9 +11,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; -using Windows.Win32.Foundation; -using Windows.Win32.UI.Input.KeyboardAndMouse; using WindowsInput; +using static Vanara.PInvoke.User32; namespace BetterGenshinImpact.GameTask.AutoFishing { @@ -285,7 +283,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing private int _noRectsCount = 0; private Rect _cur, _left, _right; - private MOUSE_EVENT_FLAGS _prevMouseEvent = 0x0; + private MOUSEEVENTF _prevMouseEvent = 0x0; private bool _findFishBoxTips; @@ -331,21 +329,21 @@ namespace BetterGenshinImpact.GameTask.AutoFishing if (_cur.X < _left.X) { - if (_prevMouseEvent != MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTDOWN) + if (_prevMouseEvent != MOUSEEVENTF.MOUSEEVENTF_LEFTDOWN) { simulator.Mouse.LeftButtonDown(); //Simulator.PostMessage(TaskContext.Instance().GameHandle).LeftButtonDown(); - _prevMouseEvent = MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTDOWN; + _prevMouseEvent = MOUSEEVENTF.MOUSEEVENTF_LEFTDOWN; //Debug.WriteLine("进度不到 左键按下"); } } else { - if (_prevMouseEvent == MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTDOWN) + if (_prevMouseEvent == MOUSEEVENTF.MOUSEEVENTF_LEFTDOWN) { simulator.Mouse.LeftButtonUp(); //Simulator.PostMessage(TaskContext.Instance().GameHandle).LeftButtonUp(); - _prevMouseEvent = MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTUP; + _prevMouseEvent = MOUSEEVENTF.MOUSEEVENTF_LEFTUP; //Debug.WriteLine("进度超出 左键松开"); } } @@ -360,21 +358,21 @@ namespace BetterGenshinImpact.GameTask.AutoFishing if (_right.X + _right.Width - (_cur.X + _cur.Width) <= _cur.X - _left.X) { - if (_prevMouseEvent == MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTDOWN) + if (_prevMouseEvent == MOUSEEVENTF.MOUSEEVENTF_LEFTDOWN) { simulator.Mouse.LeftButtonUp(); //Simulator.PostMessage(TaskContext.Instance().GameHandle).LeftButtonUp(); - _prevMouseEvent = MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTUP; + _prevMouseEvent = MOUSEEVENTF.MOUSEEVENTF_LEFTUP; //Debug.WriteLine("进入框内中间 左键松开"); } } else { - if (_prevMouseEvent != MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTDOWN) + if (_prevMouseEvent != MOUSEEVENTF.MOUSEEVENTF_LEFTDOWN) { simulator.Mouse.LeftButtonDown(); //Simulator.PostMessage(TaskContext.Instance().GameHandle).LeftButtonDown(); - _prevMouseEvent = MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTDOWN; + _prevMouseEvent = MOUSEEVENTF.MOUSEEVENTF_LEFTDOWN; //Debug.WriteLine("未到框内中间 左键按下"); } } diff --git a/BetterGenshinImpact/GameTask/AutoSkip/AutoSkipTrigger.cs b/BetterGenshinImpact/GameTask/AutoSkip/AutoSkipTrigger.cs index 0dd5d023..3c81cd57 100644 --- a/BetterGenshinImpact/GameTask/AutoSkip/AutoSkipTrigger.cs +++ b/BetterGenshinImpact/GameTask/AutoSkip/AutoSkipTrigger.cs @@ -1,114 +1,109 @@ using BetterGenshinImpact.Core.Recognition.OpenCv; using BetterGenshinImpact.GameTask.AutoSkip.Assets; -using BetterGenshinImpact.Helpers.Extensions; -using BetterGenshinImpact.View.Drawable; using Microsoft.Extensions.Logging; using System; using System.Diagnostics; -using BetterGenshinImpact.GameTask.Model; using WindowsInput; -using Point = OpenCvSharp.Point; -namespace BetterGenshinImpact.GameTask.AutoSkip +namespace BetterGenshinImpact.GameTask.AutoSkip; + +/// +/// 自动剧情有选项点击 +/// +public class AutoSkipTrigger : ITaskTrigger { - /// - /// 自动剧情有选项点击 - /// - public class AutoSkipTrigger : ITaskTrigger + private readonly ILogger _logger = App.GetLogger(); + + public string Name => "自动剧情"; + public bool IsEnabled { get; set; } + public int Priority => 20; + public bool IsExclusive => false; + + private readonly AutoSkipAssets _autoSkipAssets; + + public AutoSkipTrigger() { - private readonly ILogger _logger = App.GetLogger(); + _autoSkipAssets = new AutoSkipAssets(); + } - public string Name => "自动剧情"; - public bool IsEnabled { get; set; } - public int Priority => 20; - public bool IsExclusive => false; + public void Init() + { + IsEnabled = true; + } - private readonly AutoSkipAssets _autoSkipAssets; - - public AutoSkipTrigger() + public void OnCapture(CaptureContent content) + { + if (content.IsReachInterval(TimeSpan.FromMilliseconds(200))) { - _autoSkipAssets = new AutoSkipAssets(); + return; } - public void Init() + // 找左上角剧情自动的按钮 + content.CaptureRectArea.Find(_autoSkipAssets.StopAutoButtonRo, (_) => { - IsEnabled = true; - } + new InputSimulator().Keyboard.KeyPress(VirtualKeyCode.SPACE); + }); - public void OnCapture(CaptureContent content) + // 不存在则找右下的选项按钮 + content.CaptureRectArea.Find(_autoSkipAssets.OptionButtonRo, (optionButtonRectArea) => { - if (content.IsReachInterval(TimeSpan.FromMilliseconds(200))) + // 不存在菜单的情况下 剧情在播放中 + var menuRectArea = content.CaptureRectArea.Find(_autoSkipAssets.MenuRo); + if (menuRectArea.IsEmpty()) { - return; + optionButtonRectArea.ClickCenter(); + _logger.LogInformation("点击选项按钮"); } + }); - // 找左上角剧情自动的按钮 - content.CaptureRectArea.Find(_autoSkipAssets.StopAutoButtonRo, (_) => - { - new InputSimulator().Keyboard.KeyPress(VirtualKeyCode.SPACE); - }); - - // 不存在则找右下的选项按钮 - content.CaptureRectArea.Find(_autoSkipAssets.OptionButtonRo, (optionButtonRectArea) => - { - // 不存在菜单的情况下 剧情在播放中 - var menuRectArea = content.CaptureRectArea.Find(_autoSkipAssets.MenuRo); - if (menuRectArea.IsEmpty()) - { - optionButtonRectArea.ClickCenter(); - _logger.LogInformation("点击选项按钮"); - } - }); - - // 黑屏剧情要点击鼠标(多次) 几乎全黑的时候不用点击 - var grayMat = content.CaptureRectArea.SrcGreyMat; - var blackCount = OpenCvCommonHelper.CountGrayMatColor(content.SrcGreyMat, 0); - var rate = blackCount * 1.0 / (grayMat.Width * grayMat.Height); - if (rate > 0.7 && rate < 0.99) - { - new InputSimulator().Mouse.LeftButtonClick(); - Debug.WriteLine($"点击黑屏剧情:{rate}"); - return; - } - - // TODO 自动交付材料 - - //var grayMat = content.SrcGreyMat; - //// 找左上角剧情自动的按钮 - //var grayLeftTopMat = CutHelper.CutLeftTop(grayMat, grayMat.Width / 5, grayMat.Height / 5); - //var p1 = OldMatchTemplateHelper.FindSingleTarget(grayLeftTopMat, AutoSkipAssets.StopAutoButtonMat, 0.9); - //if (p1 is { X: > 0, Y: > 0 }) - //{ - // //_logger.LogInformation($"找到剧情自动按钮:{p1}"); - // VisionContext.Instance().DrawContent.PutRect("StopAutoButton", - // p1.CenterPointToRect(AutoSkipAssets.StopAutoButtonMat).ToRectDrawable()); - // new InputSimulator().Keyboard.KeyPress(VirtualKeyCode.SPACE); - //} - //else - //{ - // VisionContext.Instance().DrawContent.RemoveRect("StopAutoButton"); - //} - - //// 不存在则找右下的选项按钮 - //var grayRightBottomMat = content.SrcGreyRightBottomMat; - //var p2 = OldMatchTemplateHelper.FindSingleTarget(grayRightBottomMat, AutoSkipAssets.OptionMat); - //if (p2 is { X: > 0, Y: > 0 }) - //{ - // // 不存在菜单的情况下 剧情在播放中 - // var grayLeftTopMat2 = CutHelper.CutLeftTop(grayMat, grayMat.Width / 4, grayMat.Height / 4); - // var pMenu = OldMatchTemplateHelper.FindSingleTarget(grayLeftTopMat2, AutoSkipAssets.MenuMat); - // if (pMenu is { X: 0, Y: 0 }) - // { - // p2 = p2.ToDesktopPositionOffset65535(grayMat.Width - grayMat.Width / 2, - // grayMat.Height - grayMat.Height / 3 * 2); - // new InputSimulator().Mouse.MoveMouseTo(p2.X, p2.Y).LeftButtonClick(); - // _logger.LogInformation($"点击选项按钮:{p2}"); - // return; - // } - //} - - - + // 黑屏剧情要点击鼠标(多次) 几乎全黑的时候不用点击 + var grayMat = content.CaptureRectArea.SrcGreyMat; + var blackCount = OpenCvCommonHelper.CountGrayMatColor(content.SrcGreyMat, 0); + var rate = blackCount * 1.0 / (grayMat.Width * grayMat.Height); + if (rate > 0.7 && rate < 0.99) + { + new InputSimulator().Mouse.LeftButtonClick(); + Debug.WriteLine($"点击黑屏剧情:{rate}"); + return; } + + // TODO 自动交付材料 + + //var grayMat = content.SrcGreyMat; + //// 找左上角剧情自动的按钮 + //var grayLeftTopMat = CutHelper.CutLeftTop(grayMat, grayMat.Width / 5, grayMat.Height / 5); + //var p1 = OldMatchTemplateHelper.FindSingleTarget(grayLeftTopMat, AutoSkipAssets.StopAutoButtonMat, 0.9); + //if (p1 is { X: > 0, Y: > 0 }) + //{ + // //_logger.LogInformation($"找到剧情自动按钮:{p1}"); + // VisionContext.Instance().DrawContent.PutRect("StopAutoButton", + // p1.CenterPointToRect(AutoSkipAssets.StopAutoButtonMat).ToRectDrawable()); + // new InputSimulator().Keyboard.KeyPress(VirtualKeyCode.SPACE); + //} + //else + //{ + // VisionContext.Instance().DrawContent.RemoveRect("StopAutoButton"); + //} + + //// 不存在则找右下的选项按钮 + //var grayRightBottomMat = content.SrcGreyRightBottomMat; + //var p2 = OldMatchTemplateHelper.FindSingleTarget(grayRightBottomMat, AutoSkipAssets.OptionMat); + //if (p2 is { X: > 0, Y: > 0 }) + //{ + // // 不存在菜单的情况下 剧情在播放中 + // var grayLeftTopMat2 = CutHelper.CutLeftTop(grayMat, grayMat.Width / 4, grayMat.Height / 4); + // var pMenu = OldMatchTemplateHelper.FindSingleTarget(grayLeftTopMat2, AutoSkipAssets.MenuMat); + // if (pMenu is { X: 0, Y: 0 }) + // { + // p2 = p2.ToDesktopPositionOffset65535(grayMat.Width - grayMat.Width / 2, + // grayMat.Height - grayMat.Height / 3 * 2); + // new InputSimulator().Mouse.MoveMouseTo(p2.X, p2.Y).LeftButtonClick(); + // _logger.LogInformation($"点击选项按钮:{p2}"); + // return; + // } + //} + + + } } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/CaptureContent.cs b/BetterGenshinImpact/GameTask/CaptureContent.cs index b8449470..55482f8a 100644 --- a/BetterGenshinImpact/GameTask/CaptureContent.cs +++ b/BetterGenshinImpact/GameTask/CaptureContent.cs @@ -17,6 +17,8 @@ public class CaptureContent public int FrameIndex { get; } public double TimerInterval { get; } + public int FrameRate => (int)(1000 / TimerInterval); + public RectArea CaptureRectArea { get; private set; } public CaptureContent(Bitmap srcBitmap, int frameIndex, double interval) diff --git a/BetterGenshinImpact/GameTask/Model/SystemInfo.cs b/BetterGenshinImpact/GameTask/Model/SystemInfo.cs index 83211896..a4935f6b 100644 --- a/BetterGenshinImpact/GameTask/Model/SystemInfo.cs +++ b/BetterGenshinImpact/GameTask/Model/SystemInfo.cs @@ -1,13 +1,7 @@ using BetterGenshinImpact.Helpers; -using Microsoft.Win32.SafeHandles; using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Windows.Win32.Foundation; -using OpenCvSharp; +using Vanara.PInvoke; using Size = System.Drawing.Size; namespace BetterGenshinImpact.GameTask.Model diff --git a/BetterGenshinImpact/GameTask/SystemControl.cs b/BetterGenshinImpact/GameTask/SystemControl.cs index 5684f3ce..5111ba77 100644 --- a/BetterGenshinImpact/GameTask/SystemControl.cs +++ b/BetterGenshinImpact/GameTask/SystemControl.cs @@ -1,8 +1,7 @@ using System; using System.Diagnostics; using System.Linq; -using Windows.Win32.Foundation; -using static Windows.Win32.PInvoke; +using Vanara.PInvoke; namespace BetterGenshinImpact.GameTask { @@ -21,8 +20,8 @@ namespace BetterGenshinImpact.GameTask public static bool IsGenshinImpactActive2() { - var hWnd = GetForegroundWindow(); - return (nint)hWnd == TaskContext.Instance().GameHandle; + var hWnd = User32.GetForegroundWindow(); + return hWnd == TaskContext.Instance().GameHandle; } public static nint FindHandleByProcessName(params string[] names) @@ -39,29 +38,26 @@ namespace BetterGenshinImpact.GameTask return 0; } - public static unsafe string? GetActiveProcessName() + public static string? GetActiveProcessName() { try { - var hWnd = GetForegroundWindow(); - uint pid = default; - _ = GetWindowThreadProcessId(hWnd, &pid); + var hWnd = User32.GetForegroundWindow(); + _ = User32.GetWindowThreadProcessId(hWnd, out var pid); var p = Process.GetProcessById((int)pid); return p.ProcessName; } - catch (Exception ex) + catch { - Debug.WriteLine(ex); return null; } } - public static unsafe Process GetProcessByHandle(nint hWnd) + public static Process? GetProcessByHandle(IntPtr hWnd) { try { - uint pid = default; - _ = GetWindowThreadProcessId((HWND)hWnd, &pid); + _ = User32.GetWindowThreadProcessId(hWnd, out var pid); var p = Process.GetProcessById((int)pid); return p; } @@ -79,7 +75,7 @@ namespace BetterGenshinImpact.GameTask /// public static RECT GetWindowRect(IntPtr hWnd) { - Windows.Win32.PInvoke.GetWindowRect((HWND)hWnd, out var windowRect); + User32.GetWindowRect(hWnd, out var windowRect); return windowRect; } @@ -90,7 +86,7 @@ namespace BetterGenshinImpact.GameTask /// public static RECT GetGameScreenRect(IntPtr hWnd) { - GetClientRect((HWND)hWnd, out var clientRect); + User32.GetClientRect(hWnd, out var clientRect); return clientRect; } diff --git a/BetterGenshinImpact/GameTask/TaskDispatcher.cs b/BetterGenshinImpact/GameTask/TaskDispatcher.cs index f6ab5895..afd302a6 100644 --- a/BetterGenshinImpact/GameTask/TaskDispatcher.cs +++ b/BetterGenshinImpact/GameTask/TaskDispatcher.cs @@ -1,13 +1,11 @@ -using Microsoft.Extensions.Logging; +using Fischless.WindowCapture; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using System.Windows; -using Windows.Win32.Foundation; -using BetterGenshinImpact.GameTask.Model; -using Vision.WindowCapture; +using Vanara.PInvoke; namespace BetterGenshinImpact.GameTask { @@ -30,7 +28,7 @@ namespace BetterGenshinImpact.GameTask //_timer.Tick += Tick; } - public void Start(IntPtr hWnd, CaptureModeEnum mode, int interval = 50) + public void Start(IntPtr hWnd, CaptureModes mode, int interval = 50) { // 初始化任务上下文 TaskContext.Instance().Init(hWnd); @@ -39,7 +37,7 @@ namespace BetterGenshinImpact.GameTask // 初始化截图器 _capture = WindowCaptureFactory.Create(mode); - _capture.Start((HWND)hWnd); + _capture.Start(hWnd); // 启动定时器 _frameIndex = 0; diff --git a/BetterGenshinImpact/Helpers/DpiHelper.cs b/BetterGenshinImpact/Helpers/DpiHelper.cs index 5c9c68d3..5ad00d17 100644 --- a/BetterGenshinImpact/Helpers/DpiHelper.cs +++ b/BetterGenshinImpact/Helpers/DpiHelper.cs @@ -1,8 +1,6 @@ using System; using System.Windows; using System.Windows.Interop; -using Windows.Win32.Foundation; -using Windows.Win32.Graphics.Gdi; using Vanara.PInvoke; using HMONITOR = Vanara.PInvoke.HMONITOR; using HWND = Vanara.PInvoke.HWND; diff --git a/BetterGenshinImpact/Helpers/Extensions/PointExtension.cs b/BetterGenshinImpact/Helpers/Extensions/PointExtension.cs index 0b76832e..9e337b56 100644 --- a/BetterGenshinImpact/Helpers/Extensions/PointExtension.cs +++ b/BetterGenshinImpact/Helpers/Extensions/PointExtension.cs @@ -1,8 +1,6 @@ using BetterGenshinImpact.GameTask; using OpenCvSharp; using System; -using Windows.Win32.Foundation; -using BetterGenshinImpact.Core.Simulator; namespace BetterGenshinImpact.Helpers.Extensions { @@ -33,7 +31,7 @@ namespace BetterGenshinImpact.Helpers.Extensions return point; } - var rc = SystemControl.GetWindowRect((HWND)TaskContext.Instance().GameHandle); + var rc = SystemControl.GetWindowRect(TaskContext.Instance().GameHandle); return new Point(rc.X + point.X + offsetX, rc.Y + point.Y + offsetY); } diff --git a/BetterGenshinImpact/Helpers/PrimaryScreen.cs b/BetterGenshinImpact/Helpers/PrimaryScreen.cs index f9644bec..85ccee99 100644 --- a/BetterGenshinImpact/Helpers/PrimaryScreen.cs +++ b/BetterGenshinImpact/Helpers/PrimaryScreen.cs @@ -1,6 +1,7 @@ -using System.Drawing; -using Windows.Win32.Graphics.Gdi; -using static Windows.Win32.PInvoke; +using System; +using System.Drawing; +using static Vanara.PInvoke.Gdi32; +using Vanara.PInvoke; namespace BetterGenshinImpact.Helpers { @@ -13,13 +14,13 @@ namespace BetterGenshinImpact.Helpers { get { - var hdc = GetDC(default); + var hdc = User32.GetDC(IntPtr.Zero); var size = new Size { - Width = GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.HORZRES), - Height = GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.VERTRES) + Width = Gdi32.GetDeviceCaps(hdc, DeviceCap.HORZRES), + Height = Gdi32.GetDeviceCaps(hdc, DeviceCap.VERTRES) }; - ReleaseDC(default, hdc); + User32.ReleaseDC(IntPtr.Zero, hdc); return size; } } @@ -30,9 +31,9 @@ namespace BetterGenshinImpact.Helpers { get { - var hdc = GetDC(default); - var dpiX = GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.LOGPIXELSX); - ReleaseDC(default, hdc); + var hdc = User32.GetDC(IntPtr.Zero); + var dpiX = Gdi32.GetDeviceCaps(hdc, DeviceCap.LOGPIXELSX); + User32.ReleaseDC(IntPtr.Zero, hdc); return dpiX; } } @@ -43,9 +44,9 @@ namespace BetterGenshinImpact.Helpers { get { - var hdc = GetDC(default); - var dpiX = GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.LOGPIXELSY); - ReleaseDC(default, hdc); + var hdc = User32.GetDC(IntPtr.Zero); + var dpiX = Gdi32.GetDeviceCaps(hdc, DeviceCap.LOGPIXELSY); + User32.ReleaseDC(IntPtr.Zero, hdc); return dpiX; } } @@ -56,13 +57,13 @@ namespace BetterGenshinImpact.Helpers { get { - var hdc = GetDC(default); + var hdc = User32.GetDC(IntPtr.Zero); var size = new Size { - Width = GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.DESKTOPHORZRES), - Height = GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.DESKTOPVERTRES) + Width = Gdi32.GetDeviceCaps(hdc, DeviceCap.DESKTOPHORZRES), + Height = Gdi32.GetDeviceCaps(hdc, DeviceCap.DESKTOPVERTRES) }; - ReleaseDC(default, hdc); + User32.ReleaseDC(IntPtr.Zero, hdc); return size; } } @@ -74,9 +75,9 @@ namespace BetterGenshinImpact.Helpers { get { - var hdc = GetDC(default); - var scaleX = (float)GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.DESKTOPHORZRES) / GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.HORZRES); - ReleaseDC(default, hdc); + var hdc = User32.GetDC(IntPtr.Zero); + var scaleX = (float)Gdi32.GetDeviceCaps(hdc, DeviceCap.DESKTOPHORZRES) / (float)Gdi32.GetDeviceCaps(hdc, DeviceCap.HORZRES); + User32.ReleaseDC(IntPtr.Zero, hdc); return scaleX; } } @@ -87,11 +88,12 @@ namespace BetterGenshinImpact.Helpers { get { - var hdc = GetDC(default); - var scaleY = (float)GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.DESKTOPVERTRES) / GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.VERTRES); - ReleaseDC(default, hdc); + var hdc = User32.GetDC(IntPtr.Zero); + var scaleY = (float)Gdi32.GetDeviceCaps(hdc, DeviceCap.DESKTOPVERTRES) / (float)Gdi32.GetDeviceCaps(hdc, DeviceCap.VERTRES); + User32.ReleaseDC(IntPtr.Zero, hdc); return scaleY; } } } + } diff --git a/BetterGenshinImpact/View/CaptureTestWindow.xaml.cs b/BetterGenshinImpact/View/CaptureTestWindow.xaml.cs index bc8552bd..1767c825 100644 --- a/BetterGenshinImpact/View/CaptureTestWindow.xaml.cs +++ b/BetterGenshinImpact/View/CaptureTestWindow.xaml.cs @@ -1,10 +1,10 @@ using BetterGenshinImpact.Helpers.Extensions; +using Fischless.WindowCapture; using System; using System.Diagnostics; using System.Windows; using System.Windows.Media; -using Windows.Win32.Foundation; -using Vision.WindowCapture; +using Vanara.PInvoke; namespace BetterGenshinImpact.View.Test { @@ -19,7 +19,7 @@ namespace BetterGenshinImpact.View.Test InitializeComponent(); } - public void StartCapture(IntPtr hWnd, CaptureModeEnum captureMode) + public void StartCapture(IntPtr hWnd, CaptureModes captureMode) { if (hWnd == IntPtr.Zero) { @@ -29,7 +29,7 @@ namespace BetterGenshinImpact.View.Test _capture = WindowCaptureFactory.Create(captureMode); - _capture.Start((HWND)hWnd); + _capture.Start(hWnd); CompositionTarget.Rendering += Loop; } diff --git a/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs b/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs index 58ff5196..99bfc296 100644 --- a/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs +++ b/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs @@ -1,17 +1,15 @@ -using BetterGenshinImpact.Core.Simulator; -using BetterGenshinImpact.GameTask; +using BetterGenshinImpact.GameTask; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.View; using BetterGenshinImpact.View.Test; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Fischless.WindowCapture; using Microsoft.Extensions.Logging; using System; using System.Diagnostics; using System.Windows; -using Vision.WindowCapture; -using Windows.Win32.Foundation; -using static Windows.Win32.PInvoke; +using Vanara.PInvoke; namespace BetterGenshinImpact.ViewModel { @@ -19,7 +17,7 @@ namespace BetterGenshinImpact.ViewModel { [ObservableProperty] private string[] _modeNames = WindowCaptureFactory.ModeNames(); - [ObservableProperty] private string? _selectedMode = CaptureModeEnum.BitBlt.ToString(); + [ObservableProperty] private string? _selectedMode = CaptureModes.BitBlt.ToString(); private MaskWindow? _maskWindow; private readonly ILogger _logger = App.GetLogger(); @@ -98,7 +96,7 @@ namespace BetterGenshinImpact.ViewModel private void ShowMaskWindow(IntPtr hWnd) { - GetWindowRect((HWND)hWnd, out var rect); + User32.GetWindowRect(hWnd, out var rect); //var x = rect.X; //var y = rect.Y; //var w = rect.Width; diff --git a/Fischless.WindowCapture/BitBlt/BitBltCapture.cs b/Fischless.WindowCapture/BitBlt/BitBltCapture.cs new file mode 100644 index 00000000..c2cd7f54 --- /dev/null +++ b/Fischless.WindowCapture/BitBlt/BitBltCapture.cs @@ -0,0 +1,113 @@ +using System.Diagnostics; +using Vanara.PInvoke; + +namespace Fischless.WindowCapture.BitBlt; + +public class BitBltCapture : IWindowCapture +{ + private nint _hWnd; + public bool IsCapturing { get; private set; } + public bool IsClientEnabled { get; set; } = false; + public bool IsCursorCaptureEnabled { get; set; } = false; + + public void Dispose() + { + Stop(); + } + + public void Start(nint hWnd) + { + _hWnd = hWnd; + IsCapturing = true; + } + + public Bitmap? Capture() + { + if (_hWnd == IntPtr.Zero) + { + return null; + } + + try + { + if (IsClientEnabled) + { + _ = User32.GetWindowRect(_hWnd, out var windowRect); + var width = windowRect.right - windowRect.left; + var height = windowRect.bottom - windowRect.top; + + var hdcSrc = User32.GetWindowDC(_hWnd); + var hdcDest = Gdi32.CreateCompatibleDC(hdcSrc); + var hBitmap = Gdi32.CreateCompatibleBitmap(hdcSrc, width, height); + var hOld = Gdi32.SelectObject(hdcDest, hBitmap); + _ = Gdi32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, Gdi32.RasterOperationMode.SRCCOPY); + _ = Gdi32.SelectObject(hdcDest, hOld); + _ = Gdi32.DeleteDC(hdcDest); + _ = User32.ReleaseDC(_hWnd, hdcSrc); + + var bitmap = hBitmap.ToBitmap(); + _ = Gdi32.DeleteObject(hBitmap); + return bitmap; + } + else + { + _ = User32.GetClientRect(_hWnd, out var windowRect); + int x = default, y = default; + int width = windowRect.right - windowRect.left; + int height = windowRect.bottom - windowRect.top; + + Bitmap bitmap = new(width, height); + using System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bitmap); + nint hdcDest = g.GetHdc(); + Gdi32.SafeHDC hdcSrc = User32.GetDC(_hWnd == IntPtr.Zero ? User32.GetDesktopWindow() : _hWnd); + _ = Gdi32.StretchBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, width, height, Gdi32.RasterOperationMode.SRCCOPY); + g.ReleaseHdc(); + _ = Gdi32.DeleteDC(hdcDest); + _ = Gdi32.DeleteDC(hdcSrc); + return bitmap; + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + return null!; + } + + public Bitmap? Capture(int x, int y, int width, int height) + { + if (_hWnd == IntPtr.Zero) + { + return null; + } + + if (IsClientEnabled) + { + throw new NotSupportedException(); + } + + try + { + Bitmap copied = new(width, height); + using System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(copied); + nint hdcDest = g.GetHdc(); + Gdi32.SafeHDC hdcSrc = User32.GetDC(_hWnd == IntPtr.Zero ? User32.GetDesktopWindow() : _hWnd); + _ = Gdi32.StretchBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, width, height, Gdi32.RasterOperationMode.SRCCOPY); + g.ReleaseHdc(); + _ = Gdi32.DeleteDC(hdcDest); + _ = Gdi32.DeleteDC(hdcSrc); + return copied; + } + catch (Exception e) + { + Debug.WriteLine(e); + } + return null; + } + + public void Stop() + { + _hWnd = IntPtr.Zero; + IsCapturing = false; + } +} diff --git a/Fischless.WindowCapture/BitmapExtensions.cs b/Fischless.WindowCapture/BitmapExtensions.cs new file mode 100644 index 00000000..4cb4d091 --- /dev/null +++ b/Fischless.WindowCapture/BitmapExtensions.cs @@ -0,0 +1,14 @@ +namespace Fischless.WindowCapture; + +internal static class BitmapExtensions +{ + public static Bitmap Crop(this Bitmap src, int x, int y, int width, int height) + { + Rectangle cropRect = new(x, y, width, height); + Bitmap target = new(cropRect.Width, cropRect.Height); + + using System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(target); + g.DrawImage(src, new Rectangle(0, 0, target.Width, target.Height), cropRect, GraphicsUnit.Pixel); + return target; + } +} diff --git a/Fischless.WindowCapture/CaptionHelper.cs b/Fischless.WindowCapture/CaptionHelper.cs new file mode 100644 index 00000000..c488b83b --- /dev/null +++ b/Fischless.WindowCapture/CaptionHelper.cs @@ -0,0 +1,28 @@ +using System.Windows; +using Vanara.PInvoke; + +namespace Fischless.WindowCapture; + +internal static class CaptionHelper +{ + public static bool IsFullScreenMode(nint hWnd) + { + if (hWnd == IntPtr.Zero) + { + return false; + } + + int exStyle = User32.GetWindowLong(hWnd, User32.WindowLongFlags.GWL_EXSTYLE); + + if ((exStyle & (int)User32.WindowStylesEx.WS_EX_TOPMOST) != 0) + { + return true; + } + return false; + } + + public static int GetSystemCaptionHeight() + { + return (int)Math.Round(SystemParameters.CaptionHeight * DpiHelper.ScaleY); + } +} diff --git a/Fischless.WindowCapture/CaptureModeExtensions.cs b/Fischless.WindowCapture/CaptureModeExtensions.cs new file mode 100644 index 00000000..d54c60c2 --- /dev/null +++ b/Fischless.WindowCapture/CaptureModeExtensions.cs @@ -0,0 +1,9 @@ +namespace Fischless.WindowCapture; + +public static class CaptureModeExtensions +{ + public static CaptureModes ToCaptureMode(this string modeName) + { + return (CaptureModes)Enum.Parse(typeof(CaptureModes), modeName); + } +} diff --git a/Fischless.WindowCapture/CaptureModes.cs b/Fischless.WindowCapture/CaptureModes.cs new file mode 100644 index 00000000..bf0e1f9a --- /dev/null +++ b/Fischless.WindowCapture/CaptureModes.cs @@ -0,0 +1,7 @@ +namespace Fischless.WindowCapture; + +public enum CaptureModes +{ + BitBlt, + WindowsGraphicsCapture, +} diff --git a/Fischless.WindowCapture/DpiHelper.cs b/Fischless.WindowCapture/DpiHelper.cs new file mode 100644 index 00000000..50a9d63d --- /dev/null +++ b/Fischless.WindowCapture/DpiHelper.cs @@ -0,0 +1,27 @@ +using System.Windows.Interop; +using Vanara.PInvoke; +using Application = System.Windows.Application; + +namespace Fischless.WindowCapture; + +internal static class DpiHelper +{ + public static float ScaleY => GetScaleY(); + + private static float GetScaleY() + { + if (Environment.OSVersion.Version >= new Version(6, 3) + && Application.Current?.MainWindow != null) + { + HWND hWnd = new WindowInteropHelper(Application.Current?.MainWindow).Handle; + HMONITOR hMonitor = User32.MonitorFromWindow(hWnd, User32.MonitorFlags.MONITOR_DEFAULTTONEAREST); + _ = SHCore.GetDpiForMonitor(hMonitor, SHCore.MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out _, out uint dpiY); + return dpiY / 96f; + } + + HDC hdc = User32.GetDC(HWND.NULL); + float scaleY = Gdi32.GetDeviceCaps(hdc, Gdi32.DeviceCap.LOGPIXELSY); + _ = User32.ReleaseDC(HWND.NULL, hdc); + return scaleY / 96f; + } +} diff --git a/Fischless.WindowCapture/Fischless.WindowCapture.csproj b/Fischless.WindowCapture/Fischless.WindowCapture.csproj new file mode 100644 index 00000000..f008b74b --- /dev/null +++ b/Fischless.WindowCapture/Fischless.WindowCapture.csproj @@ -0,0 +1,22 @@ + + + + net7.0-windows10.0.22621.0 + enable + enable + x64 + True + True + 11.0 + true + + + + + + + + + + + \ No newline at end of file diff --git a/Fischless.WindowCapture/Graphics/GraphicsCapture.cs b/Fischless.WindowCapture/Graphics/GraphicsCapture.cs new file mode 100644 index 00000000..d76411cd --- /dev/null +++ b/Fischless.WindowCapture/Graphics/GraphicsCapture.cs @@ -0,0 +1,152 @@ +using System.Diagnostics; +using System.Windows; +using Vanara.PInvoke; +using Windows.Graphics.Capture; +using Windows.Graphics.DirectX; + +namespace Fischless.WindowCapture.Graphics; + +public class GraphicsCapture : IWindowCapture +{ + private nint _hWnd; + + private Direct3D11CaptureFramePool _captureFramePool = null!; + private GraphicsCaptureItem _captureItem = null!; + private GraphicsCaptureSession _captureSession = null!; + + public bool IsCapturing { get; private set; } + public bool IsClientEnabled { get; set; } = false; + + public void Dispose() + { + Stop(); + } + + public void Start(nint hWnd) + { + _hWnd = hWnd; + IsCapturing = true; + + _captureItem = CaptureHelper.CreateItemForWindow(_hWnd); + + if (_captureItem == null) + { + throw new InvalidOperationException("Failed to create capture item."); + } + + _captureItem.Closed += CaptureItemOnClosed; + + var device = Direct3D11Helper.CreateDevice(); + + _captureFramePool = Direct3D11CaptureFramePool.Create(device, DirectXPixelFormat.B8G8R8A8UIntNormalized, 2, + _captureItem.Size); + _captureSession = _captureFramePool.CreateCaptureSession(_captureItem); + _captureSession.IsCursorCaptureEnabled = false; + _captureSession.IsBorderRequired = false; + _captureSession.StartCapture(); + IsCapturing = true; + } + + public Bitmap? Capture() + { + if (_hWnd == IntPtr.Zero) + { + return null; + } + + try + { + using var frame = _captureFramePool?.TryGetNextFrame(); + + if (frame == null) + { + return null; + } + + Bitmap? bitmap = frame.ToBitmap(); + + if (bitmap == null) + { + return null; + } + + if (IsClientEnabled) + { + return bitmap; + } + else + { + _ = User32.GetClientRect(_hWnd, out var windowRect); + int border = Math.Max(SystemParameters.Border, 1); + int captionHeight = CaptionHelper.IsFullScreenMode(_hWnd) + ? default + : Math.Max(CaptionHelper.GetSystemCaptionHeight(), frame.ContentSize.Height - windowRect.Height - border * 2); + + using (bitmap) + { + return bitmap.Crop( + border, + border + captionHeight, + frame.ContentSize.Width - border * 2, + frame.ContentSize.Height - border * 2 - captionHeight + ); + } + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + return null; + } + + public Bitmap? Capture(int x, int y, int width, int height) + { + if (_hWnd == IntPtr.Zero) + { + return null; + } + + try + { + using var frame = _captureFramePool?.TryGetNextFrame(); + using Bitmap bitmap = frame?.ToBitmap(); + _ = User32.GetClientRect(_hWnd, out var windowRect); + int border = Math.Max(SystemParameters.Border, 1); + int captionHeight = CaptionHelper.IsFullScreenMode(_hWnd) + ? default + : Math.Max(CaptionHelper.GetSystemCaptionHeight() + , frame.ContentSize.Height - windowRect.Height - border * 2 + ); + + return bitmap.Crop( + border + x, + border + y + captionHeight, + width - border * 2, + height - border * 2 - captionHeight + ); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + return null; + } + + public void Stop() + { + _captureSession?.Dispose(); + _captureFramePool?.Dispose(); + _captureSession = null!; + _captureFramePool = null!; + _captureItem = null!; + + _hWnd = IntPtr.Zero; + IsCapturing = false; + } + + private void CaptureItemOnClosed(GraphicsCaptureItem sender, object args) + { + Stop(); + } +} diff --git a/Fischless.WindowCapture/Graphics/Helpers/CaptureHelper.cs b/Fischless.WindowCapture/Graphics/Helpers/CaptureHelper.cs new file mode 100644 index 00000000..61707aa9 --- /dev/null +++ b/Fischless.WindowCapture/Graphics/Helpers/CaptureHelper.cs @@ -0,0 +1,49 @@ +using System.Runtime.InteropServices; +using Windows.Graphics.Capture; +using WinRT; + +namespace Fischless.WindowCapture.Graphics; + +public static class CaptureHelper +{ + static readonly Guid GraphicsCaptureItemGuid = new("79C3F95B-31F7-4EC2-A464-632EF5D30760"); + + [ComImport] + [Guid("3E68D4BD-7135-4D10-8018-9FB6D9F33FA1")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + interface IInitializeWithWindow + { + void Initialize( + nint hWnd); + } + + [ComImport] + [Guid("3628E81B-3CAC-4C60-B7F4-23CE0E0C3356")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + interface IGraphicsCaptureItemInterop + { + nint CreateForWindow( + [In] nint window, + [In] ref Guid iid); + + nint CreateForMonitor( + [In] nint monitor, + [In] ref Guid iid); + } + + public static void SetWindow(this GraphicsCapturePicker picker, nint hWnd) + { + var interop = picker.As(); + interop.Initialize(hWnd); + } + + public static GraphicsCaptureItem CreateItemForWindow(nint hWnd) + { + var factory = WinrtModule.GetActivationFactory("Windows.Graphics.Capture.GraphicsCaptureItem"); + var interop = factory.AsInterface(); + var itemPointer = interop.CreateForWindow(hWnd, GraphicsCaptureItemGuid); + return GraphicsCaptureItem.FromAbi(itemPointer); + } +} diff --git a/Fischless.WindowCapture/Graphics/Helpers/Direct3D11Helper.cs b/Fischless.WindowCapture/Graphics/Helpers/Direct3D11Helper.cs new file mode 100644 index 00000000..6a5fc5ac --- /dev/null +++ b/Fischless.WindowCapture/Graphics/Helpers/Direct3D11Helper.cs @@ -0,0 +1,115 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Windows.Graphics.DirectX.Direct3D11; +using WinRT; + +namespace Fischless.WindowCapture.Graphics; + +[SuppressMessage("CodeQuality", "IDE0052:")] +public static class Direct3D11Helper +{ + internal static Guid IInspectable = new("AF86E2E0-B12D-4c6a-9C5A-D7AA65101E90"); + internal static Guid ID3D11Resource = new("dc8e63f3-d12b-4952-b47b-5e45026a862d"); + internal static Guid IDXGIAdapter3 = new("645967A4-1392-4310-A798-8053CE3E93FD"); + internal static Guid ID3D11Device = new("db6f6ddb-ac77-4e88-8253-819df9bbf140"); + internal static Guid ID3D11Texture2D = new("6f15aaf2-d208-4e89-9ab4-489535d34f9c"); + + [ComImport] + [Guid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + interface IDirect3DDxgiInterfaceAccess + { + nint GetInterface([In] ref Guid iid); + }; + + [DllImport( + "d3d11.dll", + EntryPoint = "CreateDirect3D11DeviceFromDXGIDevice", + SetLastError = true, + CharSet = CharSet.Unicode, + ExactSpelling = true, + CallingConvention = CallingConvention.StdCall + )] + static extern uint CreateDirect3D11DeviceFromDXGIDevice(nint dxgiDevice, out nint graphicsDevice); + + [DllImport( + "d3d11.dll", + EntryPoint = "CreateDirect3D11SurfaceFromDXGISurface", + SetLastError = true, + CharSet = CharSet.Unicode, + ExactSpelling = true, + CallingConvention = CallingConvention.StdCall + )] + static extern uint CreateDirect3D11SurfaceFromDXGISurface(nint dxgiSurface, out nint graphicsSurface); + + public static IDirect3DDevice CreateDevice() + { + return CreateDevice(false); + } + + public static IDirect3DDevice CreateDevice(bool useWARP) + { + var d3dDevice = new SharpDX.Direct3D11.Device( + useWARP ? SharpDX.Direct3D.DriverType.Software : SharpDX.Direct3D.DriverType.Hardware, + SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport); + var device = CreateDirect3DDeviceFromSharpDXDevice(d3dDevice); + return device; + } + + public static IDirect3DDevice CreateDirect3DDeviceFromSharpDXDevice(SharpDX.Direct3D11.Device d3dDevice) + { + IDirect3DDevice device = null; + + // Acquire the DXGI interface for the Direct3D device. + using (var dxgiDevice = d3dDevice.QueryInterface()) + { + // Wrap the native device using a WinRT interop object. + uint hr = CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.NativePointer, out nint pUnknown); + + if (hr == 0) + { + device = MarshalInterface.FromAbi(pUnknown); + Marshal.Release(pUnknown); + } + } + + return device; + } + + public static IDirect3DSurface CreateDirect3DSurfaceFromSharpDXTexture(SharpDX.Direct3D11.Texture2D texture) + { + IDirect3DSurface surface = null; + + // Acquire the DXGI interface for the Direct3D surface. + using (var dxgiSurface = texture.QueryInterface()) + { + // Wrap the native device using a WinRT interop object. + uint hr = CreateDirect3D11SurfaceFromDXGISurface(dxgiSurface.NativePointer, out nint pUnknown); + + if (hr == 0) + { + surface = Marshal.GetObjectForIUnknown(pUnknown) as IDirect3DSurface; + Marshal.Release(pUnknown); + } + } + + return surface; + } + + public static SharpDX.Direct3D11.Device CreateSharpDXDevice(IDirect3DDevice device) + { + var access = (IDirect3DDxgiInterfaceAccess)device; + var d3dPointer = access.GetInterface(ID3D11Device); + var d3dDevice = new SharpDX.Direct3D11.Device(d3dPointer); + return d3dDevice; + } + + public static SharpDX.Direct3D11.Texture2D CreateSharpDXTexture2D(IDirect3DSurface surface) + { + var access = surface.As(); + var d3dPointer = access.GetInterface(ID3D11Texture2D); + var d3dSurface = new SharpDX.Direct3D11.Texture2D(d3dPointer); + return d3dSurface; + } +} diff --git a/Fischless.WindowCapture/Graphics/Helpers/Texture2DExtensions.cs b/Fischless.WindowCapture/Graphics/Helpers/Texture2DExtensions.cs new file mode 100644 index 00000000..e31328d0 --- /dev/null +++ b/Fischless.WindowCapture/Graphics/Helpers/Texture2DExtensions.cs @@ -0,0 +1,51 @@ +using SharpDX; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using System.Drawing.Imaging; +using Windows.Graphics.Capture; + +namespace Fischless.WindowCapture.Graphics; + +public static class Texture2DExtensions +{ + public static Bitmap? ToBitmap(this Direct3D11CaptureFrame frame) + { + var texture2dBitmap = Direct3D11Helper.CreateSharpDXTexture2D(frame.Surface); + + var d3dDevice = texture2dBitmap.Device; + + // Create texture copy + var staging = new Texture2D(d3dDevice, new Texture2DDescription + { + Width = frame.ContentSize.Width, + Height = frame.ContentSize.Height, + MipLevels = 1, + ArraySize = 1, + Format = texture2dBitmap.Description.Format, + Usage = ResourceUsage.Staging, + SampleDescription = new SampleDescription(1, 0), + BindFlags = BindFlags.None, + CpuAccessFlags = CpuAccessFlags.Read, + OptionFlags = ResourceOptionFlags.None + }); + + try + { + // Copy data + d3dDevice.ImmediateContext.CopyResource(texture2dBitmap, staging); + + var dataBox = d3dDevice.ImmediateContext.MapSubresource(staging, 0, 0, MapMode.Read, + SharpDX.Direct3D11.MapFlags.None, + out DataStream stream); + + var bitmap = new Bitmap(staging.Description.Width, staging.Description.Height, dataBox.RowPitch, + PixelFormat.Format32bppArgb, dataBox.DataPointer); + + return bitmap; + } + finally + { + staging.Dispose(); + } + } +} diff --git a/Fischless.WindowCapture/Graphics/Helpers/WinRT.cs b/Fischless.WindowCapture/Graphics/Helpers/WinRT.cs new file mode 100644 index 00000000..4b06d7f8 --- /dev/null +++ b/Fischless.WindowCapture/Graphics/Helpers/WinRT.cs @@ -0,0 +1,111 @@ +using System.ComponentModel; +using System.Reflection; +using System.Runtime.InteropServices; +using WinRT; + +namespace Fischless.WindowCapture.Graphics; + +#pragma warning disable CS0649 + +[ComImport] +[Guid("3628E81B-3CAC-4C60-B7F4-23CE0E0C3356")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface IGraphicsCaptureItemInterop +{ + int CreateForWindow([In] nint window, [In] ref Guid iid, out nint result); + + int CreateForMonitor([In] nint monitor, [In] ref Guid iid, out nint result); +} + +[Guid("00000035-0000-0000-C000-000000000046")] +internal unsafe struct IActivationFactoryVftbl +{ + public readonly IInspectable.Vftbl IInspectableVftbl; + private readonly void* _ActivateInstance; + + public delegate* unmanaged[Stdcall] ActivateInstance => (delegate* unmanaged[Stdcall])_ActivateInstance; +} + +internal class Platform +{ + [DllImport("api-ms-win-core-com-l1-1-0.dll")] + internal static extern int CoDecrementMTAUsage(nint cookie); + + [DllImport("api-ms-win-core-com-l1-1-0.dll")] + internal static extern unsafe int CoIncrementMTAUsage(nint* cookie); + + [DllImport("api-ms-win-core-winrt-l1-1-0.dll")] + internal static extern unsafe int RoGetActivationFactory(nint runtimeClassId, ref Guid iid, nint* factory); +} + +/// +/// https://github.com/zlatanov/windows-screen-recorder +/// +internal static class WinrtModule +{ + private static readonly Dictionary> Cache = new(); + + public static ObjectReference GetActivationFactory(string runtimeClassId) + { + lock (Cache) + { + if (Cache.TryGetValue(runtimeClassId, out var factory)) + return factory; + + var m = MarshalString.CreateMarshaler(runtimeClassId); + + try + { + var instancePtr = GetActivationFactory(MarshalString.GetAbi(m)); + + factory = ObjectReference.Attach(ref instancePtr); + Cache.Add(runtimeClassId, factory); + + return factory; + } + finally + { + m.Dispose(); + } + } + } + + private static unsafe nint GetActivationFactory(nint hstrRuntimeClassId) + { + if (s_cookie == IntPtr.Zero) + { + lock (s_lock) + { + if (s_cookie == IntPtr.Zero) + { + nint cookie; + Marshal.ThrowExceptionForHR(Platform.CoIncrementMTAUsage(&cookie)); + + s_cookie = cookie; + } + } + } + + Guid iid = typeof(IActivationFactoryVftbl).GUID; + nint instancePtr; + int hr = Platform.RoGetActivationFactory(hstrRuntimeClassId, ref iid, &instancePtr); + + if (hr == 0) + return instancePtr; + + throw new Win32Exception(hr); + } + + public static bool ResurrectObjectReference(IObjectReference objRef) + { + var disposedField = objRef.GetType().GetField("disposed", BindingFlags.NonPublic | BindingFlags.Instance)!; + if (!(bool)disposedField.GetValue(objRef)!) + return false; + disposedField.SetValue(objRef, false); + GC.ReRegisterForFinalize(objRef); + return true; + } + + private static nint s_cookie; + private static readonly object s_lock = new(); +} diff --git a/Fischless.WindowCapture/IWindowCapture.cs b/Fischless.WindowCapture/IWindowCapture.cs new file mode 100644 index 00000000..21f741a8 --- /dev/null +++ b/Fischless.WindowCapture/IWindowCapture.cs @@ -0,0 +1,14 @@ +namespace Fischless.WindowCapture; + +public interface IWindowCapture : IDisposable +{ + public bool IsCapturing { get; } + public bool IsClientEnabled { get; set; } + + public void Start(nint hWnd); + + public Bitmap? Capture(); + public Bitmap? Capture(int x, int y, int width, int height); + + public void Stop(); +} diff --git a/Fischless.WindowCapture/WindowCaptureBenchmark.cs b/Fischless.WindowCapture/WindowCaptureBenchmark.cs new file mode 100644 index 00000000..c982371c --- /dev/null +++ b/Fischless.WindowCapture/WindowCaptureBenchmark.cs @@ -0,0 +1,44 @@ +using System.Diagnostics; + +namespace Fischless.WindowCapture; + +public class WindowCaptureBenchmark +{ + public static void Action() + { + foreach (CaptureModes mode in Enum.GetValues(typeof(CaptureModes))) + { + _ = Task.Run(async () => + { + IWindowCapture capture = WindowCaptureFactory.Create(mode); + + capture.Start(GetHwnd()); + await Task.Delay(1234); + using Bitmap frame = capture.Capture(); + frame?.Save($"Benchmark_{mode}_{frame.Width}x{frame.Height}_DPI{DpiHelper.ScaleY * 100f:F0}.jpg"); + }); + } + } + + private static nint GetHwnd() + { + Process[] processes = Process.GetProcessesByName("YuanShen"); + + if (processes.Length <= 0) + { + processes = Process.GetProcessesByName("Genshin Impact"); + } + if (processes.Length <= 0) + { + processes = Process.GetProcessesByName("GenshinImpact"); + } + if (processes.Length > 0) + { + foreach (Process? process in processes) + { + return process.MainWindowHandle; + } + } + return IntPtr.Zero; + } +} diff --git a/Fischless.WindowCapture/WindowCaptureFactory.cs b/Fischless.WindowCapture/WindowCaptureFactory.cs new file mode 100644 index 00000000..41179156 --- /dev/null +++ b/Fischless.WindowCapture/WindowCaptureFactory.cs @@ -0,0 +1,19 @@ +namespace Fischless.WindowCapture; + +public class WindowCaptureFactory +{ + public static string[] ModeNames() + { + return Enum.GetNames(typeof(CaptureModes)); + } + + public static IWindowCapture Create(CaptureModes mode) + { + return mode switch + { + CaptureModes.BitBlt => new BitBlt.BitBltCapture(), + CaptureModes.WindowsGraphicsCapture => new Graphics.GraphicsCapture(), + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null), + }; + } +}