I have an old VB6 app that I've distributed to several users running XP, Windows 7 and Windows 8. The following code is throwing an Error 91 ""Object variable or With block variable not set".
Const ssfPERSONAL = 46 'set directory to the common Documents folder
Dim strMyDocsPath As String 'stores common docsPath
On Error GoTo ErrorHandler
strMyDocsPath = CreateObject("Shell.Application").NameSpace(ssfPERSONAL).Self.Path
Specifically, the last line is the issue. I want strMyDocsPath to point to the common documents folder on the user machine. It works fine in Windows 7 and 8, but not on XP machines except the XP development machine where it runs without a hitch.
On the problem computers, I have tried re-registering scrrrun.dll and got a message it registered successfully. I tried downloading and installing the VB6 distributable SP6 files and still get the error. I have searched several forums and just can't figure it out. Any ideas?
It is usually useful to un-lump complex one-liners in one call per line fashion when debugging such kind of an error:
Dim DebugObj1 As Object
Dim DebugObj2 As Object
Dim DebugObj3 As Object
Set DebugObj1 = CreateObject("Shell.Application")
Debug.Print "1: " & CStr(DebugObj1 Is Nothing)
Set DebugObj2 = DebugObj1.NameSpace(ssfPERSONAL)
Debug.Print "2: " & CStr(DebugObj2 Is Nothing)
Set DebugObj3 = DebugObj2.Self
Debug.Print "3: " & CStr(DebugObj3 Is Nothing)
strMyDocsPath = DebugObj3.Path
Debug.Print strMyDocsPath
After that it is easier to spot what call doesn't return useful object, exactly. In your case it is most likely .NameSpace(ssfPERSONAL).
I was able to reproduce your problem in Windows XP SP3 if Shared Documents are disabled. There are many ways to disable them. See this, for example: http://www.howtogeek.com/howto/windows/how-to-remove-shared-documents-icon-from-my-computer-in-windows-xp/
As a side note, ShellSpecialFolderConstants.ssfPersonal is actually 0x05, see MSDN. The value 46 (or 0x2e) you use is CSIDL_COMMON_DOCUMENTS which indeed translates to Common Documents folder like C:\Documents and Settings\All Users\Documents. Probably, it is not the very best practice to use misleading naming.
Related
EDIT#1
I am developing a VB6 EXE application intended to output some special graphics to the Adobe Illustrator.
The example code below draws the given figure in the Adobe Illustrator as a dashed polyline.
' Proconditions:
' ai_Doc As Illustrator.Document is an open AI document
' Point_Array represented as "array of array (0 to 1)" contains point coordinates
'
Private Sub Draw_AI_Path0(ByRef Point_Array As Variant)
Dim New_Path As Illustrator.PathItem
Set New_Path = ai_Doc.PathItems.Add
New_Path.SetEntirePath Point_Array
New_Path.Stroked = True
New_Path.StrokeDashes = Array(2, 1)
End Sub
This simple code, however, can raise a variety of run-time automation errors caused by:
Incorrect client code (for example, assigning a value other than Array to the
New_Path.StrokeDashes)
Incorrect client data (for example, passing too large Point_Array to New_Path.SetEntirePath)
Unavailability of some server functions (for example when the current layer of the AI is locked)
Unexpected server behavior
EDIT#2
Unfortunately, since such errors are raised by the server app (AI, in our case) their descriptions are often inadequate, poor and misleading. The error conditions may depend on AI version, installed apps, system resources etc. A single problem can lead to different errors. Example passing too large Point_Array to New_Path.SetEntirePath (Windows XP SP3, Adobe Illustrator CS3):
For array size of 32767 and above, the error is -2147024809 (&H80070057) "Illegal Argument"
For array size of 32000 to 32766, the error is -2147212801 (&H800421FF) "cannot insert more segments in path. 8191 is maximum"
END OF EDIT#2
The traditional error handling can be used to prevent the client crash and to display the error details as shown below:
Private Sub Draw_AI_Path1(ByRef Point_Array As Variant)
Dim New_Path As Illustrator.PathItem
On Error GoTo PROCESS_ERROR
Set New_Path = ai_Doc.PathItems.Add
New_Path.SetEntirePath Point_Array
New_Path.Stroked = True
New_Path.StrokeDashes = Array(2, 1)
Exit Sub
PROCESS_ERROR:
MsgBox "Failed somewhere in Draw_AI_Path1 (" & Format(Err.Number) & ")" _
& vbCrLf & Err.Description
End Sub
As you can see, the error number and error description can be accessed easily. However, I need to know also what call causes the error. This can be very useful for large and complex procedures containing many calls to the automation interface. So, I need to know:
What error happened?
What call caused it?
In what client function it happened?
Objective #3 can be satisfied by techniques described here. So, let’s focus on objectives #1 and 2. For now, I can see two ways to detect the failed call:
1) To “instrument” each call to the automation interface by hardcoding the description:
Private Sub Draw_AI_Path2(ByRef Point_Array As Variant)
Dim New_Path As Illustrator.PathItem
Dim Proc As String
On Error GoTo PROCESS_ERROR
Proc = "PathItems.Add"
Set New_Path = ai_Doc.PathItems.Add
Proc = "SetEntirePath"
New_Path.SetEntirePath Point_Array
Proc = "Stroked"
New_Path.Stroked = True
Proc = "StrokeDashes"
New_Path.StrokeDashes = Array(2, 1)
Exit Sub
PROCESS_ERROR:
MsgBox "Failed " & Proc & " in Draw_AI_Path2 (" & Format(Err.Number) & ")" _
& vbCrLf & Err.Description
End Sub
Weak points:
Code becomes larger and less readable
Incorrect cause can be specified due to copypasting
Strong points
Both objectives satisfied
Minimal processing speed impact
2) To “instrument” all calls together by designing a function that invokes any automation interface call:
Private Function Invoke( _
ByRef Obj As Object, ByVal Proc As String, ByVal CallType As VbCallType, _
ByVal Needs_Object_Return As Boolean, Optional ByRef Arg As Variant) _
As Variant
On Error GoTo PROCESS_ERROR
If (Needs_Object_Return) Then
If (Not IsMissing(Arg)) Then
Set Invoke = CallByName(Obj, Proc, CallType, Arg)
Else
Set Invoke = CallByName(Obj, Proc, CallType)
End If
Else
If (Not IsMissing(Arg)) Then
Invoke = CallByName(Obj, Proc, CallType, Arg)
Else
Invoke = CallByName(Obj, Proc, CallType)
End If
End If
Exit Function
PROCESS_ERROR:
MsgBox "Failed " & Proc & " in Draw_AI_Path3 (" & Format(Err.Number) & ")" _
& vbCrLf & Err.Description
If (Needs_Object_Return) Then
Set Invoke = Nothing
Else
Invoke = Empty
End If
End Function
Private Sub Draw_AI_Path3(ByRef Point_Array As Variant)
Dim Path_Items As Illustrator.PathItems
Dim New_Path As Illustrator.PathItem
Set Path_Items = Invoke(ai_Doc, "PathItems", VbGet, True)
Set New_Path = Invoke(Path_Items, "Add", VbMethod, True)
Call Invoke(New_Path, "SetEntirePath", VbMethod, False, Point_Array)
Call Invoke(New_Path, "Stroked", VbSet, False, True)
Call Invoke(New_Path, "StrokeDashes", VbSet, False, Array(2, 1))
End Sub
Weak points:
Objective #1 is not satisfied since Automation error 440 is always raised by CallByName
Need to split expressions like PathItems.Add
Significant (up to 3x) processing speed drop for some types of automation interface calls
Strong points
Compact and easy readable code with no repeated on error statements
Is there other ways of handling automation errors?
Is there a workaround for the Weak point #1 for 2)?
Can the given code be improved?
Any idea is appreciated! Thanks in advance!
Serge
Think of why it is you might want to know where an error has been raised from. One reason is for simple debugging purposes. Another, more important, reason is that you want to do something specific to handle specific errors when they occur.
The right solution for debugging really depends on the problem you're trying to solve. Simple Debug.Print statements might be all you need if this is a temporary bug hunt and you're working interactively. Your solution #1 is fine if you only have a few routines that you want granular error identification for, and you can tolerate having message boxes pop up. However, like you say, it's kind of tedious and error prone so it's a bad idea to make that into boilerplate or some kind of "standard practice".
But the real red flag here is your statement that you have "large and complex procedures containing many calls to the automation interface", plus a need to handle or at least track errors in a granular way. The solution to that is what it always is - break up your large and complex procedures into a set of simpler ones!
For example, you might have a routine that did something like:
Sub SetEntirePath(New_Path As Illustrator.PathItem, ByRef Point_Array As Variant)
On Error Goto EH
New_Path.SetEntirePath Point_Array
Exit Sub
EH:
'whatever you need to deal with "set entire path" errors
End Sub
You basically pull whatever would be line-by-line error handling in your large procedure into smaller, more-focused routines and call them. And you get the ability to "trace" your errors for free. (And if you have some kind of systematic tracing system such as the one I described here - https://stackoverflow.com/a/3792280/58845 - it fits right in.)
In fact, depending on your needs, you might wind up with a whole class just to "wrap" the methods of the library class you're using. This sort of thing is actually quite common when a library has an inconvenient interface for whatever reason.
What I would not do is your solution #2. That's basically warping your whole program just for the sake of finding out where errors occur. And I guarantee the "general purpose" Invoke will cause you problems later. You're much better off with something like:
Private Sub Draw_AI_Path4(ByRef Point_Array As Variant)
...
path_wrapper.SetEntirePath Point_Array
path_wrapper.Stroked = True
path_wrapper.StrokeDashes = Array(2, 1)
...
End Sub
I probably wouldn't bother with a wrapper class just for debugging purposes. Again, the point of any wrapper, if you use one, is to solve some problem with the library interface. But a wrapper also makes debugging easier.
One would run it in the VB6 debugger. If compiled without optimisation (you won't recognise your code if optimised) you can also get a stack trace from WinDbg or WER (use GFlags to set it up). HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug is where settings are stored.
You can also start in a debugger.
windbg or ntsd (ntsd is a console program and maybe installed). Both are also from Debugging Tools For Windows.
Download and install Debugging Tools for Windows
http://msdn.microsoft.com/en-us/windows/hardware/hh852363
Install the Windows SDK but just choose the debugging tools.
Create a folder called Symbols in C:\
Start Windbg. File menu - Symbol File Path and enter
srv*C:\symbols*http://msdl.microsoft.com/download/symbols
then
windbg -o -g -G c:\windows\system32\cmd.exe /k batfile.bat
You can press F12 to stop it and kb will show the call stack (g continues the program). If there's errors it will also stop and show them.
Type lm to list loaded modules, x *!* to list the symbols and bp symbolname to set a breakpoint
da displays the ascii data found at that address
dda displaysthe value of the pointer
kv 10 displays last 10 stack frames
lm list modules
x *!* list all functions in all modules
p Step
!sysinfo machineid
If programming in VB6 then this environmental variable link=/pdb:none stores the symbols in the dll rather than seperate files. Make sure you compile the program with No Optimisations and tick the box for Create Symbolic Debug Info. Both on the Compile tab in the Project's Properties.
Also CoClassSyms (microsoft.com/msj/0399/hood/hood0399.aspx) can make symbols from type libraries.
I have a problem: I want to run a file from lotusscript code:
Dim result As Integer
result = Shell("D:\testF.dsx", 1)
And I get the following error: Illegal function call.
If I want to execute it from formula it works:
#Command([Execute]; "D:\\testF.dsx")
Thanks a lot!
I had the same problem with the shell function in lotus script with PDF files. And my workaround is to use the windows scripting host to launch the file. I am pretty sure this will solve your problem too.
Set objShell = CreateObject("WScript.Shell")
returnValue = objShell.Run("d:\testF.dsx", 3, false)enter code here
It is not possible to "Execute" a Textfile. usually there is no "run" function defined for dsx- files.
You could do it like:
Dim result as Integer
result = Shell("notepad.exe D:\testF.dsx", 1)
Or find out, which program is linked to dsx (via registry) and execute the corresponding exe with filename as Parameter. If the filename contains spaces, then it has to be enclosed:
Dim result as Integer
result = Shell({notepad.exe "D:\testF.dsx"}, 1)
And reading your last question this approach for sure is NOT the right for your request. You have to use "open" in order to process files... Like Per Hendrik told you in his response.
I had similar problems passing parameters to Outlook.
On some windows machines, this #Formula worked perfectly:
#Command([Execute]; "Outlook.exe"; "/recycle")
On Windows Terminal Servers, it caused Outlook to be unable to parse the "/recycle"
The Shell-command from LotusScript wasn't even able to locate the Outlook.exe, as it was not in the PATH.
Ralf's code helped me in this respect. The "WScript.Shell" seems able to interact with Windows registry settings. Anyway, here is the code that works for activating an open Outlook window.
Dim objShell, returnValue
Set objShell = CreateObject("WScript.Shell")
returnValue = objShell.Run("outlook.exe /recycle", 3, False)
I have an old VB6 app that opens an Word document (.doc). It has worked perfectly on Windows XP for a long time. My problem is that when I install the app on Windows 7 or Windows 8, the code will open Word, but not bring up the actual document. When it opens Word, I am able to navigate to file and open it perfectly fine, so there is no issue with the file. It seems like I'm missing something simple here, but after a lot of searching and reading, I can't pinpoint it.
I've made sure that Word is the program associated to .doc files on the Windows 7 and 8 computers, so that's not it.
Here is the code I use to open the document:
Dim iret As Long
iret = ShellExecute(hwnd, vbNullString, QuoteFilePath & File1.FileName, vbNullString, "c:\", SW_SHOWNORMAL)
Any help is appreciated!
There are a number of reasons why this special folder should not actually be used to store user documents. Microsoft and even 3rd parties have begun to use it for entirely different purposes. Depending on what applications have been installed you might even find DLLs in here.
If you mistrain users to play with this folder they might delete a file critical to the operation of some other program.
But if you insist on doing this note that it is not safe to refer to the folder by a literal string value since it can appear under varying aliases based on the user's language settings. It might even be relocated elsewhere through administrative actions.
It also hasn't been necessary to stoop to using the non-COM ShellExecute entrypoint in ages, at least as far back as version 5.0 of Shell32.dll.
This should work at least from WinXP forward:
Option Explicit
Private Const ssfCOMMONDOCUMENTS As Long = &H2E
Private Enum SHOW_WINDOW
SW_HIDE = 0
SW_SHOWNORMAL = 1
SW_SHOWMINIMIZED = 2
SW_SHOWMAXIMIZED = 3
SW_SHOWNOACTIVATE = 4
SW_SHOW = 5
SW_SHOWMINNOACTIVE = 7
SW_SHOWDEFAULT = 10
End Enum
Private Shell As Object
Private Path As String
Private Sub File1_Click()
On Error Resume Next
'Works on XP through Vista, fails on Win7:
'Shell.ShellExecute File1.FileName, , Path, "open", SW_SHOWNORMAL
'Works on XP through Win7:
Shell.ShellExecute Path & "\" & File1.FileName, , , "open", SW_SHOWNORMAL
If Err Then
MsgBox "Error " & CStr(Err.Number) & " " & Err.Description
End If
End Sub
Private Sub Form_Load()
Set Shell = CreateObject("Shell.Application")
With Shell.NameSpace(ssfCOMMONDOCUMENTS).Self
Path = .Path
End With
With File1
.Pattern = "*.doc"
.Path = Path
End With
End Sub
I have a Setup and Deployment project in Visual Studio 2010.
I would like the installer to create two shortcuts to the executable of another project in my solution. One normal shortcut that simply runs the application using current credentials and another which has the Run as administrator flag set, thereby ensuring that the user is asked for credentials with administrative rights when clicking the shortcut.
Running the application with administrative rights enables certain features that are otherwise not available.
Setting this flag doesn't seem to be possible at first glance. Can this be done directly in Visual Studio? If not, are there any other options?
Edit: If not, is it possible to modify the shortcut programmatically using a custom installer class?
I know this is quite an old question, but I needed to find an answer and I thought I could help other searchers. I wrote a small function to perform this task in VBScript (pasted below). It is easily adapted to VB.net / VB6.
Return codes from function:
0 - success, changed the shortcut.
99 - shortcut flag already set to run as administrator.
114017 - file not found
114038 - Data file format not valid (specifically the file is way too small)
All other non-zero = unexpected errors.
As mentioned by Chada in a later post, this script will not work on msi Advertised shortcuts. If you use this method to manipulate the bits in the shortcut, it must be a standard, non-advertised shortcut.
References:
MS Shortcut LNK format: http://msdn.microsoft.com/en-us/library/dd871305
Some inspiration: Read and write binary file in VBscript
Please note that the function does not check for a valid LNK shortcut. In fact you can feed it ANY file and it will alter Hex byte 15h in the file to set bit 32 to on.
If copies the original shortcut to %TEMP% before amending it.
Daz.
'# D.Collins - 12:58 03/09/2012
'# Sets a shortcut to have the RunAs flag set. Drag an LNK file onto this script to test
Option Explicit
Dim oArgs, ret
Set oArgs = WScript.Arguments
If oArgs.Count > 0 Then
ret = fSetRunAsOnLNK(oArgs(0))
MsgBox "Done, return = " & ret
Else
MsgBox "No Args"
End If
Function fSetRunAsOnLNK(sInputLNK)
Dim fso, wshShell, oFile, iSize, aInput(), ts, i
Set fso = CreateObject("Scripting.FileSystemObject")
Set wshShell = CreateObject("WScript.Shell")
If Not fso.FileExists(sInputLNK) Then fSetRunAsOnLNK = 114017 : Exit Function
Set oFile = fso.GetFile(sInputLNK)
iSize = oFile.Size
ReDim aInput(iSize)
Set ts = oFile.OpenAsTextStream()
i = 0
Do While Not ts.AtEndOfStream
aInput(i) = ts.Read(1)
i = i + 1
Loop
ts.Close
If UBound(aInput) < 50 Then fSetRunAsOnLNK = 114038 : Exit Function
If (Asc(aInput(21)) And 32) = 0 Then
aInput(21) = Chr(Asc(aInput(21)) + 32)
Else
fSetRunAsOnLNK = 99 : Exit Function
End If
fso.CopyFile sInputLNK, wshShell.ExpandEnvironmentStrings("%temp%\" & oFile.Name & "." & Hour(Now()) & "-" & Minute(Now()) & "-" & Second(Now()))
On Error Resume Next
Set ts = fso.CreateTextFile(sInputLNK, True)
If Err.Number <> 0 Then fSetRunAsOnLNK = Err.number : Exit Function
ts.Write(Join(aInput, ""))
If Err.Number <> 0 Then fSetRunAsOnLNK = Err.number : Exit Function
ts.Close
fSetRunAsOnLNK = 0
End Function
This is largely due to the fact that Windows Installer uses 'Advertised shortcuts' for the Windows Installer packages.
There is no way inherently to disable this in Visual Studio, but it is possible to modify the MSI that is produced to make sure that it does not use advertised shortcuts (or uses only one). There are 2 ways of going about this:
If your application uses a single exe or two - Use ORCA to edit the MSI. Under the shortcuts table, change the Target Entry to "[TARGETDIR]\MyExeName.exe" - where MyExeName is the name of your exe - this ensures that that particular shortcut is not advertised.
Add DISABLEADVTSHORTCUTS=1 to the the property Table of the MSI using ORCA or a post build event (using the WiRunSQL.vbs script). If you need more info on this let me know. This disables all advertised shortcuts.
it may be better to use the first approach, create 2 shortcuts and modify only one in ORCA so that you can right click and run as admin.
Hope this helps
This is not supported by Windows Installer. Elevation is usually handled by the application through its manifest.
A solution is to create a wrapper (VBScript or EXE) which uses ShellExecute with runas verb to launch your application as an Administrator. Your shortcut can then point to this wrapper instead of the actual application.
Sorry for the confusion - I now understand what you are after.
There are indeed ways to set the shortcut flag but none that I know of straight in Visual Studio. I have found a number of functions written in C++ that set the SLDF_RUNAS_USER flag on a shortcut.
Some links to such functions include:
http://blogs.msdn.com/b/oldnewthing/archive/2007/12/19/6801084.aspx
http://social.msdn.microsoft.com/Forums/en-US/windowssecurity/thread/a55aa70e-ae4d-4bf6-b179-2e3df3668989/
Another interesting discussion on the same topic was carried out at NSIS forums, the thread may be of help. There is a function listed that can be built as well as mention of a registry location which stores such shortcut settings (this seems to be the easiest way to go, if it works) - I am unable to test the registry method at the moment, but can do a bit later to see if it works.
This thread can be found here: http://forums.winamp.com/showthread.php?t=278764
If you are quite keen to do this programatically, then maybe you could adapt one of the functions above to be run as a post-install task? This would set the flag of the shortcut after your install but this once again needs to be done on Non-Advertised shortcuts so the MSI would have to be fixed as I mentioned earlier.
I'll keep looking and test out the registry setting method to see if it works and report back.
Chada
I needed to make my application to be prompted for Administator's Rights when running from Start Menu or Program Files.
I achieved this behavior after setting in \bin\Debug\my_app.exe 'Run this program as administator' checkbox to true. ( located in Properties\Compatibility section ).
While installing project, this file was copied to the Program Files (and therefore the shortcut in the Start Menu) with needed behavior.
Thanks,
Pavlo
Conditions:
Windows 98 SE
WMI not available
I have code that looks like this, written using my steroidal wrapping of VBScript using MSScript.
do
a = files.collectfiles( "c:\userver", "" )
for i = 0 to ubound( a )
f = a(i)
if strings.endswith( f, ".usv" ) then
d = files.readfilee( f )
on error resume next
executeglobal d
nErr = err.number
sErr = err.description
on error goto 0
if nErr <> 0 then
trace "*** Error " & nErr & ", " & sErr
end if
files.deletefile f
end if
next
system.sleep 10
system.cooperate
loop
There's a lot of disk activity with that call to files.collectfiles. Is there some way of detecting a change in the contents of a folder without actually scanning the folder for files?
There is a sample which claims to work on all versions from Win95 up to at leas WinXP. Developed under Win98 with VB5. Using the (then? provided links to the docu below) undocumented SHChangeNotify* Functions.
SHChangeNotifyRegister: Receive Shell Change Notifications
SHChangeNotifyRegister Function
SHChangeNotifyDeregister Function
There is another solution using ReadDirectoryChangesW here:
VB6 WinAPI ReadDirectoryChangesW (check the 5th post from Yang Kok Wah)
Define "change in the contents of a folder".
If it means that a file was added, deleted, or renamed, then the modified timestamp of the folder is updated whenever such an event occurs.
If you're instead wanting to know when files are modified, then you'll need to read them.
That said, looking at what you're trying to do (scan a folder for new .usv files, and process them and delete them), then just keeping track of the timestamp on the folder and updating it right before you call collectfiles is best (note that the correct time to log is just BEFORE calling collectfiles, otherwise you run the risk of not waking up if a file gets added during the collectfiles call or immediately afterward).
You specifically asked for something in VB and running on win98 and I have no answer for this, but MS has a c/win32 example on how to achieve this on Windows2000+ with FindFirstChangeNotification. Another thing is that apparently "FileSystemWatcher" in .NET is not working/supported on Win98. What is my point? There maybe is no easy solution for this and you have to come up with something on your own.