using ini file in vb6, problem with path to file - vb6

I have read many articles about how to use an INI file within my VB6 project. I don't have a problem with the methods, my problem is how to make the EXE file find the INI file. I don't want to hard code the path in the program. I simply want the EXE to expect the INI file to be present in the same folder the EXE is executed from.
When I run the program from inside VB6 IDE, the INI is found and processed. When I compile the program and run the EXE, nothing is found.
My code looks like:
gServer = sGetINI(sINIFile, "TOOLBOM", "ServerName", "?")
where TOOLBOM is the [Section] and "ServerName" is the key for the value.
I obtained the following code for the API:
Rem API DECLARATIONS
Declare Function GetPrivateProfileString Lib "kernel32" Alias _
"GetPrivateProfileStringA" (ByVal lpApplicationName _
As String, ByVal lpKeyName As Any, ByVal lpDefault _
As String, ByVal lpReturnedString As String, ByVal _
nSize As Long, ByVal lpFileName As String) As Long
Declare Function WritePrivateProfileString Lib "kernel32" Alias _
"WritePrivateProfileStringA" (ByVal lpApplicationName _
As String, ByVal lpKeyName As Any, ByVal lpString As Any, _
ByVal lpFileName As String) As Long
Public Function sGetINI(sINIFile As String, sSection As String, sKey _
As String, sDefault As String) As String
Dim sTemp As String * 256
Dim nLength As Integer
sTemp = Space$(256)
nLength = GetPrivateProfileString(sSection, sKey, sDefault, sTemp, _
255, sINIFile)
sGetINI = Left$(sTemp, nLength)
End Function
Public Sub writeINI(sINIFile As String, sSection As String, sKey _
As String, sValue As String)
Dim n As Integer
Dim sTemp As String
sTemp = sValue
Rem Replace any CR/LF characters with spaces
For n = 1 To Len(sValue)
If Mid$(sValue, n, 1) = vbCr Or Mid$(sValue, n, 1) = vbLf _
Then Mid$(sValue, n) = " "
Next n
n = WritePrivateProfileString(sSection, sKey, sTemp, sINIFile)
End Sub

In VB6 you can use the App.Path to specify the path to files that should be in the directory you are executing from. e.g.
sIniFile = App.Path & "\myIniFile.ini"
What is the error that you're getting?

Bob is right, this will fail as soon as anyone tries to run it on Vista or later. Writeable data files are not supposed to go in Program Files. Windows now enforces these rules. Global settings for all users belong in one folder, per-user settings in another, per-user roaming settings in another, etc.
This Visual Studio Magazine article by Karl Peterson gives some VB6 code you can drop into your project to find the locations of these folders at run-time. And then this previous article by the same author gives you a nice class for using INI files, to hide away those API declarations.

This will fail as soon as anyone tries to run it on Vista or later though.
Writeable data files are not supposed to go into Program Files. Since people did it anyway Windows began enforcing the rules beginning with Vista.
Global settings belong in an application folder under CommonAppData, per-user settings go below LocalAppData, per-using roaming settings under AppData, and so on. These locations are retrieved at runtime via Shell object or API calls.

You will want to use the FileSystemObject from the Scripting runtime to combine paths and filenames properly. Although it may seem a trivial issue in reality there are corner cases that the FileSystemObject handles.

app.path would return the path of the currently executing exe,use it

Keep the EXE in the same folder

Related

VB6: ShellExecute an EXE inside AppData

I have the following sub:
Public Sub ShellApp(URL As String)
Dim vResult As Long
vResult = ShellExecute(0, "open", URL, vbNullString, vbNullString, vbMinimizedFocus)
End If
End Sub
This is on a layer that cannot be changed due to several functionality needed on that sub.
Now, on our Main() sub for example, we check a list of added plugins saved in a text file beside the EXE, and call the above Sub in for loop with the path of the plugins to run them. So if I have 3 plugins as below in the text file:
C:\App1.EXE
C:\App2.EXE
C:\Users\AhmadMusa\AppData\Roaming\App3.exe
First two apps will run fine on all PCs (Static path), but third app will not work on any PC except mine which is not ok... Note that App3 always installed on AppData on any PC, so how to dynamically access it to run the app from any PC without adjustments on the sub.
What will be the path placed in the text file for third app so it can work on all PCs?
I tried (AppData\Roaming\App3.exe) but it does not work... I found on a thread (http://www.vbforums.com/showthread.php?529776-RESOLVED-Open-a-folder-in-AppData) that I can call something like (shell:AppData\Roaming\App3.exe) it did not work to run the App3.exe, but if I call (shell:AppData\Roaming) it will open the Roaming folder very well. But cannot run the EXE.
Any ideas ?
Thanks.
I believe that there is no way to solve the problem without altering the original procedure "ShellApp".
in case you change your mind, i think this post may come in help (with some tweekings)
Public Sub ShellApp(URL As String)
Dim vResult As Long
'vResult = ShellExecute(0, vbNullString, URL, vbNullString, vbNullString, vbNormalFocus)
vResult = ShellExecute(0, vbNullString, "cmd.exe", "/k """"" & URL & """""", vbNullString, vbNormalFocus)
End Sub
Private Sub Command1_Click()
ShellApp "%appdata%\PROGRAME.exe"
End Sub
this because only "cmd.exe" and ofcourse batch scripts are able to expend variables that are enclosed with percent character "%"
To close the console as soon as it starts change the parameter "/k" to "/c",
sub test()
dim tmp as string
tmp = environ("AppData ") & "\calc.exe"
call ShellExecute(..., tmp, ...)
end sub
fill the other arguments (...) the way you see it right
You need to expand the Environment variable (this is what the %...% does):
Debug.Print Environ("APPDATA") ' will print the expanded %appdata%
So, in your text file you should put:
%APPDATA%\App3.exe
How to expand the path? You can loop over the environment variables provided by the VB Environ function and do a string replace by yourself (the VB way) or you can profit from the ExpandEnvironmentStrings function (the Win32 API way).
Below a snippet using this second option:
Private Declare Function ExpandEnvironmentStrings Lib "kernel32.dll" _
Alias "ExpandEnvironmentStringsA" ( _
ByVal lpSrc As String, _
ByVal lpDst As String, _
ByVal nSize As Long) As Long
Public Function ExpandEnvironString(ByVal URL As String) As String
Dim buf As String, bufSize As Long
bufSize = ExpandEnvironmentStrings(URL, ByVal 0&, 0&)
buf = String(bufSize + 1, vbNullChar)
bufSize = ExpandEnvironmentStrings(URL, buf, Len(buf))
ExpandEnvironString = Left$(buf, InStr(1, buf, vbNullChar) - 1)
End Function
Before you call ShellApp(URL As String) you should expand the path:
URL = ExpandEnvironString(URL)
ShellExecute will receive the expanded path: C:\Users\AhmadMusa\AppData\Roaming\App3.exe
This is a non-breaking change, because if your initial default setting will be later changed to a custom fixed path, the ExpandEnvironmentStrings function will simply ignore it.
Example:
ExpandEnvironString("C:\App1.EXE ") will return C:\App1.EXE
More info:
you can get all your environment variables with following procedure:
Private Sub EnvironmentEntries()
Dim Entry As String, i As Long
i = 1
Do
Entry = Environ(i)
i = i + 1
If Entry = "" Then Exit Do
Debug.Print Entry
Loop
End Sub
... and check some additional info here:
Why are there directories called Local, LocalLow, and Roaming under \Users\?

Shell function sensitive to both the location of the executable and spaces in path

Until recently this VB6 code worked on my windows 7 64 bit machine
Shell "c:\My App\Helpers\Helper.exe"
The error message this now throws is
Invalid procedure call or argument (Error 5)
Since it stopped working I have found that moving the directory Helpers to the desktop fixes the issue.
So also does getting rid of the space in the path by renaming the
My App
folder as
MyApp
So also does inserting opening and closing quotes as in :
Shell """c:\My App\Helpers\Helper.exe"""
Meanwhile if the Helpers folder is on the desktop I can insert a space into the path by renaming the folder 'Hel pers' and it still works without the extra quotes.
So these all work:
Shell """c:\My App\Helpers\Helper.exe"""
Shell "c:\Users\UserA\Desktop\Helpers\Helper.exe"
Shell "c:\Users\UserA\Desktop\Hel pers\Helper.exe"
while the original no longer works though it did for years
Shell "c:\My App\Helpers\Helper.exe"
What could be the cause of this, and is there a way to restore the behaviour to the way it was before?
The Shell() function dated from much simpler times, it is ambiguous today. The command can also mean "start the c:\My program and pass it the App\Helpers.Helper.exe command line argument".
Why it triggers on your machine is impossible to tell from a distance, especially when you obfuscate the real name of the program. An infamous example is having a file or directory named Program in the root directory. Now c:\Program Files\Etcetera no longer works.
Using the double-quotes is the correct approach.
Have a look at the ShellExecute() API instead of Shell()
Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" _
(ByVal hwnd As Long, ByVal lpszOp As String, _
ByVal lpszFile As String, ByVal lpszParams As String, _
ByVal LpszDir As String, ByVal FsShowCmd As Long) _
As Long
For more info : example on microsoft.com
[EDIT]
A small example with only the parts that you (probably) need:
'1 Form with:
' 1 Command button: Name="Command1"
Option Explicit
Private Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" (ByVal hwnd As Long, ByVal lpszOp As String, ByVal lpszFile As String, ByVal lpszParams As String, ByVal lpszDir As String, ByVal FsShowCmd As Long) As Long
Private Const SW_SHOWNORMAL = 1
Private Sub Command1_Click()
Dim strPath As String
Dim strExe As String
Dim lngReturn As Long
strExe = "TestProg.exe"
strPath = "C:\Program Files (x86)\ShellTest"
lngReturn = ShellExecute(0, "Open", strExe, vbNullString, strPath, SW_SHOWNORMAL)
Caption = CStr(Now) & " : " & CStr(lngReturn)
End Sub
When you click on the command button it will execute TextProg.exe from the ShellTest directory
In the caption of the form it will show the return value of the ShellExecute command

Load shortcut then close form automatically

i want achieve when shortcut link runs ... the form closes automatically, btw im new to vb coding so any help will be much appreciated, here's my code so far
Private Sub Form_Load()
Set ss = CreateObject("WScript.Shell")
ss.Run Chr(34) & ss.specialfolders("Desktop") & "\app\SOMEGAME.lnk" & Chr(34)
End Sub
Assuming you're using VB6 (which is what your code looks like) you can close your form by calling
Unload Me
at the end of the Form_Load event handler.
However, you don't need to use a form to launch a shortcut - you can add a module to your project (right-click your project, select Add -> Module) and then just call the ShellExecute() function to launch your shortcut like so:
'Declare the ShellExecute() API function
Private Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" _
(ByVal hwnd As Long, ByVal lpOperation As String, ByVal lpFile As String, _
ByVal lpParameters As String, ByVal lpDirectory As String, _
ByVal nShowCmd As Long) As Long
Private Const SW_SHOWNORMAL As Long = 1
'Entry point of your program
Public Sub Main()
Dim sPath As String
sPath = "C:\app\SOMEGAME.lnk"
ShellExecute 0, vbNullString, sPath, vbNullString, "C:\", SW_SHOWNORMAL
End Sub
To make this work, set the Startup Object under Project Properties to Sub Main.
Using this approach, you don't have a form - your program just runs from the command-line (or from its own shortcut). It's generally better not to create / show a form if your program doesn't need it since forms use extra resources.
With that said, you should try using VB.Net or C# to write programs for Windows - VB6 is old technology without support and it can't handle a number of new technologies. If you don't already know VB6 there's little point in learning it now - your time could be put to much better use by learning VB.Net / C#.

ResolvePath for VB6 - resolve environment variables

I am looking for a function in VB6 (or some WinAPI) that might be able to satisfy this requirement: Take an input path string that includes environment variables, and output that path with environment variables resolved.
For example:
Input: "%windir%\System32\"
Output: "C:\Windows\System32\"
I could of course write my own parser, but I am wondering if this functionality already exists?
This would be similar to the Spring Framework's "ResolvePath" method.
Kernel32.dll exports a function called ExpandEnvironmentStrings:
My VB6 is rusty but you can call this by doing:
Declare Function ExpandEnvironmentStrings _
Lib "kernel32" Alias "ExpandEnvironmentStringsA" _
(ByVal lpSrc As String, ByVal lpDst As String, _
ByVal nSize As Long) As Long
Then in a function or sub:
Dim result as Long
Dim strInput As String, strOutput As String
'' Two calls required, one to get expansion buffer length first then do expansion
result = ExpandEnvironmentStrings(strInput, strOutput, result)
strOutput = Space$(result)
result = ExpandEnvironmentStrings(strInput, strOutput, result)
Worst case you can use the native implementation: ExpandEnvironmentStrings
Using the seldom used Environ() Function: http://vbcity.com/forums/t/45987.aspx

How do I programmatically update OCX references in vb6 projects?

I periodically break binary compatibility and need to recompile an entire vb6 application composed of several dozen ActiveX DLLs and OCXs in total. I've written a script to automate this process, but I have encountered a problem.
When an OCX is recompiled with project compatibility its version is incremented, and projects referencing the OCX will not recompile until their reference is updated to the new version. This is checked automatically when the project is opened normally, and the user is prompted to update the reference, but I need to do it in a script.
How do I do it?
We are doing similar things, i.e. manipulating the references to the used OCXs directly in VB6 .vbp files, in our VB6 Project References Update Tool (download here). Generally it is used to update the references when the used ActiveX change their version numbers, CLSIDs, etc.
The tools is open-source so everyone who is interested in this problem can borrow our VB code snippets to implement tasks like these.
Our tool is written in Visual Basic 6 and uses tlbinf32.dll (the TypeLib Information DLL) that allows you to programmatically extract information from type libraries.
My project, maintained over a decade, consists of a hierarchy of two dozen ActiveX DLLs and a half dozen controls. Compiled with a script system as well.
I don't recommend doing what you are doing.
What we do is as follows
Make our changes including additions
and test in the IDE.
We compile from the bottom of the
hierarchy to the top
we copy the newly complied files to
a revision directory for example
601,then 602 etc etc
we create the setup.exe
when the setup is finalized we copy
over the revision directory into our
compatibility director. Note we
never point to the compiled binary
in the project directory. Always to
a compability directory that has all
the DLLs.
The reason this works is that if you look at the IDL source using the OLE View tool you will find that any referenced control or dlls is added to the interface via a #include. If you point to the binary in your project directory then the include is picked up from the registry which can lead to a lot of strangness and compatibility.
However if the referenced DLL is present in the directory that binary exists while being used for binary compatibility, VB6 will use that instead of whatever in the registry.
Now there is one problem that you will get on an infrequent basis. Consider this heirarchy
MyUtilityDLL
MyObjectDLL
MyUIDLL
MyEXE
If you ADD a property or method to a class in MyUtilityDLL MyUIDLL may not compile giving a binary incompatibility error if you are lucky or a strange error like [inref]. In any case the solution is to compile MyUtilityDLL and then immediately copy MyUtilityDLL into the compatibility directory. Then the rest of the automated compile will work fine.
You may want to include this step in the automated build.
Note that in many cases the projects will work fine in the IDE. To if you are now aware of this you could be pulling your hair out.
I guess you would have to edit the project files (.vbp), Form files (.frm) and the control files (.ctl) that reference the DLLs and OCXs and increment the typelib version number.
You would find latest typelib version number for the control / DLL in the registry.
This could be a pain depending on how many files you have.
A hack would be to open main project with VB6 using your script and send keys to confirm the Update References and then save the project.
Good Luck
Self-answer: I have written some vb6 code to do the upgrade programmatically. It is not extensively tested, there are probably a few bugs here and there for corner cases, but I did use it successfully.
Option Explicit
Const HKEY_LOCAL_MACHINE As Long = &H80000002
Const KEY_ENUMERATE_SUB_KEYS As Long = 8
Private Declare Function RegOpenKeyEx Lib "advapi32.dll" Alias "RegOpenKeyExA" (ByVal hKey As Long, ByVal lpSubKey As String, ByVal ulOptions As Long, ByVal samDesired As Long, phkResult As Long) As Long
Private Declare Function RegEnumKeyEx Lib "advapi32.dll" Alias "RegEnumKeyExA" (ByVal hKey As Long, ByVal dwIndex As Long, ByVal lpName As String, lpcbName As Long, lpReserved As Long, ByVal lpClass As String, lpcbClass As Long, lpftLastWriteTime As Any) As Long
Private Declare Function RegCloseKey Lib "advapi32.dll" (ByVal hKey As Long) As Long
'''Returns the expected major version of a GUID if it exists, and otherwise returns the highest registered major version.
Public Function GetOcxMajorVersion(ByVal guid As String, Optional ByVal expected_version As Long) As Long
Const BUFFER_SIZE As Long = 255
Dim reg_key As Long
Dim ret As Long
Dim enum_index As Long
Dim max_version As Long: max_version = -1
ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE\Classes\TypeLib\{" & guid & "}", 0, KEY_ENUMERATE_SUB_KEYS, reg_key)
If ret <> 0 Then Err.Raise ret, , "Failed to open registry key."
Do
'Store next subkey name in buffer
Dim buffer As String: buffer = Space(BUFFER_SIZE)
Dim cur_buffer_size As Long: cur_buffer_size = BUFFER_SIZE
ret = RegEnumKeyEx(reg_key, enum_index, buffer, cur_buffer_size, ByVal 0&, vbNullString, ByVal 0&, ByVal 0&)
If ret <> 0 Then Exit Do
buffer = Left(buffer, cur_buffer_size)
'Keep most likely version
buffer = Split(buffer, ".")(0)
If Not buffer Like "*[!0-9A-B]*" And Len(buffer) < 4 Then
Dim v As Long: v = CLng("&H" & buffer) 'convert from hex
If v = expected_version Then
max_version = v
Exit Do
ElseIf max_version < v Then
max_version = v
End If
End If
enum_index = enum_index + 1
Loop
RegCloseKey reg_key
If max_version = -1 Then Err.Raise -1, , "Failed to enumerate any viable subkeys."
GetOcxMajorVersion = max_version
End Function
Public Function RemoveFilename(ByVal path As String) As String
Dim folders() As String: folders = Split(Replace(path, "/", "\"), "\")
RemoveFilename = Left(path, Len(path) - Len(folders(UBound(folders))))
End Function
'''Changes any invalid OCX references to newer registered version
Public Sub UpdateFileOCXReferences(ByVal path As String)
Dim file_data As String
Dim changes_made As Boolean
'Read
Dim fn As Long: fn = FreeFile
Open path For Input As fn
While Not EOF(fn)
Dim line As String
Line Input #fn, line
'check for ocx reference line
If LCase(line) Like "object*=*{*-*-*-*-*}[#]*#.#*[#]#*;*.ocx*" Then
'get guid
Dim guid_start As Long: guid_start = InStr(line, "{") + 1
Dim guid_end As Long: guid_end = InStr(line, "}")
Dim guid As String: guid = Mid(line, guid_start, guid_end - guid_start)
'get reference major version
Dim version_start As Long: version_start = InStr(line, "#") + 1
Dim version_end As Long: version_end = InStr(version_start + 1, line, ".")
Dim version_text As String: version_text = Mid(line, version_start, version_end - version_start)
'play it safe
If Len(guid) <> 32 + 4 Then Err.Raise -1, , "GUID has unexpected length."
If Len(version_text) > 4 Then Err.Raise -1, , "Major version is larger than expected."
If guid Like "*[!0-9A-F-]*" Then Err.Raise -1, , "GUID has unexpected characters."
If version_text Like "*[!0-9]*" Then Err.Raise -1, , "Major version isn't an integer."
'get registry major version
Dim ref_version As Long: ref_version = CLng(version_text)
Dim reg_version As Long: reg_version = GetOcxMajorVersion(guid, ref_version)
'change line if necessary
If reg_version < ref_version Then
Err.Raise -1, , "Registered version precedes referenced version."
ElseIf reg_version > ref_version Then
line = Left(line, version_start - 1) & CStr(reg_version) & Mid(line, version_end)
changes_made = True
End If
End If
file_data = file_data & line & vbNewLine
Wend
Close fn
'Write
If changes_made Then
Kill path
Open path For Binary As fn
Put fn, , file_data
Close fn
End If
End Sub
'''Changes any invalid in included files to newer registered version
Public Sub UpdateSubFileOCXReferences(ByVal path As String)
Dim folder As String: folder = RemoveFilename(path)
Dim fn As Long: fn = FreeFile
Open path For Input As fn
While Not EOF(fn)
Dim line As String
Line Input #fn, line
If LCase(line) Like "form=*.frm" _
Or LCase(line) Like "usercontrol=*.ctl" Then
Dim file As String: file = folder & Mid(line, InStr(line, "=") + 1)
If Dir(file) <> "" Then
UpdateFileOCXReferences file
End If
End If
Wend
Close fn
End Sub

Resources