Quantcast
Channel: VBForums - CodeBank - Visual Basic 6 and earlier
Viewing all 1324 articles
Browse latest View live

Simple and fast, lightweight HashList-Class (no APIs)

$
0
0
Not much to add to the Threads Title...

the implementation of cHashList comes in only about 100 lines of code, and it can
be used as a VB.Collection-compatible replacement with much better performance.

The following Methods/Properties are supported:
- CompareMode (to switch between case-insensitive and case-sensitive compares, default-behaviour is like the VB.Collection)
- UniqueKeys (to allow multiple entries with the same Key, when switched from the True-defaultsetting to False)
- Count
- Add(Item As Variant, Optional Key As String)
- Exists(Key As String)
- IndexByKey(Key As String)
- KeyByIndex(ByVal IndexOneBased As Long)
- Item(KeyOrOneBasedIndex As Variant) ... (Get, Let and Set)
- ReInit(Optional ByVal ExpectedItemCount As Long = 5000)

Indexed access (for both, Keys and Items) is by orders of magnitude faster than the VB-Collection.
What's possible now as well (compared to the VB-Collection) is the ability to overwrite Item-Values
(at a given Index- or Key-position).

Note, that in the above List a Remove-Method is missing -
I've left this out for two reasons:
1) to demonstrate that a simplified HashList-Implementation doesn't necessarily need to be a linked List
2) because Remove is not used very often, when a Collection is used primarily as a fast "Key-Lookup-Container"
... (for Queue- or Stack-scenarios one can always use the normal VBA.Collection)

Performance is about 6 times as fast, when Key-Value-pairs are added -
and about twice as fast when Items are retrieved per Key-Lookup...

Here's a ScreenShot:


Here's the Class-Code of cHashList:
Code:

Option Explicit
 
Private Type DataTableEntry
  Key As String
  Value As Variant
End Type
Private Type HashTableEntry
  DataIndexes() As Long
End Type
 
Private DataTable() As DataTableEntry, HashTable() As HashTableEntry
Private mCount As Long, mDTUBound As Long, mHashTableSize As Long
 
Public CompareMode As VbCompareMethod, UniqueKeys As Boolean

Private Sub Class_Initialize()
  UniqueKeys = True
  CompareMode = vbTextCompare
  ReInit
End Sub

Public Sub ReInit(Optional ByVal ExpectedItemCount As Long = 5000)
  mHashTableSize = 8
  Do Until mHashTableSize * 5 > ExpectedItemCount: mHashTableSize = mHashTableSize * 2: Loop
  ReDim HashTable(0 To mHashTableSize - 1)
 
  Dim i As Long
  For i = 0 To UBound(HashTable): ReDim HashTable(i).DataIndexes(0 To 0): Next
  mDTUBound = 16: ReDim DataTable(0 To mDTUBound)
  mCount = 0
End Sub

Public Property Get Count() As Long
  Count = mCount
End Property

Public Function Exists(Key As String) As Boolean
  Exists = FindIndex(Key, CalcHash(Key)) > 0
End Function
Public Function IndexByKey(Key As String) As Long
  IndexByKey = FindIndex(Key, CalcHash(Key))
End Function

Public Sub Add(Item, Optional Key As String)
Dim HashValue As Long, UB As Long
  HashValue = CalcHash(Key)
  If UniqueKeys Then If FindIndex(Key, HashValue) Then Err.Raise 457
 
  'prolong and add to the new entries to the DataTable-Array
  mCount = mCount + 1
  If mDTUBound < mCount Then mDTUBound = mDTUBound * 1.5: ReDim Preserve DataTable(0 To mDTUBound)
  DataTable(mCount).Key = Key
  DataTable(mCount).Value = Item
 
  'add the new DataIndex to the proper Hash-Buckets .DataIndexes-Array
  With HashTable(HashValue)
    UB = UBound(.DataIndexes): UB = UB + 1
    ReDim Preserve .DataIndexes(0 To UB)
    .DataIndexes(UB) = mCount
  End With
End Sub

Public Property Get KeyByIndex(ByVal IndexOneBased As Long)
  If IndexOneBased < 1 Or IndexOneBased > mCount Then Err.Raise 9
  KeyByIndex = DataTable(IndexOneBased).Key
End Property

Public Property Get Item(KeyOrOneBasedIndex)
Dim Index As Long
  If VarType(KeyOrOneBasedIndex) = vbString Then
    Index = FindIndex(KeyOrOneBasedIndex, CalcHash(KeyOrOneBasedIndex))
    If Index = 0 Then Err.Raise 457
  Else
    Index = KeyOrOneBasedIndex
    If Index < 1 Or Index > mCount Then Err.Raise 9
  End If
  If IsObject(DataTable(Index).Value) Then
    Set Item = DataTable(Index).Value
  Else
    Item = DataTable(Index).Value
  End If
End Property

Public Property Let Item(KeyOrOneBasedIndex, NewItem)
Dim Index As Long
  If VarType(KeyOrOneBasedIndex) = vbString Then
    Index = FindIndex(KeyOrOneBasedIndex, CalcHash(KeyOrOneBasedIndex))
    If Index = 0 Then Err.Raise 457
  Else
    Index = KeyOrOneBasedIndex
    If Index < 1 Or Index > mCount Then Err.Raise 9
  End If
  If IsObject(NewItem) Then
    Set DataTable(Index).Value = NewItem
  Else
    DataTable(Index).Value = NewItem
  End If
End Property
Public Property Set Item(KeyOrOneBasedIndex, NewItem)
  Item(KeyOrOneBasedIndex) = NewItem
End Property

Private Function FindIndex(Key, ByVal HashValue As Long) As Long
Dim i As Long, CM As VbCompareMethod
  With HashTable(HashValue)
    CM = CompareMode
    For i = 1 To UBound(.DataIndexes)
      If StrComp(Key, DataTable(.DataIndexes(i)).Key, CM) = 0 Then
        FindIndex = .DataIndexes(i): Exit Function
      End If
    Next
  End With 'returns Zero, when no Key can be found
End Function

Private Function CalcHash(Key) As Long
Dim i As Long, L As Long, B() As Byte
  If CompareMode Then B = LCase$(Key) Else B = Key
  L = 7919
    For i = UBound(B) To 0 Step -1: L = (i + B(i) + L) * 37 And &H7FFFFF: Next
  CalcHash = L * B(0) Mod mHashTableSize
End Function

Friend Sub CheckHashDistribution()
Dim i As Long, UB As Long, cc As Long, Min As Long, Max As Long
  Min = &H7FFFFFFF
  For i = 0 To UBound(HashTable)
    UB = UBound(HashTable(i).DataIndexes)
    If UB Then
      If Min > UB Then Min = UB
      If Max < UB Then Max = UB
      cc = cc + 1
    End If
  Next
  Debug.Print "Distribution over a HashTable with"; UBound(HashTable) + 1; "slots:"
  Debug.Print "Used-HashSlots:"; cc
  Debug.Print "Min-Entries:"; Min
  Debug.Print "Max-Entries:"; Max
End Sub

And here the Code of the Test-Form:
Code:

Option Explicit

Private Const TestEntryCount As Long = 100000

Private C As Collection, H As cHashList

Private Sub Form_Click()
Dim i As Long, T!, Item
  AutoRedraw = True
  Cls
  Print "Count of Test-Entries:"; TestEntryCount; vbLf
 
  Set C = New Collection
  Set H = New cHashList
      H.ReInit TestEntryCount
     
  T = Timer
    For i = 1 To TestEntryCount
      C.Add i, "K" & i
    Next
  Print "Collection-Add:", Timer - T & "sec"
 
  T = Timer
    For i = 1 To TestEntryCount
      H.Add i, "K" & i
    Next
  Print "cHashList-Add:", Timer - T & "sec"; vbLf
 
  T = Timer
    For i = 1 To TestEntryCount
      Item = C.Item("K" & i)
    Next
  Print "Collection-ItemByKey:", Timer - T & "sec"
 
  T = Timer
    For i = 1 To TestEntryCount
      Item = H.Item("K" & i)
    Next
  Print "cHashList-ItemByKey:", Timer - T & "sec"
 
  Print vbLf; "Indexed access is not compared (it would be much faster per HashList)"
  H.CheckHashDistribution
End Sub

Have fun with it...

Olaf

VB6 Browse folder replacement dialog

Wrapper for VB6 Collections

$
0
0
There have been a couple of roll-your-own collection classes posted in these forums, and I'm not knocking any of these. Some of them claim to be faster than the core VB6 Collection object, and they may be. That's not what this post is about.

This post is, to some degree, in response to a recent lively thread. However, in that thread, there was much discussion about the internal workings of VB6 Collections. Some was sorted, and some wasn't. What wasn't sorted was how VB6 Collections are as fast as they are. And that's also not the point of this post. We know they are relatively fast, and the code in this post continues to take advantage of that.

Some things that were sorted is how to get the keys back from a collection.

Also, it was rather thoroughly illustrated that VB6 Collection keys follow some strange rules. For instance, they compare strings (determining existence and duplication) similar to StrComp() using the vbTextCompare constant. This has all kinds of strange consequences. For one, it can be locale specific. Secondly, it's case insensitive, which can cause very strange problems. Thirdly, strange problems can arise when a character in the key string is outside of the valid &h0020 to &hD7FF USC-2 Unicode range.

Therefore, this is a wrapper that allows for strings that are case sensitive. In fact, they can have anything at all in them, and it won't matter. Basically, a HEX version of the string is what's actually placed in the collection, although this wrapper hides that from you.

This wrapper has the four members (Add, Item, Count, Remove) of a typical VB6 Collection. It also has an added set of "helper" members:

  • KeyExists - Just a boolean check if a key exists in the collection.
  • Keys() - Returns a string array with all the Collection's keys.
  • ChangeKey - Change old key to new key.
  • ChangeIndex - Change old index to new index.
  • ItemKey - This is a read/write String property. Upon supplying the Index, you can retrieve an item's Key, or you can change it.
  • ItemIndex - This is a read/write Long property. Upon supplying the Key, you can retrieve an item's Index, or you can change it.


In the spirit of my better angels, I'll give a shout out to DEXWERX and to dilettante for their assistance in fleshing out these ideas. Also, they make use of some known information about a header structure as well as an item structure of VB6 Collections. The precise origin of the teasing out of these structures is unknown, but possibly attributable to LaVolpe.

And now for the wrapper. Just place this in a Class named to your choosing (for my use, I've named it CollectionEx), and use it as a wrapper to the internal VB6 Collection object. Again, just to enumerate the advantages:

  • Keys are completely case sensitive. Basically, they're compared on a binary level rather than in a vbTextCompare way.
  • The "For Each" syntax is enabled.
  • An extra set of "helper" members is included (see list above).


Code:

'
' A class wrapper for the VB6 Collection.
' It has the advantages of still using string keys, but they're handled in a completely binary fashion.
' In other words, they're case sensitive, and not restricted to the valid VB6 Unicode range of characters.
'
' There are also a few extra methods and properties:
'
'      KeyExists          Just a boolean check if a key exists in the collection.
'      Keys()              Returns a string array with all the Collection's keys.
'      ItemKey            Based on an Index value, returns or sets the Key of an existing item.
'      ItemIndex          Based on a Key value, returns or sets the Index of an existing item.
'      ChangeKey      Change old key to new key.
'      ChangeIndex    Change old index to new index.
'
Option Explicit
'
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (pDest As Any, pSource As Any, ByVal ByteLen As Long)
'
Dim c As Collection
'

'
'*****
' The four methods and property of a typical VB6 Collection.
'*****
'
Friend Sub Add(vData As Variant, Optional Key As String, Optional Before As Variant, Optional After As Variant)
    ' It still raises an error if both are specified, and that behavior is not changed.
    ' Also errors when trying to add duplicate keys, as expected.
    If Len(Key) Then
        c.Add vData, Base16Encode(Key), Before, After
    Else
        c.Add vData, , Before, After
    End If
End Sub

Friend Function Item(ByVal IndexOrKey As Variant) As Variant
    Select Case VarType(IndexOrKey)
    Case 2 To 7 ' Integer, Long, Single, Double, Currency, & Date.  (A bit weird to pass in a date, but who knows.)
        Item = c.Item(CLng(IndexOrKey))
    Case 8
        Item = c.Item(Base16Encode(CStr(IndexOrKey)))
    End Select
    ' Returns Item = Empty if not set, because of bad IndexOrKey type (such as Null, an object or other nonsense).
End Function

Friend Function Count()
    Count = c.Count
End Function

Friend Sub Remove(ByVal IndexOrKey As Variant)
    Select Case VarType(IndexOrKey)
    Case 2 To 7 ' Integer, Long, Single, Double, Currency, & Date.  (A bit weird to pass in a date, but who knows.)
        c.Remove CLng(IndexOrKey)
    Case 8
        c.Remove Base16Encode(CStr(IndexOrKey))
    End Select
    ' It does nothing if bad IndexOrKey type (such as Null, an object or other nonsense).
End Sub

'
'*****
' Some extra handy methods.
'*****
'
Public Function NewEnum() As IUnknown
    'Attribute NewEnum.VB_UserMemId = -4
    'Attribute NewEnum.VB_MemberFlags = "40"
    '
    ' This allows use of the "For Each" syntax.
    ' Just enumerate with a variant for the item, using this class as the series.
    '
  Set NewEnum = c.[_NewEnum]
End Function

Friend Function KeyExists(Key As String) As Boolean
    Dim v As Variant
    On Error GoTo DoesntExist
    v = c.Item(Base16Encode(Key))
    KeyExists = True
DoesntExist:
End Function

Friend Function Keys() As String()
    ' Returns a string array of all the keys.
    Dim sKeys() As String
    Dim j As Long
    Dim iHold As Long
    Dim ptr As Long
    Dim sKeyTemp  As String
    '
    If c.Count = 0 Then Exit Function
    '
    ReDim sKeys(1 To c.Count)
    j = 1
    CopyMemory ptr, ByVal ObjPtr(c) + &H18, 4
    GoSub MoveKeyToArray
    For j = 2 To c.Count
        CopyMemory ptr, ByVal ptr + &H18, 4
        GoSub MoveKeyToArray
    Next j
    Keys = sKeys
    Exit Function
    '
MoveKeyToArray: ' j and ptr must be set to call this.
    iHold = StrPtr(sKeyTemp)
    CopyMemory ByVal VarPtr(sKeyTemp), ByVal ptr + &H10, 4
    sKeys(j) = Base16Decode(sKeyTemp)
    CopyMemory ByVal VarPtr(sKeyTemp), iHold, 4
    Return
End Function

Friend Property Let ChangeKey(ByVal OldKey As String, ByVal NewKey As String)
    ' OldKey must exist or error.
    Dim ptr  As Long
    Dim tKey  As String
    Dim iHold  As Long
    Dim Index As Long
    Dim vData As Variant
    '
    If c.Count Then
        OldKey = Base16Encode(OldKey)
        iHold = StrPtr(tKey)
        CopyMemory ptr, ByVal ObjPtr(c) + &H18, 4
        Index = 1
        Do
            CopyMemory ByVal VarPtr(tKey), ByVal ptr + &H10, 4
            If OldKey = tKey Then Exit Do
            Index = Index + 1
            CopyMemory ptr, ByVal ptr + &H18, 4
        Loop Until ptr = 0
        CopyMemory ByVal VarPtr(tKey), iHold, 4
    End If
    If ptr = 0 Then
        Err.Raise 5
        Exit Property
    End If
    '
    ' We've found the old key if we got to here.
    vData = c.Item(Index)
    c.Remove Index
    '
    Select Case True
    Case c.Count = 0: c.Add vData, Base16Encode(NewKey)
    Case Index > c.Count: c.Add vData, Base16Encode(NewKey), , c.Count
    Case Else: c.Add vData, Base16Encode(NewKey), Index
    End Select
End Property

Friend Property Let ChangeIndex(OldIndex As Long, NewIndex As Long)
    ' Item with key must exist or error.
    ' Indexes must be in current range of the Collection, or error.
    '
    Dim vData As Variant
    Dim sKey As String
    Dim tKey As String
    Dim ptr As Long
    Dim i As Long
    '
    If OldIndex < 1 Or OldIndex > c.Count Or NewIndex < 1 Or NewIndex > c.Count Then
        Err.Raise 9
        Exit Property
    End If
    '
    vData = c.Item(OldIndex)
    If c.Count = 1 Then Exit Property
    '
    If OldIndex <= c.Count / 2 Then
        CopyMemory ptr, ByVal ObjPtr(c) + &H18, 4
        For i = 2 To OldIndex
            CopyMemory ptr, ByVal ptr + &H18, 4
        Next i
    Else
        CopyMemory ptr, ByVal ObjPtr(c) + &H1C, 4
        For i = c.Count - 1 To OldIndex Step -1
            CopyMemory ptr, ByVal ptr + &H14, 4
        Next i
    End If
    '
    i = StrPtr(tKey)
    CopyMemory ByVal VarPtr(tKey), ByVal ptr + &H10, 4
    sKey = tKey
    CopyMemory ByVal VarPtr(tKey), i, 4
    '
    ' Now that we've got the data and key, we can delete and re-add.
    ' Key is still encoded.  No need to decode.
    c.Remove OldIndex
    '
    Select Case True
    Case c.Count = 0: c.Add vData, sKey
    Case NewIndex > c.Count: c.Add vData, sKey, , c.Count
    Case Else: c.Add vData, sKey, NewIndex
    End Select
End Property

Friend Property Let ItemKey(Index As Long, ByVal Key As String)
    ' Change an item key based on its index value.
    ' Index must be in current range of the Collection, or error.
    '
    Dim vData As Variant
    '
    If Index < 1 Or Index > c.Count Then
        Err.Raise 9
        Exit Property
    End If
    '
    vData = c.Item(Index)
    c.Remove Index
    '
    Select Case True
    Case c.Count = 0: c.Add vData, Base16Encode(Key)
    Case Index > c.Count: c.Add vData, Base16Encode(Key), , c.Count
    Case Else: c.Add vData, Base16Encode(Key), Index
    End Select
End Property

Friend Property Get ItemKey(Index As Long) As String
    ' Get a key based on its index value.  Must be in range, or error.
    Dim i    As Long
    Dim ptr  As Long
    Dim sKey  As String
    '
    If Index < 1 Or Index > c.Count Then
        Err.Raise 9
        Exit Property
    End If
    '
    If Index <= c.Count / 2 Then
        CopyMemory ptr, ByVal ObjPtr(c) + &H18, 4
        For i = 2 To Index
            CopyMemory ptr, ByVal ptr + &H18, 4
        Next i
    Else
        CopyMemory ptr, ByVal ObjPtr(c) + &H1C, 4
        For i = c.Count - 1 To Index Step -1
            CopyMemory ptr, ByVal ptr + &H14, 4
        Next i
    End If
    '
    i = StrPtr(sKey)
    CopyMemory ByVal VarPtr(sKey), ByVal ptr + &H10, 4
    ItemKey = Base16Decode(sKey)
    CopyMemory ByVal VarPtr(sKey), i, 4
End Property

Friend Property Let ItemIndex(ByVal Key As String, Index As Long)
    ' Change an item index based on its key value.
    ' Item with key must exist or error.
    ' Index must be in current range of the Collection, or error.
    '
    Dim vData As Variant
    '
    If Index < 1 Or Index > c.Count Then
        Err.Raise 9
        Exit Property
    End If
    '
    Key = Base16Encode(Key)
    vData = c.Item(Key)
    If c.Count = 1 Then Exit Property
    c.Remove Key
    '
    Select Case True
    Case c.Count = 0: c.Add vData, Key
    Case Index > c.Count:  c.Add vData, Key, , c.Count
    Case Else: c.Add vData, Key, Index
    End Select
End Property

Friend Property Get ItemIndex(ByVal Key As String) As Long
    ' Get an index based on its key value.
    Dim ptr  As Long
    Dim tKey  As String
    Dim iHold  As Long
    '
    If c.Count Then
        Key = Base16Encode(Key)
        iHold = StrPtr(tKey)
        CopyMemory ptr, ByVal ObjPtr(c) + &H18, 4
        ItemIndex = 1
        Do
            CopyMemory ByVal VarPtr(tKey), ByVal ptr + &H10, 4
            If Key = tKey Then Exit Do
            ItemIndex = ItemIndex + 1
            CopyMemory ptr, ByVal ptr + &H18, 4
        Loop Until ptr = 0
        CopyMemory ByVal VarPtr(tKey), iHold, 4
    End If
    If ptr = 0 Then ItemIndex = 0
End Property

'
'*****
' Private procedures used above.
'*****
'
Private Sub Class_Initialize()
    Set c = New Collection
End Sub

Private Function Base16Encode(s As String) As String
    Dim i As Long
    Base16Encode = Space$(Len(s) * 4)
    For i = 0 To Len(s) - 1
        Mid$(Base16Encode, i * 4 + 1, 4) = Right$("0000" & Hex$(AscW(Mid$(s, i + 1, 1))), 4)
    Next i
End Function

Private Function Base16Decode(s As String) As String
    Dim i As Long
    Base16Decode = Space$(Len(s) \ 4)
    For i = 0 To Len(s) - 1 Step 4
        Mid$(Base16Decode, i \ 4 + 1, 1) = ChrW$(val("&h" & Mid$(s, i + 1, 4)))
    Next i
End Function

I was also at the cusp of my 15000 character limit, and had to delete code comments. I've also attached it with more comments in the class module.

Enjoy,
Elroy
Attached Files

The Decimal Data Type

$
0
0
There are a few cases where API calls return 8 byte integers. FileSize and FileTime are a couple of them. These are never easily handled with VB6. Essentially, there are a couple of ways it's typically done: 1) Create a UDT with two Longs, 2) Use Currency.

Both of these have the downside of not being able to treat the results as a true 8 byte integer for arithmetic or just printing/viewing.

Also, there may be certain times when a Long integer just doesn't have the range to do something you'd like to do, and Currency is always a pain because you have to deal with the implicit-four-decimal-places.

VB6 actually has a "type" on which you can do 8 byte integer math. In fact, you can do 12 byte integer math with it. It's the Decimal data type.

Sadly, Decimal is not one of the native types. However, it is one of the data types that a Variant will handle. And VB6 will do arithmetic with it when it's in a Variant.

In fact, it's the only data type I'm aware of that uses all 16 bytes of a Variant (with a Variant always having 16 bytes). The Decimal type uses 14 bytes, and 2 bytes are reserved by the Variant to denote the data type within the variant. When a Decimal is contained with a Variant, it has the following structure:

Code:


Private Type DecimalStructure ' (when sitting in a Variant)
    VariantType As Integer  ' Reserved, to act as the Variant Type when sitting in a 16-Byte-Variant.  Equals vbDecimal(14) when it's a Decimal type.
    Base10NegExp As Byte    ' Base 10 exponent (0 to 28), moving decimal to right (smaller numbers) as this value goes higher.  Top three bits are never used.
    Sign As Byte            ' Sign bit only.  Other bits aren't used.
    Hi32 As Long            ' Mantissa.
    Lo32 As Long            ' Mantissa.
    Mid32 As Long          ' Mantissa.
End Type

The following is also in the comments of the code, but let me spell them out here as well.

  • Decimal is at the top of the math-implicit-conversion hierarchy. Therefore, in most cases, you don't have to worry about the results being rounded when you do Decimal + Integer, or Decimal * Long, etc. In other words, with the +, -, *, and / operators, if a Decimal is on either side, a Decimal will be the result.
  • The ^ operator will result in a Double (not a Decimal).
  • The MOD and \ operators will result in a Long (not a Decimal).
  • Int(), Fix(), and Abs() work just fine on Decimals, returning a Decimal type.
  • Sgn() works on Decimals, although return is an Integer but this shouldn't matter.
  • Sqr() and the trig functions will work, although they will return Double (not Decimal).


Let's examine the above Decimal structure a bit. It's a bit strange that the bytes of mantissa have a mixed-endianness about them. But we really don't need to worry about that. VB6 deals with all of that.

To some degree, we can think of these Decimals as an unsigned 12 byte integer. As such, they range from 0 to 79228162514264337593543950335, where that big number is just 2^96-1. The 96 is the 96 bits of 12 bytes (12*8).

In these Decimals, the sign bit is kept completely separate. It's in that Sign byte of the UDT structure. Only the high bit (&h80) of this byte is used. When it's on, the Decimal is interpreted as negative.

Now, the Base10NegExp is also somewhat strange. This is not a base 2 exponent like what is used in Single and Double. This is a base 10 exponent that moves the decimal the way we were taught in 5th grade. However, it's also a negative exponent, moving the decimal point to the left as the Base10NegExp byte gets larger. In other words, in 5th grade scientific notation, it's Mantissa * 10^-Base10NegExp.

Furthermore, this Base10NegExp is limited to the range of 0 to 28. Why limited and why to this range? If we count the digits in 79228162514264337593543950335, we find that there are 28. Therefore, using this Base10NegExp, we can change this number from 79228162514264337593543950335.0000 (where Base10NegExp=0) to 00007.9228162514264337593543950335 (where Base10NegExp=28). In other words, we can place the decimal point anywhere within the number (including at the end, as an integer), but we can't force zeroes on either end. Another way to say this is that "perfect precision, with no rounding, is preserved at all times with Decimals". This is particularly true with the +, -, and * operators. However, it will be forced to round to those 28 significant digits with division.

Now, here's the code related to the Decimal type that I keep in my library:

Code:


Option Explicit
'
' +, -, *, /    Decimal is at the top of the hierarchy.
'              The hierarchy is: Byte, Integer, Long, Single, Double, Currency, Decimal.
' \, Mod        Results is Long, not Decimal.
' ^            Results is Double, not Decimal.
' Int()        Works on Decimals.
' Fix()        Works on Decimals.
' Abs()        Works on Decimals.
' Sgn()        Works on Decimals, although result is an Integer.
' Sqr(), etc.  Most of these functions (including trig) return Double, not Decimal.
'
' Largest Decimal:      +/- 79228162514264337593543950335.  2^96-1 (sign bit handled separately)
' Smallest Decimal:    +/- 0.0000000000000000000000000001  Notice that both largest and smallest are same width.

'
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Dst As Any, Src As Any, ByVal numbytes As Long)
Private Declare Function VariantChangeTypeEx Lib "oleaut32" (ByRef pvargDest As Variant, ByRef pvarSrc As Variant, ByVal lcid As Long, ByVal wFlags As Integer, ByVal vt As Integer) As Long
'
Private Type DecimalStructure ' (when sitting in a Variant)
    VariantType As Integer  ' Reserved, to act as the Variant Type when sitting in a 16-Byte-Variant.  Equals vbDecimal(14) when it's a Decimal type.
    Base10NegExp As Byte    ' Base 10 exponent (0 to 28), moving decimal to right (smaller numbers) as this value goes higher.  Top three bits are never used.
    Sign As Byte            ' Sign bit only.  Other bits aren't used.
    Hi32 As Long            ' Mantissa.
    Lo32 As Long            ' Mantissa.
    Mid32 As Long          ' Mantissa.
End Type
'

Public Function DecFromCurrAsInt(ByVal c As Currency, Optional Signed As Boolean = False) As Variant
    ' Moves Currency type used as an 8 byte integer into a Decimal.
    ' Assumes 4 decimal points of Currency are undesired (and ignored).
    ' Useful for API calls returning 8 byte integer, provides a way to report the actual integer.

    '
    Const DECIMAL_NEG As Byte = 128 ' The high-bit of a byte.  Sign bit for Decimal.
    Dim u As DecimalStructure
    '
    If Signed Then
        DecFromCurrAsInt = CDec(c) * 10000  ' Note that signed Currency can be moved to Decimal relatively easily.
    Else
        CopyMemory ByVal VarPtr(u) + 8, c, 8
        u.VariantType = vbDecimal          ' Set up for moving to Variant type variable.
        CopyMemory DecFromCurrAsInt, u, 16  ' Copy the 16 Bytes from u over into the Variant V.
    End If
End Function

Public Function CurrAsIntFromDec(ByVal v As Variant, Optional Signed As Boolean = False) As Currency
    ' This moves a Decimal into a Currency, assuming that the Currency 4 decimal points are being ignored.]
    ' It's the reverse of DecFromCurrAsInt.
    ' Note that any precision seen in the Decimal beyond an 8 byte integer will throw an overflow error.

    '
    Const DECIMAL_NEG As Byte = 128 ' The high-bit of a byte.  Sign bit for Decimal.
    Dim u As DecimalStructure
    '
    ' Let's make sure our decimal is an integer.
    If Sgn(v) = -1 Then v = Fix(v - 0.5) Else v = Int(v + 0.5)
    '
    CopyMemory u, v, 16
    If u.Hi32 <> 0 Then Err.Raise 6: Exit Function                                  ' Overflow.
    '
    If Signed Then
        If (u.Mid32 And &H80000000) = &H80000000 Then Err.Raise 6: Exit Function    ' Overflow.
        u.Base10NegExp = 4                ' Safest way to prevent any division rounding.
        CopyMemory v, u, 16
        CurrAsIntFromDec = CCur(v)
    Else
        CopyMemory CurrAsIntFromDec, ByVal VarPtr(u) + 8, 8
    End If
End Function

Public Function CDecEx(StringLiteral As String) As Variant
    ' The downside of CDec() is that it's locale-aware with strings.
    ' So to do this correctly, we must use a call to VariantChangeTypeEx passing LOCALE_INVARIANT to it.

    Dim ret As Long
    Const LOCALE_INVARIANT As Long = &H7F&
    ' This will auto-typecast StringLiteral to a variant before making the call.
    ret = VariantChangeTypeEx(CDecEx, StringLiteral, LOCALE_INVARIANT, 0, vbDecimal)
    If ret Then Err.Raise ret, "CDecEx()"
End Function

Public Function DecRoundToInt(vDec As Variant) As Variant
    ' Round a Decimal to its integer, keeping Decimal type.
    ' Only really needed for division in the case where we want to force integers.

    If Sgn(vDec) = -1 Then
        DecRoundToInt = Fix(vDec - 0.5) ' This is the only way to insure that ???.5 gets rounded higher (in absolute value terms).
    Else
        DecRoundToInt = Int(vDec + 0.5)
    End If
End Function

Let me go through each procedure.

  • DecFromCurrAsInt: This moves a Currency type into a Decimal (Variant) type, and treats the Currency as an 8 byte integer. In other words, it ignores the implicit four-decimal-points. This is particularly useful for "seeing" and possibly manipulating these 8 byte integers, and actually treating them as integers.
  • CurrAsIntFromDec: This is the inverse of DecFromCurrAsInt. It puts a Decimal type (or, the integer portion) back into a Currency type. However, in this case, the Decimal carries 96 bits of mantissa precision, rather than only 64 (or 63 if the Currency sign bit is important). Therefore, there may be overflow conditions. This function is useful for possibly getting an 8 byte integer from an API, manipulating it, and then passing it back in with another API call.
  • CDecEx: It's not trivially easy to get a large integer value into a Decimal type (with full precision) within VB6 code. About the only way to do it is to type out the number as a String. Possibly something like, "12341232342345.54235423". And then, we could possibly put this into a Decimal (Variant) using the CDec("12341232342345.54235423") function. However, anytime we have a String, there is the potential for Locale issues. Therefore, CDecEx puts a String into a Decimal (Variant) while circumventing the Locale issues.
  • DecRoundToInt: This function is designed to expect a Decimal and it returns a Decimal. It's only applicable with division, as division is about the only way to start with integers and wind up with non-integers. We may desire to use strictly integers, with appropriate rounding. This DecRoundToInt provides this functionality.


IDK, maybe this all isn't the standard fare for the CodeBank. But I had it all put together for my own edification, and I thought I'd share.

Enjoy,
Elroy

p.s. Any and all comments are more than welcome.

Metafile Graphics for DPI Scaling

$
0
0
One of the headaches in addressing High DPI issues is the scaling of images within our user interfaces. These are typically small "icon" type images we display in various controls. In many cases we can head the problem off by simply dropping our use of iconic graphics, but in others we want icons for a number of reasons. ListView, TreeView, and similar controls just aren't the same when we limit ourselves to textual captions with no iconic images.

I'm not addressing Shell icons (file icons) here because that's a separate topic and one that metafile graphics can't help. There are other ways to manage scaling for those. I'm also not addressing .ICO format custom icons, which can be managed much like Shell icons and also can't be helped using metafiles.

So instead these are completely custom small images used "iconically" in your programs. Images you might have created a BMP, GIF, or ICO for back in the 96 DPI world.


Vector Graphics

Some people will claim that WMF and EMF (and EMF+) metafiles are "vector graphics" formats. I'm not sure that is accurate, but they are probably the closest thing we have natively in Windows

I see metafiles as really a kind of "GDI macro" that can be "played back" into bitmaps. By nature they have a transparent background unless you use some sort of fill operation to color it in.

Things like the hatch and pattern fills that do not scale at all (and thus are of limited real value) tend to disprove the "vector graphics" assertion for me.


Drawing Metafiles: MakeEMF

I'll focus on Enhanced Metafile (EMF) format here. Windows Metafile (WMF) can still be useful. You can do much the same thing for WMFs with only a few changes here and there to MakeEMF.

Even though EMF format images can have OpenGL and GDI+ drawing within them I am sticking with GDI for simplicity and have not investigated those options much. I'm not sure those work in a StdPicture or with VB/ActiveX controls anyway. I haven't looked into EMF+ format metafiles at all and they might even be more problematic for VB anyway.


Project1

Project1 makes use of MakeEMF.cls, a simple class for creating, drawing, and saving an EMF image to disk or retrieving it as a StdPicture object for direct use in VB.

MakeEMF only implements Ellipse and Line drawing. You could also add Arc, Curve, Rectangle, and more drawing operations, as well as fills and even more.


Name:  sshot1.png
Views: 47
Size:  4.3 KB

Project1


Project1 itself is a little silly, but just a simple demo. However it does create a simple "saved.emf" that is used in the other Projects.

I haven't seen many meaningful examples of creating and drawing metafile graphics in VB6. If you really want to do this you can probably adapt code from other GDI drawing examples to flesh out MakeEMF's repertoire with additional drawing methods.


MakeEMF: Impractical?

I suspect that it isn't worth the trouble of doing something like this in many programs. This probably explains why so little sample code seems to be floating around. I suppose you could use this as a starting point to make your own metafile graphics drawing utility.

My main direction here is using metafile graphics to address DPI scaling. Most people will probably use an existing 3rd party utility for that.

I tend to fall back on Old Reliable: Windows Draw 6 (originally by Micrografx, who were bought out and then bought out at least a second time). I doubt you can buy a copy anymore, but I have it working in Windows Vista and Windows 10 with only a slight loss in functionality.

I got used to Window Draw long ago, originally getting the Windows 3.1 version bundled with a trackball I bought in the early 1990s.

Since I'm no graphics artist my "tool bag" is pretty limited these days, mainly: IrfanView, MS Paint, IcoFX, and Windows Draw 6. These fit like old shoes aside from MS Paint which got re-boned in Windows 7 resulting in a chaotic and often inscrutable user interface.
Attached Images
 
Attached Files

RTF Label & RTF Button

$
0
0
The attached project contains an RTF Label and an RTF Button control.

I use these all the time, but I haven't messed with the code in either of them in years.

There are quite possibly better options around, but someone asked for these so I posted them.

One nice feature is that you edit the captions directly over the Label or Button (rather than in the properties window).

Also, once one of these controls is on your form, right-click and then "Edit" it to change the caption. A mini-word-processor will pop up.

Enjoy,
Elroy

p.s. A reference to the Rich Text Box control should be made before you throw these into your project.
Attached Files

Vb6 - simple tcp connect

$
0
0
During the process of trying to figure out NAT Traversal (how to get around the problem of NAT blocking external connections), I had to simplify the process of TCP/IP connections. NewSocket was just too complex to start experimenting with it.

PrjTest3.vbp is a very simple example of connecting to a listening socket, and may help some users to understand how the Socket API (ws2_32.dll) functions in Vista or better operating systems. It does not contain a lot of error handling, it does not work with UDP, it does not work with IPv6, and it does not receive messages.

In the Form_Load event, the Winsock service is started (WSAStartup), and the destination IP Address & Port are defined. In cmdConnect_Click, an IPv4 TCP socket is created using a Socket call (aliased API_Socket). Then the local Socket Address structure (sa_local) is populated. This is where Version 2 of the Socket API differs substantially from Version 1. When using GetAddrInfo, binding to a particular socket is not required. We simply use address 0.0.0.0 and port 0, and GetAddrInfo will consult the local DNS to get the Server information and bind to the socket using the appropriate local interface and the first available local port. Because we are using an IP address instead of a domain name, that trip to the local DNS is not necessary. There will only be one address in the linked list, and we copy that information to the Hints structure. From there, we copy the socket portion to sa_dest. Now we have all the information necessary to send a Connection Request (SYN) to the destination. The destination should respond with a SYN-ACK, and the local socket should send an ACK (this is all handled by the API). Once connected, we send a simple text message.

That is about as far as we can go without implementing a callback procedure to intercept messages from the operating system. NewSocket uses Emiliano Scavuzzo's subclassing technique, which does not cause the IDE to crash and is able to differentiate the individual system messages from each socket. To put a socket into the listening mode is similar, but a little more complex. We have to create a socket, bind it to a user defined listening port, and put the socket into the listening mode with API_Listen. When a ConnectionRequest is received from the other end, the socket is closed, the connection is accepted on a different socket, and the socket once again is placed in the listening mode. This allows the server to accept multiple connections on the same port number. There is one caveat here though. Servers normally use blocking calls (each connection is on a separate thread) to handle large numbers of connections. However, we are using non-blocking calls, and the error WSAEWOULDBLOCK is not uncommon and should be ignored.

J.A. Coutts
Attached Files

[VB6] - Trick Advanced Tools.

$
0
0
Hello everyone!
I present to you a small project - Add-in that allows to some extent alleviate debugging some programs as well expand compilation possibilities. All the source codes are in the attachment.
This Add-in has the following features:
  1. Fixes the bug with Not Not Array statement that causes error "Expression too complex" if you'll work with float numbers;
  2. Allows to utilize the automatic conditional constants depending on run mode (IDE/EXE) look like in C++ (NDEBUG);
  3. Allows to disable integer overflow checking in IDE;
  4. Allows to disable floating point result checking in IDE;
  5. Allows to disable array bounds checking in IDE;
  6. Provides the compilation/linking events (both in IDE and EXE), i.e. you can run the commands before/after this events. By using this events you can do many useful things (encryption, replace OBJ files, static linking, etc.)


How does it work?



For fixing Not Not bug and disabling checking it uses the module of replacing of the opcodes handlers (P_Code) to ours. Firstly it finds the table of the opcodes by the signature in the ENGINE section of VBA6.dll module. There are two opcodes types - single-byte and double-bytes. Teh single-byte opcodes is less that 0xFB. It uses the length dissasembler by Ms-Rem that i ported to VB6. Besides it finds the subroutine that redirectes performing to the next opcode as well the subroutine that handles the errors. Since now it is very easy to get an access violation error i kept some checking. For example, access to uninitialized array causes the memory violation error - it handles that error. Because of there isn't an official documentation about VB6 opcodes (i've not found it) i did all the investigations, therefore some opcodes can raise error. In this case you can write them - i'll add handlers.
For others features it uses splicing of the following functions:
  1. TipCompileProject;
  2. TipCompileProjectFull;
  3. TipMakeExe2;
  4. TipFinishExe2.
TipSetConstantValues/TipGetConstantValues functions are used in order to set/get the conditional compilation arguments. The events is just calling of ShellExecuteEx function. There are events before/after project compilation (IDE/EXE) and linking. This project was weakly testing therefore it can contain bugs.
Regading,
Кривоус Анатолий (The trick)
Attached Files

[VB6] Using IAutoComplete / IAutoComplete2 including autocomplete with custom lists

$
0
0
IAutoComplete / IAutoComplete2 / IEnumString

SHAutocomplete has many well known limitations, the biggest being if you want to supply your own list to use with it. I was very impressed with Krool's work on this interface, and not wanting to include a whole other TLB set out to do it with oleexp.

Turns out it's far easier to work with using oleexp; the only major limitation being how to go about handling multiple autocompletes with different custom lists. UPDATE: Previously this class couldn't support multiple custom lists for different controls because the v-table swapping method was only passing IEnumString, rather than a full cEnumString class. If it were possible to get the full class, one might expect to be able to just change it to As cEnumString - but that didn't work. However changing it to a Long to get the pointer itself actually produced a pointer to the full instance of the class, and voilà, the undocumented-but-ever-useful vbaObjSetAddRef to the rescue, a reference to the class instance is born!
Code:

'Before:
'Public Function EnumStringNext(ByVal this As oleexpimp.IEnumString, ByVal celt As Long, ByVal rgelt As Long, ByVal pceltFetched As Long) As Long
'now:
Public Function EnumStringNext(ByVal this As Long, ByVal celt As Long, ByVal rgelt As Long, ByVal pceltFetched As Long) As Long
Dim cObj As cEnumString
vbaObjSetAddRef cObj, this
If (cObj Is Nothing) = False Then
    EnumStringNext = cObj.IES_Next(celt, rgelt, pceltFetched)
Else
    Debug.Print "esn obj fail"
End If

End Function

Finally, IAutoCompleteDropdown is used to provide the status of the dropdown autosuggest list. The .DropdownStatus method reports whether it's down, and the text of an item if an item in the list is selected. In the sample project, this is run on an automatically updated timer enabled in the 'basic filesystem' routine. It also exposes the .ResetEnumerator call to update the dropdown list while it's open.

Here's what the code looks like:

cAutoComplete.cls
Code:

Option Explicit

Private pACO As AutoComplete
Private pACL As ACListISF
Private pACL2 As IACList2
Private pACLH As ACLHistory
Private pACLMRU As ACLMRU
Private pACM As ACLMulti
Private pObjMgr As IObjMgr
Private pDD As IAutoCompleteDropDown
Private pUnk As oleexp3.IUnknown
Private m_hWnd As Long
Private pCust As cEnumString

Private Declare Function SysReAllocString Lib "oleaut32.dll" (ByVal pBSTR As Long, Optional ByVal pszStrPtr As Long) As Long
Private Declare Sub CoTaskMemFree Lib "ole32.dll" (ByVal PV As Long)

Private Sub Class_Initialize()
Set pACO = New AutoComplete
End Sub

Public Sub AC_Filesys(hWnd As Long, lOpt As AUTOCOMPLETEOPTIONS)
Set pACL = New ACListISF
pACO.Init hWnd, pACL, "", ""
pACO.SetOptions lOpt
pACO.Enable 1
m_hWnd = hWnd
End Sub
Public Sub AC_Disable()
pACO.Enable 0
End Sub
Public Sub AC_Enable()
pACO.Enable 1
End Sub
Public Sub AC_Custom(hWnd As Long, sTerms() As String, lOpt As AUTOCOMPLETEOPTIONS)
Set pCust = New cEnumString
pCust.SetACStringList sTerms
pACO.Init hWnd, pCust, "", ""
pACO.SetOptions lOpt
pACO.Enable 1
m_hWnd = hWnd
End Sub
Public Sub AC_ACList2(hWnd As Long, lOpt As AUTOCOMPLETEOPTIONS, lOpt2 As AUTOCOMPLETELISTOPTIONS)
Set pACL = New ACListISF
Set pACL2 = pACL
If (pACL2 Is Nothing) = False Then
    pACL2.SetOptions lOpt2
    pACO.Init hWnd, pACL2, "", ""
    pACO.SetOptions lOpt
    pACO.Enable 1
    m_hWnd = hWnd
Else
    Debug.Print "Failed to create IACList2"
End If
End Sub
Public Sub AC_History(hWnd As Long, lOpt As AUTOCOMPLETEOPTIONS)
Set pACLH = New ACLHistory
pACO.Init hWnd, pACLH, "", ""
pACO.SetOptions lOpt
pACO.Enable 1
m_hWnd = hWnd

End Sub
Public Sub AC_MRU(hWnd As Long, lOpt As AUTOCOMPLETEOPTIONS)
Set pACLMRU = New ACLMRU
pACO.Init hWnd, pACLMRU, "", ""
pACO.SetOptions lOpt
pACO.Enable 1
m_hWnd = hWnd

End Sub

Public Sub AC_Multi(hWnd As Long, lOpt As AUTOCOMPLETEOPTIONS, lFSOpts As AUTOCOMPLETELISTOPTIONS, bFileSys As Boolean, bHistory As Boolean, bMRU As Boolean, bCustom As Boolean, Optional vStringArrayForCustom As Variant)

  On Error GoTo e0

Set pACM = New ACLMulti
Set pObjMgr = pACM

If bFileSys Then
    Set pACL = New ACListISF
    Set pACL2 = pACL
    pACL2.SetOptions lFSOpts
    pObjMgr.Append pACL2
End If
If bMRU Then
    Set pACLMRU = New ACLMRU
    pObjMgr.Append pACLMRU
End If
If bHistory Then
    Set pACLH = New ACLHistory
    pObjMgr.Append pACLH
End If
If bCustom Then
    Dim i As Long
    Dim sTerms() As String
    ReDim sTerms(UBound(vStringArrayForCustom))
    For i = 0 To UBound(vStringArrayForCustom)
        sTerms(i) = vStringArrayForCustom(i)
    Next i
    Set pCust = New cEnumString
    pCust.SetACStringList sTerms
    pObjMgr.Append pCust
End If

pACO.Init hWnd, pObjMgr, "", ""
pACO.SetOptions lOpt
pACO.Enable 1
m_hWnd = hWnd
  On Error GoTo 0
  Exit Sub

e0:

    Debug.Print "cAutocomplete.AC_Multi.Error->" & Err.Description & " (" & Err.Number & ")"

End Sub

Public Function DropdownStatus(lpStatus As Long, sText As String)
If pDD Is Nothing Then
    Set pDD = pACO
End If
Dim lp As Long

pDD.GetDropDownStatus lpStatus, lp
SysReAllocString VarPtr(sText), lp
CoTaskMemFree lp

End Function
Public Sub ResetEnum()
If pDD Is Nothing Then
    Set pDD = pACO
End If
pDD.ResetEnumerator
End Sub

Implementing IEnumString's functions:
Code:

Public Function IES_Next(ByVal celt As Long, ByVal rgelt As Long, ByVal pceltFetched As Long) As Long
Dim lpString As Long
Dim i As Long
Dim celtFetched As Long
If rgelt = 0 Then
    IES_Next = E_POINTER
    Exit Function
End If

For i = 0 To (celt - 1)
    If nCur = nItems Then Exit For
    lpString = CoTaskMemAlloc(LenB(sItems(nCur)) & vbNullChar)
    If lpString = 0 Then IES_Next = S_FALSE: Exit Function
   
    CopyMemory ByVal lpString, ByVal StrPtr(sItems(nCur)), LenB(sItems(nCur) & vbNullChar)
    CopyMemory ByVal UnsignedAdd(rgelt, i * 4), lpString, 4&
   
    nCur = nCur + 1
    celtFetched = celtFetched + 1
Next i
 If pceltFetched Then
    CopyMemory ByVal pceltFetched, celtFetched, 4&
 End If
 If i <> celt Then IES_Next = S_FALSE

End Function
Public Function IES_Skip(ByVal celt As Long) As Long
If nCur + celt <= nItems Then
    nCur = nCur + celt
    IES_Skip = S_OK
Else
    IES_Skip = S_FALSE
End If
End Function

For the complete code, see the attached project.

Requirements
-oleexpimp.tlb v2.0 - I've forked and continued olelib2.tlb much the same as I did with the original. This new file replaces olelib2 in the same way oleexp3 replaces olelib (you can run search and replace). This file is included in the main oleexp download.
-oleexp3.tlb v3.8 - New version released with this project (29 Sep 2016)

Thanks
Krool's project mentioned above is what inspired me to do this, and I borrowed a few techniques from his project, especially for IEnumString.
Attached Files

Improved circle drawing

$
0
0
Here's my code for drawing a circle, that has significant improvements over the internal VB6 circle drawing commands.
Code:

Private Sub DrawCircle(ByVal X0 As Long, ByVal Y0 As Long, ByVal Radius As Long, ByVal Color As Long)
    Dim xMax As Long
    Dim Y As Long
    Dim X As Long
    For Y = -Radius To Radius
        xMax = Int(Sqr(Radius * Radius - Y * Y))
        For X = -xMax To xMax
            PSet (X0 + X, Y0 + Y), Color
        Next X
    Next Y
End Sub

The built in DrawWidth property makes a PSet dot bigger, so you can try to draw a circle with it, but it is not even close to being a perfectly symmetrical circle, until it reaches quite large sizes.

The builtin Circle command allows you to make a perfect circle, but the color you set using the color number in the Circle command only effects the circle's outline. To set the interior color of the circle, you have to set the FillColor property in a separate command, and furthermore you need to set the FillStyle to even make the interior of the circle visible (otherwise it's invisible/transparent). So you need to set 2 properties before even running the Circle command, and every time you want to change the color, you need to change the FillColor property.

This DrawCircle method that I created though, makes drawing a perfectly symmetric circle as easy as running one line of code, the code to call the method. All 4 parameters needed to draw the circle are specified at the time of calling the method.

The below sample code shows how to use this method in a MouseDown event in Form1, to make it draw a green circle of radius 10 (center pixel plus 10 pixels out from the center, which some people might call radius 11). The center of the circle will be wherever you click the mouse. Note that ScaleMode property of Form1 should be Pixel (not the default Twip), and that AutoRedraw should be set to True.
Code:

Private Sub Form_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)
    DrawCircle X, Y, 10, &HFF00&
End Sub

Using OERN (On Error Resume Next)

$
0
0
This isn't any substantial piece of code, but it points out a potential problem I see experienced programmers making on these forums. Also, I must give Bonnie West some credit for pointing this out to me and forcing me to develop a clear understanding of it all.

Let me start by outlining how the three On Error... statements work, which isn't well documented in the MSDN.

On Error Resume Next - Always clears the ERR object upon execution, but leaves results in the ERR object even after end/exit from a procedure call.

On Error Goto LineLabel - Always clears the ERR object upon execution, and the ERR object is also cleared upon end/exit from a procedure call, regardless of whether Resume, Resume LineLabel, or Resume Next is used.

On Error Goto 0 - Always clears the ERR object upon execution.

It's the difference between On Error Resume Next and On Error Goto LineLabel that is often unappreciated. To illustrate, I've set up the following example. Just paste it into Form1's code and execute:

Code:


Option Explicit

Private Sub Form_Load()
    MsgBox SomeTestWithOern_TrueIfError
    Unload Me
End Sub

Private Function SomeTestWithOern_TrueIfError() As Boolean
    Dim i As Long
    Dim b As Boolean
    '
    On Error Resume Next
    i = 1 / 1 ' Does NOT cause error.
    '
    ' Just some other function maybe used herein.
    ' In this example, nothing is done with the return, but it could be.
    b = AnotherTestWithOren_TrueIfError
    '
    ' And now we return, thinking that we've only tested our i = 1/1 line for an error.
    SomeTestWithOern_TrueIfError = Err.Number <> 0
End Function

Private Function AnotherTestWithOren_TrueIfError() As Boolean
    Dim i As Long
    '
    On Error Resume Next
    i = 1 / 0 ' Causes error.
    AnotherTestWithOren_TrueIfError = Err.Number <> 0
End Function

In case you don't want to execute it, the message box reports "True", which is misleading. The SomeTestWithOern_TrueIfError didn't technically have any error. That's the point I'm trying to make.

And now, this can be fixed with the addition of a single line, an "On Error Goto 0" at the end of AnotherTestWithOren_TrueIfErro, as follows:

Code:


Option Explicit

Private Sub Form_Load()
    MsgBox SomeTestWithOern_TrueIfError
    Unload Me
End Sub

Private Function SomeTestWithOern_TrueIfError() As Boolean
    Dim i As Long
    Dim b As Boolean
    '
    On Error Resume Next
        i = 1 / 1 ' Does NOT cause error.
        '
        ' Just some other function maybe used herein.
        ' In this example, nothing is done with the return, but it could be.
        b = AnotherTestWithOren_TrueIfError
        '
        ' And now we return, thinking that we've only tested our i = 1/1 line for an error.
        SomeTestWithOern_TrueIfError = Err.Number <> 0
    On Error GoTo 0
End Function

Private Function AnotherTestWithOren_TrueIfError() As Boolean
    Dim i As Long
    '
    On Error Resume Next
        i = 1 / 0 ' Causes error.
        AnotherTestWithOren_TrueIfError = Err.Number <> 0
    On Error GoTo 0
End Function

This time, the message box reports "False"!!! In fact, I put the "On Error Goto 0" in both test functions, just as good programming practice. Also, just to make sure I always "turn off" my "On Error Resume Next" statements, I've adopted the convention of indenting between them.

Regards,
Elroy

EDIT1: Just as an FYI, even my recommendation isn't a perfect fix because the ERR object is truly a global object. Clearing ERR anywhere clears it everywhere.

[CODE] Responsive applications in VB6 (and immune to resolution changes)

$
0
0
Make VB6 applications look, feel and work state-of-the-art.

See this video for more:
https://youtu.be/2RPnJotSYj0

I HAVE HEAVILY MODIFIED THE CODE NOT COMPLETELY CODED IT. THE CREDITS CAN BE FOUND BELOW. I HAVE ALSO CONSOLIDATED OTHER PROJECTS TO CREATE A BASIC PACKAGE FOR BEGINNERS THAT WILL MAKE YOUR APPLICATIONS LOOK GREAT.

With the following code, your form controls will automatically resize with the forms and look awesome (The looks have to do with manifests which I have credited below). Also, it is responsive to resolution changes.

To use the source code:
Visit the manifest creator page for instructions on theming.

For the automatic resizing:
Add the module ScalingModule (Module2) to your project.

Then declare in each form:
Private InitialControlList() As ControlInitial

Then insert the following code in all forms to be resized:

Private Sub Form_Load()
InitialControlList = GetLocation(Me)
ReSizePosForm Me, Me.height, Me.width, Me.Left, Me.Top
End Sub

Private Sub Form_Resize()
ResizeControls Me, InitialControlList
End Sub

Play around with the sample project for more! You may need to add a few components/references for it to work. The exe should probably work out of the box (But I can't assure it).

Credits:

I modified (heavily) the inefficient code for resizing form and controls found here:
http://www.dreamincode.net/forums/to...reen-size-vb6/

The manifest creator for better looks:
http://www.vbforums.com/showthread.p...nifest-Creator
(See #79 on page 2 for better code)

A TON of interfaces to make VB6 look better:
http://www.vbforums.com/showthread.p...ary-oleexp-tlb

Taskbar Progressbar Animation:
http://www.vbforums.com/showthread.p...n-taskbar-etc)

Source Codes:

The Project's Source code (all that you saw in the video) can be found here:
http://bit.do/vb6-1-all

Everything except the taskbar integration:
http://bit.do/vb6-1-no-task

Please credit me and the others above!

VB6 Build-in types extension library (FTypes)

$
0
0
This project aim is to extend Visual Basic 6.0 build-in types (like Integer, Long, String and etc.) in order to make work with it more convinient ("one-liner" style if needed) and support extended properties/methods on that basic types.

Classes:

- ArrayEx
- ByteEx
- IntegerEx
- LongEx
- DoubleEx
- StringEx

Sample usage:

Dim s As New StringEx

s = "Hello"

MsgBox s.Clone.Parse(" Hello VBForum ").TrimL.Insert(0, "'").Concat("!!!'").Upper 'produces 'HELLO VBFORUM !!!'

MsgBox s 'produces Hello since Clone was used in first msgbox


Notes:

- Each class has default property Value that is used to assign/read value of appropriate basic VB 6.0 type;
- Each class has Clone method that produces a new instance of class with same value;
- If class function returns same type as class has - that means NO new instanse created (except Clone method) and call modifies initial value assigned at first use of class Value property;

Most valuable features:

- Aim on performance;
- ArrayEx class reports of dimensions, elements size and allows to get pointer of any type of basic array assigned via Value property;
- StringEx class has powerfull Parse method that can get byte arrays and produce utf-16 native VB6 string (f.e. from ansi, utf-8 with/without bom, utf-16 with/without bom/LE/BE text file);
- StringEx class works with dynamically buffered string, so "Concat" and other methods are very usefull within loops (f.e. 'This Is My New Test String' concatenation of 50000 iterations took ~0.37 sec against same VB6 concatenation taking ~23 sec);
- StringEx class has additional methods like Duplicate, Insert & Remove and etc as well as native to a developer Trim, TrimL/R, Replace, Left, Right and etc methods;
- StringEx class exposes a string pointer so you can manipulate it with your own RtlMoveMemory-based routines;
- ByteEx/IntegerEx/LongEx/DoubleEx classes each have Parse method capable to extract numbers from Variant string as well as get value from numeric types directly without overflow;

List of revisions:

29-Oct-2016
- 1.0.0 (with updates)
Attached Files

VB6 Built-in types extension library (FTypes)

$
0
0
This project aim is to extend Visual Basic 6.0 built-in types (like Integer, Long, String and etc.) in order to make work with it more convinient ("one-liner" style if needed) and support extended properties/methods on that basic types.

Classes:

- ArrayEx
- ByteEx
- IntegerEx
- LongEx
- DoubleEx
- StringEx

Sample usage:

Dim s As New StringEx

s = "Hello"

MsgBox s.Clone.Parse(" Hello VBForum ").TrimL.Insert(0, "'").Concat("!!!'").Upper 'produces 'HELLO VBFORUM !!!'

MsgBox s 'produces Hello since Clone was used in first msgbox


Notes:

- Each class has default property Value that is used to assign/read value of appropriate basic VB 6.0 type;
- Each class has Clone method that produces a new instance of class with same value;
- If class function returns same type as class has - that means NO new instanse created (except Clone method) and call modifies initial value assigned at first use of class Value property;

Most valuable features:

- Aim on performance;
- ArrayEx class reports of dimensions, elements size and allows to get pointer of any type of basic array assigned via Value property;
- StringEx class has powerfull Parse method that can get byte arrays and produce utf-16 native VB6 string (f.e. from ansi, utf-8 with/without bom, utf-16 with/without bom/LE/BE text file);
- StringEx class works with dynamically buffered string, so "Concat" and other methods are very usefull within loops (f.e. 'This Is My New Test String' concatenation of 50000 iterations took ~0.37 sec against same VB6 concatenation taking ~23 sec);
- StringEx class has additional methods like Duplicate, Insert & Remove and etc as well as native to a developer Trim, TrimL/R, Replace, Left, Right and etc methods;
- StringEx class exposes a string pointer so you can manipulate it with your own RtlMoveMemory-based routines;
- ByteEx/IntegerEx/LongEx/DoubleEx classes each have Parse method capable to extract numbers from Variant string as well as get value from numeric types directly without overflow;

List of revisions:

29-Oct-2016
- 1.0.0 (with updates)
Attached Files

VB6 - Simple Sock

$
0
0
SimpleSock basically performs the same functions as NewSocket. Like NewSocket, it supports IPv6 as well as IPv4. This more or less restricts it's use to Windows Vista or better, as older operating systems do not support dual stack using "ws2_32.dll". Unlike NewSocket, it cannot be used as a Control Array because of the way it handles listening sockets (more on that later).

While Emiliano Scavuzzo's subclassing technique remains fairly much intact, the rest of the program has been completely rewritten and hopefully simplified. Notifying the Class with the protocol being used (TCP/UDP) is no longer required. Instead there are separate routines to handle each task. Lets take a look at some of the basics.

UDP (User Datagram Protocol)
I started with this one because it is the simplest. UDP is a peer-to-peer protocol, because both parties are equal and either one can initiate the conversation. It is also connectionless. That is to say that data is just sent with no idea if it made it correctly to the other end. The packet size is also very limited (256 bytes). For these reasons, it is rarely used for sensitive bulk data. In the sample program provided, an instance of SimpleSock is created called "mSocket". "mSocket" defaults to IPv4, so if IPv6 is required, you must notify the instance by setting the mSocket.IPvFlg to 6. To initiate a UDP session, you simply call:
Code:

mSocket.UDPInit(Destination, PortConnect, PortLocal)
The Destination Port and the Local Port are required, but if it is not known, the Destination can be left blank. This might be the case if the initial receiver does not know where the first message will be originating from. If blank, the GetAddrInfo function will return the LoopBack address (127.0.0.1 for IPv4 & ::1 for IPv6). You can test this functionality by setting the UDP option and the Local and Destination ports (they can both be the same), and typing a message in the text box followed by an <Enter>. The program will send the message to itself and the sender address (127.0.0.1/::1) will appear in the Destination text box. In the real world however, the sender's IP address will appear in the Destination text box, at which point the user can once again call the UDPInit function to update its information.

So what information gets updated? The first time through, UPDInit creates the socket and binds it to the Local Port. It then creates a "sockaddr" for the destination using GetAddrInfo. The sockaddr structure is the part that gets updated. For those familiar with the original IPv4 structure, it looked like this:
Code:

Private Type sockaddr_in
    sin_family          As Integer  '2 bytes
    sin_port            As Integer  '2 bytes
    sin_addr            As in_addr  '4 bytes
    sin_zero(0 To 7)    As Byte    '8 bytes
End Type                            'Total 16 bytes
or reflected as:
Private Type sockaddr
    sa_family          As Integer  '2 bytes
    sa_data(0 to 13)    As Byte    '14 bytes
End Type                            'Total 16 bytes

When IPv6 came along, this had to be changed to:
Code:

Private Type sockaddr_in6
    sin6_family        As Integer  '2 bytes
    sin6_port          As Integer  '2 bytes
    sin6_flowinfo      As Long    '4 bytes
    sin6_addr          As in6_addr '16 bytes
    sin6_scope_id      As Long    '4 bytes
End Type                            'Total 28 bytes
Private Type sockaddr
    sa_family          As Integer  '2 bytes
    sa_data(0 to 25)    As Byte    '26 bytes
End Type                            'Total 28 bytes

The larger sockaddr is used to carry the information for both IP protocols, with the extra 12 bytes being ignored for IPv4. Because the packet data is of limited length, UDP data is left in the Winsock Buffer and the calling program is informed of it's length. The calling program then recovers the data and empties the Winsock Buffer.

To send data via UDP, we need the Socket Handle, the binary Data and it's length, and the sockaddr and it's length for the destination. The data is passed to the output buffer as string data and converted to byte data, or sent directly to the output buffer as byte data. Providing that the sockaddr has been updated correctly, all the information is available to send back to the other end with a call to mSocket.UDPSend.

TCP (Transport Control Protocol)
The more commonly used protocol is TCP. There are actually 2 types of TCP, because one end acts as the server, and one end acts as the client. Lets look at the client end first, because it is the simpler. We establish a connection with the other end by calling:
Code:

mSocket.TCPConnect(Destination, PortConnect)
We supply the Destination as either an IP address or a domain name, and the destination port as a long variable. GetAddrInfo will find the IP address for a Domain name, provided the name is defined in a DNS host, or it is a local network name. Normally, the Local port is not required, as the API will find the first available port. SimpleSock however does have the ability to use a selected port. If the port selected is not being used, it will bind the created socket to the port. It also eliminates the TIME_WAIT period by setting the options "SO_LINGER" & "SO_REUSEADDR". For reasons unknown, I had to set both these options to achieve the desired result. The API will send out a SYN request to the other end, and wait for a response. If the other end is listening for a connection request, it will send a SYN_ACK back to us. The API will acknowledge this by sending an ACK, and the connection is established. Once the connection is established, a "Connect" event is fired back to the calling program, and data can be sent immediately using "TCPSend".

Receipt of data is similar to UDP, except that SimpleSock removes the data from the Winsock buffer and adds it to it's own buffer. This is necessary because sent records can be quite lengthy, and are received in chunks. What is different about SimpleSock is the provision to handle encrypted data. This is accomplished by using 2 separate event messages (DataArrival/EncrDataArrival) to inform the calling program of data arrival.

To act as a TCP server, the socket is created and bound to the selected port using:
Code:

mSocket.Listen(PortListen)
When a connection request is received from the other end, the API sends an "FD_ACCEPT" message to the "PostSocket" routine. This is where SimpleSock differs from NewSocket and it predecessors. The older programs would create a new socket and a temporary instance of the class to handle it. It would then be registered as an "Accept" item, before firing off a "ConnectionRequest" event to the calling program. The calling program would then close the Listening socket and call the class "Accept" function with the new socket handle. Closing of the listening socket and de-registering it caused the Socket Collection to be destroyed and the Window closed. The new socket would then be registered as a normal socket (causing a new Window and Socket Collection to be created), ownership of the new socket transferred from the temporary Class to the original Class, and the temporary Class destroyed. The calling program would then create a new Listening Socket. If this all sounds very complicated, it was. But it was necessary in order to duplicate the way that the MS Winsock Control handled things when used as a Control Array.

When SimpleSock receives an "FD_ACCEPT" message from an incoming connection attempt, it creates and registers the new socket as it normally would, and leaves the original listening socket intact. It then fires off a "ConnectionRequest" event to the calling program. The calling program then calls mSocket.Accept with the new socket handle. The Accept function saves the listening socket handle, sets a flag, and readies the new socket to receive and send data. If another connection request is received while the new socket is open, it will be ignored because the new socket is not in the listening mode. When the new socket is closed, the listening socket handle will be restored, and another connection request will be entertained.

This simplified approach is only useful when using the SimpleSock Class directly. It will not be effective if it was made into a Control and used as a Control Array. The next step is to make the Class able to handle multiple connections on the same listening port without creating a Control.

J.A. Coutts

Note: When using Link Local IPv6 addresses to communicate with older systems such as Vista, you may have to add the interface (eg. %8) to the IP address.

Note: The sample program is a demonstration program that uses various aspects of the socket function. It may not work properly when switching from one to another. Restart the program to test different functions.
Attached Images
 
Attached Files

[VB6] SAX: Not just for XML

$
0
0
MXHTMLWriter is a handy feature added to MSXML SAX2 in version 6.0, but few have probably heard of SAX and few still of MXHTMLWriter.

See MXHTMLWriter CoClass for an overview.

There are several ways to use MXHTMLWriter but here I'll turn it "inside out" by explicitly raising events to it via IVBSAXContentHandler instead of letting other parts of MSXML raise the events. This is a very basic example showing how to do that to write HTML, and in this case the demo involves simple reporting.

Depending on your purpose you might want the results in different ways. Here I show how to get file output, String output, and Byte array output (since for that we can get UTF-8 or other character encodings).

It should be plenty speedy enough for most purposes:

Name:  sshot.png
Views: 56
Size:  3.1 KB

Here is the crux of the demo:

Code:

Private Sub Report(ByRef Dest As Variant, Optional ByVal Encoding As String = "ASCII")
    'Dest:    Can be an instance of an IStream implementation or a String.
    '
    'Encoding: Can be "UTF-8" or "Windows-1252" or "UTF-16" etc. as desired.
    '          Always ignored for String output which is always UTF-16
    '          ("Unicode").
    Const REPORT_TITLE As String = "January 2009 Sales"
    Const CSS_STYLES As String = vbNewLine _
        & "*{font:normal normal normal 8pt Arial;}" & vbNewLine _
        & "th,td{border:1px solid black;}" & vbNewLine _
        & "th{background-color:royalblue;color:white;font-weight:bold;}" & vbNewLine _
        & "td{background-color:white;color:green;}" & vbNewLine _
        & "table,th,td{border-collapse:collapse;}" & vbNewLine _
        & ".SH{color:red;}"
    Dim Attrs As MSXML2.SAXAttributes60
    Dim Handler As MSXML2.IVBSAXContentHandler
    Dim Writer As MSXML2.MXHTMLWriter60
    Dim FieldsUB As Long
    Dim Fields() As ADODB.Field
    Dim Col As Long
    Dim Row As Long
    Dim LatitudeField As Long
    Dim Value As Variant

    Set Attrs = New MSXML2.SAXAttributes60
    Set Writer = New MSXML2.MXHTMLWriter60
    Set Handler = Writer
    With Writer
        .disableOutputEscaping = False
        .indent = True
        .Encoding = "ASCII"
        .byteOrderMark = True 'Has no effect for 8-bit encodings or any String output.
        .output = Dest 'Can be an IStream implementation, or a String value to set
                      'the output type to String.
    End With
    With RS
        .MoveFirst
        FieldsUB = .Fields.Count - 1
        ReDim Fields(FieldsUB)
        For Col = 0 To FieldsUB
            Set Fields(Col) = .Fields(Col)
            If Fields(Col).Name = "Latitude" Then LatitudeField = Col
        Next
    End With
    With Handler
        .startDocument
        .startElement "", "", "HTML", Attrs
        .startElement "", "", "HEAD", Attrs 'Auto-emits a META tag for encoding.
        Attrs.addAttribute "", "", "name", "", "generator"
        Attrs.addAttribute "", "", "content", "", App.CompanyName _
                                                & " " & App.EXEName _
                                                & " " & CStr(App.Major) _
                                                & "." & CStr(App.Minor)
        .startElement "", "", "META", Attrs
        Attrs.Clear
        .endElement "", "", "META"
        .startElement "", "", "TITLE", Attrs
        .characters REPORT_TITLE
        .endElement "", "", "TITLE"
        Attrs.addAttribute "", "", "type", "", "text/css"
        .startElement "", "", "STYLE", Attrs
        Attrs.Clear
        .characters CSS_STYLES
        .endElement "", "", "STYLE"
        .endElement "", "", "HEAD"
        .startElement "", "", "BODY", Attrs
        .startElement "", "", "TABLE", Attrs
        .startElement "", "", "TR", Attrs
        For Col = 0 To FieldsUB
            .startElement "", "", "TH", Attrs
            .characters Replace$(Fields(Col).Name, "_", " ")
            .endElement "", "", "TH"
        Next
        .endElement "", "", "TR"
        Do Until RS.EOF
            'Hightlight rows for Southern Hemisphere:
            If Fields(LatitudeField).Value < 0 Then
                Attrs.addAttribute "", "", "class", "", "SH"
            Else
                Attrs.Clear
            End If
            .startElement "", "", "TR", Attrs
                For Col = 0 To FieldsUB
                    .startElement "", "", "TD", Attrs
                    Value = Fields(Col).Value
                    If Not IsNull(Value) Then .characters CStr(Value)
                    .endElement "", "", "TD"
                Next
            .endElement "", "", "TR"
            RS.MoveNext
        Loop
        .endElement "", "", "TABLE"
        .endElement "", "", "BODY"
        .endElement "", "", "HTML"
        .endDocument
    End With
    With Writer
        .Flush
        If VarType(Dest) = vbString Then
            Dest = .output 'Fetch String output.
        End If
    End With
End Sub

The attachment contains some raw data, which is why it is so large.

MSXML 6.0 has been part of Windows since Vista. You might still be able to download a redist version for XP SP2 or maybe SP3 from Microsoft.
Attached Images
 
Attached Files

[VB6, Vista+] Remember Open/Save state per-dialog instead of per-app (IFileDialog)

$
0
0
This code tip applies to the new Common Item Dialog interfaces, IFileOpenDialog and IFileSaveDialog, that replace the old Common Dialog control and GetOpenFileName/GetSaveFileName API calls in Windows Vista and newer. See this project for an introduction to using these interfaces.

Users definitely appreciate the common dialogs opening to the last path, and programmers that this is automatically handled by Windows. Previously, this was limited however to a single state memory for the entire app; but among the many new features of the new IFileDialog-based Common Dialogs is the ability to have Windows manage it automatically for individual dialogs, as well as clear the settings without mucking about in the registry.

The key is IFileDialog's .SetClientGuid method. You can specify a unique GUID for your dialog, and the settings like last path are stored under the GUID, instead of under your app's name.

This example code is based on accessing the new Common Item Dialogs through my oleexp type library, with the IID module loaded as well.

First, establish GUIDs for each dialog you want to have its own settings. Two are shown here, but there's no limit to how many an app can have. These must new, unique GUIDs. Visual Studio 6 came with a tool called GUIDGEN, but there's plenty of other GUID generators out there.

Code:

Private Declare Function CLSIDFromString Lib "ole32" (ByVal lpszGuid As Long, pGuid As Any) As Long
Private Const sGUID_Dialog1 = "{A4BF774D-0029-4c8f-9174-B397211B92F5}"
Private Const sGUID_Dialog2 = "{83E461A9-5341-46f5-8825-EAC176603E94}"

Private Function GUID_Dialog1() As UUID
Static iid As UUID
 If (iid.Data1 = 0) Then Call CLSIDFromString(StrPtr(sGUID_Dialog1), iid)
 GUID_Dialog1 = iid
End Function
Private Function GUID_Dialog2() As UUID
Static iid As UUID
 If (iid.Data1 = 0) Then Call CLSIDFromString(StrPtr(sGUID_Dialog2), iid)
 GUID_Dialog2 = iid
End Function

Then all you have to do it associate the Common Dialog with the GUID. Per MSDN, this should be the very first call after creating the dialog:
Code:

Dim pFOD As New FileOpenDialog
pFOD.SetClientGuid GUID_Dialog1 'whichever GUID you want here
'[...now set any other options

And that's all there is to it. If you want to clear the saved state, simply call .ClearClientData after setting the associated GUID (you can also clear it for the app-level state by calling it without having set a GUID).

Attached is a small project showing this technique in action. It requires oleexp.tlb (version 4.0 or newer is referenced) and addon mIID.bas from the oleexp zip.
Attached Files

[VB6, Vista+] Direct access to the system-wide image thumbnail cache

$
0
0

While in general you want to use IShellItemImageFactory to get these thumbnails, as that will also return icons, if you're interested in more control or better performance, you can use IThumbnailCache and the Windows supplied implementation LocalThumbnailCache for direct access to the main system thumbnail cache.

There's a large number of additional options, and even more still if you're using Windows 8 or higher. You can choose whether to extract if not in the cache, only retrieve it if cached already, or extract it again to update the cached version. While not shown in the picture above, these thumbnails do properly render transparency.

Main routine (see full project for declares, module-level vars, etc):
Code:

Private Sub Command1_Click()
Dim fod As New FileOpenDialog
Dim kfPics As IShellItem
Dim tSpec() As COMDLG_FILTERSPEC
Dim pBitmap As ISharedBitmap
Dim hBmp As Long
Dim lFlag As WTS_CACHEFLAGS
Dim btID As WTS_THUMBNAILID
Dim tSZ As SIZE
Dim lOpt As WTS_FLAGS
On Error GoTo e0

ReDim tSpec(1)

tSpec(0).pszName = "Image Files"
tSpec(0).pszSpec = "*.gif;*.jpg;*.png;*.ico;*.bmp"
tSpec(1).pszName = "All Files"
tSpec(1).pszSpec = "*.*"

fod.SetClientGuid GUID_ThisProject
fod.SetTitle "Choose an image"
fod.SetOkButtonLabel "Show Thumbnail"
fod.SetOptions FOS_DONTADDTORECENT
fod.SetDefaultFolder kfPics
fod.SetFileTypes 2&, VarPtr(tSpec(0).pszName)
fod.Show Me.hWnd

On Error Resume Next
fod.GetResult psiFile
On Error GoTo e0

If (psiFile Is Nothing) = False Then
    If (pCache Is Nothing) Then
        Set pCache = New LocalThumbnailCache
    End If
    'Note: Many WTS options are Win8+ only. Here we're only demonstrating basic ones that are Win7+
    If Option1(0).Value = True Then lOpt = WTS_EXTRACT Or WTS_SCALETOREQUESTEDSIZE
    If Option1(1).Value = True Then lOpt = WTS_INCACHEONLY Or WTS_SCALETOREQUESTEDSIZE
    If Option1(2).Value = True Then lOpt = WTS_FORCEEXTRACTION Or WTS_SCALETOREQUESTEDSIZE
    If Check1.Value = vbChecked Then
        If (Option1(0).Value = True) Or (Option1(2).Value = True) Then lOpt = lOpt Or WTS_EXTRACTDONOTCACHE
    End If
    pCache.GetThumbnail psiFile, cxThumb, lOpt, pBitmap, lFlag, btID
    If (pBitmap Is Nothing) = False Then
        pBitmap.GetSize tSZ
        Debug.Print "Got bitmap obj, cx=" & tSZ.CX & ",flag=0x" & Hex$(lFlag)
        PrintThumbID btID
        pBitmap.GetSharedBitmap hBmp
        Debug.Print "hBITMAP=" & hBmp
        Picture1.Cls
        hBitmapToPictureBox Picture1, hBmp
        pBitmap.Detach hBmp
        DeleteObject hBmp
    Else
        Debug.Print "Failed to get bitmap obj, flag=0x" & Hex$(lFlag)
    End If
Else
    Debug.Print "No file selected."
End If

Exit Sub

e0:
    Debug.Print "GetThumb.Error->" & Err.Description & " (0x" & Hex$(Err.Number) & ")"
End Sub

Requirements
-Windows Vista or higher. Some options in demo project are Windows 7 and higher. The interface itself has many options only available on Windows 8 and higher, although none are used in the demo.
-oleexp.tlb version 4.0 or higher. Only needed for the IDE, doesn't need to be redistributed with your exe.

Notes
Thumbnails are looked up by providing an IShellItem representing the file. In the sample, this is super easy as that's what's returned from the FileOpenDialog. But without that, you can get that reference from any number of methods, including SHGetItemFromParsingName:
Code:

Public Declare Function SHCreateItemFromParsingName Lib "shell32" (ByVal pszPath As Long, pbc As Any, riid As UUID, ppv As Any) As Long

Call SHCreateItemFromParsingName(StrPtr(pathtofile), ByVal 0&, IID_IShellItem, psi)

Or from a pidl,
Code:

Public Declare Function SHCreateItemFromIDList Lib "shell32" (ByVal pidl As Long, riid As UUID, ppv As Any) As Long
Alternative Access to Thumbnail
Closely related, there's a very simple way to get the thumbnail of an image file (and only image file; this won't return a normal icon either) represented by an IShellItem:

Code:

Dim hbmTP As Long
Dim pTP As IThumbnailProvider
Dim psiImg As IShellItem

Call SHCreateItemFromParsingName(StrPtr("C:\folder\MyImage.jpg"), ByVal 0&, IID_IShellItem, psiImg)
psiImg.BindToHandler 0&, BHID_ThumbnailHandler, IID_IThumbnailProvider, pTP
If (pTP Is Nothing) = False Then
    pTP.GetThumbnail 128&, hbmTP, WTSAT_ARGB 'where 128 is the desired size. 16-256, maybe 512 work the best
    Debug.Print "hbm=" & hbmTP
    hBitmapToPictureBox Picture1, hbmTP
Else
    Debug.Print "no ptp"
End If

Where the hBitmapToPictureBox is the same as the demo project. This code snippet also makes use of mIID.bas from the oleexp download.
This method has the bonus of an option controlling transparency.
Attached Files

[VB6, Vista+] Add the Windows Send To submenu to your popup menu

$
0
0

So at first I set out to just duplicate the functionality, but then immediately saw the FOLDERID_SendTo special folder, and realized that it should be possible to add a fully functional SendTo menu. It's not just creating something similar, it actually implements the same Send To menu you get in Explorer- using shell interfaces to perform the actions the exact same way.

This project is a little high on the complexity scale, but not too bad.

The core parts of the code look like this:
Code:

Public psiSTChild() As IShellItem 'need to store the loaded SendTo items so they can be called when selected
Public Const widBaseST = 2800&
Public widSTMax As Long

Public Function GenerateSendToMenu() As Long
'it's the callers responsibility to call DestroyMenu()
Dim mii As MENUITEMINFOW
Dim i As Long, j As Long, k As Long
Dim hIcon As Long
Dim isiif As IShellItemImageFactory
Dim hMenu As Long
Dim lpCap As Long
Dim sCap As String
hMenu = CreateMenu()
Dim s1 As String, lp1 As Long
Dim psiSendTo As IShellItem
Dim nChild As Long
Dim pcl As Long
Dim penum As IEnumShellItems

On Error GoTo e0

Call SHGetKnownFolderItem(FOLDERID_SendTo, KF_FLAG_DEFAULT, 0&, IID_IShellItem, psiSendTo)
If (psiSendTo Is Nothing) = False Then
    psiSendTo.BindToHandler 0&, BHID_EnumItems, IID_IEnumShellItems, penum
    If (penum Is Nothing) = False Then
        ReDim psiSTChild(0)
        Do While (penum.Next(1&, psiSTChild(nChild), pcl) = S_OK)
            psiSTChild(nChild).GetDisplayName SIGDN_NORMALDISPLAY, lpCap
            sCap = LPWSTRtoStr(lpCap)
            Set isiif = psiSTChild(nChild)
            isiif.GetImage 16, 16, SIIGBF_ICONONLY, hIcon
            With mii
                .cbSize = Len(mii)
                .fMask = MIIM_ID Or MIIM_STRING Or MIIM_BITMAP
                .wID = (widBaseST + j)
                .cch = Len(sCap)
                .dwTypeData = StrPtr(sCap)
                .hbmpItem = hIcon
                Call InsertMenuItemW(hMenu, j, True, mii)
   
                Call DestroyIcon(hIcon)
                j = j + 1
            End With
            Set isiif = Nothing
            nChild = nChild + 1
            ReDim Preserve psiSTChild(nChild)
        Loop
    Else
        Debug.Print "GenerateSendToMenu->Failed to get enum obj"
    End If
Else
    Debug.Print "GenerateSendToMenu->Failed to get SendTo folder obj"
End If
widSTMax = j
GenerateSendToMenu = hMenu
Exit Function
e0:
Debug.Print "GenerateSendToMenu.Error->" & Err.Description & " (" & Err.Number & ")"
End Function

GenerateSendToMenu creates a submenu for a standard API popup menu. The shell items loaded from the SendTo folder are stored in a public array, so we can access them after a selection is made:
Code:

If idCmd Then
    Select Case idCmd
        Case widBaseST To (widBaseST + widSTMax)
            Dim lp As Long
            psiSTChild(idCmd - widBaseST).GetDisplayName SIGDN_NORMALDISPLAY, lp
            If MsgBox("Send to " & LPWSTRtoStr(lp) & "?", vbYesNo, "Confirm SendTo") = vbYes Then
                ExecSendTo (idCmd - widBaseST)
            End If
    End Select
End If

Finally, we use a technique you may recall from my Create Zip Files demo- dropping an IDataObject representing the files we're moving onto an IDropTarget belonging to the destination:
Code:

Private Sub ExecSendTo(nIdx As Long)
Dim pdt As IDropTarget
psiSTChild(nIdx).BindToHandler 0&, BHID_SFUIObject, IID_IDropTarget, pdt
If ((pdt Is Nothing) = False) And ((pdoFiles Is Nothing) = False) Then
    Dim dwEffect As Long
    dwEffect = DROPEFFECT_COPY Or DROPEFFECT_MOVE Or DROPEFFECT_LINK
    pdt.DragEnter pdoFiles, MK_LBUTTON, 0&, 0&, dwEffect
    pdt.Drop pdoFiles, MK_LBUTTON, 0&, 0&, dwEffect
End If
End Sub

As an added bonus, picking the files with IFileOpenDialog makes it super-easy to get the IDataObject for the files, pdoFiles.
Code:

Dim fod As New FileOpenDialog
Dim psiaRes As IShellItemArray
With fod
    .SetOptions FOS_ALLOWMULTISELECT Or FOS_DONTADDTORECENT
    .SetTitle "Choose files for SendTo..."
    .Show Me.hWnd
    .GetResults psiaRes
    If (psiaRes Is Nothing) = False Then
        psiaRes.BindToHandler 0&, BHID_DataObject, IID_IDataObject, pdoFiles
    End If
End With

Requirements
-Windows Vista or newer
-oleexp.tlb v4.0 or higher (only for IDE, doesn't need to be included with compiled exe)
-mIID.bas - included in the oleexp download

Extra Thoughts
Generate IDataObject from file list
If you want to get an IDataObject but just have a list of file paths, you can do it like this, where sSelFullPath is a string array of full paths to the files:
Code:

Public Declare Function SHCreateShellItemArrayFromIDLists Lib "shell32" (ByVal cidl As Long, ByVal rgpidl As Long, ppsiItemArray As IShellItemArray) As Long
Public Declare Function ILCreateFromPathW Lib "shell32" (ByVal pwszPath As Long) As Long


Dim psia As IShellItemArray
Dim pdoFiles As oleexp.IDataObject
Dim apidl() As Long
Dim i As Long

ReDim apidl(0)
For i = 0 To UBound(sSelFullPath)
    ReDim Preserve apidl(i)
    apidl(i) = ILCreateFromPathW(StrPtr(sSelFullPath(i)))
Next i
Call SHCreateShellItemArrayFromIDLists(UBound(apidl) + 1, VarPtr(apidl(0)), psia)
psia.BindToHandler 0&, BHID_DataObject, IID_IDataObject, pdoFiles

Customizing the enumeration
Say, for example, you want to override the user preference for hidden files (in the pic up top, Desktop.ini is shown because my system is set to show all hidden/system files). There's two ways go about this. If you're targeting only Windows 8 and above, you can play around with the wonderful world of the IBindCtx parameter with STR_ENUM_ITEMS_FLAGS
Windows Vista and Windows 7 however, you're going to have to drop down to IShellFolder and use the .EnumObjects SHCONTF options. Doing it in VB with oleexp requires far less code than Raymond uses, if anyone is really interested I could write up the VB code.
Attached Files

[VB6] SHBrowseForFolder - Custom filter for shown items: BFFM_IUNKNOWN/IFolderFilter

$
0
0

It's possible to have complete control over what items are shown in the SHBrowseForFolder dialog. The picture above shows a filter of *.exe applied to a dialog with the BIF_BROWSEINCLUDEFILES option, but you can filter in a wide variety of ways as the IShellFolder and pidl for each item is passed, allowing you to get the name and compare by string and properties, as in the demo, or anything else you could want. The project notes where you could even filter by SHCONTF options.
This is accomplished through the BFFM_IUNKNOWN message that is received in the callback function. A lot of places have mentioned what it's for, but I wanted to show the actual details of using that message to set up a filter.

First, you create a class module that implements the IFolderFilter interface and create an instance of it before calling the dialog. The GetEnumFlags method is where you can filter by SHCONTF, but this demo is mainly concerned with examining each item in the ShouldShow method. Whether to show the item or not is based on the return code, so the class module function is swapped out to a function in the module. Here's the demo filters files, but not folders, according to the pattern specified in the text box:
Code:

Public Function ShouldShowVB(ByVal this As IFolderFilter, ByVal psf As IShellFolder, ByVal pidlFolder As Long, ByVal pidlItem As Long) As Long
Dim psi As IShellItem
Dim lpName As Long, sName As String
Dim dwAtr As Long
On Error GoTo e0

SHCreateItemWithParent 0&, psf, pidlItem, IID_IShellItem, psi
If (psi Is Nothing) = False Then
    psi.GetAttributes SFGAO_FILESYSTEM Or SFGAO_FOLDER, dwAtr
    If ((dwAtr And SFGAO_FILESYSTEM) = SFGAO_FILESYSTEM) And ((dwAtr And SFGAO_FOLDER) = 0) Then 'is in normal file system, is not a folder
        psi.GetDisplayName SIGDN_PARENTRELATIVEPARSING, lpName
        sName = LPWSTRtoStr(lpName)
        Debug.Print "ShouldShow?" & sName & "|" & gSpec
        If PathMatchSpecW(StrPtr(sName), StrPtr(gSpec)) Then
            ShouldShowVB = S_OK 'should show
        Else
            ShouldShowVB = S_FALSE 'should not show
        End If
    End If
Else
    Debug.Print "ShouldShow.NoItem"
End If

Exit Function
e0:
Debug.Print "ShouldShowVB.Error->" & Err.Description
End Function

Now that the filter object and routine are good to go, it needs to be assigned to the dialog. When the BFFM_IUNKNOWN message fires, the lParam contains a pointer to an IUnknown object which implements IFolderFilterSite, which contains the call to assign our filter class. If the messages fires but the object is Nothing, the filter class needs to be released and reset, otherwise a subsequent call to SHBrowseDialog won't be filtered.
Code:

Public Function BrowseCallbackProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal lParam As Long, ByVal lpData As Long) As Long
Dim pSite As IFolderFilterSite
Dim pUnk As oleexp.IUnknown

Select Case uMsg

    Case BFFM_IUNKNOWN
        'lParam contains a pointer to an IUnknown that implements IFolderFilterSite
        Debug.Print "Received BFFM_IUNKNOWN"
        vbaObjSetAddRef pUnk, lParam
        Debug.Print "Set obj"
        If (pUnk Is Nothing) = False Then
            Set pSite = pUnk
            If (pSite Is Nothing) = False Then
                Debug.Print "Setting filter"
                pSite.SetFilter cFilter
                Debug.Print "Filter set"
            Else
                Debug.Print "Failed to set pSite"
            End If
        Else
            Debug.Print "Failed to set pUnk"
            Set cFilter = Nothing
        End If
End Select
End Function

And that's about it. The rest is just calling the dialog like normal (+making a new instance of the cFolderFilter class first).

Requirements
-The demo project requires Windows Vista or newer, although it could theoretically be reworked to support XP.
-oleexp 4.1 or newer (this project requires a bug fixed only in 4.1, not 4.0)
-mIID.bas (included in the oleexp download)
Attached Files
Viewing all 1324 articles
Browse latest View live




Latest Images