I'm writing a script to open documents using the default program via the Windows shell based on this SO answer:
Dim Shex As Object
Set Shex = CreateObject("Shell.Application")
tgtfile = "C:\Nax\dud.txt"
Shex.Open (tgtfile)
I notice the instance of Shell.Application never gets closed. In my code, I Set Shex = Nothing, but is that enough? If I create a Word or Outlook instance, for example, I would need to close it with .Quit before setting the variable to nothing. There's nothing obviously analogous going on here.
I set a reference to Microsoft Shell Controls and Automation to explore the Shell object, but couldn't find any methods for the .Application or .Parent properties, let alone one that looked like .Quit.
Am I missing something obvious? Does the garbage collector somehow also get rid of the instance? Is it something specific to the shell object itself?
Thinking about it, I'm pretty sure #jamheadart is right and I'm just instancing a VBA class rather than creating an application class in Windows.
To be sure, though, I'm taking #Mert Y's suggestion of using a context manager to limit scope.
Final code:
With CreateObject("Shell.Application")
.Open (strPath)
End With
Related
Good morning.
I am having a problem with getting my code to find other instances of word and have hit a brick wall after much google searching.
My Code below will find all open word documents and populate them into a combo box.
My problem is we have applications (I have no control over these) that will open word documents in a new instance and therefore my code will not find/control these documents.
Any ideas?
Dim objWordDocument As Word.Document
Dim objWordApplication As Word.Application
'//find all open word documents
Set objWordApplication = GetObject(, "Word.Application")
'//clear combobox
OpenDocs.Clear
'//add all open documents to combo box
For Each objWordDocument In objWordApplication.Documents
OpenDocs.AddItem objWordDocument.Name
Next objWordDocument
From what I have seen, and come to understand, the only sure fire way to do this is to iterate through the running instances of word and then kill each one in turn to be sure that you are getting the next instance.
Since word registers in the running object table the same exact way for every instance of itself, there is no way to get through them without first closing the one you were looking at.
One option to this approach, which is probably not desirable, is to get all the file names while you are killing the application instances and then load them all back in one instance that you create.
Alternately if you know the names of the open files, you can 'getObject' by open file name since Word will push its document names into the running object table, sadly this does not sound like the case for you.
Without writing an active x MFC service, you are not going to be able to do what you are looking to do.
I hope that is helpful.
EDIT:
there was an extensive discussion on subclassing and windows API's to get handles in order to change focus. http://www.xtremevbtalk.com/showthread.php?t=314637
if you dove into that head first and were able to enumerate the word instances by hwnd then you could potentially focus each one in turn and then list the file names. I do warn you though; that is some nasty subclassing which is dark magic that only some people who really want to accidentally break stuff play with.
In any event if you wanted to take the look at one instance, kill, repeat, reopen try this:
Adapted from this thread: http://www.xtremevbtalk.com/showthread.php?t=316776
Set objWordApplication = GetObject(, "Word.Application")
'//clear combobox
OpenDocs.Clear
'//add all open documents to combo box
Do While Not objWordDocument is nothing
For Each objWordDocument In objWordApplication.Documents
OpenDocs.AddItem objWordDocument.Name
Next objWordDocument
objWordApplication.Quit False
Set objWordApplication = Nothing
Set objWordApplication = GetObject(, "Word.Application")
loop
** use create object to open a new instance of word here and then go though
** your list of files until you have opened them all as documents in the new
** instance.
It's an old thread, but I too have the need to iterate over Word instances and bumped here.
Following the #Pow-Ian's advise, I tried to do the
if you dove into that head first and were able to enumerate the word
instances by hwnd then you could potentially focus each one in turn
and then list the file names.
Although I have managed to get all the handles, I have found an easier strategy with regard to office applications through AccessibleObjectFromWindow and our question is now solved.
Also, I believe the code showed #Pow-lan's has a mistype on
Do While Not objWordDocument is nothing
and should be:
Do While Not objWordApplication is nothing
Say I created a program with a textbox on it and a button that does something with the text in the textbox.
How do I make that program COM visible, load it and automate it from another project?
My goal is to be able to automate the program using COM:
Dim myProj as object = createObject("myProgram")
myProj.setText("Hello World")
myProj.buttonClickEvent()
Similar to how you can load a new excel and automate via interop:
dim xl as object = createobject("excel.application")
Dim wb as object = xl.workbooks.add
Dim ws as object = wb.worksheets(1)
ws.cells(1,1) = "i love stackoverflow"
How do programs do this? I'm looking for the answer VB.Net specific. Thank you in advance!
This page should give you the necessary details you will need: http://www.codeproject.com/KB/vb/MusaExposingCOM.aspx
This process that you want is normally called "exposing a COM interface", this is done via early binding or late binding. Early binding means the methods (locations) are known when you create your program, late binding means the methods (locations) are looked up when you run the program. Late binding, I think, is a little slower but the lookup only has to happen once. This is negligible.
I am seeing code like "Unload frmMain" where from what I can tell frmMain is the type/module name, and I don't think it could also be simultaneously a variable name of the "ObjFrmMain" sort. Nevertheless, this command does successfully induce the form in question to unload.
So is the data type being used as an alias for its single existing instance? Or maybe for all of its instances?
Does VB6 do similar things to data types other than those derived from Form?
Yes, VB6 has odd object behavior. It gives you some shortcuts for dealing with form objects.
Load frmMain
...will load a single instance of that form under that variable name. In fact:
frmMain.lblSomeLabel.Caption = "some caption"
... will load that instance. However:
frmMain.SomeStringMember = "some value"
... will not load the form object (meaning the window itself) but you can access these variables, so in essence, the name of the form is a global variable.
You can, however, create new instances:
Dim newForm As MyForm
Set newForm = New MyForm
newForm.Show vbModal
That will actually create a new instance of MyForm, load it and show it, so you can have multiple instances of one form.
Also beware of the oddness in the New keyword:
Dim newObject As New MyClass
Set newObject = Nothing
newObject.SomeStringProperty = "some value"
This works without an "Object Reference Not Set ..." error. When you declare a reference variable using the As New syntax, you can destroy the object by setting it to Nothing and then reference that variable again and it will create a new instance.
In fact that's what's really going on with the forms. There is an implicit:
Dim frmMain As New frmMain
Personally I prefer not to use the As New syntax because it's confusing and dangerous. It also has a performance penalty, vs. this:
Dim newObject As MyClass
Set newObject = New MyClass
... but you're stuck with it for the forms.
What's happening when you call Unload frmMain is that it unloads the window (and all the controls) so all the data in those are gone, but the object frmMain is still hanging around. Therefore even after you unload it, you can still access any member variables and properties. However, if anything references any control on the form, it will trigger an implicit Load frmMain. This is the source of a lot of subtle programming errors in VB6, especially when you're trying to shut down.
Yes, it's a special functionality in VB6 and earlier. I normally tried to avoid doing it, since I saw it more as a source of confusion rather than a help.
The following comment In Visual Basic 6.0 and earlier versions, a special default instance of each form is automatically created for you, and allows you to use the form's name to access this instance. is taken from this MSDN page: Working with Multiple Forms in Visual Basic .NET: Upgrading to .NET
I'm hoping a VB/VBA expert can help me out. Consider the following:
The user opens a document in Word 2003, and within the Normal.dot AutoOpen macro, we look at current document, and if it has been opened by clicking on a link on a webpage, and meets certain other application specific criteria, close the streamed 'copy' and open the source document (found on a shared drive we can assume the user has access to):
Documents.Open origDoc
Documents(ActiveDocument.FullName).Close SaveChanges:=wdDoNotSaveChanges
Documents(origDoc).Activate
With ActiveDocument
''# Do work
End With
My thought was that I needed to call Activate to ensure that the original document was the ActiveDocument, but I'm getting a 4160 'Bad file name' error on the .Activate call. If I comment out the call to .Activate, it appears that ActiveDocument is set to the origDoc document, even if there were other documents already opened (I'm not really sure how the Documents Collection is managed, and how Word determines what next ActiveDocument would be if you programatically close the current ActiveDocument)
So, does calling .Open on a document explicitly set the Document to be the ActiveDocument? Also, does calling .Activate on the already active document cause an error?
I haven't really been able to find much documentation about this, so thanks in advance for any suggestions and insight!
The simple answer is yes. By opening the document with your code you make it the active document, which you then close in the next line and try to activate in the next, and this fails because the document is no longer open. VBA in general seems to work this way.
It is important to be careful with ActiveDocument, because it's not always self-evident what actions, in code or elsewhere, will make a document 'active' (i have no proof but even an autosave might do it). IF there's any doubt you're better off referring to a document through the Documents collection, though this can also cause errors if the document is no longer open, and you might have to resort to iterating through the collection to be sure the document is, in fact, open. I run into this a lot with excel VBA, and Word VBA seems to function identically in that regard.
Also, VBA is flaky about releasing application objects. If you're not careful you'll end up with multiple WINWORD processes,viewable in task manager, regardless of whether you Close or Quit them in your code. The code I've found to work around this amounts to simulating the process of selecting END PROCESS in task manager. It works, but there should be a better solution.
Beware that there are a variety of problems that can be encountered:
if you want to re-open the document after closing it
once....Word/Windows DOES NOT
'release' the filename and you get a 'file busy' message, or message about 'creating
a temporary copy'.
to deal with this problem, I've had to develop an elaborate system of creating/saving and
tidying up multiple versions of any other documents I open/manipulate in my Word applications
because of this design flaw in Office Open/Close/Save methods.
Use the ReadOnlyRecommended property set to False with the
.Open method
referring to the document object (named doc, above) can cause
serious errors if you do not assure
that the doc object still exists before you try and manipulate it. Remember always, that
Word is an 'open' application platform....and the user can be doing things you didn't count
on...in the last millisecond or so. This advice holds for any other object or property you may
wish to manipulate in Word.
if you manipulate the Documents collection (or any other) without
assuring that the document or other object is still there and
valid before deleting or moving it within the collection you may
get 'stack overflow' errors. Particularly if you try and
close/delete objects in a collection starting at .item(1). You
must delete items in a collection from the last one, and remember
that the collection idicies and pointers change whenever you
.add/.remove/.close items from them.
You have an error here:
Document(origDoc).Activate
Should be Documents.
Yes, you can activate the active document. Nothing happens then.
Yes, opened document becomes active.
If you are not sure, use Documents.Open(origDoc).Activate.
You shouldn't be using the ActiveDocument object in the first place unless absolutely necessary because it's very unreliable. The preferred approach would be this:
Documents(ActiveDocument.FullName).Close SaveChanges:=wdDoNotSaveChanges
Dim doc as Document
Set doc = Documents.Open(origDoc)
With doc
'Do work
End With
I can do this:
Dim fso As New FileSystemObject
or I can do this:
Dim fso As Object
Set fso = CreateObject("Scripting.FileSystemObject")
How do I know what string to use for CreateObject? For example, how would I know to use the "Scripting." part of "Scripting.FileSystemObject"? Where do you go to look that up?
It is the ProgID of the component which is registered in Windows registry under HKCR key:
HKEY_CLASSES_ROOT\Scripting.FileSystemObject
ProgID's are human readable identifiers for COM objects. They point to the actual CLSIDs, which in this case is:
HKEY_CLASSES_ROOT\CLSID\{0D43FE01-F093-11CF-8940-00A0C9054228}
This is the place where you can find the actual COM .dll that includes the implementation of the component.
In the first sample code you have provided you are doing an early-binding, and in the second one you are doing a late-binding.
Using the VB6 IDE, choose Project, References, then to pick the reference 'Microsoft Scripting Runtime'.
If you didn't know what the reference is called, you could use the References dialog's Browse button to pick the file /system 32/scrrun.dll.
With the reference chosen, close the References dialog then open the Object Browser (View menu). Change the dropdown to the most likely candidate, being 'Scripting'. This will reveal the library's classes, one of which is 'FileSystemObject'. Hence, you will have discovered the the string required for CreateObject is 'Scripting.FileSystemObject'.
If you didn't know the Reference name or the file name but you did know the class name then you could search the registry for "FileSystemObject" and it should soon be revealed that the fully-qualified name you require is 'Scripting.FileSystemObject'.
I would start by searching for FileSystemObject in the MSDN library at http://msdn.microsoft.com/library
The site is chock full of documentation, including the details of how to call CreateObject.