From 1160d8c1544a0a42021ecd7da739142206e33ee6 Mon Sep 17 00:00:00 2001
From: 2dust <31833384+2dust@users.noreply.github.com>
Date: Mon, 6 Apr 2026 15:34:25 +0800
Subject: [PATCH] Add Windows screen QR scan support for avalonia
---
v2rayN/Directory.Packages.props | 2 +-
.../Common/QRCodeAvaloniaUtils.cs | 189 ++++++++++++++++++
.../v2rayN.Desktop/Views/MainWindow.axaml.cs | 20 +-
v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj | 5 +-
4 files changed, 203 insertions(+), 13 deletions(-)
create mode 100644 v2rayN/v2rayN.Desktop/Common/QRCodeAvaloniaUtils.cs
diff --git a/v2rayN/Directory.Packages.props b/v2rayN/Directory.Packages.props
index 148129f5..3d918f99 100644
--- a/v2rayN/Directory.Packages.props
+++ b/v2rayN/Directory.Packages.props
@@ -15,7 +15,7 @@
-
+
diff --git a/v2rayN/v2rayN.Desktop/Common/QRCodeAvaloniaUtils.cs b/v2rayN/v2rayN.Desktop/Common/QRCodeAvaloniaUtils.cs
new file mode 100644
index 00000000..90f60c7f
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/Common/QRCodeAvaloniaUtils.cs
@@ -0,0 +1,189 @@
+using System.Runtime.InteropServices;
+using SkiaSharp;
+
+namespace v2rayN.Desktop.Common;
+
+public partial class QRCodeAvaloniaUtils
+{
+ public static byte[]? CaptureScreen()
+ {
+ if (!Utils.IsWindows())
+ {
+ return null;
+ }
+
+ try
+ {
+ return CaptureScreenWindows();
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog("CaptureScreen", ex);
+ return null;
+ }
+ }
+
+ private static byte[]? CaptureScreenWindows()
+ {
+ var hdcScreen = IntPtr.Zero;
+ var hdcMemory = IntPtr.Zero;
+ var hBitmap = IntPtr.Zero;
+
+ try
+ {
+ var workArea = new RECT();
+ SystemParametersInfo(SPI_GETWORKAREA, 0, ref workArea, 0);
+
+ var left = workArea.Left;
+ var top = workArea.Top;
+ var width = workArea.Right - workArea.Left;
+ var height = workArea.Bottom - workArea.Top;
+
+ if (width <= 0 || height <= 0)
+ {
+ left = 0;
+ top = 0;
+ width = GetSystemMetrics(0);
+ height = GetSystemMetrics(1);
+ }
+
+ hdcScreen = GetDC(IntPtr.Zero);
+ if (hdcScreen == IntPtr.Zero)
+ {
+ return null;
+ }
+
+ hdcMemory = CreateCompatibleDC(hdcScreen);
+ hBitmap = CreateCompatibleBitmap(hdcScreen, width, height);
+
+ if (hBitmap == IntPtr.Zero)
+ {
+ return null;
+ }
+
+ SelectObject(hdcMemory, hBitmap);
+
+ const int SRCCOPY = 0x00CC0020;
+ BitBlt(hdcMemory, 0, 0, width, height, hdcScreen, left, top, SRCCOPY);
+
+ var bmi = new BITMAPINFO
+ {
+ biSize = Marshal.SizeOf(typeof(BITMAPINFO)),
+ biWidth = width,
+ biHeight = -height,
+ biPlanes = 1,
+ biBitCount = 32,
+ biCompression = 0
+ };
+
+ var imageSize = width * height * 4;
+ var imageData = new byte[imageSize];
+
+ var scanLines = GetDIBits(hdcScreen, hBitmap, 0, (uint)height, imageData, ref bmi, 0);
+
+ if (scanLines == 0)
+ {
+ return null;
+ }
+
+ using var bitmap = new SKBitmap(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
+ Marshal.Copy(imageData, 0, bitmap.GetPixels(), imageSize);
+
+ using var image = SKImage.FromBitmap(bitmap);
+ using var encoded = image.Encode(SKEncodedImageFormat.Png, 100);
+ return encoded.ToArray();
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog("CaptureScreenWindows", ex);
+ return null;
+ }
+ finally
+ {
+ if (hBitmap != IntPtr.Zero)
+ {
+ DeleteObject(hBitmap);
+ }
+
+ if (hdcMemory != IntPtr.Zero)
+ {
+ DeleteDC(hdcMemory);
+ }
+
+ if (hdcScreen != IntPtr.Zero)
+ {
+ ReleaseDC(IntPtr.Zero, hdcScreen);
+ }
+ }
+ }
+
+ #region Win32 API
+
+ [LibraryImport("user32.dll")]
+ private static partial IntPtr GetDC(IntPtr hwnd);
+
+ [LibraryImport("user32.dll")]
+ private static partial int ReleaseDC(IntPtr hwnd, IntPtr hdc);
+
+ [LibraryImport("gdi32.dll")]
+ private static partial IntPtr CreateCompatibleDC(IntPtr hdc);
+
+ [LibraryImport("gdi32.dll")]
+ private static partial IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
+
+ [LibraryImport("gdi32.dll")]
+ private static partial IntPtr SelectObject(IntPtr hdc, IntPtr hObject);
+
+ [LibraryImport("gdi32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight,
+ IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);
+
+ [LibraryImport("gdi32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool DeleteObject(IntPtr hObject);
+
+ [LibraryImport("gdi32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool DeleteDC(IntPtr hdc);
+
+ [LibraryImport("gdi32.dll")]
+ private static partial int GetDIBits(IntPtr hdc, IntPtr hbmp, uint uStartScan, uint cScanLines,
+ byte[] lpvBits, ref BITMAPINFO lpbmi, uint uUsage);
+
+ [LibraryImport("user32.dll")]
+ private static partial int GetSystemMetrics(int nIndex);
+
+ [LibraryImport("user32.dll", EntryPoint = "SystemParametersInfoW", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool SystemParametersInfo(int uiAction, int uiParam, ref RECT pvParam, int fWinIni);
+
+ private const int SPI_GETWORKAREA = 0x0030;
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct RECT
+ {
+ public int Left;
+ public int Top;
+ public int Right;
+ public int Bottom;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct BITMAPINFO
+ {
+ public int biSize;
+ public int biWidth;
+ public int biHeight;
+ public short biPlanes;
+ public short biBitCount;
+ public int biCompression;
+ public int biSizeImage;
+ public int biXPelsPerMeter;
+ public int biYPelsPerMeter;
+ public int biClrUsed;
+ public int biClrImportant;
+ }
+
+ #endregion Win32 API
+}
diff --git a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs
index 996e7c83..b221d75d 100644
--- a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs
+++ b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs
@@ -162,8 +162,8 @@ public partial class MainWindow : WindowBase
else
{
Title = $"{Utils.GetVersion()}";
+ menuAddServerViaScan.IsVisible = false;
}
- menuAddServerViaScan.IsVisible = false;
if (_config.UiItem.AutoHideStartup && Utils.IsWindows())
{
@@ -336,17 +336,17 @@ public partial class MainWindow : WindowBase
public async Task ScanScreenTaskAsync()
{
- //ShowHideWindow(false);
+ ShowHideWindow(false);
- NoticeManager.Instance.SendMessageAndEnqueue("Not yet implemented.(还未实现)");
- await Task.CompletedTask;
- //if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
- //{
- // //var bytes = QRCodeHelper.CaptureScreen(desktop);
- // //await ViewModel?.ScanScreenResult(bytes);
- //}
+ await Task.Delay(200);
- //ShowHideWindow(true);
+ var bytes = QRCodeAvaloniaUtils.CaptureScreen();
+ if (bytes != null && ViewModel != null)
+ {
+ await ViewModel.ScanScreenResult(bytes);
+ }
+
+ ShowHideWindow(true);
}
private async Task ScanImageTaskAsync()
diff --git a/v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj b/v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj
index 6212ce12..71773766 100644
--- a/v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj
+++ b/v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj
@@ -1,4 +1,4 @@
-
+
WinExe
@@ -6,6 +6,7 @@
true
true
v2rayN
+ true
@@ -52,4 +53,4 @@
-
\ No newline at end of file
+