Progmar Marcin Załęczny

Refaktoryzacja kodu

Z czasem, gdy pisany przez nas kod będzie większy i większy, umieszczanie go w jednym pliku spowoduje trudniejszą analizę kodu. Dlatego rozbijmy to co napisaliśmy na klasy, żeby zwiększyć czytelność kodu. Przyjmijmy, że obiekt buildera oraz funkcje pomocnicze interfejsu użytkownika umieścimy w klasie Gui. Natomiast każde okno utworzone przez buildera będzie miało własną odrębną klasę.

Program podzielimy na pliki: Gui.h, Gui.cpp, Window.h, Window.cpp, MainWindow.h, MainWindow.cpp, main.cpp. Nasz prosty program nieco się rozrośnie, ale korzyść z takiego podziału docenimy później w przypadku bardziej zaawansowanych aplikacji.
Omówmy teraz poszczególne pliki.

Gui.h


#ifndef __GUI_H__
#define __GUI_H__

#include <gtk/gtk.h>

class Gui {
public:
	GtkBuilder *builder;
	
	Gui();
	virtual ~Gui();
	gboolean loadInterface(const gchar *filename);
	GtkWidget* getWidget(const gchar *widgetName);
	GtkResponseType msgBoxYesNo(const gchar *message);
};

#endif
	

Gui.cpp


#include "Gui.h"

Gui::Gui() {
	this->builder = gtk_builder_new();
}

Gui::~Gui() {
	g_object_unref(this->builder);
	this->builder = NULL;
}

gboolean Gui::loadInterface(const gchar *filename) {
	GError *error;
	
	error = NULL;
	gtk_builder_add_from_file(this->builder, filename, &error);
	if (error) {
		g_print("Wystąpił błąd: %s\n", error->message);
		g_error_free(error);
		return FALSE;
	}
	return TRUE;
}

GtkWidget* Gui::getWidget(const gchar *widgetName) {
	return GTK_WIDGET(gtk_builder_get_object(this->builder, widgetName));
}

GtkResponseType Gui::msgBoxYesNo(const gchar *message) {
	GtkWidget *dialog;
	dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, message);
	gtk_window_set_title(GTK_WINDOW(dialog), "Pytanie");
	GtkResponseType result = (GtkResponseType)gtk_dialog_run(GTK_DIALOG(dialog));
	gtk_widget_destroy(dialog);
	return result;
}
	

W plikach Gui.h i Gui.cpp zdefiniowaliśmy klasę pomocniczą Gui. Ma ona następujące metody:

  • konstruktor - w nim tworzymy obiekt typu GtkBuilder i zapamiętujemy go w atrybucie klasy.
  • destruktor wirtualny - w nim niszczymy obiekt GtkBuilder poprzez zwolnienie referencji do niego. Funkcja g_object_unref zmniejsza liczbę referencji do obiektu o 1 i jeśli ta liczba spadła do zera, to niszczy obiekt.
  • loadInterface - funkcja ta służy do wczytywania interfejsu użytkownika (stworzonego przez program Glade) z pliku. Jeśli interfejs zostanie poprawnie wczytany, to funkcja zwróci TRUE. W przeciwnym przypadku wypisze na standardowym wyjściu informację o błędzie i zwróci FALSE.
  • getWidget - funkcja ta zwraca wskaźnik na obiekt o podanej nazwie.
  • msgBoxYesNo - wyświetla modalne okienko dialogowe z podanym pytaniem i przyciskami TAK, NIE. Zwraca typ wciśniętego przycisku.

Window.h


#ifndef __WINDOW_H__
#define __WINDOW_H__

#include <gtk/gtk.h>

class Window {
public:
	GtkWidget *wnd;
	
	Window(GtkWidget *wnd);
	virtual ~Window();
	void show();
};

#endif
	

Window.cpp


#include "Window.h"

Window::Window(GtkWidget *wnd) {
	this->wnd = wnd;
}

Window::~Window() {
	gtk_widget_destroy(this->wnd);
	this->wnd = NULL;
}

void Window::show() {
	gtk_widget_show_all(this->wnd);
}
	

W plikach Window.h i Window.cpp zdefiniowaliśmy ogólną klasę Window przeznaczoną do generycznych operacji na oknie.
Ma ona następujące metody:

  • konstruktor - przypisuje przekazany mu wskaźnik na okno do wewnętrznego atrybutu.
  • destruktor wirtualny - w nim niszczymy przechowywany obiekt GtkWindow przy pomocy funkcji gtk_widget_destroy().
  • show - metoda pokazująca okno wraz z całą zawartością.

MainWindow.h


#ifndef __MAIN_WINDOW_H__
#define __MAIN_WINDOW_H__

#include <gtk/gtk.h>
#include "Window.h"

class MainWindow : public Window {
public:
	MainWindow(GtkWidget *wnd);
	virtual ~MainWindow();
	
	static void onDestroy(GtkWidget* widget, gpointer data);
	static gboolean onDeleteEvent(GtkWidget* widget, GdkEvent* event, gpointer data);
};

#endif
	

MainWindow.cpp


#include "MainWindow.h"
#include "Gui.h"

extern Gui *gui;

MainWindow::MainWindow(GtkWidget *wnd) 
	: Window(wnd) {
	g_signal_connect(G_OBJECT(this->wnd), "delete-event", G_CALLBACK(MainWindow::onDeleteEvent), NULL);
	g_signal_connect(G_OBJECT(this->wnd), "destroy", G_CALLBACK(MainWindow::onDestroy), NULL);
}

MainWindow::~MainWindow() {
}

// ==================================================================================================
// Static functions - event handlers
// ==================================================================================================
void MainWindow::onDestroy(GtkWidget* widget, gpointer data) {
	gtk_main_quit();
}

gboolean MainWindow::onDeleteEvent(GtkWidget* widget, GdkEvent* event, gpointer data) {
	if (gui->msgBoxYesNo("Czy na pewno chcesz opuścić program?") == GTK_RESPONSE_YES) {
		return FALSE;
	}
	return TRUE;
}
	

W plikach MainWindow.h i MainWindow.cpp zdefiniowaliśmy szczegółową klasę MainWindow przeznaczoną do obsługi naszego okna głównego. Dziedziczy ono po klasie Window.
Ma następujące metody:

  • konstruktor - wywołuje konstruktor nadrzędny przekazując mu wskaźnik na obiekt okna oraz podpina do sygnałów delete-event oraz destroy statyczne metody tej klasy.
  • destruktor wirtualny - ten destruktor aktualnie nic nie robi, gdyż zwolnieniem pamięci zajmowanej przez okno zajmuje się destruktor klasy nadrzędnej.
  • onDestroy - statyczna metoda obsługująca sygnał destroy. Kończy działanie funkcji gtk_main()
  • onDeleteEvent - statyczna metoda obsługująca sygnał delete-event. Pyta czy na pewno zamknąć program i jeśli tak, to kończy działanie aplikacji.

Uwaga! Jako funkcje obsługi zdarzeń możemy podpinać metody klas, ale należy pamiętać że muszą to być metody statyczne.

main.cpp


#include <gtk/gtk.h>
#include "Gui.h"
#include "MainWindow.h"

Gui *gui;

int main(int argc, char** argv) {
	gui = new Gui();
	MainWindow *window;
	
	gtk_init(&argc, &argv);
	
	if (!gui->loadInterface("main_window.ui")) {
		return -1;
	}
	window = new MainWindow(gui->getWidget("wndHelloWorld"));
	window->show();
	
	gtk_main();
	
	delete window;
	delete gui;
	return 0;
}
	

Na koniec został do omówienia nasz plik z głównym programem. Widzimy, że stał się bardziej przejrzysty. Do załadowania okna z pliku main_window.ui służy metoda gui->loadInterface(). Jeśli się nie udało załadować interfejsu, to program kończy działanie. W przeciwnym razie tworzony jest obiekt klasy HelloWorldWindow wraz ze wskaźnikiem na nasze okno pobranym z obiektu gui metodą gui->getWidget().

Po wyjściu z pętli gtk_main() musimy jeszcze zwolnić pamięć zajmowaną przez obiekt okna i gui. Zauważ, że obiekt gui został zadeklarowany na zewnątrz funkcji main() - ma on zasięg globalny. Zrobiliśmy tak, żeby łatwo można było sięgać do metod klasy gui z innych miejsc w kodzie, w tym w metodach statycznych, które obsługują sygnały okna HelloWorldWindow.

Program kompilujemy poleceniem: make. Cały projekt utworzony w tym rozdziale będziemy traktować jako punkt wyjścia do tworzenia nowych programów.