Creating a Table in Word Using PowerShell

In this article, I will be showing you how to build a table in Word using PowerShell.

First we will go ahead and make the initial object creation and open up Word.

 
$Word = New-Object -ComObject Word.Application
$Word.Visible = $True
$Document = $Word.Documents.Add()
$Selection = $Word.Selection

Now that we have done that, we can start looking at building out a table to use in our document. The first example will look more like a typical table that has the headers on the first row and the rest of the rows will consist of the actual data. Of course, the first thing that I need first is some actual data that I can put into a table.

 
$Volume = Get-WmiObject Win32_Volume

I think listing out all of the volumes on my system should suffice. Now that I have this information, I can now start building out my table. One of the values that I want to document has a specific lookup that I need to quickly build using a hash table.

 
$DriveType = @{
    0x0 = 'Unknown'
    0x1 = 'No Root Directory'
    0x2 = 'Removable Disk'
    0x3 = 'Local Disk'
    0x4 = 'Network Drive'
    0x5 = 'Compact Disk'
    0x6 = 'RAM Disk'
}

I want to ensure that I am at the very end of my current spot in the document so I can put the table after whatever I may have written. I also need to make sure that the object is treated like a collection, otherwise I won’t be able to specify the index, in this case –1 for the last item in the collection.

 
$Range = @($Selection.Paragraphs)[-1].Range

The $Range will be used soon in the construction and placement of the table. Using the $Selection, I will create a new able by using the Tables.Add() method and then saving the output of the method which is the table object that I can then use to begin processing and adding data into the table.

Looking at the parameters really doesn’t help much.

 
$Selection.Tables.add

image

This is one of those cases where a trip to MSDN will provide more useful information to determine what we can actually use.

image

Now that is much more useful to us! we can see that we need to specify the Range that was found as well as some parameters for the number of Rows and Columns as well as the behavior of the table and how the items will fit. We don’t actually have to specify the Behaviors, but in this case I will be adding those as well.

As it shows, the rows and columns are pretty much a no-brainer as we can look at how many properties we are using and that will be the Column count and as for the Rows, we will take the number of objects returned and then add 1 to the count which will account for the header row. In this case, I am going to have 6 columns and 5 rows in my table.

image

Now for my Table and AutoFit behavior, I need to figure out exactly what is available to choose from. Again, I can find this inform on MSDN, but I think this is a great exercise in PowerShell’s ability to easily explore the environment to locate what we are looking for.

 
$AutoFit = [Microsoft.Office.Interop.Word.WdDefaultTableBehavior].Assembly.GetTypes() | 
Where {$_.Name -match 'autofit'}
$AutoFit | Select FullName

$DefaultTable = [Microsoft.Office.Interop.Word.WdDefaultTableBehavior].Assembly.GetTypes() | 
Where {$_.Name -match 'defaulttable'}
$DefaultTable | Select FullName

image

Perfect! Now to see what values we have to choose from.

 
[Enum]::GetNames($AutoFit)
[Enum]::GetNames($DefaultTable)

image

For my table, I am going with wdAutoFitContent and wdWord9TableBehavior when I build out my table.

 
$Table = $Selection.Tables.add(
    $Selection.Range,($Volume.Count + 1),6,
    [Microsoft.Office.Interop.Word.WdDefaultTableBehavior]::wdWord9TableBehavior,
    [Microsoft.Office.Interop.Word.WdAutoFitBehavior]::wdAutoFitContent
)

image

I am next going to pick out the table style that will be used for this specific table. Because I want a focus on the distinction between the header and the data, I am going with Medium Shading 1 – Accent 1 which means that my table goes from this…

image

…to this:

 
$Table.Style = "Medium Shading 1 - Accent 1" 

image

Ok, right now this really doesn’t look all that impressive, but it will look nicer once we have finished with the data. And speaking of data, lets start adding the header to this now.

Think of the table as a multi-dimensional array and that you have to use coordinates to properly place the data. In this case we are using the $Table.Cell(column,row) property where we have to specify the coordinates within the parenthesis to say where the data going and if we are going to add some custom formatting to that particular cell. Once each column has been completed we simply move onto the next column and perform the same operation until completed and ready for the next row.

 
## Header
$Table.cell(1,1).range.Bold=1
$Table.cell(1,1).range.text = "Drive"
$Table.cell(1,2).range.Bold=1
$Table.cell(1,2).range.text = "DriveType"
$Table.cell(1,3).range.Bold=1
$Table.cell(1,3).range.text = "Label"
$Table.cell(1,4).range.Bold=1
$Table.cell(1,4).range.text = "FileSystem"
$Table.cell(1,5).range.Bold=1
$Table.cell(1,5).range.text = "FreeSpaceGB"
$Table.cell(1,6).range.Bold=1
$Table.cell(1,6).range.text = "CapacityGB"

image

Starting to look like something a little more useful now. The last piece of this is to add the data. Given that I really don’t know just how much data might be available at the time of this, I am relying on a For() loop to handle this for me.

 
## Values
For ($i=0; $i -lt ($Volume.Count); $i++) {
    $Table.cell(($i+2),1).range.Bold = 0
    $Table.cell(($i+2),1).range.text = $Volume[$i].Name
    $Table.cell(($i+2),2).range.Bold = 0
    $Table.cell(($i+2),2).range.text = $DriveType[[int]$Volume[$i].DriveType]
    $Table.cell(($i+2),3).range.Bold = 0
    $Table.cell(($i+2),3).range.text = $Volume[$i].Label
    $Table.cell(($i+2),4).range.Bold = 0
    $Table.cell(($i+2),4).range.text = $Volume[$i].FileSystem
    $Table.cell(($i+2),5).range.Bold = 0
    $Table.cell(($i+2),5).range.text = [math]::Round($Volume[$i].FreeSpace/1GB,2)
    $Table.cell(($i+2),6).range.Bold = 0
    $Table.cell(($i+2),6).range.text = [math]::Round($Volume[$i].Capacity/1GB,2)
}

Same approach as my header but not only do it increment the columns on each pass, I also have to make sure to increment the rows as well to ensure that the data is accurate. The end result is something like this:

image

Lastly, I want to make sure I am at the end of this and outside of my table so I can continue adding data or writing if necessary without writing to the table. I also want to add a space after the table for formatting purposes.

 
$Word.Selection.Start= $Document.Content.End
$Selection.TypeParagraph()

If you wanted more of a list type of format, that can also be accomplished using a similar approach. This time we will keep the number of columns to two and use as many rows as we need.

 
$BIOS = Get-WmiObject Win32_Bios

$Range = @($Selection.Paragraphs)[-1].Range
$Table = $Selection.Tables.add(
    $Selection.Range,5,2,
    [Microsoft.Office.Interop.Word.WdDefaultTableBehavior]::wdWord9TableBehavior,
    [Microsoft.Office.Interop.Word.WdAutoFitBehavior]::wdAutoFitContent
)

Another different is that I don’t want to focus on the first row, so a different style will be used.

 
$Table.Style = "Light Shading - Accent 1"

And lastly, we start building out the list table.

 
## Header
$Table.cell(1,1).range.Bold=1
$Table.cell(1,1).range.text = "Manufacturer"
$Table.cell(2,1).range.Bold=1
$Table.cell(2,1).range.text = "Name"
$Table.cell(3,1).range.Bold=1
$Table.cell(3,1).range.text = "Version"
$Table.cell(4,1).range.Bold=1
$Table.cell(4,1).range.text = "Serial Number"
$Table.cell(5,1).range.Bold=1
$Table.cell(5,1).range.text = "BIOS Version"
    
## Data
$Table.cell(1,2).range.Bold = 0
$Table.cell(1,2).range.text = $BIOS.Manufacturer
$Table.cell(2,2).range.Bold = 0
$Table.cell(2,2).range.text = $BIOS.Name
$Table.cell(3,2).range.Bold = 0
$Table.cell(3,2).range.text = $BIOS.Version
$Table.cell(4,2).range.Bold = 0
$Table.cell(4,2).range.text = $BIOS.SerialNumber
$Table.cell(5,2).range.Bold = 0
$Table.cell(5,2).range.text = $BIOS.SMBIOSBIOSVersion

$Word.Selection.Start= $Document.Content.End
$Selection.TypeParagraph()

The end result is something that looks like this:

image

Of course, this can easily start adding to your line count if you plan on having several tables that you want to add in your document and writing code isn’t about who has the most rows. Smile

I wrote a small function that takes an object and can build out a Table or List fairly easily and all it requires is the $Object to be added to the table and the $Selection object. Note that this is one that I use on my personal projects but can be altered to meet your needs. Another thing is that you will need to tailor your object to only have specific properties, otherwise your table/list will be overrun with data.

An example of it in use is here:

 
$Volume = @(Get-WmiObject Win32_Volume  | ForEach {
    [pscustomobject]@{
        Drive = $_.Name
        DriveType = $DriveType[[int]$_.DriveType]
        Label = $_.label
        FileSystem = $_.FileSystem
        "FreeSpace(GB)" = "{0:N2}" -f ($_.FreeSpace /1GB)
        "Capacity(GB)" = "{0:N2}" -f ($_.Capacity/1GB)
    }
})

New-WordTable -Object $Volume -Columns 6 -Rows ($Volume.Count+1) –AsTable

And here:

 
$BIOS = @(Get-WmiObject Win32_Bios | ForEach {
    [pscustomobject] @{
        Manufacturer = $_.Manufacturer
        Name = $_.Name
        Version = $_.Version
        SerialNumber = $_.SerialNumber
        BIOSVersion = $_.SMBIOSBIOSVersion
    }
})

New-WordTable -Object $BIOS -Columns 2 -Rows ($BIOS.PSObject.Properties | Measure-Object).Count -AsList

Source Code

 
Function New-WordTable {
    [cmdletbinding(
        DefaultParameterSetName='Table'
    )]
    Param (
        [parameter()]
        [object]$WordObject,
        [parameter()]
        [object]$Object,
        [parameter()]
        [int]$Columns,
        [parameter()]
        [int]$Rows,
        [parameter(ParameterSetName='Table')]
        [switch]$AsTable,
        [parameter(ParameterSetName='List')]
        [switch]$AsList,
        [parameter()]
        [string]$TableStyle,
        [parameter()]
        [Microsoft.Office.Interop.Word.WdDefaultTableBehavior]$TableBehavior = 'wdWord9TableBehavior',
        [parameter()]
        [Microsoft.Office.Interop.Word.WdAutoFitBehavior]$AutoFitBehavior = 'wdAutoFitContent'
    )
    #Specifying 0 index ensures we get accurate data from a single object
    $Properties = $Object[0].psobject.properties.name
    $Range = @($WordObject.Paragraphs)[-1].Range
    $Table = $WordObject.Tables.add(
    $WordObject.Range,$Rows,$Columns,$TableBehavior, $AutoFitBehavior)

    Switch ($PSCmdlet.ParameterSetName) {
        'Table' {
            If (-NOT $PSBoundParameters.ContainsKey('TableStyle')) {
                $Table.Style = "Medium Shading 1 - Accent 1"
            }
            $c = 1
            $r = 1
            #Build header
            $Properties | ForEach {
                Write-Verbose "Adding $($_)"
                $Table.cell($r,$c).range.Bold=1
                $Table.cell($r,$c).range.text = $_
                $c++
            }  
            $c = 1    
            #Add Data
            For ($i=0; $i -lt (($Object | Measure-Object).Count); $i++) {
                $Properties | ForEach {
                    $Table.cell(($i+2),$c).range.Bold=0
                    $Table.cell(($i+2),$c).range.text = $Object[$i].$_
                    $c++
                }
                $c = 1 
            }                 
        }
        'List' {
            If (-NOT $PSBoundParameters.ContainsKey('TableStyle')) {
                $Table.Style = "Light Shading - Accent 1"
            }
            $c = 1
            $r = 1
            $Properties | ForEach {
                $Table.cell($r,$c).range.Bold=1
                $Table.cell($r,$c).range.text = $_
                $c++
                $Table.cell($r,$c).range.Bold=0
                $Table.cell($r,$c).range.text = $Object.$_
                $c--
                $r++
            }
        }
    }
}
This entry was posted in powershell and tagged , , , . Bookmark the permalink.

16 Responses to Creating a Table in Word Using PowerShell

  1. What to do in this case: You have a Word table with a number columns. Every row will consist of values originating from an excel sheet. The information in the first column is build up out of the contents of three columns from an excel file. First line should be italic and bold, second line should be bold and the last lines ou want no formatiing. All in one Cell. I can make the whole cell bold but what to do in case of different formatiing per line?
    I have this which is almost working… I want the content of column 2 to be in different formats

    Function Create-Table($fBookmark,$ftype)
    {
    $Table = $WordDoc.Tables.Add(($WordDoc.Bookmarks.Item($fBookmark).range),($ftype.Count + 1),4)
    $bold = $Table.Cell(1,1).Range ; $bold.bold = $True
    $Table.Cell(1,1).Range.Text = “Internal Reference Number”
    $bold = $Table.Cell(1,2).Range ; $bold.bold = $True
    $Table.Cell(1,2).Range.Text = “Findings”
    $bold = $Table.Cell(1,3).Range ; $bold.bold = $True
    $Table.Cell(1,3).Range.Text = “Recommendations”
    $bold = $Table.Cell(1,4).Range ; $bold.bold = $True
    $Table.Cell(1,4).Range.Text = “Management response”
    $x = 2

    Foreach($t in $fAudittype)
    {
        $Table.Cell($x,1).Range.Text = $t.'Internal-Reference-Number'
        $Table.Cell($x,2).Range.Text = $t.'Finding-Title' # should be bold and italic
        $bold = $Table.Cell($x,2).Range ; $bold.bold = $True
        $Table.Cell($x,2).Range.InsertAfter("`n")
        $Table.Cell($x,2).Range.InsertAfter($t.Grade) # should be bold
        $Table.Cell($x,2).Range.InsertAfter("`n")
        # $bold = $Table.Cell($x,2).Range ; $bold.bold = $false
        $Table.Cell($x,2).Range.InsertAfter($t.'Finding-Text') # should be normal 
        $Table.Cell($x,3).Range.Text = $t.Recommendations
        $bold = $Table.Cell($x,4).Range ; $bold.bold = $True
        $Table.Cell($x,4).Range.InsertAfter("Response:")
        $Table.Cell($x,4).Range.InsertAfter("`n")
        $Table.Cell($x,4).Range.InsertAfter("`n")
        $Table.Cell($x,4).Range.InsertAfter("Owner: " + $t.Responsible)
        $Table.Cell($x,4).Range.InsertAfter("`n")
        $Table.Cell($x,4).Range.InsertAfter("`n")
        $Table.Cell($x,4).Range.InsertAfter("Due Date:")
        $x++
    }
    #$table.UpdateAutoFormat()   
    $table.Range.Style = "Table Grid"
    $table.Borders.InsideLineStyle = 1
    $table.Borders.OutsideLineStyle = 1 
    

    }

  2. Jeff Patton says:

    Old post, but it came in handy, found a few problems with the function, I corrected it in a gist, I’m guessing a difference in versions between word, but don’t really know. At any rate, came in super handy.


    Function New-WordTable {
    [cmdletbinding(
    DefaultParameterSetName='Table'
    )]
    Param (
    [parameter()]
    [object]$WordObject,
    [parameter()]
    [object]$Object,
    [parameter()]
    [int]$Columns,
    [parameter()]
    [int]$Rows,
    [parameter(ParameterSetName='Table')]
    [switch]$AsTable,
    [parameter(ParameterSetName='List')]
    [switch]$AsList,
    [parameter()]
    [string]$TableStyle,
    [parameter()]
    [Microsoft.Office.Interop.Word.WdDefaultTableBehavior]$TableBehavior = 'wdWord9TableBehavior',
    [parameter()]
    [Microsoft.Office.Interop.Word.WdAutoFitBehavior]$AutoFitBehavior = 'wdAutoFitContent'
    )
    #Specifying 0 index ensures we get accurate data from a single object
    $Properties = $Object[0].psobject.properties.name
    $Range = @($Word.Selection.Paragraphs)[-1].Range
    $Table = $WordObject.Selection.Tables.add($Range,$Rows,$Columns,$TableBehavior,$AutoFitBehavior)
    Switch ($PSCmdlet.ParameterSetName) {
    'Table' {
    If (-NOT $PSBoundParameters.ContainsKey('TableStyle')) {
    $Table.Style = "Medium Shading 1 – Accent 1"
    }
    $c = 1
    $r = 1
    #Build header
    $Properties | ForEach {
    Write-Verbose "Adding $($_)"
    $Table.cell($r,$c).range.Bold=1
    $Table.cell($r,$c).range.text = $_
    $c++
    }
    $c = 1
    #Add Data
    For ($i=0; $i -lt (($Object | Measure-Object).Count); $i++) {
    $Properties | ForEach {
    $Table.cell(($i+2),$c).range.Bold=0
    $Table.cell(($i+2),$c).range.text = $Object[$i].$_
    $c++
    }
    $c = 1
    }
    }
    'List' {
    If (-NOT $PSBoundParameters.ContainsKey('TableStyle')) {
    $Table.Style = "Light Shading – Accent 1"
    }
    $c = 1
    $r = 1
    $Properties | ForEach {
    $Table.cell($r,$c).range.Bold=1
    $Table.cell($r,$c).range.text = $_
    $c++
    $Table.cell($r,$c).range.Bold=0
    $Table.cell($r,$c).range.text = $Object.$_
    $c–
    $r++
    }
    }
    }
    }

  3. Memo says:

    Hey Boe

    Great article, thanks for putting it together!, just a comment that got me a little bit confused at the beginning , on the explanation of”

    “Think of the table as a multi-dimensional array and that you have to use coordinates to properly place the data. In this case we are using the $Table.Cell(column,row) ”

    its actually row,column, which is what you actually have on the full script

    $Table.cell($r,$c)

  4. Adrianz41 says:

    Script looks great, however been having some issues getting it to work…

    was originally getting the same error as Erwin but now I get the below error…

    Exception calling “Add” with “5” argument(s): “The number must be between 1 and 63.”
    At line:1 char:5
    + $Table = $Selection.Tables.add(
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ComMethodTargetInvocation

    After three hours of playing I cannot work out whats wrong 😦

  5. Devesh Krishnani says:

    Can you tell how to delete multiple tables using powershell

  6. Ben Giacaman says:

    Thank you so much for your post! I am learning a lot… Is there a way to have a table but with a “No Borders” style? I need the formatting benefits of a table, but don’t really need the lines. Perhaps another value for $table.style? Thanks in advance for the help

  7. Dirk says:

    There is a “quick and dirty” alternative way to add a table and avoid any loops in word via PowerShell using the ConvertToTable method on the Range object. Here is an example of the usage:
    $Word = New-Object -comobject Word.Application
    $Word.Visible = $true
    $Doc = $Word.Documents.Add()
    $Range = $Doc.Range()
    $text=(Get-Process | select Handle,ID,Name | ConvertTo-Csv -NoTypeInformation | Out-String) -replace ‘”‘,”
    $Range.Text = “$text”
    $separator=[Microsoft.Office.Interop.Word.WdTableFieldSeparator]::wdSeparateByCommas
    $table=$Range.ConvertToTable($separator)

  8. Hi Boe,

    Great stuff!!! This will come in handy for a system documentation script I’m working on.

    I got an error running the New-WordTable function at $Table = $WordObject.Tables.add(
    Stating: “You cannot call a method on a null-valued expression.”

    I backtracked the introduction example and added $Selection = $WordObject.Selection
    From that point I used $Selection and everything worked without a hitch!

    $Selection = $WordObject.Selection
    $Range = @($Selection.Paragraphs)[-1].Range
    $Table = $Selection.Tables.add(
    $Selection.Range,$Rows,$Columns,$TableBehavior, $AutoFitBehavior)

    Thanks again!!!

    Regards,

    Irwin

  9. FoxDeploy says:

    Hi Boe,

    It looks like WordPress messed with some of your code here, changing the double quotes to an HTML ‘&quot:’ instead.

    If i might make a suggestion, how about adding a Begin{} process to this function to handle creating the Word Object, and then building the table. I think some folks (like myself) read your description, were impressed, and then jumped to the bottom to copy the function declaration, which by itself will not work.

    • Boe Prox says:

      Thanks for the heads up on the double quote issue with the source code. Thought I had that stuff cleaned up prior to posting.

      If I add a Begin{} to the function, that would assume that the function supports pipeline input (which it currently doesn’t) and I would need to modify some parts of the code to ensure that it does take pipeline support properly. This function was mostly designed as a helper function that would be included in a script that already was performing some operations with Word.
      That being said, I will take a look at updating this code sometime soon to support creating a word object and writing a table as well as providing pipeline support for the incoming object.

      Great suggestions!

  10. FoxDeploy says:

    Shamelessly stealing this for my scripts. That business of adding a table style was hot!

    Then the new-wordtable function! Holy hell, I need to buy you a beer!

  11. davidjohnson529424291 says:

    PS E:\Documents\WindowsPowerShell\Scripts> add-type -AssemblyName “Microsoft.Office.Interop.Word”
    $BIOS = Get-WmiObject Win32_Bios
    $Range = @($Selection.Paragraphs)[-1].Range
    $Table = $Selection.Tables.add(
    $Selection.Range,5,2,
    [Microsoft.Office.Interop.Word.WdDefaultTableBehavior]::wdWord9TableBehavior,
    [Microsoft.Office.Interop.Word.WdAutoFitBehavior]::wdAutoFitContent
    )
    You cannot call a method on a null-valued expression.
    At line:5 char:1
    + $Table = $Selection.Tables.add(
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull
    PS E:\Documents\WindowsPowerShell\Scripts> $PSVersionTable
    Name Value
    —- —–
    PSVersion 4.0
    WSManStackVersion 3.0
    SerializationVersion 1.1.0.1
    CLRVersion 4.0.30319.34209
    BuildVersion 6.3.9600.17400
    PSCompatibleVersions {1.0, 2.0, 3.0, 4.0}
    PSRemotingProtocolVersion 2.2

  12. $BIOS = Get-WmiObject Win32_Bios
    $Range = @($Selection.Paragraphs)[-1].Range
    $Table = $Selection.Tables.add(
    $Selection.Range,5,2,
    [Microsoft.Office.Interop.Word.WdDefaultTableBehavior]::wdWord9TableBehavior,
    [Microsoft.Office.Interop.Word.WdAutoFitBehavior]::wdAutoFitContent )
    You cannot call a method on a null-valued expression.
    At line:5 char:1
    + $Table = $Selection.Tables.add(

    PS E:\Documents\WindowsPowerShell\Scripts> $PSVersionTable

    Name Value
    —- —–
    PSVersion 4.0
    WSManStackVersion 3.0
    SerializationVersion 1.1.0.1
    CLRVersion 4.0.30319.34209
    BuildVersion 6.3.9600.17400
    PSCompatibleVersions {1.0, 2.0, 3.0, 4.0}
    PSRemotingProtocolVersion 2.2

Leave a comment