Przejdź do zawartości

GTK+/Analiza kodu

Z Wikibooks, biblioteki wolnych podręczników.

Analizę zostanie przeprowadzona w dwóch częściach, pierwszej gdzie kod będzie czytany po kolei, to analiza ogólna bez wyszczególnianie roli GTK+ w kodzie. Druga będzie przeprowadzona w kolejności wykonywania się kodu związanego z GTK+.


Plik nagłówkowy kalkulator.h zawiera definicje struktury, która pełni rolę nowego typu danych (odpowiednik klasy w C++). Pola tej struktury to wskaźniki oraz zmienne przechowujące stan kalkulatora.

      // pamięć zarządzana przez GTK+
	GtkWidget 	*window;
	GtkWidget 	*table;
	GtkWidget 	*entry;

Dla całego kalkulatora wystarczy nam tylko trzy wskaźniki do jego interfejsu graficznego - GtkWidget, z czego tylko jeden jest tak naprawdę wykorzystywany - entry.

	// pamiec zarządzana przez nas
	GString		*value1;
	GString		*value2;

Dwie kolejne składowe to wskaźniki do GString, to właśnie w nich będą przechowywane dwie wartości, na których należy wykonać określone działanie. Wartości te są otrzymywane w formie napisów a nie liczb.

	// numer operacji - działania
	// 1 + 
	// 2 -
	// 3 * 
	// 4 /
	gshort		 operation;
	// stan kalkulatora
	gboolean 	 is_value1;
	gboolean 	 is_value2;
	gboolean	 is_result;

Kolejnymi elementami są zmienne stanu:
operation - informuje jaki rodzaj działania ma być wykonany na wartościach value1 i value2;
is_value1 oraz is_value2 - informują, które z tych dwóch liczb obecnie są zdefiniowane;
is_result - dzięki niej wiemy, że miało miejsce jakieś działanie i otrzymano wynik, jest to istotne w sytuacji gdy do wyniku widocznego w polu GtkEntry zamierzamy np. dodać kolejną liczbę.

	// ostatnio wciśnięty przycisk
	// 1  1
	// 2  2
	// ...
	// 9  9
	// 0  0
	// 10 +
	// 11 -
	// 12 *
	// 13 /
	// 14 =
	// 15 ,
	// 16 <-
	// 17 C
	gint8        prev_button;

Za pomocą zmiennej prev_button śledzimy jaki przycisk został wciśnięty jako ostatni. Pozwala to np. stwierdzić, iż przycisk sumy (=) został już wciśnięty i nie dopuścić do tego, aby kolejne jego wciśnięci spowodowało ponowne wykonanie działania na wyniku (przechowywany w pierwszej zmiennej value1) i wartością z drugiej zmiennej value2.


Plik kalkulator.c:

//#define DEBUG
 
 
// funkcja debugująca
void info (Kalkulator * pkalkulator, gchar* event)
{
	Kalkulator *kalkulator = pkalkulator;
 
	printf ("\n =============\n %s\n - - - - -\nStruktura:\n \
		value1: %s \n \
		value2: %s \n \
		is_value1: %d \n \
		is_operatn2: %d \n \
		is_result: %d \n \
		prev_button: %d \n - - - - -\n",
		event,
		kalkulator->is_value1 == TRUE? kalkulator->value1->str :"",
		kalkulator->is_value2 == TRUE? kalkulator->value2->str :"",
		kalkulator->is_value1,
		kalkulator->is_value2,
		kalkulator->is_result,
		kalkulator->prev_button);
 
}

Jak widzimy obecnie debugowanie jest wyłączone. Funkcja info() został wstawiona w różnych miejscach programu - możesz wstawić ją w miejscach, które Cię interesują. Funkcja pobiera dwa parametry, wskaźnik do struktury Kalkulator oraz napis, który określa w drukowanym komunikacje jakiego zdarzenia dotyczy obecny wydruk wartości ze struktury.

// funkcja odpowiadająca za właściwe wykonywanie obliczeń na 2 wartościach
void licz(Kalkulator *kalkulator)
{
	double  d_value1, d_value2, result;
	char    s_result[50];
 
	d_value1 = strtod( (char*)g_string_free (kalkulator->value1, FALSE),
						NULL );
	d_value2 = strtod( (char*)g_string_free (kalkulator->value2, FALSE), 
						NULL );
	#ifdef DEBUG
	printf ("liczby to %f , %f \n",d_value1,d_value2);
	#endif
 
	switch (kalkulator->operation)
	{
	case 1:
		result = d_value1 + d_value2;
		break;
	case 2:
		result = d_value1 - d_value2;
		break;
	case 3:
		result = d_value1 * d_value2;
		break;
	case 4:
		if ( d_value2 >= 1 )
			result = d_value1 / d_value2;
		else
			result = d_value1;
		break;
	}
	sprintf (s_result,"%f",result);
	kalkulator->value1 = g_string_new (s_result);
	/*
	kalkulator->value1 (wynik)
	kalkulator->value2 (puste)
	*/
}

Funkcja ta, to odseparowany od reszty kodu mechanizm zamiany napisów na liczby, wykonaniu obliczeń - w tym obsługa dzielenia przez zero, ponownej zamiany - tym razem z liczby na napis oraz umieszczeniu wyniku w pierwszej zmiennej, druga zostaje wyczyszczona. Jeżeli następnym działaniem będzie dodanie do wyniku kolejnej liczby, liczba ta zostanie umieszczona w value2 i dodana do wyniku poprzedniego działania z value1.
Choć funkcja ta pobiera wskaźnik do struktury Kalkulator to sama nie ustawia flag dotyczących stanu zmiennych value1 oraz value2 (czyli is_value1, is_value2). Obecnie robi to funkcja nadrzędna (z której została wywołan), nie jest to dobrym rozwiązaniem biorąc pod uwagę fakt, iż funkcja licz() i tak ma pośredni dostęp do tych flag i całej struktury.
Drugim rozwiązaniem jest zmiana funkcji w taki sposób, aby pobierała cztery parametry - dwa napisy, rodzaj działania oraz wskaźnik do wyniku. W ten sposób odseparowalibyśmy całkowicie tą funkcję od naszego programu.
Kolejną sprawą jest niepoprawne zabezpieczenie przez dzieleniem przez zero - założony warunek nie zadziała na liczby ujemne np. -1 albo liczby zbliżone do zera np. 0,08. Niemniej jednak nie ma to na razie większego znaczenia, ponieważ nasz kalkulator jeszcze nie obsługuje znaku -. Chodzi tu o brak odpowiedniego przycisku. W każdym bądź razie wynik może być ujemny, problem polega na tym, że nie mamy możliwości wprowadzenia liczby ze znakiem -.
Zostaje jeszcze wyjaśnienie kwestii zwalnianej pamięci. Jak zapewne zauważyłeś, zmienne typu GString podczas pobierania z nich wartości jednocześnie jest zwalniana przez nich pamięć, którą alokowały. Do pierwszej zmiennej zostanie przydzielona nowa pamięć w ciele funkcji licz(), następnie umieszcza tam wynik. Druga zmienna value2 zostaje puszczona bez przydziału pamięci i w dodatku bez ustawiania flagi is_value2 na wartość FALSE. Nowa pamięć zostanie przydzielona w funkcji jednego z czterech dostępnych działań (+,-,*,/), tuż przed ponownym wywołaniem funkcji licz(). Jest to istotny fakt. Gdyby gdzieś "po drodze" jakiś kod próbował uzyskać dostęp do wartości tej zmiennej spowodowało by to błąd ochrony pamięci i "wysypanie" się programu. Nie stanie to się jednak ponieważ flaga informująca, że zmienna ta jest pusta zostanie ustawiona zaraz po wywołaniu funkcji licz() w funkcji nadrzędnej.

//==========================================
// obsługa zdarzeń przycisków numerycznych
static void btn_1_clicked( GtkWidget *widget,
			   gpointer   pkalkulator )
{
	g_print ("clicked: 1\n");
	Kalkulator *kalkulator = pkalkulator;
 
	if ( kalkulator->is_result ) 
		gtk_entry_set_text ( GTK_ENTRY( kalkulator->entry ),"");
 
	gtk_entry_append_text ( GTK_ENTRY(kalkulator->entry),"1");
	kalkulator->prev_button = 1;
}

Następne w kodzie są funkcje obsługujące zdarzenie clicked dla przycisków numerycznych. Każda funkcja licząc od btn_1_clicked do btn_0_clicked (czyli w sumie 10 funkcji) wyglądają tak samo).
Funkcja otrzymuje wskaźnik do naszej centralnej struktury danych oraz informacje o stanie kalkulatora i wykonanych operacji. Sprawdza, czy w kontrolce GtkEntry (naszym wyświetlaczu) nie znajduje się wynik poprzedniego działania. Jeżeli tak, to czyścimy tekst w kontrolce. Wynik jest zawsze przechowywany w zmiennej value1. Teraz możemy dodać do końca wybraną cyfrę. Bez wspomnianego sprawdzania i wyczyszczenia znajdującej się na wyświetlaczu wartości obecnie dodawana cyfra, w tym przypadku 1, zostałaby dopisana na koniec poprzedniego wyniku. Dla liczby 12 otrzymalibyśmy 13,0912 przy założeniu, że wynikiem z poprzedniego działania była liczba 13,09.

// obsługa zdarzeń przycisków funkcyjnych
static void btn_comma_clicked( GtkWidget *widget,
	  		       gpointer   pkalkulator )
{
	g_print ("clicked: ,\n");
	Kalkulator *kalkulator = pkalkulator;
	gtk_entry_append_text ( GTK_ENTRY(kalkulator->entry),",");
	kalkulator->prev_button = 15;
}

Po funkcjach obsługi przycisków numerycznych znajduje się funkcja obsługi przycisku wstawiającego przecinek. Jest to bardzo prosta funkcja, wstawia jedynie przecinek bez sprawdzania żadnych warunków - co jest oczywistym błędem! Ponieważ pozwala wstawić przecinek jako pierwszy np. ,13 albo dwa razy np. 2,34,22 co na pewno skończy się błędem podczas konwersji na typ double.

static void btn_back_clicked( GtkWidget *widget,
			      gpointer   pkalkulator )
{
	g_print ("clicked: <-\n");
	Kalkulator *kalkulator = pkalkulator;
	GString *tmp=NULL;
 
	tmp = g_string_new (
			gtk_entry_get_text (GTK_ENTRY(kalkulator->entry)));
	tmp = g_string_truncate ( tmp, tmp->len - 1 );
	gtk_entry_set_text ( GTK_ENTRY( kalkulator->entry ), 
			     g_string_free( tmp, (gboolean)NULL ) );
	kalkulator->prev_button = 16;
}

Ta funkcja obsługuje cofnięcia/backspace - jak kto woli. Pobiera ona tekst z wyświetlacza a następnie obcina jego ostatni znak. Oczywiście na koniec (podobnie jak wcześniej omawiana funkcja) oznacza swoje wciśnięcie poprzez nadanie odpowiedniej wartości dla zmiennej prev_button.

static void btn_clear_clicked( GtkWidget *widget,
			       gpointer   pkalkulator )
{
	g_print ("clicked: C\n");
	Kalkulator *kalkulator = pkalkulator;
 
	kalkulator->operation = 0;
 
	if (kalkulator->is_value1 == TRUE)
	{
		g_string_free (kalkulator->value1,TRUE); 
		kalkulator->value1 = g_string_new (NULL); 
	}
	if (kalkulator->is_value2 == TRUE)
	{
		g_string_free (kalkulator->value2,TRUE); 
		kalkulator->value2 = g_string_new (NULL); 
	}
 
	kalkulator->is_value1 = FALSE;
	kalkulator->is_value2 = FALSE;
	kalkulator->is_result = FALSE;
 
	gtk_entry_set_text ( GTK_ENTRY(kalkulator->entry),"");
 
	#ifdef DEBUG
	info(kalkulator,"clear");
	#endif
	kalkulator->prev_button = 17;
}

Funkcja zwrotna btn_clear_clicked() to obsługa przycisku (C) - resetującego wszystkie ustawiania oraz zapisane wartości. Sprawdza czy value1 oraz value2 posiadają jakieś wartości, jeżeli tak używana pamięć zostaje zwolniona. Następnie są czyszczone wszystkie flagi informacyjne. Na koniec czyszczony jest wyświetlacz oraz zgodnie z przyjętą zasadą ustawiana zmienna ostatnio wciśniętego przycisku.

// obsługa zdarzeń przycisków działań
static void btn_add_clicked( GtkWidget *widget,
			     gpointer   pkalkulator )
{
	g_print ("clicked: +\n");
	Kalkulator *kalkulator = pkalkulator;
	gint8 test = 0;
	gchar * text_entry = GTK_ENTRY(kalkulator->entry)->text;
 
	// +
	kalkulator->operation = 1;
	#ifdef DEBUG
	info(kalkulator,"w funkcji +");
	#endif
 
	// test
	if ( kalkulator->is_value1 == FALSE && 
	     kalkulator->is_value2 == FALSE &&
	     strlen (text_entry) > 0 )
	{
		// nalezy ustawić value1
		test = 1;
	}
	if ( kalkulator->is_value1 == TRUE && 
	     kalkulator->is_value2 == FALSE &&
	     strlen (text_entry) > 0 &&
	     kalkulator->prev_button != 10 &&
	     kalkulator->prev_button != 11 &&
	     kalkulator->prev_button != 12 &&
	     kalkulator->prev_button != 13 &&
	     kalkulator->prev_button != 14 &&
	     kalkulator->prev_button != 15 &&
	     kalkulator->prev_button != 16  ) 
	{
		// ustawic value2 i obliczyć wynik
		test = 2;
	}
 
	switch (test)
	{
		case 1:
			kalkulator->value1 = g_string_new ( 
					gtk_entry_get_text(GTK_ENTRY(kalkulator->entry)));
			kalkulator->is_value1 = TRUE;
			gtk_entry_set_text ( GTK_ENTRY( kalkulator->entry ),"");
			break;
 
		case 2:
			kalkulator->value2 = g_string_new (
					gtk_entry_get_text(GTK_ENTRY(kalkulator->entry)));
   		 	kalkulator->is_value2 = TRUE;
			licz (kalkulator);
			gtk_entry_set_text (GTK_ENTRY (kalkulator->entry),
				 	    (gchar*)kalkulator->value1->str);
			kalkulator->is_value1 = TRUE;
			kalkulator->is_value2 = FALSE;
			kalkulator->is_result = TRUE;
			break;
	}
 
	#ifdef DEBUG
	info(kalkulator,"koniec funkcji +");	
	#endif
	kalkulator->prev_button = 10;
}

Zawartość funkcji obsługującej przyciski działań są takie same dla każdego działania (+,-,*,/) - podobnie jak funkcje obsługi przycisków numerycznych. Są to też najciekawsze funkcje z całego programu.
Pojawia się tu zmienna test, będzie przechowywała wynik testu sprawdzającego jakie działania są możliwe, a jakie operacje powinny zostać wykonane. Zmiennej text_entry przypisujemy wartość jaka obecnie znajduje się w wyświetlaczu. Potrzebna będzie nam do sprawdzenia czy przypadkiem nie jest pusta. Następnie co robimy to ustawiamy znany nam już rodzaj działania, w tym przypadku użytkownik wcisnął przycisk (+). Czas na test, który ustali co należy zrobić.
Pierwszy test polega na sprawdzeniu czy nie podano żadnej liczby wcześniej, jeżeli tak to czy została wprowadzona liczba do pobrania z wyświetlacza?. Jeżeli tak to znaczy, że mamy doczynienia z sytuacją kiedy została podana pierwsza liczba a następnie użytkownik wcisną przycisk (+). Jest to pożądana sytuacja, oznacza, że pomyślnie zdefiniowano pierwszą liczba.
Drugi test jest trochę bardziej rozbudowany, sprawdza czy przycisk (+) został wywołany w celu zsumowania dwóch liczb wcześniej wprowadzonych. Ma to miejsce, gdy podamy pierwszą liczbę, wybierzemy działanie (w tym przypadku dodawanie), następnie podamy druga liczbę i zamiast wciśnięcia przycisku sumy (=) zostanie wciśnięty ponownie przycisk działania (+). trzy pierwsze linie warunku drugiego są takie same jak pierwszego, poza tym, że pierwsza liczba musi być już podana. Kolejne warunki to zabezpieczenie przed wykonaniem podwójnego obliczenia w przypadku gdy ostatnio został wciśnięty ten sam przycisk, lub którykolwiek z przycisków działania (+,-,*,/), sumy (=), przecinka (,) oraz usunięcia ostatniej cyfry (<-). Dwa ostatnie z logicznego punktu widzenia nie są wymagane, (,) powinien być obsługiwany w funkcji btn_comma_clicked(), wówczas zbędna była by powyższa restrykcja. Ostatni warunek (<-) praktycznie jest zbędny. Gdy już wszystkie warunki zostaną spełnione zmienna test zostanie ustawiona na wartość równą 2, co oznacza, że jest już zdefiniowana pierwsza zmienna, na wyświetlaczu znajduje się druga, którą trzeba pobrać a następnie wykonać określone działanie. Oczywiście wszystkie niepożądane sytuacje zostały wykluczone. Jest to jednoznaczne z wykorzystaniem przycisku (+) w roli przycisku sumowania (=).
Teraz gdy już wiemy co należy zrobić zostaje wykonać tylko odpowiednie operacje. Pierwsza z nich mam miejsce gdy zmienna test jest równa 1. Wówczas do naszej wewnętrznej struktury dodajemy pierwszą liczbę, ustawiamy flagę informującą jej zdefiniowanie oraz czyścimy wyświetlacz przygotowując w nim miejsce na drugą wartość. Zostaje tu również przypisana pamięć dlavalue1. Będzie to miało rzadko miejsce, ponieważ w przyszłości (podczas obliczeń) w funkcji licz() zmienna ta zostanie zwolniona a następnie ponownie wykorzystana na przechowywanie wyniku działania. Kolejne operacje na tym wyniku będą dotyczyły już tylko kolejnej sytuacji, ponieważ is_value1 (czyli wynik) będzie miało wartość TRUE. Kolejne przydzielenie pamięci w tym miejscu nastąpi gdy będziemy chcieli wykonać nowe obliczenia niezwiązane z obecnym wynikiem. Wtedy należy użyć przycisku czyszczącego poprzednie dane, czyli (C), a kolejne obliczenia doprowadzą do ponownego przypisania pamięci w tym miejscu.
Druga sytuacja polega na pobraniu drugiej wartości liczbowej w identyczny sposób jak to miało miejsce w pierwszej sytuacji. Teraz mamy już dwie wartości i rodzaj działania, wywołujemy wcześniej opisywaną funkcję licz(). Zwróć uwagę, że przed nią została zaalokowana pamięć dla zmiennej value2, która mogła być wcześniej zwolniona z funkcji licz() (sytuacja ta została szczegółowo omówiona w opisie tej funkcji). Gdy funkcja licz() zakończy swoje działanie, a my będziemy w posiadaniu wyniku, który został umieszczone przez tą funkcję w naszej wewnętrznej strukturze możemy go wreszcie wyświetlić na wyświetlaczu kalkulatora. na końcu ustawiamy flagi związane z tą sytuacją. Ostatnią czynnością funkcji btn_add_clicked() jest ustawienie swojego identyfikatora w zmiennej identyfikującej ostatnio użyty przycisk.