PDA

View Full Version : Solved: Starting a "clean" Winword process



Frosty
06-19-2012, 11:04 AM
I want to start a new "clean" winword process (essentially, the same as running winword /a from the Run command), from within Word VBA. Below is sort of the best I can do. Any ideas on how to eliminate the need to pause execution via messagebox? Timer functions, DoEvents within Do Loops until GetObject returns something other than nothing don't seem to prevent endless loops.

It might be a matter of fresh eyes... here is the code I have thus far...
Note: stepping through the code doesn't require the msgbox klugey "pause" ... but running the code without the messagebox doesn't give enough "time" between the shell and the GetObject to allow GetObject to succeed.

'----------------------------------------------------------------------------------------------------------
' Demo starting and stopping the process
'----------------------------------------------------------------------------------------------------------
Public Sub StartAndStop()
Dim oApp As Word.Application
'get the clean app process
Set oApp = fGetNewWinword(True)

'now close it
fCloseApp oApp
End Sub
'----------------------------------------------------------------------------------------------------------
' Return a separate Winword process, with no addins
'----------------------------------------------------------------------------------------------------------
Public Function fGetNewWinword(Optional bPauseExecution As Boolean = True) As Object

'use the shell process to launch a process of word with no addins
Shell "winword /a", vbHide

'we need to wait for the Shell command to finish processing
If bPauseExecution Then
MsgBox "Pause the script to allow GetObject to work.", vbInformation, "fGetNewWinword"
End If
'since a "clean" process of Word always starts with "Document1", this should be enough
Set fGetNewWinword = GetObject("Document1").Application
End Function
'----------------------------------------------------------------------------------------------------------
' End the passed Winword process, making sure it is not *this* winword process
'----------------------------------------------------------------------------------------------------------
Public Function fCloseApp(oApp As Object, Optional bIgnoreThisApp As Boolean = True) As Boolean
'if we ignore this app, let's make sure it's not the same one
If bIgnoreThisApp Then
'if the active document isn't the same, or the window count is different
'this seems to be a reasonable test not to close this app
If ActiveDocument.FullName <> oApp.ActiveDocument.FullName _
Or Windows.Count <> oApp.Windows.Count Then
oApp.ActiveDocument.Saved = True
oApp.Quit
Set oApp = Nothing
End If
End If
End Function

dipique
06-19-2012, 01:14 PM
Depending on how important this is, I have one extremely messy idea:
1. Loop through Word add-ins pre-initialization, disable those you don't want, then re-enable them after the process has started.
2. Use the CreateObject method to get around the waiting requirements.

#2 is easy (Set objWord = CreateObject("Word.Application")), but #1 is harder. I found this link (http://us.generation-nt.com/answer/how-start-word-without-com-addins-help-59551122.html) that might provide some guidance.

If you have issues with ghost applications laying around (invisible word processes that won't go away), you can get rid of them by looping through process IDs, checking if they're Word applications, and killing them if they're not the current process ID. Again, a messy process, but here's a link (http://www.xtremevbtalk.com/showthread.php?t=298539) to offer some ideas in that arena.

Let me know if that's a direction that looks hopeful and I'll be happy to poke around with you.

Hopefully that gives you some ideas.

Dan

dipique
06-19-2012, 01:19 PM
On another note, Word doesn't always start with a document named "document1". Proof of concept: in Windows 7, open a new document with Winword (it will be named "document1"). Now, shift-click the open application's icon in the task bar to open a new process. The new process will open as "document2". The same behavior can be observed by running Run->"Winword" twice without closing the first occurance.

Dan

Frosty
06-19-2012, 01:39 PM
Thanks, Dan... I actually ran across both those links in my own searching.

The purpose of using Shell instead of CreateObject is to allow using the /a switch. Even if you don't load up addins, you still end up using the base "Normal" template as the original shell of a new document (which is the main thing I want to avoid, although not having addins loaded is useful to avoid any document events programmed into any addins, which can complicate and/or slow down subsequent processing)

When creating a new macro template, you generally don't want to have it based on your existing normal, but rather a "clean" normal (the one that Word creates if it doesn't find a normal.dot/.dotm), so that you aren't left with any extra styles/listtemplates and various other things that get added to Normal over time (and essentially cause document bloat).

I'm attempting to write my own "RecreateProject" code which involves:
1. Exporting all the code in a project
2. Creating a "clean" document shell
3. Importing the code into the new document shell
4. Recreating any programmatic references from the original project
5. Compiling the project and saving.

I'm not worrying about UI elements, at the moment, because I know that will be a pain, and I'm not even sure if I'm going to have the UI elements in the main code library. My general thought is that I'd like to separate out UI, CodeLibrary and Autotext/BuildingBlocks/Styles into 3 separate global addins.

Most of the time spent on this kind of manual recreation is in doing the code and the programmatic references. In 2007/2010 the ribbon stuff is not that onerous, since it's really just a manual copy/paste, although getting custom icons in can be a pain, but that's a separate issue

As for the "Document1" issue... you're right, sort of. Word creates a list when it starts of "acceptable" first names for a new document. And then it obviously updates that list as new documents are created. But while separate processes will talk to that central list, it appears that the /a switch allows restarting the list, if no documents are currently open and utilizing the name.

If you do Start > Run > Winword /a -- even with an existing process of Word open, you will get the first document as "Document1" -- as long as no existing processes of Word currently have a Document1 open.

The benefit of using the Shell command to do this, means that as long as no Document1 exists in any open Winword processes, the document created will be a document1. At least, this has true in my rudimentary testing and my vague memory of how Word creates that list. My testing is done this way...

1. Start > Run > Winword /a -- document1 is created (first winword process created)
2. Start > Run > Winword /a -- document2 is created (2nd winword process created, but it doesn't also use Document1)
3. Close document1 in the original process, but leave the actual application running
4. Start > Run > winword /a -- document1 is created (3rd winword process created, but restarts the list)

The "Document1" trick seemed to be the only way to easily use GetObject to retrieve a specific instance of Word. Everything else seems a lot more complicated, and I wasn't able to find any specific code samples to shorten what would be a fairly involved process of getting PID handles and somehow converting them to an application object (since GetObject will always return the earliest process created-- not a solution when using one application to create a new application of the same process type).

And ultimately, since I could remove the need for doing any of this simply by initially starting Word using the winword /a process, opening my "Recompiler" project and then running there, I didn't want to spend *too* much time on it, but it would be a useful thing to be able to create a "clean new" word document without having to close word, reopen with a special switch, save the new document, then close word back down...etc.

Thanks again for the thoughts!

Frosty
06-19-2012, 01:41 PM
Further note: I just tried your Shift+Click on the task bar in Win 7... that doesn't open up a new process (check Task Manager), but simply creates a new document in the existing process, so even if Document1 has been closed, the iteration is continued (so if you've created Document2, Document3, Document4... the Shift+Click Document will be Document5, but a winword /a will be Document1.

Is that the same for you?

Frosty
06-19-2012, 01:51 PM
Here's a demo of the way the code works... even with a fairly small normal, you can see the difference in file size between the two processes. However, I don't have a better method than the messagebox kluge to make it seamless...

Run the Demo_TwoNewDocs sub, and check the file size of the two documents...

Option Explicit
'----------------------------------------------------------------------------------------------------------
' Demo comparing the two different ways of getting documents
'----------------------------------------------------------------------------------------------------------
Private Sub Demo_TwoNewDocs()
Dim oCleanDoc As Document
Dim oNewDoc As Document

Set oCleanDoc = fCreateCleanNewDocument("C:\Temp\Clean.dotm", wdFormatXMLTemplateMacroEnabled)

Set oNewDoc = Documents.Add
oNewDoc.SaveAs2 "C:\Temp\JustNew.dotm", wdFormatXMLTemplateMacroEnabled
'close them both
oCleanDoc.Close
oNewDoc.Close
End Sub
'----------------------------------------------------------------------------------------------------------
' Demo starting and stopping the process
'----------------------------------------------------------------------------------------------------------
Private Sub Demo_StartAndStop()
Dim oApp As Word.Application
'get the clean app process
Set oApp = fGetNewWinword

'now close it
fCloseApp oApp
End Sub
'----------------------------------------------------------------------------------------------------------
' Return a document opened in this application, by creating in a "clean" application
'----------------------------------------------------------------------------------------------------------
Public Function fCreateCleanNewDocument(sDocFullName As String, _
Optional lSaveFormat As WdSaveFormat = wdFormatXMLDocument, _
Optional bOverwriteExisting As Boolean = True) As Document
Dim oNewDoc As Document
Dim oCleanApp As Word.Application

'get a clean app
Set oCleanApp = fGetNewWinword

'now use the clean app to create the document, save it, and close it
With oCleanApp
Set oNewDoc = .Documents.Add
If bOverwriteExisting Then
On Error Resume Next
Kill sDocFullName
'Kill sDocFullName & fFileExtension(lSaveFormat)
On Error GoTo 0
End If
oNewDoc.SaveAs2 FileName:=sDocFullName, fileformat:=lSaveFormat
oNewDoc.Close
End With

'now close that clean app
fCloseApp oCleanApp

'now open the new clean doc in this app's process
Set oNewDoc = Documents.Open(sDocFullName) ' & fFileExtension(lSaveFormat))

'and return it
Set fCreateCleanNewDocument = oNewDoc

End Function
'----------------------------------------------------------------------------------------------------------
' Get the file extension of the save format (including the period) of the passed save format
'----------------------------------------------------------------------------------------------------------
Public Function fFileExtension(lSaveFormat As WdSaveFormat) As String
Dim sRet As String
Select Case lSaveFormat
Case wdFormatDocument, wdFormatDocument97
sRet = ".doc"
Case wdFormatTemplate, wdFormatTemplate97
sRet = ".dot"
Case wdFormatXMLDocument
sRet = ".docx"
Case wdFormatXMLDocumentMacroEnabled
sRet = ".docm"
Case wdFormatXMLTemplate
sRet = ".dotx"
Case wdFormatXMLTemplateMacroEnabled
sRet = ".dotm"
Case wdFormatPDF
sRet = ".pdf"
Case wdFormatRTF
sRet = ".rtf"
Case wdFormatXML
sRet = ".xml"
Case wdFormatDOSText, wdFormatDOSTextLineBreaks, wdFormatText, wdFormatTextLineBreaks
sRet = ".txt"
Case wdFormatHTML
sRet = ".html"
Case Else
sRet = ""
End Select
fFileExtension = sRet
End Function
'----------------------------------------------------------------------------------------------------------
' Return a separate Winword process, with no addins
'----------------------------------------------------------------------------------------------------------
Public Function fGetNewWinword(Optional bPauseExecution As Boolean = True) As Object

'use the shell process to launch a process of word with no addins
Shell "winword /a", vbHide

'we need to wait for the Shell command to finish processing
If bPauseExecution Then
MsgBox "Pause the script to allow GetObject to work.", vbInformation, "fGetNewWinword"
End If
'since a "clean" process of Word always starts with "Document1", this should be enough
Set fGetNewWinword = GetObject("Document1").Application
End Function
'----------------------------------------------------------------------------------------------------------
' End the passed Winword process, making sure it is not *this* winword process
'----------------------------------------------------------------------------------------------------------
Public Function fCloseApp(oApp As Object, Optional bIgnoreThisApp As Boolean = True) As Boolean
'if we ignore this app, let's make sure it's not the same one
If bIgnoreThisApp Then
'if the active document isn't the same, or the window count is different
'this seems to be a reasonable test not to close this app
If ActiveDocument.FullName <> oApp.ActiveDocument.FullName _
Or Windows.Count <> oApp.Windows.Count Then
oApp.ActiveDocument.Saved = True
oApp.Quit
Set oApp = Nothing
End If
End If
End Function

fumei
06-19-2012, 09:43 PM
When creating a new macro template, you generally don't want to have it based on your existing normal, but rather a "clean" normal (the one that Word creates if it doesn't find a normal.dot/.dotm), All the more reason to not have ANYTHING in Normal.

I don't. My Normal is "empty". No code. No custom styles whatsoever. It is "clean".

I do agree though that new macro templates should, in principle, be based on as clean a normal as possible.

dipique
06-20-2012, 04:58 AM
Hmm. All right, one more idea for you... what about doing this from a VBScript file rather than a word macro? Then you can rename normal.dotm, run the macro with the CreateObject function (eliminating the pause), close the application, and place the normal.dotm template back in place.

Frosty
06-20-2012, 08:23 AM
Well the problem is actually with GetObject and trying to get a second instance, since CreatObject doesn't allow the addins switch. I agree with Fumei that normal should be kept clear, but I'm trying to write this for use in a couple of different environments, so not all of those decisions are up to me.
I could solve this by transferring the code to excel VBA too. But the goal is simply to eliminate a spurious msgbox in a developer-only function, so that doesn't seem worth the translation effort.

I appreciate the brainstorms though.

dipique
06-20-2012, 09:53 AM
EDIT: Never mind--this works if it has already failed once because the process is already open, but fails from a clean start.



One more idea: how about using the Sleep method?


Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

'--------------------------------------------------------------------------
Public Function fGetNewWinword() As Object
'GetObject will encounter an error if the function hasn't worked
On Error GoTo TryAgain

'use the shell process to launch a process of word with no addins
Shell "winword /a", vbHide

'This integer will be used to make sure we don't get stuck in a loop
Dim i As Integer

'This is where we attempt to get the new application
GetWord:
Set fGetNewWinword = GetObject("Document1").Application

'This will tell you how many time we had to sleep, in case you're interested
'MsgBox i

'If it was successful (i.e. didn't cause an error) then we're done.
Exit Function

'We encountered an error, so let's wait and try again
TryAgain:

'Wait for 50ms (about 1/20th of a second)
Sleep 50

'Increment the counter
i = i + 1

'If we've already waited 5 seconds, register a timeout and bail
If i > 100 Then
MsgBox "Time Out"
Exit Function
End If

'If we've waited less than 5 seconds, try to get the application again.
GoTo GetWord
End Function

Note: the "PtrSafe" in the original declaration is only necessary because I'm using 64 bit office.

...And that's my last idea.

Dan

dipique
06-20-2012, 10:01 AM
Further note: I just tried your Shift+Click on the task bar in Win 7... that doesn't open up a new process (check Task Manager), but simply creates a new document in the existing process, so even if Document1 has been closed, the iteration is continued (so if you've created Document2, Document3, Document4... the Shift+Click Document will be Document5, but a winword /a will be Document1.

Is that the same for you?

It sure is! I thought it opened in a separate process because that's how I open a new window (and a new process) in Excel, where by default everything opens in the same window (and process).

Huh.

Dan

Frosty
06-20-2012, 10:34 AM
Hmm... I had thought of Sleep, but didn't try it. It doesn't work either... but testing it did lead me to the actual solution. It appears that use of the shell command causes the other process (even started minimized) to get focus. The end-result is that I didn't need the msgbox, I just needed to click my mouse somewhere in the original process... which lead to this code being the "working" model. Still left myself an out, but in my testing it appears to get the right instance of Word within a few tries at most...

I swapped some things around to use Early Binding... although that didn't have any effect. It was really Application.Activate that solved the main problem. Learn something new every day... thanks for the help!

Here's the working code and a demo of it... I'm also going to post a .docm version of the project if the other code is perhaps of use to anyone. It's not fully debugged (although I've used it on both large and small projects and it seems pretty solid thus far after a day or two of testing), so use at your own risk :)

'----------------------------------------------------------------------------------------------------------
' Demo starting and stopping the process
'----------------------------------------------------------------------------------------------------------
Public Sub Demo_StartAndStopApp()
Dim oApp As Word.Application
'get the clean app process
Set oApp = fGetNewWinword

'now close it
fCloseApp oApp
End Sub
'----------------------------------------------------------------------------------------------------------
' Return a separate Winword process, with no addins
'----------------------------------------------------------------------------------------------------------
Public Function fGetNewWinword(Optional bPauseExecution As Boolean = False) As Word.Application
Dim oApp As Word.Application
Dim i As Long

'use the shell process to launch a process of word with no addins
Shell "winword /a", vbHide

'since the shell command makes the new app primary, it sends us into an endless loop
'until we interact with this application.
'This seems to solve...
Application.Activate

'An option to wait for the Shell command to finish processing
If bPauseExecution Then
MsgBox "Pause the script to allow GetObject to work.", vbInformation, "fGetNewWinword"
End If
'this is the number of attempts to try... in testing the highest I saw was 37
For i = 1 To 100
'this doesn't appear necessary, but it does seem to lessen the average number of iterations
DoEvents
'since a "clean" process of Word always starts with "Document1", this should be enough
On Error Resume Next
Set oApp = GetObject("Document1").Application
On Error GoTo 0
'if we've found it, we're done
If Not oApp Is Nothing Then
Exit For
End If
Next
'in case we're interested in how many loops are needed
Debug.Print i

Set fGetNewWinword = oApp
End Function
'----------------------------------------------------------------------------------------------------------
' End the passed Winword process, making sure it is not *this* winword process
'----------------------------------------------------------------------------------------------------------
Public Function fCloseApp(oApp As Word.Application, _
Optional bIgnoreThisApp As Boolean = True) As Boolean
'if we ignore this app, let's make sure it's not the same one
If bIgnoreThisApp Then
'if the active document isn't the same, or the window count is different
'this seems to be a reasonable test not to close this app
If ActiveDocument.FullName <> oApp.ActiveDocument.FullName _
Or Windows.Count <> oApp.Windows.Count Then
oApp.ActiveDocument.Saved = True
oApp.Quit
Set oApp = Nothing
End If
End If
End Function

Frosty
06-20-2012, 10:48 AM
The only thing to add to this to make it really robust would be to determine if there are any winword processes currently open with a Document1 also open. But that's the kind of thing I would add if I were trying to deploy this to end-users. But that's probably not worth it, since anyone who is using this would be able to troubleshoot that kind of item.

Again: thanks for the help!

dipique
06-20-2012, 11:26 AM
I'm glad you got this sorted out. Imagine how easy your job would be if computers would tell you what the problem was so you could focus on fixing it!