首页 > 解决方案 > WPF - Graphics.CopyFromScreen 返回尺寸错误的图像

问题描述

使用 WPF,我想截取一个由矩形定义的区域的屏幕截图,该矩形(x,y)代表左上角,以及矩形的widthheight

在此示例中,source屏幕截图的矩形由左上角定义,source.x=0 和 source.y=0,source.width=200 和 source.height=200。

为了确保该功能执行我想要的操作,我将屏幕截图作为图像显示在屏幕上。source我在 position的矩形旁边显示屏幕截图的图像destination。在本例中,destination由destination.x = source.x + source.width (=200)、destination.y=0、destination.width 和destination.height = 200 定义。

这是应用程序的最小工作示例:

<Window x:Class="ScreenViewerWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ScreenViewerWPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        
        WindowState="Maximized"
        Topmost="True"
        AllowsTransparency="True"  WindowStyle="None"
        >
    <Window.Background>
        <SolidColorBrush Opacity="0.5" Color="White"/>
    </Window.Background>
    <Grid Name="grid">
    </Grid>
</Window>
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Image = System.Windows.Controls.Image;

namespace ScreenViewerWPF
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            var source = new
            {
                top = 0,
                left = 0,
                width = 200,
                height = 200
            };
            var dest = new
            {
                top = 0,
                left = 200,
                width = 200,
                height = 200
            };
            //rectangle to visualize the source
            grid.Children.Add(new System.Windows.Shapes.Rectangle()
            {
                Margin = new Thickness(source.left, source.top, 0, 0),
                Width = source.width,
                Height = source.height,
                Stroke = new SolidColorBrush(System.Windows.Media.Color.FromRgb(255, 0, 0)),
                HorizontalAlignment = HorizontalAlignment.Left,
                VerticalAlignment = VerticalAlignment.Top,
            });
            // rectangle to visualize the destination
            grid.Children.Add(new System.Windows.Shapes.Rectangle()
            {
                Margin = new Thickness(dest.left, dest.top, 0, 0),
                Width = dest.width,
                Height = dest.height,
                Stroke = new SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 0, 255)),
                HorizontalAlignment = HorizontalAlignment.Left,
                VerticalAlignment = VerticalAlignment.Top,
            });

            Bitmap bmp = new Bitmap(120*source.width/96, 120 * source.height/96);
            Graphics g = Graphics.FromImage(bmp);
            g.CopyFromScreen(source.left, source.top, 0, 0, bmp.Size);
            bmp.Save("test.jpg", ImageFormat.Jpeg);
            // grabed image
            Image image = new()
            {
                Margin = new Thickness(dest.left, dest.top, 0, 0),
                Width = dest.width,
                Height = dest.height,
                HorizontalAlignment = HorizontalAlignment.Left,
                VerticalAlignment = VerticalAlignment.Top,
            }; grid.Children.Add(image);
            BitmapSource bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(
                bmp.GetHbitmap(),
                IntPtr.Zero,
                Int32Rect.Empty,
                BitmapSizeOptions.FromWidthAndHeight(source.width, source.height)
             );
            image.Source = bitmapSource;
        }
        protected override void OnSourceInitialized(EventArgs e)
        {
            base.OnSourceInitialized(e);
            MakeClicThrough();
        }

        #region make the window clic-through able
        // this is for being able to clic through the form
        [DllImport("user32.dll", SetLastError = true)]
        static extern int GetWindowLong(IntPtr hWnd, int nIndex);
        [DllImport("user32.dll")]
        static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
        const int GWL_EXSTYLE = -20;
        const int WS_EX_LAYERED = 0x80000;
        const int WS_EX_TRANSPARENT = 0x20;
        private void MakeClicThrough()
        {
            var hwnd = new WindowInteropHelper(this).Handle;
            var style = GetWindowLong(hwnd, GWL_EXSTYLE);
            SetWindowLong(hwnd, GWL_EXSTYLE, style | WS_EX_LAYERED | WS_EX_TRANSPARENT);
        }
        #endregion
    }
}

请注意这一行Bitmap bmp = new Bitmap(120*source.width/96, 120 * source.height/96);,这是我用来“尝试”以使事情正常进行的修复。120 是我的计算机 DPI,96 是常数 DPI。

如果没有120/96,我会得到这个(运行应用程序时屏幕左上角的屏幕截图):

在此处输入图像描述

可以看出,两张图片不匹配,右边一张像是“放大了”。

有了 ratio 120/96,我得到了这个,它更好但还不够好(如您所见,第 41 行在右图的图像中,而在左边它位于矩形下方。

在此处输入图像描述

我想在矩形中有“完全”的好图像(或尽可能接近)。如果可能的话,一个优雅的解决方案。

标签: c#wpfgraphicsbitmapscreenshot

解决方案


在计算屏幕上的实际(物理)几何图形时,您需要考虑 Per-Monitor DPI 的缩放。

(...)

var dpi = VisualTreeHelper.GetDpi(grid);
Bitmap bmp = new((int)(source.width * dpi.DpiScaleX), (int)(source.height * dpi.DpiScaleY));

(...)

BitmapSource bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(
    bmp.GetHbitmap(),
    IntPtr.Zero,
    Int32Rect.Empty,
    BitmapSizeOptions.FromEmptyOptions() // or BitmapSizeOptions.FromWidthAndHeight(bmp.Width, bmp.Height)
);
image.Source = bitmapSource;

Image 的位置仍然看起来很奇怪,但它是由 WindowState 和 WindowStyle 设置引起的,因此不是这里的主题。


推荐阅读