目标:在 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,使其在视觉上位于中间。
XML
<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
嵌套。
SnapsToDevicePixels
与UseLayoutRounding
有助于边界像素对齐,减少细纹模糊。
三、模型定义(数据与视图绑定)
cs
public class PrintImageModel
{
public BitmapSource BitmapSource { get; set; }
public string ImageInfo { get; set; } // 标题/说明(拍摄时间、层号等)
}
四、代码绑定(含 DPI 统一与布局选择)
说明:
先对图片按深度/序号排序,只取前 6 张;
将位图统一到 96 DPI,避免打印引擎因 DPI 不一致而缩放异常;
根据数量选择 ItemsPanel;
绑定到
ItemsControl
。
cs
三、模型定义(数据与视图绑定)
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×3
2 => 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) 组装绑定模型,并统一 DPI
var imageModelList = new List<PrintImageModel>(list.Count);
foreach (var item in list)
{
if (item.Mat == null) continue;
var src = BitmapUtils.ToBitmapSource2(item.Mat); // 你现有的 Mat → BitmapSource
var fixedDpi = EnsureDpi96(src);
imageModelList.Add(new PrintImageModel
{
BitmapSource = fixedDpi,
ImageInfo = GetImageInfo(item.Meta, timeStr, item.Depth)
});
}
// 3) 选择合适的 ItemsPanel
imagesItemsControl.ItemsPanel = GetPanelForCount(doc.Resources, imageModelList.Count);
// 4) 绑定数据
imagesItemsControl.ItemsSource = imageModelList;
}
关于 BitmapUtils.ToBitmapSource2:如果它内部使用了
MemoryStream
临时对象,请务必在BitmapImage
完成初始化后Freeze()
,以防打印时跨线程访问异常。
五、打印清晰度与缩放的关键点
-
统一 DPI(推荐 96):WPF 视觉树的度量与渲染以 96 DPI 为基准。若源图为 300 DPI,但尺寸以像素为准,打印时仍以像素为依据,可能出现缩放计算偏差,统一 DPI 可减少不可控因素。
-
像素对齐 :启用
SnapsToDevicePixels
与UseLayoutRounding
,避免边界半像素导致的灰边。 -
高质量缩放 :
RenderOptions.BitmapScalingMode="HighQuality"
能在缩小图片时明显改善清晰度。 -
避免多层缩放 :不要在
Image
外再套ViewBox
,否则缩放叠乘影响清晰度。 -
打印管线 :如对分页/边距有严格控制,考虑使用
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、横竖版自动切换等)。