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,使其在视觉上位于中间。

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 嵌套。

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

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

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

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

说明:

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

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

  3. 根据数量选择 ItemsPanel;

  4. 绑定到 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(),以防打印时跨线程访问异常。


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

  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、横竖版自动切换等)。

相关推荐
hqwest2 小时前
C#WPF实战出真汁08--【消费开单】--餐桌面板展示
c#·wpf·ui设计·wpf界面设计
★YUI★18 小时前
学习游戏制作记录(玩家掉落系统,删除物品功能和独特物品)8.17
java·学习·游戏·unity·c#
谷宇.18 小时前
【Unity3D实例-功能-拔枪】角色拔枪(二)分割上身和下身
游戏·unity·c#·游戏程序·unity3d·游戏开发·游戏编程
LZQqqqqo19 小时前
C# 中 ArrayList动态数组、List<T>列表与 Dictionary<T Key, T Value>字典的深度对比
windows·c#·list
Dm_dotnet21 小时前
Stylet启动机制详解:从Bootstrap到View显示
c#
三千道应用题1 天前
WPF&C#超市管理系统(6)订单详情、顾客注册、商品销售排行查询和库存提示、LiveChat报表
开发语言·c#·wpf
唐青枫1 天前
别滥用 Task.Run:C# 异步并发实操指南
c#·.net
我好喜欢你~1 天前
C#---StopWatch类
开发语言·c#
一阵没来由的风2 天前
拒绝造轮子(C#篇)ZLG CAN卡驱动封装应用
c#·can·封装·zlg·基础封装·轮子