piątek, 27 września 2013

Sprawdzanie właściwości przed jej ustawieniem

Niniejszy temat został wywołany na forum StackOverflow.Com gdzie padło pytanie o sens niniejszego kodu (który tutaj został lekko zmodyfikowany dla celów prezentacyjnych):

1For i =1 to 10000
2    If Me.Rows(i).Hidden = True Then
3        Me.Rows(i).Hidden = False
4    End If
5Next

Na pierwszy rzut oka w istocie- jaki jest sens sprawdzać czy dany wiersz jest ukryty skoro chcemy i tak ostatecznie odkryć wszystkie dziesięć tysięcy wierszy. Wystarczy przecież wykonać poniższą pętlę:

1For i =1 to 10000
2    Me.Rows(i).Hidden = False
3Next

Otóż pierwszy zapis jest bardzo uzasadniony i ma swoją wyraźną przewagę nad pętlą drugą (choć efekt ostatecznie będzie na 100% identyczny). Porównawczo wygląda to następująco (dla identycznych parametrów środowiska, w którym wykonany został test dla każdego wariantu):

1. pętla pierwsza, wszystkie wiersze były uprzednio odkryte- czas wykonania- 0.2 sek
2. pętla druga, wszystkie wiersze były uprzednio odkryte- czas wykonania- 13.1 sek

3. pętla pierwsza, wszystkie wiersze były uprzednio ukryte- czas wykonania- 15.6 sek


Z czego wynikają różnice. Zasadniczo z prostego założenia, zgodnie z którym odczyt właściwości odbywa się szybciej niż jej ustawienie. Jeżeli istnieje uzasadnienie, że większość elementów (tu: wierszy) może nie wymagać ustawiania właściwości (tu: odkrywania) to warto uprzednio sprawdzić bieżący stan danej właściwości (tu: czy wiersz jest ukryty czy odkryty). Zasadę tę warto stosować do wszystkich właściwości o ile pracujemy na relatywnie dużej kolekcji.

piątek, 20 września 2013

Kolejność ma znaczenie czyli o porządkach w kolekcji

Chyba nie każdy (szczególnie początkujący) programista zdaje sobie sprawę z faktu, że elementy każdej z kolekcji ułożone są w określonym porządku. Często porządek ten nie ma znaczenia dla zadania jakie realizujemy, w innych sytuacjach wiedza ta może wpłynąć na prawidłowość wykonania operacji lub szybkość jej wykonania. Poniżej kilka przykładów, informacji i ciekawostek dot. tego zagadnienia w odniesieniu do aplikacji Excel i Word, gdzie każdy z przykładów zostanie przedstawiony w zapisie pętli For Each.

1. Skoroszyty:

1Dim WB As Workbook
2For Each WB In Application.Workbooks

pętla wykonywać się będzie w kolejności w jakiej skoroszyty były otwarte lub utworzone.

2. Arkusze

1Dim SH As WorkSheet
2For Each SH In ActiveWorkbook.WorkSheets

pętla wykona się w kolejności od lewego do prawego arkusza. Nie ma znaczenia czy arkusz jest ukryty czy nie.

3. Komórki Cells
1Dim Cell as Range
2For Each Cell In Selection.Cells

pętla zostanie wykonana w porządku w jakim piszemy i czytamy, od lewej do prawej, od górnego wiersza w dół zaznaczenia.

4. Komentarze w arkuszu Excel
1Dim KOM as Comment
2For Each KOM In ActiveSheet.Comments

o kolejności decyduje adres komórki, w której znajduje się komentarz, a następnie kolejność tej komórki w obszarze arkusza zgodnie z zasadami opisanymi dla komórek Cells w punkcie 3 powyżej.

5. Komentarze w dokumencie Word
1Dim KOM as Comment
2For Each KOM In ActiveDocument

pętla zostanie wykonana w kolejności występowania komentarzy w dokumencie, od początku dokumentu (od pierwszego komentarza) do końca (do ostatniego komentarza).

6. Zakładki w dokumencie Word. W tym przypadku dostępne są dwa warianty:
1Dim BM as Bookmark
2For Each BM In ActiveDocument.Bookmarks

zakładki zostaną ułożone w kolejności ... alfabetycznej, wg nazw zakładek.

1Dim BM as Bookmark
2For Each BM In ActiveDocument.Content.Bookmarks

w tym przypadku zakładki zostaną ułożone w kolejności występowania w dokumencie.

 7. Kształty Shape w Excelu

1Dim SH As Shape
2For Each SH In ActiveSheet.Shapes

o kolejności decyduje parametr ZOrderPosition, a więc ułożenie względem osi Z arkusza. Pierwszym kształtem będzie ten `najbliżej obszaru komórek`, ostatni to ten `najbliżej nas.`

Oczywiście każda z kolekcji posiada swoją własną kolejność. Powyższe wybrane przykłady mają na celu zwrócenie uwagi na ten aspekt. Dla innych obiektów warto przeprowadzić swoje własne próby.

poniedziałek, 16 września 2013

Obszar Range dwuwymiarowy do tablicy Array jednowymiarowej


Jedną z typowych operacji wykonywanych w VBA jest pobieranie danych z kolejnych komórek arkusza i wykonywanie określonych działań na pobranych wartościach. Proste działanie w którym najczęściej wykorzystujemy prostą pętlę For...Next.

Co jednak ważne, w sytuacji gdy operacja dotyczy wielu komórek o wiele bardziej efektywnym pozostaje przeniesienie wartości komórek do tablicy Array i przeprowadzenie dalszych działań na elementach tablicy. W sytuacji tej należy jednak pamiętać, że utworzona tablica pozostaje tablicą dwu-wymiarową.

Sytuacja ta ma miejsce także wtedy gdy pobieramy dane z pojedynczej kolumny lub pojedynczego wiersza arkusza. Spójrzmy na poniższy przykład.



 utworzenie poniższych tablic:
1rowArr = Range("A1:C1")
2colArr = Range("A1:A3"

sprawi, że obie tablice będą dwuwymiarowe. Pobranie z nich wartości C będzie wymagało utworzenia następujących zapytań:

1Debug.Print rowArr(1, 3)
2Debug.Print colArr(3, 1)

Istnieje jednak możliwość utworzenia z w/w tablic tablic jednowymiarowych. Aby to uczynić należy transponować tablice. Prześledźmy to w kolejnych krokach dla obu tablic jednocześnie:

1rowArr = Application.Transpose(rowArr)
2colArr = Application.Transpose(colArr)

W wyniku powyższego działania tablica rowArr pozostanie dwuwymiarowa podczas gdy colArr stanie się tablicą jednowymiarową. Aby więc pobrać wartość C z tablic wywołamy następujące instrukcje:

1Debug.Print rowArr(3, 1)
2Debug.Print colArr(3)

Jeżeli jednak tablicę rowArr transponujemy ponownie także i ona stanie się jednowymiarową:

1rowArr = Application.Transpose(rowArr)
2Debug.Print rowArr(3)

wtorek, 10 września 2013

Jak ukryć makro po stronie aplikacji Excel i zachować jego publiczny charakter

Wyobraźmy sobie sytuację, w której szereg współpracujących procedur Sub znajduje się w naszym projekcie VBA. Makra rozdzielone zostały na szereg modułów. Zależy nam jednak na tym, aby makra były dostępne z poziomu każdej inne procedury, a jednocześnie zależy nam na tym, aby szereg z tych makr nie był dostępny i widoczny z poziomu Excela...

Powyższe tytułem wstępu, a teraz uporządkujmy możliwe rozwiązania:

Wariant A- raczej oczywisty. Nadanie procedurze parametru Private:

    Private Sub MojaProcedura()

sprawi, że procedura dostępna będzie z poziomu tylko modułu w którym jest makro oraz okna Immediate z przy zachowaniu pełnej referencji: Module1.MojaProcedura. Makro nie będzie dostępne z poziomu aplikacji Excel.

Wariant B oczywisty. Dodanie u góry modułu wpisu:
  
    Option Private Module

co sprawi, że wszystkie makra danego modułu nie są widoczne po stronie Excela i jednocześnie zachowują swój publiczny charakter w zakresie pracy z nimi z poziomu innych modułów i okna Immediate.

Wariant C nie zawsze oczywisty. Konstrukcja publicznej procedury z parametrem:

    Public MojaProceduraParametr(boParametr as Boolean)

czyni z tej procedury procedurę o podobnej charakterystyce jak w wariancie B- nie jest widoczna po stronie aplikacji Excel, ale jest w pełni dostępna z poziomu okna Immediate i innych modułów. Oczywiście wywołując ją należy podać parametr nie mniej nie mamy żadnego obowiązku, aby ten parametr wykorzystać w dalszej części procedury.

Szersze wykorzystanie wariantu C dla aplikacji MS Word, gdzie ze względu na specyficzną sytuację związaną z makrami typu AutoExec rozwiązanie C jest szczególnie przydatne, opisane zostało pod poniższym tematem w serwisie StackOverflow:

Hide AutoExec() and AutoNew() macros from the macro list yet still have them run?

czwartek, 5 września 2013

Wyrażenia regularne RegExp raz jeszcze

Postanowiłem raz jeszcze wrócić do zagadnienia związanego z wyrażeniami regularnymi. Tym razem poszerzę temat o dwa obszary- pobieranie określonego fragmentu n-tego elementu spełniającego kryterium wyszukiwania oraz włączenie RegExp do własnej funkcji użytkownika (UDF).

Spójrzmy na początek na poniższy przykładowy tekst:

    Questionnaire results from company web.
    Name: John Smith
    Phone: 1234567
    Name: Jan Kowalski
    Phone: 9876545321
    Name: Johan Schmitt
    Phone: 00112233

Z pomocą RegExp i VBA spróbujemy przygotować rozwiązanie, które umożliwi pobranie np. drugiego imienia  i nazwiska (Jan Kowalski) czy też trzeciego numeru telefonu (00112233) z naszego przykładowego tekstu.

Trudność pierwsza- musimy odnaleźć fragment tekstu, który zaczyna się od Name. W tym celu nasz wzorzec będzie miał postać:
1.Pattern = "Name:\s*(.*)\s*

W wyniku czego jesteśmy w stanie otrzymać kolekcję składającą się z elementów:

    Name: John Smith
    Name: Jan Kowalski 
    Name: Johan Schmitt 

Trudność druga- jak pobrać samo imię i nazwisko i pominąć początkowy fragment z wyników wyszukiwania? W tym celu będziemy musieli sięgnąć głębiej w metodę .Execute. Samo wywołanie tej metody tworzy kolekcję zawierającą wszystkie wystąpienia spełniające kryterium .Pattern. Istnieje jednak możliwość pobrania określonego fragmentu n-tego elementu sięgając do kolekcji .SubMatches. Elementami należącymi do tej kolekcji będą wszystkie fragmenty, które zostały ujęte w nawiasach w naszym wzorcu .Pattern.
Ogólna składnia metody .Execute wyglądałaby następująco:

1.Execute(tekst)(nteWystąpienie).SubMatches(ntyFragment)

Proponuję zebrać w całość nasz kod. Na początek testowa procedura wywołująca z dodatkowymi komentarzami wewnątrz kodu:

01Sub Pobieranie_nTego_Elementu()
02    Dim myText As String
03    myText = "Questionnaire results from company web." & Chr(13) & _
04            "Name: John Smith" & Chr(10) & _
05            "Phone: 1234567" & Chr(10) & _
06            "Name: Jan Kowalski" & Chr(10) & _
07            "Phone: 9876545321" & Chr(10) & _
08            "Name: Johan Schmitt" & Chr(10) & _
09            "Phone: 00112233"
10  
11    'wzorzec .Pattern ze względu na Name
12    Debug.Print GetItem(myText, "Name:\s+(.*)\s*", 1)
13    'wzorzec .Pattern ze względu na Phone
14    Debug.Print GetItem(myText, "Phone:\s+(.*)\s*", 2)
15End Sub

A teraz funkcja właściwa uwzględniająca przedstawione powyżej istotne elementy metody .Execute z dodatkowym komentarzem:
01Function GetItem(ByVal STR As String, Wzorzec As String, iNum As Long)
02 
03    Dim objRegExp As Object
04    Set objRegExp = CreateObject("vbscript.regexp")
05    
06    With objRegExp
07        .Global = True 'niezbędne ustawienie
08        .Pattern = Wzorzec
09         
10        'kluczowa konstrukcja metody i kolekcji SubMatches
11        GetItem = .Execute(STR)(iNum).SubMatches(0)
12        
13    End With
14End Function

Po wywołaniu procedury Pobieranie_nTego_elementu() uzyskamy w oknie immediate dokładnie to czego szukaliśmy, a więc odpowiednio:

    Jan Kowalski
    00112233