如题,使用C++做GDI类库来让WPF程序调用,然后捕获屏幕,至于为什么不用现成的nuget包,大多数开源的nuget包(捕获屏幕的)要么缺少维护,要么过于复杂,有没有什么简单的呢?这不就来了,ScreenCaptureWpfEasy,一个开源项目,当然除了WPF其它Windows程序也是可以调用的

先说环境,VS2022,C++20,NET8,示例程序使用了IUWM项目美化

一,C++部分

1,函数名

extern "C" [[nodiscard]] bool __cdecl CaptureFrame(
    int x, int y,
    const int width, const int height,
    uint8_t** outBuffer,
    int* outWidth, int* outHeight) noexcept
  • extern "C" 保证函数名不被 C++ 修饰,便于动态调用

  • [[nodiscard]] 提示“调用方应检查返回值”

  • noexcept 表明不会抛异常

2,统一变量 

void*      pBits    = nullptr;
HBITMAP    hBitmap  = nullptr;
constexpr int bytesPerPixel = 2;
size_t     rowBytes = 0;
int        stride   = 0;
size_t     bufSize  = 0;
uint8_t*   buffer   = nullptr;
uint8_t*   srcBase  = nullptr;
struct BI16 { BITMAPINFOHEADER hdr; DWORD masks[3]; } bmi16 = {};
  • bytesPerPixel、rowBytes、stride、bufSize 用于后续快速计算

3,捕获屏幕,检查DC

HDC hScreenDC = GetDC(nullptr);
if (!hScreenDC) return false;

HDC hMemDC = CreateCompatibleDC(hScreenDC);
if (!hMemDC) goto cleanup_dc;
  • GetDC(nullptr) 拿到整个屏幕的设备上下文

  • CreateCompatibleDC 创建与屏幕 DC 格式匹配的内存 DC,用于后续 

4,准备 转入

bmi16.hdr.biSize        = sizeof(BITMAPINFOHEADER);
bmi16.hdr.biWidth       = width;
bmi16.hdr.biHeight      = -height;      // 负值:自顶向下
bmi16.hdr.biPlanes      = 1;
bmi16.hdr.biBitCount    = 16;           // 16 位 RGB565
bmi16.hdr.biCompression = BI_BITFIELDS;
bmi16.masks[0] = 0xF800;
bmi16.masks[1] = 0x07E0;
bmi16.masks[2] = 0x001F;
  • 构造 BITMAPINFOHEADER 并通过三个掩码指定 RGB565 的 R/G/B 分量位置

  • 自顶向下 的 biHeight 使像素行从上至下存储,避免出现捕获奇葩画面

 5,创建 DIBSection 并执行 BitBlt

hBitmap = CreateDIBSection(
    hMemDC,
    reinterpret_cast<BITMAPINFO*>(&bmi16),
    DIB_RGB_COLORS,
    &pBits, nullptr, 0);
if (!hBitmap) goto cleanup_dc;

SelectObject(hMemDC, hBitmap);
if (!BitBlt(hMemDC, 0, 0, width, height, hScreenDC, x, y, SRCCOPY))
    goto cleanup_bitmap;
  • CreateDIBSection 在内存中做连续像素缓冲,格式为 RGB565

  • SelectObject 把该位图挂到 hMemDC,再用 BitBlt 从屏幕 DC 拷贝指定区域到内存 DC

6,内存管理

rowBytes = size_t(width) * bytesPerPixel;
stride   = ((width * 16 + 31) / 32) * 4;
bufSize  = rowBytes * size_t(height);
srcBase  = static_cast<uint8_t*>(pBits);

buffer = static_cast<uint8_t*>(CoTaskMemAlloc(bufSize));
if (!buffer) goto cleanup_bitmap;
  • 每行实际像素字节数 = width * 2

  • DIBSection 每行实际占用字节数(含对齐填充),按 4 字节对齐

  • 分配大小是 rowBytes × height

7,像素筛选

for (int row = 0; row < height; ++row) {
    std::memcpy(
        buffer + size_t(row) * rowBytes,
        srcBase + size_t(row) * stride,
        rowBytes);
}
  • 遍历每行,将有意义的像素数据从 srcBase 拷贝到 buffer

  • 跳过每行末尾的填充字节,确保输出紧凑连续

8,结果输出和资源释放 

*outBuffer = buffer;
*outWidth  = width;
*outHeight = height;

DeleteObject(hBitmap);
DeleteDC(hMemDC);
ReleaseDC(nullptr, hScreenDC);
return true;
  • 将缓冲指针和宽高回传给调用方

  • 释放 GDI 和 DC

 9,错误清理

cleanup_bitmap:
    DeleteObject(hBitmap);
cleanup_dc:
    if (hMemDC)    DeleteDC(hMemDC);
    if (hScreenDC) ReleaseDC(nullptr, hScreenDC);
    return false;
  • 在 DIB 创建后或 DC 创建后触发集中执行清理

二,WPF部分 

1,先做个布局

<ui:Page x:Class="NavigationViewExample.Pages.HomePage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      xmlns:local="clr-namespace:NavigationViewExample.Pages"
      xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
      mc:Ignorable="d" 
      d:DesignHeight="450" d:DesignWidth="800"
      Title="Home" Loaded="Page_Loaded_1">
    <ui:ScrollViewerEx VerticalScrollBarVisibility="Visible">
        <Grid >
            <Image x:Name="imageAA" Margin="0,0,372,238"  />
            <Button Content="Left+" Height="36" Width="93" VerticalAlignment="Top" Click="Button_Click" />
            <Button Content="Top+" Height="36" Width="92" Margin="98,0,0,0" VerticalAlignment="Top" Click="Button_Click_1" />
            <Button Content="Width+" Height="36" Width="93" Margin="0,41,0,0" VerticalAlignment="Top" Click="Button_Click_2" />
            <Button Content="Height+" Height="36" Width="93" Margin="97,41,0,0" VerticalAlignment="Top" Click="Button_Click_3" />
            <Button Content="Left-" Height="36" Width="93" Margin="0,88,0,0" VerticalAlignment="Top" Click="Button_Click_4" />
            <Button Content="Top-" Height="36" Width="93" Margin="97,88,0,0" VerticalAlignment="Top" Click="Button_Click_5" />
            <Button Content="Width-" Height="36" Width="93" Margin="0,129,0,0" VerticalAlignment="Top" Click="Button_Click_6" />
            <Button Content="Height-" Height="36" Width="93" Margin="97,129,0,0" VerticalAlignment="Top" Click="Button_Click_7" />
        </Grid>
    </ui:ScrollViewerEx>
</ui:Page>

这样你将看到以下界面(记得安装iNKORE.UI.WPF.Modern)  

2,界面后台代码 

namespace NavigationViewExample.Pages
{
    /// <summary>
    /// Interaction logic for HomePage.xaml
    /// </summary>
    public partial class HomePage : Page
    {
        public HomePage()
        {
            InitializeComponent();
        }
        private double left = 100;
        private double top  = 100;
        private double right= 100;
        private double bottom= 100;
        private string filePath="";
        private void Page_Loaded_1(object sender, RoutedEventArgs e)
        {            
            string baseDir = AppDomain.CurrentDomain.BaseDirectory;              
            filePath = Path.Combine(baseDir, "Appdata", "data2.txt");
            if (File.Exists(filePath))
            {
                string content = File.ReadAllText(filePath).Trim();
                var tokens = content.TrimEnd('_').Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
                left = double.Parse(tokens[0], CultureInfo.InvariantCulture);
                top = double.Parse(tokens[1], CultureInfo.InvariantCulture);
                right = double.Parse(tokens[2], CultureInfo.InvariantCulture);
                bottom = double.Parse(tokens[3], CultureInfo.InvariantCulture);
                imageAA.Margin = new Thickness(left, top, right, bottom);
            }
            else 
            {
                File.WriteAllText(filePath, "100_100_100_100");
                imageAA.Margin = new Thickness(left, top, right, bottom);
            }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            imageAA.Margin = new Thickness(left=left+1, top, right,bottom);
            File.WriteAllText(filePath, $"{left}_{top}_{right}_{bottom}");
        }

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            imageAA.Margin = new Thickness(left , top=top+1, right , bottom );
            File.WriteAllText(filePath, $"{left}_{top}_{right}_{bottom}");
        }

        private void Button_Click_2(object sender, RoutedEventArgs e)
        {
            imageAA.Margin = new Thickness(left , top , right = right + 1, bottom );
            File.WriteAllText(filePath, $"{left}_{top}_{right}_{bottom}");
        }

        private void Button_Click_3(object sender, RoutedEventArgs e)
        {
            imageAA.Margin = new Thickness(left, top, right , bottom = bottom + 1);
            File.WriteAllText(filePath, $"{left}_{top}_{right}_{bottom}");
        }

        private void Button_Click_4(object sender, RoutedEventArgs e)
        {
            imageAA.Margin = new Thickness(left = left - 1, top, right, bottom);
            File.WriteAllText(filePath, $"{left}_{top}_{right}_{bottom}");
        }

        private void Button_Click_5(object sender, RoutedEventArgs e)
        {
            imageAA.Margin = new Thickness(left, top = top - 1, right, bottom);
            File.WriteAllText(filePath, $"{left}_{top}_{right}_{bottom}");
        }

        private void Button_Click_6(object sender, RoutedEventArgs e)
        {
            imageAA.Margin = new Thickness(left, top, right = right - 1, bottom);
            File.WriteAllText(filePath, $"{left}_{top}_{right}_{bottom}");
        }

        private void Button_Click_7(object sender, RoutedEventArgs e)
        {
            imageAA.Margin = new Thickness(left, top, right, bottom = bottom - 1);
            File.WriteAllText(filePath, $"{left}_{top}_{right}_{bottom}");
        }
    }
}

 3,做个窗口,方便选择捕获选区

<ui:Page x:Class="NavigationViewExample.Pages.HomePage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      xmlns:local="clr-namespace:NavigationViewExample.Pages"
      xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
      mc:Ignorable="d" 
      d:DesignHeight="450" d:DesignWidth="800"
      Title="Home" Loaded="Page_Loaded_1">
    <ui:ScrollViewerEx VerticalScrollBarVisibility="Visible">
        <Grid >
            <Image x:Name="Screenimage" Margin="0,0,372,238"  />
            <Button Content="Left+" Height="36" Width="93" VerticalAlignment="Top" Click="Button_Click" />
            <Button Content="Top+" Height="36" Width="92" Margin="98,0,0,0" VerticalAlignment="Top" Click="Button_Click_1" />
            <Button Content="Width+" Height="36" Width="93" Margin="0,41,0,0" VerticalAlignment="Top" Click="Button_Click_2" />
            <Button Content="Height+" Height="36" Width="93" Margin="97,41,0,0" VerticalAlignment="Top" Click="Button_Click_3" />
            <Button Content="Left-" Height="36" Width="93" Margin="0,88,0,0" VerticalAlignment="Top" Click="Button_Click_4" />
            <Button Content="Top-" Height="36" Width="93" Margin="97,88,0,0" VerticalAlignment="Top" Click="Button_Click_5" />
            <Button Content="Width-" Height="36" Width="93" Margin="0,129,0,0" VerticalAlignment="Top" Click="Button_Click_6" />
            <Button Content="Height-" Height="36" Width="93" Margin="97,129,0,0" VerticalAlignment="Top" Click="Button_Click_7" />
            <Button Content="选择捕获区域" Margin="46,170,0,0" VerticalAlignment="Top" Width="128" Click="Button_Click_8"/>
            <Button Content="Start" Margin="46,206,0,0" Height="34" Width="90" Click="Button_Click_9" VerticalAlignment="Top"/>
        </Grid>
    </ui:ScrollViewerEx>
</ui:Page>

 后台代码太多了,这里就不放了

 4,调用

        [DllImport("kernel32.dll", SetLastError = true)]// 在应用启动时设置 DLL 搜索路径
        private static extern bool SetDllDirectory(string lpPathName);
        [DllImport("ScreenCaptureWpfEasy.dll", CallingConvention = CallingConvention.Cdecl)]// 原有 P/Invoke 声明
        private static extern bool CaptureFrame(int x, int y, int width, int height, out IntPtr buffer, out int outWidth, out int outHeight);
        [DllImport("ScreenCaptureWpfEasy.dll", CallingConvention = CallingConvention.Cdecl)]

引用dll

        private static extern void FreeBuffer(IntPtr buffer);
        private int CatchLeft = 100;
        private int CatchTop = 300;
        private int CatchWidth = 900;
        private int CatchHeight = 600;
        private async Task CaptureLoopAsync()
        {
            while (_running == true)
            {
                if (!CaptureFrame(CatchLeft, CatchTop, CatchWidth, CatchHeight, out IntPtr bufPtr, out int width, out int height))
                {
                    MessageBox.Show("Capture failed");
                    return;
                }
                int stride = width * 2;
                int bufSize = height * stride;
                var bmp = BitmapSource.Create(width, height, 96, 96, PixelFormats.Bgr565, null, bufPtr, bufSize, stride);
                bmp.Freeze();
                Application.Current.Dispatcher.Invoke(() =>
                {
                    Screenimage.Source = bmp;
                });
                FreeBuffer(bufPtr);
                await Task.Delay(100);//循环间隔
            }
        }

缓存大小以及刷新频率

        private volatile bool _running = false;
        private void Button_Click_9(object sender, RoutedEventArgs e)
        {
            if (_running == false)
            {
                _running = true;
                Task.Run(CaptureLoopAsync);
                St.Content= "stop";
            }
            else 
            {
                _running = false;
                St.Content = "start";
            }
        }

防止重开等

三,演示

github    阿里云盘 (压缩包密码:20250730)

sbcsdn,传了视频毛都没有

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐