Kiedy piszemy aplikację okienkową, prędzej czy później dochodzi do sytuacji, w której trzeba powiązać interfejs użytkownika z pewnymi metodami w kodzie. W przypadku prostych programików problem w zasadzie nie istnieje, ale gdy aplikacja rozrasta się, sytuacja staje się nieco bardziej skomplikowana.
Przyjmijmy, że pojedynczą funkcjonalność wywoływaną z poziomu interfejsu użytkownika nazwiemy akcją. Podstawowym problemem jest fakt, iż do akcji można dostać się z wielu różnych miejsc w interfejsie użytkownika. Na przykład może być ona dostępna równolegle z poziomu menu aplikacji, paska narzędzi oraz menu kontekstowego. W takiej sytuacji nie ma już mowy o oprogramowywaniu zdarzeń elementów interfejsu (w sensie: implementowania akcji wewnątrz metody obsługi zdarzenia), jak można byłoby zrobić w prostej aplikacji, ponieważ utrzymywanie takiego kodu będzie horrorem. Rozwiązanie tego problemu nie jest zbyt trudne – wystarczy samą funkcjonalność przenieść do osobnej metody i wywoływać ją w metodach obsługujących zdarzenia.
To jednak nie wszystko. Często zdarza się, że pewną akcję można wywołać tylko w odpowiednich warunkach. Na przykład edycja i usunięcie elementu znajdującego się na liście jest możliwa tylko wówczas, gdy element ten jest zaznaczony. Oczywiście w odpowiednich metodach można sprawdzać zawsze, czy wszystkie konieczne warunki są spełnione, ale takie rozwiązanie jest niewygodne z punktu widzenia użytkownika. Znacznie mniej frustrująca jest sytuacja, w której jasno widać, że akcja jest niedostępna, niż gdy wprawdzie można ją wywołać, ale tylko po to, aby dostać po oczach komunikatem ostrzegawczym (lub, o zgrozo, bez żadnego efektu).
Spośród akcji można wyróżnić też takie, których zadaniem jest przełączenie pewnego stanu. Przykładem może być zawijanie wierszy w Notatniku: haczyk obok odpowiedniej pozycji menu informuje o tym, czy dana akcja jest aktywna, czy nie.
Ustawienie własności Enabled danej kontrolki w sytuacji, gdy zmiana warunków powoduje uniemożliwienie wywołania odpowiadającej jej akcji nie jest problemem. Gorzej, gdy po pierwsze – jak już wspominałem – akcja jest wywoływana z poziomu kilku kontrolek, a po drugie – warunków wymaganych do wywołania akcji jest kilka. Zauważmy też, że przeciętna aplikacja zawiera tych akcji kilkanaście lub kilkadziesiąt. Próba uzmysłowienia sobie, ile warunków w różnych miejscach trzeba byłoby wprowadzić, aby ręcznie aktualizować stany akcji może przyprawić o ból głowy.
Dużo frameworków wprowadza gotowe rozwiązania. Delphi udostępnia użytkownikom niewizualny komponent TActionManager, w którym można definiować akcje oraz odpowiadające im kontrolki. Windows Presentation Foundation oferuje podobny, bardzo wygodny w użyciu mechanizm. Ja chciałbym zaprezentować prostego managera przydatnego w sytuacji, gdy framework nie udostępnia nam odpowiedniego gotowca lub gdy nie chcemy korzystać z dużego rozwiązania, być może znacznie przerastającego nasze potrzeby.
Mój pomysł opiera się na trzech elementach: State, Condition i Action. Rozpatrzmy je po kolei.
State
Obiekt State jest odpowiedzialny za przechowywanie informacji o pojedynczym atomowym stanie jakiegoś elementu aplikacji. Na przykład może to być informacja o tym, czy w liście został zaznaczony jakiś element lub czy użytkownik zaznaczył jakiś tekst w polu tekstowym. Stan musi być użyty w taki sposób, by można było łatwo i jednoznacznie określić jego wartość, a także by możliwie łatwo można było go aktualizować. Na przykład kontrolka listy elementów udostępnia zdarzenie informujące o tym, że zmienił się indeks zaznaczonego elementu, zaś pole tekstowe – również poprzez zdarzenie – informuje o zmianie zaznaczenia. W obsłudze tych zdarzeń można łatwo określić wartość odpowiadającego im stanu.
Celem istnienia tej klasy jest ujednolicenie dostępu do warunków mogących wpływać na stany akcji.
Oto przykładowa implementacja klasy stanu w C#:
public class StateChangedEventArgs : EventArgs
{
private bool newValue;
public StateChangedEventArgs()
{
newValue = true;
}
public StateChangedEventArgs(bool newNewValue)
{
newValue = newNewValue;
}
public bool NewValue
{
get
{
return newValue;
}
set
{
newValue = value;
}
}
}
public delegate void StateChangedDelegate(
object State,
StateChangedEventArgs e);
public class State
{
private bool value;
public State(bool initialValue = true)
{
value = initialValue;
StateChanged = null;
}
public bool Value
{
get
{
return value;
}
set
{
this.value = value;
if (StateChanged != null)
StateChanged(this, new StateChangedEventArgs(this.value));
}
}
public event StateChangedDelegate StateChanged;
}
Condition
Condition jest klasą, która agreguje kilka różnych stanów. Dzięki temu jeden stan może być wykorzystywany w kilku różnych warunkach (być może w połączeniu z różnymi innymi stanami). Przykładowa implementacja w C# jest następująca:
public class ConditionChangedEventArgs : EventArgs
{
private bool newValue;
public ConditionChangedEventArgs()
{
newValue = true;
}
public ConditionChangedEventArgs(bool newNewValue)
{
newValue = newNewValue;
}
public bool NewValue
{
get
{
return newValue;
}
set
{
newValue = value;
}
}
}
public delegate void ConditionChangedDelegate(
object Condition,
ConditionChangedEventArgs e);
public enum ConditionKind
{
And,
Or
}
public class Condition
{
private List<State> stateList;
private ConditionKind conditionKind;
private bool value;
private void StateChanged(object State, StateChangedEventArgs e)
{
EvaluateCondition();
}
private void EvaluateCondition()
{
if (stateList.Count == 0)
{
if (value != false)
{
value = false;
if (ConditionChanged != null)
ConditionChanged(this,
new ConditionChangedEventArgs(false));
}
}
else
{
switch (conditionKind)
{
case ConditionKind.And:
{
bool tmpValue = true;
int i = 0;
while (i < stateList.Count && tmpValue)
{
tmpValue &= stateList[i].Value;
i++;
}
if (value != tmpValue)
{
value = tmpValue;
if (ConditionChanged != null)
ConditionChanged(this,
new ConditionChangedEventArgs(tmpValue));
}
break;
}
case ConditionKind.Or:
{
bool tmpValue = false;
int i = 0;
while (i < stateList.Count && !tmpValue)
{
tmpValue |= stateList[i].Value;
i++;
}
if (value != tmpValue)
{
value = tmpValue;
if (ConditionChanged != null)
ConditionChanged(this,
new ConditionChangedEventArgs(tmpValue));
}
break;
}
}
}
}
public Condition()
{
stateList = new List<State>();
value = false;
conditionKind = ConditionKind.And;
}
public void AddState(State state)
{
int i = 0;
while (i < stateList.Count && stateList[i] != state)
i++;
if (i < stateList.Count)
return;
state.StateChanged += StateChanged;
stateList.Add(state);
EvaluateCondition();
}
public void RemoveState(State state)
{
int i = 0;
while (i < stateList.Count && stateList[i] != state)
i++;
if (i == stateList.Count)
return;
state.StateChanged -= StateChanged;
stateList.RemoveAt(i);
EvaluateCondition();
}
public ConditionKind ConditionKind
{
get
{
return conditionKind;
}
set
{
conditionKind = value;
EvaluateCondition();
}
}
public bool Value
{
get
{
return value;
}
}
public event ConditionChangedDelegate ConditionChanged;
}
Action
Klasa Action opisuje pewną akcję. Powinna ona mieć następujące cechy:
Po powiązaniu z kontrolkami, powinna automatycznie przywiązać się do odpowiednich zdarzeń typu Click.
Powinna mieć własności typu Condition, regulujące dostępność (Enabled), widoczność (Visibility) oraz zaznaczenie (Down, Checked itp.) kontrolek, które są powiązane z akcją.
Powinna mieć zdarzenie Executed, wywoływane w momencie wybrania którejkolwiek z kontrolek, za które odpowiada akcja.
Oto przykładowa implementacja klasy Action:
public delegate void ActionExecutedDelegate(
object Action,
EventArgs e);
public class Action
{
private Condition enabledCondition;
private Condition visibleCondition;
private Condition checkedCondition;
private List<object> controlList;
private void ApplyEnabled(int i, bool newValue)
{
if (controlList[i] is Button)
((Button)controlList[i]).Enabled = newValue;
else if (controlList[i] is ToolStripMenuItem)
((ToolStripMenuItem)controlList[i]).Enabled = newValue;
else if (controlList[i] is ToolStripButton)
((ToolStripButton)controlList[i]).Enabled = newValue;
else
throw new InvalidOperationException(
"Internal control list contains not supported control!");
}
private void ApplyEnabledToAll(bool newValue)
{
for (int i = 0; i < controlList.Count; i++)
ApplyEnabled(i, newValue);
}
private void ApplyVisible(int i, bool newValue)
{
if (controlList[i] is Button)
((Button)controlList[i]).Visible = newValue;
else if (controlList[i] is ToolStripMenuItem)
((ToolStripMenuItem)controlList[i]).Visible = newValue;
else if (controlList[i] is ToolStripButton)
((ToolStripButton)controlList[i]).Visible = newValue;
else
throw new InvalidOperationException(
"Internal control list contains not supported control!");
}
private void ApplyVisibleToAll(bool newValue)
{
for (int i = 0; i < controlList.Count; i++)
ApplyVisible(i, newValue);
}
private void ApplyChecked(int i, bool newValue)
{
if (controlList[i] is Button)
return;
else if (controlList[i] is ToolStripMenuItem)
((ToolStripMenuItem)controlList[i]).Checked = newValue;
else if (controlList[i] is ToolStripButton)
((ToolStripButton)controlList[i]).Checked = newValue;
else
throw new InvalidOperationException(
"Internal control list contains not supported control!");
}
private void ApplyCheckedToAll(bool newValue)
{
for (int i = 0; i < controlList.Count; i++)
ApplyChecked(i, newValue);
}
private void EnabledConditionChanged(object Condition,
ConditionChangedEventArgs e)
{
ApplyEnabledToAll(e.NewValue);
}
private void VisibleConditionChanged(object Condition,
ConditionChangedEventArgs e)
{
ApplyVisibleToAll(e.NewValue);
}
private void CheckedConditionChanged(object Condition,
ConditionChangedEventArgs e)
{
ApplyCheckedToAll(e.NewValue);
}
private void ToolStripMenuItemClicked(object sender,
EventArgs e)
{
if (ActionExecuted != null)
ActionExecuted(this, new EventArgs());
}
private void ButtonClicked(object sender, EventArgs e)
{
if (ActionExecuted != null)
ActionExecuted(this, new EventArgs());
}
void ToolStripButonClicked(object sender, EventArgs e)
{
if (ActionExecuted != null)
ActionExecuted(this, new EventArgs());
}
public Action()
{
controlList = new List<object>();
ActionExecuted = null;
enabledCondition = null;
visibleCondition = null;
checkedCondition = null;
}
public void AddControl(object control)
{
int i = 0;
while (i < controlList.Count && controlList[i] != control)
i++;
if (i < controlList.Count)
return;
if (control is Button)
((Button)control).Click += ButtonClicked;
else if (control is ToolStripMenuItem)
((ToolStripMenuItem)control).Click +=
ToolStripMenuItemClicked;
else if (control is ToolStripButton)
((ToolStripButton)control).Click +=
ToolStripButonClicked;
else
throw new ArgumentException("control",
"This control is not supported!");
controlList.Add(control);
if (enabledCondition != null)
ApplyEnabled(controlList.Count - 1,
enabledCondition.Value);
if (visibleCondition != null)
ApplyVisible(controlList.Count - 1,
visibleCondition.Value);
if (checkedCondition != null)
ApplyChecked(controlList.Count - 1,
checkedCondition.Value);
}
public void RemoveControl(object control)
{
int i = 0;
while (i < controlList.Count &&
controlList[i] != control)
i++;
if (i == controlList.Count)
return;
if (control is Button)
((Button)control).Click -= ButtonClicked;
else if (control is ToolStripMenuItem)
((ToolStripMenuItem)control).Click -=
ToolStripMenuItemClicked;
else if (control is ToolStripButton)
((ToolStripButton)control).Click -=
ToolStripButonClicked;
else
throw new ArgumentException("control",
"This control is not supported!");
}
public void Execute()
{
if ((enabledCondition != null &&
enabledCondition.Value == true) ||
enabledCondition == null)
{
if (ActionExecuted != null)
ActionExecuted(this, new EventArgs());
}
}
public Condition EnabledCondition
{
get
{
return enabledCondition;
}
set
{
if (enabledCondition != null)
enabledCondition.ConditionChanged -=
EnabledConditionChanged;
enabledCondition = value;
if (enabledCondition != null)
{
enabledCondition.ConditionChanged +=
EnabledConditionChanged;
ApplyEnabledToAll(enabledCondition.Value);
}
}
}
public Condition VisibleCondition
{
get
{
return visibleCondition;
}
set
{
if (visibleCondition != null)
visibleCondition.ConditionChanged -=
VisibleConditionChanged;
visibleCondition = value;
if (visibleCondition != null)
{
visibleCondition.ConditionChanged +=
VisibleConditionChanged;
ApplyVisibleToAll(visibleCondition.Value);
}
}
}
public Condition CheckedCondition
{
get
{
return checkedCondition;
}
set
{
if (checkedCondition != null)
checkedCondition.ConditionChanged -=
CheckedConditionChanged;
checkedCondition = value;
if (checkedCondition != null)
{
checkedCondition.ConditionChanged +=
CheckedConditionChanged;
ApplyCheckedToAll(checkedCondition.Value);
}
}
}
public bool CanExecute
{
get
{
if (enabledCondition != null)
return enabledCondition.Value;
else
return true;
}
}
public event ActionExecutedDelegate ActionExecuted;
}
Przykład
Zobaczmy, w jaki sposób wykorzystać tak przygotowany mechanizm.
Załóżmy, że mamy program z przyciskiem, który powinien być dostępny tylko wówczas, gdy zaznaczony jest jakiś element listy oraz gdy włączony jest znajdujący się na formatce checkbox. Tworzymy aplikację z formatką, na której znajduje się przycisk, listbox i checkbox. Pierwszym krokiem jest przygotowanie naszej architektury stanów, warunków i akcji. Ponieważ jest to funkcjonalność, która powinna działać "za kurtyną", większą jej część umieściłem w pliku Form1.Designer.cs.
Pierwszym krokiem jest przygotowanie odpowiednich pól klasy formatki. W pliku Form1.Designer.cs dopisujemy następujące linijki:
// Wstawione przez designera
private System.Windows.Forms.Button button1;
private System.Windows.Forms.ListBox listBox1;
private System.Windows.Forms.CheckBox checkBox1;
// Te dopisujemy
private State listboxSelectionState;
private State checkboxCheckedState;
private Condition buttonActionAvailable;
private Action buttonAction;
}
Następnie przygotowujemy metodę, która zainicjuje wszystkie obiekty naszego mechanizmu.
private void InitializeActions()
{
listboxSelectionState = new State(false);
checkboxCheckedState = new State(false);
buttonActionAvailable = new Condition();
// Tutaj definiujemy, które stany będą
// wpływały na dany warunek.
buttonActionAvailable.AddState(
listboxSelectionState);
buttonActionAvailable.AddState(
checkboxCheckedState);
buttonAction = new Action();
// Przypinamy do akcji kontrolkę
buttonAction.AddControl(button1);
// Definiujemy warunek dostępności
// akcji
buttonAction.EnabledCondition = buttonActionAvailable;
// Określamy metodę wywoływaną
// po wywołaniu akcji.
buttonAction.ActionExecuted += buttonAction_ActionExecuted;
}
Zgodnie z ostatnią linijką potrzebna nam będzie metoda buttonAction_ActionExecuted. Jej zadaniem będzie wykonanie akcji dostępnej po wciśnięciu przycisku. Jednak tutaj kończy się część działań "za kurtyną", więc ograniczymy się w jej implementacji do wywołania metody, która znajdzie się w pliku Form1.cs.
Teraz wzbogaćmy konstruktor o wywołanie naszej metody inicjującej akcje. Zauważmy, że akcje odwołują się do zdarzeń przypinanych do nich kontrolek, więc – co ważne – kontrolki muszą istnieć, gdy konfigurujemy akcje. Dlatego też metoda inicjująca akcje wywołana zostanie już po wywołaniu metody inicjującej komponenty.
public Form1()
{
InitializeComponent();
InitializeActions();
}
Na koniec pozostało nam tylko oprogramowanie aktualizacji stanów, gdy zmienione zostanie zaznaczenie listy plików lub gdy zmieni się stan checkboxa.
I to wszystko. Wielką zaletą rozwiązania jest fakt, że definicja zależności (która z reguły raz napisana, nie będzie się już zmieniać, a przynajmniej nieczęsto) siedzi w Form1.Designer.cs, zaś faktyczny kod aplikacji zamyka się w czterech metodach, z których najdłuższa ma dwie linijki. Sprawdźmy, czy wszystko działa.
Chętnie wysłucham pomysłów na udoskonalenie powyższego mini-frameworka. Zachęcam do komentowania.
Potrzebowałem dziś przedebuggować assembly zawierającą komponent, który nie działał prawidłowo w design-time. W takiej sytuacji trzeba przypiąć się jedną instancją Visual Studio do drugiej, co umożliwi postawienie pułapek w assembly i przygwożdżenie błędnego kodu.
Problem polega jednak na tym, że Visual Studio w wersji Express nie udostępnia możliwości przypięcia się do procesu nie będącego jednym z aktualnie edytowanych projektów. Okazuje się jednak, że można ten brak obejść. W tym celu:
Dodajemy do solucji nowy projekt – na przykład aplikację konsolową;
Zapisujemy go na dysku i zamykamy Visual Studio
Edytujemy plik projektu (.csproj), dodając do niego na początku następujące linijki:
<PropertyGroup>
<StartAction>Program</StartAction>
<StartProgram>C:\Program Files\Microsoft Visual Studio 10.0\
Common7\IDE\VCSExpress.exe</StartProgram>
</PropertyGroup>
Zapisujemy, po czym ponownie uruchamiamy Visual Studio
Próba uruchomienia tak spreparowanego projektu spowoduje uruchomienie drugiej instancji środowiska, którą można już debuggować.
Moim pierwszym pecetem był 486 DX/33 wyposażony w dysk twardy o pojemności 300 Mb oraz w 8 Mb RAMu. Choć wydaje się, że jest to bardzo mało, konfiguracja ta była w zupełności wystarczająca, by uruchomić na niej Windows 95. Ponieważ w czasie, który wspominam, funkcjonowała jeszcze zasada Moore’a, kolejny sprzęt, który postawiłem na biurku był już wyposażony w procesor Intel Celeron (800 Mhz) oraz 392 Mb RAMu, co umożliwiło zainstalowanie na nim bodaj najpopularniejszego do tej pory systemu, Windowsa XP.
Współczesne komponenty komputerów stacjonarnych – w porównaniu z czasami, o których wcześniej wspomniałem – są wyjątkowo tanie. Szukałem ostatnio cen RAMu; 4 Gb (czyli maksymalna ilość, którą można zaoferować 32-bitowemu systemowi) całkiem przyzwoitej firmy kosztuje w tej chwili około 160 PLN. Gwałtownego spadku cen doświadczyłem też kupując dyski twarde. Obecnie mam zainstalowane trzy: 80, 160 i 250 Gb. Wszystkie są tej samej firmy, wszystkie kupiłem praktycznie w takiej samej cenie (błąd około 10 PLN), tyle że pomiędzy poszczególnymi zakupami mijał mniej więcej rok czasu. W tej chwili za tą samą cenę można kupić dysk o pojemności 1 Tb.
Kiedyś jednak cena jednego bajtu – czy to zaszytego w kości pamięci, czy też na dysku twardym – była znacznie wyższa. Komputery PC były bardzo popularne, ale programista pisząc programy musiał brać pod uwagę, iż będą uruchamiane w środowiskach ubogich w zasoby; na porządku dziennym była walka o każdy, pojedynczy bajt używanej pamięci operacyjnej.
Wówczas właśnie zrodziła się idea reusability. Polegała ona z grubsza na osiąganiu oszczędności podczas korzystania z zasobów poprzez wielokrotne ich używanie. Sprowadzało się to na przykład do wielokrotnego używania jednej zmiennej do wielu różnych celów. Tym sposobem zadeklarowany na początku bloku int i najpierw służył jako iterator w pętli, następnie stawał się pośrednikiem dla danych wprowadzanych do programu przez użytkownika, by zakończyć życie jako zmienna przechowująca sumę elementów potrzebnych do obliczenia średniej jakiegoś zestawu danych. Uderzało to znacząco w czytelność kodu źródłowego, ale było też czasami jedyną metodą na zrealizowanie zamierzonego celu. Powiem więcej – metoda ta funkcjonuje z powodzeniem do dnia dzisiejszego w sytuacjach, gdy zasoby dostępne dla programisty są mocno ograniczone – na przykład podczas oprogramowywania różnego rodzaju prostych procesorów.
Współczesne trendy programowania bardzo mocno odbiegają od niegdysiejszych, jeśli weźmiemy pod uwagę ilość zużywanych zasobów. Dosyć powiedzieć, że programy na popularne w tym momencie systemy operacyjne dla urządzeń mobilnych (Android, Windows Phone 7) pisze się teraz w językach przeznaczonych dla maszyn wirtualnych, nie zaś w języku kompilowanym do rozkazów procesora. Kiedyś dla wszystkich systemów opartych na Windows CE można było pisać w C++, teraz programy dla Windows Phone 7 uruchamiane są na wirtualnej maszynie .NET postawionej na urządzeniu. Programy dla Androida, z kolei, pisze się w Javie. Żadną tajemnicą jest fakt, iż programy takie pracują nieco mniej wydajnie, niż gdyby ich rozkazy wykonywał sam procesor. Ale jakie ma to znaczenie w erze, w której na rynek niebawem ma zostać wprowadzony telefon z dwurdzeniowym procesorem i spadek wydajności programu jest prawie nieodczuwalny?
Ponieważ dawna reusability (w kontekście implementacji kodu źródłowego) straciła sens, współcześnie pojęcie reusability zostało uogólnione i jest interpretowane w zupełnie inny sposób. Programowanie stało się bardzo istotną gałęzią przemysłu, więc jak grzyby po deszczu zaczęły powstawać różne techniki pozwalające na zwiększenie wydajności pracy programistów i ograniczenie kosztów produkcji oprogramowania. Jedna z nich polega na skupieniu się na modularnej konstrukcji architektury programu. Każdy z modułów musi w jak najmniejszym stopniu zależeć od innych, jednocześnie realizując pewien zamknięty zestaw zlecanych mu zadań. Tym sposobem istnieje możliwość wyekstrahowania go z jednego projektu i włączenia w drugi, w którym istnieje potrzeba zrealizowania podobnej funkcjonalności. Ponowne użycie tego modułu (reuse) pozwala na znaczne ograniczenie czasu potrzebnego na realizację kolejnego projektu, a to właśnie czas jest teraz najdroższym elementem rozwoju oprogramowania.
Mimo iż pierwotna reusability (w kontekście oprogramowania dla komputerów PC) nie ma zwykle większego sensu i współcześnie jest uznawana za technikę, która poprzez obniżenie czytelności kodu źródłowego przyczynia się do zwiększenia zasobów potrzebnych do realizacji projektu, spotkałem się ostatnio z kilkoma przypadkami jej zaistnienia – których skutki były opłakane.
Mówię teraz o lokalizacji programów, czyli o dostosowaniu ich do różnych języków i kultur, a ściślej – o procesie tłumaczenia interface’u użytkownika. Proces ten odbywa się poprzez ekstrakcję wszystkich ciągów znaków, które pojawiają się w programie, a następnie przetłumaczeniu ich na inny język i włączeniu do zasobów programu. Tym sposobem raz napisany i skompilowany program może być uruchamiany w wielu różnych wersjach językowych – włącznie z przełączaniem ich w trakcie jego pracy. Przykładem takiego programu jest ProCalc 2 dostępny w dziale download: w zależności od wersji językowej systemu operacyjnego uruchomi się on z napisami po polsku, po włosku lub – w każdym innym wypadku – po angielsku.
Tłumaczenie interface’u niesie ze sobą również zagrożenia związane z tym, że gramatyki różnych języków są zbudowane w odmienny sposób. Polacy znają tylko trzy czasy, tymczasem ktoś naliczył się u Anglików i Amerykanów aż czterdziestu dziewięciu różnych wariacji na temat czasu przeszłego, teraźniejszego i przyszłego (a czasem nawet ich kombinacji). Z drugiej strony Polak jest w stanie odmienić rzeczownik przez siedem przypadków, podczas gdy Amerykanin korzysta tylko z dwóch – mianownika i – rzadziej – dopełniacza (poprzez dodanie ‘s). Innymi słowy, wyrażenie zapisane tak samo w jednym języku, a mające kilka znaczeń, w innym może wyglądać w każdym przypadku inaczej. Na przykład „Elements saved” oznacza: „Elementy zostały zapisane”. Ale jeśli wyrażenie to wystąpi po liczbie, na przykład: „10 elements saved”, należy je przetłumaczyć nieco inaczej: „10 elementów zostało zapisanych”.
Okazuje się, iż wielu projektantów oprogramowania nie bierze powyższego faktu pod uwagę i podczas ekstrakcji ciągów znaków stosują starą zasadę reusability. Jeśli więc w kilku miejscach w programie (ba, czasem nawet w kilku programach) występuje fraza „Elements saved”, do biblioteki tłumaczeń włączają oni to wyrażenie tylko raz i stosują je wielokrotnie. Tym sposobem tłumacz postawiony jest przed zadaniem niemożliwym do zrealizowania, bo – jak pokazałem wcześniej – nierealne jest przygotowanie tylko jednej wersji tłumaczenia, która będzie pasowała wszędzie.
Wydawałoby się, że opisany przeze mnie problem jest oczywisty i żaden rozsądnie myślący programista nie dopuści do jego powstania. Niestety, najwyraźniej tak nie jest. Otóż bowiem tak doświadczony w projektowaniu wielojęzycznych interface’ów użytkownika developer, jakim jest Microsoft popełnił w swoim programie takie oto tłumaczenie (mowa o wersji release oprogramowania):
Zachęcam do zgadnięcia, o jakim programie mowa, jakie tłumaczenie powinno wystąpić w tym miejscu i z czego wynika zabawna pomyłka tłumacza.
Nieprawidłowe tłumaczenie jest tylko jednym przypadkiem nieprawidłowego stosowania opisanej przeze mnie techniki. W pracy analizowałem kiedyś kod, który pisał dla nas zewnętrzny programista. Stosował on tam archaiczne reusability bez skrępowania i argumentował to zwiększeniem wydajności programu i zmniejszeniem zajmowanych przez niego zasobów. Przesłał nam też tytuł książki, na której bazował wszystkie „optymalizacje” wprowadzone do swojego kodu. Okazało się, iż książka ta traktuje o optymalizowanie kodu dla procesorów Pentium i 486 i została wydana w 1997 roku. Sporo mieliśmy pracy z poprawieniem jego algorytmów tak, by dało się z nimi później pracować.
Nie należy lekceważyć technik związanych z reusability. Pozwalają one na realne ograniczenie czasu pracy programistów. Nawet pierwotną reusability można stosować z powodzeniem w niektórych przypadkach. Jednak – jak to w życiu bywa – technika niewłaściwie zastosowana bardzo szybko obraca się przeciw jej użytkownikowi, utrudniając tym samym pracę nad projektem.
Ostatnimi czasy interesuję się wszelkimi wydarzeniami mającymi miejsce w świecie urządzeń, technologii i oprogramowania mobilnego. Nie mógł więc ujść mojej uwagi bardzo kontrowersyjny alians Nokii z Microsoftem.
Jeśli przyjrzymy się historii Nokii z przeciągu ostatnich pięciu lat, alians ten okaże się znacznie mniejszym zaskoczeniem niż mogłoby się to na początku wydawać. Cofnijmy się na chwilę do roku 2007. Nokia jest u szczytu swoich możliwości, kończy rok ze sprzedanymi przeszło 437,1 milionami telefonów (drugi jest Samsung utrzymujący się na poziomie 161,2 milionów, źródło). Pod koniec października 2007 roku kurs akcji Nokii osiąga wartość nie notowaną od pięciu lat – blisko 40 USD. I aż trudno uwierzyć, że właśnie w takich okolicznościach zaczyna się wielki upadek tej popularnej firmy.
Jest 9 stycznia 2007 roku. Apple, po długim okresie domysłów i plotek ogłasza, iż pracuje nad projektem nowego, rewolucyjnego telefonu – iPhone’a. Musi minąć jeszcze przeszło pół roku, żeby telefon ten trafił na rynek, jednak marketing Apple’a robi swoje: ludzie ustawiają się w kolejkach przed sklepami nawet na sto godzin przed premierą, stoisk z nowymi telefonami spod znaku jabłuszka w nocy poprzedzającej premierę strzeże policja i ochrona. iPhone – mimo dosyć wysokiej ceny – sprzedaje się w zastrzaszającym tempie: 74 dnia po premierze został sprzedany milionowy egzemplarz.
Na czym polegał sukces Apple’a? Oczywiście marketing odegrał tu swoją rolę. Jednak Apple uderzył w rynek innowacją: postawił na ekran dotykowy (wprawdzie takie telefony były już wówczas na rynku, ale niezbyt popularne), skonstruował atrakcyjny, efektowny i użyteczny interface użytkownika przystosowany do obsługi palcami oraz wyeksponował multimedialne cechy urządzenia: bardzo duży ekran (jeśli dobrze pamiętam, największy w urządzeniu tego typu w czasie premiery) oraz dużo pamięci przeznaczonej do przechowywania multimediów. Urządzenie nie było bez wad – do głównych zaliczany był brak schowka, niemożność wysyłania MMSów, a także zamknięcie możliwości instalowania oprogramowania firm trzecich, które nie przeszło procesu cyfrowej certyfikacji, nie przeszkodziły one jednak w wielkim sukcesie Apple’a.
Czy iPhone był naprawdę tak dobry, żeby zacząć wypierać z rynku telefony Nokii? Myślę, że raczej nie. Nokia była w stanie zaprojektować i wypuścić na rynek telefon o podobnych (albo nawet i lepszych) parametrach i pozbawiony wad iPhone’a. Jednak w kierownictwie Nokii ktoś popełnił poważny błąd, ponieważ z wielkiego sukcesu iPhone’a nie wyciągnięto wniosków i Nokia nie zmodyfikowała swojej rynkowej strategii, aby sprostać nadchodzącym wyzwaniom.
Co dzieje się dalej? Nokia wydaje na świat model 5800, który stał się pierwszym telefonem z ekranem dotykowym, który został przyjęty pozytywnie przez rynek (Nokia 7700 nigdy nie weszła do sprzedaży, zaś 7710 z Symbianem S90 nie odniósł większego sukcesu). Telefon nie miał jednak szans konkurować z iPhone – był raczej ewolucją niż rewolucją w stosunku do poprzednich modeli, ponadto do użytkowania potrzebował czasem rysika, podczas gdy iPhone został zaprojektowany pod obsługę przy pomocy palców. Ponadto sukcesowi marketingowemu – jak na złość – przeszkodziła wadliwa konstrukcja słuchawki, która przestawała działać po bardzo krótkim czasie. Nokia oczywiście zadziałała szybko i umożliwiła darmową wymianę wadliwego komponentu, jednak wydarzenie to z pewnością wpłynęło na statystyki sprzedaży.
Po umiarkowanym sukcesie modelu 5800 Nokia postawiła na telefony z serii N (od eNtertainment) – lata 2008 i 2009 przyniosły modele N78, N79, N85, N96, a także N97. Jednak wszystkie stanowiły tylko wariacje na temat: zmieniała się wersja systemu, parametry sprzętowe i rozdzielczość wbudowanego aparatu fotograficznego, ale nadal brakowało czegoś nowego, rewolucyjnego.
Tymczasem konkurencja Nokii nie spała, a wręcz przeciwnie – różne firmy prześcigały się w pomysłach na „iPhone killera”.
W trzecim kwartale 2009 roku pojawia się popularny Samsung Omnia. Czwarty kwartał to premiery telefonów z Androidem, między innymi Motorola Droid oraz HTC Hero. Początek roku 2010 zaznaczył się wejściem do sprzedaży telefonu Google Nexus One, a niedługo potem Sony Ericsson zaatakowało rynek XPerią X10. I dopiero po tych wszystkich premierach, pod koniec 2010 roku Nokia zaczyna wprowadzać telefony nowej generacji, z systemem Symbian^3: Nokię N8, a niedługo potem C7 i E7 (która, moim zdaniem niesłusznie, odziedziczyła po świetnej serii handheldów miano Communicator).
Rynek był jednak bezlitosny wobec Nokii i długi okres stagnacji, gdy fińska firma wypuszczała jeden po drugim telefony różniące się głównie wyglądem, mocno odznaczył się w historii finansowej firmy. Wystarczy tylko popatrzeć na kurs akcji Nokii w okresie od 2007 roku do dziś.
Jest początek roku 2011. Nokia systematycznie traci zainteresowanie rynku, konkurencja co chwilę wprowadza telefony, które budzą wielkie zainteresowanie, zaś premiery fińskiego producenta przechodzą bez większego echa. Nokii potrzeba rewolucji – czegoś, co wstrząsnie rynkiem. I w takich właśnie okolicznościach decyduje się ona na sojusz z Microsoftem.
Nie zdziwiłem się specjalnie, że Nokia wykonała tak gwałtowny ruch, ponieważ było to dla niej mniej więcej jak być lub nie być, natomiast zachodziłem w głowę, dlaczego fiński producent nie zdecydował się na bardzo popularnego Androida, tylko na odsuniętego w tło Windows Phone 7. Wszystko stało się jasne, gdy przeczytałem, kim był wcześniej obecny CEO Nokii, pan Stephen Elop.
Proponuję zobaczyć, jak zareagował rynek na opublikowaną 11 lutego decyzję o aliansie Nokii i Microsoftu.
Moim zdaniem decyzja Nokii to zdecydowany strzał w kolano. Po pierwsze, system Microsoftu nie zyskał zbyt dużej popularności; najlepszym epitetem, którego można użyć do określenia interface’u użytkownika WP7 jest „ascetyczny”. Wystarczy zobaczyć, jak wyglądają pewne wspólne elementy każdego systemu mobilnego w Symbianie, Androidzie i Windows Phone 7:
Ekran główny
Menu
Kontakty
Ustawienia
Screenshoty z Nokia RDA, Android Emulator oraz dzięki uprzejmości MicrosoftFeed.com
Windows Phone nie przegrywa jednak z innymi systemami tylko z powodu swojego interface’u. Większym problemem jest fakt, iż wszystkie technologie Microsoftu są zamknięte, tymczasem zarówno Symbian jak i Android są systemami otwartymi, w każdej chwili można z Internetu ściągnąć kompletne kody źródłowe każdego z nich.
Jest jeszcze jeden problem. Miarą współczesnego systemu operacyjnego dla urządzeń mobilnych jest liczba developerów, którzy piszą dla niego oprogramowanie. Nokia udostępnia SDK Symbiana darmowo na swoich stronach, a poza tym na Symbiana można pisać w QT. Poza tym programy napisane dla Symbiana można bez problemu publikować i każdy może instalować je bez większych ograniczeń (w przypadku programów korzystających np. z połączeń telefonicznych lub wysyłających SMSy wymagana jest certyfikacja). Google również udostępnia kompletny SDK dla Androida; ba, istnieje nawet odpowiedni plugin do Eclipse’a pozwalający na łatwe projektowanie i programowanie aplikacji. Oczywiście programy również można bez ograniczeń publikować w postaci pakietów apk.
Microsoft jednak nie przepuścił okazji do zysku. Wprawdzie, jeśli chodzi o środowisko programistyczne, pobił na głowę zarówno Symbiana, jak i Androida, ponieważ udostępnił darmowo specjalną wersję Microsoft Visual Studio przeznaczoną do pisania programów dla Windows Phone 7. Na tym jednak kończy się otwartość tej firmy: żeby zainstalować napisany przez siebie program na fizycznym urządzeniu, trzeba je najpierw odblokować. Aby jednak mieć taką możliwość, trzeba być zarejestrowanym developerem Windows Phone 7, co wiąże się z rocznymi opłatami (obecnie $99). Ale i na tym nie koniec; odblokować można tylko kilka urządzeń do celów deweloperskich, natomiast publikowanie programów może odbywać się tylko przez Microsoft Marketplace. Warto wspomnieć o jeszcze jednym ograniczeniu: zarejestrowany deweloper może opublikować tylko 100 darmowych programów, za publikację każdego kolejnego musi zapłacić $19.99 (ograniczenie to nie dotyczy programów płatnych).
Jestem fanem Nokii od mojego pierwszego telefonu. Przez moje ręce przeszły modele 3210, 3510i, N-Gage, 6230i, E66 i C7. Obawiam się jednak, że moja przygoda z tą marką ma się już ku końcowi. Ponieważ zbliża się termin przedłużenia umowy abonamentowej, mam możliwość wybrania nowego telefonu i będzie nim najprawdopodobniej HTC Desire Z lub HTC Desire HD z Androidem na pokładzie – zainstalowałem już Eclipse i Netbeans, aby nauczyć się pisać aplikacje na tą platformę.
Strategia Nokii jest bardzo ryzykowna. Będziemy musieli jednak poczekać jeszcze trochę, by dowiedzieć się, czy firma ta zaskoczy nas niedługo jakimś nowym, rewelacyjnym telefonem, który pozwoli jej wrócić na falę, czy jednak Windows Phone 7 stanie się ostatnim gwoździem do trumny tego bardzo popularnego niegdyś producenta komórek.
Czasami zdarza się potrzeba zrobienia twardego restartu palmtopa i zreinstalowania wszystkich aplikacji. Niestety, Microsoft ActiveSync nie jest dostatecznie sprytny i wykryje palmtopa jako nowe urządzenie – dodając tym samym kolejny partnership. Jest to dosyć irytujące tym bardziej, że nie zostanie mu przypisana nazwa naszego urządzenia (bo takowy już istnieje). Wystarczy zastosować jednak prosty trick, by usunąć poprzednie partnerstwo i utworzyć nowe przy kolejnej synchronizacji. W tym celu należy:
Przechodzimy do katalogu C:\Documents and Settings\\Dane Aplikacji\Microsoft\ActiveSync\Profiles i usuwamy podkatalog odpowiadający poprzednio utworzonemu partnerstwu;
Uruchamiamy edytor rejestru, przechodzimy do gałęzi Mój komputer\HKEY_CURRENT_USER\Software\Microsoft\Windows CE Services\Partners i usuwamy podgałąź odpowiadającą poprzedniemu partnerstwu – można to sprawdzić, odczytując klucz DisplayName (partnerstwa są zapisane w postaci liczby heksadecymalnej, na przykład, 63ef340a)
Podczas następnej synchronizacji zostanie utworzone nowe partnerstwo o takiej samej nazwie, jak nazwa urządzenia.
Ostatnio bardzo często spotykam się w Internecie z pytaniem, czy istnieje możliwość pisania w wersjach Express Visual Studio programów na platformę .NET Compact Framework. Oficjalnie jest to niemożliwe: polityka marketingowa Microsoftu zadecydowała o włączeniu do środowiska wsparcia dla .NET CF dopiero od wersji Professional (dla zainteresowanych, około 3.5k PLN za jedną licencję BOX). Okazuje się jednak, że taka możliwość istnieje. Zaznaczam, iż pomysł nie jest mój, ale mimo usilnych poszukiwań, nie udało mi się odnaleźć pierwotnego artykułu, na bazie którego przygotowałem moje środowisko pod kompilację dla urządzeń mobilnych.
Rozwiązanie jest następujące:
Instalujemy na urządzeniu mobilnym .NET Compact Framework 3.5; jeśli takowego nie posiadamy, albo z innego powodu nie chcemy instalować na nim środowiska uruchomieniowego, możemy posłużyć się darmowym emulatorem urządzenia Windows Mobile. Nie zapomnijmy tylko o doinstalowaniu obrazów systemu Windows Mobile. Dodam tylko, że emulator jest uruchamiany z linii poleceń.
Odszukujemy w urządzeniu, w katalogu \Windows serię plików GAC_*.dll (zobacz screen). Każdy z nich stanowi pojedynczą assembly dla .NET CF. Kopiujemy wszystkie (uważamy tylko na wersję – jest w nazwie pliku) na komputer stacjonarny i umieszczamy w wygodnym dla nas katalogu (w moim przypadku jest to D:\Dokumenty\C\C#\CF_Assemblies\3.5).
Zmieniamy nazwy plików według klucza:GAC_mscorlib_v3_5_0_0_cneutral_1.dllnamscorlib.dll
Upewniamy się, że na komputerze jest zainstalowane .NET Framework 3.5 (będziemy korzystali z kompilatora csc.exe, dołączanego do pakietu)
W katalogu projektu C#, który będziemy chcieli kompilować dla .NET CF tworzymy plik wsadowy o następującej treści:
set compilerdir="C:\windows\Microsoft.NET\Framework\v3.5"
set asmdir="D:\Dokumenty\C\C#\CF_Assemblies\3.5"
set projectdir="D:\Dokumenty\C\C#\CF\Hello World\"
%compilerdir%\csc.exe /noconfig /nostdlib /debug+ /optimize-
/define:DEBUG;MOBILE /out:"%projectdir%HelloWorld.exe"
/r:%asmdir%\mscorlib.dll /r:%asmdir%\System.dll
/r:%asmdir%\System.Data.dll /r:%asmdir%\System.Drawing.dll
/r:%asmdir%\System.Windows.Forms.dll "%projectdir%Form1.cs"
"%projectdir%Form1.Designer.cs" "%projectdir%Program.cs"
pause
Dla wygody możemy skonfigurować Visual Studio w taki sposób, by po wywołaniu polecenia z menu External Tools był wywoływany plik compile.bat z katalogu projektu. Zadanie to pozostawimy jednak jako ćwiczenie dla studenta :)
I to by było na tyle. Efekt działania przykładowej aplikacji można podziwiać poniżej.
Na koniec kilka notatek. Po pierwsze, musimy cały czas mieć na uwadze, że o ile VS C# Express nie będzie nam przeszkadzało w projektowaniu aplikacji dla .NET Compact Frameworka, to z drugiej strony nie będzie nam też pomagało. Mam tu na myśli na przykład fakt, iż designer formatki będzie ją projektował pod .NET dla win32, więc – na przykład – po narysowaniu przycisku na formatce i próbie kompilacji dla .NET CF kompilator przerwie kompilację z następującym komunikatem:
Kompilator Microsoft (R) Visual C# 2008 w wersji 3.5.30729.4926
dla programu Microsoft (R) .NET Framework w wersji 3.5
Copyright (C) Microsoft Corporation. Wszelkie prawa zastrzeżone.
d:\Dokumenty\C\C#\CF\Hello World CF\Form1.Designer.cs(51,26): error CS1061:
Element "System.Windows.Forms.Button" nie zawiera definicji
"UseVisualStyleBackColor", a nie odnaleziono metody rozszerzającej
"UseVisualStyleBackColor", która przyjmuje pierwszy argument typu
"System.Windows.Forms.Button" (czy nie brakuje dyrektywy using lub
odwołania do zestawu?).
W takiej sytuacji wystarczy zajrzeć do Form1.Designer.cs i usunąć linijkę, w której następuje przypisanie do nieistniejącej w CF własności UseVisualStyleBackColor.
Sprytny programista wpadnie pewnie na jeszcze jeden ciekawy pomysł – otóż assemblies .NET CF można dołączać jako referencje (References) do projektu. Korzyści z takiego rozwiązania są ogromne – natychmiast po dodaniu zacznie działać Code Insight, podpowiadając nam namespace’y, klasy i ich metody oraz własności, co jest bezcenne w przypadku assemblies nie występujących w przypadku win32 – przykładowo Microsoft.WindowsMobile.PocketOutlook.dll
Zachęcam do dzielenia się opiniami na temat programowania dla .NET CF w Visual Studio Express. Może wymyślicie jeszcze ciekawsze usprawnienia?
Stoisz na rozstaju dróg. Jest słoneczny, letni poranek, rosa skrzy się na trawie; na nitkach pajęczyny wygląda jak przezroczyste perły. Kropelki wody porozwieszane pomiędzy gałęziami sprawiają wrażenie, jak gdyby ktoś przyozdobił drzewa diamentami. Rozglądasz się; widzisz rozlane, niewielkie leśne jeziorko. Jego powierzchnia jest gładka, marszczona gdzieniegdzie przez poranną bryzę, która niesie Ci zapach lasu. Na brzegu – postrzępiony kapelusz wędkarza, który skoro rano wybrał się na wczesny połów. Obracasz się powoli – słońce przebłyskujące pomiędzy gałęziami drzew wydaje się świecić na zielono. Otacza Cię soczysta, głęboka, żywa zieleń przyprószona tu i ówdzie błękitnymi plamkami niezapominajek albo czerwonym kapeluszem muchomora. Robisz kilka kroków: żwir chrzęści pod Twoimi stopami, a wygrzewające się na poboczu ropuchy niechętnie ustępują Ci drogi. Trącasz jakąś trawę z której zrywa się latająca paleta kolorów – motyl. Robi przed Tobą kilka wdzięcznych ósemek, po czym odlatuje w zacienione, jeszcze, gąszcze lasu.
Stoisz na skale wysuniętej poza zbocza wzgórza na które przed chwilą mozolnie się piąłeś. Rozglądasz się – widzisz głęboki, gęsto zadrzewiony wąwóz, na dnie którego płynie cicho pomarańczowy – od wypłukiwanych tu i ówdzie złóż żelaza – strumyk. Czujesz podmuch wiatru, ale nie jest to już spokojna bryza, ale żywy wiatr tych wzgórz. Zamykasz oczy – i nie słyszysz już szumu drzew, ale czujesz go podróżując wraz z wiatrem, który wciągnął Cię w szaloną podróż. Przelatujesz wąwozem, pokonujesz przełęcz i mkniesz ponad zielonymi płaskowyżami ku dalekim wzniesieniom. A może zawracasz – by przez zielone wrota wylać się na szerokie równiny pokryte szachownicami zasianych pól, czerwienią dachówek pokrywających domy okolicznych wiosek, i błękitem niewielkich jeziorek i strumyków?
Skręcasz z drogi, wijącej się wśród wąwozów i przełęczy by nagle, ku swojemu zdziwieniu, pomiędzy szczytami pobliskich wzniesień dostrzec szeroko rozlane jezioro. Zamyka je tama – jak gdyby zawieszona na sąsiednich szczytach. Na jednym z brzegów pastelowymi kolorami mienią się niewielkie domki, spokojne fale syzyfową pracą próbują podmyć piaszczyste wybrzeże. Wchodzisz na rozchybotany, wiszący mostek. Przed Tobą, w oddali bieli się żagiel, pod Tobą przepływa mała łódka; pozostawia za sobą kilwater, który znika powoli, rozmywając się coraz bardziej na lustrzanej płaszczyźnie. Wchodzisz na tamę; z jednej strony widzisz jezioro, druga znów kusi Cię, byś wzleciał i jak myszołów pomknął nad głęboko werżniętą w góry dolinką, widząc od czasu do czasu stadka krów i owiec albo konopiastogrzywą klacz pasącą się na łące.
***
A Teraz siedzisz w pociągu i z rozrywającym serce żalem widzisz Twoje wzgórza oddalające się od Ciebie w rytm stukotu kół na szynach. Zaciśnięta kurczowo na poręczy ręka drży, bo chciałbyś wyrwać się, uciec, wrócić – choć wiesz, że nie możesz. Jedziesz odwiedzić cywilizację: rozkopane ulice, brudne dzielnice z dziećmi bawiącymi się w błocie podwórek; długie oczekiwania na zatłoczony autobus; ciągnące sie w nieskończoność korki – zrezygnowane i zmęczone twarze ludzi; betonowo-stalowe zbrojenia, które teraz muszą zastąpić Ci korzenie drzew; falujące, wzburzone morze nieznajomych, często wrogich Ci twarzy.
A Twoje wzgórza będą czekać… najpierw postarzeje się wiatr – i zamiast hasać radośnie pośród drzew zacznie zrywać ich liście, które pokryją kolorowym dywanem leśną ściółkę. Potem uśnie, otoczony płynącą bielą, która zagłuszy leśne bicie serca. Drewniane szkielety będą skrzypieć, gnąc się pod naporem śniegu, skarżąc się – świadome swej bezsilności – na samotniczy los.
A później zbudzi się życie – zielone pędy będą walczyć na zacienionej przez starszyznę ziemi o swój promyk słońca, drzewa wypełnią się zielenią, a wiatr znów będzie zapraszał do podróży.
A Ty? Przystaniesz czasami zdumiony, bo w głębi miasta poczujesz nagle zapach zieleni, zapach wolności, która wciąż ciągnie Cię w Twoje strony. A to tylko niegościnny, miejski wiatr zagarnął w Twoje nozdrza złudny zapach z pobliskiego parku…
Atlantyda – mityczna wyspa, o której dowiadujemy się z ust Platona. Zamieszkana przez ludzi żyjących w pokoju i harmonii; oprócz wspaniałych świątyń i ogrodów otaczających zabudowę miast, królowała na niej nowoczesna technika – według niektórych nawet bardziej zaawansowana od współczesnej.
Tajemnicza: poza źródłowym zapisem Platona w jego księgach (Timajos i Kritias), właściwie nic o niej nie wiemy. Możemy jedynie spekulować o tym, jaka była jej historia, jak wyglądała, jacy byli ludzie, którzy na niej żyli. Niezgłębioną tajemnicą pozostają również jej ostatnie dni – nie wiadomo, czy za jej zniszczenie odpowiada trzęsienie ziemi, wybuch wulkanu, czy inny równie niszczycielski kataklizm. A może wcale nie została zniszczona?
Inspirująca: urzeka swą tajemniczością i marzeniami o Utopii, świecie pokoju, piękna i harmonii. Wielu twórców poddało się jej urokowi i tak powstało wiele książek, filmów, obrazów, albumów muzycznych, nawet gier komputerowych, które starają się przenieść odbiorcę w to niesamowite miejsce.
Atlantydę, ustami Sophii Hapgood, twórcy gry „Indiana Jones and the Fate of Atlantis” opisują następująco:
Here, my friends, is Atlantis – as it might have appeared in its heyday. Glorious, prosperous, socially and technically advanced – beyond our wildest dreams. 5000 years ago – when everyone else still wore animal skins, the mighty spirits of Atlantis dared to build the city where knowledge and power were united in true happiness. Centuries later, the famous philosopher Plato wrote about it; he placed Atlantis on the continent out in the deep ocean.
Choć Atlantyda wydaje się być jedynie mitem, greckim podaniem ludowym usłyszanym i spisanym przez Platona, wielu ludzi wierzy w jej istnienie i nie zaprzestaje jej poszukiwań – prowadzonych naukowymi metodami. Polska Wikipedia informuje:
Mityczną wyspę umiejscawiano w różnych punktach na kuli ziemskiej:
u wybrzeży położonej na peryferiach Europy Skandynawii (autorem tej XVII-wiecznej koncepcji jest Olof Rudbeck),
w rejonie Spitsbergenu, na półkuli południowej (atlantyckie wyspy: Wniebowstąpienia i Św. Heleny miały być jej pozostałościami).
Później skupiono się ponownie na basenie Morza Śródziemnego, gdzie najczęściej wskazywano na wyspę Santoryn (Thira) na Morzu Egejskim. Potwierdzeniem istnienia Atlantydy na tym obszarze miały być wykopaliska prowadzone na Krecie, które wykazały ślady nieznanej dotąd kultury (minojskiej).
Doszukiwano się śladów zatopionego kontynentu w wyspach na Atlantyku: Maderze, Azorach i Wyspach Kanaryjskich. Na dwóch pierwszych nie istniało jednak przed ich odkryciem przez Europejczyków w XIV w. żadne osadnictwo, mieszkańcy Wysp Kanaryjskich, Guanczowie, odznaczający się rozwiniętą kulturą i techniką, zostali jednak wytępieni wcześniej przez Hiszpanów.
Wielu zwolenników ma teoria istnienia Atlantydy w Andach, w okolicach jeziora Titicaca. Stosowną teorię ukuł James M. Allen. Stwierdził on, że cała dzisiejsza Ameryka Południowa była podzielona na 10 królestw. Atlantyda miała się znajdować na płaskowyżu na terenie dzisiejszej Boliwii. Uwiarygodnieniem tej teorii miało być odkrycie na głębokości 27 metrów tajemniczych ruin budowli pod powierzchnią jeziora Titicaca. Dotyczy to również ruin miasta Tiahuanaco. Znaleziska odkryte na jego terenie sugerują, iż jego mieszkańcy opanowali zaawansowane technologie, jak obróbkę metali (wytop w temperaturze +1600oC, co oficjalnie opanowano dopiero w latach 30 XX wieku). Jego ludność obeznana była także ze stawianiem struktur, których elementy były idealnie dopasowane do siebie, niemal z chirurgiczną precyzją. Natomiast wiek Tihuanaco do dziś nie jest jasno określony. Skrajna teoria szacuje go na 16 tysięcy lat, inna na początek I tysiąclecia naszej ery.
Niepewne poszlaki wskazywały również na Karaiby, Morze Północne, Kaukaz i Amazonię. Istnieje też pogląd, jakoby Atlantydą była Antarktyda przed jej zlodowaceniem. Jego zwolennicy powołują się na mapę Orontiusa Fineusa.
Atlantydę identyfikuje się również z południowohiszpańskim antycznym miastem Tartessos.
Zwolennicy innych teorii zakładali, że niedobitki Atlantów (mieszkańców Atlantydy), ocalałych po zatopieniu wyspy, rozproszyły się po świecie, dając podwaliny pod wszystkie cywilizacje Ameryki, Azji i Europy.
Każdego roku powstają nowe koncepcje. W listopadzie 2004 roku amerykańscy naukowcy stwierdzili, że odkryli pozostałości Atlantydy na dnie morskim między Cyprem a Syrią. Pięć miesięcy wcześniej inni naukowcy wypatrzyli Atlantydę na zdjęciach satelitarnych południowej Hiszpanii. W obu przypadkach odkrywcy twierdzą, że to, co znaleźli, dokładnie pasuje do opisu Platona.
Najbardziej racjonalną wydaje się teoria, że mityczną Atlantydą jest Kreta. Według zwolenników tej teorii zagłada kultury minojskiej przypomina kataklizm opisany przez Platona. Podczas erupcji olbrzymiego wulkanu na Thirze powstała gigantyczna fala tsunami, która zniszczyła portowe miasta Krety, dla której handel morski stanowił podstawę gospodarki. Ponadto olbrzymie ilości pyłu wyrzucone podczas eksplozji w atmosferę doprowadziły do zmian klimatycznych trwających około 10 lat (ślady tych popiołów znaleziono, między innymi, w lodowcach Grenlandii), które sprowadziły nieurodzaj, co osłabiło gospodarkę minojskiej Krety. Osłabienie potężnego sąsiada wykorzystali Mykeńczycy z kontynentalnej Grecji, którzy ostatecznie podbili wyspę (czego śladem mają być opisy wojen Greków z Atlantami).
Odnalezione do tej pory dowody są tak nikłe, że o istnieniu Atlantydy można wypowiadać się tylko w terminach wiary. Co więcej, rozsądek podpowiada, że Atlantyda nie tyle nie istniała, co jest odzwierciedleniem jednej ze starożytnych cywilizacji, niefortunnie opisanej w Księgach przez Platona.
A ja jednak wierzę w istnienie Atlantydy – dokładnie takiej, jaka opisywana jest w literaturze czy pokazywana na ekranach telewizorów czy wyświetlaczy komputerowych: wspaniała, dumna, pełna pokoju i harmonii współgrającej z nowoczesną techniką wspomagającą życie jej mieszkańców. Jest to wiara nie poparta ani powodowana jakimikolwiek przesłankami, dowodami czy faktami, nie skłaniająca się ku żadnej współczesnej teorii – są to po prostu, jak to ujął autor jednego z artykułów, zaginione marzenia.
Wśród wszystkich utworów zainspirowanych Zaginionym Lądem, najbardziej godny uwagi wydaje mi się album Davida Arkenstone’a „Atlantis – A Symphonic Journey„. Wrażenie niesamowitej podróży przez czas do mitycznego lądu budują same nazwy poszczególnych utworów:
The Dream Of The Gods
Tower Of Light
In The Gardens Of The Citadel
The Temple Of Poseidon
Jewel Of The Sea
Festival Of The Goddess
The Painted Sails
Across The Great Oceans
Fire And Water
Below The Ocean – The Spirit Of Atlantis
Każdy utwór to osobna opowieść, pokazująca piękno Atlantydy, opisująca jej zagładę („Fire and Water„), a wreszcie przenosząca słuchacza do czasów współczesnych i opisująca – według mnie – podróż przez opustoszałe ulice, zniszczone świątynie i powalone pomniki zatopionego w odmętach oceanu miasta. Wreszcie dynamiczne zakończenie – będące jakby chwilowym powrotem do dni chwały, ostatnim hymnem ku chwale mieszkańców, którzy podzielili równie smutny, co okrutny los dumnego miasta.
Nieco okrojonej wersji utworu można posłuchać jako tła prezentacji znajdującej się w serwisie YouTube:
Dziś 3. listopada – patronem tego dnia jest święty Hubert.
Za Wikipedią:
Św. Hubert urodził się w 655 roku w Gaskonii, był potomkiem królewskiego rodu Merowingów. Zamiłowanie do polowania odziedziczył po swoim ojcu. Towarzyszył mu często na łowach. Ponoć w wieku 14 lat uratował ojcu życie podczas polowania na niedźwiedzia w Pirenejach. Pełnoletni Hubert udaje się na dwór króla Frankonii, gdzie poślubia córkę Pepina z Heristal. Miał z nią syna Floriberta, późniejszego następcę na biskupstwie Liege.
Młody Hubert najwięcej czasu spędzał w lasach, gdzie nieustannie polował, łowiectwo było jego pasją. Ponoć prowadził także swobodne, wręcz hulaszcze życie. Tak było do roku 695, kiedy polując w Górach Ardeńskich, nie bacząc na nic, w sam Wielki Piątek napotkał białego jelenia z promieniejącym krzyżem w wieńcu. Miał wtedy usłyszeć głos Stwórcy ostrzegający go za jego niepohamowaną pasję i nakazujący mu udać się do Lamberta – biskupa Maastricht – Tongres. Przejęty objawieniem, czyni jak mu głos nakazał. Udaje się na służbę bożą do biskupa Lamberta. Studiuje wiedzę kanoniczną i prowadzi działalność misjonarską w Ardenach i Brabancji. Po śmierci Biskupa Lamberta w 704 lub 705 roku, z rąk papieża Sergiusza otrzymuje sakrę biskupią.
3. listopada (lub w weekend poprzedzający lub następujący po tej dacie) we wszystkich większych stadninach i stajniach obchodzona jest hucznie zabawa zwana Hubertusem. Zaczyna się ona „pogonią za lisem” – jednemu z jeźdźców do lewego ramienia przypina się lisią kitę, którą pozostali jeźdźcy próbują zerwać. Alternatywą może być również sytuacja w której lisa ukrywa się gdzieś w lesie, a uczestnicy mają za zadanie odnalezienie go. Później następuje biesiada przy tradycyjnych potrawach – je się wtedy chleb ze smalcem, bigos oraz pieczoną dziczyznę.
Hubertus jest również obchodzony przez myśliwych – odbywają się wtedy uroczyste polowania podczas których zachowywane są historyczne wzorce i ceremoniały. Środowiska jeździeckie zrezygnowały z tradycyjnego niegdyś polowania na lisa, bowiem „zabawa” ta jest wyjątkowo okrutna – podobno niejednokrotnie zdarzało się, że zaszczute zwierzę zdychało nie od kuli myśliwego, a z wycieńczenia.
Święty Hubert jest patronem myśliwych, jeźdźców, leśników, strzelców, sportowców, kuśnierzy, matematyków i metalowców. Dzisiejszy dzień możemy zatem nazwać dniem jeźdźca – z tej okazji wszystkim jeźdźcom oraz amazonkom życzę wszystkiego najlepszego!
Potrzebowałem ostatnio funkcji, która sprawdzi, czy nazwa pliku pasuje do zadanej maski. W Internecie w wielu miejscach można znaleźć funkcję StringMatchesMask – jej pierwotna wersja leży chyba na Torrym. Problem polega jednak na tym, że stosuje strategię zachłanną podczas interpretacji gwiazdki, co powoduje z kolei, że stwierdzi, że nazwa adas.txt nie pasuje do wzorca *as.*.
Pozostało mi zatem napisanie poprawionej, rekurencyjnej wersji odpowiedniej funkcji. Rekordów prędkości pewnie nie pobije, ale za to działa poprawnie. Oto jej kod:
function StringMatchesMask(S, mask: string;
case_sensitive: Boolean = false): Boolean;
function InternalStringMatchesMask(PS, PMask: PChar): boolean;
begin
while PMask^ <> #0 do
begin
case PMask^ of
'?': begin
if PS^ = #0 then
begin
result := false;
exit
end;
inc(PMask);
inc(PS);
end;
'*': begin
inc(PMask);
if PMask^ = #0 then
begin
result := true;
exit
end;
while (PS^ <> #0) do
begin
if not (PMask^ in ['?', '*']) and (PMask^ <> PS^) then
begin
inc(PS);
if PS^ = #0 then
begin
result := false;
exit
end;
end else
begin
if InternalStringMatchesMask(PS, PMask) then
begin
result := true;
exit
end else
begin
inc(PS^);
if PS^ = #0 then
begin
result := false;
exit
end;
end;
end;
end;
result := false;
exit
end;
else begin
if PMask^ = PS^ then
begin
inc(PMask);
inc(PS);
end else
begin
result := false;
exit
end;
end;
end;
end;
if PS^ = #0 then
result := true else
result := false;
end;
begin
if case_sensitive then
result:=InternalStringMatchesMask(PChar(s),
PChar(mask)) else
result:=InternalStringMatchesMask(PChar(UpperCase(s)),
PChar(UpperCase(mask)));
end;