wtorek, 26 stycznia 2016

• Opisowy typ zmiennej

W artykule Uchwyt okna w 64-bitowym środowisku VBA 7, poruszającym zagadnienie przekazania uchwytu hwnd w 32 bitowej i 64 bitowej wersji MS Access 2010+ oraz udzielenia odpowiedzi na pytanie: „Czy w 64 bitowym VBA 7 właściwość Me.hwnd jest liczbą typu LongLong” w celu sprawdzenia typu zmiennej użyłem funkcji:

Function VarType(VarName) As VbVarType

zwracającej liczbę typu Integer określającą typ zmiennej.
Aby określić opisowy typ zmiennej, który odpowiada zwracanej wartości, spróbuję napisać funkcję, która zwróci nam numeryczny i opisowy typ zmiennej. Najprościej by było skorzystać z tabeli ze strony msdn.microsoft.com - Opis Funkcji VarType

StałaWartośćOpis
vbEmpty0Empty (puste, niezainicjowana)
vbNull1Null (brak prawidłowych wartości)
vbInteger2Integer (liczba całkowita)
vbLong3Long integer (liczba całkowita długa)
vbSingle4Single-precision (liczba zmiennoprzecinkowa pojedynczej precyzji)
vbDouble5Double-precision (liczba zmiennoprzecinkowa podwójnej precyzji)
vbCurrency6Currency value (wartość walutowa)
vbDate7Date value (Data/Godzina)
vbString8String (ciąg znaków)
vbObject9Object (obiekt)
vbError10Error value (wartość błędu)
vbBoolean11Boolean value (wartość logiczna)
vbVariant12Variant (używana tylko w przypadku tablicy typu Variant)
vbDataObject13A data access object (obiekt dostępu do danych)
vbDecimal14Decimal value (wartość dziesiętna)
vbByte17Byte value (wartość bajtowa)
vbLongLong20LongLong integer (liczba 64-bitowa, tylko w środowisku 64-bitowym)
vbUserDefinedType36Variants that contain user-defined types (typ Variant zawierający typy zdefiniowane przez użytkownika)
vbArray8192Array
• vbArray
Funkcja VarType(...) nigdy nie zwraca samej wartości vbArray. W przypadku przekazania tablicy do funkcji VarType(...) zwracana jest wartość  = 8192 powiększona o wartość stałej odpowiadającej typowi przekazanej tablicy.

 ' zadeklarujmy tablicę typu Long
Dim lngTablica() As Long
 VarType(lngTablica) '- zwraca 8195 = vbArray + vbLong = 8192 + 3

 ' zadeklarujmy tablicę typu Object
Dim objObiekty() As Object
 VarType(objObiekty) '- zwraca 8201 = vbArray + vbObject = 8192 + 9
 
• vbVariant
Stała vbVariant jest zwracana tylko w połączeniu ze stałą vbArray w celu wskazania, że argument funkcji VarType(...) jest tablicą wartości typu Variant.
 ' zadeklarujmy tablicę typu Variant
Dim varTablica() As Variant
 VarType(varTablica) ' - zwraca 8204 = vbArray + vbVariant = 8192 + 12

 ' zadeklarujmy zmienną typu Variant
Dim varVariant As Variant
 ' przekształćmy ją w tablicę typu Variant
  varVariant = Array("Lista elementów")
 VarType(varVariant) ' - zwraca 8204 = vbArray + vbVariant = 8192 + 12

 ' Ale przypisanie do zmiennej typu Variant, tablicy typu Long, nie daje tablicy typu Variant
Dim lngTablica() As Long
  ' przypisz do zmiennej typu Variant tablicę typu Long
  varVariant = lngTablica
 VarType(varVariant) ' - zwraca 8192 = vbArray + vbLong = 8192 + 3
 
• vbObject
Jeżeli obiekt ma właściwość domyślną, wywołanie funkcji VarType(obiekt) zwróci typ domyślnej właściwości obiektu.
  ' dla formantu klasy CommandButton (przycisk)
  VarType (Me.btnTest) ' - zwraca 9 = vbObject
  
  ' dla niezwiązanego formantu klasy TextBox (pole tekstowe),
  ' którego domyślną właściwością jest .Value funkcja VarType zwraca:
  ' 1. dla pustego formantu
  VarType (Me.txtTextBox) ' - zwraca 1 = vbNull - Null (brak poprawnych danych)
  
  ' 2. po wpisaniu z klawiatury liczby 123
  VarType (Me.txtTextBox) ' - zwraca 8 = vbString - String (ciąg znaków)
  
  ' 3. po wypełnieniu formantu za pomocą kodu
  Me.txtTextBox = 123
  MsgBox VarType(Me.txtTextBox) ' - zwraca 2 = vbInteger - Integer (liczba całkowita)
 
• vbInteger
Po zadeklarowaniu zmiennej jako Variant i przypisaniu jej wartości z zakresu typu Byte funkcja VarType(VarName) zwróci domyślny typ vbInteger (liczba całkowita). Nie ma powodów do obaw, że dla niejawnego przypisania do zmiennej typu Variant liczby całkowitej leżącej poza zakresem liczby typu Integer (-32768 do 32767), VBA obetnie liczbę do zakresu Integer. W takim przypadku funkcja VarType(VarName) zwróci nam typ vbLong (liczba całkowita długa).
• vbDouble
Po zadeklarowaniu zmiennej jako Variant i przypisaniu jej wartości z zakresu typu Single funkcja VarType(VarName) zwróci domyślny typ vbDouble (liczba zmiennoprzecinkowa podwójnej precyzji).
 ' zadeklarujmy zmienną typu Variant
 Dim varVariant As Variant
  
  varVariant = 123
  MsgBox VarType(varVariant) ' - zwraca 2 = vbInteger - Integer (liczba całkowita)
  
  varVariant = 32768
  MsgBox VarType(varVariant) ' - zwraca 3 = vbLong - Long (liczba całkowita długa)
  
  varVariant = 123.01
  MsgBox VarType(varVariant) ' - zwraca 5 = vbDouble -
                             '   Double (liczba zmiennoprzecinkowa podwójnej precyzji)
 

Mając już jako takie podstawy odnośnie typów zmiennych zwracanych przez funkcję VarType(VarName), spróbę napisać funkcję zwracającą liczbowy i opisowy typ zmiennych. Jeden mały problem to stała vbLongLong patrz tabela typów zmiennych. Stała ta występuje tylko w 64-bitowym środowisku VBA 7. Ale od czego mamy stałą kompilacji warunkowej Win64. No to jeden problem z głowy. Drugim problemem jest określenie typu tablicy, kiedy wartość zwracana przez funkcję VarType(VarName) jest większa od vbArray = 8192. W tym przypadku musimy rekurencujnie wywołać funkcję z argumentem (iType - vbArray), by określić typ tablicy. I to już wszystko:

' Public Function GetVarType(iType As Integer) As String
' -------------------------------------------------------
' autor: Zbigniew Bratko - 01.2016
' [iType] - liczba typu Integer, zwracana przez funkcję VarType(varname)
' [Out] - zwraca wartość opisową typu zmiennej, przypisanej do argumentu iType
' Uwaga !
'   - w przypadku przekazania argumentu iType > vbArray odpowiadajacego tablicy,
'   następuje rekurencyjne wywołanie funkcji GetVarType(iType - vbArray)
'   w celu określenia typu tablicy.
'   - stała kompilacji warunkowej #If Win64 została użyta dla typu vbLongLong, ponieważ
'   w 32-bitowym środowisku stała ta nie występuje (wystąpi błąd kompilacji)
'
' wywołanie - GetVarType(VarType(VarName))
'

Public Function GetVarType(iType As Integer) As String

  Select Case iType
    Case vbEmpty
      GetVarType = "Typ = " & CStr(vbEmpty) & " - Empty (niezainicjowana)"
    Case vbNull
      GetVarType = "Typ = " & CStr(vbNull) & " - Null (brak poprawnych danych)"
    Case vbInteger
      GetVarType = "Typ = " & CStr(vbInteger) & " - Integer (liczba całkowita)"
    Case vbLong
      GetVarType = "Typ = " & CStr(vbLong) & " - Long (liczba całkowita długa)"
    Case vbSingle
      GetVarType = "Typ = " & CStr(vbSingle) & " - Single (liczba zmiennoprzecinkowa" & _
                   " pojedynczej precyzji)"
    Case vbDouble
      GetVarType = "Typ = " & CStr(vbDouble) & " - Double (liczba zmiennoprzecinkowa" & _
                   " podwójnej precyzji)"
    Case vbCurrency
      GetVarType = "Typ = " & CStr(vbCurrency) & " - Currency (typ walutowy)"
    Case vbDate
      GetVarType = "Typ = " & CStr(vbDate) & " - Date (data, godzina)"
    Case vbString
      GetVarType = "Typ = " & CStr(vbString) & " - String (ciąg znaków)"
    Case vbObject
      GetVarType = "Typ = " & CStr(vbObject) & " - Object (obiekt)"
    Case vbError
      GetVarType = "Typ = " & CStr(vbError) & " - Error (błąd)"
    Case vbBoolean
      GetVarType = "Typ = " & CStr(vbBoolean) & " - Boolean (wartość logiczna Prawda/Fałsz)"
    Case vbVariant
      GetVarType = "Typ = " & CStr(vbVariant) & " - Variant (tylko dla tablic Variant)"
    Case vbDataObject
      GetVarType = "Typ = " & CStr(vbDataObject) & " - (Dane dostępu do obiektu)"
    Case vbDecimal
      GetVarType = "Typ = " & CStr(vbDecimal) & " - Decimal (typ dziesiętny)"
    Case vbByte
      GetVarType = "Typ = " & CStr(vbByte) & " - Byte (liczba całkowita - bajt)"
    #If Win64 Then
      Case vbLongLong
        GetVarType = "Typ = " & CStr(vbLongLong) & " - LongLong (liczba całkowita 8-bajtowa" & _
                    " - tylko 64-bitowe środowisko)"
    #End If
    Case vbUserDefinedType
      GetVarType = "Typ = " & CStr(vbUserDefinedType) & _
                   " - Variants that contain user-defined" & _
                   " types" & vbNewLine & _
                   " - (typ Variant zawierający typy zdefiniowane przez użytkownika)"
    ' Case vbArray
      ' wartość vbArray nie jest zwracana przez funkcję VarType
      ' GetVarType = "Typ = " & CStr(vbArray) & " - Tablica"
    Case Is > vbArray
      ' wywołaj rekurencyjnie, by sprawdzić typ tablicy
      GetVarType = "Tablica (" & CStr(vbArray) & "), " & GetVarType(iType - vbArray)
    Case Else
      GetVarType = "Typ = " & CStr(iType) & " - unknown (nieznany)"
  End Select
 
End Function
 
' przykładowe wywołanie
Private Sub btnTest_Click()
 ' zmienna typu Variant
 Dim varVariant As Variant
   ' konwertuj na tablicę
   varVariant = Array("Ala", "Ola")

   MsgBox GetVarType(VarType(varVariant))

 ' zmienna typu String
 Dim strString As String

   MsgBox GetVarType(VarType(strString))
End Sub

wtorek, 19 stycznia 2016

• Uchwyt okna w 64-bitowym VBA 7.

• Czy w 64-bitowym VBA 7 właściwość Me.hwnd jest liczbą typu LongLong ?

Z artykułu VBA 7.0 i MS Access 2010+ wiemy, że w 32-bitowym środowisku wszystkie uchwyty i wskaźniki są liczbą typu Long, a w środowisku 64-bitowym liczbą typu LongLong. W celu ułatwienia pisanie przenośnego kodu wprowadzono w środowisku VBA 7 nową liczbę typu LongPtr, która przyjmuje w 32-bitowym środowisku typ Long (liczba 4 bajtowa), a w 64-bitowej wersji typ LongLong (8 bajtowa liczba).
Przypisanie uchwytu lub wskaźnika w środowisku 64-bitowym do zmiennej typu Long jest niepoprawne i może prowadzić do nieprzewidzianych błędów, ponieważ 64-bitowy uchwyt lub wskaźnik może zostać „ucięty” do wartości 32-bitowej i wskazywać na nieprawidłowy obiekt. Piszę, „może zostać ucięty”, gdyż w przypadku nieprzekroczenia przez uchwyt zakresu liczby Long, nie zmieni on wartości przy przypisaniu do zmiennej typu Long.

• Jak przekazać uchwyt hwnd w 32-bitowej i 64-bitowej wersji MS Access 2010+ ?

Napiszmy prostą przykładową funkcję by pobrać i przypisać do zmiennej hWind uchwyt hwnd okna formularza MS Access i odczytać jego tytuł za pomocą funkcji API GetTextWindow(...).

#If VBA7 Then
  Private Declare PtrSafe Function GetWindowText Lib "user32" _
          Alias "GetWindowTextA" ( _
          ByVal hwnd As LongPtr, _
          ByVal lpString As String, _
          ByVal cch As Long) As Long
#Else
  Private Declare Function GetWindowText Lib "user32" _
          Alias "GetWindowTextA" ( _
          ByVal hwnd As Long, _
          ByVal lpString As String, _
          ByVal cch As Long) As Long
#End If

#If VBA7 Then
  Private Function GetFormCaption(hWind As LongPtr) As String
#Else
  Private Function GetFormCaption(hWind As Long) As String
#End If

  Dim lRet As Long
  Dim sBuffer As String
  Const conSize_Buffer = 256

  ' przygotuj buffor na przyjęcie tytułu okna
  sBuffer = String(conSize_Buffer, vbNullChar)

  ' pobierz do bufora tytuł okna i zwróć ilość pobranych znaków
  lRet = GetWindowText(hWind, sBuffer, conSize_Buffer)
  
  GetFormCaption = Left$(sBuffer, lRet)

End Function

' przykładowe wywołanie
Private Sub btnTest_Click()

  ' ustaw nowy tytuł formularza
  Me.Caption = "Nowy tytuł formularza"
  
  ' odczytaj tytuł formularza
  MsgBox GetFormCaption(Me.hwnd)
  
End Sub

Po uruchomieniu kodu pojawia się okno komunikatu:

Wszystko się zgadza. Odczytany tytuł formularza jest taki sam, jaki został ustawiony za pomocą właściwości:

Me.Caption = "Nowy tytuł formularza"

Praktycznie w tym miejscu, w poczuciu dobrze spełnionego obowiązku, powinienem zakończyć ten artykuł. Jednakże przeprowadzę jeszcze jeden malutki test odnośnie typu uchwytu hwnd. Zgodnie z wszystkimi wytycznymi Microsoftu uchwyt w 64-bitowym środowisku powinien być 8-bajtową liczbą typu LongLong, a w 32-bitowym środowisku 4-bajtową liczbą typu Long. Do sprawdzenia typu zmiennej użyję funkcji VarType( varname ), która dla zmiennej typu LongLong zwraca wartość vbLongLong = 20, a dla zmiennej typu Long zwraca wartość vbLong = 3 i tylko te dwa typy będą uwzględnione w procedurze testowej.

Private Sub GetTypeHwnd()
#If VBA7 Then
  Dim hWind As LongPtr
#Else
  Dim hWind As Long
#End If

Dim sRet As String

  'sprawdź typ zmiennej hWind
  sRet = sRet & "Zmienna  hWind  jest typu: " & _
         IIf(VarType(hWind) = vbLongLong, "LongLong", "Long")
  sRet = sRet & vbNewLine & vbNewLine
  ' sprawdź typ uchwytu okna
  sRet = sRet & "Uchwyt Me.hwnd jest typu: " & _
         IIf(VarType(Me.hwnd) = vbLongLong, "LongLong", "Long")
  
  MsgBox sRet

End Sub

Znowu ZONK ! Co widać na poniższym oknie komunikatu:

Zmienna hWind jest typu LongLong, bo tak została zadeklarowana:

Dim hWind As LongPtr

Ale dlaczego właściwość Me.hwnd jest liczbę typu Long w 64-bitowym VBA 7.0 ? Aby odpowiedzieć na to pytanie wystarczy spojrzeć na deklarację tej właściwości w oknie „Object Browser”

Property Hwnd As Long
Problem ten dotyczy także metody Application.hWndAccessApp, która ma postać:
Function hWndAccessApp() As Long
i w 64-bitowym środowisku VBA 7.0 zwróci uchwyt okna MS Access jako liczbę typu Long zamiast liczby typu LongLong.

niedziela, 10 stycznia 2016

• Wywołanie funkcje API w 64-bitowym VBA 7.0

Z postu z dnia 25 grudnia 2015 r. • VBA 7.0 i MS Access 2010+  „mniej więcej wiemy” (co nie znaczy, że mniej niż więcej , że w pakiecie Office 2010+ wprowadzono nową wersję Microsoft Visual Basic for Applications (VBA) oznaczoną jako VBA 7.0. Wersja ta poprawia wydajność aplikacji w 64 bitowych środowiskach. Równocześnie umożliwia tworzenie aplikacji zgodnych z wcześniejszymi wersjami pakietu Office (2007 i wersje niższe). Aby umożliwić przystosowanie kodu do konkretnej wersji pakietu Office wprowadzono dwie nowe stałe kompilacji warunkowej:

VBA7
• pozwala ustalić, czy używane jest nowe środowisko VBA 7.0, czy też starsze wersje VBA.
Win64
• umożliwia sprawdzenie, czy środowisko VBA jest 64 bitowe, czy 32 bitowe.

oraz nowy kwalifikator:

PtrSafe
• używany do deklarowania procedur i funkcji z zewnętrznych bibliotek DLL w VBA 7 (zawsze w 64 bitowym środowisku VBA7, opcjonalnie w 32 bitowym)

• Jak zadeklarować i uruchomić funkcje API w MS Access 2010+

API - Application Programming Interface jest to zbiór funkcji i procedur znajdujących się w plikach dll (Dynamic Link Library). Inaczej mówiąc są to biblioteki dołączane dynamicznie.

Podstawowe biblioteki w systemie Windows to:
• USER32.dll - funkcje zarządzania środowiskiem Windows
• KERNEL32.dll - funkcje zarządzania funkcjami systemu operacyjnego
• GDI32.dll - Graphics Device Interface - funkcje zarządzające wyprowadzaniem danych na zewnętrzne urządzenia.

Jak zadeklarować i wywołać funkcje API, tak by prawidłowo działały zarówno w 32-bitowym, jak i 64-bitowym środowisku VBA 7.0 oraz w wersjach niższych VBA możemy dowiedzieć się na portalu MSDN (Microsoft Developer Network), na stronie: Working with VBA in the 32-bit and 64-bit Versions of Office 2010. Skorzystajmy więc z rad Microsoftu:

Applies to: Microsoft Office 2010
Published: May 2010
Provided by: Frank Rice, Microsoft Corporation

Jak używać stałej kompilacji VBA7 dowiemy się na podstawie przykładowej funkcji DisplayExcelWindowSize zwracającej położenie i wymiary okna głównego programu Excel 2010. W tym celu musimy zadeklarować strukturę RECT oraz dwie funkcje API:

• funkcję FindWindow znajdującą okno o określonej klasie i (lub) tytule,
• funkcję GetWindowRect określającą położenie i wymiary okna.

Ponieważ deklaracje tych funkcji są różne w wersji 32 i 64  bitowej, należy zastosować stałą kompilacji warunkowej VBA7 przekierowującą kompilator do odpowiedniej sekcji kodu.
Funkcja DisplayExcelWindowSize wywołuje FindWindowi GetWindowRect i wyświetla okno komunikatu z położeniem i wymiarami okna programu Excel 2010.

' A user-defined type to store the window dimensions.
Type RECT
    Left As Long
    Top As Long
    Right As Long
    Bottom As Long
End Type

' Test which version of VBA you are using.
#If VBA7 Then
   ' API function to locate a window.
   Declare PtrSafe Function FindWindow Lib "user32" _
      Alias "FindWindowA" ( _
      ByVal lpClassName As String, _
      ByVal lpWindowName As String) As LongPtr
    
   ' API function to retrieve a window's dimensions.
   Declare PtrSafe Function GetWindowRect Lib "user32" ( _
      ByVal hwnd As LongPtr, _
      lpRect As RECT) As Long
#Else
   ' API function to locate a window.
   Declare Function FindWindow Lib "user32" _
      Alias "FindWindowA" ( _
      ByVal lpClassName As String, _
      ByVal lpWindowName As String) As Long
    
   ' API function to retrieve a window's dimensions.
   Declare Function GetWindowRect Lib "user32" ( _
      ByVal hwnd As Long, _
      lpRect As RECT) As Long
#End If


Sub DisplayExcelWindowSize()
   Dim hwnd As Long, uRect As RECT
   
   ' Get the handle identifier of the main Excel window.
   hwnd = FindWindow("XLMAIN", Application.Caption)
   
   ' Get the window's dimensions into the RECT UDT.
   GetWindowRect hwnd, uRect
   
   ' Display the result.
   MsgBox "The Excel window has these dimensions:" & _
      vbCrLf & " Left: " & uRect.Left & _
      vbCrLf & " Right: " & uRect.Right & _
      vbCrLf & " Top: " & uRect.Top & _
      vbCrLf & " Bottom: " & uRect.Bottom & _
      vbCrLf & " Width: " & (uRect.Right - uRect.Left) & _
      vbCrLf & " Height: " & (uRect.Bottom - uRect.Top)
End Sub

Po zapoznaniu się z kodem ze strony Microsoftu, pozostało tylko przekopiowanie kodu do modułu programu Excel 2010 (wersja 64 bitowa) i uruchomienie funkcji DisplayExcelWindowSize (...), by uzyskać informacje o położeniu i wymiarach głównego okna programu Excel 2010. W dobrym zwyczaju jest skompilowanie kodu przed jego uruchomieniem.
No i  ZONK !

Kompilator zgłasza błąd „Niezgodność typów”. I co najgorsze (nie dla mnie), kompilator ma rację.
Zgodnie z deklaracją, funkcja FindWindow (...) zwraca typ LongPtr, który w 64 bitowym środowisku jest liczbą typu LongLong. „Zamiatając pod dywan”, szybko zmieniamy deklarację uchwytu okna hwnd:

Dim hwnd As Long
  na
Dim hwnd As LongPtr
  lub
Dim hwnd As LongLong

i możemy cieszyć się wynikiem:

Wszystko by było dobrze, gdyby nie pakiet Office w wersjach 2007 i niższych. Próba kompilacji poprawionego kodu w środowisku VB6 kończy się niepowodzeniem.

Kompilator wyświetla okno komunikatu informujące, że napotkał „Niezdefiniowany typ użytkownika” i podświetla linię kodu zawierającą deklarację uchwytu okna: Dim hwnd As LongPtr. Podobny błąd wystąpi, gdybyśmy zadeklarowali uchwyt okna hwnd jako typ LongLong. I nie ma się czemu dziwić, ponieważ wersja VBA 6.0 Office 2007 i wersji niższych nie obsługuje typów LongPtrLongLong.
Aby kod działał prawidłowo w VBA 7.0 i wersjach niższych VBA musimy więc poprawić deklarację uchwytu okna hwnd, tak by był prawidłowego typu w obu wersjach VBA. W tym celu powinniśmy skorzystać ze stałej kompilacji warunkowej Win64 i typu LongLong

Sub DisplayExcelWindowSize()
#If Win64 Then
   Dim hwnd As LongLong
#Else
   Dim hwnd As Long
#End If

lub ze stałej kompilacji warunkowej VBA7 i typu LongPtr
Sub DisplayExcelWindowSize()
#If VBA7 Then
   Dim hwnd As LongPtr
#Else
   Dim hwnd As Long
#End If
Dim uRect As RECT

' Get the handle identifier of the main Excel window.
hwnd = FindWindow("XLMAIN", Application.Caption)

' Get the window's dimensions into the RECT UDT.
GetWindowRect hwnd, uRect

' Display the result.
MsgBox "The Excel window has these dimensions:" & _
   vbCrLf & " Left: " & uRect.Left & _
   vbCrLf & " Right: " & uRect.Right & _
   vbCrLf & " Top: " & uRect.Top & _
   vbCrLf & " Bottom: " & uRect.Bottom & _
   vbCrLf & " Width: " & (uRect.Right - uRect.Left) & _
   vbCrLf & " Height: " & (uRect.Bottom - uRect.Top)
End Sub