Click here to Skip to main content
Click here to Skip to main content

FlowDocument pagination with repeating page headers

By , 17 Dec 2008
 

Introduction

Flow documents are designed to optimize viewing and readability. Rather than being set to one predefined layout, flow documents dynamically adjust and reflow their content based on run-time variables such as window size, device resolution, and optional user preferences. In addition, flow documents offer advanced document features, such as pagination and columns.

Unfortunately, three key pieces of functionality are missing from the FlowDocument pagination functionality:

  • Page headers
  • Page footers
  • Repeating table headers

This article describes how you can provide this functionality using a custom DocumentPaginator.

Background

Document pagination is provided by the DocumentPaginator class. This class can be subclassed to override to provide custom behaviour. But, document pagination is a pain in the behind. I didn't want to write all the behaviour from scratch.

The default Document Paginator used by a FlowDocument is not defined in the System namespace, and can't be subclassed. But, we can create a DocumentPaginator subclass which takes a DocumentPaginator as input and uses it for basic pagination. This approach has been described by Feng Yuan in his blog.

Headers and footers

The sizes for page headers and footers usually are fixed. Therefore, adding them can be accomplished by subtracting the space needed for headers and footers from the available content area and translating the content area so it does not overlap with the header area. The following source code does exactly this:

public class PimpedPaginator : DocumentPaginator {

    public override DocumentPage GetPage(int pageNumber) {
        // Use default paginator to handle pagination
        Visual originalPage = paginator.GetPage(pageNumber).Visual;

        ContainerVisual visual = new ContainerVisual();
        ContainerVisual pageVisual = new ContainerVisual() {
            Transform = new TranslateTransform(
                definition.ContentOrigin.X, 
                definition.ContentOrigin.Y
            )
        };
        pageVisual.Children.Add(originalPage);
        visual.Children.Add(pageVisual);

        // Create headers and footers
        if(definition.Header != null) {
            visual.Children.Add(CreateHeaderFooterVisual(definition.Header, 
                                definition.HeaderRect, pageNumber));
        }
        if(definition.Footer != null) {
            visual.Children.Add(CreateHeaderFooterVisual(definition.Footer, 
                                definition.FooterRect, pageNumber));
        }

        // Check for repeating table headers
        // (...will be described later in this article)

        return new DocumentPage(
            visual, 
            definition.PageSize, 
            new Rect(new Point(), definition.PageSize),
            new Rect(definition.ContentOrigin, definition.ContentSize)
        );
    }
}

Et voila, headers and footers can be added. This was the easy part.

Repeating table headers

Automatically adding repeating table headers is a bit more complicated. Basically, there are four separate problems that need to be solved:

  • Finding the tables in the document
  • Determining if a table spans multiple pages
  • Finding the table header
  • Inserting the table header if needed

The first three problems can be addressed in a similar manner. The structure of WPF elements is contained in two different trees: the Logical tree and the Visual tree. The logical tree contains the basic structure of a WPF element, and closely resembles the XAML used to describe the element. This did seem like the most logical place to look. Alas, the logical tree of the DocumentPage produced by the default paginator turned out to be of little use, so I decided to inspect the Visual tree. It's huge, but revealing:

-- BEGIN PAGE 0 -------
PageVisual (0)
  ContainerVisual (1)
    ContainerVisual (2)
      SectionVisual (3)
        ContainerVisual (4)
          ParagraphVisual (5)
            ParagraphVisual (6)
              LineVisual (7)
          ParagraphVisual (5)
            ParagraphVisual (6)
              LineVisual (7)
          ParagraphVisual (5)
            ParagraphVisual (6)
              LineVisual (7)
          ParagraphVisual (5)
            ParagraphVisual (6)
              ParagraphVisual (7)
                ParagraphVisual (8)
                  LineVisual (9)
            ParagraphVisual (6)
              ParagraphVisual (7)
                ParagraphVisual (8)
                  LineVisual (9)
          ParagraphVisual (5)
            ParagraphVisual (6)
              LineVisual (7)
          ParagraphVisual (5)
            RowVisual (6)
              ParagraphVisual (7)
                ContainerVisual (8)
                  SectionVisual (9)
                    ContainerVisual (10)
                      ParagraphVisual (11)
                        ParagraphVisual (12)
                          LineVisual (13)
                ContainerVisual (8)
              ParagraphVisual (7)
                ContainerVisual (8)
                  SectionVisual (9)
                    ContainerVisual (10)
                      ParagraphVisual (11)
                        ParagraphVisual (12)
                          LineVisual (13)
                ContainerVisual (8)
              ParagraphVisual (7)
                ContainerVisual (8)
                  SectionVisual (9)
                    ContainerVisual (10)
                      ParagraphVisual (11)
                        ParagraphVisual (12)
                          LineVisual (13)
                ContainerVisual (8)
              ParagraphVisual (7)
                ContainerVisual (8)
                  SectionVisual (9)
                    ContainerVisual (10)
                      ParagraphVisual (11)
                        ParagraphVisual (12)
                          LineVisual (13)
                ContainerVisual (8)
            RowVisual (6)
(A lot of entries have been omitted for brevity...)
            RowVisual (6)
              ParagraphVisual (7)
                ContainerVisual (8)
                  SectionVisual (9)
                    ContainerVisual (10)
                      ParagraphVisual (11)
                        ParagraphVisual (12)
                          LineVisual (13)
                ContainerVisual (8)
              ParagraphVisual (7)
                ContainerVisual (8)
                  SectionVisual (9)
                    ContainerVisual (10)
                      ParagraphVisual (11)
                        ParagraphVisual (12)
                          LineVisual (13)
                ContainerVisual (8)
              ParagraphVisual (7)
                ContainerVisual (8)
                  SectionVisual (9)
                    ContainerVisual (10)
                      ParagraphVisual (11)
                        ParagraphVisual (12)
                          LineVisual (13)
                ContainerVisual (8)
              ParagraphVisual (7)
                ContainerVisual (8)
                  SectionVisual (9)
                    ContainerVisual (10)
                      ParagraphVisual (11)
                        ParagraphVisual (12)
                          LineVisual (13)
                ContainerVisual (8)
    ContainerVisual (2)
-- END PAGE 0 -------

-- BEGIN PAGE 1 -------
PageVisual (0)
  ContainerVisual (1)
    ContainerVisual (2)
      SectionVisual (3)
        ContainerVisual (4)
          ParagraphVisual (5)
            RowVisual (6)
              ParagraphVisual (7)
                ContainerVisual (8)
                  SectionVisual (9)
                    ContainerVisual (10)
                      ParagraphVisual (11)
                        ParagraphVisual (12)
                          LineVisual (13)
                ContainerVisual (8)
              ParagraphVisual (7)
                ContainerVisual (8)
                  SectionVisual (9)
                    ContainerVisual (10)
(...and so on)

The RowVisual elements are the prime suspects to be a row in a table. Every page with a table contains a bunch of them contained in a ContainerVisual. Since the number of children matches the number of columns in this particular document, this is probably the element we are looking for.

By walking the visual tree, we can now find the answers to our questions. If the last element in a page is a RowVisual, there is a good chance that this table will continue on the next page, so we need to save the header of that table for future use. We can find this header by looking for the first RowVisual in the containing ContainerVisual. Conversely, if a page starts with a RowVisual, this probably is the continuation of a table on the previous page, so we should repeat the table header stored earlier. These methods search for the table rows:

public class PimpedPaginator : DocumentPaginator {

    /// <summary>
    /// Checks if the page ends with a table.
    /// </summary>
    /// <remarks>
    /// There is no such thing as a 'TableVisual'. There is a RowVisual, which
    /// is contained in a ParagraphVisual if it's part of a table. For our
    /// purposes, we'll consider this the table Visual
    /// 
    /// You'd think that if the last element on the page was a table row, 
    /// this would also be the last element in the visual tree, but this is not true
    /// The page ends with a ContainerVisual which is aparrently  empty.
    /// Therefore, this method will only check the last child of an element
    /// unless this is a ContainerVisual
    /// </remarks>
    /// <param name="originalPage"></param>
    /// <returns></returns>
    private bool PageEndsWithTable(DependencyObject element, 
                 out ContainerVisual tableVisual, out ContainerVisual headerVisual) {
        tableVisual = null;
        headerVisual = null;
        if(element.GetType().Name == "RowVisual") {
            tableVisual = (ContainerVisual)VisualTreeHelper.GetParent(element);
            headerVisual = (ContainerVisual)VisualTreeHelper.GetChild(tableVisual, 0);
            return true;
        }
        int children = VisualTreeHelper.GetChildrenCount(element);
        if(element.GetType() == typeof(ContainerVisual)) {
            for(int c = children - 1; c >= 0; c--) {
                DependencyObject child = VisualTreeHelper.GetChild(element, c);
                if(PageEndsWithTable(child, out tableVisual, out headerVisual)) {
                    return true;
                }
            }
        } else if(children > 0) {
            DependencyObject child = VisualTreeHelper.GetChild(element, children - 1);
            if(PageEndsWithTable(child, out tableVisual, out headerVisual)) {
                return true;
            }
        }
        return false;
    }


    /// <summary>
    /// Checks if the page starts with a table which presumably has wrapped
    /// from the previous page.
    /// </summary>
    /// <param name="element"></param>
    /// <param name="tableVisual"></param>
    /// <param name="headerVisual"></param>
    /// <returns></returns>
    private bool PageStartsWithTable(DependencyObject element, 
                                     out ContainerVisual tableVisual) {
        tableVisual = null;
        if(element.GetType().Name == "RowVisual") {
            tableVisual = (ContainerVisual)VisualTreeHelper.GetParent(element);
            return true;
        }
        if(VisualTreeHelper.GetChildrenCount(element)> 0) {
            DependencyObject child = VisualTreeHelper.GetChild(element, 0);
            if(PageStartsWithTable(child, out tableVisual)) {
                return true;
            }
        }
        return false;
    }
}

If both cases are true (i.e., the page starts with a table row and ends with a table row) and the ContainerVisuals of both tables are the same, the table spans the entire page. In this case, we do not need to save the first row as the table header.

This approach works pretty well, but has a drawback: it is not possible to detect the case where one table ends on a page and the next page starts with a new table. Fortunately, this situation is pretty rare, and can be completely prevented by adding a paragraph as a table title before the table (naming your tables is a good idea anyway).

This leaves us with one problem to solve: inserting the table header in the generated page. This is going to take some space. The best solution would be to bump the content on the bottom of the page to the next page. After all, that's what a FlowDocument was designed for. Unfortunately, the contents of a page are generated by the default FlowDocument paginator. Bumping content to the next page would involve rewriting the entire DocumentPaginator, which I wanted to avoid altogether.

We'll have to make some room in the existing page. Since the table header usually is much smaller than the rest of the content (typically about 50 times as small), the easiest solution is to vertically scale down the page content a bit to create some headroom and add the table header to the top of the page. If your table headers aren't huge, the resulting distortion is not noticeable. This is the code to insert the table header:

// Check for repeating table headers
if(definition.RepeatTableHeaders) {
    // Find table header
    ContainerVisual table;
    if(PageStartsWithTable(originalPage, out table) && currentHeader != null) {
        // The page starts with a table and a table header was
        // found on the previous page. Presumably this table 
        // was started on the previous page, so we'll repeat the
        // table header.
        Rect headerBounds = VisualTreeHelper.GetDescendantBounds(currentHeader);
        Vector offset = VisualTreeHelper.GetOffset(currentHeader);
        ContainerVisual tableHeaderVisual = new ContainerVisual();
        
        // Translate the header to be at the top of the page
        // instead of its previous position
        tableHeaderVisual.Transform = new TranslateTransform(
            definition.ContentOrigin.X,
            definition.ContentOrigin.Y - headerBounds.Top
        );

        // Since we've placed the repeated table header on top of the
        // content area, we'll need to scale down the rest of the content
        // to accomodate this. Since the table header is relatively small,
        // this probably is barely noticeable.
        double yScale = (definition.ContentSize.Height - headerBounds.Height) / 
                         definition.ContentSize.Height;
        TransformGroup group = new TransformGroup();
        group.Children.Add(new ScaleTransform(1.0, yScale));
        group.Children.Add(new TranslateTransform(
            definition.ContentOrigin.X,
            definition.ContentOrigin.Y + headerBounds.Height
        ));
        pageVisual.Transform = group;

        ContainerVisual cp = VisualTreeHelper.GetParent(currentHeader) as ContainerVisual;
        if(cp != null) {
            cp.Children.Remove(currentHeader);
        }
        tableHeaderVisual.Children.Add(currentHeader);
        visual.Children.Add(tableHeaderVisual);
    }

    // Check if there is a table on the bottom of the page.
    // If it's there, its header should be repeated
    ContainerVisual newTable, newHeader;
    if(PageEndsWithTable(originalPage, out newTable, out newHeader)) {
        if(newTable == table) {
            // Still the same table so don't change the repeating header
        } else {
            // We've found a new table. Repeat the header on the next page
            currentHeader = newHeader;
        }
    } else {
        // There was no table at the end of the page
        currentHeader = null;
    }
}

The solution works pretty well, except for one glitch. See if you can spot it:

Example.png

Conclusion

The entire class is included with this article, I hope you can find use for it. Currently, there is one known issue with the implementation: the header background colors are not properly printed when repeated on a new page.

History

  • 17 December 2008: First version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Bram Fokke
Software Developer Tetra BV
Netherlands Netherlands
Member
When I was a kid I started programming in GW Basic. Nowadays I prefer curly braces and mainly develop in C#. I spend my working days developing medical software for Tetra BV in Utrecht, the Netherlands.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionWorking examplemembernawhcs3 Mar '13 - 13:10 
Dear Bram Fokke,
 
Can you show us a completely working example? I mean a xaml file and also a runnable cs code.
 
Thanks
SuggestionFlow Document pagination repeated page headers not worked for multiple rowsmemberMember 92033489 Jul '12 - 3:03 
Flow Document pagination repeated page headers not worked for multiple rows
for example if i am having 2 rows in table header means this take only first row to next page
Questioncan u just send me the sample which this file has been usedmembersharathkumar1116 May '12 - 0:02 
hi,
can u please update me the sample project ... using this file...
 
i am unable to use this .cs file in my project..and i dont know how to use DocumentPaginator.... please help me... i need to implement header and footer ...
QuestionHow to set a table as header?memberAABB85A20 Dec '11 - 4:49 
Hey,
 
how do I set/define a Table as Header? Or a header at all?
 
I'm new to C#, so sometimes it's a bit difficult to understand the whole thing.
 
Thanks!
GeneralMy vote of 3memberhsmcc7 Nov '11 - 14:50 
need running sample
GeneralTranslating into VB.NETmemberHenrique C6 Jun '11 - 3:00 
Hi,
 
I'm having problems while trying to translate it for VB.NET:
 
originalPage.DumpVisualTree(Console.Out)
 
'DumpVisualTree' is not a member of 'System.Windows.Media.Visual'.
 

Friend ReadOnly Property ContentSize() As Size
   Get
      Return PageSize.Subtract(New Size(Margins.Left + Margins.Right, Margins.Top + 
             Margins.Bottom + HeaderHeight + FooterHeight))
   End Get
End Property
 
'Subtract' is not a member of 'System.Windows.Size'.
 
Could anyone help me ?
GeneralRe: Translating into VB.NETmemberMember 79809546 Jun '11 - 22:17 
Hi,
 
You can remove this line:
originalPage.DumpVisualTree(Console.Out)
 

create the following extension method:
public static Size Subtract(this Size s1, Size s2) {
return new Size(s1.Width - s2.Width, s1.Height - s2.Height);
}
GeneralRe: Translating into VB.NETmemberAABB85A20 Dec '11 - 4:09 
How do I create this extension method exactly? And where?
GeneralPossible for having running sample,memberHema Bairavan5 Jun '11 - 21:35 
Hi Dear Bram fokke,
 
This was a nice article and helps most of peoples who are looking in to the wpf reporting side.
Mean while i tried with the code you have given and finding challenge in executing the application.
 
Is is possible for us to have full runnins sample application so that it will be more helpfull for us? if so please attach the same.
 
THanks
Generaladd content in header & footermemberPooja290125 May '11 - 20:21 
how to add content in header & footer
my code only displaying rectange
GeneralPrinting UI ContainersmemberJohnDev24 Sep '10 - 9:34 
First of all, thank you for such a useful piece of code. It really helped with the tables I was printing.
 
I needed to include some rich text boxes in my printout so I placed them in a BlockUIContainer and added them to my flow document. As you probably already know, BlockUIContainers and InlineUIContainers are not printed.
This is because the code in the PimpedPaginator's constructor creates a copy of the flow document using the TextRange Save and Load methods which ignore UI Containers.
If you change the DataFormats parameter from Xaml to XamlPackage then in theory flow document images will get copied (I have not tested this) however I have confirmed that UI Containers are still ignored.
 
The fix is to change over to the XamlWriter to copy the flow document.
If you replace the MemoryStream, TextRange Save/Load code in the constructor with the following single line of code then UI Containers will be copied and will appear in the printout as expected.
 
FlowDocument copy = XamlReader.Parse(XamlWriter.Save(document)) as FlowDocument;
 
I hope this information is helpful.
GeneralUnable to Repeat Table HeadersmemberMember 391315121 May '10 - 3:17 
HI!
I have a flowdocument consisting of multiple tables (no text). Each table may have many rows so it may cover more than one page.
But i am unable to repeat the headers of each table correctly..
For each table, I have a rowgroup with a row with the headers and another rowgroup with the table data.
 
I set the name of the first rowgroup RowgroupHeaders.Name = "RowVisual" so it can be detected in
"PageStartsWithHeader" function (if (element.GetType().Name == "RowVisual")).
The headers are printed in every page of the documents but unfortunately headers for the second and third table are not printed correctly. Instead of printing their header, it is printed the header of the first table.
(this happens because the following line
if (newTable == table)
is always true!)
 
Do you have any ideas?
 
Thank you in advance
k
GeneralCopy enhancementmemberLudo GO17 May '10 - 22:57 
Hello,
 
I had some problems using this marvelous paginator.
This was due to a customized Table within my FlowDocument. Xml parser didn't find MyTable markup in presentation XML namespace.
 
I had to change the copy part code from Paginator's constructor.
Replace the copy code by this :
 

MemoryStream stream = new MemoryStream();
 
XamlWriter.Save(cpy, stream);
//go to the beginning of stream
stream.Flush();
stream.Seek(0, SeekOrigin.Begin);
 
copy = XamlReader.Load(stream) as FlowDocument;

 
Like this costomized namespace and controls are included.
However you have to be carefull since Binding elements will be serialized except if custome dependancy properties have this attribut : [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
 
regards !
QuestionUnable to repeat TableHeadersmemberMember 30759384 Feb '10 - 22:43 
Hi,
 
I'm making test with your project to repeat table headers on a FlowDocument that only contains 2 large tables (so they must be printed on 3 pages).

The fact is that When I print this Doc, no headers are repeated (propoertie repeatTableHeader is set to true in my class) when using PrintDocument() Method from PrintDialog Object.
 
When I try to use PrintVisual() Method from the same object, I can get the header on the second page, but it's removed from the first page, as the code suggest :
 
***********************************************************************************************************
 
ContainerVisual cp = VisualTreeHelper.GetParent(currentHeader) as ContainerVisual;
if(cp != null) {
cp.Children.Remove(currentHeader);
}
tableHeaderVisual.Children.Add(currentHeader);
 
***********************************************************************************************************
 

Is this a normal behavior or Am i missing something in code that make my header not being duplicated ?...
 
Let me know if you want me to post some of my code to go in further explanations .
 
Thanks in Advance,
 
Vincent
GeneralI am unable to call the "DrawHeaderFooter"memberK Mehar18 Sep '09 - 1:09 
Hi,
Can you please give me an example for how to call 'DrawHeaderFooter" Delegate
 
Thanks in Advance
 
Mehar
AnswerRe: I am unable to call the "DrawHeaderFooter"memberbarcrab18 Sep '09 - 18:30 
Try this.
 
private void Button_Click(object sender, RoutedEventArgs e)
{
   PrintDialog printDialog = new PrintDialog();
   if (printDialog.ShowDialog() == true)
   {
     FlowDocument doc = new FlowDocument();
     doc.Blocks.Add(new Paragraph(new Run("Testing Header")));
 
     PimpedPaginator.Definition def = new PimpedPaginator.Definition();
     def.HeaderHeight = 100.0000;
     def.Header = new PimpedPaginator.DrawHeaderFooter(DoHeaderStuff);
     
     PimpedPaginator pimpedPaginator = new PimpedPaginator(doc, def);
 
     printDialog.PrintDocument(pimpedPaginator , "Flow Document Header Test");
   }
}
public void DoHeaderStuff(DrawingContext context, Rect bounds, int pageNr)
{
  context.DrawRectangle(System.Windows.Media.Brushes.LightBlue, (System.Windows.Media.Pen)null, bounds);
}
 
Also note that the Headers/Footers won't appear in the document reader. They only appear when printed out.
 
Anyway I hope this helps.
QuestionFirst Page Header and Repeating Table HeadersmemberCaipus19 Aug '09 - 22:29 
Hi Bram,
 
i like your PimpedPaginator.
 
In my case, the first page's header should have a larger size, on the following pages it should have a smaller header and the content size should be sized vice versa.
 
Do you have a code snippet for this issue?
 
Also i like the repeating tbale headers, but in my app i have 3 tables and only the second one should repeat it's header. Do you have a tip for me?
 
Have a nice day
Markus
GeneralA question and can you update the code please? =)membermcvf25 Jul '09 - 20:33 
Hi Bram: Have it almost working but am puzzled by compile error at PageSize.Subtract (I can't quite follow your earlier response to a message regarding this). Also, I'm thinking you want us to instantiate the paginator like so ....
 
mypag = new PimpedPaginator(myFlowdoc, d);
 
But what do I pass in as a Definition obj?
 
Many many thanks for your efforts in a fine article!
 
-Mick
GeneralRe: A question and can you update the code please? =)memberdafokka25 Jul '09 - 23:04 
Subtract() is an extension method, which subtracts two variables of the class Size:
 
public static Size Subtract(this Size s1, Size s2) {
    return new Size(s1.Width - s2.Width, s1.Height - s2.Height);
}
 
The PageDefinition object can be found in the source code. It contains stuff like page size and delegates to draw a header and a footer.
QuestionWorking examplememberMember 44356638 Apr '09 - 7:50 
Hi can you please provide a working example for the code posted. Or if you can post an implementaion of "DrawHeaderFooter" delegate.
 
Actually my header itself is a flow docuemnt.
 
Will appreciate any response.
 
Thanks in advance.
 
Mohit
GeneralHi.memberascotravel8 Mar '09 - 22:27 
Hi ,
Can you post a code example hoe to use it?
Thanks.
Have fun
GeneralRe: Hi.memberMember 79809546 Jun '11 - 22:13 
I had the same problem with image. I solved it by removing the part of code that create copy of the FlowDocument. I'm working with original FlowDoc without copying and it's working for me.
 
It's my changed ctor:
public PimpedPaginator(FlowDocument document, Definition def) {
this.paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
this.definition = def;
paginator.PageSize = def.ContentSize;
 
// Change page size of the document to
// the size of the content area
document.ColumnWidth = double.MaxValue; // Prevent columns
document.PageWidth = definition.ContentSize.Width;
document.PageHeight = definition.ContentSize.Height;
document.PagePadding = new Thickness(0);
}
 
Hope this help.
GeneralImages: DocumentPage class vs MS.Internal.PtsHost.FlowDocumentPagemembernpcomplete121 Jan '09 - 9:19 
My FlowDocument contains a paragraph that contains an InlineUIContainer that contains an Image
In this article the overridden method GetPage gets the original MS.Internal.PtsHost.FlowDocumentPage
and creates a new DocumentPage utilizing the Visual property from the old page.
For some reasons the image disappears if I use this paginator. Any idea why?
If the original paginator is used the image remains.
It disappears when I crate the XPS document.
Thanks
GeneralMethod Not FoundmemberMISCC17 Dec '08 - 16:50 
I can't find the method definition for originalPage.DumpVisualTree() and PageSize.Subtract(), then the class can not be compile.
GeneralRe: Method Not FoundmemberBram Fokke17 Dec '08 - 23:31 
My bad, I thought I had removed all references to all helper methods.
 
PageSize.Subtract is pretty straightforward: it subtracts two sizes:
 
public static Size Subtract(this Size s1, Size s2) {
    return new Size(s1.Width - s2.Width, s1.Height - s2.Height);
}
 
You can safely remove the part where I dump the visual tree - this is for educational purposes only.
 
Thanks for the head-up, I will modify the article accordingly.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 17 Dec 2008
Article Copyright 2008 by Bram Fokke
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid