poniedziałek, 16 maja 2016

• Konwersja: String na Hex

Czasami potrzebujemy przekonwertować ciąg znaków na zapis heksadecymalny. Zrobić taką konwersję możemy na dziesiątki sposobów. Podstawowe czynności to pobranie kolejnych (pojedynczych) znaków konwertowanego ciągu, odczytanie dla każdego znaku kodu ANSI w reprezentacji dziesiętnej, a następnie przekonwertowanie wartości dziesiętnej kodu ANSI na postać heksadecymalną. Podczas konwersji na postać heksadecymalną należy zadbać, by wartości liczbowe mniejsze od 16 dla których zapis heksadecymalny ma postać (&H0 = 0 do &HF = F) zapisać w postaci dwuznakowej (&H0=00 do &HF=0F), by można było jednoznacznie dokonać konwersji w kierunku przeciwnym tj. z zapisu heksadecymalnego na zapis dziesiętny.

Aby przekonwertować ciąg znaków (jego poszczególne znaki ) na zapis heksadecymalny posłużymy się funkcjami:
• Asc(ciąg)
zwracającą wartość typu Integer odpowiadającą kodowi znaku pierwszego elementu w ciągu znaków ciąg
• Hex(liczba)
zwracającą wartość typu String reprezentującą heksadecymalną (szesnastkową) wartość argumentu liczba
• StrConv(ciąg, konwersja)
zwracającą wartość typu Variant podtyp String w postaci ciągu znaków poddanych konwersji określonej przez argument konwersja
• Mid(ciąg, start[, długość])
zwracającą wartość typu Variant podtyp String zawierającą podaną liczbę znaków długość z ciągu znaków, począwszy od pozycji start.
• Right(ciąg, długość)
zwracającą wartość typu Variant podtyp String zawierającą podaną liczbę znaków długość począwszy od prawej strony ciągu znaków.
• Format(wyrażenie[, format[, pierwszy_dzień_tygodnia [, pierwszy_tydzień_roku]]])
zwracającą wartość typu Variant podtyp String zawierającą wyrażenie sformatowane zgodnie z instrukcjami zawartymi w wyrażeniu formatującym.
• String(liczba, znak)
zwracającą wartość typu Variant podtyp String zawierającą ciąg składający się ze znaku powtórzonego podaną liczbę razy
oraz instrukcją Mid(...)
• Mid(zmienna_znakowa, początek[, długość]) = ciąg
zastępującą podaną liczbę znaków w argumencie zmienna_znakowa typu Variant podtyp String znakami z innego ciągu znaków. Liczba zastępowanych znaków jest zawsze mniejsza lub równa liczbie znaków w zmiennej_znakowej

Wykorzystując wyżej wymienione funkcję przedstawię trzy wersje funkcji konwertujących ciąg znaków na zapis heksadecymalny.

1. Funkcja TextToHexMid(ByRef sText As String) As String
Dla każdego kolejnego znaku ciągu wejściowego, zwróconego przez funkcję Mid$, pobierany jest jego kod ANSI za pomocą funkcji Asc, który konwertowany jest do postaci heksadecymalnej. Następnie z przodu dopisywany jest znak "0" i z tak powstałego ciągu za pomocą funkcji Right$ pobierane są dwa ostatnie znaki.

Public Function TextToHexMid(ByRef sText As String) As String
Dim i As Long

  For i = 1 To Len(sText)
    TextToHexMid = TextToHexMid & Right$("0" & Hex$(Asc(Mid$(sText, i, 1))), 2)
  Next

End Function

2. Funkcja TextToHexFormat(ByRef sText As String) As String
Ciąg wejściowy konwertowany jest za pomocą funkcji StrConv do tablicy bajtów. Każdy element tablicy (kod ANSI) konwertowany jest do postaci heksadecymalnej i formatowany za pomocą funkcji Format$ do postaci dwuznakowej.

Public Function TextToHexFormat(ByRef sText As String) As String
Dim aBytes() As Byte
Dim i As Long

  aBytes = StrConv(sText, vbFromUnicode)
  
  For i = LBound(aBytes) To UBound(aBytes)
    TextToHexFormat = TextToHexFormat & Format$(Hex$(aBytes(i)), "00")
  Next i

End Function

3. Funkcja TextToHex(ByRef sText As String) As String
Ciąg wejściowy konwertowany jest za pomocą funkcji StrConv do tablicy bajtów. Przygotowywany jest bufor wyjściowy dwukrotnie dłuższy od ciągu wejściowego. Każdy element tablicy (kod ANSI) konwertowany jest do postaci heksadecymalnej i dopisywany za pomocą instrukcji Mid$ na kolejnych miejscach w buforze wyjściowym.
Option Compare Database
Option Explicit
' • Function TextToHex(ByRef sText As String) As String
'  Funkcja konwertująca ciąg znaków na na postać heksadecymalną
' --------------------------------------------------------------------------------------
' autor: Zbigniew Bratko - 04.2016
'
' Pobiera:
'  • sText - ciąg wejściowy, który ma zostać poddany konwersji.
' Zwraca:
'  Przy powodzeniu zwraca ciąg znaków przekonwertowany do postaci heksadecymalnej.
'  Każdy znak ciągu wejściowego [sText] zapisany jest za pomocą dwóch znaków. Dla znaków,
'  których kod ANSI jest mniejszy od 16, heksadecymalny zapis poprzedzony jest cyfrą "0" (ZERO)
'  Przy niepowodzeniu zwraca ciąg zerowej długości.
' --------------------------------------------------------------------------------------
' Ciąg wejściowy konwertowany jest za pomocą funkcji StrConv do tablicy bajtów.
' Przygotowywany jest bufor wyjściowy dwukrotnie dłuższy od ciągu wejściowego.
' Każdy element tablicy (kod ANSI) konwertowany jest do postaci heksadecymalnej
' i dopisywany za pomocą instrukcji Mid$ na kolejnych miejscach w buforze wyjściowym.
Public Function TextToHex(ByRef sText As String) As String
Dim aBytes() As Byte
Dim bAsc As Byte
Dim lLen As Long
Dim i As Long

  lLen = Len(sText)
  ' zamienia ciąg z postaci Unicode na znaki z domyślnej strony kodowej systemu (ANSI)
  aBytes = StrConv(sText, vbFromUnicode)
  ' przygotuj bufor wyjściowy (2x dłuższy)
  TextToHex = String(2 * lLen, vbNullChar)
  ' konwertuj poszczególne bajty do postaci heksadecymalnej
  For i = 0 To lLen - 1
    bAsc = aBytes(i)
    If bAsc > &HF Then
      Mid$(TextToHex, 2 * i + 1, 2) = Hex$(bAsc)
    Else
      Mid$(TextToHex, 2 * i + 1, 2) = "0" & Hex$(bAsc)
    End If
  Next

End Function

Prawie wszystko już wiemy na temat konwersji ciągu znaków na postać heksadecymalną. Piszę prawie ponieważ pozostało tylko przetestować szybkość działania wyżej przedstawionych funkcji.

Test będzie się składał z dwóch zadań:
• ciąg znaków "Ala ma Asa a Ola" (o długości 16 znaków) będzie 1000 razy konwertowany na postać heksadecymalną,
• ciąg znaków o długości 131 072 znaków będzie jednokrotnie poddany konwersji na postać heksadecymalną
Option Compare Database
Option Explicit

#If VBA7 Then
  Private Declare PtrSafe Function timeGetTime Lib "winmm.dll" () As Long
#Else
  Private Declare Function timeGetTime Lib "winmm.dll" () As Long
#End If


Public Function SpeedTextToHex()
Dim sText As String
Dim sRet As String
Dim lTime As Long
Dim i As Long
Const cMyText As String = "Ala ma Asa a Ola"
Const cForTo As Long = 1000

  ' rozruch
  sText = cMyText
  lTime = timeGetTime
    For i = 1 To cForTo
      DoEvents
    Next
  lTime = timeGetTime - lTime
  
  Debug.Print String(45, "-")
  Debug.Print "Ilość wywołań = " & cForTo, "Len(sText) = " & Len(sText)
  Debug.Print String(45, "-")
  
  ' wywołuj poszczególne funkcje 1000 razy dla ciągu o długości Len(sText) = 16 znaków
  sRet = ""
  lTime = timeGetTime
    For i = 1 To cForTo
      sRet = TextToHexFormat(sText)
    Next
  Debug.Print "TextToHexFormat", (timeGetTime - lTime); "milisekund"
  
  sRet = ""
  lTime = timeGetTime
    For i = 1 To cForTo
      sRet = TextToHexMid(sText)
    Next
  Debug.Print "TextToHexMid", , (timeGetTime - lTime); "milisekund"
  
  sRet = ""
  lTime = timeGetTime
    For i = 1 To cForTo
      sRet = TextToHex(sText)
    Next
  Debug.Print "TextToHex", , (timeGetTime - lTime); "milisekund"

  '=================================================================
  ' utwórz ciąg znaków o długości 131 072 znaków
  sText = cMyText
  For i = 1 To 13
    sText = sText & sText
  Next
  
  Debug.Print String(45, "-")
  Debug.Print "Ilość wywołań = 1", "Len(sText) = " & Len(sText)
  Debug.Print String(45, "-")
  
  ' wywołaj poszczególne funkcje dla ciągu o długości Len(sText) = 131 072 znaków
  sRet = ""
  lTime = timeGetTime
    sRet = TextToHexFormat(sText)
  Debug.Print "TextToHexFormat", (timeGetTime - lTime); "milisekund"
  sRet = ""
  lTime = timeGetTime
    sRet = TextToHexMid(sText)
  Debug.Print "TextToHexMid", , (timeGetTime - lTime); "milisekund"
  
  sRet = ""
  lTime = timeGetTime
    sRet = TextToHex(sText)
  Debug.Print "TextToHex", , (timeGetTime - lTime); "milisekund"
 
End Function
  

Szybkość funkcji konwertującej krótki tekst jest bez znaczenia. Czy to będą 3/100 sekundy, czy 8/1000 lub 3/1000 sekundy dla pojedynczej operacji konwersji, praktycznie nie jesteśmy w stanie stwierdzić, która funkcja wykonuje się najdłużej. Różnice szybkości będą zauważalne dla bardzo dużej ilości operacji (rzędu 100 000 lub więcej). Dla krótkich tekstów funkcja TextToHex(...) jest ok. 3 razy szybsza od funkcji TextToHexMid(...) oraz 10 razy szybsza od funkcji TextToHexFormat(...)
Inaczej sprawa wygląda dla bardzo długich ciągów znaków. Przykładowo dla ciągu o długości 130 000 znaków obie funkcje: TextToHexMid(...) oraz TextToHexFormat(...) są przeraźliwie wolne w porównaniu z funkcją TextToHex(...), która jest ok. 400 x szybsza od obu funkcji.

środa, 11 maja 2016

• Test funkcji HexToDec

W poprzednim artykule HexToDec pisałem o problemach konwersji z zapisu postaci heksadecymalnej na postać dziesiętną przy użyciu funkcji Val(ciąg). Aby ustrzec się błędów podczas konwersji z postaci heksadecymalnej na postać dziesiętną powinniśmy pilnować typów liczb, jakie powinna zwrócić funkcja konwertująca, poprzez stosowanie znaków deklaracji typu liczby (sufiksów %&^. Zaproponowałem, by zamiast używać funkcji Val(ciąg) i sufiksów typu liczby (%&^) w funkcjach konwertujących, używać funkcji konwersji typu według poniższego schematu:


Debug.Print "CByte", CByte("&H" & sHex)
Debug.Print "CInt", CInt("&H" & sHex)
Debug.Print "CLng", CLng("&H" & sHex)
Debug.Print "CSng", CSng("&H" & sHex)
Debug.Print "CDbl", CDbl("&H" & sHex)
Debug.Print "CDec", CDec("&H" & sHex)
Debug.Print "CCur", CCur("&H" & sHex)
#If VBA7 Then
   Debug.Print "CLngLng", CLngLng("&H" & sHex)
   Debug.Print "CLngPtr", CLngPtr("&H" & sHex)
#End If
Public Function HexToDec(ByVal sHex As String)

 Debug.Print "CByte", CByte("&H" & sHex)
  Debug.Print "CInt", CInt("&H" & sHex)
  Debug.Print "CLng", CLng("&H" & sHex)
  Debug.Print "CSng", CSng("&H" & sHex)
  Debug.Print "CDbl", CDbl("&H" & sHex)
  Debug.Print "CDec", CDec("&H" & sHex)
  Debug.Print "CCur", CCur("&H" & sHex)
  #If VBA7 Then
    Debug.Print "CLngLng", CLngLng("&H" & sHex)
    Debug.Print "CLngPtr", CLngPtr("&H" & sHex)
  #End If

End Function

Na podstawie powyższego schematu, napisałem funkcją testującą konwersję z zapisu postaci heksadecymalnej na postać dziesiętną:


Public Function testHexToDec(ByVal sHex As String)
On Error GoTo ErrHandler
Const errOVERFLOW = 6
Const errMISMATCH = 13

 Debug.Print "CByte", CByte("&H" & sHex)
  Debug.Print "CInt", CInt("&H" & sHex)
  Debug.Print "CLng", CLng("&H" & sHex)
  Debug.Print "CSng", CSng("&H" & sHex)
  Debug.Print "CDbl", CDbl("&H" & sHex)
  Debug.Print "CDec", CDec("&H" & sHex)
  Debug.Print "CCur", CCur("&H" & sHex)
  #If VBA7 Then
    Debug.Print "CLngLng", CLngLng("&H" & sHex)
    Debug.Print "CLngPtr", CLngPtr("&H" & sHex)
  #End If

ExiHere:
  Exit Function
ErrHandler:
  If Err.Number = errOVERFLOW Or Err.Number = errMISMATCH Then
    Debug.Print "Błąd nr " & Err.Number & "  " & Err.Description
    Resume Next
  Else
    MsgBox "Błąd nr " & Err.Number & vbNewLine & Err.Description
    Resume ExiHere
  End If
End Function 
 

Poniżej wyniki przykładowych wywołań funkcji konwersji typu:
&HFFFF,
&HFFFF FFFF,
&HFFFF FFFF FFFF,
&HFFFF FFFF FFFF FFFF,
&H8000,
&H8000 0000,
&H8000 0000 0000,
&H8000 0000 0000 0000 0000,
oraz test konwersji dolnej i górnej granicy zakresu liczbu typu Long
Hex$(-2 147 483 648) i Hex$(2 147 483 647)

Test w 64-bitowym środowisku VBA7 na konwersję do postaci heksadecymalnej i potem powrotną konwersję na postać dziesiętną, liczby 2 147 483 647 będącej górnym zakresem liczby typu Long

' górna granica zakresu Long = 2 147 483 647
   Hex$(2147483647) = 7FFFFFFF
   CLng("&H" & "7FFFFFFF") = 2147483647
' w wersji skróconej test ma postać:
   CLng("&H" & Hex$(2147483647)) = 2147483647
' i równość ta jest prawdziwa

oraz liczby -2 147 483 648 będącej dolną granicą zakresu liczby typu Long

' dolna granica zakresu Long = -2 147 483 648
   Hex$(-2147483648) = FFFFFFFF80000000
   CLng("&H" & "FFFFFFFF80000000") = Błąd nr 13. Type mismatch
' wynik nieznany, gdyż MS Access wyświetla komunikat:
' MS Access podczas konwersji na postać heksadecymalną potraktował liczbę -2147483648 jako typ LongLong i aby otrzymać prawidłową wartość, musimy przekonwertowanej liczbę z postaci heksadecymalnej na typ LonLong
   CLngLng("&H" & "FFFFFFFF80000000") = -2147483648

Wcześniej pisałem, by ustrzec się błędów podczas konwersji z postaci heksadecymalnej na postać dziesiętną powinniśmy pilnować typów liczb, jakie powinna zwrócić funkcja konwertująca. Jak widać, powinniśmy pilnować także typu liczby poddawanej konwersji na postać heksadecymalną.

' dolna granica zakresu Long = -2 147 483 646
   Hex$(CLng(-2147483648)) = 80000000
   CLng("&H" & "80000000") = -2147483648
' w wersji skróconej test ma postać:
   CLng("&H" & Hex$(CLng(-2147483648))) = -2147483648
' i równość ta jest prawdziwa

niedziela, 1 maja 2016

• Konwersja - HexToDec

Czasami potrzebujemy przekonwertować liczbę całkowitą na postać heksadecymalną. Żaden problem. Od zawsze w VBA istniała funkcja do tego celu przeznaczona. Jest nią funkcja Hex(liczba) zwracająca wartość typu String reprezentującą heksadecymalną (szesnastkową) wartość liczby (do ośmiu znaków szesnastkowych w środowisku 32-bitowym i szesnastu znaków w środowisku 64-bitowym. Obowiązkowy argument liczba jest dowolnym poprawnym wyrażeniem numerycznym lub wyrażeniem znakowym. Jeżeli argument liczba nie jest liczbą całkowitą, przed obliczeniem wartości funkcji Hex(liczba) jest ona zaokrąglana do najbliższej parzystej liczby całkowitej.
No to zróbmy mały test funkcji Hex(liczba):

Public Function TestHex()
Dim iInt As Integer
Dim lLng As Long
Dim llLngLng As LongLong

  Debug.Print "--------------------------------------------"
  Debug.Print "Hex$(65535)   = "; Hex$(65535)
  Debug.Print "Hex$(-1)      = "; Hex$(-1)
  Debug.Print "--------------------------------------------"
  iInt = -1: lLng = -1: llLngLng = -1
  Debug.Print "iInt=-1; lLng=-1; llLng=-1"
  Debug.Print "--------------------------------------------"
  Debug.Print "Hex$(iInt)    = "; Hex$(iInt)
  Debug.Print "Hex$(lLng)    = "; Hex$(lLng)
  Debug.Print "Hex$(llLngLng)= "; Hex$(llLngLng)
 
 DoCmd.RunCommand acCmdDebugWindow

End Function

Na pierwszy rzut oka wyniki nie są zbyt optymistyczne.
Liczba 65535 w zapisie heksadecymalnym ma postać &HFFFF.
Liczba -1 w zapisie heksadecymalnym ma również postać &HFFFF.
Liczba -1 przypisana do zmiennej typu Integer ma postać &HFFFF.
Liczba -1 przypisana do zmiennej typu Long ma postać &HFFFFFFFF.
Liczba -1 przypisana do zmiennej typu LongLong ma postać &HFFFFFFFFFFFFFFFF.

Jeżeli chodzi o konwersję z zapisu postaci heksadecymalnej na postać dziesiętną MS Access i jego VBA nie posiada wbudowanej funkcji dokonującej takiej konwersji. Ale w  artukule Q161304 How To Convert Hexadecimal Numbers to Long Integer możemy znaleźć rozwiązanie problemu konwersji liczby z reprezentacji heksadecymalnej, na postać dziesiętną za pomocą funkcji  Val(ciąg).

Funkcja Val(ciąg) zwraca wartość liczb tworzących ciąg w postaci wartości numerycznej odpowiedniego typu. Funkcja Val(...) przerywa odczyt ciągu przy pierwszym znaku, który nie jest fragmentem liczby. Symbole i znaki, które często stanowią element wartości numerycznych, na przykład znak dolara ($), czy przecinek (,) nie są rozpoznawane przez funkcję Val(...). Funkcja ta rozpoznaje jednak symbol podstawy &O (dla systemu ósemkowego) i &H (dla systemu szesnastkowego). Spacje, tabulatory i znaki wysuwu wiersza są pomijane. Liczby z zakresu &H80000000 (-2147483648) do &HFFFFFFFF (-1) oraz liczby z zakresu &H8000 (-32768) do &HFFFF (-1) są traktowane przez funkcję Val(ciąg) jako liczby ujemne.

Jak widzimy poniżej wyniki są trochę dziwne:

Val("&HFFFF") = -1
Val("&HFFFFFFFF") = -1
Val("&H" & Hex$(65535)) = -1

Przedstawiona w tym artykule funkcja HexToLong(...)

Function HexToLong(ByVal sHex As String) As Long
   HexToLong = Val("&H" & sHex & "&")
End Function

zwraca następujące wartości:

HexToLong("FFFF") = 65535
HexToLong("FFFFFFFF") = -1
HexToLong(Hex$(65535)) = 65535
HexToLong(Hex$(-1)) = 65535

Jak widać, zawsze jest coś nie tak. Aby ustrzec się błędów podczas konwersji z postaci heksadecymalnej na postać dziesiętną powinniśmy pilnować typów liczb, jakie powinna zwrócić funkcja konwertująca. W tym celu możemy użyć jednego ze znaków deklaracji (sufiksu).

Znak deklaracji (sufiks)Typ danychPrzykład
%IntegerDim intLiczba%
&LongDim lngLiczba&
^LongLongDim lnglngLiczba&
@CurrencyDim curLiczba@
!SingleDim sngLiczba!
#DoubleDim dblLiczba#

W wyniku konwersji z postaci heksadecymalnej na postać dziesiętną zawsze otrzymamy liczbę całkowitą, gdyż funkcja Hex(liczba) zaokrągla do najbliższej parzystej liczby całkowitej, więc przydatne mogą być jedynie funkcje konwertująca na liczbę typu Integer, Long lub LongLong (w środowisku VBA7).

Function HexToInteger(ByVal sHex As String) As Integer
   HexToInteger = Val("&H" & sHex & "%")
End Function
___________________________________________________________________________________

Function HexToLongLong(ByVal sHex As String) As LongLong
   HexToLongLong = Val("&H" & sHex & "^")
End Function

Moim zdaniem, zamiast używać funkcji Val(ciąg) i sufiksów typu (%,&, ^) w funkcjach konwertujących, możemy użyć funkcje konwersji typu według poniższego schematu:

Public Function HexToDec(ByVal sHex As String)

 Debug.Print "CByte", CByte("&H" & sHex)
  Debug.Print "CInt", CInt("&H" & sHex)
  Debug.Print "CLng", CLng("&H" & sHex)
  Debug.Print "CSng", CSng("&H" & sHex)
  Debug.Print "CDbl", CDbl("&H" & sHex)
  Debug.Print "CDec", CDec("&H" & sHex)
  Debug.Print "CCur", CCur("&H" & sHex)
  #If VBA7 Then
    Debug.Print "CLngLng", CLngLng("&H" & sHex)
    Debug.Print "CLngPtr", CLngPtr("&H" & sHex)
  #End If

End Function

wtorek, 29 marca 2016

• Opcje startowe MS Access

Zazwyczaj w bazie danych przekazanej użytkownikowi końcowemu ukrywa się główne okno bazy danych programu MS Access, aby uniemożliwić mu przeglądanie struktury bazy, danych zawartych w tabelach i zwracanych przez kwerendy, przypadkowych (bądź specjalnych) zmian danych, usunięcia bądź zmiany nazw obiektów itp. Aby uniknąć tego typu problemów powinniśmy zapewnić użytkownikowi („użyszkodnikowi”) dostęp do danych za pomocą tzw. interfejsu użytkownika, opartego na różnego rodzaju formularzach.
Samo ukrycie okna nawigacji (okna bazy danych), poprzez odznaczenie pola wyboru „Wyświetl okienko nawigacji” za pomocą polecenia Menu/Plik/Opcje/ Bieżąca baza danych/Nawigacja

Opcje startowe MS Access
niewiele daje, gdyż użytkownik może wybrać polecenie Menu/Plik/Opcje/

i w otwartym oknie „Opcje programu Access” wybrać kategorię: Bieżąca baza danych/Nawigacja, zaznaczyć pole wyboru „Wyświetl okienko nawigacji”, ponownie uruchomić (zdefragmentować) bazę i cieszyć się widokiem okienka nawigacji (okna bazy danych). Użytkownik może także uruchomić bazę trzymając wciśnięty klawisz „Shift” i dokonać zmian w Opcjach startowych bazy. Po normalnym uruchomieniu bazy, użytkownik może odsłonić panel nawigacji (okno bazy) za pomocą klawisza F11, jak również wykorzystać inne klawisze specjalne programu MS Access (patrz tabelka poniżej), by zaglądnąć tam, gdzie nie powinien.

  Sekwencja klawiszy  Skutek
F11Wyświetlenie okna bazy danych na wierzchu
CTRL+GWyświetlenie okna analizy programu
CTRL+F11Przełączanie pomiędzy niestandardowym paskiem menu i wbudowanym paskiem menu
ALT+F11Wyświetlenie okna edytora VBA
CTRL+BREAKWprowadzenie trybu przerwania i wyświetlenie bieżącego modułu w oknie modułu
SHIFTWciśnięcie podczas startu bazy umożliwia pominięcie opcji startowych tzn. makro Autoexec i formularz startowy nie będą uruchamiane.

• Opcje startowe aplikacji MS Access

Prawie wszystkie opcje dostępne w oknie dialogowym „Opcje programu Access” można zmieniać za pomocą kodu VBA. W większości przypadków, o ile właściwość CurrentDbProperties("PropertyName") nie została ustawiona w oknie dialogowym „Opcje programu Access”, to właściwość nie istnieje w zbiorze Properties obiektu Application i próba odwołania się do nieistniejącej właściwości

' sprawdź, czy istnieje właściwość sPrpName
IsObject (CurrentDb.Properties(sPrpName))

powoduje wygenerowanie błędu nr 3270 - „Nie odnaleziono właściwości”. Błąd ten (nr 3270) musimy przechwycić, a w obsłudze błędu utworzyć żądana właściwość i dodać ją do kolekcji CurrentDb.Properties
Do ustawienia każdej z tych właściwości służy metoda SetOption obiektu Application, lub właściwość CurrentDb.Properties(sPropertyName) = vPropertyValue, a do pobierania bieżącej wartości metoda GetOption lub właściwość CurrentDb.Properties(sPropertyName). Składnia wygląda następująco:

Application.SetOption sPropertyName, vPropertyValue
CurrentDb.Properties(sPropertyName) = vPropertyValue

gdzie sPropertyName jest jedną z nazw z poniższej tabeli, a vPropertyValue wartością, której typ musi być zgodny z typem danych właściwości. W większości przypadków, o ile właściwość nie została już ustawiona w oknie dialogowym „Opcje programu Access”, nie jest ona dołączona do zbioru Properties obiektu Application.

  Opcja  Właściwość do ustawieniaTyp danych
Tytuł aplikacjiAppTitledbText
Ikona aplikacjiAppIcondbText
Wyświetl formularzStartupFormdbText
Wyświetl okno bazy danychStartupShowDBWindowdbBoolean
Wyświetl pasek stanuStartupShowStatusBardbBoolean
Wyświetl pasek menuStartupMenuBardbText
Pasek menu skrótówStartupShortcutMenuBardbText
Zezwalaj na pełne menuAllowFullMenusdbBoolean
Zezwalaj na domyślne menu skrótówAllowShortcutMenusdbBoolean
Zezwalaj na wbudowane paski narzędziAllowBuiltInToolbarsdbBoolean
Zezwalaj na zmiany w paskach narzędziAllowToolbarChangesdbBoolean
Zezwalaj na wyświetlenie kodu po wystąpieniu błęduAllowBreakIntoCodedbBoolean
Użyj specjalnych klawiszy programu AccessAllowSpecialKeysdbBoolean
Nazwa wstążki (Ribbon)CustomRibbonIDdbText
Zezwalaj na użycie klawisza Shift do ominięcia opcji startowych AllowBypassKeydbBoolean
AllowBypassKey nie jest właściwością wbudowaną do bazy danych Accessa. Aby z niej korzystać, należy ją utworzyć, dołączyć do kolekcji Properties i ustawić jej wartość.

No to spróbujmy zabezpieczyć bazę danych tak, by ograniczyć użytkownikowi do minimum możliwość ingerowania w strukturę i dane naszej bazy danych.

Option Compare Database
Option Explicit
 
 ' • Public Function ChangeProperty( _
 '                            sPropertyName As String, _ 
 '                            vPropertyType As Variant, _ 
 '                            vPropertyValue As Variant) As Boolean 
 ' ------------------------------------------------------------------
 ' autor: Zbigniew Bratko - 03.2016

 ' [sPropertyName] - nazwa właściwości bazy danych
 ' [vPropertyType] - typ właściwości sPropertyName,
 ' [vPropertyValue] - wartość na jaką ma zostać ustawiona właściwość sPropertyName,
 '
 ' [Out] - przy powodzeniu zwraca TRUE, przy niepowodzeniu FALSE
 '

Public Function ChangeProperty( _
                            sPropertyName As String, _
                            vPropertyType As Variant, _
                            vPropertyValue As Variant) As Boolean
Dim dbs As DAO.Database
Dim prp As DAO.Property
Const cPropertyNotFound As Long = 3270
On Error GoTo ErrHandler
  
  Set dbs = Application.CurrentDb
    ' spróbuj ustawić wartość właściwości,
    ' jeżeli właściwość nie istnieje, wygenerowany zostanie błąd nr 3270
    ' i właściwość zostanie utworzona w obsłudze błędu
    dbs.Properties(sPropertyName) = vPropertyValue

  ChangeProperty = True

ExitHere:
  ' zniszcz zmienną obiektową
  Set dbs = Nothing
  Exit Function
ErrHandler:
  If Err.Number = cPropertyNotFound Then
    ' właściwość nie istnieje,
    With dbs
      ' utwórz właściwość o określonym typie i wartości początkowej
      Set prp = .CreateProperty(sPropertyName, vPropertyType, vPropertyValue)
        'dodaj właściwość do kolekcji
        .Properties.Append prp
        'odśwież kolekcję Properties
        .Properties.Refresh
        ' zniszcz zmienną obiektową
        Set prp = Nothing
      End With
    Resume Next
  Else
    MsgBox "Nieoczekiwany błąd nr " & Err.Number & "." & vbNewLine & Err.Description
    ChangeProperty = False
    Resume ExitHere
  End If

End Function


' przykładowa funkcja zmieniająca opcje startowe MS Access
Public Function LockDatabase()
Dim fRet As Boolean

  ' ustawienia obowiązują dopiero po ponownym uruchomieniu bazy
  fRet = ChangeProperty("AppTitle", dbText, "Moja baza")
  fRet = ChangeProperty("StartupForm", dbText, "frmMojFormularz")
  fRet = ChangeProperty("StartUpShowDBWindow", dbBoolean, False)
  fRet = ChangeProperty("AllowFullMenus", dbBoolean, False)
  fRet = ChangeProperty("AllowShortcutMenus", dbBoolean, False)
  ' AllowBuiltInToolbars=TRUE, ponieważ nie zadziała instrukcja obiektu DoCmd
  ' ukrywająca wstążkę: DoCmd.ShowToolbar "Ribbon", acToolbarNo
  fRet = ChangeProperty("AllowBuiltInToolbars", dbBoolean, TRUE)
  fRet = ChangeProperty("AllowToolbarChanges", dbBoolean, False)
  fRet = ChangeProperty("AllowBreakIntoCode", dbBoolean, False)
  fRet = ChangeProperty("AllowSpecialKeys", dbBoolean, False)
  fRet = ChangeProperty("AllowBypassKey", dbBoolean, False)
  fRet = ChangeProperty("StartupShowStatusBar", dbBoolean, False)

End Function

Funkcję LockDatabase() należy wywołać za pomocą makra AutoExec Action = "RunCode"; Argument = "ChangeProperty()" lub np. w zdarzeniu OnLoad() formularza startowego.

Ale dalej mamy problem z ukryciem panelu nawigacji (okna bazy). Co prawda, menu Plik jest mocno okrojone (tylko trzy polecenia Menu), ale użytkownik może wybrać polecenie Menu/Plik/Opcje prywatności/ i w otwartym oknie „Opcje programu Access” wybrać kategorię: Bieżąca baza danych/Nawigacja, zaznaczyć pole wyboru „Wyświetl okienko nawigacji” oraz pozmieniać inne opcje, ponownie uruchomić (zdefragmentować) bazę i uzyskać dostęp do okienka nawigacji (okna bazy danych) i innych obiektów bazy.

Nie tylko polecenie Menu/Plik/Opcje prywatności umożliwia użytkownikowi zmianę opcji startowych bazy danych. Także Pasek narzędzi Szybki dostęp pozwala na bezproblemowe dostanie się do okna dialogowego „Opcje programu Access”.

Wystarczy wybrać polecenie Dostosuj pasek narzędzi Szybki dostęp i następnie pozycję Więcej poleceń... by otworzyć okno dialogowe „Opcje programu Access” i tam zmienić wybrane opcje.

Umieszczony na Pasku narzędzi Szybki dostęp przycisk „Visual Basic (Alt+F11)” umożliwia otwarcie edytora Visual Basic i przeglądanie całego kodu VBA.

Umieszczony na Pasku narzędzi Szybki dostęp przycisk „Opcje programu Access” umożliwiają otwarcie okna„Opcje programu Access”.
Jak widać, w dalszym ciągu występują problemy z ukryciem okna nawigacji (dla MS Access wersja 2007+) lub okna bazy danych (dla MS Access wersja 2003-). To co jest proste w MS Access 2003 i wersjach niższych, nie jest tak oczywiste dla późniejszych wersji MS Access w których pojawia się okno nawigacji (ang. Navigation Pane), Pasek narzędzi Szybki dostęp (ang. Quick Access Toolbar), nowa właściwość Nazwa niestandardowej wstążki (ang. CustomRibbonID), a także Wstążki (ang. Ribbons), zamiast dawnych pasków narzędzi i wiele innych, mniej lub bardziej ważnych elementów. Nie wspomnę o problemach pobierania uchwytów okien, nazw klas i wielu innych problemach związanych z VBA7.

Ale o tym, być może kiedyś.

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 .

poniedziałek, 29 lutego 2016

• Funkcje API - Mutex, jedna instancja bazy MS Access

W poprzednim artykule • Funkcje API - Jedna instancja bazy MS Access przedstawiłem kilka rozwiązań dotyczących zabezpieczenia bazy danych MS przed otwarciem przez użytkownika drugiej instancji otwartej bazy danych. Przedstawione metody działają w oparciu o opcje startowe MS Access tj. makro AutoExec lub właściwość StartupForm = „Formularz startowy”. Akcja makra AutoExec (Action ="RunCode") lub zdarzenie OnLoad() formularza wywoływały funkcję własną CountAccessInstances(...) przedstawioną w art. • Funkcje API - ile uruchomiono instancji MS Access •. Funkcja własna CountAccessInstances w zależności od przekazanych argumentów wyszukiwała okna MS Access klasy OMain o tytule zgodnym z tekstem przekazanym w opcjonalnym argumencie sAccessTitle.

' poglądowy kod
If CountAccessInstances([opconalnie: JakisTytulOkna MS Access]) > 1 Then
  MsgBox "Nie możesz uruchomić następnej bazy danych, [instancji bazy danych] "
  ' zamknij otwieraną bazę
  Application.Quit
End If

Uniemożliwienia otwarcie drugiej instancji bazy danych wymagało przekazania w argumencie funkcji własnej CountAccessInstances(...) tytułu okna MS Access. Wymogiem poprawnego działania funkcji było, by bieżąca baza danych miała ustawioną właściwość CurrentDb.Properties("AppTitle"), gdyż przy braku tej właściwośći, nie można było jednoznacznie określić tytułu okna MS Access, gdyż tytuł okna MS Access zmienia się w zależności od jego wersji. Umożliwiało to otwarcie drugiej instancji bieżącej bazy danych za pomocą innej wersji MS Access.

• Mutex - synchronizacja procesów.

•    Co to jest mutex?

Tak ogólnie można napisać, że mutex jest obiektem służącym do synchronizacji wątków i dostępny jest dla wszystkich wątków uruchomionych aktualnie w systemie. Bardziej szczegółowe dane dotyczące mutexu możemy uzyskać z Wikipedii w artykule Problem wzajemnego wykluczania. Poniżej cytuję fragment z tego artykułu:

Algorytmy wzajemnego wykluczania (w skrócie często nazywane mutex, z ang. mutual exclusion) są używane w przetwarzaniu współbieżnym w celu uniknięcia równoczesnego użycia wspólnego zasobu (np. zmiennej globalnej) przez różne wątki/procesy w częściach kodu zwanych sekcjami krytycznymi. Sekcja krytyczna jest fragmentem kodu, w którym wątki (lub procesy) odwołują się do wspólnego zasobu. Sama w sobie nie jest ani mechanizmem, ani algorytmem wzajemnego wykluczania. Program, proces lub wątek może posiadać sekcje krytyczne bez mechanizmów czy algorytmów implementujących wzajemne wykluczanie.

Aby stworzyć mutex należy wywołać funkcję CreateMutex(...), która po stwierdzeniu braku mutexu o podanej nazwie w systemie tworzy go, a funkcja GetLastError zwraca wartość ERROR_SUCCESS=0. Jeśli mutex już istnieje, funkcja CreateMutex(...) tworzy nowy uchwyt, a funkcja GetLastError zwraca wartość ERROR_ALREADY_EXISTS.
W chwili tworzenia mutexu wątek żąda natychmiastowego prawa własności do niego. Inne wątki moga otworzyć mutex za pomocą funkcji OpenMutex() i czekać na objęcie mutexu w posiadanie. Aby uwolnić mutex należy wywołć funkcję ReleaseMutex(). Jeśli wątek zakończy się i nie uwolni mutexu, to taki mutex uważany jest za porzucony i każdy czekający wątek może objąć go w posiadanie.
Teoretycznie jawne zamykanie uchwytów nie jest niezbędne, gdyż system zamyka wszystkie uchwyty w chwili zakończenia procesu, jednak zalecane jest, kiedy obiekt jest niepotrzebny.

• Mutex - jedna instancja bazy.

W celu uniemożliwienia otwarcie drugiej instancji bieżącej bazy danych wykorzystamy trzy funkcje API:
• CreateMutex(...) - tworzy, bądź otwiera nazwany lub nienazwany mutex i zwraca uchwyt do niego
• ReleaseMutex(...) - zwalnia mutex, proces przestaje być właścicielem mutexu, co pozwala przejąć go przez inny proces.
• CloseHandle(...) - zamyka obiekt identyfikowany przez uchwyt.  
Option Compare Database
Option Explicit

' • Function OneInstanceDb() As Long
' --------------------------------------------------------------------------------------
' autor: Zbigniew Bratko - 02.2016
' Tworzy nazwany muteks, uniemożliwiający otwarcie drugiej instancji bieżącej bazy danych
' [Out] - Przy powodzeniu tworzy nazwany muteks i zwraca liczbę różną od 0 (Zero)
'         Przy niepowodzeniu zwraca 0 (Zero)
'

#If VBA7 Then
  Private Declare PtrSafe Function CreateMutex Lib "kernel32" _
           Alias "CreateMutexA" _
           (lpMutexAttributes As SECURITY_ATTRIBUTES, _
           ByVal bInitialOwner As Long, _
           ByVal lpName As String) As LongPtr
  Private Declare PtrSafe Function ReleaseMutex Lib "kernel32" _
          (ByVal hMutex As LongPtr) As Long
  Private Declare PtrSafe Function CloseHandle Lib "kernel32" _
          (ByVal hObject As LongPtr) As Long
  Private Type SECURITY_ATTRIBUTES
          nLength As Long
          lpSecurityDescriptor As LongPtr
          bInheritHandle As Long
  End Type
  Private m_hMutex As LongPtr
#Else
  Private Declare Function CreateMutex Lib "kernel32" _
          Alias "CreateMutexA" _
          (lpMutexAttributes As SECURITY_ATTRIBUTES, _
          ByVal bInitialOwner As Long, _
          ByVal lpName As String) As Long
  Private Declare Function ReleaseMutex Lib "kernel32" _
          (ByVal hMutex As Long) As Long
  Private Declare Function CloseHandle Lib "kernel32" _
          (ByVal hObject As Long) As Long
  Private Type SECURITY_ATTRIBUTES
            nLength As Long
            lpSecurityDescriptor As Long
            bInheritHandle As Long
    End Type
    Private m_hMutex As Long
#End If
Private Const ERROR_ALREADY_EXISTS = &HB7
Private Const ERROR_SUCCESS = 0&


' Funkcja własna OneInstanceDb() - uruchomienie:
' makro AutoExec:
' Action = RunCode
' FunctionName = OneInstanceDb ()
' lub
' Zdarzenie OnLoad formularza startowego (StartUpForm)
Public Function OneInstanceDb() As Long

#If VBA7 Then
  Dim hMutex As LongPtr
#Else
  Dim hMutex As Long
#End If

Dim sa As SECURITY_ATTRIBUTES

  sa.nLength = Len(sa)
  ' utwórz nazwany muteks i zostań jego właścicielem (bInitialOwner=1)
  hMutex = CreateMutex(sa, 1, "NazwaMuteksu")
  ' sprawdź, czy nie wystąpił błąd podczas tworzenia muteksu
  If (Err.LastDllError = ERROR_SUCCESS) Then
    ' zapisz uchwyt muteksu w zmiennej prywatnej na poziomie modułu
    m_hMutex = hMutex
    OneInstanceDb = 1
    ' ... inne instrukcje (np. DoCmd.OpenForm "StartUpForm")
  Else
    If (Err.LastDllError = ERROR_ALREADY_EXISTS) Then
      ' muteks już istnieje, zamknij uchwyt
      Call CloseHandle(hMutex)
      MsgBox "Nie możesz uruchomić drugiej instancji bieżącej bazy danych!"
    Else
      ' (*) - patrz uwagi
      MsgBox "Nieprzewidziany błąd nr " & Err.LastDllError
    End If
    Application.Quit
  End If

End Function


' • Function RemoveMutex() As Long
' -------------------------------------------------------
' autor: Zbigniew Bratko - 02.2016
' Zwalnia muteks i zamyka uchwyt muteksu
' [Out] - Przy powodzeniu zwraca liczbę różną od 0 (Zero)
'         Przy niepowodzeniu zwraca 0 (Zero)

' Funkcja własna RemoveMutex()
' funkcję zwalniającą muteks należy uruchomić przy zamykaniu bazy, lub gdy chcemy
' zezwolić użytkownikowi na otwarcie drugiej instancji bazy.
Public Function RemoveMutex() As Long
Dim lRet As Long
    
    If m_hMutex <> 0 Then
      ' zwolnij muteks
      If ReleaseMutex(m_hMutex) <> 0 Then
        RemoveMutex = CloseHandle(m_hMutex)
      End If
    End If

End Function

Uwagi:
(*) komunikat typu:

MsgBox "Nieprzewidziany błąd nr " & Err.LastDllError

jest dość zdawkowy i nie daje końcowemu użytkownikowi zbyt wiele informacji o przyczynie błędu.
W następnym artykule (być może) postaram się przedstawić rozwiązanie, by komunikaty o błędach były bardziej przyjazne dla użytkownika.

• Inne rozwiązania

Nasuwa mi się jeszcze jedno (dość nietypowe) rozwiązanie oparte o utworzenie, przez zabezpieczaną bazę, swojego własnego, prywatnego okna o indywidualnym tytule, którego istnienie świadczyć będzie, że baza została juz uruchomiona. Dodatkowo, utworzone okno będzie mogło spełniać rolę "malutkiego" komunikatora pomiędzy bazami. Ale o tym będzie w którymś kolejnym artykule. Gdy artykuł będzie gotowy, podam tutaj link do niego.

• Funkcje API - Mutex, jedna instancja bazy MS Access • Przykładowa baza MS Access do pobrania

poniedziałek, 22 lutego 2016

• Funkcje API - Jedna instancja bazy MS Access

logo

Czasami projekt bazy danych wymaga, (z takich, czy innych względów), by użytkownik nie mógł otworzyć drugiej instancji bieżącej bazy danych. Sposobów zabezpieczenia się (a raczej pseudozabezpieczenia) przed otwarciem drugiej instancji bieżącej bazy jest kilka, jeśli nie kilkanaście. Generalnie wszystkie metody będą działały w oparciu o opcje startowe MS Access:

• Akcja makro Autoexec
Makro jest uruchamiane automatycznie podczas startu MS Access. Za pomocą funkcji uruchamianej przez to makro dokonujemy "pseudozabezpieczeń". Możemy również otworzyć jakiś formularz (może być ukryty), który będzie ... patrz punkt niżej:
Właściwość: StartupForm
Podczas startu MS Access automatycznie otwierany jest formularz startowy: StartupForm = „Nazwa formularza startowego” w którym np. w zdarzeniu OnLoad uruchamiamy funkcję, która tworzy "pseudozabezpieczenie"

Obie te opcje startowe można pominąć, otwierając bazę danych trzymając wciśnięty klawisz „Shift”. Ale z pomocą przychodzi właściwość AllowBypassKey, za pomocą której możemy uniemożliwić używanie klawisza SHIFT dla opcji startowych. Tak przy okazji, w celu dodatkowego zabezpieczeni możemy skorzystać z właściwości AllowSpecialKeys za pomocą której możemy uniemożliwić korzystanie użytkownikowi ze specjalnych klawiszy programu MS Access

  Sekwencja klawiszy  Skutek
F11Wyświetlenie okna bazy danych na wierzchu
CTRL+GWyświetlenie okna analizy programu
ALT+F11Wyświetlenie okna edytora VBA
CTRL+F11Przełączanie pomiędzy niestandardowym paskiem menu i wbudowanym paskiem menu
CTRL+BREAKWprowadzenie trybu przerwania i wyświetlenie bieżącego modułu w oknie modułu
SHIFTWciśnięcie podczas startu bazy umożliwia pominięcie opcji startowych tzn. makro Autoexec i formularz startowy nie są uruchamiane.

• Pseudozapezpieczenia

• Zapis pliku na dysku

Pierwszym, narzucającym się rozwiązaniem jest zapisanie „gdzieś” na dysku, podczas startu bazy, małego pliku tekstowego, takie niby „cookies”, który będzie usuwany przy zamknięciu bazy danych. Podczas startu bazy danych, z poziomu makra Autoexec lub formularza startowego, uruchomiana będzie „Jakas_Funkcja” sprawdzającej obecność pliku. Jak plik istnieje, to zostanie wywołana metoda Application.Quit dla nowo otwieranej instancji bazy.

' przykładowy kod
If Len(Dir("Sciezka_MojPlik")) > 0 Then
  ' plik istnieje, zamknij otwieraną bazę
  MsgBox "Nie możesz uruchomić następnej bazy danych"
  Application.Quit
End If

No niby tak, ale jeżeli zawiesi się System (baza), wtedy plik "Sciezka_MojPlik" nie zostanie usunięty z dysku, i przy następnym uruchomieniu naszej bazy możemy mieć poważne kłopoty. Plik istnieje (i będzie istniał, aż do jego usunięcia), więc może się zdarzyć tak, że zawsze będzie QUIT.

• Tylko jedna instancja MS Access

Innym sposobem jest sprawdzenie podczas uruchomienia bazy danych, czy inna instancja MS Access jest otwarta. W tym celu możemy skorzystać z funkcji własnej CountAccessInstances(...) przedstawionej w art. • Funkcje API - ile uruchomiono instancji MS Access •. Jeżeli funkcja zwróci wartość > 1, wtedy nie pozwalamy na uruchomienie bazy.

' przykładowy kod
If CountAccessInstances > 1 Then
  MsgBox "Nie możesz uruchomić następnej bazy danych"
  ' zamknij otwieraną bazę
  Application.Quit
End If

Niestety, to rozwiązanie ma jeden wielki minus. W ten sposób uniemożliwimy użytkownikowi otwarcie jakiejkolwiek innej bazy danych, gdy otwarta jest „ta nasza jedyna baza”.

• Tylko jedna instancja bieżącej bazy MS Access

Aby umożliwić użytkownikowi otwarcie innych baz, z wyłączeniem drugiej instancji bieżącej bazy danych, musimy podczas uruchamiania bazy sprawdzić, czy nie została wcześniej otwarta pierwsza instancja bazy. W tym celu skorzystamy z funkcji własnej GetTextWindow(...) przedstawionej w art.  • Funkcje API - jak pobrać tytuł (tekst) okna • w celu pobrania tytułu (tekstu) okna MS Access otwieranej bazy danych oraz (tak jak w poprzednim przykładzie), z funkcji własnej CountAccessInstances(...) przedstawionej w art. • Funkcje API - ile uruchomiono instancji MS Access • w celu pobrania ilości okien MS Access o tytule (tekście) zwróconym przez funkcję własną GetTextWindow(...).

' przykładowy kod  
Dim sCurrDbTitle As String

' pobierz tytuł okna MS Access bieżącej bazy
sCurrDbTitle = GetTextWindow(Application.hWndAccessApp)
' pobierz ilość otwartych instancji MS Access o tytule okna sCurrDbTitle
If CountAccessInstances(sCurrDbTitle) > 1 Then
  MsgBox "Nie możesz uruchomić drugiej instancji bieżącej bazy danych!"
  ' zamknij otwieraną bazę
  Application.Quit
End If

Niestety, to rozwiązanie też ma swoje wady. Jeżeli baza danych nie ma ustawionej właściwości CurrentDb.Properties("AppTitle"), którą to właściwość można ustawić za pomocą: Menu Plik/Opcje/Bieżąca baza danych/Opcje aplikacji/Tytuł Aplikacji, to tytuł okna MS Access zmienia się, wraz ze zmianą wersji MS Access.

Przykładowo dla bazy testowej: C:\tmp\dbTest.accdb
MS Access 2016 - "Access — dbTest : Baza danych- C:\tmp\dbTest.accdb (format pliku programu Access 2007–2016)"
MS Access 2010 - "Microsoft Access — dbTest : Baza danych (Access 2007 - 2010)"
MS Access 2007 - "Microsoft Access — dbTest : Baza danych (Access 2007)"
Jeżeli wymogiem dla prawidłowego działania kodu jest konieczność ustawienia właściwości CurrentDb.Properties("AppTitle") to nie musimy korzystać z funkcji własnej GetTextWindow(...), tytuł okna MS Access pobierzemy dzięki właściwości CurrentDb.Properties("AppTitle").

' przykładowy kod  
Dim sCurrDbTitle As String

' pobierz tytuł okna MS Access bieżącej bazy
sCurrDbTitle = CurrentDb.Properties("AppTitle")
' pobierz ilość otwartych instancji MS Access o tytule okna sCurrDbTitle
If CountAccessInstances(sCurrDbTitle) > 1 Then
  MsgBox "Nie możesz uruchomić drugiej instancji bieżącej bazy danych!"
  ' zamknij otwieraną bazę
  Application.Quit
End If

• Inne rozwiązania

Innym rozwiązaniem jest uruchomienie „jakieś funkcji”, która będzie pilnowała, by otwarta była jedna instancja bieżącej bazy danych. Innymi słowy „Jakaś funkcja” musi być uruchomiona podczas startu bazy danych i przez cały czas działania bazy musi sprawdzać, czy nie została otwarta druga instancja bieżącej bazy danych. Najprostszym rozwiązaniem jest otwarcie ukrytego formularza startowego z włączonym Timerem, w którym to w pewnych odstępach czasu (patrz. właściwość Form.TimerInterval), będzie sprawdzała, czy nie jest otwarta druga instancja bazy (druga instancja MS Access)

Oczywistym rozwiązaniem powinno być wykorzystanie systemowych mechanizmów służących do synchronizacji wątków, czyli algorytmów wzajemnego wykluczania (w skrócie mutex, z ang. mutual exclusion) Jak wykorzystać mutex w celu uniemożliwienia otwarcia drugiej instancji bazy danych MS Access opisałem w artykule • Funkcje API - Mutex, jedna instancja bazy MS Access •