środa, 18 grudnia 2013

Narzędzia programisty VBA

Oczywiście, że podstawowym środowiskiem pracy programisty VBA pozostaje Integrated Developer Editor (IDE) dostarczany w pakiecie Office. Nie mniej istnieje kilka narzędzi pomocniczych, które można zintegrować ze środowiskiem lub jako osobne aplikacje mogą stać się niezastąpioną pomocą w codziennej pracy z VBA. Oto kilka przykładowych programów wartych polecenia i uwagi.

1. EZ-Tool 
 Link do strony dostawcy: www

Narzędzie instalowane jako dodatek środowiska IDE. Wspiera programistę w szeregu czynności: tworzenie procedur, zarządzanie komentarzami, obsługą błędów. Dostarcza schematy dla ADO. W moim odczuciu narzędzie w stylu 'must have'.

2. RibbonX Visual Designer 2010
Link do strony dostawcy: www

Narzędzie wspomagające tworzenie elementów wstążki- zakładek, przycisków i innych kontrolek.

3. Excel VBA Code Cleaner
Link do strony dostawcy: www

Jak sama nazwa wskazuje narzędzie oczyszczające kod ze zbędnych elementów. Narzędzie polecane w przypadku dużych projektów, projektów tworzonych w dłuższych okresach czasu, itp.



piątek, 13 grudnia 2013

Unikalne wartości tablicy Array (2/2)

Kontynuując poprzedni wpis o tym samym tytule tym razem trzecia z technik odnajdywania unikalnych elementów kolekcji. Technika ta opiera się o wykorzystanie słownika Dictionary. Wszystkich czytelników, których rozwiązanie to w jakiś sposób zainteresuje odsyłam do tej strony, na której znaleźć można dodatkowe informacje.

Poniższy kod, korzystając z obiektu słownika, zwróci do arkusza unikalne wartości z tablicy. Wewnątrz kodu zawarte zostały dodatkowe informacje i komentarze wyjaśniające cały mechanizm.

01Sub Unikaty_słownik()
02  
03    'tworzymy obiekt zewnętrznej biblioteki _
04     wykorzystując późne wiązanie
05    Dim Słownik As Object
06    Set Słownik = CreateObject("Scripting.Dictionary")
07    
08    Dim Element As Variant
09    Dim arrTablica As Variant
10    Dim i As Long
11 
12    'nasza przykładowa tablica
13    arrTablica = Array("Do", "Re", "Mi", "Do", "Fa", "Sol", _
14                "Re", "Mi", "Fa", "La", "Do", "Si", _
15                "Do", "Re", "Mi", _
16                "fa", "sol", "LA", "sI")
17 
18    'tworzymy słownik
19    For Each Element In arrTablica
20        'wymaga stosowania funkcji konwersji tekstu
21         Słownik(StrConv(Element, vbProperCase)) = 1
22    Next
23 
24    'zwracamy elementy do arkusza
25    For Each Element In Słownik.keys
26        i = i + 1
27        Cells(i, 1) = Element
28    Next
29 
30    'Wynik: Do,Re,Mi,Fa,Sol,La,Si
31 
32End Sub

Na koniec kluczowe pytanie, którą technikę użyć- kolekcję czy słownik. Osobiście nie udzielę jednoznacznej odpowiedzi, ale wskażę na kilka różnic i podobieństw boldem oznaczając lepsze rozwiązanie.

a) Kolekcja jest obiektem wbudowanym języka VBA, Słownik wymaga tworzenia referencji do osobnej biblioteki.

b) Kolekcja wymaga stosowania obsługi błędów czego nie ma w przypadku Słownika i co jest rozwiązaniem (pozornie?) bardziej profesjonalnym.

c) Dodawanie elementu do kolekcji ignoruje wielkość liter, słownik uwzględnia wielkość liter przez co wymaga stosowanie dodatkowych technik VBA.

d) Kolekcja posiada ograniczony zasób metod i właściwości podczas gdy słownik pozostaje bardziej elastyczny pod względem szerszych zastosowań.

poniedziałek, 9 grudnia 2013

Unikalne wartości tablicy Array (1/2)

 Jak skutecznie odnaleźć unikalne wartości z tablicy Array?
 powyższe pytanie usłyszałem podczas kursu VBA prowadzonego ostatnio w Warszawie.

Osobiście stosuję dwie (lub trzy) techniki odszukiwania unikatów:

Technika 1. wartości tablicy wrzucamy w pionowy, czysty obszar arkusza, następnie stosując polecenie usuwania duplikatów (RemoveDuplicates) pozbywamy się zbędnych elementów, a następnie wynik pobieramy do naszej tablicy w kodzie VBA. Przyznam jednak, że rozwiązanie to nie należy do najbardziej efektywnych.

Technika 2. o wiele łatwiej i skuteczniej będzie sięgnąć po obiekt kolekcji (Collection) w VBA. W tym celu wykorzystamy fakt, że dodanie elementu do kolekcji, nie jest możliwe jeżeli element taki już istnieje. Jeżeli podejmujemy taką próbę to kompilator zwraca błąd wykonania. W naszej procedurze zignorujemy taki błąd w wyniku czego zignorujemy jednocześnie dany element.

01Sub Unikaty_kolekcja()
02  
03    Dim arrKolekcja As New Collection
04    Dim Element As Variant
05    Dim arrTablica As Variant
06    Dim i As Long
07 
08    'nasza przykładowa tablica
09    arrTablica = Array("Do", "Re", "Mi", "Do", "Fa", "Sol", _
10                "Re", "Mi", "Fa", "La", "Do", "Si", _
11                "Do", "Re", "Mi", _
12                "fa", "sol", "LA", "sI")
13 
14    'ignorujemy błąd powtarzajacych się elementów
15    On Error Resume Next
16    'tworzymy kolekcję
17    For Each Element In arrTablica
18         arrKolekcja.Add Element, Element
19    Next
20 
21    'przywracamy standardową obsługę błędów
22    On Error GoTo 0
23 
24     
25    'zwracamy elementy kolekcji do arkusza
26    For i = 1 To arrKolekcja.Count
27        Cells(i, 1) = arrKolekcja(i)
28    Next
29 
30    'Wynik: Do,Re,Mi,Fa,Sol,La,Si
31 
32End Sub

Więcej informacji na temat kolekcji znaleźć można pod tym linkiem.
Kolekcje nie są jedynym czysto programistycznym sposobem na uzyskanie unikatów z tablicy. O alternatywnej technice napiszę w osobnym poście. Zapraszam do lektury już wkrótce.

poniedziałek, 2 grudnia 2013

Co w rzeczywistości zawiera komórka w Excelu?

Z pytaniem umieszczonym w tytule niniejszego posta spotkałem się kilkukrotnie, zarówno przeglądając różne wpisy i pytania na forach internetowych, jak również prowadząc szkolenie z zakresu VBA dla Excela. Chodzi bowiem o sytuację, w której chcemy określić rodzaj informacji zawartej w komórce z uwzględnieniem rodzaju formatowania jaki został zastosowany w danym zakresie arkusza.

Gdzie jednak znajduje się problem? Z punktu widzenia VBA liczba, data, czas i procent- wszystkie te elementy są liczbami. Także wartość Prawda/Fałsz w praktyce jest liczbą odpowiadającą 1 lub 0. W naszej sytuacji określić rzeczywisty typ danych znajdujących się w komórce.

W celu rozwiązania tego problemu wystarczy skonstruować prostą funkcję, której pełną postać znajdziecie Państwo poniżej. Kluczowa w tej funkcji pozostaje kolejność sprawdzania poszczególnych typów. Prześledźmy to na przykładzie typu liczbowego, który sprawdzany jest jako ostatni. Musimy się najpierw upewnić, że podane wartości nie są żadnym z typów: datą, wartością czasu, procentem lub wartością Prawda/Fałsz. Każdy z tych typów będąc domyślnie numerycznym zostałby więc rozpoznany jako liczba. Tymczasem nasza funkcja wydaje się działać prawidłowo co prezentuje poniższy zrzut ekranu.


01Public Function TypWartości(KOMÓRKA As Range)
02    
03    If IsEmpty(KOMÓRKA) Then
04                TypWartości = "PUSTA"
05    ElseIf Application.IsText(KOMÓRKA) Then
06                TypWartości = "TEKSTOWA"
07    ElseIf Application.IsLogical(KOMÓRKA) Then
08                TypWartości = "LOGICZNA"
09    ElseIf Application.IsErr(KOMÓRKA) Or Application.IsError(KOMÓRKA) Then
10                TypWartości = "BŁĄD"
11    ElseIf IsDate(KOMÓRKA) Then
12                TypWartości = "DATA"
13    ElseIf InStr(1, KOMÓRKA.Text, ":") <> 0 Then
14                TypWartości = "CZAS"
15    ElseIf Right(KOMÓRKA.Text, 1) = "%" Then
16                TypWartości = "PROCENT"
17    ElseIf IsNumeric(KOMÓRKA) Then
18                TypWartości = "LICZBA"
19    End If
20End Function

środa, 27 listopada 2013

Dynamiczne wymiary tablicy wielowymiarowej

Zgodnie z definicją dynamiczna tablica wielowymiarowa może zmienić swój rozmiar (ilość elementów) tylko w zakresie ostatniego jej wymiaru. Jeżeli więc zdefiniujemy naszą tablicę następująco:

1Dim myArrayA() As Variant
2ReDim myArrayA(1,1)

to redefinicja wymiaru dotyczyć może tylko drugiego wymiaru, pierwszy wymiar zawsze pozostanie o stałej wielkości równej 1. Możliwe będą więc następujące dalsze procesy:

1ReDim myArrayA(1,2)
2ReDim myArrayA(1,5)
3'itd.

Możemy jednak stworzyć tablicę wielowymiarową jako złożenie dwóch tablic. W sytuacji tej każdy element tablicy głównej zawierał będzie tablicę podrzędną. Tak mógłby wyglądać zapis takiego rozwiązania:

1Dim myArrayB() As Variant
2ReDim myArrayB(1)
3    
4myArrayB(0) = Array("0x0", "0x1", "0x2")
5myArrayB(1) = Array("1x0", "1x1")
6myArrayB(1) = Array("1x0", "1x1", "1x2")
7 
8ReDim Preserve myArrayB(2)
9myArrayB(2) = Array("2x0", "2x1", "2x2")

Jak widać w powyższym przypadku wymiar tablicy głównej można swobodnie zmieniać jak również elementy tablicowe w jej wnętrzu mogą również posiadać różne wymiary. Rozwiązanie to posiada jednak swoje wady, z których części można zaradzić, o czym w dalszej części wpisu.

Wadą pierwszą jest to, że o ile w przypadku pierwszej tablicy dany element pobieramy korzystając z zapisu:

1myArrayA(0,0)
2myArrayA(0,1

o tyle w drugim przypadku zapis ten jest następujący:

1myArrayB(0)(0)
2myArrayB(0)(1)

O ile z powyższym w większości przypadków radzimy sobie bez problemu, o tyle problem zasadniczy pojawia się w sytuacji, gdy próbujemy przenieść tablicę do komórek Excela. Dla powyższych przypadków (indeks wymiaru tablic to 2) zapisy te będą następujące:

1ActiveCell.Resize(3,3) = myArrayA    >> zapis poprawny i skuteczny
2ActiveCell.Resize(3,3) = myArrayB    >> zapis poprawny ale nieskuteczny

Chcąc rozwiązać problem dot. tablicy myArrayB i skutecznie przenieść jej wartości do zakresu komórek arkusza wystarczy tablicę tą dwukrotnie transponować. Poniższa instrukcja umożliwia prawidłowe wykonanie tej operacji:

1ActiveCell.Resize(3, 3) = Application.Transpose( _
2                           Application.Transpose(myArrayB))

czwartek, 21 listopada 2013

Funkcja MID w roli operatora

Znana i powszechnie stosowana funkcja MID języka VBA jest nie tylko poleceniem zwracającym fragment tekstu, ale także operatorem, którego zadaniem jest podmiana fragmentu tekstu dla określonej zmiennej.

Wyobraźmy sobie taki tekst:"Uzupełnienie k..... przez f...... MID", w którym w miejsce kropek chcemy wstawić odpowiednie rozwinięcie wyrazów aby uzyskać tekst: "Uzupełnienie kropek przez funkcję MID".

Poniżej przedstawiam dwie proste procedury realizujące to zadanie, pierwsza z nich wykorzystuje funkcję MID w nowej roli. Druga z procedur da dokładnie ten sam efekt. Co jednak ciekawe- pierwsza procedura wykona się około 2-3 razy krócej niż druga.

01Sub Przykład1()
02    
03    Dim strTekst As String
04    strTekst = "Uzupełnienie k..... przez f...... MID"
05    
06    Mid(strTekst, 15, 5) = "ROPEK"
07    Mid(strTekst, 28, 6) = "UNKCJĘ"
08    MsgBox strTekst
09    
10End Sub
11 
12Sub Przykład2()
13    
14    Dim strTekst As String
15    strTekst = "Uzupełnienie k..... przez f...... MID"
16    strTekst = Left(strTekst, 14) & "ROPEK" & Mid(strTekst, 20, 8) _
17                    & "UNKCJĘ" & Right(strTekst, 4)
18    MsgBox strTekst
19End Sub

Źródło powyższego działania funkcji MID zaczerpnąłem z tego pytania na StackOverflow.Com. Zainteresowane osoby zachęcam do zapoznania się z innym ciekawostkami zawartymi w odpowiedziach.  O niektórych z tych ciekawostek wspominałem już w swoich wpisach. O innych z pewnością kiedyś wspomnę w strefie wiedzy. Wiele z tych tematów poruszane podczas kursów VBA, które prowadzimy w Warszawie, Krakowie i Wrocławiu.

wtorek, 12 listopada 2013

TypeName i TypeOf- przydatne funkcje i operatory

Muszę przyznać, że prowadząc szkolenie z zakresu VBA zazwyczaj brakuje czasu aby zaprezentować i omówić dwie ważne instrukcje języka VBA: funkcję TypeName i operator TypeOf. Cel stosowania obu instrukcji jest podobny- określić rodzaj obiektu lub zmiennej. Sposób stosowania i wynik mogą być zgoła odmienne.

Spójrzmy na różnice i podobieństwa pomiędzy prezentowanymi instrukcjami przez pryzmat przykładów.

1. Sprawdzamy czy bieżące zaznaczenie odpowiada obiektowi określonego typu, tu: obiektowi Range
 

1Debug.Print TypeOf Selection Is Range
2Debug.Print TypeName(Selection) = "Range"

choć w obu wypadkach uzyskamy wyniku True proszę zwrócić uwagę, że wynikiem pracy funkcji TypeName jest ciąg tekstowy. TypeOf można użyć tylko w relacji do obiektu.


Oczywiście możemy dokonać podobnego sprawdzenia w relacji do zmiennej:
1Dim A As Variant
2Set A = Selection
3Debug.Print TypeOf A Is Range
4Debug.Print TypeName(A) = "Range"

Nie mniej tylko instrukcja TypeName pozwala nam określić jakiego typu jest dana zmienna:
1Dim B As Variant
2B = 1
3Debug.Print TypeName(A)    >> Range
4Debug.Print TypeName(B)    >> Integer

2. Jeżeli badana zmienna przyjmuje wartość Nothing operator TypeOf zwróci błąd a funkcja TypeName tekst Nothing:
1Dim C As Variant
2Set C = Nothing
3Debug.Print TypeOf C Is Range    >> błąd wykonania
4Debug.Print TypeName(C)    >> Nothing

3. Operator TypeOf zwraca wartości Prawda/Fałsz, funkcja TypeName zwraca ciąg tekstowy z nazwą typu zmiennej:
1Debug.Print TypeOf A Is Range    >> True
2Debug.Print TypeName(A)    >> Range

4. Operator TypeOf działa szybciej niż funkcja TypeName.

Dodatkowe informacje dot. w/w instrukcji można znaleźć tutaj:
TypeName
TypeOf

wtorek, 5 listopada 2013

Ograniczona ilość znaków w linii (MS Word)

Wyobraźmy sobie sytuację, że określonym dokumencie MS Word chcemy dokonać takiego podziału tekstu, aby w żadnej linii nie było więcej niż 30 znaków. Przykładowy paragraf, który pierwotnie wygląda następująco: 

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. Nunc viverra imperdiet enim. Fusce est. 

chcemy automatycznie podzielić na następujące linie, z których żadna nie ma więcej niż 30 znaków: 

Lorem ipsum dolor sit amet,
consectetuer adipiscing elit.
Maecenas porttitor congue
massa. Fusce posuere, magna
sed pulvinar ultricies, purus
lectus malesuada libero, sit
amet commodo magna eros quis
urna. Nunc viverra imperdiet
enim. Fusce est.


Poniższe makro wykona za nas tą operację dla wszystkich paragrafów aktywnego dokumentu. Tym razem wszystkie uwagi, ciekawostki i założenia zostały omówione w postaci komentarzy wewnątrz kodu.

01Sub Rozwiązanie()
02    Dim PAR As Paragraph
03    
04    'zmienne pomocnicze odpowiedzialne za
05    'kontrolę długości poszczególnych fragmentów
06    'tekstu dokumentu
07    Dim znakPAR&, startPAR&, i&, znakCEL&
08    
09        'dla każdego paragrafu dokumentu
10        For Each PAR In ActiveDocument.Paragraphs
11            
12            'określamy długość paragrafu, jego punkt początkowy,
13            'ilość znaków w linii (tu: 30)
14            znakPAR = PAR.Range.Characters.Count
15            startPAR = PAR.Range.Start
16            znakCEL = 30
17        
18        'dalsza procedura ma sens o ile długość paragrafu
19        'jest większa od limitu długości linii
20        If znakPAR > znakCEL Then
21          
22          For i = (startPAR + znakCEL) To (startPAR + znakPAR) Step znakCEL
23            'sprawdzamy czy znak podziału nie wypada w środku wyrazu
24            If Len(Trim(ActiveDocument.Range(i - 1, i + 1).Text)) < 2 Then
25                
26                'jeżeli nie to wstawiamy miękki znak podziału
27                'sprawdziwszy gdzie jest pusta spacja
28                If ActiveDocument.Range(i - 1, i) = " " Then
29                    ActiveDocument.Range(i, i).InsertBefore Chr(11)
30                Else
31                    ActiveDocument.Range(i + 1, i + 1).InsertAfter Chr(11)
32                End If
33                
34            Else
35                'jeżeli podział miałby nastąpić w środku wyrazu to
36                'przesuwamy się na początek tegoż i wstawiamy przejście
37                'do nowej linii
38                ActiveDocument.Range(i, i).Select
39                Selection.MoveLeft wdWord
40                Selection.InsertBefore Chr(11)
41                i = Selection.Start
42            End If
43            
44          Next
45        End If
46        Next
47End Sub

czwartek, 31 października 2013

Niemodalne okienko MsgBox

Standardowe okienko MsgBox występujące w języku VBA ma tą (niekiedy uciążliwą) cechę, że jest okienkiem modalnym. Modalnym czyli takim, które należy zamknąć aby móc powrócić do pracy z aplikacją znajdującą się w tle. Jak więc wywołać proste okienko z przyciskami Tak/Nie/Anuluj w trybie niemodalnym? Jest kilka możliwych wariantów spośród których skupię się szczegółowo tylko na jednym.

Wariant A. Należy stworzyć okno UserForm, a następnie wyświetlić go w trybie niemodalnym:

1UserForm1.Show vbModeLess

Wariant B. Wykorzystać okienko znajdujące się w zewnętrznej bibliotece Windows Script Hosting. Poniżej prezentuję kompletny kod, który wyświetla okienko z trzema przyciskami oraz wykonuje akcję zależnie od wybranego przycisku.

01Sub Okno_Popup()
02 
03    Dim wsMsgBox As Object
04    Set wsMsgBox = CreateObject("WScript.Shell")
05 
06    Dim mbResult As Integer
07    
08    'czas oczekiwania, opcjonalny, _
09    niestety nie zawsze działa!
10        Dim iTime As Integer
11            iTime = 2
12        
13    'wyświetlamy okienko określając jego parametry
14    mbResult = wsMsgBox.Popup("Okienko by Windows Scripting.", _
15                iTime, "Okienko niemodalne MsgBox!", 3)
16    
17    Select Case mbResult
18        Case 6
19            Debug.Print "klawisz yes/tak"
20        Case 7
21            Debug.Print "klawisz no/nie"
22        Case Else
23            Debug.Print "pozostałe"
24    End Select
25    
26End Sub

Rozwiązanie to posiada dodatkową opcję, która pozwala określić czas, przez jaki okienko będzie widoczne. Niestety, parametr ten nie działa stabilnie. W praktyce tylko ustawienie czasu oczekiwania na 1 sekundę gwarantuje ukrycie okna. W przypadku dłuższego czasu oczekiwania okienko i tak oczekuje na akcję za strony użytkownika. Być może błąd ten zależny jest od wersji oprogramowania nie mniej nie udało mi się odnaleźć informacji na temat rozwiązania tego problemu.

Dodatkowe informacje na temat okna Popup z biblioteki Windows Scripting znajdziecie Państwo pod tym linkiem.

wtorek, 22 października 2013

Losowy dokument w Wordzie

Tworząc szereg rozwiązań w VBA dla aplikacji MS Word często testuję określone techniki korzystając z przykładowego 'losowego' dokumentu. Dokument taki można stworzyć w bardzo prosty sposób wykorzystując ukryte polecenia aplikacji MS Word. W tym celu proszę utworzyć nowy pusty dokument w Word'zie i wykorzystać jedną z poniższych technik.

Wariant 1. Aby stworzyć losowy tekst 'Lorem ipsum' sięgamy po polecenie:

=Lorem()
które należy wpisać w pierwszym linii dokumentu i wcisnąć enter. W wyniku otrzymamy trzy paragrafy losowego tekstu.

Rozwiązanie to posiada możliwość parametryzowania:

=Lorem(10) 
utworzy 10 paragrafów z losową ilością zdań

=Lorem(10,3)
utworzy 10 paragrafów i po trzy zdania w każdym paragrafie.

Wariant 2. Jeżeli tekst, z których chcemy pracować ma być w języku polskim sięgamy wtedy po polecenie:

=Rand()
które działa podobnie jak prezentowane powyżej polecenie Lorem. Dostępne więc będą wiarianty:

=Rand(10)
=Rand(10,3)

które odpowiednio definiują ilość paragrafów i zdań w każdym paragrafie.

Technika ta przyda się w jednym z kolejnych wpisów, który wkrótce pojawi się w sekcji Strefa Wiedzy. Strefa Wiedzy prowadzona jest jako rozszerzenie dla kursów VBA organizowanych przez naszą firmę w Krakowie, Warszawie i Wrocławiu.

środa, 16 października 2013

Metoda Find w aplikacji MS Word- Przykład 1

Metoda .Find to jedno z bardziej przydatnych narzędzi nie tylko w Wordzie, ale także w Excelu (o czym wkrótce napiszę kilka postów). Ogólna zasada działania metody jest prosta- Znajdź >> Zaznacz. O wiele częściej sięgamy po to rozwiązanie w konfiguracji Znajdź >> Zamień. Tak czy inaczej w pracy z metodą .Find warto zawsze:

a) skorzystać z rejestratora makr
b) wykorzystać umiejętnie znaki specjalne (Wildcards) dostępne w pracy z tą metodą.

Od czasu do czasu przedstawię proste przykłady pracy z techniką Znajdź >> Zamień. Na początek następujące zadanie:

Dokument składa się z szeregu 12-to cyfrowych liczb umieszczonych w kolejnych paragrafach. Zadanie polega na wstawieniu pojedynczych cudzysłowów na początku i końcu każdego ciągu oraz na umieszczeniu wszystkich wpisów w jednej linii. Poniższy zrzut ekran pokazuje sytuację przed i po. Nieprzypadkowo na zrzucie ekranu dostrzec można również znaki podziału paragrafu, które będą kluczowe w rozwiązaniu problemu.


Podejście A. Teoretycznie możemy wykorzystać pętle i przeszukać cały dokument na okoliczność ciągów znaku uprzednio częściowo dokonawszy zamiany znaku paragrafu na przecinek. Rozwiązanie choć skuteczne to nieefektywne  i nie będzie tu prezentowane.

Podejście B. Wykorzystując dwukrotnie metodę .Find >> .Replace osiągniemy nasz cel. Proszę zwrócić uwagę na parametr .Wildcards ustawiony odmiennie w pierwszej i drugiej części kodu.

01Sub Podejście_B()
02 
03    With Selection.Find       
04        .Text = "^p"
05        .Replacement.Text = ","
06        .MatchWildcards = False
07        .Execute Replace:=wdReplaceAll
08        
09        .Text = "([0-9]{12})"
10        .MatchWildcards = True
11        .Replacement.Text = "'\1'"
12        .Execute Replace:=wdReplaceAll
13 
14    End With
15 
16End Sub

Podejście C. Kluczowe w tym wariancie jest zastąpienie symbolu znaku paragrafu. W miejsce '^p' wstawiamy inny odpowiednik nowego paragrafu wyrażony jako '^13', który nie powoduje kolizji z ustawionym naprzemiennie parametrem .Wildcards w powyższym przykładzie. Dzięki temu nasze ostateczne rozwiązanie staje się jeszcze bardziej kompaktowe.
01Sub Podejście_C()
02 
03    With Selection.Find
04        .Text = "([0-9]{10})(^13)"
05        .Replacement.Text = "'\1',"
06        .MatchWildcards = True
07        .Execute Replace:=wdReplaceAll
08    End With
09 
10End Sub

Prowadząc szkolenie z VBA zawsze staram się prezentować rozwiązania optymalne, choć często wychodzę od rozwiązań naturalnych-intuicyjnych by następnie móc zaprezentować ewolucję rozwiązania podobnie jak ma to miejsce powyżej.

czwartek, 10 października 2013

Podręczna paleta kolorów wypełnienia

Wyobraźmy sobie sytuację, w której chcemy ułatwić użytkownikowi nanoszenie kolorów w określonym obszarze arkusza. Odpowiednie kolory mogą oznaczać status danego produktu, zamówienia, tworzyć kolorystyczną informacje o postępie zdarzenia, itp. Ot np. spójrzmy na poniższy układ prostej tabeli wraz z wyświetlonym szybkim menu podręcznym przyspieszającym kolorowanie.


Poniżej w kilku krokach prezentuję technikę tworzenia niniejszego rozwiązania.

Krok 1. Procedura tworząca i wyświetlająca menu podręczne z paletą kolorystyczną. Procedura ta wykorzystuje tradycyjne techniki tworzenia menu oparte o obiekty/kolekcje CommandBar/CommandBars.
01Sub RightClickMenu_Utwórz()
02   
03    Dim tmpCB As CommandBar
04    Dim tmpCBpop As CommandBarPopup
05       
06    'na wszelki wypadek
07    RightClickMenu_Usuń
08       
09    Set tmpCB = Application.CommandBars.Add("KolorMenu", msoBarPopup, , True)
10    Set tmpCBpop = tmpCB.Controls.Add(ID:=1691)
11    tmpCB.ShowPopup
12 
13    'usuwamy po wykonaniu operacji
14    RightClickMenu_Usuń
15End Sub

Krok 2. Jak łatwo zauważyć w powyższej procedurze tworzone menu jest tymczasowe. Niezbędna jest więc procedura usuwania paska zaraz po tym gdy zostanie wykorzystany.
1Sub RightClickMenu_Usuń()
2    On Error Resume Next
3    Application.CommandBars("KolorMenu").Delete
4End Sub

Krok 3. Wykorzystamy obsługę zdarzenia BeforeRightClick w celu wyświetlenia menu tylko w kolumnie C odpowiedniego arkusza. Co ważne, poniższe rozwiązanie wyświetli nasze nowe menu po kliknięciu prawym klawiszem myszy. Nadal pozostawimy użytkownikowi dostęp do standardowego menu podręcznego o ile wybierze na klawiaturze konfigurację Shift + F10.
1Private Sub Worksheet_BeforeRightClick(ByVal Target As Range, Cancel As Boolean)
2    If Target.Column = 3 Then
3        Cancel = True
4        RightClickMenu_Utwórz
5    End If
6End Sub

I na koniec mała wskazówka- wykorzystana w kroku 1 metoda .ShowPopup posiada dodatkowe, opcjonalne parametry, które umożliwiają wskazanie miejsca, w którym ma zostać wyświetlone nasze menu podręczne.
Tworzenie interakcji i rozwiązań ułatwiających pracę z Excelem (i innymi aplikacjami Office) jest ważną umiejętnością każdego programisty co staram się często podkreślać prowadząc kurs z programowania w VBA.

piątek, 4 października 2013

Przechowywanie wartości zmiennych po zamknięciu pliku

Szukając odpowiedni na pytanie jak przechować zmienne po zamknięciu pliku przychodzi mi do głowy szereg pomysłów. Na początek wymienię kilka z nich- wykorzystując komórkę w arkuszu (tylko Excel), korzystając z właściwości dokumentu, korzystając z kolekcji Variables (tylko Word), korzystając z rejestru komputera (tylko lokalnie, nie przenoszone z plikiem).

Relatywnie ciekawą techniką jest jednak wykorzystanie instrukcji Put (dla zapisu) i Get (dla odczytu) z zapisem do pliku tekstowego. Rozwiązanie to mam kilka dodatkowych zalet:
1.  można szybko i skutecznie przechować wartości tablic, co nie jest możliwe w rozwiązaniach alternatywnych,
2. utworzony plik tekstowy sprawia wrażenie zaszyfrowanego, nie każdy będzie w stanie odczytać jego zawartość.

Skoro są zalety to i zazwyczaj istnieją wady rozwiązania. Otóż i owszem- ograniczona wielkość zmiennej- dla tablicy będzie to niewiele ponad 3000 rekordów.

Jak działa taki mechanizm? Jak wygląda kod zapisu i odczytu?

Wyobraźmy sobie taką oto sytuację, że obszar tabeli Excela A1:E7 chcemy zapisać w postaci zmiennej tablicowej i przesłać w postaci pliku tekstowego do innej osoby.


W celu zapisu posłużymy się takim poniższym przykładowym kodem. Dodatkowe parametry zostały opisane w postaci komentarzy wewnątrz kodu.

01Sub Write_Array_To_File()
02 
03    Dim Plik As String
04   
05    Dim fileNum As Integer
06   
07    Dim tblRange() As Variant
08        tblRange = Range("A1:E7").Value
09         
10'nasz plik wynikowy
11    Plik = "Array.To.File.VBA.bin"
12'gdy plik istnieje to uprzednio go skasujemy _
13wymagana obsługa błędów na okoliczność gdy plik nie istnieje
14    On Error Resume Next
15        Kill Plik
16    On Error GoTo 0
17     
18'procedura tworzenia pliku
19    fileNum = FreeFile
20   
21    Dim arrLen As Long
22'parametr długości wg pomocy z MSDN: _
23=2 + ilość wymiarów * 8 + _
24    wielkość wymiaru 1 * wielkość wymiaru 2 * ilość wymiarów
25'w praktyce wartość ta jest za mała i zwraca błąd, dlatego _
26    eksperymentalnie została dodana dodatkowa wartość
27'opcjonalnie można ustawić wartość na poziom max = 32767
28    arrLen = (2 + 2 * 8) + _
29            (UBound(tblRange, 1) * UBound(tblRange, 2) * 2) + 255
30     
31'otwieramy plik do zapisu i wywołujemy instrukcję Put
32    Open Plik For Random As #fileNum Len = arrLen
33        Put #fileNum, 1, tblRange
34    Close #fileNum
35 
36End Sub

Jeżeli operacja powiodła się w domyślnej lokalizacji znajdziemy plik o nazwie "Array.To.File.VBA.bin". Próba otwarcia tego pliku w prostych edytorach tekstu da nam następujący lub podobny efekt:




Plik jest niewielki i jednocześnie sprawia wrażenie jakoby zawartość była zaszyfrowana. Teraz możemy przesłać plik do odbiorcy.

W jaki sposób możemy odczytać nasz plik. Wystarczy uruchomić poniższą procedurę aby wartość naszej tabeli została wprowadzona do arkusza Excel począwszy od aktywnie zaznaczonej komórki. (Dodatkowe uwagi dot. kodu w postaci komentarzy poniżej)

01Sub Read_Array_From_File()
02    
03    Dim Plik As String
04    Dim fileNum As Integer
05    Dim tblRange() As Variant
06 
07'plik musi znajdować się w domyślnej lokalizacji
08    Plik = "Array.To.File.VBA.bin"
09    fileNum = FreeFile
10 
11    Dim lenAAA As Integer
12'nie znamy wielkości tabeli- ustawiamy max wartość parametru
13        lenAAA = 32767
14     
15'otwarcie pliku i odczyt zwaratości do zmiennej
16    Open Plik For Random As #fileNum Len = lenAAA
17        Get #fileNum, 1, tblRange
18    Close #fileNum
19  
20'przeniesienie tabeli do arkusza względem aktywnej komórki
21    ActiveCell.Resize(UBound(tblRange, 1), UBound(tblRange, 2)) = tblRange
22End Sub

Przydatne linki referencyjne do strony Microsoft MSDN:

Put Statement
Get Statement

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

piątek, 30 sierpnia 2013

Przeglądarka zdjęć w Excelu

W czasie dwóch prowadzonych szkoleń z VBA dla Excel w Krakowie i Wrocławiu padło pytanie o możliwość stworzenia czegoś na wzór przeglądarki zdjęć w Excelu w oparciu o nazwy plików graficznych umieszczonych w kolumnie arkusza.

Oczywiste pytanie, które również zadałem brzmiało- po co i dlaczego mielibyśmy tworzyć takie rozwiązanie? Jak się okazało, i tu będę bazował na podobnych elementach uzyskanych odpowiedzi, chodziło o szybką i skuteczną weryfikację jak wygląda dany produkt w bazie produktów danej firmy.

Poniżej przedstawiam wariant uproszczony przygotowany w trzech krokach. Wszelkie modyfikacje i rozszerzenia są jak najbardziej możliwe i zalecane.

Krok 1. Tworzymy listę plików graficznych. 
W tym celu w komórce A1 dowolnego arkusza proszę wprowadzić ścieżkę do katalogu, w którym znajdują się nasze pliki graficzne, np: c:\Users\Imię Naziwsko\Documents\Moje obrazy.

Następnie proszę wywołać poniższe makro, którego celem będzie utworzenie listy wszystkich plików graficznych JPG począwszy od rzędu 11 w kolumnie A.
01Sub ListaPlikowKatalogu_All()
02 
03    'instrukcja działa dla katalogu określonego jako bieżący
04    ChDir ActiveSheet.Cells(1, 1)
05    
06    Dim TMP As String
07    Dim Wiersz As Integer
08    Wiersz = 11
09    'inicjacja kryterium nazwy- pliki JPG
10    Cells(Wiersz, 1) = Dir("*.jpg")
11    
12    Do
13        Wiersz = Wiersz + 1
14        TMP = Dir()
15        Cells(Wiersz, 1) = TMP
16 
17    Loop While TMP <> ""
18    
19    Cells(10, 1).Select
20End Sub

Krok 2.  Wstawianie i usuwanie grafiki.
Poniższe dwa makra będą odpowiedzialne za wyświetlenie oraz usunięcie pliku graficznego z naszego arkusza. Dodatkowe informacje na temat ich działania zawarte są w komentarzach wewnątrz kodu.

01Sub Pokaz_Fote(Kolumna)
02    Dim Nazwa$, Sciezka$
03        Sciezka = ActiveSheet.Cells(1, Kolumna)
04    Nazwa = Sciezka & "\" & ActiveCell.Value
05 
06    'kasujemy wszystkie kształty-
07    'tu wywołujemy osobne makro
08    Kasuje_Wszystkie_Kształty
09    
10    'wstawianie pliku graficznego i wyświetlenie go w określonej pozycji
11    'uwaga! nie znamy proporcji grafiki a musimy podać parametry wymiarów
12    ActiveSheet.Shapes.AddPicture Nazwa, True, True, _
13                            ActiveCell.Offset(0, 1).Left, _
14                            ActiveCell.Offset(0, 1).Top, 1600, 1600
15                            
16    'dlatego tu następuje dostosowanie wymiaru i proporcji
17    With ActiveSheet.Shapes(1)
18        .LockAspectRatio = msoTrue
19        .ScaleHeight 1, msoTrue, msoScaleFromTopLeft
20        .Width = 200
21    End With
22 
23End Sub
1Sub Kasuje_Wszystkie_Kształty()
2    Dim SHP As Shape
3    For Each SHP In ActiveSheet.Shapes
4        SHP.Delete
5    Next 'SHP
6End Sub

Krok 3. Automatyczne działanie z pomocą obsługi zdarzenia.
Na koniec połączymy nasze makra ze zdarzeniem. Chcemy bowiem aby wyświetlił się nam obraz tylko wtedy, gdy zaznaczymy pojedynczą komórkę w kolumnie zawierającej nazwę pliku, a w każdym z pozostałych przypadków chcemy usunąć zbędną grafikę. W tym celu w module naszego arkusza dodamy następujący kod obsługi zdarzenia Worksheet_SelectionChange:

01Private Sub Worksheet_SelectionChange(ByVal Target As Range)
02On Error GoTo ErrorHandler
03    
04    'gdy wybrano potencjalną pozycję z listy plików graficznych
05  If Target.Row > 10 And Target.Cells.Count = 1 And _
06                UCase(Right(Target.Value, 3)) = "JPG" Then
07                'wywoływne jest makro wyświetlające kolejne wybierane pliki
08                Call Wstawianie_Zdjec.Pokaz_Fote(Target.Column)
09    
10  Else
11        Kasuje_Wszystkie_Kształty
12  End If
13Exit Sub
14ErrorHandler:
15    Debug.Print "Błąd obsługi zdarzenia o godzinie: " & Time
16End Sub

poniedziałek, 26 sierpnia 2013

Nawiasy mają znaczenie!- procedura parametryzowana

Myślę, że wielu programistów nie zdaje sobie sprawy z faktu, że wywołując zewnętrzną procedurę parametryzowaną Sub ma znaczenie to, czy parametry zostaną podane w nawiasie czy też nie. Chodzi o różnicę, którą obrazują następujące składnie wywołania:

1ProceduraParametryzowana Argument
2ProceduraParametryzowana (Argument)

Zanim wyjaśnię  różnicę i przyczynę różnicy najlepiej będzie jeżeli spojrzymy na przykład:
01Sub Foo()
02 
03    Dim STR As String
04 
05    STR = "Hello"
06 
07    Boo (STR)  'wywołanie 1
08    Debug.Print STR '>> Hello
09 
10    Boo STR   'wywołanie 2
11    Debug.Print STR '>> Hello World
12 
13End Sub
1Sub Boo(ByRef PARAM As String)
2 
3    PARAM = PARAM + " World"
4 
5End Sub

Wywołanie procedury Foo sprawi, że najpierw otrzymamy w wyniku 'Hello', a następnie uzyskamy wartość zmiennej 'Hello World". Skąd ta różnica? Otóż ujęcie argumentów procedury w nawias (wywołanie 1 powyżej) zmienia typ parametru z ByRef na ByVal, to zaś sprawia, że zmienna przekazana do podprocedury `Boo` nie zmienia swojej wartości. Przekazanie argumentu bez nawiasu sprawia, że parametr jest typu ByRef a więc wszystkie operacje na nim wykonane w podprocedurze zostaną również przekazane do procedury nadrzędnej.

Podobną różnicę w zachowaniu i wyniku wywołania podprocedury znajdziemy korzystając z instrukcji Call. W tym przypadku dodatkowy nawias będzie zmieniał tryb przekazania parametru. Obrazują to następujące przykłady:

1Call Boo(STR)   >> przekazanie ByRef >>Wynik Hello World
2Call Boo((STR)) >> przekazanie ByVal >>Wynik Hello

poniedziałek, 19 sierpnia 2013

MS Word- podmiana akcji przycisku na wstążce

Aplikacja MS Word udostępnia ciekawą i praktyczną opcję, która umożliwia zamianę standardowej akcji wywołanej dowolnym przyciskiem znajdującym się na wstążce na akcję własną użytkownika. Proces zamiany jest relatywnie łatwy do wykonania i zaprogramowania choć od razu muszę zaznaczyć rzecz ważną- podmiana ta dotyczyć będzie wskazanego szablonu lub dokumentu. Nie zmienia ona akcji danego przycisku dla wszystkich dokumentów. Tak czy inaczej rozwiązanie to znajduje szereg praktycznych i przydatnych zastosowań. Żałuję też jednocześnie, że podobnych rozwiązań nie da się zrealizować we wszystkich aplikacjach MS Office.

Aby osiągnąć cel opisany w powyższym wprowadzeniu musimy wykonać kilka kroków i operacji. Całość zaprezentuję na bazie aktualizacji działania przycisku Wersja Robocza w zakładce Widok aplikacji MS Word 2010 (przycisk zaznaczony na poniższym zrzucie ekranu).



Krok 1. Otwieramy dokument, w którym chcemy zapisać zindywidualizowane zachowanie przycisku.

Krok 2. Przechodzimy do zakładki i wywołujemy polecenie: Widok >> Makra >> Wyświetl makra.

Krok 3. W wyświetlonym oknie Makra, w jego środkowej części z listy rozwijalnej Makra w: wybieramy opcję: Polecenia programu Word.

Krok 4. Chyba najtrudniejszy z etapów- na otrzymanej liście makr musimy odnaleźć i zaznaczyć to makro, które (wydaje się nam) powiązane jest z danym przyciskiem. Czym się kierować w poszukiwaniach- intuicją i logiką opartą o nazwy zakładek, nazwę grupy poleceń czy wreszcie nazwę naszego przycisku, który modyfikujemy.

W naszym przykładzie od razu trafimy na pewną trudność- otóż przycisk Wersja robocza nie będzie reprezentowany przez makro ViewDraft  lecz makro ViewNormal. Aby się przekonać czy dokonaliśmy właściwego wyboru możliwe, że będziemy musieli wykonać i powtórzyć kilka kolejnych kroków.

Proszę pamiętać, aby zaznaczyć wybrane makro i nie zmieniać tego zaznaczenia!

Krok 5. W naszym oknie Makra na liście Makra w: dokonujemy ponownej zmiany- tym razem wskazujemy tam plik Worda, z którym chcemy powiązać indywidualne ustawienia przycisku.

Wskazówka! jeżeli w tym kroku zamiast pliku wskażemy odpowiedni  szablon, np. Normal.Dotm, to nasza zmiana obsługi przycisku powiązana zostanie z tym szablonem a w konsekwencji ze wszystkimi dokumentami, które na bazie szablonu powstaną.

Krok 6. Klikamy w przycisk Utwórz w wyniku czego zostaniemy przeniesieni do edytora VBA w obszar utworzonego makra (proszę zwrócić uwagę na lokalizację kodu- znajduje się on w module w naszym dokumencie). W tym konkretnym przypadku makro powinno mieć następującą postać:
01Sub ViewNormal()
02    '
03    ' ViewNormal Makro
04    ' Zmienia widok edycji na normalny
05    '
06    If ActiveWindow.View.SplitSpecial = wdPaneNone Then
07        ActiveWindow.ActivePane.View.Type = wdNormalView
08    Else 
09        ActiveWindow.View.Type = wdNormalView
10 
11    End If
12 
13End Sub

Krok 7. W tym momencie każda modyfikacja, która zostanie dokonana w otrzymanym makrze zostanie jednocześnie powiązana z przyciskiem 'Wersja robocza'. Przekonajmy się o tym dodając do naszego kodu proste polecenie:

1MsgBox "Dokonano zmiany działania przycisku"

A następnie proszę przejść do aplikacji MS Word i wcisnąć przycisk, którego kod poddaliśmy modyfikacji.

Krok 8. Proszę pamiętać o zapisaniu naszego pliku w wariancie z obsługą makr, a więc rozszerzeniem DOCM.



poniedziałek, 12 sierpnia 2013

Zapisanie zakresu komórek arkusza w postaci pliku JPG

Niniejsze rozwiązanie znane jest wielu praktykom VBA, szczególnie tym osobom, które wysyłają  fragmenty arkusza w postaci grafiki wstawionej w wiadomości e-mail.

Jak wyglądać będzie makro, którego zadaniem będzie zapisanie utworzenie pliku graficznego JPG prezentującego fragment obszaru arkusza? Pełne rozwiązanie poniżej. W tym wypadku wszystkie dodatkowe komentarze zostały umieszczone poniżej.

01Sub SaveRangeAsJPG()
02 
03Dim SHT As Worksheet
04Dim RNG As Range
05 
06    Application.ScreenUpdating = False
07    
08    'określamy arkusz z naszym obszare
09    Set SHT = Sheets("Arkusz1")
10 
11    'określamy zakres, który będziemy eksportować...
12    Set RNG = SHT.Range("A1:D5")
13    '... kopiujemy ten obszar
14    RNG.CopyPicture Appearance:=xlScreen, Format:=xlPicture
15 
16    'cały trick kryje się w następujących krokach:
17        '1. tworzymy wykres- proszę zwrócić uwagę na _
18        szerokość i wysokość wykresu- taki zapis pozawala _
19        zachować proporcje naszego obszaru
20    With SHT.ChartObjects.Add(Left:=100, Top:=100, _
21                        Width:=RNG.Width, Height:=RNG.Height)
22        With .Chart
23            'wklejamy w wykres nasze komórki
24            .Paste
25            
26            'wykorzystujemy metodę export wykresu
27            .Export ThisWorkbook.Path & "\TabelaExport.jpg"
28        End With
29        
30        'dla porządku usuwamy wykres- nie jest nam już potrzebny
31        .Delete
32 
33    End With
34 
35End Sub

I jeszcze dwa obrazy graficzne na koniec- zrzut ekranu z naszą tabelą, która została poddana procesowi exportu oraz plik, który powstał w wyniku działania powyższego makra.









poniedziałek, 5 sierpnia 2013

Testowanie zgodności ciągów tekstowych- wyrażenia regularne RegExp- 4/4

W ostatnim wpisie dot. wyrażeń regularnych RegExp chciałbym zwrócić uwagę metodę .Execute, którą wykorzystałem poprzednio w celu odnalezienia i pobrania adresu e-mail z podanego ciągu tekstowego.

Metoda .Execute zwraca kolekcję Matches Collection, która zawiera wszystkie wystąpienia ciągów tekstowych spełniających określone kryteria.  W tej sytuacji w relatywnie łatwy sposób możemy pobrać elementy odpowiadające naszemu wzorcowi.

Prześledźmy to na jednym tylko przykładzie, którego zadaniem będzie pobranie wszystkich adresów e-mail z podanego ciągu tekstowego. W zakresie dalszego zrozumienia zastosowania kolekcji Matches odsyłam do komentarzy w poniższym kodzie.

01Sub RegExp_pobranie_wybranych()
02 
03    'proszę pamiętać o referencji do biblioteki RegExp
04    Dim objRE As New RegExp
05    Dim Tekst As String
06    Dim Wynik As Variant
07    
08    'nasz przeszukiwany tekst- zawiera 3 e-maile w różnych zapisach
09    Tekst = "Tekst zawiera e-maile adres.email@domena.com.pl w treści. " & _
10            "E-maile jan.kowalski@domena.pl są na różnych pozycjach. " & _
11            "regexp_cool@domena.com oraz posiadają różną konstrukcję."
12    
13    With objRE
14        .Global = True
15        .Pattern = _
16        "([A-Za-z0-9_\.-]+)@([A-Za-z0-9_\.-]+[A-Za-z0-9_][A-Za-z0-9_])"
17        If .test(Tekst) Then
18            'Ważne! nasz wynik jest obiektem typu Match,
19            'wymaga użycia instrukcji Set!
20            Set Wynik = .Execute(Tekst)
21        End If
22    End With
23    
24    'zwracamy wynik- wszystkie pobrane e-maile
25    Dim EMail As Match
26    For Each EMail In Wynik
27        Debug.Print EMail.Value
28    Next
29    'inny sposób na zwrócenie odnalezionych e-maili
30        Debug.Print Wynik(0).Value
31        Debug.Print Wynik(1).Value
32        Debug.Print Wynik(2).Value
33    
34    'dodatkowo możemy pobrać także inne parametry
35    'odnalezionych fragmentów tekstu
36    
37    For Each EMail In Wynik
38        'adres e-mail, pozycja w tekście, długość tekstu e-maila
39        Debug.Print EMail.Value, EMail.FirstIndex, EMail.Length
40    Next
41End Sub

środa, 31 lipca 2013

Testowanie zgodności ciągów tekstowych- wyrażenia regularne RegExp- 3/4

Zanim uruchomimy pierwszą pełną procedurę z wykorzystaniem techniki wyrażeń regularnych (RegExp) przyjrzyjmy się dodatkowo właściwościom obiektu RegExp

.Pattern       pozwoli nam zdefiniować wzorzec porównawczy,
.Global        określa, czy wzorzec dotyczy pierwszego (= False), 
               czy wszystkich wystąpień w tekście (= True),
.IgnoreCase    określa sposób testowania wyrażenia ze względu na wielkość liter (domyślnie =False,
               wyszukiwanie binarne uwzględniające wielkość liter),

oraz metodom obiektu Regexp:
.Test       zwraca informację czy można dopasować wyrażenie do ciągu tekstowego,
.Replace    dokonuje zamiany fragmentu wzorcowego na nowy wskazany ,
.Execute    wykonuje operację zwracając obiekt Match z kolekcji MatchCollection.

W dużym skrócie podsumowując powyższe elementy obiektu  RegExp określę to w sposób następujący- pozwalają one na wskazanie wzorca a następnie dokonanie zamiany fragmentu tekstu na inny lub pobranie poszczególnych elementów w całości lub dla pierwszego wystąpienia.

Czas na zebranie całości zagadnienia w konkretne rozwiązania i prezentację przykładowych pełnych konstrukcji kodu VBA. Wykonując poniższe operacje w środowisku VBA proszę nie zapomnieć o Kroku 1, a więc ustanowieniu referencji do biblioteki RegExp.

Przykład 1. zamiana wyrazów w ciągu tekstowy, wyraz 'jest' na 'był'

01Dim objRE As New RegExp
02Dim Tekst As String
03Dim Wynik As Variant
04 
05Tekst = "To jest 123 tekst 45! 6 Zawiera też 789 liczby 0."
06With objRE
07    .IgnoreCase = True
08    .Global = True
09    .Pattern = "jest"
10    Wynik = .Replace(Tekst, "był")
11End With
12Debug.Print Wynik    '>> To był 123 tekst 45! 6 Zawiera też 789 liczby 0.

Przykład 2. z podanego ciągu tekstowego pozostawić tylko cyfry
 
01Dim objRE As New RegExp
02Dim Tekst As String
03Dim Wynik As Variant
04 
05Tekst = "To jest 123 tekst 45! 6 Zawiera też 789 liczby 0."
06With objRE
07    .Global = True
08    .Pattern = "[a-żA-Ż!?.]*\s*"
09    Wynik = .Replace(Tekst, vbNullString)
10End With
11Debug.Print Wynik    '>> 1234567890

Przykład 3. z podanego ciągu tekstowego usunąć wszystkie liczby

01Dim objRE As New RegExp
02Dim Tekst As String
03Dim Wynik As Variant
04 
05Tekst = "To jest 123 tekst 45! 6 Zawiera też 789 liczby 0."
06With objRE
07    .Global = True
08    .Pattern = "\d"
09    Wynik = .Replace(Tekst, vbNullString)
10End With
11Debug.Print Wynik    '>> To jest  tekst !  Zawiera też  liczby

Przykład 4. W przykładzie nr 2 dodatkowo pozostały podwójne spacje oraz zbędna spacja przed kropką i wykrzyknikiem. To wszystko także chcemy usunąć i wyprostować. W tej sytuacji utworzymy kaskadowy mechanizm RegExp:
01Dim objRE As New RegExp
02Dim Tekst As String
03Dim Wynik As Variant
04 
05Tekst = "To jest 123 tekst 45! 6 Zawiera też 789 liczby 0."
06With objRE
07    .Global = True
08'usuwamy liczby
09    .Pattern = "\d"
10    Wynik = .Replace(Tekst, vbNullString)
11'usuwamy spacje podwójne
12    .Pattern = "\s{2,}"
13    Wynik = .Replace(Wynik, " ")
14'usuwamy spację przed znakami specjalnymi...
15    .Pattern = "(\s)([.!?])"
16'...stosując technikę zamiany
17    Wynik = .Replace(Wynik, "$2")
18End With
19Debug.Print Wynik    '>>To jest tekst! Zawiera też liczby.

Przykład 5. W powyższym przykładzie zastosowana została technika zamiany kolejności wyrazów. Jest to jedna z wielu dodatkowych korzyści pracy z RegExp. Poniżej prosty przykład, który zamienia kolejność 3 pierwszych wyrazów podanego tekstu:
01Dim objRE As New RegExp
02Dim Tekst As String
03Dim Wynik As Variant
04 
05Tekst = "To jest 123 tekst 45! 6 Zawiera też 789 liczby 0."
06With objRE
07'ważne ustawienie dot. właściwości Global
08    .Global = False 
09    .IgnoreCase = True
10    .Pattern = "(\S+)(\s+)(\S+)(\s+)(\S+)*"
11    Wynik = .Replace(Tekst, "$5$4$3$2$1")
12End With
13Debug.Print Wynik   '>>123 jest To tekst 45! 6 Zawiera też 789 liczby 0.

Przykład 6. Z podanego tekstu chcemy pobrać tylko adres e-mai. W tym celu wykorzystamy jeden z wielu możliwych wzorców odpowiadających adresowi e-mail i stworzymy następujący kod:
01Dim objRE As New RegExp
02Dim Tekst As String
03Dim Wynik As Variant
04 
05Tekst = "Ten tekst zawiera email gdzieś adres.email@domena.com.pl w swojej treści."
06With objRE
07    .Global = True
08    .Pattern = _
09    "([A-Za-z0-9_\.-]+)@([A-Za-z0-9_\.-]+[A-Za-z0-9_][A-Za-z0-9_])"
10    If .test(Tekst) Then
11        Wynik = .Execute(Tekst)(0).Value
12    End If
13End With
14Debug.Print Wynik

Powyższy przykład zamiast metody .Replace wykorzystuje metodę .Execute. Ale o tym napiszę już w kolejnym wpisie za kilka dni...


Osobom, które są zainteresowane wyrażeniami regularnymi polecam poniższe linki, które pozwolą na poszerzenie i ugruntowanie informacji przedstawionych w ostatnich postach. Linki te poświęcone są ogólni RegExp i niekoniecznie wskazują na bezpośrednie wykorzystanie tej techniki w VBA.

Korzystanie z wyrażeń regularnych w programie Microsoft Visual Basic 6.0Regular-Expressions.Info
.NET Framework Regular Expressions
RegExp Help
Regular-Expressions.info
Introduction to Regular Expressions

poniedziałek, 22 lipca 2013

Testowanie zgodności ciągów tekstowych- wyrażenia regularne RegExp- 2/4

Kontynuując temat tworzenia wyrażeń regularnych RegExp omówię najważniejszą umiejętność związaną z tą techniką, a więc budowanie wzorców porównawczych.
 

Krok 2.
To najtrudniejsza część pracy z RegExp- budowa składni tworzącej kryteria. Spojrzymy na to zadanie najpierw przez pryzmat wybranych znaków (kodów) specjalnych, następnie przedstawię proste przykłady, a na koniec zaprezentuję kilka złożonych wzorców.

A) Znaki i symbole specjalne
\d    cyfra
\D    nie cyfra
\s    spacja
\S    nie spacja
\w    znak alfanumeryczny, litera
\W    nie znak alfanumeryczny
\b    granica słowa, pozycja między słowem a spacją
\n    znak nowej linii (vbCrLf, Chr(10), Chr(13), vbNewLine)
\r    znak nowej linii (Chr(13))
\t    znak tabulacji (vbTab)
.     (kropka) dowolny znak z wyjątkiem znaku nowej linii
*     powtórzenie symbolu dowolną ilość lub zero razy
?     powtórzenie symbolu zero lub jeden raz
+     powtórzenie poprzedniego wyrażenia co najmniej raz
\     używany jako znak wyjścia dla znaków specjalnych
^     na początku tekstu
      także symbol zaprzeczenia(patrz poniżej)
$     na końcu tekstu
[]    przedział znaków
      [A-Z]  dowolna wielka litera
      [a-z]  dowolna mała litera
      [0-9]  dowolna cyfra
      [^0-9] nie zawiera cyfry
{}    wskazuje ilość dopuszczalnych powtórzeń: 
      a{1}   jednokrotne powtórzenie litery a
      a{0,2} zero do dwóch powtórzeń litery a
()    grupowanie
|     alternatywa dla grupowania

Proszę jednocześnie pamiętać, że dla prostych wyrażeń bez udziału powtórzeń i znaków specjalnych zastosujemy technikę podobną do tej opisanej przy pracy z operatorem Like w jednym z wcześniejszych postów. RegExp jest także, co do zasady, wrażliwy na wielkość liter

Spójrzmy na przykłady:
RegExp Pattern   Dopasowanie    Wyjaśnienie (opcjonalne)
           (czerwonym-dopasowanie)
abc              abcabcabc
234              1234567
Jan              Jan
                 Janek
                 janek          wielkość liter ma znaczenie
ab*c             acde
                 abcde
                 abbbbcde
ab?c             acde
                 abcde
                 abbbcde        brak dopasowania
ab+c             acde           brak dopasowania
                 abcde
                 abbbcde
ab.d             abcd
                 ab1d
                 abc1d          brak dopasowania 
a.*d             ad
                 abcd
                 asfas13wbd
\d\d             10
                 99
                 100
                 9999           dwa podwójne układy cyfr
\w+              alfa           dowolny, każdy wyraz
                 Witaj Jan! 
\w+@\w+          a@a            uproszczony schemat dla e-maila
                 email@domena.pl
\w+@\w+.\w+      email@domena.pl
                 jan.kowalski@domena.pl
^ab.*c           abc
                 ab123fdcdef
                 babc           tekst nie zaczyna się od a
^ab.*c$          abc
                 ab123c
                 ab123cd        nie kończy się na c
                 ab123cdc       powtórzenie c nie ma znaczenia
                 bacdc          nie zaczyna się od a
^\d              1abc           zaczyna się od cyfry
\d$              abc1           kończy się cyfrą
[abc]            abc            znaki wskazane w nawiasie
                 abcdef
                 abcdefabc   
[^abc]           abcdefabc      nie a, nie b, nie c     

[A-Z]            abcDEF         wielkie litery
                 ABCDEF
                 abcdef
^[A-Z].+         Abc123d        zaczyna się od wielkiej litery
                 abc123d
[+*?.]           ..??**++       nawiasy likwidują znaczenie znaków specjalnych
^[1-9][0-9]*$    1              liczba całkowita większa od zera
                 1000
                 -1000
                 123.45         tylko liczba całkowita
(ab)+            ab
                 ababab
                 bababa
(ab|ba)+         ab
                 ba
                 ababab         rozpozna jako jeden ciąg
                 bacbacab       rozpozna trzy ciągi
                 abcdef
(abc)?           abc            ale także dla pustego ciągu
a(\d+)c          a123c          zawiera cyfry pomiędzy a i c
                 a1c
                 ac
                 abc  
a{2}             alfaabeta
a{1,2}           alfaabeta

Inne gotowe przykłady z tego źródła:
A) weryfikacja, czy podano prawidłowy e-mail:
^[A-Za-z0-9_\.-]+@[A-Za-z0-9_\.-]+[A-Za-z0-9_][A-Za-z0-9_]$

Wskazówka! w sieci znaleźć można wiele przykładów weryfikacji adresów e-mail, kolejny z nich podam w trzeciej lekcji poświęconej RegExp. Zainteresowanych tym zagadnieniem odsyłam do tej dyskusji.

B) weryfikacja zapisu daty w układzie: yyyy-MMM -dd, gdzie MMM to trzyliterowy skrót zapisany z wielkiej litery:
^\d\d\d\d-[A-Z][a-z][a-z]-\d\d$

C)  weryfikacja daty jak powyżej rozszerzona, sprawdzamy, czy miesiąc podany w języku polskim lub angielskim:
^20\d\d-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(0[1-9]|[1-2][0-9]|3[01])$ 

^20\d\d-(Sty|Lut|Mar|Kwi|Maj|Cze|Lip|Sie|Wrz|Paź|Lis|Gru)-(0[1-9]|[1-2][0-9]|3[01])$