GTK+/Ogólna analiza kodu

Z Wikibooks, biblioteki wolnych podręczników.

Analiza zostanie przeprowadzona w dwóch częściach. Pierwszej, gdzie kod będzie czytany po kolei, to analiza ogólna bez wyszczególniania roli GTK+ w kodzie. Druga będzie przeprowadzona w kolejności wykonywania się kodu z użyciem biblioteki 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ęcie 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ła 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 zostanie wydrukowany w komunikacje jako zdarzenie dotyczące wydruku 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łana), 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 niemniej 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 ),"");
		kalkulator->is_result = FALSE;
	}
 
	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, wpisuje 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 lub 42,,46 co na pewno nie jest pożądane. Niemniej funkcja strtod() z pewnością poradzi sobie z tymi błędami na swój sposób.

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, FALSE ) );
	kalkulator->prev_button = 16;
}

To funkcja obsługuje backspace (<-). 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  ) // ,
	{
		// 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ługi przycisku dodawania (+) jest taka sama dla każdego z działań (+,-,*,/) - 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, jakie operacje powinny zostać wykonane. Zmiennej text_entry przypisujemy wartość jaka obecnie znajduje się na 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 jakaś liczba do wyświetlacza, którą moglibyśmy pobrać?. Jest to związane z sytuacją, kiedy została podana pierwsza liczba, a następnie użytkownik wcisną przycisk (+). To pożądana sytuacja, oznacza, że pomyślnie zdefiniowano pierwszą liczbę.
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) a następnie podamy drugą 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 się 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 (,). Ten ostatni z logicznego punktu widzenia nie jest wymagany, (,) powinien być obsługiwany w funkcji btn_comma_clicked(), wówczas zbędna była by powyższa restrykcja. 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ć i 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 ma 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 go na podanie drugiej wartości. 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 do przechowywanie wyniku wykonanego 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. Następne 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 bieżące dane, czyli (C). Kolejne obliczenia doprowadzą do ponownego przypisania pamięci dla zmiennej value1.
Druga sytuacja polega na pobraniu drugiej liczby 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 w 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 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 prev_button.

static void btn_result_clicked( GtkWidget *widget,
				gpointer   pkalkulator )
{
	g_print ("clicked: =\n");
	Kalkulator *kalkulator = pkalkulator;
	// zamiast:    ((Kalkulator*)pkalkulator)->operation 
 
	gchar * text_entry = GTK_ENTRY(kalkulator->entry)->text;
 
	#ifdef DEBUG
	info(kalkulator,"w funkcji =");
	#endif
 
	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->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;
	}
 
	#ifdef DEBUG
	info(kalkulator,"koniec funkcji =");	
	#endif
	kalkulator->prev_button = 14;
}

Funkcja btn_result_clicked() to nic innego jak obsługa przycisku sumującego (=). Nie różni się ona niczym od poprzednio omawianej funkcji btn_add_clicked() poza tym, że sprawdzany jest tylko warunek drugi. Ujmując rzecz wprost, obliczanie jest możliwe tylko wtedy, gdy zostały już podane dwie liczby.

// obsługa zdarzeń emitowanych podczas zamykania programu
static gboolean delete_event( GtkWidget *widget,
			      GdkEvent  *event,
			      gpointer   data )
{
	g_print("delete_event: FALSE\n");
	return FALSE;
}
 
static void destroy( GtkWidget *widget,
		     gpointer  pkalkulator )
{
	g_print("destroy: gtk_main_quit | g_free\n");
	Kalkulator *kalkulator = pkalkulator;
 
	gtk_main_quit ();
 
	if (kalkulator->is_value1 == TRUE)
	{
		g_string_free (kalkulator->value1,TRUE); 
	}
	if (kalkulator->is_value2 == TRUE)
	{
		g_string_free (kalkulator->value2,TRUE); 
	}
	g_free ( kalkulator );
}

Funkcje wywoływane podczas zamykania programu w GTK+. Interesuje nas funkcja destroy(). Zwalnia ona zarezerwowaną pamięć dla wskaźników ze struktury Kalkulator oraz pamięć zarezerwowaną przez samą strukturę.

int main( int argc, char *argv[] )
{
	Kalkulator *kalkulator;
	kalkulator = g_malloc ( sizeof( Kalkulator ) );
	kalkulator->operation = -1;
	kalkulator->is_value1 = FALSE;
	kalkulator->is_value2 = FALSE;
	kalkulator->is_result = FALSE;
...

To ostatnia część omawiana w ogólnej analizie kodu. Dalszy kod jest już ściśle związany z budową interfejsu graficznego. Pierwszą rzeczą jaką robimy w głównej funkcji programu jest utworzenie nowej struktury na potrzeby programu. Jej kluczowe flagi ustawiane są na wartości początkowe. Wartość -1 dla zmiennej operation oznacza brak zdefiniowanego działania. Jest to wartość umowna, w dodatku nigdy nie sprawdzana w kodzie.