How to have two text columns?

Aug 21, 2012 at 5:32 PM

Is there a way to have a paragraph property or document property that has two text columns instead of one? And if not, can I manually do it somehow (like with OpenXML or something)

Thanks

Aug 14, 2014 at 2:16 PM
Has there been any movement on this question? I, too, need to be able to do two column format.
Aug 15, 2014 at 11:01 AM
There's no reference to sections columns, as far as I can see.
Each section <w:sectPr> has a <w:cols> tag, if you set Word to split the document/section into columns, the number of columns is added to that tag using w:num="x":
    <w:sectPr w:rsidR="00AB7916" w:rsidRPr="00D820F2" w:rsidSect="004729AB">
      <w:pgSz w:w="11909" w:h="16834" w:code="9"/>
      <w:pgMar w:top="1699" w:right="1138" w:bottom="1699" w:left="1138" w:header="0" w:footer="0" w:gutter="0"/>
      <w:cols w:num="2" w:space="708"/>
      <w:docGrid w:linePitch="360"/>
    </w:sectPr>
To add the option of changing the columns of a section to DocX, I made the following changes:

The Container.cs method to get the Sections threw an error at the end, I believe that's caused by the fact that it's looking for the body-tag while already "inside" the body-tag. It should just be looking for the sectPr right away, so I changed the commented lines into the next line :
        public virtual List<Section> Sections
        {
            get
            {
             //...
                //XElement body = Xml.Element(XName.Get("body", DocX.w.NamespaceName));
                //XElement baseSectionXml = body.Element(XName.Get("sectPr", DocX.w.NamespaceName));
                XElement baseSectionXml = Xml.Element(XName.Get("sectPr", DocX.w.NamespaceName));
             //...              
            }
        }
Then I added the (simple) code to add the number of columns to a section to Section.cs:
    public void SectionColumns(int columns)
    {
            XElement cols = Xml.Element(XName.Get("cols", DocX.w.NamespaceName));
            cols.SetAttributeValue(XName.Get("num", DocX.w.NamespaceName), columns);        
    }
This is the (dummy) code I used to test the new method:
            using (DocX document = DocX.Load(doc))
            { 
                int c  = 2;             
                foreach (Section s in document.Sections)
                {
                    s.SectionColumns(c);
                    c++;
                }   
                document.SaveAs(doc + ".colsect.docx");
            }
So if you're comfortable editing the DocX source code, you could replicate this and you should be able to set the number of columns for a section/the document.
Let me know if/when you've tested this, if it works, I'll submit a patch.
Aug 15, 2014 at 3:21 PM
Annika89,
Thank you very much. The code does make text columns. However, I'm not very proficient in using sections in DocX. Nothing I have tried has worked to give me a header centered in a section with one column followed by a section in two columns. What I get is two columns for the whole document. The code I'm using is below. I would appreciate suggestions on the proper way to insert the section breaks. Thanks for working on this.
    Public p As Object
    Public s As Object
    Public FontSize As Double = 10.0
    Public doc As Object
    Public XMLDocCreated As Boolean = False
    Public DocFileName As String = ""
    Public Sub CreateXMLDoc()
        doc = DocX.Create(DocFileName)
        With doc
            .PageWidth = 550.0
            .PageHeight = 850.0
            .MarginLeft = 50.0
            .MarginRight = 50.0
            FontSize = 10.0
            .MarginTop = 50.0
            .MarginBottom = 50.0
        End With
        XMLDocCreated = True
    End Sub


    Public Sub FillDoc()
        Dim PrintLine As String = ""
        Dim NextLine As String = " " + vbCrLf
        MsgBox("Nbr Sections = " + doc.Sections.Count.ToString)
        s = doc.sections(0)
        s.SectionColumns(1)
        p = doc.InsertParagraph(" ")
        doc.Paragraphs(0).Alignment = Alignment.center
        doc.Paragraphs(0).Append("Header").Bold().Fontsize(FontSize).Font(New FontFamily("Garamond"))
        doc.Paragraphs(0).Append(NextLine)
        p = doc.InsertParagraph(" ")
        doc.insertSection()
        MsgBox("Nbr Sections = " + doc.Sections.Count.ToString)
        s = doc.sections(doc.Sections.Count - 1)
        s.SectionColumns(2)
        p = doc.InsertParagraph(" ")
        PrintLine = "Joe Blow" + vbCrLf + "Address Line 1" + vbCrLf + "City State Zip" + vbCrLf + "(H) 000-000-0000"
        doc.Paragraphs(2).Append(PrintLine)
        doc.Save()
        Process.Start("Winword.exe", DocFileName)

    End Sub
Aug 18, 2014 at 6:49 PM
Annika89,
I have tried numerous combinations of sections and text columns. From what I can tell, it seems to set the columns for all sections based on the last call to s.SectionColumns. It does not seem possible to set different numbers of columns from section to section.
Aug 19, 2014 at 9:51 AM
To test my code, I used an existing Word document and that does give the desired results (at least, for me).
Sections created by DocX aren't the same as a simple section created by Word, however.

Section added using Microsoft Word:
    <w:p w:rsidR="003E6883" w:rsidRDefault="003E6883" w:rsidP="00D820F2">
      <w:pPr>
        <w:sectPr w:rsidR="003E6883" w:rsidSect="004729AB">
          <w:pgSz w:w="11909" w:h="16834" w:code="9" />
          <w:pgMar w:top="1699" w:right="1138" w:bottom="1699" w:left="1138" w:header="0" w:footer="0" w:gutter="0" />
          <w:cols w:space="708" />
          <w:docGrid w:linePitch="360" />
        </w:sectPr>
      </w:pPr>
    </w:p>
Section added using document.InsertSection in DocX:
    <w:p>
      <w:pPr>
        <w:sectPr>
          <w:type w:val="continuous"/>
        </w:sectPr>
      </w:pPr>
    </w:p>
As you can see, the section added by DocX doesn't contain a w:cols tag. So the columns will only be set to the main section of the document, which is probably what causes all sections to have the same number of columns.
It's my bad for not testing the code with a DocX sample of the section, of course.

It helps to change the code to insert a section to include a w:cols tag (change the commented line in Container.cs):
        public virtual void InsertSection(bool trackChanges)
        {
            var newParagraphSection = new XElement
            (
                //XName.Get("p", DocX.w.NamespaceName), new XElement(XName.Get("pPr", DocX.w.NamespaceName), new XElement(XName.Get("sectPr", DocX.w.NamespaceName), new XElement(XName.Get("type", DocX.w.NamespaceName), new XAttribute(DocX.w + "val", "continuous"))))
                XName.Get("p", DocX.w.NamespaceName), new XElement(XName.Get("pPr", DocX.w.NamespaceName), new XElement(XName.Get("sectPr", DocX.w.NamespaceName), new XElement(XName.Get("cols", DocX.w.NamespaceName), new XAttribute(DocX.w + "space", "708")), new XElement(XName.Get("type", DocX.w.NamespaceName), new XAttribute(DocX.w + "val", "continuous"))))
            );

            if (trackChanges)
                newParagraphSection = HelperFunctions.CreateEdit(EditType.ins, DateTime.Now, newParagraphSection);

            Xml.Add(newParagraphSection);
        }
And change the InsertSectionPageBreak as well:
        public virtual void InsertSectionPageBreak(bool trackChanges = false)
        {
            var newParagraphSection = new XElement
            (
                //XName.Get("p", DocX.w.NamespaceName), new XElement(XName.Get("pPr", DocX.w.NamespaceName), new XElement(XName.Get("sectPr", DocX.w.NamespaceName)))
                XName.Get("p", DocX.w.NamespaceName), new XElement(XName.Get("pPr", DocX.w.NamespaceName), new XElement(XName.Get("sectPr", DocX.w.NamespaceName), new XElement(XName.Get("cols", DocX.w.NamespaceName), new XAttribute(DocX.w + "space", "708"))))
            );

            if (trackChanges)
                newParagraphSection = HelperFunctions.CreateEdit(EditType.ins, DateTime.Now, newParagraphSection);

            Xml.Add(newParagraphSection);
        }
Now you can use SectionColumns on sections created by DocX.

However, it seems there's something wrong with the way DocX handles sections in general. This code does create the sections and makes the first part one column and splits the second part into two columns. However, the section created makes a pagebreak while I'm only inserting a section (this happens without the SectionColumns as well).
            using (DocX document = DocX.Create(doc))
            {
                document.InsertParagraph("This is a test. This is a test. This is a test. This is a test. This is a test. This is a test. This is a test. This is a test. This is a test. This is a test.");
                document.InsertSection();
                document.InsertParagraph("This is another test. This is another test. This is another test. This is another test. This is another test. This is another test. This is another test. This is another test. This is another test.");
                
                Section sBase = document.Sections[0];
                sBase.SectionColumns(1);
                Section s = document.Sections[1];
                s.SectionColumns(2);
                
                document.Save();
            }
I believe that's because the section gets a "continuous" value, which indeed makes a section continuous rather than creating a pagebreak, however, I think the value should be added to the next section rather than the one that's being added. It's a bit complicated, both in Word and DocX...
Aug 19, 2014 at 3:12 PM
Thank you for the updated version. I'm afraid that DocX will not be suitable for what I have to do so I'll have to automate Word. I can do it that way but would prefer not to since what I need to do is not really supported using late binding. DocX is losing my page layout when I add sections and then when the last section is added, it regains the margins, etc. and applies the columns to the first and last sections. The middle sections have the right columns but have lost the page layout.
Aug 20, 2014 at 8:36 AM
The reason for losing the page layout is that the section that's being added doesn't have any pagesize/margin attributes. That part can be fixed by expanding the two methods above:
        public virtual void InsertSection(bool trackChanges)
        {
            var newParagraphSection = new XElement
            (
                //XName.Get("p", DocX.w.NamespaceName), new XElement(XName.Get("pPr", DocX.w.NamespaceName), new XElement(XName.Get("sectPr", DocX.w.NamespaceName), new XElement(XName.Get("type", DocX.w.NamespaceName), new XAttribute(DocX.w + "val", "continuous"))))
                XName.Get("p", DocX.w.NamespaceName), 
                    new XElement(XName.Get("pPr", DocX.w.NamespaceName), 
                             new XElement(XName.Get("sectPr", DocX.w.NamespaceName), 
                                          new XElement(XName.Get("type", DocX.w.NamespaceName), new XAttribute(DocX.w + "val", "continuous")), 
                                          new XElement(XName.Get("cols", DocX.w.NamespaceName), new XAttribute(DocX.w + "space", "708")), 
                                          new XElement(XName.Get("pgSz", DocX.w.NamespaceName), new XAttribute(DocX.w + "w", "11906"), new XAttribute(DocX.w + "h", "16838")), 
                                          new XElement(XName.Get("pgMar", DocX.w.NamespaceName), new XAttribute(DocX.w + "top", "1440"), 
                                                       new XAttribute(DocX.w + "right", "1440"), new XAttribute(DocX.w + "bottom", "1440"), 
                                                       new XAttribute(DocX.w + "left", "1440"), new XAttribute(DocX.w + "header", "708"), 
                                                       new XAttribute(DocX.w + "footer", "708"), new XAttribute(DocX.w + "gutter", "0"))))
            );

            if (trackChanges)
                newParagraphSection = HelperFunctions.CreateEdit(EditType.ins, DateTime.Now, newParagraphSection);

            Xml.Add(newParagraphSection);
        }

        public virtual void InsertSectionPageBreak(bool trackChanges = false)
        {
            var newParagraphSection = new XElement
            (
                //XName.Get("p", DocX.w.NamespaceName), new XElement(XName.Get("pPr", DocX.w.NamespaceName), new XElement(XName.Get("sectPr", DocX.w.NamespaceName)))
                XName.Get("p", DocX.w.NamespaceName), 
                    new XElement(XName.Get("pPr", DocX.w.NamespaceName), 
                             new XElement(XName.Get("sectPr", DocX.w.NamespaceName),
                                          new XElement(XName.Get("cols", DocX.w.NamespaceName), new XAttribute(DocX.w + "space", "708")),
                                          new XElement(XName.Get("pgSz", DocX.w.NamespaceName), new XAttribute(DocX.w + "w", "11906"), new XAttribute(DocX.w + "h", "16838")), 
                                          new XElement(XName.Get("pgMar", DocX.w.NamespaceName), new XAttribute(DocX.w + "top", "1440"), 
                                                       new XAttribute(DocX.w + "right", "1440"), new XAttribute(DocX.w + "bottom", "1440"), 
                                                       new XAttribute(DocX.w + "left", "1440"), new XAttribute(DocX.w + "header", "708"), 
                                                       new XAttribute(DocX.w + "footer", "708"), new XAttribute(DocX.w + "gutter", "0"))))
            );

            if (trackChanges)
                newParagraphSection = HelperFunctions.CreateEdit(EditType.ins, DateTime.Now, newParagraphSection);

            Xml.Add(newParagraphSection);
        }
(There's probably a more efficient way to set this many attributes, but it works...)

There's still a problem with the "continuous" attribute. A possible workaround is to add a method to Section.cs to set this value 'manually':
    public void SectionColumns(int columns)
    {
        XElement cols = Xml.Element(XName.Get("cols", DocX.w.NamespaceName));
        if (cols == null)
        {
            Xml.Add(new XElement(XName.Get("cols", DocX.w.NamespaceName)));
            cols = Xml.Element(XName.Get("cols", DocX.w.NamespaceName));
        }
        cols.SetAttributeValue(XName.Get("num", DocX.w.NamespaceName), columns);
        
    }
    
    public void SetContinuous(bool continuous)
    {
        if(continuous)
        {
            XElement type = Xml.Element(XName.Get("type", DocX.w.NamespaceName));
            if (type == null)
            {
                Xml.Add(new XElement(XName.Get("type", DocX.w.NamespaceName)));
                type = Xml.Element(XName.Get("type", DocX.w.NamespaceName));
            }
            type.SetAttributeValue(XName.Get("val", DocX.w.NamespaceName), "continuous");
        }
        else if(!continuous)
        {
            var type = Xml.Element(XName.Get("type", DocX.w.NamespaceName));
            if (type != null)
            {
                IEnumerable<XAttribute> typeAttributes = type.Attributes();
                foreach (XAttribute a in typeAttributes)
                {
                    if (a.Value.Equals("continuous"))
                        a.Remove();
                }
                if (!type.HasAttributes)
                    type.Remove();
            }
            
        }           
            
    }
(I updated the SectionColumns method as well, to avoid null references, the "cols" element should be created if it doesn't exist.)

Hopefully with this, you can create some sort of workaround to use the sections with DocX rather than Word automation.
Aug 27, 2014 at 4:28 PM
I have been unable to get this to correctly change between different numbers of columns. Even with this change, my tests still have the first section always having the same number of columns as the last section.
Aug 28, 2014 at 7:50 AM
Perhaps this helps, this is my test code to create a new document with multiple sections with a different number of columns:
            using (DocX document = DocX.Create(doc))
            {
                document.InsertParagraph("One column. This is a test. This is a test. This is a test. This is a test. This is a test. This is a test. This is a test. This is a test. This is a test. This is a test.");
                document.InsertSection();
                document.InsertParagraph("Two columns. This is another test. This is another test. This is another test. This is another test. This is another test. This is another test. This is another test. This is another test. This is another test.");
                document.InsertSection();
                document.InsertParagraph("Three columns. Third test. Third test. Third test. Third test. Third test. Third test. Third test. Third test. Third test. Third test. Three columns. Third test. Third test. Third test. Third test. Third test. Third test. Third test. Third test. Third test. Third test.");
                document.InsertSection();
                document.InsertParagraph("One column. This is the last paragraph in this test. This is the last paragraph in this test. This is the last paragraph in this test.");
                
                Section sBase = document.Sections[0];
                sBase.SectionColumns(1);
                sBase.SetContinuous(true);
                Section s = document.Sections[1];
                s.SectionColumns(2);
                s.SetContinuous(true);
                Section sT = document.Sections[2];
                sT.SectionColumns(3);
                sT.SetContinuous(true);
                Section sThree = document.Sections[3];
                sThree.SectionColumns(1);
                sThree.SetContinuous(true);
                
                document.Save();
            }
If you use SetContinuous(false) each section will start a new page.

If you're using an existing Word document, it's possible the "base" section is actually the last one rather than the first, since Word adds the section right before the end of the body tag while DocX inserts it after the opening body tag.
Aug 28, 2014 at 12:37 PM
Thank you, Annika89. I had been putting section information inline which worked OK until the end. There is one additional problem which needs to be addressed. If the page size, margins, etc., are not the same as preset in Container.cs, then they are overwritten until the last section. The problem is in blocks like
               XName.Get("p", DocX.w.NamespaceName),
                    new XElement(XName.Get("pPr", DocX.w.NamespaceName),
                             new XElement(XName.Get("sectPr", DocX.w.NamespaceName),
                                          new XElement(XName.Get("type", DocX.w.NamespaceName), new XAttribute(DocX.w + "val", "continuous")),
                                          new XElement(XName.Get("cols", DocX.w.NamespaceName), new XAttribute(DocX.w + "space", "708")),
                                          new XElement(XName.Get("pgSz", DocX.w.NamespaceName), new XAttribute(DocX.w + "w", "11906"), new XAttribute(DocX.w + "h", "16838")),
                                          new XElement(XName.Get("pgMar", DocX.w.NamespaceName), new XAttribute(DocX.w + "top", "1440"),
                                                       new XAttribute(DocX.w + "right", "1440"), new XAttribute(DocX.w + "bottom", "1440"),
                                                       new XAttribute(DocX.w + "left", "1440"), new XAttribute(DocX.w + "header", "708"),
                                                       new XAttribute(DocX.w + "footer", "708"), new XAttribute(DocX.w + "gutter", "0"))))
I don't normally code in C# but will be trying to replace the literals with document parameters.
Thanks for your help.
Aug 29, 2014 at 9:31 AM
Edited Aug 29, 2014 at 10:47 AM
You're welcome :)
And you're right, that piece of code now sets the pagesize and margins to the DocX default. If that's not what you're using, it will cause problems.
One thing you could do, is creating an extra InsertSection constructor that takes optional arguments for the page width/height and all top/bottom/left/right/header/footer/gutter margins, like this:
        public virtual void InsertSection(bool trackChanges = false, int pageWidth = 11906, int pageHeight = 16838, int marginTop = 1440, int marginRight = 1440, int marginBottom = 1440, int marginLeft = 1440, int marginHeader = 708, int marginFooter = 708, int marginGutter = 0)
        {
            var newParagraphSection = new XElement
            (
                //XName.Get("p", DocX.w.NamespaceName), new XElement(XName.Get("pPr", DocX.w.NamespaceName), new XElement(XName.Get("sectPr", DocX.w.NamespaceName), new XElement(XName.Get("type", DocX.w.NamespaceName), new XAttribute(DocX.w + "val", "continuous"))))
                XName.Get("p", DocX.w.NamespaceName), 
                    new XElement(XName.Get("pPr", DocX.w.NamespaceName), 
                             new XElement(XName.Get("sectPr", DocX.w.NamespaceName), 
                                          new XElement(XName.Get("type", DocX.w.NamespaceName), new XAttribute(DocX.w + "val", "continuous")), 
                                          new XElement(XName.Get("cols", DocX.w.NamespaceName), new XAttribute(DocX.w + "space", "708")), 
                                          new XElement(XName.Get("pgSz", DocX.w.NamespaceName), new XAttribute(DocX.w + "w", pageWidth), new XAttribute(DocX.w + "h", pageHeight)), 
                                          new XElement(XName.Get("pgMar", DocX.w.NamespaceName), new XAttribute(DocX.w + "top", marginTop), 
                                                       new XAttribute(DocX.w + "right", marginRight), new XAttribute(DocX.w + "bottom", marginBottom), 
                                                       new XAttribute(DocX.w + "left", marginLeft), new XAttribute(DocX.w + "header", marginHeader), 
                                                       new XAttribute(DocX.w + "footer", marginFooter), new XAttribute(DocX.w + "gutter", marginGutter))))
            );

            if (trackChanges)
                newParagraphSection = HelperFunctions.CreateEdit(EditType.ins, DateTime.Now, newParagraphSection);

            Xml.Add(newParagraphSection);
        }
In C# you can use it like this, I'm not sure about VB.NET though:
document.InsertSection(marginLeft: 800, marginRight: 800);
-- Edit --

You could also use the document's page size and margins in the InsertSection method. However, the page size is returned in float which eventually changes the int value if you convert the float back to an int. If the page sizes aren't exactly the same, the section will be on a new page. If you want to use this, you should also make sure you're getting the page sizes in the unedited int-format:
        public virtual void InsertSectionWithDocumentMargins(bool trackChanges = false)
        {
            var newParagraphSection = new XElement
            (
                //XName.Get("p", DocX.w.NamespaceName), new XElement(XName.Get("pPr", DocX.w.NamespaceName), new XElement(XName.Get("sectPr", DocX.w.NamespaceName), new XElement(XName.Get("type", DocX.w.NamespaceName), new XAttribute(DocX.w + "val", "continuous"))))
                XName.Get("p", DocX.w.NamespaceName), 
                    new XElement(XName.Get("pPr", DocX.w.NamespaceName), 
                             new XElement(XName.Get("sectPr", DocX.w.NamespaceName), 
                                          new XElement(XName.Get("type", DocX.w.NamespaceName), new XAttribute(DocX.w + "val", "continuous")), 
                                          new XElement(XName.Get("cols", DocX.w.NamespaceName), new XAttribute(DocX.w + "space", "708")), 
                                          new XElement(XName.Get("pgSz", DocX.w.NamespaceName), new XAttribute(DocX.w + "w", Document.ExactPageWidth), new XAttribute(DocX.w + "h", Document.ExactPageHeight)),
                                          new XElement(XName.Get("pgMar", DocX.w.NamespaceName), new XAttribute(DocX.w + "top", (int)(Document.MarginTop * 15.0f)),
                                                       new XAttribute(DocX.w + "right", (int)(Document.MarginRight * 15.0f)), new XAttribute(DocX.w + "bottom", (int)(Document.MarginBottom * 15.0f)),
                                                       new XAttribute(DocX.w + "left", (int)(Document.MarginLeft * 15.0f)), new XAttribute(DocX.w + "header", "708"),
                                                       new XAttribute(DocX.w + "footer", "708"), new XAttribute(DocX.w + "gutter", "0"))))
            );

            if (trackChanges)
                newParagraphSection = HelperFunctions.CreateEdit(EditType.ins, DateTime.Now, newParagraphSection);

            Xml.Add(newParagraphSection);
        }
I created this alongside PageWidth and PageHeight:
        public int ExactPageWidth
        {
            get
            {
                XElement body = mainDoc.Root.Element(XName.Get("body", DocX.w.NamespaceName));
                XElement sectPr = body.Element(XName.Get("sectPr", DocX.w.NamespaceName));
                if (sectPr != null)
                {
                    XElement pgSz = sectPr.Element(XName.Get("pgSz", DocX.w.NamespaceName));

                    if (pgSz != null)
                    {
                        XAttribute w = pgSz.Attribute(XName.Get("w", DocX.w.NamespaceName));
                        if (w != null)
                        {
                            int width;
                            if (int.TryParse(w.Value, out width))
                                return width;
                        }
                    }
                }

                return 11906;
            }
        }
        
        public int ExactPageHeight
        {
            get
            {
                XElement body = mainDoc.Root.Element(XName.Get("body", DocX.w.NamespaceName));
                XElement sectPr = body.Element(XName.Get("sectPr", DocX.w.NamespaceName));
                if (sectPr != null)
                {
                    XElement pgSz = sectPr.Element(XName.Get("pgSz", DocX.w.NamespaceName));

                    if (pgSz != null)
                    {
                        XAttribute h = pgSz.Attribute(XName.Get("h", DocX.w.NamespaceName));
                        if (h != null)
                        {
                            int height;
                            if (int.TryParse(h.Value, out height))
                                return height;
                        }
                    }
                }

                return 16838;
            }
        }
Even though I didn't come across this while testing with the default page size/margins, it's possible the same goes for the margins as well.
Aug 30, 2014 at 3:59 PM
Annika89,
You've worked really hard on this. I appreciate it. I did the suggestion about adding the parameters to insertsection and it works great with adjustment for changing the page width from 100 = 1 inch to 1440 = 1 inch. I was out of pocket for a couple of days and did not see the later subroutines. I just inserted them and they work perfectly. This appears to be a complete solution to the multiple column problem. Thanks again.