0

In VBA, I declared a Windows API function that requires a pointer to a struct argument. For representing this struct I created a Public Type and declared the function parameter as ByRef.

Now, this struct pointer may be null, so I tried to assign Nothing to a variable of my UDT, but that doesn't work.

How can I make this work?

Here are the basic excerpts from my code:

Private Declare PtrSafe Function GetNumberFormatEx& Lib "Kernel32" ( _
  ByVal lpLocaleName As LongPtr, _
  ByVal dwFlags&, _
  ByVal lpValue As LongPtr, _
  ByRef lpFormat As NumberFormat, _
  ByVal lpNumberStr As LongPtr, _
  ByVal cchNumber& _
)


Public Type NumberFormat
  NumDigits As Integer
  LeadingZero As Integer
  Grouping As Integer
  lpDecimalSep As LongPtr
  lpThousandSep As LongPtr
  NegativeOrder As Integer
End Type


Public Function FormatNumberLocale$(srcValue As Double, lcid$, Optional flags& = 0, Optional customFormat$ = vbNullString)
  Dim buffer$
  Dim charCount&
  Dim numFormat As NumberFormat

  buffer = String(100, 0)
  'numFormat = Nothing    ' THIS DOESN'T WORK !!!
  charCount = GetNumberFormatEx(StrPtr(lcid), flags, StrPtr(Str$(srcValue)), numFormat, StrPtr(buffer), 100)
  
  If charCount > 0 Then FormatNumberLocale = Left$(buffer, charCount)
End Function

Edit

I changed the declaration to:

Private Declare PtrSafe Function GetNumberFormatEx& Lib "Kernel32" ( _
  ByVal lpLocaleName As LongPtr, _
  ByVal dwFlags&, _
  ByVal lpValue As LongPtr, _
  ByVal lpFormat As LongPtr, _
  ByVal lpNumberStr As LongPtr, _
  ByVal cchNumber& _
)

and called the function like this:

...
Dim value$

buffer = String(100, 0)
value = Str$(srcValue)
charCount = GetNumberFormatEx(StrPtr(lcid), flags, StrPtr(value), CLngPtr(0&), StrPtr(buffer), 100)

But even when calling it with basic parameters, like ?FormatNumberLocale(123,"en"), charCount is always 0, and Err.LastDllError always returns 87 (0x57): ERROR_INVALID_PARAMETER.

Any ideas?

AxD
  • 2,714
  • 3
  • 31
  • 53
  • Is [this answer](https://stackoverflow.com/a/23273757/19353309) of some help to you explaining the difference between `Null` and `Nothing`? Since you're NumberFormat isn't an object variable, assigning it Nothing won't work? Just speculating, I'm not that familiar with UDT. – Notus_Panda Jul 23 '23 at 11:14
  • VBA doesn't know StrPtr type. Probably this is VB.Net. Pls. correct the tags. – Black cat Jul 23 '23 at 17:28
  • @Blackcat StrPtr is a VBA function https://bettersolutions.com/vba/functions/strptr-function.htm – M. Johnstone Jul 23 '23 at 18:22
  • @AxD Do you have an example populating the NumberFormat structure? If optional and In parameter I think should be a matter of setting it to a Null Ptr or VarPtr(numFormat) and ByVal lpFormat As LongPtr. I was digging up code for a similar case for TzSpecificLocalTimeToSystemTime was working on. I think GSerg solution should work. – M. Johnstone Jul 24 '23 at 01:07
  • 1
    @M.Johnstone: You may just copy above code into a VBA module and try for yourself. Everything in the example above is at the ready. Providing actual data from the type is working, but I need to provide a NULL-Pointer to the function. That's the question here. – AxD Jul 24 '23 at 12:06
  • `Any ideas?` - your `Str$(srcValue)` adds a leading space that is not allowed. – GSerg Jul 24 '23 at 14:58
  • Oh, yes. Gee, I didn't notice that … D'uh – AxD Jul 24 '23 at 20:58
  • Is the LCID correct? Try obtaining your local lcid [GetSystemDefaultLocaleName](https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getsystemdefaultlocalename) Just experimenting with it atm. Apart from the guess when I get around to doing a .Net COM wrapper for Formating numbers etc. Atm only got around with DateTime and havent got around to formatting. Apart from that [VBCorLib](github.com/kellyethridge/VBCorLib) thou it's VB6 only compatible thou might be able to borrow sections for formatting. – M. Johnstone Jul 24 '23 at 21:34
  • OK got it working will update my answer later when cleaned up the code. I first obtained my local lcid and used byte array for buffer, i.e. varptr(buffer(0)). Then resized the byte array buffer to (charCount - 1) * 2 and stringOutput = buffer. – M. Johnstone Jul 24 '23 at 23:07
  • Updated my answer. Improvements could add error handling to raise errors. – M. Johnstone Jul 24 '23 at 23:25
  • Also added an custom format example. Note you can't pass UDT's as optional parameters. Possible could wrap the NumberFormat UDT in a class and pass the object as optional and if is missing parameter use NULL_PTR else VarPtr of the NumberFormat UDT obtained from the class wrapping it. – M. Johnstone Jul 25 '23 at 04:28

2 Answers2

2

The idiomatic VBA way of passing NULL for a struct pointer is declaring the argument as ByRef As Any:

Private Declare PtrSafe Function GetNumberFormatEx Lib "Kernel32" ( _
  ByVal lpLocaleName As LongPtr, _
  ByVal dwFlags As Long, _
  ByVal lpValue As LongPtr, _
  ByRef lpFormat As Any, _
  ByVal lpNumberStr As LongPtr, _
  ByVal cchNumber As Long _
) As Long

and then passing either a struct variable (numFormat in your example) or ByVal 0& for null.

GSerg
  • 76,472
  • 17
  • 159
  • 346
  • There is no `Any` type in VBA: See [Why can't I use the "Any" as a name in VBA?](https://stackoverflow.com/questions/25316973/why-cant-i-use-the-any-as-a-name-in-vba) – AxD Jul 23 '23 at 22:44
  • @AxD If you read [the answer](https://stackoverflow.com/a/25317051/11683) under that question, you will see that it correctly points out that `Any` is a reserved word that can be used in a Declare statement, but cannot be used to declare a variable of type `Any`. That entire question is irrelevant to begin with, because it discusses using `Any` as a variable name even, not as a type. – GSerg Jul 24 '23 at 07:58
  • Ah, I see … I changed to `Any`, but unfortunately to no avail. After doing some more testing, I updated my question now as there seems to be another issue with the declaration. – AxD Jul 24 '23 at 12:36
  • I [received another excellent solution, similar to yours, here](https://www.vba-forum.de/View.aspx?ziel=81306-Kann_Windows_API-Funktion_GetNumberFormatEx()_nicht_aufrufen_%E2%80%93_was_mache_ich_falsch?). Something in that solution I wasn't aware of yet is the ability to provide `ByVal` as function argument if `Any` is used. I have never seen `Any` being documented anywhere. – AxD Jul 24 '23 at 21:01
  • I dont think it's appropriate for ByRef lpFormat As ANY as lpFormat is a POINTER to a NUMBERFMT structure. – M. Johnstone Jul 25 '23 at 08:45
  • @M.Johnstone What do you think `ByRef` does? – GSerg Jul 25 '23 at 08:45
  • @Greg It' just misleading that acccepts ANY type which I technically a pointer could be for any type. – M. Johnstone Jul 25 '23 at 08:55
-1
'@References
' https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getnumberformatex
' https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getsystemdefaultlocalename

Option Explicit

'@Reference
' https://learn.microsoft.com/en-us/windows/win32/api/winnls/ns-winnls-numberfmta
Public Type NumberFormat
  NumDigits As Long             'Number of fractional digits placed after the decimal separator.
  LeadingZero As Long           '0   No leading zeros, 1   Leading zeros
  Grouping As Long              'Values in the range 0 through 9 and 32 are valid
                                'Typical examples of settings for this member are: 0 to group digits as in 123456789.00;
                                '3 to group digits as in 123,456,789.00; and 32 to group digits as in 12,34,56,789.00.
  lpDecimalSep As LongPtr       'Pointer to a null-terminated decimal separator string.
  lpThousandSep As LongPtr      'Pointer to a null-terminated thousand separator string.
  NegativeOrder As Long         'Negative number mode. This mode is equivalent to the locale information specified by the value
                                'https://learn.microsoft.com/en-us/windows/win32/intl/locale-ineg-constants
End Type

Private Const LOCALE_NOUSEROVERRIDE As Long = &H80000000
Private Const NULL_PTR As LongPtr = 0

' https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
' https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--1000-1299-
Private Const ERROR_OUTOFMEMORY As Long = 14           '(0xE)
Private Const ERROR_INVALID_PARAMETER As Long = 87     '(0x57)
Private Const ERROR_INSUFFICIENT_BUFFER As Long = 122  '(0x7A)
Private Const ERROR_INVALID_FLAGS As Long = 1004       '(0x3EC)

Private Declare PtrSafe Function GetSystemDefaultLocaleName Lib "Kernel32" ( _
    ByVal lpLocaleName As LongPtr, _
    ByVal cchLocaleName As Long _
    ) As Long
    
Private Declare PtrSafe Function GetNumberFormatEx Lib "Kernel32" ( _
    ByVal lpLocaleName As LongPtr, _
    ByVal dwFlags As Long, _
    ByVal lpValue As LongPtr, _
    ByVal lpFormat As LongPtr, _
    ByVal lpNumberStr As LongPtr, _
    ByVal cchNumber As Long _
    ) As Long
    
'@Exceptions
'   ERROR_INSUFFICIENT_BUFFER. A supplied buffer size was not large enough, or it was incorrectly set to NULL.
Public Function GetSystemLocalName() As String
    Const LOCALE_NAME_MAX_LENGTH As Long = 85
    Const CHAR_LENGTH As Long = 2
    
    Dim buffer() As Byte
    ReDim buffer(LOCALE_NAME_MAX_LENGTH * CHAR_LENGTH)
    Dim bufferPtr As LongPtr
    bufferPtr = VarPtr(buffer(0))
    Dim charCount As Long
    charCount = GetSystemDefaultLocaleName(bufferPtr, LOCALE_NAME_MAX_LENGTH)
    If charCount > 0 Then
        ReDim Preserve buffer((charCount - 1) * CHAR_LENGTH)
        GetSystemLocalName = buffer
    Else
        Select Case Err.LastDllError
        Case ERROR_INSUFFICIENT_BUFFER
            Err.Raise Err.LastDllError, "GetSystemLocalName", "A supplied buffer size was not large enough, or it was incorrectly set to NULL."
        Case Else
            Err.Raise Err.LastDllError, "GetSystemLocalName", "Unexpected error occurred."
        End Select
    End If
End Function

'@Exceptions
'   ERROR_INSUFFICIENT_BUFFER. A supplied buffer size was not large enough, or it was incorrectly set to NULL.
'   ERROR_INVALID_FLAGS. The values supplied for flags were not valid.
'   ERROR_INVALID_PARAMETER. Any of the parameter values was invalid.
'   ERROR_OUTOFMEMORY. Not enough storage was available to complete this operation.
Public Function FormatNumberLocale(ByVal value As Double, ByVal lcid As String, Optional ByVal flags As Long = 0) As String
    Const MAX_BUFFER_LENGTH As Long = 100
    Const CHAR_LENGTH As Long = 2
        
    Dim buffer() As Byte
    ReDim buffer(MAX_BUFFER_LENGTH * CHAR_LENGTH)
    Dim bufferPtr As LongPtr
    bufferPtr = VarPtr(buffer(0))
    Dim charCount As Long
    charCount = GetNumberFormatEx(StrPtr(lcid), flags, StrPtr(CStr(value)), NULL_PTR, bufferPtr, MAX_BUFFER_LENGTH)
    If charCount > 0 Then
        ReDim Preserve buffer((charCount - 1) * CHAR_LENGTH)
        FormatNumberLocale = buffer
    Else
        Select Case Err.LastDllError
        Case ERROR_INSUFFICIENT_BUFFER
            Err.Raise Err.LastDllError, "FormatNumberLocale", "A supplied buffer size was not large enough, or it was incorrectly set to NULL."
        Case ERROR_INVALID_FLAGS
            Err.Raise Err.LastDllError, "FormatNumberLocale", "The values supplied for flags were not valid."
        Case ERROR_INVALID_PARAMETER
            Err.Raise Err.LastDllError, "FormatNumberLocale", "Any of the parameter values was invalid."
        Case ERROR_OUTOFMEMORY
            Err.Raise Err.LastDllError, "FormatNumberLocale", "Not enough storage was available to complete this operation."
        Case Else
            Err.Raise Err.LastDllError, "FormatNumberLocale", "Unexpected error occurred."
        End Select
    End If
End Function

Public Sub FormatNumberLocaleTest()
    Dim value As Double
    Dim lcid As String
    Dim valueLocal As String
    
    value = 12345.678
    lcid = GetSystemLocalName()
    valueLocal = FormatNumberLocale(value, lcid, LOCALE_NOUSEROVERRIDE)
    Debug.Print " Value: " & value
    Debug.Print " Format value local: " & valueLocal
    Debug.Print " System Local Name:  " & lcid
    Debug.Print
    
    lcid = "de-DE"
    valueLocal = FormatNumberLocale(value, lcid)
    Debug.Print " Value: " & value
    Debug.Print " Format value local: " & valueLocal
    Debug.Print " System Local Name:  " & lcid
    Debug.Print
End Sub

'Output:
' Value: 12345.67
' Format value local: 12,345.67
' System Local Name:  en-AU
'
' Value: 12345.67
' Format value local: 12.345,67
' System Local Name:  de-DE


'Notes UDT types are not allowed to optional and must be passed by reference
'Possible work around wrap the UDT in an object and check if missing use NULL_PTR or VarPtr of UDT of type NumberFormat
Public Function FormatNumberCustom(ByVal value As Double, ByVal lcid As String, ByVal flags As Long, ByRef customFormat As NumberFormat) As String
    Const MAX_BUFFER_LENGTH As Long = 100
    Const CHAR_LENGTH As Long = 2

    Dim buffer() As Byte
    ReDim buffer(MAX_BUFFER_LENGTH * CHAR_LENGTH)
    Dim bufferPtr As LongPtr
    bufferPtr = VarPtr(buffer(0))
    Dim charCount As Long
    charCount = GetNumberFormatEx(StrPtr(lcid), flags, StrPtr(CStr(value)), VarPtr(customFormat), bufferPtr, MAX_BUFFER_LENGTH)
    If charCount > 0 Then
        ReDim Preserve buffer((charCount - 1) * CHAR_LENGTH)
        FormatNumberCustom = buffer
    Else
        Select Case Err.LastDllError
        Case ERROR_INSUFFICIENT_BUFFER
            Err.Raise Err.LastDllError, "FormatNumberCustom", "A supplied buffer size was not large enough, or it was incorrectly set to NULL."
        Case ERROR_INVALID_FLAGS
            Err.Raise Err.LastDllError, "FormatNumberCustom", "The values supplied for flags were not valid."
        Case ERROR_INVALID_PARAMETER
            Err.Raise Err.LastDllError, "FormatNumberCustom", "Any of the parameter values was invalid."
        Case ERROR_OUTOFMEMORY
            Err.Raise Err.LastDllError, "FormatNumberCustom", "Not enough storage was available to complete this operation."
        Case Else
            Err.Raise Err.LastDllError, "FormatNumberCustom", "Unexpected error occurred."
        End Select
    End If
End Function

Public Sub CustomFormatNumberTest()
    Dim value As Double
    Dim lcid As String
    Dim customFormat As String

    value = -12345.678
    Dim decimalSeparator As String
    decimalSeparator = "@"
    Dim thousandSepartor As String
    thousandSepartor = "#"
    
    Dim customNumberFormat As NumberFormat
    customNumberFormat.NumDigits = 2   '
    customNumberFormat.LeadingZero = 1 'Leading zero's
    customNumberFormat.Grouping = 3
    customNumberFormat.lpDecimalSep = StrPtr(decimalSeparator)
    customNumberFormat.lpThousandSep = StrPtr(thousandSepartor)
    customNumberFormat.NegativeOrder = 4 'Number, space, negative sign; for example, 1.1 -

    lcid = "de-DE"
    customFormat = FormatNumberCustom(value, lcid, 0, customNumberFormat)
    Debug.Print " Value: " & value
    Debug.Print " Custom format value : " & customFormat
    Debug.Print " System Local Name:  " & lcid
    Debug.Print
    
    lcid = "en-AU"
    customFormat = FormatNumberCustom(value, lcid, 0, customNumberFormat)
    Debug.Print " Value: " & value
    Debug.Print " Custom format value : " & customFormat
    Debug.Print " System Local Name:  " & lcid
    Debug.Print
    
    value = 12345.678
    lcid = "en-AU"
    customFormat = FormatNumberCustom(value, lcid, 0, customNumberFormat)
    Debug.Print " Value: " & value
    Debug.Print " Custom format value : " & customFormat
    Debug.Print " System Local Name:  " & lcid
    Debug.Print
End Sub

'Output:
' Value: -12345.678
' Custom format value : 12#345@68 -
' System Local Name:  de-DE
'
' Value: -12345.678
' Custom format value : 12#345@68 -
' System Local Name:  en-AU
'
' Value: 12345.678
' Custom format value : 12#345@68
' System Local Name:  en-AU
M. Johnstone
  • 72
  • 1
  • 6