I am trying to create a vba program in excel that exports the user entered data to XML format, so far I have the following:
Below image shows 4 columns
Student Id
Student Name
Student Age
Student Mark
The Export button opens a popup that let the user choose the location of the output xml file with a Convert button
Once the user clicked on the Convert button, the below xml data is generated into the default.xml file
<?xml version="1.0"?>
<data>
<student><id>1</id>
<name>Jad</name>
<age>25</age>
<mark>17</mark>
</student>
</data>
The output seems fine to me so far, but I am looking to add more functionalities, I am trying to add a "Mark" column dynamically on user button click as shown below
Once the user clicks on Add Mark, a new column will appear in order to let the user enter a new grade, or it is better if we can place the new column in a separate form, for example we may add an additional field named Material Name, so on each button click 2 fields will appear Material Name and Material Mark), the expected excel sheet may be something like the below
the expected output of the xml file may be something like the below
<?xml version="1.0"?>
<data>
<student><id>1</id>
<name>Jad</name>
<age>25</age>
<materials>
<material>
<name>Maths</name>
<mark>17</marks>
</material>
<material>
<name>Physics</name>
<mark>18</marks>
</material>
</materials>
</student>
</data>
The function I am used to generate XML file is shown below
Function fGenerateXML(rngData As Range, rootNodeName As String) As String
'===============================================================
' XML Tags
' Table
Const HEADER As String = "<?xml version=""1.0""?>"
Dim TAG_BEGIN As String
Dim TAG_END As String
Const NODE_DELIMITER As String = "/"
'===============================================================
Dim intColCount As Integer
Dim intRowCount As Integer
Dim intColCounter As Integer
Dim intRowCounter As Integer
Dim rngCell As Range
Dim strXML As String
' Initial table tag...
TAG_BEGIN = vbCrLf & "<" & rootNodeName & ">"
TAG_END = vbCrLf & "</" & rootNodeName & ">"
strXML = HEADER
strXML = strXML & TAG_BEGIN
With rngData
' Discover dimensions of the data we
' will be dealing with...
intColCount = .Columns.Count
intRowCount = .Rows.Count
Dim strColNames() As String
ReDim strColNames(intColCount)
' First Row is the Field/Tag names
If intRowCount >= 1 Then
' Loop accross columns...
For intColCounter = 1 To intColCount
' Mark the cell under current scrutiny by setting
' an object variable...
Set rngCell = .Cells(1, intColCounter)
' Is the cell merged?..
If Not rngCell.MergeArea.Address = _
rngCell.Address Then
MsgBox ("!! Cell Merged ... Invalid format")
Exit Function
End If
strColNames(intColCounter) = rngCell.Text
Next
End If
Dim Nodes() As String
Dim NodeStack() As String
' Loop down the table's rows
For intRowCounter = 2 To intRowCount
strXML = strXML & vbCrLf & TABLE_ROW
ReDim NodeStack(0)
' Loop accross columns...
For intColCounter = 1 To intColCount
' Mark the cell under current scrutiny by setting
' an object variable...
Set rngCell = .Cells(intRowCounter, intColCounter)
' Is the cell merged?..
If Not rngCell.MergeArea.Address = _
rngCell.Address Then
MsgBox ("!! Cell Merged ... Invalid format")
Exit Function
End If
If Left(strColNames(intColCounter), 1) = NODE_DELIMITER Then
Nodes = Split(strColNames(intColCounter), NODE_DELIMITER)
' check whether we are starting a new node or not
Dim i As Integer
Dim MatchAll As Boolean
MatchAll = True
For i = 1 To UBound(Nodes)
If i <= UBound(NodeStack) Then
If Trim(Nodes(i)) <> Trim(NodeStack(i)) Then
'not match
'MsgBox (Nodes(i) & "," & NodeStack(i))
MatchAll = False
Exit For
End If
Else
MatchAll = False
Exit For
End If
Next
' add close tags to those not used afterwards
' don't count it when no content
If Trim(rngCell.Text) <> "" Then
If MatchAll Then
strXML = strXML & "</" & NodeStack(UBound(NodeStack)) & ">" & vbCrLf
Else
For t = UBound(NodeStack) To i Step -1
strXML = strXML & "</" & NodeStack(t) & ">" & vbCrLf
Next
End If
If i < UBound(Nodes) Then
For t = i To UBound(Nodes)
' add to the xml
strXML = strXML & "<" & Nodes(t) & ">"
If t = UBound(Nodes) Then
strXML = strXML & Trim(rngCell.Text)
End If
Next
Else
t = UBound(Nodes)
' add to the xml
strXML = strXML & "<" & Nodes(t) & ">"
strXML = strXML & Trim(rngCell.Text)
End If
NodeStack = Nodes
Else
' since its a blank field, so no need to handle if field name repeated
If Not MatchAll Then
For t = UBound(NodeStack) To i Step -1
strXML = strXML & "</" & Trim(NodeStack(t)) & ">" & vbCrLf
Next
End If
ReDim Preserve NodeStack(i - 1)
End If
' the last column
If intColCounter = intColCount Then
' add close tags to those not used afterwards
If UBound(NodeStack) <> 0 Then
For t = UBound(NodeStack) To 1 Step -1
strXML = strXML & "</" & Trim(NodeStack(t)) & ">" & vbCrLf
Next
End If
End If
Else
' add close tags to those not used afterwards
If UBound(NodeStack) <> 0 Then
For t = UBound(NodeStack) To 1 Step -1
strXML = strXML & "</" & Trim(NodeStack(t)) & ">" & vbCrLf
Next
End If
ReDim NodeStack(0)
' skip if no content
If Trim(rngCell.Text) <> "" Then
strXML = strXML & "<" & Trim(strColNames(intColCounter)) & ">" & Trim(rngCell.Text) & "</" & Trim(strColNames(intColCounter)) & ">" & vbCrLf
End If
End If
Next
Next
End With
strXML = strXML & TAG_END
' Return the HTML string...
fGenerateXML = strXML
End Function
For more info you can refer to this link https://www.codeproject.com/Articles/6950/Export-Excel-to-XML-in-VBA
Please let me know if you have any suggestions.
It appears the XML Generator you are using already has a function to dynamically search for values until it reaches the last column.
Assuming we only have to modify the first row, it would be as simple as adding a new header to the last empty column
Here are two macros as an example:
Sub ButtonClick()
Call Add_XML_Header("/student/mark")
End Sub
Sub Add_XML_Header(Header As String)
Dim LastColumn As Integer
LastColumn = ActiveSheet.Cells(1, Columns.Count).End(xlToLeft).Column
ActiveSheet.Cells(1, LastColumn + 1).Value = Header
End Sub
Assign the first one titled ButtonClick to the button being used in your form.
This will result in an output like this:
Example1
If you wish to go with second option of 2 headers, simply modify the ButtonClick sub like so:
Sub ButtonClick()
Call Add_XML_Header("/student/material/name")
Call Add_XML_Header("/student/material/mark")
End Sub
However, this will slightly differ from your posted example. It will add both columns to the first row horizontally like the other headers rather than vertically as you had shown.
Here's what it would look like:
Example2
Related
Hello fellow stackholders
I have this in-efficient VBA macro where i convert rows to XMl and after that post it to a web-service. It all works fine and it post everything correctly - the problem is when the excel sheet has more than 1500 rows, then it takes forever to convert. it takes hours, if you go above 10 k lines (had a co-worker who tried).
My question: Is there a way for me to speed this up, so 10.000 rows wont take half a day?
So far my code looks like this:
Dim xDOC As DOMDocument
Dim XMLHttpRequest As XMLHTTP
Dim URL As String
' Variables for XML-bulk
Dim strXML As String
Dim varTable As Variant
Dim intRow As Integer
Dim intCol As Integer
Dim strRowElementName As String
Dim strTableElementName As String
Dim varColumnHeaders As Variant
' Set custom names
strTableElementName = "postdata"
strRowElementName = "general-journal-line"
' Select the whole table in the current sheet
Range("A7").Select
Range(Selection, Selection.End(xlToRight)).Select
Range(Selection, Selection.End(xlDown)).Select
' Get table data
varTable = Selection.Value
varColumnHeaders = Selection.Rows(1).Value
' Build XML
strXML = ""
strXML = strXML & "<" & strTableElementName & ">"
For intRow = 2 To UBound(varTable, 1)
strXML = strXML & "<" & strRowElementName & ">"
strXML = strXML & "<journal-template-name>KASSE</journal-template-name>"
strXML = strXML & "<journal-batch-name>" + ThisWorkbook.Sheets("SMARTapi-Opsaetning").Range("C8") + "</journal-batch-name>"
strXML = strXML & "<userid>" + Environ("computername") + "\" + Application.UserName + "</userid>"
strXML = strXML & "<account-type>G/L Account</account-type>"
For intCol = 1 To UBound(varTable, 2)
strXML = strXML & "<" & varColumnHeaders(1, intCol) & ">" & _
varTable(intRow, intCol) & "</" & varColumnHeaders(1, intCol) & ">"
Next
strXML = strXML & "</" & strRowElementName & ">"
Next
strXML = strXML & "</" & strTableElementName & ">"
Debug.Print strXML
After this i post it at a webservice:
Set XMLHttpRequest = New MSXML2.XMLHTTP
With XMLHttpRequest
.Open "POST", URL, False
.setRequestHeader "Content-Type", "text/xml; encoding='utf-8'"
.Send strXML
End With
Set xDOC = New DOMDocument
Do Until xDOC.readyState = 4
Loop
xDOC.LoadXML (XMLHttpRequest.responseText)
It all works great when there is less than 500 rows - any help to make it more efficient would be much appreciated.
EDIT: Changed the code to this, yet it is still somewhat slow.
Dim xDOC As DOMDocument
Dim XMLHttpRequest As XMLHTTP
Dim URL As String
' Variabler til XML-bulk
Dim strXML As String
Dim varTable As Variant
Dim intRow As Integer
Dim intCol As Integer
Dim strRowElementName As String
Dim strTableElementName As String
Dim varColumnHeaders As Variant
Dim strKonstant As String
' Set custom names
strTableElementName = "postdata"
strRowElementName = "general-journal-line"
' Find lines and get them before building the xml
Dim lRowCount As Long
Application.ActiveSheet.UsedRange
lRowCount = Worksheets("SMARTapi-Upload").UsedRange.Rows.Count
varTable = Range("A7", "J" + CStr(lRowCount))
varColumnHeaders = Range("A7", "J7")
strKonstant = "<" & strRowElementName & "><journal-template-name>KASSE</journal-template-name><journal-batch-name>" + ThisWorkbook.Sheets("SMARTapi-Opsaetning").Range("C8") + "</journal-batch-name><userid>" + Environ("computername") + "\" + Application.UserName + "</userid><account-type>G/L Account</account-type><balancing-account-type>G/L Account</balancing-account-type>"
' Build XML
strXML = ""
strXML = strXML & "<" & strTableElementName & ">"
For intRow = 2 To UBound(varTable, 1)
strXML = strXML & strKonstant
For intCol = 1 To UBound(varTable, 2)
strXML = strXML & "<" & varColumnHeaders(1, intCol) & ">" & _
varTable(intRow, intCol) & "</" & varColumnHeaders(1, intCol) & ">"
Next
strXML = strXML & "</" & strRowElementName & ">"
Next
strXML = strXML & "</" & strTableElementName & ">"
' HER SENDES XML MED DATA FRA TABELLEN
Set XMLHttpRequest = New MSXML2.XMLHTTP
With XMLHttpRequest
.Open "POST", URL, False
.setRequestHeader "Content-Type", "text/xml; encoding='utf-8'"
.Send strXML
End With
Set xDOC = New DOMDocument
Do Until xDOC.readyState = 4
Loop
xDOC.LoadXML (XMLHttpRequest.responseText)
Do everything that #Vityata recommends in his answer, this is all good stuff and useful in all writing endeavours.
Also, if you're looking to speed up the main loop in this (which I'd assume is where most of the delay is coming from) - there isn't a lot going on in there to slow it down. However, there are a couple of things that you repeatedly do within the loop that produce the same result each time:
strXML = strXML & "<journal-batch-name>" + ThisWorkbook.Sheets("SMARTapi-Opsaetning").Range("C8") + "</journal-batch-name>"
The above line grabs the value of cell C8 in another tab every time you start a new row. I'd assume that this doesn't actually change, so why do it every time? Grab it once and store it.
strXML = strXML & "<userid>" + Environ("computername") + "\" + Application.UserName + "</userid>"
The above line reads the computer name each row. No need. Again, do it once and store it.
You can also reduce the time taken a little more by examining the large block you build each row for the bits that never change and store the result of all of your concatenation outside the loop too.
My code would look something like this:
Dim xDOC As DOMDocument
Dim XMLHttpRequest As XMLHTTP
Dim URL As String
' Variables for XML-bulk
Dim strXML As String
Dim varTable As Variant
Dim intRow As Integer
Dim intCol As Integer
Dim strRowElementName As String
Dim strTableElementName As String
Dim varColumnHeaders As Variant
Dim CalcState As Long
Dim strC8 As String
Dim strComputerName As String
Dim strPrefix As String
CalcState = Application.Calculation
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
' Set custom names
strTableElementName = "postdata"
strRowElementName = "general-journal-line"
strC8 = ThisWorkbook.Sheets("SMARTapi-Opsaetning").Range("C8")
strComputerName = Environ("computername")
' Select the whole table in the current sheet
Range("A7").Select
Range(Selection, Selection.End(xlToRight)).Select
Range(Selection, Selection.End(xlDown)).Select
' Get table data
varTable = Selection.Value
varColumnHeaders = Selection.Rows(1).Value
strPrefix = "<" & strRowElementName & ">" & _
"<journal-template-name>KASSE</journal-template-name>" & _
"<journal-batch-name>" + strC8 + "</journal-batch-name>" & _
"<userid>" + strComputerName + "\" + Application.UserName + "</userid>" & _
"<account-type>G/L Account</account-type>"
' Build XML
strXML = "<" & strTableElementName & ">"
For intRow = 2 To UBound(varTable, 1)
strXML = strXML & strPrefix
For intCol = 1 To UBound(varTable, 2)
strXML = strXML & "<" & varColumnHeaders(1, intCol) & ">" & _
varTable(intRow, intCol) & "</" & varColumnHeaders(1, intCol) & ">"
Next
strXML = strXML & "</" & strRowElementName & ">"
Next
strXML = strXML & "</" & strTableElementName & ">"
Debug.Print strXML
Application.Calculation = CalcState
Application.ScreenUpdating = True
Note: I have NO idea what you're picking up from .Sheets("SMARTapi-Opsaetning").Range("C8") but I gave the variable I store it in the name strC8 - you might want to change that to something more meaningful to you.
I'll leave the Range Selection.End etc. that #Vityata talks about for you as something to look into yourself. There's no better way to learn something than researching and then doing it for yourself.
EDIT/UPDATE:
I've had a look at this, mocking up a 10,000 row, 26 column table and analysed the time taken to append the text to strXML each row and I've noticed that things really start to slow down once the strXML length exceeds 25,000 characters.
I'm sure someone here will know why, but I guess the way text is appended to a string is a new string is built copying the data from the old string together with that being appended and the longer the string is, the longer each copy takes.
When the routine I originally wrote starts, it takes a couple of a hundredths of a second to add 100 rows of data to strXML.
By the time the string is 80,000 characters in length, the time taken to add 100 more rows to strXML is 12 seconds! It gets exponentially slower.
For that reason, I suggest using an array of strings to hold your output XML, each that stops adding new data once it gets over 20,000 characters in length.
When I did this using my old i7, I could read the whole 10,000 x 26 table into the array and spit it out into the immediate window in around 3 seconds.
You'll just need to adjust the output mechanism I've build there that sends the output to the immediate window into whatever you're going to send the XML to.
Here's the adjusted code:
Dim xDOC As DOMDocument
Dim XMLHttpRequest As XMLHTTP
Dim URL As String
' Variables for XML-bulk
Dim strXML As String
Dim varTable As Variant
Dim intRow As Integer
Dim intCol As Integer
Dim strRowElementName As String
Dim strTableElementName As String
Dim varColumnHeaders As Variant
Dim CalcState As Long
Dim strC8 As String
Dim strComputerName As String
Dim strPrefix As String
Dim outputtext(10000) As String
Dim characterlimit As Long
Dim VarRw As Long
Dim VarICount As Long
characterlimit = 20000 'Don't go too much above 20,000 here or it will slow down
CalcState = Application.Calculation
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
' Set custom names
strTableElementName = "postdata"
strRowElementName = "general-journal-line"
strC8 = ThisWorkbook.Sheets("SMARTapi-Opsaetning").Range("C8")
strComputerName = Environ("computername")
' Select the whole table in the current sheet
Range("A7").Select
Range(Selection, Selection.End(xlToRight)).Select
Range(Selection, Selection.End(xlDown)).Select
' Get table data
varTable = Selection.Value
varColumnHeaders = Selection.Rows(1).Value
strPrefix = "<" & strRowElementName & ">" & _
"<journal-template-name>KASSE</journal-template-name>" & _
"<journal-batch-name>" + strC8 + "</journal-batch-name>" & _
"<userid>" + strComputerName + "\" + Application.UserName + "</userid>" & _
"<account-type>G/L Account</account-type>"
' Build XML
strXML = "<" & strTableElementName & ">"
VarRw = 0
For intRow = 2 To UBound(varTable, 1)
If Len(strXML) > characterlimit Then
outputtext(VarRw) = strXML
VarRw = VarRw + 1
strXML = ""
End If
strXML = strXML & strPrefix
For intCol = 1 To UBound(varTable, 2)
strXML = strXML & "<" & varColumnHeaders(1, intCol) & ">" & _
varTable(intRow, intCol) & "</" & varColumnHeaders(1, intCol) & ">"
Next
strXML = strXML & "</" & strRowElementName & ">"
Next
strXML = strXML & "</" & strTableElementName & ">"
outputtext(VarRw) = strXML
For VarICount = 0 To VarRw
Debug.Print outputtext(VarICount)
Next
Application.Calculation = CalcState
Application.ScreenUpdating = True
Read this at least twice: How to avoid using Select in Excel VBA
Then concentrate on this part:
Range("A7").Select
Range(Selection, Selection.End(xlToRight)).Select
Range(Selection, Selection.End(xlDown)).Select
and make sure that you rewrite the whole code, without using the words Select and Active at all. And remove the Debug.Print lines.
At the end read this - How To Speed Up VBA Code and write Application.ScreenUpdating = False somewhere on the top.
For Each cell In rng
workSheetName = Format(SaturdayIsComing(), "mm-dd-yyyy") & " " & cell.Value
If WorksheetExists(workSheetName) Then
Dim localRange, localCell As Range
Set localRange = Worksheets(workSheetName).Range("D8:D19")
Dim contents As Variant
contents = ""
Dim firstLine As Boolean
firstLine = True
For Each localCell In localRange
If Len(localCell.Value) > 0 Then
If firstLine Then
contents = contents & localCell.Value & Chr(11)
Else
contents = contents & Chr(9) & Chr(9) & Chr(9) & localCell.Value & Chr(11)
End If
Else
contents = fixString(contents)
End If
If Len(contents) > 0 Then
firstLine = False
End If
Next localCell
For Each cc In wDoc.SelectContentControlsByTag(cell.Value & "Notes")
If Len(contents) > 0 Then
cc.Range.Text = fixString(contents)
Else
cc.Range.Text = "No Issues Found"
End If
Next
Else
errorCodesString = errorCodesString & cell.Value & ":"
End If
Next cell
Output to Word
Forgot to terminate the meeting
This is a test message\'s
If my cell contains a ' then I get an error saying
One of the values passwed to this method or property is incorrect
I know a ' is a comment in VBA. How do I go around this while preserving the notes that someone had added to the Excel cell?
You need to write a piece of code to search for quotes, either the single (') or double (") variety and either add a backslash before them OR double the character so '' in place of ' and "" in place of " and run this on contents before assigning it to cc.Range.Text.
This routine can also check for other instances of incorrect strings and fix them.
Something like this would do:
Function fixString(ByVal strIn As Variant) As String
Dim i As Integer
Const strIllegals = "\'"""
For i = 1 To Len(strIllegals)
strIn = Replace(strIn, Mid$(strIllegals, i, 1), "\" & Mid$(strIllegals, i, 1))
Next i
fixString = strIn
End Function
Try changing cell.Value to Replace(cell.Value, "'", "")
Or is it contents that has the apostrophe in it? A bit confusing.
Try changing contents to Replace(contents , "'", "")
I have the following code and need to write a header into the file. I am not sure how i go about adding the code so that i can write a header to the file.
My code is as follows:-
Private Enum eCommaSemiColon
Comma = 0
Semicolon = 1
End Enum
Dim lFile As Long
Dim lRow As Long
Dim s As String
Dim lCounter As Long
If Me.CurrentInterval = 1 And Me.CurrentTrial = 1 Then
lFile = FreeFile
Open Me.FileName.Value For Output As #lFile
For lRow = 0 To Me.FileContents.Count - 1
Select Case Me.CommaOrSemiColon.Value
Case eCommaSemicolon.Comma
For lCounter = 0 To Me.NoOfColumns.Value - 1
If lCounter = 0 Then
s = Me.FileContents.Get(lRow, 0)
Else
s = s & "," & Me.FileContents.Get(lRow, lCounter)
End If
Next
Print #lFile, s
Case eCommaSemicolon.Semicolon
For lCounter = 0 To Me.NoOfColumns.Value - 1
If lCounter = 0 Then
s = Replace(CStr(Me.FileContents.Get(lRow, 0)), ".", ",")
Else
s = s & ";" & Replace(CStr(Me.FileContents.Get(lRow, lCounter)), ".", ",")
End If
Next
Print #lFile, s
Case Else
End Select
Next
Close #lFile
Else
End If
At some point before you print to the file, you will simply have to prefix the header string to the s variable. Then print the file. It is that easy
Assuming it's tab-delimited (modify if needed) something like this will create a header string:
Dim header as String
header = "Time" & vbTab & "Number1" & vbTab & "Number2" & vbTab & ...
Then, before you print to file, prefix the header string to the s (which contains the full file text), separating with a Line Feed:
s = header & vbCRLF & s
Now, the s variable should contain the header string and the rest of the file text, so you can use your normal I/O print statement to write the fule.
Sorry for the two fold question in one post.
This indirectly relates to a question I posted recently here: vba: return page number from selection.find using text from array which was solved
Program purpose:
Firstly: add a footer with custom page numbers to documents (i.e. 0.0.0, Chapter.Section,Page representative) in a selected folder and sub folders.
Secondly: create a TOC with the custom page numbers saved as roottoc.docx in the root folder selected.
I now have two new problems before I can fully clean and finally put this to bed, I will post the full code at the end of this post.
Solved First of all, from what I have discovered and just read elsewhere too the getCrossReferenceItems(refTypeHeading) method will only return the text upto a certain length from what of finds. I have some pretty long headings which means this is quite an annoyance for the purpose of my code. So the first question I have is is there something I can do with the getCrossReferenceItems(refTypeHeading) method to force it to collect the full text from any referenced headings or is there an alternative way round this problem.
Solved Secondly the createOutline() function when called in ChooseFolder() produces the correct results but in reverse order, could someone point the way on this one too please.
Unfortunately the actual results I am recieving will be difficulty to exactly replicate but if a folder is made containing a couple of documents with various headings. The directory name should be the the same as what is in the Unit Array i.e. Unit(1) "Unit 1", the file names are made up of two parts i.e. Unit(1) & " " & Criteria(1) & ext becoming "Unit 1 p1.docx" etc, the arrays Unit and Criteria are in the ChooseFolder Sub. chapArr is a numerical representative of the Unit array contents soley for my page numbering system, I used another array because of laziness at this point in time. I could have used some other method on the Unit array to achieve the same result which I might look at when cleaning up.
When running the ChooseFolder Sub if the new folder with documents in is located in My Document then My Documents will be the folder to locate and select in the file dialogue window. This should produce results that are similar and will give an example of what I am talking about.
Complete code:
Public Sub ChooseFolder()
'Declare Variables
'|Applications|
Dim doc As Word.Document
'|Strings|
Dim chapNum As String
Dim sResult As String
Dim Filepath As String
Dim strText As String
Dim StrChapSec As String
'|Integers|
Dim secNum As Integer
Dim AckTime As Integer
Dim FolderChosen As Integer
'|Arrays|
Dim Unit() As Variant
Dim ChapArray() As Variant
Dim Criteria() As Variant
'|Ranges|
Dim rng As Range
'|Objects|
Dim InfoBox As Object
'|Dialogs|
Dim fd As FileDialog
'Constants
Const ext = ".docx"
'Set Variable Values
secNum = 0 'Set Section number start value
AckTime = 1 'Set the message box to close after 1 seconds
Set InfoBox = CreateObject("WScript.Shell") 'Set shell object
Set fd = Application.FileDialog(msoFileDialogFolderPicker) 'Set file dialog object
FolderChosen = fd.Show 'Display file dialogue
'Set Array Values
'ToDo: create form to set values for Arrays
'Folder names
Unit = Array("Unit 1", "Unit 2")
'Chapter Numbers
chapArr = Array("1", "2")
'Document names
Criteria = Array("P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P9", "M1", "M2", "M3", "M4", "D1", "D2", "D3")
If FolderChosen <> -1 Then
'didn't choose anything (clicked on CANCEL)
MsgBox "You chose cancel"
Else
'Set sResult equal to selected file/folder in file dialogue
sResult = fd.SelectedItems(1)
End If
' Loop through unit array items
For i = LBound(Unit) To UBound(Unit)
unitName = Unit(i)
' Test unit folder being looked at and concatenate sResult with
' unitName delimited with "\"
If unitName = "Unit 105" Then
Filepath = sResult & "\unit 9"
Else
Filepath = sResult & "\" & unitName
End If
' Loop through criteria array items
For j = LBound(Criteria) To UBound(Criteria)
criteriaName = Criteria(j)
' Set thisFile equal to full file path
thisfile = Filepath & "\" & unitName & " " & criteriaName & ext 'Create file name by concatenating filePath with "space" criteriaName and ext
' Test if file exists
If File_Exists(thisfile) = True Then
' If file exists do something (i.e. process number of pages/modify document start page number)
' Inform user of file being processed and close popup after 3 seconds
Select Case InfoBox.Popup("Processing file - " & thisfile, AckTime, "This is your Message Box", 0)
Case 1, -1
End Select
' Open document in word using generated filePath in read/write mode
' Process first section footer page number and amend to start as intPages (total pages) + 1
Set doc = Documents.Open(thisfile)
With doc
With ActiveDocument.Sections(1)
chapNum = chapArr(i)
secNum = secNum + 1
' Retrieve current footer text
strText = .Footers(wdHeaderFooterPrimary).Range.Text
.PageSetup.DifferentFirstPageHeaderFooter = False
' Set first page footer text to original text
.Footers(wdHeaderFooterFirstPage).Range.Text = strText
' Set other pages footer text
.Footers(wdHeaderFooterPrimary).Range.Text = Date & vbTab & "Author: Robert Ells" & vbTab & chapNum & "." & secNum & "."
Set rng = .Footers(wdHeaderFooterPrimary).Range.Duplicate
rng.Collapse wdCollapseEnd
rng.InsertBefore "{PAGE}"
TextToFields rng
End With
ActiveDocument.Sections(1).Footers(1).PageNumbers.StartingNumber = 1
Selection.Fields.Update
Hide_Field_Codes
ActiveDocument.Save
CreateOutline sResult, chapNum & "." & secNum & "."
End With
Else
'If file doesn't exist do something else (inform of non existant document and close popup after 3 seconds
Select Case InfoBox.Popup("File: " & thisfile & " - Does not exist", AckTime, "This is your Message Box", 0)
Case 1, -1
End Select
End If
Next
Filepath = ""
secNum = 0
Next
End Sub
Private Function TextToFields(rng1 As Range)
Dim c As Range
Dim fld As Field
Dim f As Integer
Dim rng2 As Range
Dim lFldStarts() As Long
Set rng2 = rng1.Duplicate
rng1.Document.ActiveWindow.View.ShowFieldCodes = True
For Each c In rng1.Characters
DoEvents
Select Case c.Text
Case "{"
ReDim Preserve lFldStarts(f)
lFldStarts(f) = c.Start
f = f + 1
Case "}"
f = f - 1
If f = 0 Then
rng2.Start = lFldStarts(f)
rng2.End = c.End
rng2.Characters.Last.Delete '{
rng2.Characters.First.Delete '}
Set fld = rng2.Fields.Add(rng2, , , False)
Set rng2 = fld.Code
TextToFields fld.Code
End If
Case Else
End Select
Next c
rng2.Expand wdStory
rng2.Fields.Update
rng1.Document.ActiveWindow.View.ShowFieldCodes = True
End Function
Private Function CreateOutline(Filepath, pgNum)
' from https://stackoverflow.com/questions/274814/getting-the-headings-from-a-word-document
'Declare Variables
'|Applications|
Dim App As Word.Application
Dim docSource As Word.Document
Dim docOutLine As Word.Document
'|Strings|
Dim strText As String
Dim strFileName As String
'|Integers|
Dim intLevel As Integer
Dim intItem As Integer
Dim minLevel As Integer
'|Arrays|
Dim strFootNum() As Integer
'|Ranges|
Dim rng As Word.Range
'|Variants|
Dim astrHeadings As Variant
Dim tabStops As Variant
'Set Variable values
Set docSource = ActiveDocument
If Not FileLocked(Filepath & "\" & "roottoc.docx") Then
If File_Exists(Filepath & "\" & "roottoc.docx") Then
Set docOutLine = Documents.Open(Filepath & "\" & "roottoc.docx", ReadOnly:=False)
Else
Set docOutLine = Document.Add
End If
End If
' Content returns only the
' main body of the document, not
' the headers and footer.
Set rng = docOutLine.Content
minLevel = 5 'levels above this value won't be copied.
astrHeadings = returnHeaderText(docSource) 'docSource.GetCrossReferenceItems(wdRefTypeHeading)
docSource.Select
ReDim strFootNum(0 To UBound(astrHeadings))
For i = 1 To UBound(astrHeadings)
With Selection.Find
.Text = Trim(astrHeadings(i))
.Wrap = wdFindContinue
End With
If Selection.Find.Execute = True Then
strFootNum(i) = Selection.Information(wdActiveEndPageNumber)
Else
MsgBox "No selection found", vbOKOnly 'Or whatever you want to do if it's not found'
End If
Selection.Move
Next
docOutLine.Select
With Selection.Paragraphs.tabStops
'.Add Position:=InchesToPoints(2), Alignment:=wdAlignTabLeft
.Add Position:=InchesToPoints(6), Alignment:=wdAlignTabRight, Leader:=wdTabLeaderDots
End With
For intItem = LBound(astrHeadings) To UBound(astrHeadings)
' Get the text and the level.
' strText = Trim$(astrHeadings(intItem))
intLevel = GetLevel(CStr(astrHeadings(intItem)))
' Test which heading is selected and indent accordingly
If intLevel <= minLevel Then
If intLevel = "1" Then
strText = " " & Trim$(astrHeadings(intItem)) & vbTab & pgNum & strFootNum(intItem) & vbCr
End If
If intLevel = "2" Then
strText = " " & Trim$(astrHeadings(intItem)) & vbTab & pgNum & strFootNum(intItem) & vbCr
End If
If intLevel = "3" Then
strText = " " & Trim$(astrHeadings(intItem)) & vbTab & pgNum & strFootNum(intItem) & vbCr
End If
If intLevel = "4" Then
strText = " " & Trim$(astrHeadings(intItem)) & vbTab & pgNum & strFootNum(intItem) & vbCr
End If
If intLevel = "5" Then
strText = " " & Trim$(astrHeadings(intItem)) & vbTab & pgNum & strFootNum(intItem) & vbCr
End If
' Add the text to the document.
rng.Collapse (False)
rng.InsertAfter strText & vbLf
docOutLine.SelectAllEditableRanges
' tab stop to set at 15.24 cm
'With Selection.Paragraphs.tabStops
' .Add Position:=InchesToPoints(6), _
' Leader:=wdTabLeaderDots, Alignment:=wdAlignTabRight
' .Add Position:=InchesToPoints(2), Alignment:=wdAlignTabCenter
'End With
rng.Collapse (False)
End If
Next intItem
docSource.Close
docOutLine.Save
docOutLine.Close
End Function
Function returnHeaderText(doc As Word.Document) As Variant
Dim returnArray() As Variant
Dim para As Word.Paragraph
Dim i As Integer
i = 0
For Each para In doc.Paragraphs
If Left(para.Style, 7) = "Heading" Then
ReDim Preserve returnArray(i)
returnArray(i) = para.Range.Text
i = i + 1
End If
Next
returnHeaderText = returnArray
End Function
Function FileLocked(strFileName As String) As Boolean
On Error Resume Next
' If the file is already opened by another process,
' and the specified type of access is not allowed,
' the Open operation fails and an error occurs.
Open strFileName For Binary Access Read Write Lock Read Write As #1
Close #1
' If an error occurs, the document is currently open.
If Err.Number <> 0 Then
' Display the error number and description.
MsgBox "Error #" & Str(Err.Number) & " - " & Err.Description
FileLocked = True
Err.Clear
End If
End Function
Private Function GetLevel(strItem As String) As Integer
' from https://stackoverflow.com/questions/274814/getting-the-headings-from-a-word-document
' Return the heading level of a header from the
' array returned by Word.
' The number of leading spaces indicates the
' outline level (2 spaces per level: H1 has
' 0 spaces, H2 has 2 spaces, H3 has 4 spaces.
Dim strTemp As String
Dim strOriginal As String
Dim intDiff As Integer
' Get rid of all trailing spaces.
strOriginal = RTrim$(strItem)
' Trim leading spaces, and then compare with
' the original.
strTemp = LTrim$(strOriginal)
' Subtract to find the number of
' leading spaces in the original string.
intDiff = Len(strOriginal) - Len(strTemp)
GetLevel = (intDiff / 2) + 1
End Function
Private Function File_Exists(ByVal sPathName As String, Optional Directory As Boolean) As Boolean
'Returns True if the passed sPathName exist
'Otherwise returns False
On Error Resume Next
If sPathName <> "" Then
If IsMissing(Directory) Or Directory = False Then
File_Exists = (Dir$(sPathName) <> "")
Else
File_Exists = (Dir$(sPathName, vbDirectory) <> "")
End If
End If
End Function
Sub Hide_Field_Codes()
Application.ActiveWindow.View.ShowFieldCodes = False
End Sub
Kevin's Solutions:
Question part 1, Answer
I thought initially that something went wrong when I added your function, but it was due to a blank heading on the following line after the actual heading in the documents. I suppose an If statement to test if there is text present could solve this. :-)
I haven't tested this bit yet (due to being tired), but if the heading is inline with normal text, would this function pick up only the heading or both heading and normal text?
Question part 2, Answer
Just worked, although with one niggle (the list produced is no longer indented as desired in the main CreateOutline function). Time is getting on now so will have to pick this up again tomorrow :-)
Thanks yet again kevin, this is where I should have concentrated more during programming at uni instead of thinking about the pub.
Phil :-)
welcome back! :-)
For the reversed data from the CreateOutline function - change your Collapse function to have a false parameter. Collapse defaults to putting the cursor at the beginning of the selection, but this will put it at the end so you're adding to the end of the doc instead of the beginning:
' Add the text to the document.
rng.Collapse(False) 'HERE'
rng.InsertAfter strText & vbLf
docOutLine.SelectAllEditableRanges
rng.Collapse(False) 'AND HERE'
For the CrossReferenceItems issue, try this and let me know if there's any data missing from what it returns. Call this instead of the CrossReferenceItems method:
Function returnHeaderText(doc As Word.Document) As Variant
Dim returnArray() As Variant
Dim para As Word.Paragraph
Dim i As Integer
i = 0
For Each para In doc.Paragraphs
If Left(para.Style, 7) = "Heading" Then
ReDim Preserve returnArray(i)
returnArray(i) = para.Range.Text
i = i + 1
End If
Next
returnHeaderText = returnArray
End Function
Basically what we have here
Getting the headings from a Word document
Public Sub CreateOutline()
Dim docOutline As Word.Document
Dim docSource As Word.Document
Dim rng As Word.Range
Dim astrHeadings As Variant
Dim strText As String
Dim intLevel As Integer
Dim intItem As Integer
Set docSource = ActiveDocument
Set docOutline = Documents.Add
' Content returns only the
' main body of the document, not
' the headers and footer.
Set rng = docOutline.Content
astrHeadings = _
docSource.GetCrossReferenceItems(wdRefTypeHeading)
For intItem = LBound(astrHeadings) To UBound(astrHeadings)
' Get the text and the level.
strText = Trim$(astrHeadings(intItem))
intLevel = GetLevel(CStr(astrHeadings(intItem)))
' Add the text to the document.
rng.InsertAfter strText & vbNewLine
' Set the style of the selected range and
' then collapse the range for the next entry.
rng.Style = "Heading " & intLevel
rng.Collapse wdCollapseEnd
Next intItem
End Sub
Private Function GetLevel(strItem As String) As Integer
' Return the heading level of a header from the
' array returned by Word.
' The number of leading spaces indicates the
' outline level (2 spaces per level: H1 has
' 0 spaces, H2 has 2 spaces, H3 has 4 spaces.
Dim strTemp As String
Dim strOriginal As String
Dim intDiff As Integer
' Get rid of all trailing spaces.
strOriginal = RTrim$(strItem)
' Trim leading spaces, and then compare with
' the original.
strTemp = LTrim$(strOriginal)
' Subtract to find the number of
' leading spaces in the original string.
intDiff = Len(strOriginal) - Len(strTemp)
GetLevel = (intDiff / 2) + 1
End Function
but I need the page number for each heading too.
I tried doing a search for each heading, select the search result and retrieve the wdActiveEndPageNumber.
This didn't work, was slow and is sure an ugly approach.
I'd like to paste the found stuff into another word document like:
rng.InsertAfter "Page: " & pageNum & " Header: " & strText & vbNewLine
I may not understand the question, then, but this code goes through the document, looking for lines that are only headers and gets the page its on.
Public Sub SeeHeadingPageNumber()
On Error GoTo MyErrorHandler
Dim sourceDocument As Document
Set sourceDocument = ActiveDocument
Dim myPara As Paragraph
For Each myPara In sourceDocument.Paragraphs
myPara.Range.Select 'For debug only
If InStr(LCase$(myPara.Range.Style.NameLocal), LCase$("heading")) > 0 Then
Debug.Print myPara.Range.Information(wdActiveEndAdjustedPageNumber)
End If
DoEvents
Next
Exit Sub
MyErrorHandler:
MsgBox "SeeHeadingPageNumber" & vbCrLf & vbCrLf & "Err = " & Err.Number & vbCrLf & "Description: " & Err.Description
End Sub
Try using a Table of Content field. The following code dissects a TOC and gives you the item, page number and style. You might have to parse each string to get the exact info or formatting you need.
Public Sub SeeTOCInfo()
On Error GoTo MyErrorHandler
Dim sourceDocument As Document
Set sourceDocument = ActiveDocument
Dim myField As Field
For Each myField In sourceDocument.TablesOfContents(1).Range.Fields
Debug.Print Replace(myField.Result.Text, Chr(13), "-") & " " & " Type: " & myField.Type
If Not myField.Result.Style Is Nothing Then
Debug.Print myField.Result.Style
End If
DoEvents
Next
Exit Sub
MyErrorHandler:
MsgBox "SeeTOCInfo" & vbCrLf & vbCrLf & "Err = " & Err.Number & vbCrLf & "Description: " & Err.Description
End Sub
This will insert the page number of the referenced Heading:
rng.InsertCrossReference ReferenceType:=wdRefTypeHeading, _
ReferenceKind:=wdPageNumber, ReferenceItem:=intItem
But only works if you're inserting in the same document. You could insert in the current document and then cut/paste out to a new document.