Faster string comparison in vb6 - performance

Am trying to make my vb6 app run faster, the reason is that am populating vbaccelerators sgrid with about 10k items all at once (this is a requirement from client).
I had to populate about 20 columns for each of the 10k items, and i have to perform string comparison in about more than half of them, so i wrote a string comparison function and did profiling
Function IsEqual(byval value1 as string, Byval value2 as string) as boolean
' content, various versions are below
End function
currently the items = 5000 and each of the time below shows the time it took and various versions of the function:
LCase$(Value1) = LCase$(value2)
time: 29149 ms
(StrComp(Value1, value2, 1) = 0 )
time: 30836 ms
If StrComp(Value1, value2, 1) = 0 Then
IsEqual = True
Else
IsEqual = False
End If
time 34180 ms
If StrComp(Value1, value2, 1) = 0 Then IsEqual = True
time 28387 ms
Timing is done with:
Declare Function timeBeginPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
Declare Function timeEndPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
Declare Function timeGetTime Lib "winmm.dll" () As Long
which returns the time in milliseconds.
Is there anyway to make the comparison faster?

Things that might improve performance include..
Change your parameters to ByRef
Using ByVal parameters copies the variables onto the stack. While this is a generally a good idea, if your comparison function is well-behaved and doesn't change the variables, making an extra copy of the data is not necessary.
Populate the grid on demand,
Only populate the parts of the grid that are showing on screen - track this with grid movement events. There are even grid controls for VB6 that facilitate this by letting you define "virtual" items and raising events to let you know which ones you need to populate. TList is the one I'm familiar with - I'll temper that suggestion with the caveat that it's licensing model can be a real PITA to work with.

This should be pretty fast.
Option Explicit
Private Declare Sub DerefByte Lib "msvbvm60" Alias "GetMem1" (ByVal Add As Long, ByRef Value As Byte)
Private Declare Sub DerefLong Lib "msvbvm60" Alias "GetMem4" (ByVal Add As Long, ByRef Value As Long)
Private Sub Form_Load()
Debug.Print IsEqual("Hello", "hello")
Debug.Print IsEqualB("Hello", "hello")
End Sub
Public Function IsEqualB(Str1 As String, Str2 As String) As Boolean
Dim lpS1 As Long, lpS2 As Long
Dim t1 As Byte, t2 As Byte
Dim lSz As Long
Dim i As Long
IsEqualB = True
lpS1 = StrPtr(Str1)
lpS2 = StrPtr(Str2)
DerefLong lpS1 - 4, lSz
If lSz = LenB(Str2) Then
For i = 0 To lSz - 1 Step 2
DerefByte lpS1 + i, t1
DerefByte lpS2 + i, t2
If Not (t1 = t2) Then
IsEqualB = False
Exit For
End If
Next
Else
IsEqualB = False
End If
End Function
Public Function IsEqual(Str1 As String, Str2 As String) As Boolean
Dim lpS1 As Long, lpS2 As Long
Dim t1 As Byte, t2 As Byte
Dim lSz As Long
Dim i As Long
IsEqual = True
lpS1 = StrPtr(Str1)
lpS2 = StrPtr(Str2)
DerefLong lpS1 - 4, lSz
If lSz = LenB(Str2) Then
For i = 0 To lSz - 1 Step 2
DerefByte lpS1 + i, t1
DerefByte lpS2 + i, t2
If Not (t1 Or &H20) = (t2 Or &H20) Then
IsEqual = False
Exit For
End If
Next
Else
IsEqual = False
End If
End Function
The basic premise here is to do byte by byte comparison mod 2 over the Unicode strings. One of the above functions is case sensitive, IsEqualB, then other is insensitive IsEqual.
Of course, it uses a couple of undocumented functions in the Visual Basic 6 runtime: but if you want speed, that's what you have to do, unfortunately.

Have you tried:
Function IsEqual(byval value1 as string, Byval value2 as string) as boolean
Return StrComp(LCase$(Value1), LCase$(value2), vbBinaryCompare) = 0
End function

You can probably cut your execution time in half by using "OPTION COMPARE TEXT". Place this line at the top of your code module.
OPTION COMPARE TEXT
This line, when used, will cause string compare within the code module be case insensitive. Because of this, you can simply use:
Function IsEqual(byval value1 as string, Byval value2 as string) as boolean
IsEqual = (Value1 = Value2)
End Function

look at the LockWindowUpdate() WinAPI call. This can really help grids when you are populating them . Make sure you call it once to lock the window and once to unlock it.

Related

Byte shifting / casting VB6

I'm extremely unfamiliar with VB6 so please excuse the rookie question:
I'm attempting to turn a long into it's component bytes. In C it is simple because of the automatic truncation and the bitshift operators. For the life of me I cannot figure out how to do this in VB6.
Attempts so far have all generally looked something like this
sys1 = CByte(((sys & &HFF000000) / 16777216)) ' >> 24
sys2 = CByte(((sys & &HFF0000) / 65536)) ' >> 16
sys1 and sys2 are declared as Byte and sys is declared as Long
I'm getting a type mismatch exception when I try to do this. Anybody know how to convert a Long into 4 Bytes ??
Thanks
You divide correctly, but you forgot to mask out only the least significant bits.
Supply the word you want to divide into bytes, and the index (0 is least significant, 1 is next, etc.)
Private Function getByte(word As Long, index As Integer) As Byte
Dim lTemp As Long
' shift the desired bits to the 8 least significant
lTemp = word / (2 ^ (index * 8))
' perform a bit-mask to keep only the 8 least significant
lTemp = lTemp And 255
getByte = lTemp
End Function
Found on FreeVBCode.com. Not tested, sorry.
Option Explicit
Private Declare Sub CopyMemory Lib "kernel32" _
Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal _
Length As Long)
Public Function LongToByteArray(ByVal lng as Long) as Byte()
'Example:
'dim bytArr() as Byte
'dim iCtr as Integer
'bytArr = LongToByteArray(90121)
'For iCtr = 0 to Ubound(bytArr)
'Debug.Print bytArr(iCtr)
'Next
'******************************************
Dim ByteArray(0 to 3)as Byte
CopyMemory ByteArray(0), Byval VarPtr(Lng),Len(Lng)
LongToByteArray = ByteArray
End Function
You can convert between simple value types and Byte arrays by combining UDTs and the LSet statement.
Option Explicit
Private Type DataBytes
Bytes(3) As Byte
End Type
Private Type DataLong
Long As Long
End Type
Private DB As DataBytes
Private DL As DataLong
Private Sub cmdBytesToLong_Click()
Dim I As Integer
For I = 0 To 3
DB.Bytes(I) = CByte("&H" & txtBytes(I).Text)
Next
LSet DL = DB
txtLong.Text = CStr(DL.Long)
txtBytes(0).SetFocus
End Sub
Private Sub cmdLongToBytes_Click()
Dim I As Integer
DL.Long = CLng(txtLong.Text)
LSet DB = DL
For I = 0 To 3
txtBytes(I).Text = Right$("0" & Hex$(DB.Bytes(I)), 2)
Next
txtLong.SetFocus
End Sub

Need to create a 30 second delay in Visual Basic 6

How can I create a 30 second delay in visual basic 6? I simply want VB6 to wait 30 seconds before moving on to the next line of code!!
The best approach that I know of to accomplish this in VB6 is to include a call to WaitForSingleObject or some other similar Wait API function in your loop. A good example of this approach is the MsgWaitObj function written by Sergey Merzlikin (source article):
Option Explicit
'********************************************
'* (c) 1999-2000 Sergey Merzlikin *
'********************************************
Private Const STATUS_TIMEOUT = &H102&
Private Const INFINITE = -1& ' Infinite interval
Private Const QS_KEY = &H1&
Private Const QS_MOUSEMOVE = &H2&
Private Const QS_MOUSEBUTTON = &H4&
Private Const QS_POSTMESSAGE = &H8&
Private Const QS_TIMER = &H10&
Private Const QS_PAINT = &H20&
Private Const QS_SENDMESSAGE = &H40&
Private Const QS_HOTKEY = &H80&
Private Const QS_ALLINPUT = (QS_SENDMESSAGE Or QS_PAINT _
Or QS_TIMER Or QS_POSTMESSAGE Or QS_MOUSEBUTTON _
Or QS_MOUSEMOVE Or QS_HOTKEY Or QS_KEY)
Private Declare Function MsgWaitForMultipleObjects Lib "user32" _
(ByVal nCount As Long, pHandles As Long, _
ByVal fWaitAll As Long, ByVal dwMilliseconds _
As Long, ByVal dwWakeMask As Long) As Long
Private Declare Function GetTickCount Lib "kernel32" () As Long
' The MsgWaitObj function replaces Sleep,
' WaitForSingleObject, WaitForMultipleObjects functions.
' Unlike these functions, it
' doesn't block thread messages processing.
' Using instead Sleep:
' MsgWaitObj dwMilliseconds
' Using instead WaitForSingleObject:
' retval = MsgWaitObj(dwMilliseconds, hObj, 1&)
' Using instead WaitForMultipleObjects:
' retval = MsgWaitObj(dwMilliseconds, hObj(0&), n),
' where n - wait objects quantity,
' hObj() - their handles array.
Public Function MsgWaitObj(Interval As Long, _
Optional hObj As Long = 0&, _
Optional nObj As Long = 0&) As Long
Dim T As Long, T1 As Long
If Interval <> INFINITE Then
T = GetTickCount()
On Error Resume Next
T = T + Interval
' Overflow prevention
If Err <> 0& Then
If T > 0& Then
T = ((T + &H80000000) _
+ Interval) + &H80000000
Else
T = ((T - &H80000000) _
+ Interval) - &H80000000
End If
End If
On Error GoTo 0
' T contains now absolute time of the end of interval
Else
T1 = INFINITE
End If
Do
If Interval <> INFINITE Then
T1 = GetTickCount()
On Error Resume Next
T1 = T - T1
' Overflow prevention
If Err <> 0& Then
If T > 0& Then
T1 = ((T + &H80000000) _
- (T1 - &H80000000))
Else
T1 = ((T - &H80000000) _
- (T1 + &H80000000))
End If
End If
On Error GoTo 0
' T1 contains now the remaining interval part
If IIf((T1 Xor Interval) > 0&, _
T1 > Interval, T1 < 0&) Then
' Interval expired
' during DoEvents
MsgWaitObj = STATUS_TIMEOUT
Exit Function
End If
End If
' Wait for event, interval expiration
' or message appearance in thread queue
MsgWaitObj = MsgWaitForMultipleObjects(nObj, _
hObj, 0&, T1, QS_ALLINPUT)
' Let's message be processed
DoEvents
If MsgWaitObj <> nObj Then Exit Function
' It was message - continue to wait
Loop
End Function
Keep in mind that the Sleep solution in #sanderd's answer will actually lock the application. In other words, all the UI pieces will be unresponsive.
If your aim is to simply prevent the control from moving on to the next line while allowing the UI to be responsive, there are other choices.
One is to loop for 30 seconds in the following manner:
' Module Level
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
' Inside a method
Dim dt as Date
dt = Now
Do While DateDiff("s", dt, now) < 30
DoEvents
Sleep 50 ' put your app to sleep in small increments
' to avoid having CPU go to 100%
Loop
This is not the most elegant way to achieve what you want but it gets the job done.
System.Threading.Thread.Sleep(30*1000) (#Moox my bad ;))
Note that calling this method in your UI-thread will hang the entire application while waiting for 30 seconds. A better alternative would be spawning a new thread for the code you want to execute.
edit:
Since your other questions are about VB6, here's a link that provides a VB6 Sleep method:
http://www.freevbcode.com/ShowCode.Asp?ID=7556
System.Threading.Thread.Sleep(100)
For the sake of a complete set of solutions:
If you're using vb6 in an application environment (like excel), you can use
Application.OnTime (now + TimeValue("00:00:30")), "ProcedurePart2"
to call a procedure (with no parameters) after 30 seconds without locking up the application or consuming CPU power.
This would, for instance, work in any VBA environment. Depending on the nature of your VB6 app (standalone vs add-on), this option may be available to you.
Put the rest of the code in a sub and then initialize a 30 second timer to call that sub. Naturally deactivate the timer before exiting.
How about this?
Public Sub delay(PauseTime as integer)
Dim start As single
start = Timer
Do While Timer < start + PauseTime
If (Timer < start) Then 'midnight crossover
start = start - (86400 + 1)
End If
DoEvents ' Yield to other processes.
Loop
End Sub
I think Timer is the best solution , but there is a little modification.
The only method to disable the timer is to Raise an event to do this.
Example:
'Global Declaration
Private event TimeReached()
dim counter as integer
Sub Timer_Tick () handles Timer.tick
if counter>1 then
RaiseEvent TimeReached
else
counter+=1
end sub
sub TimeHandler () handles me.TimeReached
Timer.enabled=false
'Code To be Processed Here
end sub
I tried too many different ways to make a delay , and this is the most quickest method i've ever discovered
This is a program to display message after 30 second when you click a button.
sub button1_click () handles button1.click
Dim instance =now
do while now.subtract(instance).seconds<30
loop
msgbox("30 seconds passed")
endsub
Sub delay()
Dim a As Integer = TimeOfDay.Second
Do Until TimeOfDay.Second = a + 30
Loop
End Sub

Printer Page Size Problem

I am trying to set a custom paper size by doing:
Printer.Height = 2160
Printer.Width = 11900
But it doesn't seen to have any effect. After setting this up, i ask for that values and it returns the default ones. And this:
Printer.PaperSize = 256
Returns an error...
Any ideas??
Either your printer doesn't allow these properties to be set, or you're exceeding their maximum allowed values. From the Visual Basic Reference
If you set the Height and Width
properties for a printer driver that
doesn't allow these properties to be
set, no error occurs and the size of
the paper remains as it was. If you
set Height and Width for a printer
driver that allows only certain values
to be specified, no error occurs and
the property is set to whatever the
driver allows. For example, you could
set Height to 150 and the driver would
set it to 144.
I don't know why you're getting an error when you set the Papersize property to 256. It works for me. Also, the documentation states, "Setting a printer's Height or Width property automatically sets PaperSize to vbPRPSUser.", which equals 256.
I was actually involved with the same problem but I just happen to find a breakthrough.
First you need to create a custom form that defines you custom paper size. Then, you need to
refer to Windows API to check the form name you've just created. You'll get the for name
from an array returned from a function and use the array index where the form name was found.
Finally use it as the value for printer.papersize
Example below:
Public Type PRINTER_DEFAULTS
pDatatype As Long
pDevMode As Long
DesiredAccess As Long
End Type
Public Type FORM_INFO_1
Flags As Long
pName As Long ' String
Size As SIZEL
ImageableArea As RECTL
End Type
Public Declare Function EnumForms Lib "winspool.drv" Alias "EnumFormsA" _
(ByVal hPrinter As Long, ByVal Level As Long, ByRef pForm As Any, _
ByVal cbBuf As Long, ByRef pcbNeeded As Long, _
ByRef pcReturned As Long) As Long
Public Declare Sub CopyMemory Lib "KERNEL32" Alias "RtlMoveMemory" _
(pDest As Any, pSource As Any, ByVal cbLength As Long)
Public Declare Sub Sleep Lib "KERNEL32" (ByVal dwMilliseconds As Long)
Public Declare Function OpenPrinter Lib "winspool.drv" Alias _
"OpenPrinterA" (ByVal pPrinterName As String, phPrinter As Long, _
pDefault As PRINTER_DEFAULTS) As Long
Public Declare Function ClosePrinter Lib "winspool.drv" _
(ByVal hPrinter As Long) As Long
Public Declare Function lstrcpy Lib "KERNEL32" Alias "lstrcpyA" _
(ByVal lpString1 As String, ByRef lpString2 As Long) As Long
'UDF
Public Function PtrCtoVbString(ByVal Add As Long) As String
Dim sTemp As String * 512, x As Long
x = lstrcpy(sTemp, ByVal Add)
If (InStr(1, sTemp, Chr(0)) = 0) Then
PtrCtoVbString = ""
Else
PtrCtoVbString = Left(sTemp, InStr(1, sTemp, Chr(0)) - 1)
End If
End Function
Public Function IsFormExist(ByVal DeviceName As String, ByVal isFormName As String, ByVal PrinterHandle As Long) As Long
Dim NumForms As Long, i As Long
Dim FI1 As FORM_INFO_1
Dim pd As PRINTER_DEFAULTS
Dim aFI1() As FORM_INFO_1 ' Working FI1 array
Dim Temp() As Byte ' Temp FI1 array
Dim FormIndex As Integer
Dim BytesNeeded As Long
Dim RetVal As Long
On Error GoTo cleanup
FormIndex = 0
ReDim aFI1(1)
' First call retrieves the BytesNeeded.
RetVal = OpenPrinter(DeviceName, PrinterHandle, pd)
If (RetVal = 0) Or (PrinterHandle = 0) Then
'Can't access current printer. Bail out doing nothing
Exit Function
End If
RetVal = EnumForms(PrinterHandle, 1, aFI1(0), 0&, BytesNeeded, NumForms)
ReDim Temp(BytesNeeded)
ReDim aFI1(BytesNeeded / Len(FI1))
' Second call actually enumerates the supported forms.
RetVal = EnumForms(PrinterHandle, 1, Temp(0), BytesNeeded, BytesNeeded, _
NumForms)
Call CopyMemory(aFI1(0), Temp(0), BytesNeeded)
For i = 0 To NumForms - 1
With aFI1(i)
If isFormName = PtrCtoVbString(.pName) Then
' Found the desired form
FormIndex = i + 1
Exit For
End If
End With
Next i
IsFormExist = FormIndex ' Returns the number when form is found.
cleanup:
'Release the printer handle
If (PrinterHandle <> 0) Then Call ClosePrinter(PrinterHandle)
End Function
'Here We Go
dim papercode as long, printername as string, formname as string
printername=printer.Devicename
formname = "myform"
papercode=IsFormExist(printername, formname, Printer.hdc)
if papercode<>0 then
printer.papersize=papercode
end if
Give it a try, good luck
Are you sure the error isn't related to the maximum print width of the printer itself? Many printers have a max print width of 8.25" (11880) to allow 1/4" margins on either side of a 8.5" wide paper.
Quickest way to check would be to simply set the print wide to 11880 or lower and see if it works.
Another possibility would be permissions to the printer. If it's a shared network resource it may be locked down.
The solution is to use windows 98. It does not work with win2k, neither winXP. The same code, the same printer.
Regards.
I'm testing this code, but I can not see the custom form I created using printers and scanners in the Control Panel Windows XP Professional SP3.
Note: I could check in regedit that this form exists and its ID is 512 in a string value and it contains the name of the form created in the printers control panel.
Why this function does not return my custom form, I am using an HP Laserjet 1020.

VB6 Type Mismatch in For loop condition

I have been trying to find out why in the following code, the third time through the loop I am getting a Error type 13 Mismatch when the line "For lCount = 0 To maxCount" is being evaluated. I had originally thought the problem was in getting the value from the vArray, but testing shows it to be triggered by the "For" line. I haven't a clue as to how the type would be changing during the processing of the loop. Thanks!
Public Function FindCodeIndex(vArray As Variant, MatchValue As String) As Integer
''This function locates a value in a combo box returning the index or -1 if not found
Dim lCount As Long
Dim maxCount As Long
Dim arrayStr As String
On Error GoTo ErrorHandler
maxCount = UBound(vArray)
For lCount = 0 To maxCount
arrayStr = vArray(1, lCount)
If UCase$(arrayStr) = UCase$(MatchValue) Then
FindCodeIndex = Int(lCount)
Exit Function
End If
Next lCount
FindCodeIndex = -1
Exit Function
ErrorHandler:
MsgBox "Unexpected error in frmComment::FindCodeIndex()" & vbCrLf & _
"Error Code: " & CStr(Err.Number) & " Error Desc: " & Err.Description
Public Function FindCodeIndex(Array() As String, ByVal MatchValue As String) As Long
Dim index As Long
Dim upper_bound As Long
upper_bound= UBound(Array)
MatchValue = UCase(MatchValue)
For index = 0 To upper_bound
If UCase(Array(index)) = MatchValue Then
FindCodeIndex = index
Exit Function
End If
Next index
FindCodeIndex = -1
End Function
The function mentions that the code is being written for a ComboBox (are you actually copying each item in the List() method into an array and sending this to your function?). This seems a little over-complicated if you are using the standard VB ComboBox. Just use the following code:
Private Declare Function SendMessage Lib "User32.dll" Alias "SendMessageA" (ByVal hWnd As Long, ByVal uMsg As Long, ByRef wParam As Any, ByRef lParam As Any) As Long
Private Const CB_FINDSTRINGEXACT As Long = &H158
Public Function FindCodeIndex(ByRef cmb As ComboBox, ByRef sMatchValue As String) As Long
'This function locates a value in a combo box returning the index or -1 if not found
FindCodeIndex = SendMessage(cmb.hWnd, CB_FINDSTRINGEXACT, ByVal -1, ByVal sMatchValue
End Function
It is a lot quicker and smaller to use the Windows API in this case.

Modern day, unicode-friendly ".ini file" to store config data in VB6

I'd like to store the contents of a data structure, a few arrays, and a dozen or so variables in a file that can be saved and reloaded by my software as well as optionally edited in a text editor by a user reloaded. For the text editing, I need the data to be clearly labeled, like in a good ole .ini file:
AbsMaxVoltage = 17.5
There's a GUI, and one could argue that the user should just load and save and modify from the GUI, but the customer wants to be able to read and modify the data as text.
It's easy enough to write code to save it and reload it (assuming all the labels are in the same place and only the data has changed). With more work (or using some of the INI R/W code that's already out there I could pay attention to the label so if a line gets deleted or moved around the variables still get stuffed correctly, but both of these approaches seem pretty old-school. So I'm interested in how the brightest minds in programming would approach this today (using decade-old VB6 which I have to admit I still love).
Disclaimer: I'm an electrical engineer, not a programmer. This isn't my day job. Well maybe it's a few % of my day job.
Cheers!
Lots of people will recommend XML to you. The problem is XML is still so trendy, some people use it everywhere without really thinking about it.
Like Jeff Atwood said, it's hard for non-programmers to read XML and particularly to edit it. There are too many rules, like escaping special characters and closing the tags in the correct order. Some experts recommend you treat XML as a binary format, not a text format at all.
I recommend using INI files, provided the maximum size limit of 32K is not a problem. I've never reached that limit in many similar situations in my own VB6. INI files are easy for ordinary folk to edit, and it's easy to read and write them from VB6. Just use some of the excellent drop-in code freely available on the web.
I'm sure the class Jay Riggs
provided in his answer is excellent, because
it's from VBAccelerator.
I would also recommend this class,
because anything by Karl Peterson
will be excellent too.
A couple of other points to think about:
Have you considered which directory to put the files into?
You mentioned "Unicode-friendly" in the question. INI files aren't Unicode, but that doesn't matter in practise. Unless you want to store characters that aren't supported on the current code page - like Chinese on an English computer - an unusual requirement, and one that will cause you other problems in a VB6 program anyway.
Legendary Windows guru Raymond Chen described the advantages of XML configuration files over INI files. Many of them rely on the XML file being read-only. The one legitimate advantage is if the data is highly structured - class heirarchies or the like. From your description that doesn't apply.
Consider using XML. It's completely standard, many text editors will highlight/manage it properly, every programming language and script language on Earth has good support for reading it, and it handles Unicode perfectly.
For simple name/value pairs as you suggest, it's quite readable. But you have the added advantage that if someday you need something more complex -- e.g. multi-lined values or a list of distinct values -- XML provides natural, easy ways of representing that.
P.S. Here's how to read XML in VB6.
Back in the olden days this class helped me use INI files with my VB6 programs:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "cInifile"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
' =========================================================
' Class: cIniFile
' Author: Steve McMahon
' Date : 21 Feb 1997
'
' A nice class wrapper around the INIFile functions
' Allows searching,deletion,modification and addition
' of Keys or Values.
'
' Updated 10 May 1998 for VB5.
' * Added EnumerateAllSections method
' * Added Load and Save form position methods
' =========================================================
Private m_sPath As String
Private m_sKey As String
Private m_sSection As String
Private m_sDefault As String
Private m_lLastReturnCode As Long
#If Win32 Then
' Profile String functions:
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
Private Declare Function GetPrivateProfileString Lib "KERNEL32" Alias
"GetPrivateProfileStringA" (ByVal lpApplicationName As Any, ByVal
lpKeyName As Any, ByVal lpDefault As Any, ByVal lpReturnedString As
String, ByVal nSize As Long, ByVal lpFileName As String) As Long
#Else
' Profile String functions:
Private Declare Function WritePrivateProfileString Lib "Kernel" (ByVal
lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpString As
Any, ByVal lpFileName As String) As Integer
Private Declare Function GetPrivateProfileString Lib "Kernel" (ByVal
lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As
Any, ByVal lpReturnedString As String, ByVal nSize As Integer, ByVal
lpFileName As String) As Integer
#End If
Property Get LastReturnCode() As Long
LastReturnCode = m_lLastReturnCode
End Property
Property Get Success() As Boolean
Success = (m_lLastReturnCode <> 0)
End Property
Property Let Default(sDefault As String)
m_sDefault = sDefault
End Property
Property Get Default() As String
Default = m_sDefault
End Property
Property Let Path(sPath As String)
m_sPath = sPath
End Property
Property Get Path() As String
Path = m_sPath
End Property
Property Let Key(sKey As String)
m_sKey = sKey
End Property
Property Get Key() As String
Key = m_sKey
End Property
Property Let Section(sSection As String)
m_sSection = sSection
End Property
Property Get Section() As String
Section = m_sSection
End Property
Property Get Value() As String
Dim sBuf As String
Dim iSize As String
Dim iRetCode As Integer
sBuf = Space$(255)
iSize = Len(sBuf)
iRetCode = GetPrivateProfileString(m_sSection, m_sKey, m_sDefault, sBuf,
iSize, m_sPath)
If (iSize > 0) Then
Value = Left$(sBuf, iRetCode)
Else
Value = ""
End If
End Property
Property Let Value(sValue As String)
Dim iPos As Integer
' Strip chr$(0):
iPos = InStr(sValue, Chr$(0))
Do While iPos <> 0
sValue = Left$(sValue, (iPos - 1)) & Mid$(sValue, (iPos + 1))
iPos = InStr(sValue, Chr$(0))
Loop
m_lLastReturnCode = WritePrivateProfileString(m_sSection, m_sKey, sValue,
m_sPath)
End Property
Public Sub DeleteKey()
m_lLastReturnCode = WritePrivateProfileString(m_sSection, m_sKey, 0&,
m_sPath)
End Sub
Public Sub DeleteSection()
m_lLastReturnCode = WritePrivateProfileString(m_sSection, 0&, 0&, m_sPath)
End Sub
Property Get INISection() As String
Dim sBuf As String
Dim iSize As String
Dim iRetCode As Integer
sBuf = Space$(8192)
iSize = Len(sBuf)
iRetCode = GetPrivateProfileString(m_sSection, 0&, m_sDefault, sBuf, iSize,
m_sPath)
If (iSize > 0) Then
INISection = Left$(sBuf, iRetCode)
Else
INISection = ""
End If
End Property
Property Let INISection(sSection As String)
m_lLastReturnCode = WritePrivateProfileString(m_sSection, 0&, sSection,
m_sPath)
End Property
Property Get Sections() As String
Dim sBuf As String
Dim iSize As String
Dim iRetCode As Integer
sBuf = Space$(8192)
iSize = Len(sBuf)
iRetCode = GetPrivateProfileString(0&, 0&, m_sDefault, sBuf, iSize, m_sPath)
If (iSize > 0) Then
Sections = Left$(sBuf, iRetCode)
Else
Sections = ""
End If
End Property
Public Sub EnumerateCurrentSection(ByRef sKey() As String, ByRef iCount As Long)
Dim sSection As String
Dim iPos As Long
Dim iNextPos As Long
Dim sCur As String
iCount = 0
Erase sKey
sSection = INISection
If (Len(sSection) > 0) Then
iPos = 1
iNextPos = InStr(iPos, sSection, Chr$(0))
Do While iNextPos <> 0
sCur = Mid$(sSection, iPos, (iNextPos - iPos))
If (sCur <> Chr$(0)) Then
iCount = iCount + 1
ReDim Preserve sKey(1 To iCount) As String
sKey(iCount) = Mid$(sSection, iPos, (iNextPos - iPos))
iPos = iNextPos + 1
iNextPos = InStr(iPos, sSection, Chr$(0))
End If
Loop
End If
End Sub
Public Sub EnumerateAllSections(ByRef sSections() As String, ByRef iCount As
Long)
Dim sIniFile As String
Dim iPos As Long
Dim iNextPos As Long
Dim sCur As String
iCount = 0
Erase sSections
sIniFile = Sections
If (Len(sIniFile) > 0) Then
iPos = 1
iNextPos = InStr(iPos, sIniFile, Chr$(0))
Do While iNextPos <> 0
If (iNextPos <> iPos) Then
sCur = Mid$(sIniFile, iPos, (iNextPos - iPos))
iCount = iCount + 1
ReDim Preserve sSections(1 To iCount) As String
sSections(iCount) = sCur
End If
iPos = iNextPos + 1
iNextPos = InStr(iPos, sIniFile, Chr$(0))
Loop
End If
End Sub
Public Sub SaveFormPosition(ByRef frmThis As Object)
Dim sSaveKey As String
Dim sSaveDefault As String
On Error GoTo SaveError
sSaveKey = Key
If Not (frmThis.WindowState = vbMinimized) Then
Key = "Maximised"
Value = (frmThis.WindowState = vbMaximized) * -1
If (frmThis.WindowState <> vbMaximized) Then
Key = "Left"
Value = frmThis.Left
Key = "Top"
Value = frmThis.Top
Key = "Width"
Value = frmThis.Width
Key = "Height"
Value = frmThis.Height
End If
End If
Key = sSaveKey
Exit Sub
SaveError:
Key = sSaveKey
m_lLastReturnCode = 0
Exit Sub
End Sub
Public Sub LoadFormPosition(ByRef frmThis As Object, Optional ByRef lMinWidth =
3000, Optional ByRef lMinHeight = 3000)
Dim sSaveKey As String
Dim sSaveDefault As String
Dim lLeft As Long
Dim lTOp As Long
Dim lWidth As Long
Dim lHeight As Long
On Error GoTo LoadError
sSaveKey = Key
sSaveDefault = Default
Default = "FAIL"
Key = "Left"
lLeft = CLngDefault(Value, frmThis.Left)
Key = "Top"
lTOp = CLngDefault(Value, frmThis.Top)
Key = "Width"
lWidth = CLngDefault(Value, frmThis.Width)
If (lWidth < lMinWidth) Then lWidth = lMinWidth
Key = "Height"
lHeight = CLngDefault(Value, frmThis.Height)
If (lHeight < lMinHeight) Then lHeight = lMinHeight
If (lLeft < 4 * Screen.TwipsPerPixelX) Then lLeft = 4 *
Screen.TwipsPerPixelX
If (lTOp < 4 * Screen.TwipsPerPixelY) Then lTOp = 4 * Screen.TwipsPerPixelY
If (lLeft + lWidth > Screen.Width - 4 * Screen.TwipsPerPixelX) Then
lLeft = Screen.Width - 4 * Screen.TwipsPerPixelX - lWidth
If (lLeft < 4 * Screen.TwipsPerPixelX) Then lLeft = 4 *
Screen.TwipsPerPixelX
If (lLeft + lWidth > Screen.Width - 4 * Screen.TwipsPerPixelX) Then
lWidth = Screen.Width - lLeft - 4 * Screen.TwipsPerPixelX
End If
End If
If (lTOp + lHeight > Screen.Height - 4 * Screen.TwipsPerPixelY) Then
lTOp = Screen.Height - 4 * Screen.TwipsPerPixelY - lHeight
If (lTOp < 4 * Screen.TwipsPerPixelY) Then lTOp = 4 *
Screen.TwipsPerPixelY
If (lTOp + lHeight > Screen.Height - 4 * Screen.TwipsPerPixelY) Then
lHeight = Screen.Height - lTOp - 4 * Screen.TwipsPerPixelY
End If
End If
If (lWidth >= lMinWidth) And (lHeight >= lMinHeight) Then
frmThis.Move lLeft, lTOp, lWidth, lHeight
End If
Key = "Maximised"
If (CLngDefault(Value, 0) <> 0) Then
frmThis.WindowState = vbMaximized
End If
Key = sSaveKey
Default = sSaveDefault
Exit Sub
LoadError:
Key = sSaveKey
Default = sSaveDefault
m_lLastReturnCode = 0
Exit Sub
End Sub
Public Function CLngDefault(ByVal sString As String, Optional ByVal lDefault As
Long = 0) As Long
Dim lR As Long
On Error Resume Next
lR = CLng(sString)
If (Err.Number <> 0) Then
CLngDefault = lDefault
Else
CLngDefault = lR
End If
End Function
Would and XML file be acceptable:-
<config>
<someAppPart
AbsMaxVoltage="17.5"
AbsMinVoltage="5.5"
/>
<someOtherAppPart
ForegroundColor="Black"
BackgroundColor="White"
/>
</config>
Its very easy to consume in VB6, you don't need to worry about positioning etc.
The downside is its the user tweaking it can make it unparsable, however thats true if you write your own parser for a config file.
If we can assume your saved settings are simply a set of name/value pairs without a two-level hierarchy requirement (i.e. INI "Keys" within "Sections") you might just persist them as such:
AbsMaxVoltage=17.5
AbsMinVoltage=5.5
For writing the persistence format this is a case where you might consider the FSO, since the access volume is low anyway. The FSO can handle read/writing Unicode text files.
I think I'd do something like read lines and parse them using a Split() on "=" specifying just 2 parts (thus allowing "=" within values as well). For loading these I'd store them into a simple Class instance where the Class has two properties (Name and Value) and add each one to a Collection using Name as the Key. Make Value the default property if desired.
Maybe even implement some form of comment text line too using a generated sequence-numbered special Name value stored as say Name="%1" Value="comment text" with generated unique Names to avoid Collection Key collisions. Blank lines might be similarly preserved.
Then persisting as necessary means simply using a For Each on the Collection and using the FSO to write Name=Value out to disk.
To simulate a hierarchy you could simply use Names like:
%Comment: somAppPart settings
someAppPart.AbsMaxVoltage=17.5
someAppPart.AbsMinVoltage=5.5
%someOtherPart settings
someOtherAppPart.ForegroundColor=Black
someOtherAppPart.BackgroundColor=White
The parsing is cheap, so any probing of the Collection might be preceded by a full reparse (as the INI API calls do). Any changing of values in the program might do a full rewrite to disk (like the INI API calls do).
Some of this can be automated by just wrapping the Collection with some logic in another Class. The result could be syntax like:
Settings("someOtherAppPart", "ForegroundColor") = "Red"
aka
Settings.Value("someOtherAppPart", "ForegroundColor") = "Red"
This would reload the Collection, then probe the Collection for an Item keyed "someOtherAppPart.ForegroundColor" and create it or set its Value to "Red" and then flush the Collection to disk. Or you might eschew frequent rewriting and use distinct Load and Save methods.
Make it as simple or fancy as desired.
In any case, the result is a text file users can hack at with Notepad. The only reason for the FSO is to have an easy way of read/writing Unicode text. One could also screw around with Byte array I/O and explicit conversions (array to String) and line level parsing as required to avoid the FSO. If so just don't forget about the UTF-16LE BOM.

Resources