Export Excel data to INI file - vba

Summary: Export values from a column to an INI file.
I have a spreadsheet that has data in column M and I want to copy the data from column M to a an .ini file. Each cell in the column contains a file name of a person's progress report. This data would be like:
Smith, John Progress Report 2015-07-30.doc
The .ini file would be something like:
[Settings]
smith=Smith, John Progress Report 2015-07-30
I would prefer to have the key in lowercase and remove the ".doc" from the filename.
I want to export those filenames to the .ini and use the last name as the key. I don't really know where to start on this. Excel doesn't have its own access to writing to .ini files, but it can use Word or Windows API from what I read on the internet. Also, I guess Excel can just simply append the data to the .ini as a text file.
So I have some ideas, but I didn't see anything else on StackOverflow that specifically worked on this task to get me started. If you find something that does this specifically then that would be great. Otherwise, if someone can get me started on the code to complete this task, I would really appreciate it.

This should get you started. It uses the value from M1 and writes it to an .ini file. Just add a loop to iterate all your rows and you should be all set.
Private 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 Sub ExportToIni()
Dim re As Object
Set re = CreateObject("VBScript.RegExp")
re.IgnoreCase = True
re.Pattern = "^(([^,]+),.+)\.doc$"
Dim strText As String
strText = Trim$(Cells(1, 13)) ' [M1]
Dim m As Object, c As Object, strLastName As String, strFileName As String
If re.Test(strText) Then
Set c = re.Execute(strText)
strFileName = c(0).SubMatches(0)
strLastName = LCase$(c(0).SubMatches(1))
WritePrivateProfileString "Settings", strLastName, strFileName, "c:\path\to\your.ini"
End If
End Sub
The .ini file:
[Settings]
smith=Smith, John Progress Report 2015-07-30

This will do what you want:
Sub SaveIni()
Dim c as range
Open "C:\Temp\Output.ini" For Output As #1
For Each c In Range("M1:M" & Range("M" & Rows.Count).End(xlUp).Row)
Print #1, LCase(Split(c, ",")(0)) & "=" & Split(c, ".doc")(0)
Next
Close #1
End Sub
Uses split on the comma to get the name and lower cases it then adds an = then splits the initial string on .doc to drop that off, we could have used replace here if you so wish.
Here is my test input:
Smith, John Progress Report 2015-07-30.doc
davis, andrew Progress Report 2015-07-30.doc
MALCOLMSON, Aaron File 20jio32ejo23oji32eojie23oj23eoje23joe3e23oji-07-30.doc
Green, Frank Report 20-07-30.doc
Here is my ini file:
smith=Smith, John Progress Report 2015-07-30
davis=davis, andrew Progress Report 2015-07-30
malcolmson=MALCOLMSON, Aaron File 20jio32ejo23oji32eojie23oj23eoje23joe3e23oji-07-30
green=Green, Frank Report 20-07-30
Note, this will OVERWRITE your ini file if it already exists If you wish to append then change this:
Open "C:\Temp\Output.ini" For Output As #1
to this:
Open "C:\Temp\Output.ini" For Append As #1

Related

VB.NET / First row missing when writing to a CSV file

Wrote a program that writes name, postcode, device type, damage type to a CSV file and for some reason it misses the first row.
Thanks
Sub fileWriting(ByVal file As String, ByVal name As String, ByVal postcode As String, ByVal dmgType As String, ByVal devType As String) ' writing to files function
Dim ObjStreamWriter As StreamWriter
ObjStreamWriter = New StreamWriter(file, True)
ObjStreamWriter.Write(name & ",")
ObjStreamWriter.Write(postcode & ",")
ObjStreamWriter.Write(dmgType & ",")
ObjStreamWriter.WriteLine(devType)
ObjStreamWriter.Close()
End Sub
One point on the code above:
ObjStreamWriter = New StreamWriter(file, True)
will append to an existing file, if the file already exists. Therefore the text you're appending may exist further down in the file depending how much you've already written.
Additionally, StreamWriter implements IDisposable so the preferred method of implementation is to instantiate it in a Using block, if you're immediately closing the object.
Using ObjStreamWriter = New StreamWriter(file, True)
ObjStreamWriter.Write(name & ",")
ObjStreamWriter.Write(postcode & ",")
ObjStreamWriter.Write(dmgType & ",")
ObjStreamWriter.WriteLine(devType)
End Using
I'm not sure if these will fix your issue as there's insufficient code to really determine that, but if you're having issues with text not flushing, this ensures that you're automatically doing everything needed to properly close the file. Normally that'd be text missing from the end of the file, not the start, but it might help.

Excel VBA bug accessing HelpFile property from macro-disabled instance?

I think I've stumbled upon a bug in Excel - I'd really like to verify it with someone else though.
The bug occurs when reading the Workbook.VBProject.HelpFile property when the workbook has been opened with the opening application's .AutomationSecurity property set to ForceDisable. In that case this string property returns a (probably) malformed Unicode string, which VBA in turn displays with question marks. Running StrConv(..., vbUnicode) on it makes it readable again, but it sometimes looses the last character this way; this might indicate that the unicode string is indeed malformed or such, and that VBA therefore tries to convert it first and fails.
Steps to reproduce this behaviour:
Create a new Excel workbook
Go to it's VBA project (Alt-F11)
Add a new code module and add some code to it (like e.g. Dim a As Long)
Enter the project's properties (menu Tools... properties)
Enter "description" as Project description and "abc.hlp" as Help file name
Save the workbook as a .xlsb or .xlsm
Close the workbook
Create a new Excel workbook
Go to it's VBA project (Alt-F11)
Add a fresh new code module
Paste the code below in it
Adjust the path on the 1st line so it points to the file you created above
Run the Test routine
The code to use:
Const csFilePath As String = "<path to your test workbook>"
Sub TestSecurity(testType As String, secondExcel As Application, security As MsoAutomationSecurity)
Dim theWorkbook As Workbook
secondExcel.AutomationSecurity = security
Set theWorkbook = secondExcel.Workbooks.Open(csFilePath)
Call MsgBox(testType & " - helpfile: " & theWorkbook.VBProject.HelpFile)
Call MsgBox(testType & " - helpfile converted: " & StrConv(theWorkbook.VBProject.HelpFile, vbUnicode))
Call MsgBox(testType & " - description: " & theWorkbook.VBProject.Description)
Call theWorkbook.Close(False)
End Sub
Sub Test()
Dim secondExcel As Excel.Application
Set secondExcel = New Excel.Application
Dim oldSecurity As MsoAutomationSecurity
oldSecurity = secondExcel.AutomationSecurity
Call TestSecurity("enabled macros", secondExcel, msoAutomationSecurityLow)
Call TestSecurity("disabled macros", secondExcel, msoAutomationSecurityForceDisable)
secondExcel.AutomationSecurity = oldSecurity
Call secondExcel.Quit
Set secondExcel = Nothing
End Sub
Conclusion when working from Excel 2010:
.Description is always readable, no matter what (so it's not like all string properties behave this way)
xlsb and xlsm files result in an unreadable .HelpFile only when macros are disabled
xls files result in an unreadable .HelpFile in all cases (!)
It might be even weirder than that, since I swear I once even saw the questionmarks-version pop up in the VBE GUI when looking at such a project's properties, though I'm unable to reproduce that now.
I realize this is an edge case if ever there was one (except for the .xls treatment though), so it might just have been overlooked by Microsoft's QA department, but for my current project I have to get this working properly and consistently across Excel versions and workbook formats...
Could anyone else test this as well to verify my Excel installation isn't hosed? Preferably also with another Excel version, to see if that makes a difference?
Hopefully this won't get to be a tumbleweed like some of my other posts here :) Maybe "Tumbleweed generator" might be a nice badge to add...
UPDATE
I've expanded the list of properties to test just to see what else I could find, and of all the VBProject's properties (BuildFileName, Description, Filename, HelpContextID, HelpFile, Mode, Name, Protection and Type) only .HelpFile has this problem of being mangled when macros are off.
UPDATE 2
Porting the sample code to Word 2010 and running that exhibits exactly the same behaviour - the .HelpFile property is malformed when macros are disabled. Seems like the code responsible for this is Office-wide, probably in a shared VBA library module (as was to be expected TBH).
UPDATE 3
Just tested it on Excel 2007 and 2003, and both contain this bug as well. I haven't got an Excel XP installation to test it out on, but I can safely say that this issue already has a long history :)
I've messed with the underlying binary representation of the strings in question, and found out that the .HelpFile string property indeed returns a malformed string.
The BSTR representation (underwater binary representation for VB(A) strings) returned by the .HelpFile property lists the string size in the 4 bytes in front of the string, but the following content is filled with the ASCII representation and not the Unicode (UTF16) representation as VBA expects.
Parsing the content of the BSTR returned and deciding for ourselves which format is most likely used fixes this issue in some circumstances. Another issue is unfortunately at play here as well: it only works for even-length strings... Odd-length strings get their last character chopped off, their BSTR size is reported one short, and the ASCII representation just doesn't include the last character either... In that case, the string cannot be recovered fully.
The following code is the example code in the question augmented with this fix. The same usage instructions apply to it as for the original sample code. The RecoverString function performs the needed magic to, well, recover the string ;) DumpMem returns a 50-byte memory dump of the string you pass to it; use this one to see how the memory is layed out exactly for the passed-in string.
Const csFilePath As String = "<path to your test workbook>"
Private Declare Sub CopyMemoryByte Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Byte, ByVal Source As Long, ByVal Length As Integer)
Private Declare Sub CopyMemoryWord Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Integer, ByVal Source As Long, ByVal Length As Integer)
Private Declare Sub CopyMemoryDWord Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Long, ByVal Source As Long, ByVal Length As Integer)
Function DumpMem(text As String) As String
Dim textAddress As LongPtr
textAddress = StrPtr(text)
Dim dump As String
Dim offset As Long
For offset = -4 To 50
Dim nextByte As Byte
Call CopyMemoryByte(nextByte, textAddress + offset, 1)
dump = dump & Right("00" & Hex(nextByte), 2) & " "
Next
DumpMem = dump
End Function
Function RecoverString(text As String) As String
Dim textAddress As LongPtr
textAddress = StrPtr(text)
If textAddress <> 0 Then
Dim textSize As Long
Call CopyMemoryDWord(textSize, textAddress - 4, 4)
Dim recovered As String
Dim foundNulls As Boolean
foundNulls = False
Dim offset As Long
For offset = 0 To textSize - 1
Dim nextByte As Byte
Call CopyMemoryByte(nextByte, textAddress + offset, 1)
recovered = recovered & Chr(CLng(nextByte) + IIf(nextByte < 0, &H80, 0))
If nextByte = 0 Then
foundNulls = True
End If
Next
Dim isNotUnicode As Boolean
isNotUnicode = isNotUnicode Mod 2 = 1
If foundNulls And Not isNotUnicode Then
recovered = ""
For offset = 0 To textSize - 1 Step 2
Dim nextWord As Integer
Call CopyMemoryWord(nextWord, textAddress + offset, 2)
recovered = recovered & ChrW(CLng(nextWord) + IIf(nextWord < 0, &H8000, 0))
Next
End If
End If
RecoverString = recovered
End Function
Sub TestSecurity(testType As String, secondExcel As Application, security As MsoAutomationSecurity)
Dim theWorkbook As Workbook
secondExcel.AutomationSecurity = security
Set theWorkbook = secondExcel.Workbooks.Open(csFilePath)
Call MsgBox(testType & " - helpfile: " & theWorkbook.VBProject.HelpFile & " - " & RecoverString(theWorkbook.VBProject.HelpFile))
Call MsgBox(testType & " - description: " & theWorkbook.VBProject.Description & " - " & RecoverString(theWorkbook.VBProject.Description))
Call theWorkbook.Close(False)
End Sub
Sub Test()
Dim secondExcel As Excel.Application
Set secondExcel = New Excel.Application
Dim oldSecurity As MsoAutomationSecurity
oldSecurity = secondExcel.AutomationSecurity
Call TestSecurity("disabled macros", secondExcel, msoAutomationSecurityForceDisable)
Call TestSecurity("enabled macros", secondExcel, msoAutomationSecurityLow)
secondExcel.AutomationSecurity = oldSecurity
Call secondExcel.Quit
Set secondExcel = Nothing
End Sub

How can I find the installation directory of a specific program?

I have successfully coded some VBA macros for work which basically create a data file, feed it to a program and post-treat the output from this program.
My issue is that the program installation path is hard coded in the macro and the installation may vary accross my colleagues computers.
The first thing I thought is that I can gather from everyone the different installation directories and test for all of them in the code. Hopefully, one of them will work. But it doesn't feel that clean.
So my other idea was to somehow get the installation directory in the code. I thought it would be possible as in Windows, if I right click on a shortcut, I can ask to open the file's directory. What I'm basically looking for is an equivalent in VBA of this right click action in Windows. And that's where I'm stuck.
From what I found, Windows API may get the job done but that's really out of what I know about VBA.
The API FindExecutable seemed not too far from what I wanted but I still can't manage to use it right. So far, I can only get the program running if I already know its directory.
Could you give me some pointers ? Thanks.
Here's another method for you to try. Note that you might see a black box pop up for a moment, that's normal.
Function GetInstallDirectory(appName As String) As String
Dim retVal As String
retVal = Split(CreateObject("WScript.Shell").Exec("CMD /C FOR /r ""C:\"" %i IN (*" & appName & ") DO (ECHO %i)").StdOut.ReadAll, vbCrLf)(2)
GetInstallDirectory = Left$(retVal, InStrRev(retVal, "\"))
End Function
It's not as clean as using API but should get the trick done.
Summary:
retVal = Split(CreateObject("WScript.Shell").Exec("CMD /C FOR /r ""C:\"" %i IN (*" & appName & ") DO (ECHO %i)").StdOut.ReadAll, vbCrLf)(1)
"CMD /C FOR /r ""C:\"" %i IN (*" & appName & ") DO (ECHO %i)" is a command that works in CMD to loop through files rooted at a defined path. We use the wildcard with the appName variable to test for the program we want. (more info on FOR /R here) Here, we have created the CMD application using a Shell object (WScript.Shell) and Executed the command prompt CMD passing arguments to it directly after. The /C switch means that we want to pass a command to CMD and then close the window immediately after it's processed.
We then use .StdOut.ReadAll to read all of the output from that command via the Standard Output stream.
Next, we wrap that in a Split() method and split the output on vbCrLf (Carriage return & Line feed) so that we have a single dimension array with each line of the output. Because the command outputs each hit on a new line in CMD this is ideal.
The output looks something like this:
C:\Users\MM\Documents>(ECHO C:\Program Files\Microsoft
Office\Office14\EXCEL.EXE ) C:\Program Files\Microsoft
Office\Office14\EXCEL.EXE
C:\Users\MM\Documents>(ECHO
C:\Windows\Installer\$PatchCache$\Managed\00004109110000000000000000F01FEC\14.0.4763\EXCEL.EXE
)
C:\Windows\Installer\$PatchCache$\Managed\00004109110000000000000000F01FEC\14.0.4763\EXCEL.EXE
C:\Users\olearysa\Documents>(ECHO
C:\Windows\Installer\$PatchCache$\Managed\00004109110000000000000000F01FEC\14.0.7015\EXCEL.EXE
)
C:\Windows\Installer\$PatchCache$\Managed\00004109110000000000000000F01FEC\14.0.7015\EXCEL.EXE
We're only interested in the third line of the output (the first line is actually blank), so we can access that index of the array directly by using (2) after it (because arrays are zero-indexed by default)
Finally, we only want the path so we use a combination of Left$() (which will return n amount of characters from the left of a string) and InStrRev() (which returns the position of a substring starting from the end and moving backwards). This means we can specify everything from the left until the first occurrence of \ when searching backwards through the string.
Give this a try, assuming you know the name of the .exe:
#If Win64 Then
Declare PtrSafe Function FindExecutable Lib "shell32.dll" Alias "FindExecutableA" _
(ByVal lpFile As String, ByVal lpDirectory As String, ByVal lpResult As String) As Long
#Else
Declare Function FindExecutable Lib "shell32.dll" Alias "FindExecutableA" _
(ByVal lpFile As String, ByVal lpDirectory As String, ByVal lpResult As String) As Long
#End If
Const SYS_OUT_OF_MEM As Long = &H0
Const ERROR_FILE_NOT_FOUND As Long = &H2
Const ERROR_PATH_NOT_FOUND As Long = &H3
Const ERROR_BAD_FORMAT As Long = &HB
Const NO_ASSOC_FILE As Long = &H1F
Const MIN_SUCCESS_LNG As Long = &H20
Const MAX_PATH As Long = &H104
Const USR_NULL As String = "NULL"
Const S_DIR As String = "C:\" '// Change as required (drive that .exe will be on)
Function GetInstallDirectory(ByVal usProgName As String) As String
Dim fRetPath As String * MAX_PATH
Dim fRetLng As Long
fRetLng = FindExecutable(usProgName, S_DIR, fRetPath)
If fRetLng >= MIN_SUCCESS_LNG Then
GetInstallDirectory = Left$(Trim$(fRetPath), InStrRev(Trim$(fRetPath), "\"))
End If
End Function
Example of how to use, let's try looking for Excel:
Sub ExampleUse()
Dim x As String
x = "EXCEL.EXE"
Debug.Print GetInstallDirectory(x)
End Sub
Output (on my machine anyway) is
C:\Program Files\Microsoft Office\Office14\
Assuming you are working on PC only and the people are working with their own copies and not a shared network copy. I would recommend the following.
Create a Sheet called 'Config', place the path with the exe in there, and then hide it.
Use use FileScriptingObject ('Tools' > 'References' > 'Microsoft Scripting Runtime') to see if the path in 'Config' exists
If it does not, ask the user for the location using a 'open file dialog box' and remember that in the 'Config' Sheet for next time.
The below code may help as a pointer.
Dim FSO As New FileSystemObject
Private Function GetFilePath() As String
Dim FlDlg As FileDialog
Dim StrPath As String
Set FlDlg = Application.FileDialog(msoFileDialogOpen)
With FlDlg
.Filters.Clear
.Filters.Add "Executable Files", "*.exe"
.AllowMultiSelect = False
.ButtonName = "Select"
.Title = "Select the executable"
.Show
If .SelectedItems.Count <> 0 Then GetFilePath = .SelectedItems(1)
End With
Set FlDlg = Nothing
End Function
Private Function FileExists(ByVal StrPath As String) As Boolean
FileExists = FSO.FileExists(StrPath)
End Function

How do I fix my VBA renaming program's 0 return?

I am open to completely changing this code. The link to the original is in the code itself. I'm sure there's an easier way to do it and the actual renaming part is NOT my own code, so I will redo it so it isn't plagiarizing. I can't use a batch file renamer to do it; I need to make it myself to stay out of trouble with legal :) No grey area!
Anyways, after a few dozen attempts on my own, I finally caved and grabbed this code online that is supposed to rename the files I specify. I edited it to fit my parameters and assigned variables/directories. When I run it, however, I always get a return of zero and the files are not being renamed. The one thing I could think of is that this directory is going to the full path name of the folder instead of the part after the last "\". But I'm not sure how to fix this either. I thought about trying to tell it to only tell it to pull, say the last 8 characters of the string, but that won't work either as these string lengths will vary anywhere from one character to 20 or so characters.
Here is my code:
Private Sub Apply_Click()
'This will initiate Module 1 to do a batch rename to find and replace all
'Module 1 will then initiate the resolving links process
Dim intResponse As Integer 'Alerts user to wait until renaming is complete
intResponse = MsgBox("Your folders are being updated. Please wait while your files are renamed and your links are resolved.")
If intResponse = vbOK Then 'Tests to see if msgbox_click can start a new process
Dim i As Integer
Dim from_str As String
Dim to_str As String
Dim dir_path As String
from_str = Old_Name_Display.Text
to_str = New_Name.Text
dir_path = New_Name.Text
If Right$(dir_path, 1) <> "\" Then dir_path = dir_path _
& "\"
Old_Name_Display = dir$(dir_path & "*.*", vbNormal)
Do While Len(Old_Name_Display) > 0
' Rename this file.
New_Name = Replace$(Old_Name_Display, from_str, to_str)
If New_Name <> Old_Name_Display Then
Name Old_Name_Display.Text As New_Name.Text
i = i + 1
End If
' Get the next file.
Old_Name_Display = dir$()
Loop
MsgBox "Renamed " & Format$(i) & " files. Resolving links now."
If intResponse = vbOK Then
MsgBox "You selected okay. Good luck coding THIS." 'Filler line to test that next step will be ready to initialize
Else: End
End If
Exit Sub
'Most of batch renaming process used from VB Helper, sponsored by Rocky Mountain Computer Consulting, Inc. Copyright 1997-2010; original code available at http://www.vb-helper.com/howto_rename_files.html
End Sub
Does anyone have another theory on why I get a 0 return/how to fix that potential above problem?
It doesn't look like the directory is getting referenced in the rename.
Change
Name Old_Name_Display.Text As New_Name.Text
to
Name Dir_Path & Old_Name_Display.Text As Dir_Path & New_Name.Text

How to find or get files in Directory with Specific word in the file name Visual Basic.net?

I need to get files from a directory containing specific characters in it's name:
The following code below will return any file with the .csv extension. The problem is there are other csv file I need to leave alone or not get.
Dim FileLocation As DirectoryInfo = _
New DirectoryInfo("C:\Folder\Subfolder\Data\Input\")
Dim fi As FileInfo() = FileLocation.GetFiles("*.csv")
Instead of getting any csv file, I would like to get a file with the word data, so any file name containing the word data. Example: *my_data_file.csv*
How do I do this with the code above?
You can update the filter with the string you want to account for (caps will automatically be taken care of):
Dim fi As FileInfo() = FileLocation.GetFiles("*data*.csv")
In any case, bear in mind that this filtering is not "too accurate". For example, the code above would also account for any file (including "data"), whose extension includes csv (e.g., *.csva, *.csvb, etc.). If you want a 100%-reliable approach you should better set up a loop and carry out the filtering "manually"; loops are pretty fast and you wouldn't even notice the difference.
Example of a loop:
Dim fi As List(Of FileInfo) = New List(Of FileInfo)
For Each File In FileLocation.GetFiles()
If (File IsNot Nothing) Then
If (Path.GetExtension(File.ToString.ToLower) = ".csv") Then
If (File.ToString.ToLower.Contains("data")) Then fi.Add(File)
End If
End If
Next
This code will work for sure under your exact requirements and might take care of more complex requests. I have accounted for a List just to show the point clearer.
If you can use LINQ extensions then you can do it this way:
' Get Files {directory} {recursive} {ext} {word in filename}
Private Function Get_Files(ByVal directory As String, _
ByVal recursive As IO.SearchOption, _
ByVal ext As String, _
ByVal with_word_in_filename As String) As List(Of IO.FileInfo)
Return IO.Directory.GetFiles(directory, "*" & If(ext.StartsWith("*"), ext.Substring(1), ext), recursive) _
.Where(Function(o) o.ToLower.Contains(with_word_in_filename.ToLower)) _
.Select(Function(p) New IO.FileInfo(p)).ToList
End Function
Usage example:
For Each file As IO.FileInfo In Get_Files("C:\Folder\Subfolder\Data\Input\", _
IO.SearchOption.TopDirectoryOnly, _
"csv", _
"data")
MsgBox(file.Name)
Next
Replace the wildcard search below "." with your search criteria, for example you want all files that start with name "Hospital*"
Dim Folder As New IO.DirectoryInfo("C:\SampleFolder")
For Each File as IO.FileInfo in Folder.GetFiles("*.*",IO.SearchOption.AllDirectories)
ListBox1.Items.Add(File.FullName)
Next
I would have added this as a comment to the accepted answer, but I do not have enough points to do so:
I just wanted to add varocarbas's answer that, if anyone was wondering (as I was) if this would work in a web scenario as well, it will. Just place the web path inside Server.MapPath() like this:
Dim FileLocation As DirectoryInfo =
New DirectoryInfo(Server.MapPath("/Folder/SubFolder/Data/Input/"))
NOTE: Will NOT work with full url's (no 'http://www.123.com').
Dim Folder As New IO.DirectoryInfo("C:\SampleFolder")
For Each File as IO.FileInfo in Folder.GetFiles("*.*",IO.SearchOption.AllDirectories)
ListBox1.Items.Add(File.FullName)
Application.DoEvents()
Next