Gdy tworzymy aplikacje okienkowe w .NET z użyciem Windows Forms, możemy korzystać z mnóstwa (co najmniej kilkudziesięciu) typów kontrolek dostępnych out-of-the-box. Nie wszystkie nawet mają rację bytu jako rzeczywiste kontrolki, ale w wielu przypadkach fakt, że nimi są, ułatwia korzystanie z API, które się kryje za taki komponentami. (Przykładem jest kontrolka BackgroundWorker).
Nie oznacza to jednak, że nie istnieje czasami potrzeba stworzenia własnego rodzaju kontrolki, specyficznej dla pisanego programu. Nawet jeśli miałaby ona być użyta tylko w jednym egzemplarzu, wyodrębnienie jej logiki poza zasadniczy kod aplikacji powinno wyjść im obu na dobre. W tym sensie jest ten rodzaj dodatkowej warstwy abstrakcji, który zazwyczaj opłaca się stworzyć.
Jak to się robi? Przede wszystkim należy wiedzieć, że w .NET kontrolki definiowane przez programistę mogą być jednego z trzech rodzajów. Stosunkowo najprostszym jest user control, które to stanowi po prostu pojemnik na kilka innych kontrolek zebranych w całość. Mogą to być na przykład dwa obiekty typu
NumericUpDown
opatrzone etykietami “Od” i “Do”, jeśli w naszym programie wielokrotnie i w różnych miejscach zachodzi potrzeba podawania przedziału liczbowego. Właściwości takiej customowej kontrolki są najczęściej bezpośrednio mapowane na właściwości kontrolek składowych, a zdarzenia są sygnalizowane w odpowiedzi na niektóre ze zdarzeń pochodzących z tychże kontrolek. W sumie więc można powiedzieć, że trochę tak jak funkcje i klasy pozwalają na wielokrotne wykorzystywanie kodu, tak user controls dają możliwość ponownego wykorzystania pewnych fragmentów UI.
Drugim typem są kontrolki dziedziczone (inherited controls). Nazwa wskazuje, że powstają one przez odziedziczenie już istniejącego rodzaju (klasy) kontrolki i dodanie do niej lub zmianę funkcjonalności. Może to obejmować dodanie nowych właściwości, częściowego lub całkowitego obsłużenia niektórych zdarzeń i inne tego typu modyfikacje zachowania kontrolki. Ważne jest tutaj to, iż efekt, jaki chcemy osiągnąć, jest na tyle zbliżony do jednej z istniejących kontrolek, że można ją wykorzystać i nie zaczynać od zera.
No ale nie zawsze tak jest i czasem naprawdę trzeba zacząć od podstaw. Jeżeli bazujemy na zupełnie abstrakcyjnym typie z góry hierarchii interfejsu – jak Control
czy Component
– to mamy do czynienia z kontrolką typu owner-draw. Dla niej musimy ręcznie rysować całą zawartość przy pomocy instrukcji GDI+ (stąd nazwa), opierając się przy tym na prostych zdarzeniach w rodzaju kliknięć myszy. Nie jest to więc łatwe i szybkie do wykonania zadanie.
Bywa aczkolwiek i tak, że jest ono koniecznie. Zanim się do niego zabierzemy, lepiej jednak przejrzeć alternatywy w postaci kontrolek odziedziczonych lub user controls. Możliwe, że w ten sposób zaoszczędzimy sobie pracy.
Zdarza się, że pracuje nad złożonym systemem, na który składa się kilka osobnych projektów. IDE znają dobrze takie przypadki i potrafią je obsługiwać – stąd chociażby pojęcie solution (dawniej workspace) w Visual Studio. Dla pojedynczych aplikacji i bibliotek wydaje się ono zbędne, jednak staje się nieodzowne wtedy, gdy nasze projekty zależą od siebie.
Typowa sytuacja to wspólna biblioteka (framework, engine czy co jeszcze kto woli) rozwijana razem z programami, które z niej korzystają. (W najprostszym przypadku to może być po prostu jakaś aplikacja testowa). Wówczas pojawiają się zależności między projektami na etapie ich budowania: wynik szeroko pojętej “kompilacji” jednego jest wejściem do procesu budowania innego. Jeśli nie poświęcimy temu faktowi należytej uwagi, to mogą nas czekać kłopoty. W najlepszym razie jest to konieczność wciskania F7 (Build Solution) więcej niż raz, aż do zbudowania wszystkich projektów. W gorszym – uruchamianie (i debugowanie!) aplikacji korzystającej z nieaktualnej, bo nieprzekompilowanej wersji biblioteki.
Zależności między projektami w procesie budowania da się na szczęście określić. W Visual Studio służy do tego opcja Project Dependencies z menu – a jakże – Project. Możemy w niej określić dla każdego projektu, z jakimi innymi projektami z tego samego solution jest on powiązany, czyli które z nich powinny być już wcześniej od niego zbudowane. Na podstawie tak podanej sieci zależności da się następnie określić właściwą kolejności “kompilacji” dla wszystkich projektów w danym solution. VS oczywiście to czyni, używając do tego zapewne sortowania topologicznego w analogiczny sposób jak dla kompilacji jednego projektu składającego się z wielu plików.
Pakiet Visual Studio to nie tylko samo środowisko programistyczne (IDE), ale i kilka mniejszych narzędzi. Większość z nich dotyczy albo programowania na platformy mobilne, albo .NET, ale jest przynajmniej jedno, które może okazać się przydatne dla każdego programisty Windows. Chodzi o program Spy++ (spyxx.exe).
Co ta aplikacja potrafi? Otóż pozwala ona na podgląd różnych obiektów w systemie, tj. okien, wątków i procesów wraz z ich właściwościami (jak na przykład wartości uchwytów czy ilość zaalokowanych bajtów). Na pierwszy rzut oka może nie wydawać się to jakoś wybitnie zachwycające, jako że zwykły Menedżer zadań potrafi (prawie) to samo, z dokładnością do mniejszej liczby szczegółów.
Wyróżniającym i znacznie przydatniejszym feature’em Spy++ jest bowiem co innego. Umożliwia on mianowicie podglądanie, filtrowanie i logowanie wszystkich lub wybranych komunikatów Windows (
WM_PAINT
, WM_LBUTTONDOWN
, itd.) dochodzących do wskazanego okna lub grupy okien, wraz z ich parametrami (wParam
, lParam
) oraz wartościami zwróconymi przez okno w odpowiedzi na nie.
Działa to przy tym prosto i intuicyjnie. Najpierw wybieramy sobie okno do podglądania (Spy > Log Messages lub Spy > Find Window), co możemy zrobić przy pomocy przeciągnięcia celownika myszą w jego obszar. Potem możemy określić, jakiego rodzaju komunikaty potrzebujemy przechwytywać oraz jakie informacje chcemy z nich wyciągnąć. Wynikiem będzie log mniej więcej takiej postaci:
<00694> 0002009C P WM_MOUSEMOVE fwKeys:0000 xPos:51 yPos:259
<00695> 0002009C P WM_MOUSELEAVE
<00696> 0002009C P WM_PAINT hdc:00000000
<00697> 0002009C P WM_TIMER wTimerID:5 tmprc:00000000
<00698> 0002009C P WM_TIMER wTimerID:2 tmprc:00000000
która to, jak sądzę, powinna być zrozumiała dla każdego średnio zaawansowanego programisty Windows :]
Po co nam jednak coś takiego?… Ano odpowiedź jest prosta: do debugowania :) Można oczywiście podchodzić do tego w “tradycyjny” sposób przy pomocy pracy krokowej tudzież breakpointów, ale często ogranicza nas tutaj swego rodzaju zasada nieoznaczoności, gdy samo debugowanie zmienia działanie programu – chociażby ze względu na ciągłe przełączenia między nim a IDE. To oczywiście znacznie utrudnia wykrycie i poprawienie usterek.
Jak nietrudno się domyślić, istnieją też inne programy oferujące podobną funkcjonalność co Spy++, jak np. Winspector. Z Visual Studio otrzymujemy jednak dobre narzędzie out-of-the-box, więc czegóż można by chcieć więcej? ;]
Hmm… pewnie tego, by dowiedzieć się mniej więcej, jak ono działa. O tym można przeczytać na blogu jego autora.
Tak, to stuprocentowa prawda – większość tych ryb w hodowlach w Chile jest dotknięta tą chorobą. Efektem tego będzie na pewno wzrost cen łososia również w sklepach.
No dobrze, ale co z tego – a dokładniej, czemu o tym piszę tutaj (bo fakt, że lubię dania z tych ryb pewnie nie jest wystarczającym uzasadnieniem ;])?… Otóż tytuł tej notki to doskonały przykład informacji, która jest prawdziwa, dokładna, a jednocześnie całkiem nieprzydatna i wywołująca tylko zdziwienie u odbiorcy.
To zupełnie tak, jak z niektórymi… komunikatami o błędach, produkowanymi przez kompilator C++ pracujący w ramach Visual Studio. Z punktu widzenia analizy kodu przez tenże kompilator mają one sens, jednak dla czytającego je programisty często mówią tyle co nic.
Z czasem aczkolwiek można nabrać wprawy i zacząć domyślać się, co tak naprawdę kompilator “miał na myśli”, produkując tę czy inną wiadomość o błędzie. To jednak wciąż wybitnie niepraktyczne i dlatego postaram się dzisiaj pomóc w tej kwestii, wyjaśniając prawdziwe znaczenie niektórych komunikatów wypluwanych przez Visual C++:
else
to często efekt postawienia o jednego średnika za dużo – mianowicie średnika po bloku if
. Jeśli używamy makr do zastępowania powtarzających się fragmentów kodu, to może tu chodzić o niepoprawne zdefiniowanie takiego makra.->
, .
(kropką) lub ::
. Niekoniecznie musi to znaczyć, że została ona źle zadeklarowana (i nie jest klasą/strukturą/unią) – najczęściej po prostu nie została zadeklarowana w ogóle.switch
(takie, które zawierają deklaracje nowych zmiennych). Bardzo podobny błąd dotyczy też używania etykiet i instrukcji goto
, które, jak wiadomo, są złem potwornym (w tym przypadku żartuję oczywiście).w
)string
. Nie jest to możliwe – ze względu na obecność konstruktora kopiującego – i choć teoretycznie możliwe jest obejście tego faktu, stosowanie go jest bardzo, bardzo złym pomysłem. Lepiej po prostu przymknąć oko na “marnotrawstwo pamięci” i zmienić unię w strukturę.Nie jest oczywiście możliwe wyliczenie wszystkich okoliczności, w których może objawić się każdy możliwy błąd. Powyższa lista jest więc trochę subiektywna w tym sensie, że zawiera tylko te pozycje, które przytrafiły mi się osobiście podczas kodowania. Bardziej doświadczeni programiści pewnie mogliby ją znacznie poszerzyć.
Debugując program w pracy krokowej w Visual Studio, możemy podglądać wartości wszystkich zmiennych, jakie są dostępne w zasięgu punktu wykonania. Jest to możliwe za pośrednictwem okienka tzw. czujek (watches). Automatycznie wypełnia się ono zmiennymi lokalnymi oraz ewentualnym wskaźnikiem this
, pozwalającym podejrzeć wartości pól obiektu, jeśli znajdujemy się akurat w jego metodzie.
Jednak oprócz rzeczywistych zmiennych możemy śledzić też pewne specjalne “pseudozmienne”, udostępniane przez debuger VS. Mają one nazwy zaczynające się od znaku dolara $ i potrafią być bardzo przydatne, co pokażę na przykładzie.
Powiedzmy, że mamy kod składający się z wielu następujących po sobie wywołań funkcji bibliotecznych. Każda z nich zwraca rezultat liczbowy, informujący o (nie)powodzeniu operacji, którego jednak w kodzie nie sprawdzamy – pewnie dlatego, że nie chciało nam się pisać tych wszystkich if
ów :) Taka sytuacja może wystąpić chociażby przy ciągu wywołaniach DirectX: ogromna większość funkcji z tej biblioteki zwraca wynik typu HRESULT
, który jest liczba zawierającą kod błędu, jeśli takowy wystąpił.
Zauważamy teraz, że ów ciąg wywołań najpewniej zawiera jakiś błąd. Objawem w DX może być chociażby słynne “nic nie widać” :) Jak teraz dojść, które z wielu wywołań zwraca błąd, jeśli w żadnym z nich nie tylko nie sprawdzamy wyniku, ale wręcz w ogóle go nie zapisujemy?…
Istnieje na szczęście proste i wygodne rozwiązanie, które nie wymaga żadnych zmian w kodzie. Otóż przy pomocy debugera VS i jego pseudozmiennych możemy podejrzeć wartość rejestrów procesora. Wystarczy użyć symbolu $nazwa_rejestru
, jak np. $eax
. I właśnie $eax
jest tutaj świetnym przykładem, bo rozwiązuje nasz problem. Otóż przez rejestr EAX przekazywany jest zawsze 32-bitowy rezultat zwracany przez każdą porządną funkcję. Przechodząc więc krokowo po naszym kodzie możemy sprawdzić, jak zmienia się ten rejestr po każdym wywołaniu, podglądając w ten sposób wartość, jaką zwróciła w nim funkcja. Wystarczy więc tylko znaleźć to, w którym rezultat informuje o niepowodzeniu… i voila – mamy nasz błąd :)
Wbudowany w Visual Studio system autouzupełniania kodu w przypadku C++ często działa całkiem dobrze, ale wiele osób pewnie stwierdziłoby, że częściej nie działa w ogóle. Każdy na pewno spotkał się z sytuacją, że po wpisaniu operatora kropki czy strzałki (->
) jego oczom nie ukazywała się wcale lista składników klasy, a jedynie mało pomocny komunikat na pasku stanu. Sugeruje on zapoznanie się z określonym artykułem w MSDN, traktującym o rozwiązywaniu problemów z IntelliSense.
Przyznajmy teraz: czy ktoś rzeczony rzeczywiście artykuł przeczytał? :) Mam co do tego spore wątpliwości, lecz od razu zapewniam: nic straconego. Nie ma tam bowiem nic specjalnie użytecznego ;> Nie istnieje niestety żaden specjalny trik, który sprawiłby, aby mechanizm autouzupełniania w VS dla kodu C++ działał co najmniej w połowie tak dobrze jak chociażby dla C#. Jeśli więc chcemy, by był on dla nas choć trochę użyteczny, musimy sami mu pomóc. Jak?
#define
‘y) wpływają na sposób, w jaki jest on przetwarzany. Przykładem może być dołączenie windows.h raz normalnie, a drugi raz z uprzednio zdefiniowanym makrem WIN32_LEAN_AND_MEAN
.Nie ma oczywiście gwarancji, że powyższe kroki sprawią, że IntelliSense poradzi sobie z podpowiadaniem w każdej sytuacji. Kod C++, zwłaszcza skomplikowany i korzystający z wielu “sztuczek językowych” (jak np. bibliotek Boost) nie jest łatwy do programowej analizy. Pozostaje więc mieć nadzieję, że w przyszłych wersjach VS z autouzupełnianiem będzie już lepiej; podobno w wersji 2010 jest z tym już całkiem nieźle :)
Zorientowanie się w dużym pliku z kodem (gdzie przez ‘duży’ rozumiem przynajmniej taki, który przekracza tysiąc linii) może niekiedy przysparzać kłopotów. W IDE są oczywiście narzędzia nawigacyjne, pozwalające na przejście do poszczególnych klas, metod czy deklaracji, o ile tylko znamy chociaż ich nazwy. Nie zawsze jednak tak jest. Jeśli o danej metodzie pamiętamy tylko to, że “była długa i skomplikowana”, a o jakiejś właściwości jedynie tyle, iż “jest gdzieś wśród parunastu innych deklaracji”, to najpewniej oznacza, że w ich poszukiwaniu będziemy musieli przeglądnąć cały plik od początku do końca.
Chyba że… No właśnie – chyba że dałoby się spojrzeć na kod z daleka, by zobaczyć jego ogólną strukturę. Wiadomo bowiem, że metoda “długa i skomplikowana” będzie miała najpewniej sporą ilość wcięć, a długi ciąg deklaracji, jedna pod drugą, też były łatwy do odróżnienia od innych kawałków kodu. W książce o wiele mówiącej nazwie Czytanie kodu znalazłem kiedyś radę, że do uzyskania takiego ogólnego spojrzenia można wykorzystać edytor tekstu typu Word, pozwalający na podgląd wydruku wielu stron naraz.
O wiele wygodniej byłoby jednak mieć podobną możliwość wprost w IDE. NetBeans posiada namiastkę czegoś takiego, jednak za jej pomocą można tylko szybko stwierdzić, gdzie w kodzie znajdują się błędy kompilacji (pisałem zresztą o tym trochę ponad rok temu). Porządną, wielkoskalową, a w dodatku całkiem funkcjonalną “mapę kodu” da się za to znaleźć w… Visual Studio.
Mówię tu o darmowym pluginie o nazwie RockScroll, będącym zresztą początkowo wewnętrznym narzędziem Microsoftu. Tym, co wtyczka ta robi, jest zastąpienie standardowego pionowego paska przewijania przez szerszy pionowy pasek, pokazujący podgląd aktualnie edytowanego w postaci długiej “miniaturki” z kolorowaną składnią. RockScroll działa przy tym podobnie jak zwyczajny pasek przewijania, a więc pozwala na przejście kliknięciem do wybranego miejsca w pliku. Ponadto potrafi też zaznaczać breakpointy oraz koloruje wszystkie wystąpienia wskazanego (dwukrotnie klikniętego) słowa w danym pliku – całkiem przydatne. Jedynym mankamentem jest chyba tylko brak wsparcia dla zwijanych i rozwijanych regionów kodu oraz ewentualnie fakt, że w poziomie plugin zajmuje jakieś cztery razy więcej miejsca niż standardowy pasek przewijania. Na szerokoekranowych monitorach ciężko jednak uznać to za wadę :)