如何在 WPF 中向流文档添加页脚

本文关键字:文档 添加 WPF | 更新日期: 2023-09-27 18:17:04

我的应用程序中有一个FlowDocument,如下所示:

   <FlowDocumentScrollViewer Grid.Row="1">
        <FlowDocument>
              <Section>
                    <Paragraph>Header</Paragraph>
               </Section>
               <Section>
                  <Paragraph >
                        Footer
                   </Paragraph>
               </Section>
        </FlowDocument>
   </FlowDocumentScrollViewer>

我想要的是将第一个<Section>设置为保留在文档顶部作为页眉,将第二个设置为保留在文档底部作为页脚。

谁能告诉我最好的方法?

如何在 WPF 中向流文档添加页脚

起点是一篇博客文章,展示了如何包装DocumentPaginator并向页面添加装饰。限制是它旨在直接与打印 API 一起使用。它不能直接与查看器一起使用,因为他们要求他们的文档是FlowDocument的,并且不可能覆盖子类中的DocumentPaginator

我为FlowDocumentPageViewer找到的解决方案有几个部分。我没有尝试将其改编为FlowDocumentScrollViewer,但使用相同的原理是可能的。

自定义 XAML 模板

我首先使用 Blend 提取 FlowDocumentPageViewer 的默认 XAML 模板。无论如何,我实际上想删除一些位,但是页眉/页脚的关键更改是(作为统一差异(

-    <Style TargetType="{x:Type FlowDocumentPageViewer}">
+    <Style TargetType="{x:Type local:CustomFlowDocumentPageViewer}">
                         <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" Uid="Border_47" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
                             <AdornerDecorator ClipToBounds="True" Uid="AdornerDecorator_1">
-                                <DocumentPageView DocumentPaginator="{x:Null}" KeyboardNavigation.IsTabStop="False" DocumentViewerBase.IsMasterPage="True" PageNumber="0" KeyboardNavigation.TabIndex="1" KeyboardNavigation.TabNavigation="Local" Uid="DocumentPageView_1"/>
+                                <DocumentPageView DocumentPaginator="{x:Null}" KeyboardNavigation.IsTabStop="False" DocumentViewerBase.IsMasterPage="True" PageNumber="0" KeyboardNavigation.TabIndex="1" KeyboardNavigation.TabNavigation="Local" Uid="DocumentPageView_1" x:Name="DocumentPageView_1"/>
                             </AdornerDecorator>
                         </Border>

对查看器进行子类化

由于您应该能够从 XAML 差异中找出问题,因此我的子类称为 CustomFlowDocumentPageViewer 。由于FlowDocumentPageViewer要求其DocumentFlowDocument,因此尝试更改文档是没有意义的。但我能做的是拦截它并将其DocumentPaginator包装在一个自定义的中,我将其注入DocumentPageView. XAML 差异的后半部分是使我能够找到它。

        private DocumentPageView _PageView;
        public DocumentPaginator CustomDocumentPaginator { get; private set; }
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _PageView = (DocumentPageView)GetTemplateChild("DocumentPageView_1");
            if (_PageView != null) _PageView.DocumentPaginator = CustomDocumentPaginator;
        }
        protected override void OnDocumentChanged()
        {
            base.OnDocumentChanged();
            var doc = Document as FlowDocument;
            CustomDocumentPaginator = doc == null ? null : new DocumentPaginatorWrapper((doc as IDocumentPaginatorSource).DocumentPaginator, new Thickness { Bottom = 32 /* 0.333in */ });
            if (_PageView != null) _PageView.DocumentPaginator = CustomDocumentPaginator;
        }

当然,只更改DocumentPageView.DocumentPaginator有一个大问题:如果您调用myCustomFlowDocumentPageViewer.Print(),那么它将打印原始文档,没有页眉和页脚。解决方案简单但很长(大约 100 行(;如果您使用referencesource.microsoft.com您应该能够弄清楚如何覆盖OnPrintCommand以将CustomDocumentPaginator发送到XpsDocumentWriter而不是((IDocumentPaginatorSource)document).DocumentPaginator

换行文档分页器

我说起点是一篇博客文章,展示了如何包装DocumentPaginator。有一些微妙之处没有提到,可能是因为它打算打印一次文档,所以作者没有遇到它们。

  1. DocumentPage是一次性的。当您从包装的分页器获取页面时,您有责任处理它。如果不这样做,您可能会发现,当您来回导航时,将包装页面的Visual添加到ContainerVisual时会随机ArgumentException: Specified Visual is already a child of another Visual or the root of a CompositionTarget
  2. 获得该ArgumentException的另一种方法是,当您已经有页面的包装副本时,尝试包装页面。 例如,如果您打印,可能会发生这种情况,因为XpsDocumentWriter将请求当前正在呈现到屏幕上的页面的副本。

因此,您需要仔细管理页面的生命周期。我的分页器包装器的修改版本是

    class DocumentPaginatorWrapper : DocumentPaginator
    {
        private Thickness _Margin;
        private DocumentPaginator _Paginator;
        // Required to avoid "ArgumentException: Specified Visual is already a child of another Visual or the root of a CompositionTarget" when printing
        private IDictionary<int, DocumentPage> _PageCache = new Dictionary<int, DocumentPage>();
        public DocumentPaginatorWrapper(DocumentPaginator paginator, Thickness margin)
        {
            _Margin = margin;
            _Paginator = paginator;
            this.PageSize = paginator.PageSize;
            // Events
            paginator.ComputePageCountCompleted += (s, ev) => this.OnComputePageCountCompleted(ev);
            paginator.GetPageCompleted += (s, ev) => this.OnGetPageCompleted(ev);
            paginator.PagesChanged += (s, ev) => this.OnPagesChanged(ev);
        }
        public override DocumentPage GetPage(int pageNumber)
        {
            DocumentPage cachedPage;
            if (_PageCache.TryGetValue(pageNumber, out cachedPage) && cachedPage.Visual != null) return cachedPage;
            DocumentPage page = _Paginator.GetPage(pageNumber);
            // Create a wrapper visual for transformation and add extras
            ContainerVisual newpage = new ContainerVisual();
            // TODO Transform the wrapped page, add your headers and footers.
            // This is highly idiosyncratic.
            ...
            // NB We assume that page.BleedBox has X=0, Y=0, Size=page.PageSize
            cachedPage = new DocumentPageWrapper(page, newpage, PageSize, new Rect(PageSize), new Rect(page.ContentBox.X, page.ContentBox.Y, page.ContentBox.Width + _Margin.Left + _Margin.Right, page.ContentBox.Height + _Margin.Top + _Margin.Bottom));
            _PageCache[pageNumber] = cachedPage;
            return cachedPage;
        }
        public override bool IsPageCountValid { get { return _Paginator.IsPageCountValid; } }
        public override int PageCount { get { return _Paginator.PageCount; } }
        public override Size PageSize
        {
            get { var value = _Paginator.PageSize; return new Size(value.Width + _Margin.Left + _Margin.Right, value.Height + _Margin.Top + _Margin.Bottom); }
            set { _Paginator.PageSize = new Size(value.Width - _Margin.Left - _Margin.Right, value.Height - _Margin.Top - _Margin.Bottom); }
        }
        public override IDocumentPaginatorSource Source { get { return _Paginator.Source; } }
    }
    /// <summary>
    /// It's necessary to dispose the page returned by the wrapped DocumentPaginator when the page we return is disposed,
    /// because otherwise we inconsistently get ArgumentExceptions due to the page.Visual still being in use.
    /// </summary>
    class DocumentPageWrapper : DocumentPage
    {
        internal DocumentPageWrapper(DocumentPage originalPage, Visual visual, Size pageSize, Rect bleedBox, Rect contentBox)
            : base(visual, pageSize, bleedBox, contentBox)
        {
            _OriginalPage = originalPage;
        }
        private DocumentPage _OriginalPage;
        public override void Dispose()
        {
            base.Dispose();
            if (_OriginalPage != null)
            {
                _OriginalPage.Dispose();
                _OriginalPage = null;
            }
        }
    }

你可以试试这个..希望这有帮助。

<Section Name="MainBody" SectionExtensions.ResetPageNumbers="True">
    <!-- Add the document title to the header -->
    <SectionExtensions.HeaderTemplate>
        <DataTemplate>
            <Border BorderBrush="Black" BorderThickness="0,0,0,1">
                <TextBlock Text="Document title" FontSize="16" />
            </Border>
        </DataTemplate>
    </SectionExtensions.HeaderTemplate>

    <!-- Add the page count to the footer -->
    <SectionExtensions.FooterTemplate>
        <DataTemplate>
            <Border BorderBrush="Black" BorderThickness="0,1,0,0">
                <TextBlock Text="{Binding}" FontSize="16" />
            </Border>
        </DataTemplate>
    </SectionExtensions.FooterTemplate>
    <Paragraph>
    ...
    </Paragraph>
</Section>