当前位置: 首页 > news >正文

WPF 打印报告图片大小的自适应(含完整示例与详解)

目标:在 FlowDocument 报告里,根据 1~6 张图片的数量, 自动选择 2 行 × 3 列 的最佳布局;在只有 1、2、4 张时保持“占满感”,打印清晰且不变形。

规则一览:

  • 1 张 → 占满 2×3(大图居中)

  • 2 张 → 1×2(只占一行)

  • 3 张 → 1×3(只占一行)

  • 4 张 → 2×2(视觉上仍居中于 2×3 区域)

  • 5–6 张 → 2×3

  

一、为什么要“自适应 + 占满感”?

打印报告的图片既要 等比缩放,又要在不同数量时 视觉均衡。常见坑:

  • 直接 UniformGrid 2×3 固定排布,1/2/4 张图会显得松散;

  • 源图 DPI 不一致导致打印缩放异常;

  • Stretch 使用不当造成拉伸变形或留白过多。

本文通过 ItemsControl + 动态 ItemsPanelTemplate 实现布局切换,并配合 DPI 统一、像素对齐 提升打印品质。

二、XAML(FlowDocument 内)

说明:预定义 5 套布局模板;其中 2×2 居中 模板用外层 2×3 容器包一层 2×2,使其在视觉上位于中间。

<FlowDocument.Resources><!-- 2×3 大面板,用于 1 张图占满整个区域(或作为通用容器) --><ItemsPanelTemplate x:Key="Panel1x1"><UniformGrid Rows="2" Columns="3"/></ItemsPanelTemplate><!-- 1 行 2 列:用于 2 张图 --><ItemsPanelTemplate x:Key="Panel1x2"><UniformGrid Rows="1" Columns="2"/></ItemsPanelTemplate><!-- 1 行 3 列:用于 3 张图 --><ItemsPanelTemplate x:Key="Panel1x3"><UniformGrid Rows="1" Columns="3"/></ItemsPanelTemplate><!-- 2×2 居中在 2×3 区域:用于 4 张图(让视觉更均衡) --><ItemsPanelTemplate x:Key="Panel2x2Centered"><Grid><!-- 外层 2×3 占位 --><Grid.RowDefinitions><RowDefinition/><RowDefinition/></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition/><ColumnDefinition/></Grid.ColumnDefinitions><!-- 内层 2×2 真正承载图片,放中间两列(或居中对齐) --><UniformGrid Rows="2" Columns="2"Grid.RowSpan="2" Grid.ColumnSpan="2"Grid.Row="0" Grid.Column="0"HorizontalAlignment="Center" VerticalAlignment="Center"/></Grid></ItemsPanelTemplate><!-- 2×3:用于 5~6 张图 --><ItemsPanelTemplate x:Key="Panel2x3"><UniformGrid Rows="2" Columns="3"/></ItemsPanelTemplate><!-- 单项模板:标题 + 图片(等比缩放、像素对齐) --><DataTemplate x:Key="PrintImageTemplate"><StackPanel Orientation="Vertical"><TextBlock Text="{Binding ImageInfo}"FontSize="12" Margin="5" TextWrapping="Wrap"/><!-- 外层 Grid 允许拉伸占满单元格,Image 使用 Uniform 保持比例 --><Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"SnapsToDevicePixels="True" UseLayoutRounding="True"><Image Source="{Binding BitmapSource}"Stretch="Uniform"RenderOptions.BitmapScalingMode="HighQuality"RenderOptions.EdgeMode="Aliased"Margin="5"/></Grid></StackPanel></DataTemplate>
</FlowDocument.Resources><!-- 图像区域(2 行 3 列的总体设计) -->
<ItemsControl Name="ImagesItemsControl"Grid.Row="6" Grid.ColumnSpan="3" Margin="0,5,0,15"ItemTemplate="{StaticResource PrintImageTemplate}"><!-- 让每项容器撑满自身单元格,避免 Image 因容器未拉伸而显示偏小 --><ItemsControl.ItemContainerStyle><Style TargetType="ContentPresenter"><Setter Property="HorizontalAlignment" Value="Stretch"/><Setter Property="VerticalAlignment" Value="Stretch"/></Style></ItemsControl.ItemContainerStyle>
</ItemsControl>

小贴士

  • 打印走 FlowDocument 时,尽量保持控件树简单,避免过多 ViewBox 嵌套。

  • SnapsToDevicePixelsUseLayoutRounding 有助于边界像素对齐,减少细纹模糊。

三、模型定义(数据与视图绑定)

public class PrintImageModel
{public BitmapSource BitmapSource { get; set; }public string ImageInfo { get; set; }  // 标题/说明(拍摄时间、层号等)
}

四、代码绑定(含 DPI 统一与布局选择)

说明:

  1. 先对图片按深度/序号排序,只取前 6 张;

  2. 将位图统一到 96 DPI,避免打印引擎因 DPI 不一致而缩放异常;

  3. 根据数量选择 ItemsPanel;

  4. 绑定到 ItemsControl

三、模型定义(数据与视图绑定)public class PrintImageModel
{public BitmapSource BitmapSource { get; set; }public string ImageInfo { get; set; }  // 标题/说明(拍摄时间、层号等)
}// 根据数量选择 ItemsPanelTemplate
private static ItemsPanelTemplate GetPanelForCount(ResourceDictionary res, int count)
{ItemsPanelTemplate Panel(string key) => res[key] as ItemsPanelTemplate;return count switch{<= 0 => Panel("Panel2x3"),        // 回退1    => Panel("Panel1x1"),         // 单图占满 2×32    => Panel("Panel1x2"),3    => Panel("Panel1x3"),4    => Panel("Panel2x2Centered"), // 2×2 居中_    => Panel("Panel2x3"),         // 5~6};
}// 将 BitmapSource 统一到 96 DPI,打印更稳定
private static BitmapSource EnsureDpi96(BitmapSource src)
{if (src == null) return null;const double dpi = 96.0;if (Math.Abs(src.DpiX - dpi) < 0.1 && Math.Abs(src.DpiY - dpi) < 0.1)return src; // 已经是 96 DPI// 复制像素数据到新的 96 DPI 位图var format = src.Format; // 保留原像素格式int stride = (src.PixelWidth * format.BitsPerPixel + 7) / 8;byte[] buffer = new byte[stride * src.PixelHeight];src.CopyPixels(buffer, stride, 0);var normalized = BitmapSource.Create(src.PixelWidth, src.PixelHeight, dpi, dpi,format, src.Palette, buffer, stride);normalized.Freeze();return normalized;
}public void BindReportImages(FlowDocument doc,IEnumerable<(int Depth, Mat Mat, object Meta)> tempList,string timeStr)
{if (doc == null) return;var imagesItemsControl = doc.FindName("ImagesItemsControl") as ItemsControl;if (imagesItemsControl == null) return;// 1) 排序并取前 6 张var list = tempList?.OrderBy(s => s.Depth).Take(6).ToList() ?? new();// 2) 组装绑定模型,并统一 DPIvar imageModelList = new List<PrintImageModel>(list.Count);foreach (var item in list){if (item.Mat == null) continue;var src = BitmapUtils.ToBitmapSource2(item.Mat); // 你现有的 Mat → BitmapSourcevar fixedDpi = EnsureDpi96(src);imageModelList.Add(new PrintImageModel{BitmapSource = fixedDpi,ImageInfo = GetImageInfo(item.Meta, timeStr, item.Depth)});}// 3) 选择合适的 ItemsPanelimagesItemsControl.ItemsPanel = GetPanelForCount(doc.Resources, imageModelList.Count);// 4) 绑定数据imagesItemsControl.ItemsSource = imageModelList;
}

关于 BitmapUtils.ToBitmapSource2:如果它内部使用了 MemoryStream 临时对象,请务必在 BitmapImage 完成初始化后 Freeze(),以防打印时跨线程访问异常。


五、打印清晰度与缩放的关键点

  1. 统一 DPI(推荐 96):WPF 视觉树的度量与渲染以 96 DPI 为基准。若源图为 300 DPI,但尺寸以像素为准,打印时仍以像素为依据,可能出现缩放计算偏差,统一 DPI 可减少不可控因素。

  2. 像素对齐:启用 SnapsToDevicePixelsUseLayoutRounding,避免边界半像素导致的灰边。

  3. 高质量缩放RenderOptions.BitmapScalingMode="HighQuality" 能在缩小图片时明显改善清晰度。

  4. 避免多层缩放:不要在 Image 外再套 ViewBox,否则缩放叠乘影响清晰度。

  5. 打印管线:如对分页/边距有严格控制,考虑使用 DocumentPaginator 或 FixedDocument,避免 Flow 文档自动分页带来的不可控换行。


六、常见问题(FAQ)

Q1:为什么 4 张图不用 2×3?
A:2×2 更大更聚焦。通过“2×2 居中到 2×3”视觉上仍与其他布局保持一致的占位比例。

Q2:只有 1 张图为何不用 1×1?
A:使用 2×3 的容器能与 2×3 总体版式对齐,且大图居中更美观,留白更合理。

Q3:源图是 16 位灰度(例如医学影像)怎么办?
A:先在内存中转换为 8 位或 24 位 BGR 的 BitmapSource,再参与绑定与 DPI 统一,避免打印时的像素格式兼容性问题。

Q4:图片很大(4K/8K)会卡顿?
A:打印前对像素尺寸进行约束(如最长边不超过 A4/A3 目标像素),并在后台线程解码,主线程只做绑定。

七、结语

通过 动态 ItemsPanel + DPI 统一 + 像素对齐,我们实现了打印报告中 1~6 张图片的自适应与高质量输出。你可以直接把本文的 XAML 与 C# 片段粘到你的项目里使用,或在此基础上扩展更多版式(如 3×3、横竖版自动切换等)。

http://www.lryc.cn/news/625172.html

相关文章:

  • Rust 入门 生命周期-next2 (十九)
  • 牛津大学xDeepMind 自然语言处理(1)
  • Centos7 使用lamp架构部署wordpress
  • 接口和抽象类的区别(面试回答)
  • 【深度长文】Anthropic发布Prompt Engineering全新指南
  • Java面向对象三大特性:封装、继承、多态深度解析与实践应用
  • ⭐CVPR2025 RigGS:从 2D 视频到可编辑 3D 关节物体的建模新范式
  • 音频分类模型笔记
  • OOP三大特性
  • 【计算机视觉与深度学习实战】05计算机视觉与深度学习在蚊子检测中的应用综述与假设
  • 网络基础——协议认识
  • Pytest项目_day18(读取ini文件)
  • Unity 中控开发 多路串口服务器(一)
  • 深层语义知识图谱:提升NLP文本预处理效果的关键技术
  • C++ 多进程编程深度解析【C++进阶每日一学】
  • 一个基于纯前端技术实现的五子棋游戏,无需后端服务,直接在浏览器中运行。
  • 深度学习篇---softmax层
  • Maven 生命周期和插件
  • 大数据分析-读取文本文件内容进行词云图展示
  • 大厂求职 | 2026海尔校园招聘,启动!
  • Vuex 状态持久化企业级解决方案
  • ​Kali Linux 环境中的系统配置文件与用户配置文件大全
  • MongoDB 从入门到精通:安装配置与基础操作指令详解
  • 计算机组成原理(9) - 整数的乘除法运算
  • 抽象类和接口的区别
  • VLN视觉语言导航(3)——神经网络的构建和优化 2.3
  • qsort函数使用及其模拟实现
  • Android Cutout(屏幕挖孔)详解
  • SpringBoot--Spring MVC 拦截器注入与 new 的区别
  • gdb的load命令和传给opeocd的monitor flash write_image erase命令的区别