You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在C#中为PDFium添加自定义矩形绘图工具箱项及多区域标注功能?

嘿,这个需求其实是自定义控件+PDF文本提取的组合活儿,我帮你拆解成几个核心模块来实现,一步步来:

整体实现方案

核心思路是:自定义一个支持多区域绘制的Windows控件(可添加到工具箱) + PDF坐标与屏幕坐标的转换 + PDFium的区域文本提取 + 区域管理与排序。

1. 制作自定义工具箱控件(支持多颜色矩形绘制)

因为PDFium没有现成的绘制控件,咱们自己造一个:

步骤1:创建自定义控件

新建一个类继承自Panel,直接在承载PDF显示的容器上扩展绘制逻辑(避免控件层级冲突),核心是处理鼠标事件完成矩形绘制:

public class PdfRegionAnnotator : Panel
{
    // 存储所有标注区域
    public List<AnnotatedRegion> Regions { get; set; } = new List<AnnotatedRegion>();
    private Rectangle? _currentDrawingRect;
    private Color _currentDrawColor = Color.Red;
    private Point _startPoint;

    // 设置当前绘制颜色的属性
    public Color CurrentDrawColor
    {
        get => _currentDrawColor;
        set => _currentDrawColor = value;
    }

    public PdfRegionAnnotator()
    {
        DoubleBuffered = true; // 避免绘制闪烁
        MouseDown += OnMouseDown;
        MouseMove += OnMouseMove;
        MouseUp += OnMouseUp;
        Paint += OnPaint;
    }

    private void OnMouseDown(object? sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Left)
        {
            _startPoint = e.Location;
            _currentDrawingRect = new Rectangle(_startPoint, new Size(0, 0));
        }
    }

    private void OnMouseMove(object? sender, MouseEventArgs e)
    {
        if (_currentDrawingRect.HasValue && e.Button == MouseButtons.Left)
        {
            // 更新当前绘制的矩形大小
            int x = Math.Min(_startPoint.X, e.X);
            int y = Math.Min(_startPoint.Y, e.Y);
            int width = Math.Abs(e.X - _startPoint.X);
            int height = Math.Abs(e.Y - _startPoint.Y);
            _currentDrawingRect = new Rectangle(x, y, width, height);
            Invalidate(); // 触发重绘
        }
    }

    private void OnMouseUp(object? sender, MouseEventArgs e)
    {
        if (_currentDrawingRect.HasValue && _currentDrawingRect.Value.Width > 10 && _currentDrawingRect.Value.Height > 10)
        {
            // 添加到区域列表,默认排序号为当前列表长度+1
            Regions.Add(new AnnotatedRegion
            {
                ScreenRect = _currentDrawingRect.Value,
                DrawColor = _currentDrawColor,
                Order = Regions.Count + 1
            });
        }
        _currentDrawingRect = null;
        Invalidate();
    }

    private void OnPaint(object? sender, PaintEventArgs e)
    {
        // 按排序顺序绘制已保存的区域
        foreach (var region in Regions.OrderBy(r => r.Order))
        {
            using var pen = new Pen(region.DrawColor, 2);
            e.Graphics.DrawRectangle(pen, region.ScreenRect);
            // 可选:绘制排序序号
            e.Graphics.DrawString(region.Order.ToString(), Font, Brushes.Black, region.ScreenRect.Location);
        }

        // 绘制正在绘制的临时虚线矩形
        if (_currentDrawingRect.HasValue)
        {
            using var pen = new Pen(_currentDrawColor, 2) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dash };
            e.Graphics.DrawRectangle(pen, _currentDrawingRect.Value);
        }
    }

    // 提供调整区域排序的方法
    public void MoveRegionUp(AnnotatedRegion region)
    {
        if (region.Order <= 1) return;
        var prevRegion = Regions.First(r => r.Order == region.Order - 1);
        (region.Order, prevRegion.Order) = (prevRegion.Order, region.Order);
        Invalidate();
    }

    public void MoveRegionDown(AnnotatedRegion region)
    {
        if (region.Order >= Regions.Count) return;
        var nextRegion = Regions.First(r => r.Order == region.Order + 1);
        (region.Order, nextRegion.Order) = (nextRegion.Order, region.Order);
        Invalidate();
    }
}

// 定义区域实体类,存储关键信息
public class AnnotatedRegion
{
    public Rectangle ScreenRect { get; set; } // 屏幕坐标矩形
    public PdfRect PdfRect { get; set; } // 对应PDF页面的坐标
    public Color DrawColor { get; set; }
    public int Order { get; set; }
    public string ExtractedText { get; set; } = string.Empty;
}

// 对应PDFium的矩形结构(单位:点)
public struct PdfRect
{
    public double Left { get; set; }
    public double Top { get; set; }
    public double Right { get; set; }
    public double Bottom { get; set; }
}

步骤2:添加到工具箱

编译项目后,右键Visual Studio工具箱 → 选择项 → 浏览,找到你编译生成的DLL,选中后确认,这个PdfRegionAnnotator控件就会出现在工具箱里,直接拖到你的PDF阅读器窗体上即可。

2. 屏幕坐标转PDF页面坐标

这是关键!PDF的坐标原点在页面左下角,而屏幕坐标原点在左上角,还要考虑缩放和滚动偏移:

// 假设你用的是基于PDFium的PdfViewer控件,获取当前页面信息
public PdfRect ConvertScreenToPdfRect(Rectangle screenRect, PdfViewer pdfViewer)
{
    var currentPage = pdfViewer.CurrentPage;
    var pageSize = currentPage.Size; // PDF页面实际尺寸(单位:点)
    var zoomFactor = pdfViewer.ZoomFactor; // 当前缩放比例
    var scrollOffset = pdfViewer.ScrollPosition; // 滚动偏移量

    // 转换屏幕X坐标到PDFX
    double pdfX = (screenRect.X + scrollOffset.X) / zoomFactor;
    // 反转Y轴:PDF页面高度 - 转换后的屏幕Y值
    double pdfY = pageSize.Height - (screenRect.Y + scrollOffset.Y) / zoomFactor;
    double pdfWidth = screenRect.Width / zoomFactor;
    double pdfHeight = screenRect.Height / zoomFactor;

    return new PdfRect
    {
        Left = pdfX,
        Top = pdfY - pdfHeight, // 注意PDFium的Top/Bottom定义,测试时可调整
        Right = pdfX + pdfWidth,
        Bottom = pdfY
    };
}

3. 用PDFium提取区域内文本

调用PDFium的原生API,针对转换后的PDF矩形区域提取文本:

public string ExtractTextFromPdfRect(PdfRect pdfRect, IntPtr pdfPageHandle)
{
    var textBuffer = new StringBuilder(1024);
    // 调用PDFium的文本提取方法
    FPDFText_GetTextByRect(pdfPageHandle, pdfRect.Left, pdfRect.Top, pdfRect.Right, pdfRect.Bottom, textBuffer);
    return textBuffer.ToString().Trim();
}

// PDFium的P/Invoke声明(根据你使用的PDFium版本调整)
[DllImport("pdfium.dll", CharSet = CharSet.Unicode)]
private static extern void FPDFText_GetTextByRect(IntPtr page, double left, double top, double right, double bottom, StringBuilder text);

4. 区域排序与文本保存

排序功能

给窗体添加“上移”“下移”按钮,绑定事件实现排序调整:

// 假设用ListBox显示所有区域,选中后点击按钮调整顺序
private void btnMoveUp_Click(object sender, EventArgs e)
{
    if (listBoxRegions.SelectedItem is AnnotatedRegion selectedRegion)
    {
        pdfRegionAnnotator.MoveRegionUp(selectedRegion);
        RefreshRegionList(); // 更新ListBox显示
    }
}

private void RefreshRegionList()
{
    listBoxRegions.Items.Clear();
    foreach (var region in pdfRegionAnnotator.Regions.OrderBy(r => r.Order))
    {
        listBoxRegions.Items.Add($"区域{region.Order} - {region.DrawColor.Name}");
    }
}

保存文本到文件

遍历排序后的区域,提取文本并写入文件:

private void btnSaveText_Click(object sender, EventArgs e)
{
    using var saveFileDialog = new SaveFileDialog
    {
        Filter = "文本文件|*.txt",
        Title = "保存区域文本"
    };

    if (saveFileDialog.ShowDialog() == DialogResult.OK)
    {
        using var writer = new StreamWriter(saveFileDialog.FileName);
        foreach (var region in pdfRegionAnnotator.Regions.OrderBy(r => r.Order))
        {
            // 先转换坐标并提取文本
            var pdfRect = ConvertScreenToPdfRect(region.ScreenRect, pdfViewer);
            region.ExtractedText = ExtractTextFromPdfRect(pdfRect, pdfViewer.CurrentPage.Handle);

            // 写入文件,包含区域信息和文本
            writer.WriteLine($"=== 区域{region.Order} ({region.DrawColor.Name}) ===");
            writer.WriteLine($"屏幕坐标:{region.ScreenRect}");
            writer.WriteLine($"PDF坐标:左{pdfRect.Left:F2}, 上{pdfRect.Top:F2}, 右{pdfRect.Right:F2}, 下{pdfRect.Bottom:F2}");
            writer.WriteLine("文本内容:");
            writer.WriteLine(region.ExtractedText);
            writer.WriteLine();
        }
        MessageBox.Show("文本保存成功!");
    }
}

5. 注意事项

  • PDFium部署:确保项目中包含对应平台(x86/x64)的pdfium.dll,并设置为“复制到输出目录”。
  • 坐标验证:测试时可先提取整个页面文本,对比区域提取结果,确保坐标转换正确。
  • 重绘处理:当PDF页面切换、缩放、滚动时,调用pdfRegionAnnotator.Invalidate()重新绘制区域。
  • 区域编辑:可添加删除区域功能,直接从Regions列表移除选中项后重绘。

内容的提问来源于stack exchange,提问作者Bumba

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 07:09:03