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

C# WinForms递归实现PrintDocument多页打印图片丢失问题求助

问题描述

副业从事金属加工,开发了基于C# Windows Forms + SQL Server的管理程序,其中Category表支持多级子分类,配套Product表存储产品信息。需求是通过PrintDocument功能动态生成层级分明的产品目录,Category类结构如下:

public class ProductCategory
{
    private int iD = -1;
    private int parentID = -1;
    private string catName = string.Empty;
    private List<ProductCategory> childCategories = new List<ProductCategory>();
}

采用递归函数遍历多级嵌套分类,但多页打印时出现问题:当图片即将跨页时无法正常打印,设置e.HasMorePages = true; return;后,后续未完成的图片无法继续处理。附当前打印功能代码:

private void CreateDocument()
{
    PrintDialog printDialog1 = new PrintDialog();
    printDialog1.Document = printDocument1;

    DialogResult result = printDialog1.ShowDialog();

    if (result == DialogResult.OK)
    {
        printDocument1.Print();
    }
}

private void printDocument1_PrintPage(object sender, PrintPageEventArgs e)
{
    pageNumber++;

    PrintPageNumber(pageNumber, e);

    yPos = e.MarginBounds.Top;

    if (pageNumber == 1)
    {
        PrintCompanyInfo(e);
    }

    for (int j = catIndex; j < orderedCategories.Count; j++)
    {
        categoryFontSize = categoryDefaultFontSize;
        yPos += mainCategoryPaddingY;

        DoCategoryRecursive(orderedCategories[j], e);
        catIndex = j;
    }
}

private int catIndex = 0;

private void PrintCompanyInfo(PrintPageEventArgs e)
{
    // Create image.
    Image newImage = Image.FromFile(companyLogo);

    // Create coordinates for upper-left corner of image.
    int x = e.MarginBounds.Left;
    int y = e.MarginBounds.Top;

    // Draw image to screen.
    e.Graphics.DrawImage(newImage, x, y, photoWidth, photoHeight);

    e.Graphics.DrawString("Leo InnoTech (Pty) Ltd", new Font("Calibri", 28, FontStyle.Regular), Brushes.Black, new Point(350, 100));
    e.Graphics.DrawString("Registration Number: 2024 / 378584 / 07", new Font("Arial", 9, FontStyle.Bold), Brushes.Black, new Point(410, 160));
    e.Graphics.DrawString("Phone:", new Font("Calibri", 14, FontStyle.Regular), Brushes.Black, new Point(410, 210));
    e.Graphics.DrawString("0611 864 142", new Font("Calibri", 14, FontStyle.Regular), Brushes.Black, new Point(490, 210));
    e.Graphics.DrawString("Email Address:", new Font("Calibri", 14, FontStyle.Regular), Brushes.Black, new Point(350, 235));
    e.Graphics.DrawString("leoinnotechsa@gmail.com", new Font("Calibri", 14, FontStyle.Regular), Brushes.Blue, new Point(490, 235));

    yPos = y + photoHeight + productPaddingY;
}

private void PrintPageNumber(int pageNr, PrintPageEventArgs e)
{
    int x = e.MarginBounds.Right;
    int y = e.MarginBounds.Bottom;

    e.Graphics.DrawString(pageNr.ToString(), new Font("Calibri", 14, FontStyle.Regular), Brushes.Black, new Point(x, y));
}

private void DoCategoryRecursive(ProductCategory cat, PrintPageEventArgs e)
{
    // Load all Products for cat
    List<Product> products = Products_Manager_SqlServer.Load_Products_ByCategoryID_Controller(connectionString, cat.ID);

    if (products.Count > 0 || cat.ChildCategories.Count > 0)
    {
        if (yPos > e.MarginBounds.Bottom)
        {
            e.HasMorePages = true;
            return;
        }

        // Add category name to document
        e.Graphics.DrawString(cat.CatName, new Font("Calibri", categoryFontSize, FontStyle.Bold), Brushes.Black, new Point(xPos, yPos));

        PrintProducts(products, e);

        if (cat.ChildCategories.Count > 0)
        {
            foreach (ProductCategory child in cat.ChildCategories)
            {
                yPos += minorCategoryPaddingY;
                categoryFontSize -= fontSizeDecrement;
                DoCategoryRecursive(child, e);
            }
        }
        else
        {
            return;
        }
    }
    else
    {
        return;
    }
}

private void PrintProducts(List<Product> products, PrintPageEventArgs e)
{
    string photoPath = defaultPhotoPath;
    Image newImage = null;

    foreach (Product product in products)
    {
        yPos += productPaddingY;

        if (yPos + photoHeight > e.MarginBounds.Bottom)
        {
            e.HasMorePages = true;
            return;
        }

        // Get primary Photo
        List<ProductPhoto> photos = ProductPhotos_Manager_SqlServer.Load_PhotoPrimary_ByProduct_Controller(connectionString, product.ID);

        if (photos.Count > 0)
        {
            photoPath = photos[0].Photo;
        }

        // Create image.
        newImage = Image.FromFile(photoPath);

        // Draw image to document.
        e.Graphics.DrawImage(newImage, xPos + photoOffsetX, yPos, photoWidth, photoHeight);

        yPos += photoHeight;

        if (yPos > e.MarginBounds.Bottom)
        {
            e.HasMorePages = true;
            return;
        }
    }
}

需求:获取问题解决方向,或更合适的遍历处理方式,目标目录为层级分明的分类+产品图片样式。


解决方向

核心问题根源

当前递归打印的逻辑与打印状态强耦合,中断打印返回后,无法记录递归的当前位置(如正在处理的分类、已打印到第几个产品),导致下一页打印时要么重复处理,要么直接跳过未完成内容。

推荐解决方案

1. 提前展平打印任务(最优方案)

脱离打印过程中的递归,提前将所有需要打印的内容(分类标题、产品图片)按顺序展平为一个打印任务列表,每个任务包含类型、内容、所需高度等信息。打印时逐页从列表中取任务,记录当前任务索引,下一页从该索引继续处理。

示例实现思路:

// 提前构建打印任务列表
List<PrintTask> printTasks = new List<PrintTask>();
BuildPrintTasks(orderedCategories, printTasks, categoryDefaultFontSize);

// 打印时维护当前任务索引
private int currentTaskIndex = 0;

private void printDocument1_PrintPage(object sender, PrintPageEventArgs e)
{
    pageNumber++;
    PrintPageNumber(pageNumber, e);
    yPos = e.MarginBounds.Top;

    if (pageNumber == 1)
    {
        PrintCompanyInfo(e);
    }

    while (currentTaskIndex < printTasks.Count)
    {
        var task = printTasks[currentTaskIndex];
        if (task.Type == PrintTaskType.Category)
        {
            // 检查当前位置是否能放下分类标题
            if (yPos + task.Height > e.MarginBounds.Bottom)
            {
                e.HasMorePages = true;
                return;
            }
            // 绘制分类标题
            e.Graphics.DrawString(task.Text, task.Font, Brushes.Black, xPos, yPos);
            yPos += task.Height + task.Padding;
            currentTaskIndex++;
        }
        else if (task.Type == PrintTaskType.Product)
        {
            // 检查是否能放下产品图片
            if (yPos + productPaddingY + photoHeight > e.MarginBounds.Bottom)
            {
                e.HasMorePages = true;
                return;
            }
            // 绘制产品图片(注意释放资源)
            using (Image img = Image.FromFile(task.ImagePath))
            {
                e.Graphics.DrawImage(img, xPos + photoOffsetX, yPos + productPaddingY, photoWidth, photoHeight);
            }
            yPos += productPaddingY + photoHeight;
            currentTaskIndex++;
        }
    }
    e.HasMorePages = false;
}

// 辅助类:定义打印任务类型和属性
public enum PrintTaskType { Category, Product }
public class PrintTask
{
    public PrintTaskType Type { get; set; }
    public string Text { get; set; }
    public string ImagePath { get; set; }
    public Font Font { get; set; }
    public int Height { get; set; }
    public int Padding { get; set; }
}

// 构建打印任务列表(这里递归构建任务没问题,因为不在打印过程中执行)
private void BuildPrintTasks(List<ProductCategory> categories, List<PrintTask> tasks, int fontSize)
{
    foreach (var cat in categories)
    {
        // 添加分类任务
        Font catFont = new Font("Calibri", fontSize, FontStyle.Bold);
        int catHeight = (int)Graphics.FromImage(new Bitmap(1, 1)).MeasureString(cat.CatName, catFont).Height;
        tasks.Add(new PrintTask
        {
            Type = PrintTaskType.Category,
            Text = cat.CatName,
            Font = catFont,
            Height = catHeight,
            Padding = mainCategoryPaddingY
        });

        // 添加该分类下的产品任务
        var products = Products_Manager_SqlServer.Load_Products_ByCategoryID_Controller(connectionString, cat.ID);
        foreach (var prod in products)
        {
            var photos = ProductPhotos_Manager_SqlServer.Load_PhotoPrimary_ByProduct_Controller(connectionString, prod.ID);
            string imgPath = photos.Count > 0 ? photos[0].Photo : defaultPhotoPath;
            tasks.Add(new PrintTask
            {
                Type = PrintTaskType.Product,
                ImagePath = imgPath,
                Padding = productPaddingY
            });
        }

        // 递归处理子分类
        if (cat.ChildCategories.Count > 0)
        {
            BuildPrintTasks(cat.ChildCategories, tasks, fontSize - fontSizeDecrement);
        }
    }
}

2. 修复递归打印的状态记录(备选)

如果坚持使用递归,需要显式记录递归的当前状态:

  • 给ProductCategory添加标记字段,记录该分类是否已打印、已打印到第几个产品。
  • 打印时,递归函数先检查分类的打印状态,从已打印的产品索引开始继续处理。
  • 此方式复杂度高,容易出现状态混乱,仅建议在无法重构代码时使用。

3. 资源优化注意事项

  • 当前代码中Image.FromFile后未释放资源,长期运行会导致内存泄漏,需用using语句或手动调用Dispose()。
  • 打印前提前加载所有产品图片路径,避免打印过程中频繁访问数据库,提升打印稳定性和速度。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 21:18:20