Pokazywanie postów oznaczonych etykietą LongPtr. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą LongPtr. Pokaż wszystkie posty

niedziela, 6 marca 2016

• Funkcje API - właściwość Err.LastDllError

W poprzednim artykule • Funkcje API - Mutex, jedna instancja bazy MS Access • zasygnalizowałem problem zbyt lakonicznego komunikatu w przypadku niepowodzenia podczas wywołania funkcji API CreateMutex(...). Komunikat typu:

MsgBox "Nieprzewidziany błąd wywołania biblioteki DLL." & vbNewLine & _
Numer błędu: " & Err.LastDllError

wydaje się być dość zdawkowy i nie daje końcowemu użytkownikowi zbyt wiele informacji o przyczynie błędu.
Spróbuję przedstawić rozwiązania pokazujące, jak w przypadku wystąpienia błędu zewnętrznej biblioteki DLL, przekonwertować „suchy numer” błędu Err.LastDllError na opisowy komunikat tekstowy, odnoszący się do zaistniałego błędu.
Zrobię to na przykładzie wywołania funkcji API • GetFileAttributes(...), która przy powodzeniu zwraca atrybuty pliku lub katalogu, a przy niepowodzeniu zwraca wartość INVALID_FILE_ATTRIBUTES = -1.
Bardziej szczegółowe informacje o przyczynach niepowodzenia wywołania funkcji, możemy uzyskać z opisu kodu błędu systemowego wygenerowanego przy wywołaniu biblioteki dołączanej dynamicznie (DLL). Kod błędu systemowego możemy pobrać dzięki właściwości Err.LastDllError.

• Opis błędu o kodzie odpowiadającym właściwości Err.LastDllError

Najpierw musimy zadeklarować funkcję API • GetFileAttributes(...), a następnie spróbować wywołać ją z nieprawidłowym argumentem, tak by wywołanie funkcji • GetFileAttributes(...) zakończyło się niepowodzeniem, w wyniku czego funkcja zwróci wartość INVALID_FILE_ATTRIBUTES = -1), a właściwość Err.LastDllError zwróci nam systemowy kod błędu wywołania biblioteki DLL.

Option Compare Database
Option Explicit

#If VBA7 Then
  Private Declare PtrSafe Function GetFileAttributes Lib "kernel32" _
          Alias "GetFileAttributesA" _
          (ByVal lpFileName As String) As Long
#Else
  Private Declare Function GetFileAttributes Lib "kernel32" _
          Alias "GetFileAttributesA" _
          (ByVal lpFileName As String) As Long
#End If


' przykładowe wywołania
Public Function MojTest()
Dim lAttrib As Long

   ' próba pobrania atrybutów pliku, który nie istnieje
   lAttrib = GetFileAttributes("C:\NotExistingFile")
   If lAttrib = INVALID_FILE_ATTRIBUTES Then Debug.Print "A - Err = "; Err.LastDllError
   
   ' próba pobrania atrybutów pliku w nieistniejącej lokalizacji (brak stacji dysków B:\)
   lAttrib = GetFileAttributes("B:\MojPlik.txt")
   If lAttrib = INVALID_FILE_ATTRIBUTES Then Debug.Print "B - Err = "; Err.LastDllError
    
   ' próba pobrania atrybutów pliku z błędnie wpisanej lokalizacji
   lAttrib = GetFileAttributes(":MojPlik.txt")
   If lAttrib = INVALID_FILE_ATTRIBUTES Then Debug.Print "C - Err = "; Err.LastDllError
     
   ' próba pobrania atrybutów pliku z pustego dysku DVD (CD)
   lAttrib = GetFileAttributes("Z:\NotExistingFile")
   If lAttrib = INVALID_FILE_ATTRIBUTES Then Debug.Print "D - Err = "; Err.LastDllError
      
   ' próba pobrania atrybutów pliku z pustej stacji DVD (CD)
   lAttrib = GetFileAttributes("Z:\MojPlik.txt")
   If lAttrib = INVALID_FILE_ATTRIBUTES Then Debug.Print "E - Err = "; Err.LastDllError
      
    DoCmd.RunCommand acCmdDebugWindow
    
End Function

Po wywołaniu przykładowej funkcji MojTest() w oknie „Immediate” możemy zobaczyć wyniki:

Nie wiem jak innym, ale mi cyferki (liczby) typu: 2, 3, 123, 1, 21 nic nie mówią. Tym bardziej użytkownikowi, który zobaczy poniższy komunikat:

Aby dowiedzieć się czegoś więcej o przyczynie błędu wywołania funkcji DLL, wykorzystamy funkcję API:
• FormatMessage(...), która formatuje tekst komunikatu związanego z kodem błędu wywołania biblioteki DLL i zapisuje sformatowany komunikat do bufora wyjściowego. Zwraca ilość znaków zapisanych w buforze wyjściowym, (nie uwzględnienia znaku końca ciągu vbNullChar).

Option Compare Database
Option Explicit

' • Function LastDllErrorDescr(lNoLastError As Long,[sFunctionName As String = ""]) As String
' --------------------------------------------------------------------------------------
' autor: Zbigniew Bratko - 03.2016
' Zwraca sformatowany tekst komunikatu związany z kodem błędu wywołania biblioteki DLL
' [lNoLastError] - numer błędu wywołania funkcji DLL, odpowiadający właściwości Err.LastDllError
' [sFunctionName] - opcjonalny argument odnoszący się do funkcji w której wystąpił błąd
' [OUT] - Przy powodzeniu zwraca tekst komunikatu związany z kodem błędu wywołania biblioteki DLL,
'         przy niepowodzeniu (braku opisu błędu) zwraca informację: "Nie znaleziono opisu błędu."
'

#If VBA7 Then
  Private Declare PtrSafe Function FormatMessage Lib "kernel32" _
          Alias "FormatMessageA" _
          (ByVal dwFlags As Long, _
          lpSource As Any, _
          ByVal dwMessageId As Long, _
          ByVal dwLanguageId As Long, _
          ByVal lpBuffer As String, _
          ByVal nSize As Long, _
          Arguments As LongPtr) As Long
#Else
  Private Declare Function FormatMessage Lib "kernel32" _
          Alias "FormatMessageA" _
          (ByVal dwFlags As Long, _
          lpSource As Any, _
          ByVal dwMessageId As Long, _
          ByVal dwLanguageId As Long, _
          ByVal lpBuffer As String, _
          ByVal nSize As Long, _
          Arguments As Long) As Long
#End If

Private Const FORMAT_MESSAGE_FROM_SYSTEM = &H1000
Private Const INVALID_FILE_ATTRIBUTES = -1
Private Const cDescrNotExist As String = "Nie znaleziono opisu błędu."


' Funkcja własna LastDllErrorDescr
Public Function LastDllErrorDescr(lNoLastError As Long, _
                        Optional sFunctionName As String = "") As String
Dim sBufferMessage As String
Dim lLenMessage As Long
Const cBufferSize As Long = 256
  
  ' przygotuj bufor na przyjęcie komunikatu o błędzie
  sBufferMessage = String(cBufferSize, vbNullChar)
  ' zapisz tekst komunikatu do bufora sBufferMessage
  lLenMessage = FormatMessage(ByVal FORMAT_MESSAGE_FROM_SYSTEM, _
                      0&, _
                      ByVal lNoLastError, _
                      0&, _
                      ByVal sBufferMessage, _
                      ByVal cBufferSize, _
                      0)
  
  ' nie znaleziono opisu błędu
  If lLenMessage = 0 Then
    ' przygotuj własny komunikat
    sBufferMessage = cDescrNotExist
    lLenMessage = Len(sBufferMessage)
  End If
  
  ' pobierz tekst komunikatu z bufora
  If Len(sFunctionName) = 0 Then
    LastDllErrorDescr = Left$(sBufferMessage, lLenMessage)
  Else
    LastDllErrorDescr = "Błąd nr " & lNoLastError & vbNewLine & _
                        "Funkcja " & sFunctionName & vbNewLine & _
                        "Opis błędu: " & vbNewLine & _
                        Left$(sBufferMessage, lLenMessage)
  End If

End Function

' przykładowe wywołania
Public Function MojTestBis()
Dim lAttrib As Long

  ' próba pobrania atrybutów pliku, który nie istnieje
  lAttrib = GetFileAttributes("C:\NotExistingFile")
  If lAttrib = INVALID_FILE_ATTRIBUTES Then
    Debug.Print "A - Err = "; Err.LastDllError & "  " & LastDllErrorDescr(Err.LastDllError)
  End If
  
  ' próba pobrania atrybutów pliku w nieistniejącej lokalizacji (brak stacji dysków B:\)
  lAttrib = GetFileAttributes("B:\MojPlik.txt")
  If lAttrib = INVALID_FILE_ATTRIBUTES Then
    Debug.Print "B - Err = "; Err.LastDllError & "  " & LastDllErrorDescr(Err.LastDllError)
  End If
  
  ' próba pobrania atrybutów pliku z błędnie wpisanej lokalizacji
  lAttrib = GetFileAttributes(":MojPlik.txt")
  If lAttrib = INVALID_FILE_ATTRIBUTES Then
    Debug.Print "C - Err = "; Err.LastDllError & "  " & LastDllErrorDescr(Err.LastDllError)
  End If
  
  ' próba pobrania atrybutów pliku z pustego dysku DVD (CD)
  lAttrib = GetFileAttributes("Z:\NotExistingFile")
  If lAttrib = INVALID_FILE_ATTRIBUTES Then
    Debug.Print "D - Err = "; Err.LastDllError & "  " & LastDllErrorDescr(Err.LastDllError)
  End If
  
  ' próba pobrania atrybutów pliku z pustej stacji DVD (CD)
  lAttrib = GetFileAttributes("Z:\MojPlik.txt")
  If lAttrib = INVALID_FILE_ATTRIBUTES Then
    Debug.Print "E - Err = "; Err.LastDllError & "  " & LastDllErrorDescr(Err.LastDllError)
  End If
  
  DoCmd.RunCommand acCmdDebugWindow
  
End Function

Po wywołaniu przykładowej funkcji MojTestBis() w oknie „Immediate” możemy zobaczyć wyniki:

Uwaga: Właściwość Err.LastDllError odnosi się tylko do wywołania funkcji z zewnętrznych bibliotek DLL z kodu programu pisanego w języku VBA. Tak wywoływana funkcja zwykle zwraca kod oznaczający, czy wywoływana funkcja zakończyła się sukcesem, czy też niepowodzeniem i ustawiana jest właściwość Err.LastDllError. Właściwość Err.LastDllError powinna być sprawdzana po każdym niepowodzeniu wywoływanej funkcji biblioteki DLL. Jeśli właściwość Err.LastDllError została ustawiona,  nie jest zgłaszany żaden wyjątek .

wtorek, 16 lutego 2016

• Funkcje API - tytuły okien otwartych instancji MS Access

W poprzednim artykule •  Funkcje API - ile uruchomiono instancji MS Access • dowiedzieliśmy się jak wykorzystać interfejs API, aby odczytać ile instancji MS Access jest otwartych. Często informacja typu: „Otwarto 2 instancje MS Access” jest niewystarczająca. Oprócz ilości otwartych instancji MS Access, potrzebna jest informacja jakie instancje MS Access są otwarte. Odpowiedź na to pytanie nie jest łatwa (przynajmniej dla mnie). Jedynie co mogę zrobić, to określić ilość otwartych instancji MS Access i pobrać tytuły (nazwy) widniejące na pasku tytułowym otwartych okien MS Access.

Do tego celu wykorzystam interfejs Windows API i jego funkcję:
• FindWindowEx - zwracającą uchwyt okna potomnego, którego albo nazwa klasy, albo (i) tytuł odpowiadają nazwom przekazanym w argumentach funkcji. Z artykułu • Funkcje API - jak pobrać nazwę klasy okna • wiemy, że okno aplikacji MS Access jest klasy OMain, a jego „rodzicem” (właścicielem) jest główne okno Windows, czyli Pulpit (ang. Desktop). Pobranie uchwytu Pulpitu umożliwia funkcja API • GetDesktopWindow. W celu znalezienia pierwszego okna aplikacji MS Access powinniśmy wywołać funkcję • FindWindowEx następująco:

hDesktop = GetDesktopWindow
hwndNext = FindWindowEx(ByVal hDesktop, ByVal 0, ByVal "OMain", ByVal vbNullString)
' następne (ewentualne) instancje MS Access znajdziemy po przekazaniu do funkcji FindWindowEx znaleziony uchwyt okna hwndNext o ile jest różny od 0 (zera)
hwndNext = FindWindowEx(ByVal hDesktop, ByVal hwndNext, ByVal "OMain", ByVal vbNullString)

Wiedząc jak znaleźć okna aplikacji MS Access, zostaje nam tylko pobranie tytułu okna.W tym celu skorzystam z własnej funkcji GetTextWindow(...) zwracającą tekst (tytuł) okna o przekazanym w argumencie uchwycie hwnd. Funkcja ta korzysta z dwóch innych funkcji API:
• GetWindowTextLength - zwracającą długość tekstu okna o przekazanym w argumencie uchwycie hwnd
• GetWindowText - zwracającą długość pobranego do buforu tekstu okna, a w buforze właściwy tekst okna o uchwycie hwnd. Funkcję własną  GetTextWindow(...) przedstawiłem w artykule: • Funkcje API - jak pobrać tytuł (tekst) okna •.

Wyszukiwanie okien aplikacji MS Access jest szczególnym przypadkiem bardziej ogólnego zagadnienia, jakim jest znalezienie wszystkich okien „potomnych” (dzieci) okna nadrzędnego o uchwycie hParent i klasie ClassName. Aby napisać bardziej uniwersalną funkcję, będę potrzebował drugiej funkcji własnej GetWinClassName(...) z wcześniej wzmiankowanego artykułu • Funkcje API - jak pobrać nazwę klasy okna •, by znając uchwyt okna, pobrać nazwę klasy w celu przekazania do funkcji •FindWindowEx.

Option Compare Database
Option Explicit

 ' • Function EnumWindowInstances(ByVal hParent As LongPtr, _
 '                                ByVal sWindowClassName As String, _
 '                                ByVal sWindowTitle As String, _
 '                                ByRef arrRet() As String) As Long
' ----------------------------------------------------------------------
' autor: Zbigniew Bratko - 01.2016

' [hParent] - uchwyt okna nadrzędnego (rodzica), które będzie przeszukiwane w celu
'             znalezienia okien potomnych (dzieci) spełniających kryteria przekazane
'             w argumentach,
' [sWindowClassName] - nazwa szukanej klasy okna, dla ciągu zerowej długości lub
'                      znaku vbNullString wyliczane są wszystkie okna o tytule sWindowTitle,
' [sWindowTitle] - tytuł szukanego okna, dla ciągu zerowej długości lub znaku vbNullString
'                  wyliczane są wszystkie okna klasy sClassName,
' [arrRet] - zwracana ByRef tablica z tytułami (tekstem) okien spełniających kryteria
'            przekazane w argumentach,
' [Out] - zwraca liczbę znalezionych okien, spełniających kryteria przekazane  w argumentach,
'         a w argumencie ByRef arrRet zwraca tablicę z tytułami okien spełniających
'         Przy niepowodzeniu zwraca 0 (Zero), a w argumencie ByRef arrRet niezainicjowaną tablicę,
' UWAGA! - Jeżli argumenty [sWindowClassName] i [sWindowTitle] są ciągiem zerowej długości
'          lub równe vbNullString zwrócone zostaną wszystkie okna potomne (dzieci) okna
'          nadrzędnego o uchwycie [hParent]
'

#If VBA7 Then
  Private Declare PtrSafe Function FindWindowEx Lib "user32" _
          Alias "FindWindowExA" _
          (ByVal hWnd1 As LongPtr, _
          ByVal hWnd2 As LongPtr, _
          ByVal lpsz1 As String, _
          ByVal lpsz2 As String) As LongPtr
  Private Declare PtrSafe Function GetDesktopWindow Lib "user32" () As LongPtr
#Else
  Private Declare Function FindWindowEx Lib "user32" _
          Alias "FindWindowExA" _
          (ByVal hWnd1 As Long, _
          ByVal hWnd2 As Long, _
          ByVal lpsz1 As String, _
          ByVal lpsz2 As String) As Long
  Private Declare Function GetDesktopWindow Lib "user32" () As Long
#End If

' Funkcja własna EnumWindowInstances
#If VBA7 Then
  ' Deklaracja funkcji - zarówno w 32 jak i 64 bitowym środowisku VBA 7
  Public Function EnumWindowInstances(ByVal hParent As LongPtr, _
                                 ByVal sWindowClassName As String, _
                                 ByVal sWindowTitle As String, _
                                 ByRef arrRet() As String) As Long
    Dim hNext As LongPtr
#Else
  ' Deklaracja funkcji - tylko 32 bitowe środowisko VBA 6
  Public Function EnumWindowInstances(hParent As Long, _
                                 ByVal sWindowClassName As String, _
                                 ByVal sWindowTitle As String, _
                                 ByRef arrRet() As String) As Long
    Dim hNext As Long
#End If

Dim lCount As Long
 
 ' konwertuj ciąg zerowej długości "" na vbNullString,
 ' gdyż ciąg zerowej długości "", to nie to samo co vbNullString
 If Len(sWindowTitle) = 0 Then sWindowTitle = vbNullString
 If Len(sWindowClassName) = 0 Then sWindowClassName = vbNullString
  
  ' szukaj pierwszego wystąpienia okna spełniającego kryteria
  hNext = FindWindowEx(hParent, 0&, ByVal sWindowClassName, ByVal sWindowTitle)
  
  Do Until hNext = 0
    ' zwiększ wymiar tablicy
    ReDim Preserve arrRet(0 To lCount)
    ' zapisz do tablicy tytuł okna
    arrRet(lCount) = GetTextWindow(hNext)
    ' zwiększ licznik
    lCount = lCount + 1
    ' szukaj następnego wystąpienia okna spełniającego kryteria
    hNext = FindWindowEx(ByVal hParent, ByVal hNext, ByVal sWindowClassName, ByVal sWindowTitle)
  Loop
  
  EnumWindowInstances = lCount

End Function

' przykładowe wywołanie
Private Sub btnTest_Click()
#If VBA7 Then
  Dim hParent     As LongPtr
#Else
  Dim hParent     As Long
#End If

Dim sClassName  As String
Dim sArray()    As String
Dim lRet        As Long
Dim sWinTitle   As String
Dim i           As Integer
  
  ' pobierz nazwę klasy okna - (tutaj okno  MS Access)
  sClassName = GetWinClassName(Application.hWndAccessApp)
  
  ' pobierz uchwyt okna rodzica
  hParent = GetDesktopWindow
  
  ' pobierz ilość wystąpień okna klasy sClassName
  ' a do tablicy sArray() tytuły (tekst) okien
  lRet = EnumWindowInstances(hParent, sClassName, "", sArray())
  
  If lRet = 0 Then
    MsgBox "Nie znaleziono okien spełniających kryteria !"
  Else
    For i = 0 To lRet - 1
      ' utwórz listę tytułów (tekstu) znalezionych okien
      sWinTitle = sWinTitle & CStr(i + 1) & ". '" & CStr(sArray(i)) & "'" & vbNewLine
    Next
  End If
  
  MsgBox "Znaleziono okna (szt. " & CStr(lRet) & ") o tytułach:" & _
          vbNewLine & vbNewLine & sWinTitle
     
End Sub

Po testowym uruchomieniu 4 różnych baz (Access 2007, Access 2010, Access 2016) i biężącej, testowej bazy Access 2016 z poziomu której uruchomiona została procedura Sub btnTest_Click wywołująca funkcję własną EnumWindowInstances(...) wyświetlone zostanie okno komunikatu informujące o uruchomionych instancjach MS Access:

Po małej modyfikacji procedury Sub btnTest_Click, tak by wyszukała wszystkie okna „potomne” (dzieci) okna głównego „Pulpitu”, a w oknie „Immediate” wylistowała okna, które mają tytuł (tekstu):


 ' przykładowe wywołanie
Private Sub btnTest_Click()
#If VBA7 Then
  Dim hParent     As LongPtr
#Else
  Dim hParent     As Long
#End If

Dim sClassName  As String
Dim sArray()    As String
Dim lRet        As Long
Dim sWinTitle   As String
Dim i           As Integer
  
  ' pobierz uchwyt okna rodzica
  hParent = GetDesktopWindow
  
  ' pobierz ilość wystąpień wszystkich okien potomnych
  ' a do tablicy sArray() tytuły (tekst) okien
  lRet = EnumWindowInstances(hParent, "", "", sArray())
  
  If lRet = 0 Then
    MsgBox "Nie znaleziono okien spełniających kryteria !"
  Else
    For i = 0 To lRet - 1
      If Len(sArray(i)) > 0 Then
        ' w oknie Immediate listuj okna mające tytuł
        Debug.Print CStr(i + 1) & ". " & sArray(i)
      End If
      ' utwórz listę tytułów (tekstu) znalezionych okien
      sWinTitle = sWinTitle & CStr(i + 1) & ". '" & CStr(sArray(i)) & "'" & vbNewLine
    Next
  End If
  
  MsgBox "Znaleziono okna (szt. " & CStr(lRet) & ") o tytułach:" & _
          vbNewLine & vbNewLine & sWinTitle, vbInformation, "API - lista tytułów okien"
End Sub
          

     
'  Lista okien, których tytuł (tekst) nie jest pusty 
'  -------------------------------------------------
'  10. Start
'  29. Network Flyout
'  30. Default IME
'  31. Przełączanie zadań
'  32. CiceroUIWndFrame
'  33. MSCTFIME UI
'  34. Default IME
'  37. CiceroUIWndFrame
'  38. TF_FloatingLangBar_WndTitle
'  39. Wyszukaj
'  40. MSCTFIME UI
'  41. Default IME
'  42. Properties
'  43. API - Lista tytułów okien
'  48. Toolbar Options
'  49. Debug
'  53. Microsoft Visual Basic for Applications - Instancje_MS_Access [running] - [Immediate]
'  54. MSCTFIME UI
'  55. Default IME
'  56. art_09 - Google Chrome
'  57. MSCTFIME UI
'  58. Default IME
'  60. Szukaj
'  94. DDE Server Window
'  95. OfficePowerManagerWindow
' 103. MSCTFIME UI
' 104. Default IME
' 105. COMODO Internet Security Premium 
' 106. CCisEventsHandlerWindow_2604_0
' 107. Default IME
' 108. GDI+ Window
' 109. Default IME
' 117. Menu Start
' 118. MS_WebcheckMonitor
' 119. MCI command handling window
' 120. Default IME
' 121. BluetoothNotificationAreaIconWindowClass
' 127. HiddenFaxWindow
' 128. Media Center SSO
' 129. Default IME
' 132. Default IME
' 133. MSCTFIME UI
' 134. REALTEK 11n USB Wireless LAN Utility
' 135. REALTEK 11n USB Wireless LAN Utility
' 136. GDI+ Window
' 137. Default IME
' 139. Default IME
' 140. Miernik baterii
' 141. Default IME
' 143. Piriform CCleaner
' 144. GDI+ Window
' 145. Default IME
' 146. USB 3.0 Monitor
' 147. Default IME
' 150. MSCTFIME UI
' 152. Lista szybkiego dostępu
' 158. DDE Server Window
' 159. GDI+ Window
' 160. Default IME
' 161. TaskEng - proces aparatu Harmonogramu zadań
' 162. Default IME
' 165. Default IME
' 166. MCI command handling window
' 167. Default IME
' 168. Task Host Window
' 169. Default IME
' 170. AMD EEU Client
' 171. Default IME
' 172. Default IME
' 174. DWM Notification Window
' 175. Default IME
' 176. CisTray
' 177. Program Manager

W sumie listing obejmuje 172 okna różnego typu. Ilość okien potomnych Pulpitu zależy od konfiguracji Systemu, ilości uruchomionych aplikacji bezpośrednio, jak również różnych usług uruchomionych w tle.
Muszę wyraźnie zaznaczyć, że funkcja własna EnumWindowInstances(...) uwzględnia tylko okna „potomne” (dzieci) okna nadrzędnego o uchwycie hParent. Aby wylistować następne pokolenie okien „potomnych” konieczna jest modyfikacja funkcji własnej EnumWindowInstances(...). Najprościej by było zrobić wywołanie rekurencyjne, ale na razie poprzestanę na powyższym przykładzie. Być może kiedyś ...

środa, 10 lutego 2016

• Funkcje API - ile uruchomiono instancji MS Access

W poprzednim artykule • Funkcje API - jak pobrać nazwę klasy okna o uchwycie hwnd • dowiedzieliśmy się jak wykorzystać interfejs API, aby odczytać nazwę klasy okna. Po uruchomieniu przykładowego kodu wiemy, że nazwa klasy głównego okna MS Access brzmi OMain, a okno formularza jest klasy OForm.

Czasami w trakcie działania aplikacji MS Access potrzebujemy sprawdzić, czy otwarta jest inna baza MS Access, a jeżeli tak, to ile instancji MS Access jest uruchomionych. Do tego celu wykorzystamy interfejs Windows API i jego dwie funkcje:
• GetDesktopWindow - zwracającą uchwyt pulpitu czyli okna obejmującego cały ekran. Na pulpicie osadzone są wszystkie pozostałe okna
• FindWindowEx - zwracającą uchwyt okna potomnego, którego albo nazwa klasy, lub (i) tytuł odpowiadają nazwom przekazanym w argumentach funkcji.

Option Compare Database
Option Explicit
 
' • Function CountAccessInstances( _
'                  Optional ByVal sAccessTitle As String = vbNullString) As Long
' --------------------------------------------------------------------------------
' autor: Zbigniew Bratko - 02.2016

' [sAccessTitle] - argument opcjonalny, tytuł szukanego okna,
'                  dla ciągu zerowej długości lub znaku vbNullString
'                  wyliczane są wszystkie okna MS Access,
' [Out] - zwraca liczbę uruchomionych instancji MS Access,
'         o tytule odpowiadającym argumentowi sAccessTitle
'         Przy niepowodzeniu zwraca 0 (Zero)
'
 
#If VBA7 Then
  Private Declare PtrSafe Function FindWindowEx Lib "user32" _
          Alias "FindWindowExA" _
          (ByVal hWnd1 As LongPtr, _
          ByVal hWnd2 As LongPtr, _
          ByVal lpsz1 As String, _
          ByVal lpsz2 As String) As LongPtr
#Else
  Private Declare Function FindWindowEx Lib "user32" _
          Alias "FindWindowExA" _
          (ByVal hWnd1 As Long, _
          ByVal hWnd2 As Long, _
          ByVal lpsz1 As String, _
          ByVal lpsz2 As String) As Long
#End If


' Funkcja własna CountAccessInstances
Public Function CountAccessInstances( _
                      Optional ByVal sAccessTitle As String = vbNullString) As Long
#If VBA7 Then
  Dim hNext As LongPtr
#Else
  Dim hNext As Long
#End If
  
' licznik
Dim lCount As Long
' nazwa klasy okna MS Access
Const cAccWindowClass As String = "OMain"

  ' konwertuj ciąg zerowej długości "" na vbNullString,
  ' gdyż ciąg zerowej długości "", to nie to samo co vbNullString
  If Len(sAccessTitle) = 0 Then sAccessTitle = vbNullString
  
  ' szukaj pierwszego wystąpienia potomnego okna MS Access klasy "OMain"
  ' i tytule sDbTitle, w oknie rodzica "Pulpit" (domyślnie dla argumentu hWnd1 = 0)
  hNext = FindWindowEx(0, 0, ByVal cAccWindowClass, ByVal sAccessTitle)
    
  Do Until hNext = 0
    ' zwiększ licznik
    lCount = lCount + 1
    ' szukaj następnego wystąpienia okna MS Access klasy "OMain"
    hNext = FindWindowEx(0, ByVal hNext, ByVal cAccWindowClass, ByVal sAccessTitle)
  Loop

  CountAccessInstances = lCount
  
End Function


' przykładowe wywołanie
Private Sub btnTest_Click()
Dim lRet As Long
  
  lRet = CountAccessInstances()
  MsgBox "Uruchomionych instancji MS Access: " & CStr(lRet) & " egz."
 
End Sub

piątek, 5 lutego 2016

• Funkcje API - jak pobrać nazwę klasy okna

W poprzednim artykule • Funkcje API - jak pobrać tytuł (tekst) okna o uchwycie hwnd • dowiedzieliśmy się jak wykorzystać interfejs API, aby odczytać tytuł (tekst) okna. Czasami w trakcie działania aplikacji MS Access potrzebujemy sprawdzić nie tylko jaki tytuł (tekst) ma okno, ale także nazwę klasy okna w systemie Windows.

Klasa okna jest to struktura opisująca podstawowe cechy okna: kolor tła, szerokość obramowania, menu sterowania, przyciski minimalizuj, maksymalizuj, zamknij, ikona paska tytułowego, paski przewijania, przypisany do okna kursor myszy i wiele innych cech okna.

• Odczyt nazwy klasy okna o uchwycie hwnd.

MS Access nie posiada żadnych metod i właściwości pozwalających na odczytanie nazwy klasy okna. Aby pobrać nazwę klasy okna musimy skorzystać z interfejsu API i jednej z jego funkcji:
• GetClassName - zwracającą długość pobranej do buforu nazwy klasy okna, a w buforze nazwę klasy okna o uchwycie hwnd.

Option Compare Database
Option Explicit

' • Function GetWinClassName(hWind As Long;[LongLong];[LngPtr]) As String
' --------------------------------------------------------------
' autor: Zbigniew Bratko - 01.2016
' [hWind] - uchwyt okna, którego nazwa klasy będzie pobierana
'           w środowisku 32 bitowym liczba typu Long
'           w środowisku 64 bitowym liczba typy LongLong
'           w środowisku VBA7 może być typu LongPtr
' [Out] - zwraca nazwę klasy okna
' Przy niepowodzeniu zwraca ciąg zerowej długości ""
'

#If VBA7 Then
  Private Declare PtrSafe Function GetClassName Lib "user32" _
          Alias "GetClassNameA" _
          (ByVal hwnd As LongPtr, _
          ByVal lpClassName As String, _
          ByVal nMaxCount As Long) As Long
#Else
  Private Declare Function GetClassName Lib "user32" _
          Alias "GetClassNameA" _
          (ByVal hwnd As Long, _
          ByVal lpClassName As String, _
          ByVal nMaxCount As Long) As Long
#End If

' Funkcja własna GetWinClassName
#If VBA7 Then
  ' Deklaracja funkcji - zarówno w 32 jak i 64 bitowym środowisku VBA 7
  Public Function GetWinClassName(hWind As LongPtr) As String
#Else
  ' Deklaracja funkcji - tylko 32 bitowe środowisko VBA 6
  Public Function GetWinClassName(hWind As Long) As String
#End If
  
Dim sBuffer           As String
Dim lLenText          As Long
Const cMaxSizeBuffer  As Long = 256

   ' przygotuj bufor na przyjęcie tekstu
   sBuffer = String(cMaxSizeBuffer, vbNullChar)
   ' pobierz długość zwróconego tekstu
   lLenText = GetClassName(hWind, sBuffer, cMaxSizeBuffer)
   ' utnij nadmiarowy ciąg znaków vbNullChar w buforze
   GetWinClassName = Left$(sBuffer, lLenText)
  
End Function

' przykładowe wywołanie:
Private Sub btnTest_Click()
 
  ' pobierz nazwę klasu okna formularza i okna aplikacji MS Access
  MsgBox "Nazwa klasy okna formularza: " & _
         GetWinClassName(Me.hwnd) & vbNewLine & _
        "Nazwa klasy okna MS Access: " & _
         GetWinClassName(Application.hWndAccessApp)
End Sub

Po uruchomieniu przykładowego wywołania, MS Access wyświetla okno komunikatu:

• OMain - jest to nazwa klasy okna MS Access.
• OForm - jest to nazwa klasy okna formularza.

Obie te nazwy klas okien warto zapamiętać, gdyż przydadzą się na następnych stronach, na których będziemy korzystali z funkcji API, by odwoływać się do okien MS Access.

poniedziałek, 1 lutego 2016

• Funkcje API - jak pobrać tytuł (tekst) okna

Czasami w trakcie działania aplikacji MS Access potrzebujemy sprawdzić jaki tytuł (lub tekst) ma okno w systemie Windows. MS Access posiada właściwości pozwalające na odczyt tytułów niektórych okien. Jest to właściwość obiekt.Caption obiektu formularz i obiektu raport. Właściwości obiekt.Caption nie należy mylić z właściwością obiekt.Name, gdyż ta ostatnia zwraca nazwę obiektu, która została podana w trakcie zapisywania projektu. Nazwa ta jest widoczna w „Oknie nawigacji”, dawniej zwanym „Oknem bazy danych”.

Warto pamiętać, że jeżeli w oknie „Arkusz właściwości”, w trybie „Projektowanie”, nie nadamy właściwości Tytuł (Caption) jakiejkowiek wartości (pozostawimy to pole puste), to otwarty formularz (raport) będzie miał tytuł odpowiadający jego nazwie, a właściwość obiekt.Caption będzie ciągiem zerowej długości "". Jak widać, okno formularza (raportu) może mieć tytuł odpowiadający nazwie obiektu obiekt.Name, lub jego tytułowi obiekt.Caption (jeżeli takowa właściwość została ustawiona w oknie „Projektowanie”)

Zagadnienie pobrania tytułu okna jest bardziej skomplikowane, w przypadku pobierania tytułu okna Aplikacji (napis na pasku tytułowym okna MS Access). Teoretycznie sprawa jest niby prosta. Tytuł okna bazy danych przechowywany jest we właściwości CurrentDb.Properties("AppTitle"), ale w przypadku próby pobrania tej właściwości, gdy nie jest ona ustawiona w oknie „Opcje programu Access/Bieżąca baza danych/Tytuł aplikacji”, program MS Access wygeneruje błąd Error.Number = 3270 i wyświetli okno komunikatu o treści:

Aby nasz kod działał prawidłowo, musimy obsłużyć błąd nr 3270, generowany przy próbie odczytu nieistniejącej właściwości "AppTitle", by wyświetlić odpowiednią informację, że nasza aplikacja nie ma ustawionego tytułu.


    MsgBox Forms("NazwaFormularza").Caption '(formularz musi być otwarty)
    ' lub
    MsgBox Me.Caption '(dla bieżącego formularza)
    ' podobnie dla raportu
    MsgBox Reports("NazwaRaportu").Caption '(raport musi być otwarty)
    ' lub
    MsgBox Me.Caption '(dla bieżącego raportu)

 
 ' Okno MS Access 
  ' dla okna bieżącej aplikacji musimy obsłużyć błąd nr 3270,
  ' generowany przy próbie odczytu nieistniejącej właściwości "AppTitle":
  Private Const conPropNotFoundError = 3270
  
  On Error Resume Next
    MsgBox CurrentDb.Properties("AppTitle") ' właściwość musi być ustawiona
    If Err.Number = conPropNotFoundError Then
      MsgBox "Bieżąca Aplikacja:" & vbNewLine & _
            CurrentDb.Name & vbNewLine & _
            "nie posiada tytułu."
      Err.Clear
    End If
  On Error GoTo 0
 

Ale nie zawsze jest tak prosto. Czasami potrzebujemy pobrać tekst (tytuł) okna aplikacji, która nie udostępnia metody pozwalajacej na pobranie tekstu okna. Wtedy z pomocą przychodzi interfejs Windows API i jego dwie funkcje:
• GetWindowTextLength - zwracającą długość tekstu okna o przekazanym w argumencie uchwycie hwnd
• GetWindowText - zwracającą długość pobranego do buforu tekstu okna, a w buforze właściwy tekst okna o uchwycie hwnd.

Option Compare Database
Option Explicit

' • Function GetTextWindow(hWind As Long;[LongLong];[LngPtr]) As String
' --------------------------------------------------------------
' autor: Zbigniew Bratko - 01.2016
' [hWind] - uchwyt okna, którego tytuł (tekst) będzie pobierany
'           w środowisku 32 bitowym liczba typu Long
'           w środowisku 64 bitowym liczba typy LongLong
'           w środowisku VBA7 może być typu LongPtr
' [Out] - zwraca tytuł (tekst) okna
' Przy niepowodzeniu zwraca ciąg zerowej długości ""
'

#If VBA7 Then
  ' Środowisko VBA 7 - zarówno 32 jak i 64 bitowe
  Private Declare PtrSafe Function GetWindowText Lib "user32" _
          Alias "GetWindowTextA" _
          (ByVal hwnd As LongPtr, _
          ByVal lpString As String, _
          ByVal cch As Long) As Long
  Private Declare PtrSafe Function GetWindowTextLength Lib "user32" _
           Alias "GetWindowTextLengthA" ( _
           ByVal hwnd As LongPtr) As Long
#Else
  ' Środowisko VBA 6 - tylko 32 bitowe
  Private Declare Function GetWindowText Lib "user32" _
          Alias "GetWindowTextA" _
          (ByVal hwnd As Long, _
          ByVal lpString As String, _
          ByVal cch As Long) As Long
  Private Declare Function GetWindowTextLength Lib "user32" _
          Alias "GetWindowTextLengthA" ( _
          ByVal hwnd As Long) As Long
#End If

' Funkcja własna GetTextWindow
#If VBA7 Then
  ' Deklaracja funkcji - zarówno w 32 jak i 64 bitowym środowisku VBA 7
  Public Function GetTextWindow(hWind As LongPtr) As String
#Else
  ' Deklaracja funkcji - tylko 32 bitowe środowisko VBA 6
  Public Function GetTextWindow(hWind As Long) As String
#End If
 
Dim sBuffer    As String
Dim lLenText   As Long
 
   ' pobierz długość tekstu okna
   lLenText = GetWindowTextLength(hWind)
   ' uwzględnij dodawany znak końca ciągu znaków
   lLenText = lLenText + 1
   ' przygotuj buffor na przyjęcie tekstu
   sBuffer = String(lLenText, vbNullChar)
   ' pobierz długość zwróconego tekstu
   lLenText = GetWindowText(hWind, sBuffer, lLenText)
   ' utnij nadmiarowy ciąg znaków vbNullChar w buforze
   GetTextWindow = Left$(sBuffer, lLenText)
 
End Function

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

  ' pobierz tytuł okna bieżącej aplikacji MS Access
  MsgBox GetTextWindow(Application.hWndAccessApp)

Exit 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 LongPtr i LongLong.
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

piątek, 25 grudnia 2015

• VBA 7.0 i MS Access 2010+

Na stronie Alexa Gorbatcheva pod adresem http://alexgorbatchev.com/SyntaxHighlighter/ znalazłem skrypt służący do podświetlania (kolorowania) elementów składniowych kodu źródłowego programu. Na swoim blogu Kevin Junghans w artykule Adding SyntaxHighlighter to Blogger Dynamic Views przedstawił, jak zastosować SyntaxHighlighter na swoim blogu. Początkowo skorzystałem z rad Kevina, ale po pewnym czasie opracowałem swój sposób na zastosowanie skryptu służącego do kolorowania składni. Ale o tym będzie później. Na razie przejdę do właściwego tematu, jakim jest 64-bitowy Access, a raczej 64-bitowe VBA 7.0.

Microsoft Office 2010+ jest obecnie dostępny w wersji 32 i 64-bitowej. Wersja 64 bitowa umożliwia pracę ze znacznie większymi zbiorami danych. Możliwość ta jest szczególnie ważna podczas pracy z dużą ilością danych w programie Microsoft Excel 2010+. Wraz z wprowadzeniem nowej, 64 bitowej, wersji pakietu Microsoft Office 2010+, wprowadzono nową wersję Visual Basic for Applications (VBA) pod nazwą VBA 7.0, która przeznaczona jest do pracy zarówno w 32-bitowych i 64-bitowych aplikacjach.
Niestety, jedynie wersja 32 bitowa pakietu Office 2010+ pozwala nadal używać 32 bitowe dodatki dla pakietu Office oraz korzystanie ze „starego” kodu bez konieczności jakichkolwiek modyfikacji w kodzie VBA. Wszystkie dodatkowe wtyczki dla Office będą działały jedynie w 32 bitowej wersji, gdyż nie są one zgodne z 64 bitowym Officem. Próba instalacji jakiegokolwiek dodatku (pluginu) zakończy się niepowodzeniem. Jeśli więc zdecydujmy się na zainstalowanie 64 bitowego Office, to musimy się liczyć z pewnymi ograniczeniami funkcjonalności 64 bitowego Office w porównaniu do jego 32 bitowej wersji.

Nowości i zmiany w VBA 7.0

• VBA7 - stała kompilacji warunkowej
Umożliwia sprawdzenie, czy uruchomiona aplikacja używa VBA 7.0 (64 bitowego lub 32 bitowego), czy poprzedniej 32 bitowej wersji VBA (6.0). Zwracana wartość VBA7 = TRUE informuje, że uruchomiona została aplikacja pakietu Office w wersji 2010 lub nowszej.
• Win64 - stała kompilacji warunkowej
Pozwala stwierdzić, czy uruchomiona została 32 bitowa, czy 64 bitowa wersja pakietu Microsoft Office.
 
Public Sub testVBA()
 ' Sprawdź wersję VBA
 #If VBA7 Then
   ' środowisko VBA 7 (wersja 32 lub 64 bitowa MS Office +2010+)
   #If Win64 Then
      ' 64-bitowe środowisko VBA 7.
      MsgBox "Kod VBA 7 uruchomiony w 64-bitowej wersji MS Office 2010+"
   #Else
      ' 32-bitowe środowisko VBA 7.
      MsgBox "Kod VBA 7 uruchomiony w 32-bitowej wersji MS Office 2010+"
   #End If
 #Else
   ' Środowisko wersji VBA 6, lub wersji niższej
   MsgBox "Kod VBA uruchomiony w wersji VBA 6 lub niższej."
 #End If

End Function
 
• PtrSafe - kwalifikator
Używany jest do deklarowania procedur i funkcji z zewnętrznych bibliotek DLL w 64 bitowym VBA 7. Określa, że deklaracja funkcji jest zgodna z 64 bitowymi środowiskami programistycznymi. Atrybut ten jest obowiązkowy w systemach 64 bitowych. Deklarując funkcję z zewnętrznej biblioteki należy za każdą instrukcją Declare umieścić atrybut (kwalifikator, słowo kluczowe) PtrSafe. Należy także zmienić 32 bitowe odwołania do uchwytów i wskaźników na zgodne z typem LongPtr, a 64 bitowym argumentom przypisać nowy typ danych LongLong. Aby zapewnić wsteczną kompatybilność z VBA w wersji 6.0 atrybut PtrSafe nie jest wymagany w 32 bitowym środowisku VBA 7, ale nic nie stoi na przeszkodzie, by kwalifikator ten stosować w 32 bitowym środowisku VBA 7.
 
#If VBA7 Then
   ' Aplikacja używa 32 lub 64-bitowej wersji VBA 7 (Office 2010 i wyższe wersje).
   ' Zastosuj atrybut PtrSafe po instrukcji Declare
   Public Declare PtrSafe Function...
#Else
   ' Aplikacja używa 32 bitowej wersji VBA 6 (Office 2007 i niższe wersje)
   ' Nie używaj atrybutu PtrSafe po instrukcji Declare
   Public Declare Function...
#End If
 
• LongLong - typ danych
8 bajtowy typ danych. Przechowuje liczby w przedziale wartości od -9.223.372.036.854.775.808 do 9.223.372.036.854.775.807. Typ ten jest dostępny tylko w 64 bitowym środowisku (w 64 bitowej wersji pakietu Office).
• LongPtr - typ danych
W 64 bitowym środowisku VBA 7 jest to 8 bajtowa liczba typu LongLong. W 32 bitowym środowisku VBA 7 jest to liczba 4 bajtowa typu Long. Typ ten (LongPtr) jest zalecany przy deklarowaniu uchwytów i wskaźników w obu środowiskach VBA 7 (64 i 32 bitowym). Korzystanie z typu LongPtr umożliwia pisanie kodu, który będzie działał zarówno w 32 jak i 64 bitowym środowisku VBA 7.
Typ LongPtr obsługiwany jest tylko w VBA 7 (zarówno w 32 bitowym jak i 64 bitowym środowisku).
Ponieważ 32 bitowe VBA nie miało takiego typu, więc w przypadku zadeklarowania wskaźnika (uchwytu) jako typ Long, wszystkie zmienne 64 bitowe zostały by „ucięte” do wartości 32 bitowej i mogłyby wskazywać na nieprawidłowy uchwyt (wskaźnik).
 
 ' Sprawdź wersję VBA
 #If VBA7 Then
    ' Aplikacja używa 32 lub 64-bitowej wersji VBA 7 (Office 2010 i wyższe wersje).
    ' Zastosuj atrybut PtrSafe po instrukcji Declare
    ' i zmień deklaracje uchwytów i wskaźników na typ LongPtr
     Public Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageA" ( _
                                                  ByVal hwnd As LongPtr, _
                                                  ByVal wMsg As Long, _
                                                  ByVal wParam As LongPtr, _
                                                  lParam As Any) As LongPtr
 #Else
    ' Aplikacja używa 32 bitowej wersji VBA 6 (Office 2007 i niższe wersje)
    ' Nie używaj atrybutu PtrSafe po instrukcji Declare
    ' deklaracje uchwytów i wskaźników są typu Long
     Public Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _
                                                  ByVal hwnd As Long, _
                                                  ByVal wMsg As Long, _
                                                  ByVal wParam As Long, _
                                                  lParam As Any) As Long
 #End If
 

Ponieważ atrybut PtrSafe nie jest wymagany w 32 bitowym środowisku VBA 7, możemy skorzystać ze stałej kompilacji Win64 i typu LongLong, bez używania typu LongPtr.

 
 ' Sprawdź, czy środowisko VBA jest 64 bitowe
 #If Win64 Then
    ' Aplikacja używa 64-bitowej wersji VBA 7 (Office 2010 i wyższe wersje).
    ' Zastosuj atrybut PtrSafe po instrukcji Declare
    ' i zmień deklaracje uchwytów i wskaźników na typ LongLong
     Public Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageA" ( _
                                                  ByVal hwnd As LongLong, _
                                                  ByVal wMsg As Long, _
                                                  ByVal wParam As LongLong, _
                                                  lParam As Any) As LongLong
 #Else
    ' Aplikacja używa 32 bitowej wersji VBA 7 (Office 2010+) lub VBA 6 (Office 2007-)
    ' Nie używaj atrybutu PtrSafe po instrukcji Declare
    ' deklaracje uchwytów i wskaźników powinny być typu Long
     Public Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _
                                                  ByVal hwnd As Long, _
                                                  ByVal wMsg As Long, _
                                                  ByVal wParam As Long, _
                                                  lParam As Any) As Long
 #End If
 

Powiem szczerze. Nie wiem która metoda deklaracji jest optymalna. Czy korzystanie ze stałej kompilacji VBA7 i typu LongPtr do deklaracji uchwytów i wskaźników, czy korzystanie ze stałej kompilacji Win64 i typu LongLong do deklaracji uchwytów i wskaźników w środowisku 64 bitowym, a typu Long w 32 bitowych VBA 7 i VBA 6.