Qt – 2 – Απαιτούμενα εργαλεία για τη δημιουργία πλαισίων διαλόγων

2 – Απαιτούμενα εργαλεία για τη δημιουργία πλαισίων διαλόγων

 

Τώρα που έχουμε μια γενική εικόνα της Qt, θα επιστρέψουμε σε μια πρακτική εφαρμογή για να δουμε πως συνεργάζονται οι κλάσεις μεταξύ τους. Το πρώτο εκτενές πρόγραμμα θα μετατρέπει αριθμούς μεταξύ δεκαδικών, δεκαεξαδικών και δυαδικών μορφών, οπως φαίνεται στο σχεδιάγραμμα 2.1

 

Ο χρήστης του προγράμματος δύναται να εισάγει έναν αριθμό μονού byte (από 0 εώς 255) σε ένα από τα 3 πεδία εισόδου.  Το πρόγραμμα ανανεώνει τα άλλα δύο πεδία εισόδου με την τιμή μετατροπής

 

2.1 Ποιά είναι η διαφορά μεταξύ παραθύρων διαλόγων και Widgets;

 

Η κύρια συνάρτηση main() είναι σχεδόν ίδια με την συνάρτηση main() του προγράμματος “Hello World” που εξετάσαμε στην ενότητα 1.1:

 

// byteConverter/main.cpp

 

#include <QApplication>

#include “ByteConverterDialog.h”

 

int main(int argc, char *argv[])

{

QApplication a(argc, argv);

ByteConverterDialog bc;

bc.setAttribute(Qt::WA_QuitOnClose);

bc.show();

 

return a.exec();

}

 

Υπάρχει μόνο μία εξαίρεση: Η κλάση QLabel έχει αντικατασταθεί με την ByteConvertedDialog.

Αυτή η κλάση κληρονομείται από την QDialog και ο ορισμός της τοποθετείται στην κεφαλή του αρχείου ByteConverterDialog.h 1 H οδηγία #include η οποία ενσωματώνει το αρχείο κεφαλής στον κώδικα της εφαρμογής κάνει χρήση των εισαγωγικών (“) αντί των τριγωνικών υποστηριγμάτων (<>), εφόσον το αρχείο βρίσκεται στον ίδιο φάκελο με την main.cpp.

 

Έχουμε εισάγει επίσης, το χαρακτηριστικό WA_QuitOnClose στο πλαίσιο διαλόγου, ώστε να διασφαλίσουμε τον ομαλό τερματισμό του εφόσον κλείσει. Η προσθήκη αυτή δεν ήταν απαραίτητη στα προηγούμενα παραδείγματα, καθώς δεν κάναμε χρήση κλάσεων που κληρονομήθηκαν από την QDialog όπως στο κύριο παράθυρο. Εφόσον τα πλαίσια διαλόγων συνήθως παρέχουν δια-πληροφορίες, το χαρακτηριστικό αυτό δεν είναι ενεργοποιημένο εξ ορισμού για το QDialog. Ουτοσιάλως το κλείσιμο ενός πλαισίου διαλόγου, δεν πρέπει να προκαλεί τον τερματισμό του προγράματος, εκτός εάν υπάρχει ένα σοβαρό σφάλμα.

 

Περικλείουμε τα περιεχόμενα του αρχείου ByteConverterDialog.h με χαρακτήρες συμπερίληψης, αποτελούμενο απο τρεις εντολές προεπεξεργαστή #ifndef , #define και #endif:

 

1 Για τα αρχεία κεφαλής που δημιουργήσαμε εμείς, κάνουμε χρήση του προτύπου της C/C++ , την επέκταση .h, ώστε να είναι ξεκάθαρο το είδος του αρχείου.

 

// byteConverter/ByteConverterDialog.h

 

#ifndef BYTECONVERTERDIALOG_H

#define BYTECONVERTERDIALOG_H

 

#include <QDialog>

class QLineEdit;

 

class ByteConverterDialog : public QDialog

{

Q_OBJECT

 

public:

ByteConverterDialog();

 

private:

QLineEdit* decEdit;

QLineEdit* hexEdit;

QLineEdit* binEdit;

};

 

#endif

 

Η χρήση των χαρακτήρων συμπερίληψης είναι μια σύνηθες τεχνική στον προγραματισμό C/C++ για(την) αποφυγή προβλημάτων που προκύπτουν εάν πάνω από ένα αρχείο κώδικα επιχειρήσει την ενσωμάτωση ενός αρχείου κεφαλής με χρήση της #include, το οποίο συμβαίνει συνήθως σε μεγάλα προγράμματα με πολλές ανεξάρτητες υλοποιημένες ενότητες. Σε αυτή την περίπτωση, κατά την πρώτη εκτέλεση του ByteConverterDialog.h , ορίζεται η λέξη κλειδί BYTECONVERTERDIALOG_H. Εάν ένα μετέπειτα πηγαίο αρχείο προσπαθήσει να εκτελέσει την εντολή #include ByteConverterDialog.h ξανά, η οδηγία #ifndef …endif (“if not defined”) κάνει τον προεπεξεργαστή να παραλείψει τα περιεχόμενα των αρχείων κεφαλής. Χωρίς τους χαρακτήρες συμπερίληψης, ο μεταγλωττιστής θα είχε επισημάνε οτι οι λέξεις κλειδιά και οι κλάσεις έχουν οριστεί πολλές φορές και θα έβγαζε ένα μήνυμα λάθους.

Συμπεριλαμβάνουμε το αρχείο κεφαλής QDialog, εφόσον η κλάση ByteConverterDialog κληρονομεί από την QDialog. Προκειμένου να είναι διαθέσιμες οι λειτουργίες της QDialog εκτός της κλάσης ByteConverterDialog ,κάνουμε χρήση του ελέγχου πρόσβασης public.

 

Η δήλωση της κλάσης QLineEdit αποτελεί μια προχωρημένη δήλωση. Αντικείμενα της κλάσης ByteConverterDialog εμπεριέχουν τρεις ιδιωτικές μεταβλητές που δείχνουν αντικείμενα τύπου QLineEdit, έτσι ώστε ο μεταγλωτιστής C++ να γνωρίζει ότι η QLineEdit αποτελεί μια κλάση για να μπορέσει να εκτελέσει την δήλωση ByteConverterDialog, χωρίς να χρειάζεται να γνωρίζει τον ακριβή ορισμό της δήλωσης της κλάσης στο σημείο αυτό.

 

Ο συμβολισμός Q_OBJECT πρέπει να χρησιμοποιείται σε κάθε παράγωγο των κύριων κλάσεων QObject, συμπεριλαμβανομένου των έμμεσων, καθώς ορίζει συναρτήσεις πουμε την απουσία των οποίων η έννοια του σήματος / υποδοχής (signal/slot) δεν μπορεί να λειτουργήσει. (Περισότερα στην ενότητα 2.1.1.)

 

2 Εναλλακτικά, μπορούμε να συμπεριλάβουμε το αρχείο κεφαλής QLineEdit πριν την δήλωση του ByteConverterDialog, κάνοντας υποχρεωτική την ανάγνωση του, το οποίο θα καθυστερούσε αισθητά την μεταγλώττιση ειδικά σε παλαιούς υπολογιστές. Για τον λόγο αυτό, προσπαθούμε να βελτιστοποιούμε το αρχείο κεφαλής έτσι ώστε να ενσωμτατώνονται μόνο τα απολύτως απαραίτητα.

 

Ο κατασκευαστής (constructor) είναι η μόνη δημόσια συνάρτηση της κλάσης. Θα αποθηκεύσουμε δείκτες στα αντικείμενα QLineEdit, οι οποίοι θα προβάλλονται από τον μετατροπέα byte στις τρεις ενότητες μεταβλητών (decEdit, hexEdit, και binEdit) καθώς επιθυμούμε να ανανεώνονται τα πεδία εισόδου στα οποία ο χρήστης δεν εισάγει δεδομένα κατευθειαν, ώστε να διασφαλίζουμε ότι οι τρεις γραμμές εξόδου δείχουν το ίδιο κείμενο. Επειδή αυτό αποτελεί μια λεπτομέρεια υλοποίησης της κλάσης ByteConverterDialog, τις ορίζουμε σαν ιδιωτικές (private) μεταβλητές.

 

2.1.1 Κληρονομικότητα από QObject

 

Όπως προαναφέραμε, πρέπει πάντα να κάνουμε χρήση του συμβολισμού Q_OBJECT όταν μια κλάση κληρονομεί άμεσα ή έμμεσα από το QObject 3  .Ο συμβολισμός αυτός ορίζει το πλήθος συναρτήσεων που υλοποιούν τη λογική του σήματος / υποδοχής (signal/slot). Δυστυχώς, έαν απουσιάζει ο συμβολισμός αυτός στον ορισμό της κλάσης που κληρομεί (ται;)από το QObject, ούτε ο μεταγλωτιστής ούτε o διασυνδετής (linked) θα αναφέρουν σφάλμα. Αντιθέτως, τα σήματα και οι υποδοχείς της κλάσης θα παραμείνουν άγνωστα στο Qt, και κατά την εκτέλεση οι αντίστοιχες συνδέσεις δεν θα λειτουργούν.

 

Εφαρμογές που μεταγλωττίζονται με πληροφορίες αποσφαλμάτωσης θα επισημαίνουν κατά την εκτέλεση (σε παράθυρο γραμμής εντολών) ότι δεν υπάρχει σήμα ή υποδοχέας κάθε φορά που ένα κομμάτι κώδικα εκτελείται και προσπαθεί να αποκτήσει πρόσβαση σε ένα άγνωστο σήμα ή υποδοχέα. Το μήνυμα λάθους είναι το ακόλουθο:

Object::connect: No such slot QObject::decChanged(QString)

 

Ωστόσο, το παραπάνω μήνυμα λάθους είναι μη συγκεκριμένο. Επιπλέον, θα το συναντήσουμε έαν γράψουμε λάθος το όνομα ενός σήματος ή υποδοχέα ή την λίστα ορισμάτων λανθασμένα.

 

Κάθε αρχείο που κάνει χρήση του συμβολισμού Q_OBJECT υποχρεούται να εκχωρηθεί στο πρόγραμμα γραμμής-εντολών (βλ. σελίδα 56). Το εργαλείο αυτό παράγει αυτόματα τον κώδικα που μετατρέπεται σε καθαρή C++ από τον μηχανισμό σήματος / υποδοχέα 4

 

Σε περίπτωση χρήσης του qmake στην εφαρμογή μας, το εργαλείο qmake αναζητεί όλα τα αρχεία κεφαλής και πηγαίου κώδικα που αναφέρονται στο αρχείο .pro του συμβολισμού Q_OBJECT. Όταν βρίσκει ένα, το qmake παράγει αυτόματα τις απαιτούμενες εντολές κατασκεύης βασισμένες στην moc, μέσα στα περιεχόμενα των αρχείων αυτών 5

 

Για να λειτουργήσει σωστά, πρέπει να οριστεί το αρχείο κεφαλής στο αρχείο .pro . Πρέπει να γίνει χρήση των μεταβλητών κεφαλής της qmake (HEADERS), όπως αντιστοίχως θα γινόταν με την μεταβλητή SOURCES για τα αρχεία κώδικα:

 

3 Ορισμένοι μεταγλωττιστές παράγουν σφάλματα, όταν ο συμβολισμός Q_OBJECT τερματιστεί με άνω τελεία όπου, για λόγους φορητότητας, συνίσταται οπωσδήποτε η παράλειψη του.

 

4 Ο moc δεν τροποποιεί τα αρχεία μας αλλά παρέχει νέο κώδικα που διαχωρίζει τα αρχεία που πρέπει να προσέξουμε όταν γράφουμε τα αρχεία Makefiles με το χέρι. Σε περίπτωση χρήσης του qmake όπως συνιστούμε, δεν χρειάζεται να μας απασχολεί αυτό.

 

5 Η qmake δεν ανιχνεύει την μετέπειτα προσθήκη του συμβολισμού Q_OBJECT στα αρχεία.

 

#byteConverter/byteConverter.pro

 

TEMPLATE = app

 

SOURCES = main.cpp

ByteConverterDialog.cpp

HEADERS = ByteConverterDialog.h

 

Σε περίπτωση που η moc δεν καλείται για αρχεία που εμπεριέχουν συμβολισμούς Q_OBJECT, ο διασυνδετής βγάζει μηνύματα λάθους για άγνωστα σύμβολα, και το GCC εξάγει τα παρακάτω μηνύματα λάθους:

 

ld: Undefined symbols:

vtable for ByteConverterDialog

ByteConverterDialog::staticMetaObject

 

Σε περίπτωση που συναντήσουμε αυτό το μήνυμα λάθους, ελέγχουμε τα παρακάτω:

 

  • Έχουν οριστεί σωστά οι μεταβλητές κεφαλής (HEADERS) της qmake;

 

  • Have the qmake variable HEADERS been properly defined?

 

  • Η εκ νέου παραγωγή των αρχείων Makefiles με χρήση της qmake διορθώνει το πρόβλημα;

 

  • Is the problem resolved if the Makefiles are regenerated with qmake?

 

2.1.2 Σύνθετες Μορφές Σχεδιάγραματων

 

Θα ασχοληθούμε με την υλοποίηση της κλάσης ByteConverterDialog. Κατά την δημιουργία των περιστάσεων (instances) της κλάσης, η συνάρτηση του κατασκευαστή παράγει όλα τα QLineEdit widgets που προβάλλονται απο το νέο αντικείμενο ByteConverterDialog και τα εισάγει στον νέο μας διάταξη. Ωστόσο, αυτό δεν είναι τόσο απλό όπως προηγουμένως: όταν ο χρήστης αλλάζει το μέγεθος του πλαισίου διαλόγου, για τη σωστή και διαισθητική λειτουργία της εφαρμογής μας, πρέπει να γίνει χρήση των εμφωλευμένων διατάξεων. Το σχέδιο 2.2 δίχνει πώς η Qt διασφαλίζει ότι τα πεδία εισόδου θα εμφανίζονται πάντα στο πάνω μέρος του παραθύρου και το κουμπί τερματισμού εμφανίζεται στην κάτω-δεξία γωνία του παραθύρου.

 

Δεν συντρέχει λόγος ανησυχίας: ακόμη και αν ο πηγαίος κώδικας του κατασκευαστή (constructor) γίνει μεγάλος, γίνεται χρήση απλών συναρτήσεων :

 

But don’t panic: Even though the source code for the constructor becomes quite long, it uses only simple functions:

 

// byteConverter/ByteConverterDialog.cpp

 

#include “ByteConverterDialog.h”

#include <<QLabel>

#include <QLineEdit>

#include <QPushButton>

#include <QVBoxLayout>

#include <QHBoxLayout>

#include <QGridLayout>

 

ByteConverterDialog::ByteConverterDialog()

{

// Generate the necessary layouts

QVBoxLayout* mainLayout = new QVBoxLayout(this);

QGridLayout* editLayout = new QGridLayout;

QHBoxLayout* buttonLayout = new QHBoxLayout;

 

mainLayout->addLayout(editLayout);

mainLayout->addStretch();

mainLayout->addLayout(buttonLayout);

 

// Generate the labels and line-edits and add them

// to the object pointed at by editLayout

QLabel* decLabel = new QLabel(tr(“Decimal”));

QLabel* hexLabel = new QLabel(tr(“Hex”));

QLabel* binLabel = new QLabel(tr(“Binary”));

decEdit = new QLineEdit;

hexEdit = new QLineEdit;

binEdit = new QLineEdit;

 

editLayout->addWidget(decLabel, 0, 0);

editLayout->addWidget(decEdit, 0, 1);

editLayout->addWidget(hexLabel, 1, 0);

editLayout->addWidget(hexEdit, 1, 1);

editLayout->addWidget(binLabel, 2, 0);

editLayout->addWidget(binEdit, 2, 1);

 

// Create the Quit button and add it to the object pointed

// at by buttonLayout

QPushButton* exitButton = new QPushButton(tr(“Quit”));

 

buttonLayout->addStretch();

buttonLayout->addWidget(exitButton);

 

Το σχέδιο 2.3 απεικονίζει ποιες διατάξεις εμπλέκονται με ποια widgets. Δώστε προσοχή σε αυτές καθώς εξετάζουμε βήμα-βήμα τον παραπάνω κώδικα.

 

Το αντικείμενο mainLayout, μια κάθετη διάταξη κουτιού, είναι υπέθυνο για την διάταξη ολόκληρου του πλαισίου διαλόγου. Έτσι, περνάμε έναν δείκτη στο αντικείμενο ByteConverterDialog όταν καλούμε τον κατασκευαστή του. Για να γίνει αυτό, κάνουμε χρήση του δείκτη this, εφόσον βρισκόμαστε στην συνάρτηση της ίδιας της κλάσης ByteConverterDialog.

 

Το αντικείμενο editLayout είναι υπεύθυνο για την διάταξη των ετικετών και των widget γραμμής επεξεργασίας. Για να γίνει οργανωμένη στοίβαξη των στοιχειων και να οργανωθούν τα widgets σε μια και μοναδική στήλη, κάνουμε χρήση της διάταξης πλέγματος.

 

Η διάταξη buttonLayout, την οποία κατασκευάζουμε με την τρίτη νέα κλήση, θα είναι υπεύθυνη για διαχείριση του κουμπιού τερματισμού (Quit). Ωστόσο, πριν μπορέσουμε να παράξουμε widgets όμοια του κουμπιού αυτού και την προσθήκη τους στις editLayout και buttonLayout, πρέπει να τις προσθέσουμε τις διατάξεις αυτές στη βασική mainLayout με χρήση της addLayout(), αντίστοιχο του addWidget(). Σε περίπτωση προσθήκης widgets σε μία διάταξη που δεν συσχετίζεται με ένα widget, θα μας εμφανίσει το ακόλουθο μήνυμα σφάλματος στο παράθυρο της γραμμής εντολών.

 

QLayout::addChildWidget: προσθήκη προτύπου εμφάνισης στο γονικό πριν προσθέσουμε θυγατρικά.

 

Τα widgets θα παραμείνουν ορατά. Ως εκ τούτου, πρέπει να παράγουμε την βασική διάταξη της κλάσης πρώτα, και στη συνέχεια να συνεχίσουμε στην επόμενη διάταξη “layer”, και ούτω καθεξής.

 

Για να διασφαλίσουμε οτι τα πεδία εισόδου βρίσκονται στο πάνω μέρος του παραθύρου διαλόγου ByteConverterDialog και το κουμπί τερματισμού κάτω-δεξιά, κάνουμε χρήση των τεντωμάτων (Stretches).

 

Τα τεντώματα (Stretches) καταλαμβάνουν το κενό που δεν χρειάζονται τα widgets με συνέπεια να δημιουργούν κενά τμήματα στο παράθυρο διαλόγου μας. Έαν παραλείπαμε τα τεντώματα στο παράδειγμα μας, τα widgets θα καταλάμβαναν ολόκληρο το χώρο. Στην περίπτωση που ο χρήστης αύξανε το μέγεθος του πλαισίου διαλόγου χωρίς τεντώματα, θα βλέπαμε κάτι αντίστοιχο με το σχήμα 2.4

 

Για την αποφυγή αυτής της συμπεριφοράς, προσθέτουμε ένα τέντωμα μεταξύ των συναρτήσεων editLayout και buttonLayout.

 

Τώρα μπορούμε να παράξουμε ετικέτες και γραμμές επεξεργασίας και να τις εμπιστευτούμε στην editLayout. Σώζουμε τα αντικείμενα γραμμών επεξεργασίας στις μεταβλητές decEdit, hexEdit, and binEdit της ιδιωτικής κλάσης, καθώς θέλουμε να μπορούμε να επεξεργαζόμαστε τα περιεχόμενα τους με χρήση κώδικα που εμπεριέχεται σε άλλες συναρτήσεις. Γιαόλα τα υπόλοιπα αντικείμενα τα διαχειριζόμαστε χωρίς τους αντίστοιχους δίκτες, επειδή δεν χρειάζεται να αποκτήσουμε πρόσβαση έξω απο τον κατασκευαστή.

 

Now we can generate the labels and line edits and entrust them to the editLayout. We save the line edit objects in the private class variables decEdit, hexEdit, and binEdit, because we want to change their contents through code stored in other functions. For all other objects, we can manage without corresponding pointers because we do not need to access them outside the constructor.

 

Για την διασφάλιση της εμφάνισης του κουμπιού τερματισμού στο κάτω δεξία άκρο του παραθύρου διαλόγου, γεμίζουμε την διάταξη buttonLayout με ένα τέντωμα (stretch) πριν ρυθμίσουμε το ίδιο το κουμπί.

 

Προσθέτοντας όλα τα widgets και τις υποδιατάξεις στο αντικείμενο mainLayout ή τα παράγωγά του με χρήση των QObject::addWidget() and QObject::addLayout(), διασφαλίζουμε ότι όλα τα αντικείμενα που παρήχθησαν από τον κατασκευαστή με νέα, που κληρονομήθηκαν από το αντικείμενο ByteConverterDialog. Καθώς τώρα σχηματίζουν μια ιεραρχία αντικειμένων κατανεμημένου σωρού, με χρήση της διαχείρηση μνήμης της Qt, δεν απαιτείται χειροκίνητη διαγραφή τους. Όταν το αντικείμενο ByteConverterDialog διαγραφεί, όλα τα παιδιά του θα εξαφανιστούν αυτόματα.

 

Μήπως απογοητευτήκατε από τον όγκο του κώδικα που απαιτήθηκε για την δημιουργία ενός απλού πλαισίου διαλόγου; Ακολουθεί βοήθεια στο Κεφάλαιο 3, όπου εξηγεί πώς δύναται να σχεδιάσουμε ένα πλαίσιο διαλόγου με χρήση του Qt Designer με αυτόματη παραγωγή κώδικα. Περισσότερες πληροφορίες αναφορικά με τις διατάξεις στο Κεφάλαιο 5.

 

2.1.3 Αύξηση Ευχρηστίας

 

Παρά τις βελτιώσεις στην διάταξη, το πλαίσιο διαλόγου δεν συμπεριφέρεται ιδανικά σε ορισμένα σημεία:

 

Ο τίτλος του παραθύρου εμφανίζει το όνομα του προγράμματος byteConverter. Θα ήταν προτιμότερο κάτι πιο περιγραφικό.

 

Το κουμπί τερματισμού πρέπει να γίνει το προεπιλεγμένο κουμπί του πλαισίου διαλόγου. Το προκαθορισμένο κουμπί ενεργοποιείται από το (Enter) ακόμη και εάν προς το παρόν δεν είναι εστιασμένο στο πληκτρολόγιο. Τα περισότερα σχέδια widgets δίνουν έμφαση στο προκαθορισμένο κουμπί με έναν συγκεκριμένο τρόπο.

 

Σε αυτό το στάδιο μπορούμε να εισάγουμε οποιουσδήποτε αριθμούς στα widgets γραμμών επεξεργασίας. Είναι αναγκαίο να περιορίσουμε αυτές σε έγκυρες τιμές, μεταξύ ολόκληρων δεκαδικών αριθμών απο 0 εώς 255, δεκαεξαδικούς αριθμούς με μέγιστο δύο ψηφίων, και δυαδικών αριθμών με μέγιστο οχτώ bits.

 

Η λύση των παραπάνω προβλημάτων γίνεται με την προσθήκη των παρακάτω γραμμών κώδικα στον κατασκευαστή:6

 

// byteConverter/ByteConverterDialog.cpp (continued)

 

exitButton->setDefault(true);

 

// Limit input to valid values

QIntValidator* decValidator =

new QIntValidator(0, 255, decEdit);

decEdit->setValidator(decValidator);

 

QRegExpValidator* hexValidator =

new QRegExpValidator(QRegExp(“[0-9A-Fa-f]{1,2}”), hexEdit);

hexEdit->setValidator(hexValidator);

 

QRegExpValidator* binValidator =

new QRegExpValidator(QRegExp(“[01]{1,8}”), binEdit);

binEdit->setValidator(binValidator);

 

setWindowTitle(tr(“Byte Converter”));

 

 

Ορισμός τίτλου Παραθύρου

 

Τα πρώτα δύο προβλήματα επιλύονται με μια επιπλέον γραμμή κώδικα. Για την επίλυση του πρώτου προβλήματος, κάνουμε χρήση της συνάρτησης setWindowTitle(), η οποία καθορίζει τον τίτλο του παραθύρου του widget εάν αυτό καταλαμβάνει ένα παράθυρο ανωτάτου επιπέδου (top-level). Η συνάρτηση αυτή αποτελεί μια μέθοδο της κλάσης QWidget. Εφόσον το ByteConverterDialog έχει το QWidget σαν βασική κλάση, αυτή κληρονομεί την συνάρτηση, και μπορούμε απλά να την καλέσουμε.

 

Ορισμός του προεπιλεγμένου Κουμπιού

Specifying the Default Button

 

Το προεπιλεγμένο κουμπί του πλαισίου διαλόγου ορίζεται μέσω της ενημέρωσης του (αντί του πλαισίου διαλόγου) ως το προεπιλεγμένο κουμπί. (Επισημαίνουμε ωστόσο, ότι η κλήση του setDefault(true) σε ένα αντικείμενο QPushButton έχει επίδραση μόνο εάν το κουμπί χρησιμοποιείται σε ένα πλαίσιο διαλόγου—στο κυρίως παράθυρο δεν υπάρχουν προεπιλεγμένα κουμπιά. Εάν προσπαθήσουμε να ορίσουμε ένα προεπιλεγμένο κουμπί για το κυριώς παράθυρο, η Qt θα εμφανίσει ένα, χωρίς όμως να το ενεργοποιεί όταν ο χρήστης πατήσει το πλήκτρο Enter).

 

6 Για την μεταγλώττιση του παραγόμενου κώδικα, προσθέστε τις γραμμές #include που λείπουν (παραλείπονται παραπάνω για λόγους σαφήνειας) για τις κλάσεις που χρησιμοποιήθηκαν για πρώτη φορά εδώ, QIntValidator και QRegExpValidator!

 

Έλεγχος Δεδομένων Εισόδου Χρήστη

Checking User Input

 

Το τρίτο πρόβλημα, που περιορίζει την εισαγωγή  έγκυρων τιμών στα widget γραμμών επεξεργασίας, απαιτεί περισσότερη δουλειά, αλλά μπορεί να επιλυθεί μέσω επικυρωτών (validators). Αυτοί κληρονομούν από την QValidator όπως η βασική κλάση. Ένας επικυρωτής συσχετίζεται με ένα γονικό αντικείμενο που δέχεται δεδομένα εισόδου και ενημερώνει έαν το αντικείμενο μπορεί ή όχι να δεχτεί την τρέχουσα τιμή εισόδου.

 

Για τον έλεγχο της εγκυρότητας του δεκαδικού αριθμού, κάνουμε χρήση ενός αντικειμένου QIntValidator. Αυτό δημιουργείται με την κλήση του κατασκευαστή και περνώντας σε αυτόν, ως το πρώτο και δεύτερο γνώρισμα, τις έλαχιστες και μέγιστες τιμές που επιτρέπονται. Το τρίτο γνώρισμα εδώ, το decEdit, αποτελεί δείκτη στο αντικείμενο γραμμής επεξεργασίας που θέλουμε να ορίσουμε εως το γονικό αντικείμενο του επικυρωτή (validator). Αυτή η κλήση, εκτός απο την δέσμευση του επικυρωτή στο widget εισόδου, επιτρέπει την αυτόματη διαχείριση μνήμης, έτσι ώστε ο επικυρωτής θα επαναδιατεθεί μάζι με το widget. Η κλήση της setValidator() προκαλεί τον επικυρωτή να ελέγχει τα δεδομένα εισόδου που δίνονται απο το αντικείμενο που δείχνει το decEdit. Τώρα ο χρήστης μπορεί να πληκτρολογήσει ολόκληρους αριθμούς μεταξύ του 0 και 255 στο πεδίο εισόδου.

 

Για τον έλεγχο της εγκυρότητας των δεκαεξαδικών αριθμών, πρέπει να κάνουμε χρήση μιας άλλου τύπου επικύρωσης: QRegExpValidator. Αυτή συγκρίνει την είσοδο, που διαβάζει σαν σειρά χαρακτήρων, με μια κανονική έκφραση η οποία είναι [0-9A-Fa-f]{1,2}. Η πρώτη υποέκφραση σε αγκύλες καθορίζει τους χαρακτήρες που επιτρέπονται στην είσοδο: τα ψηφία 0 εώς 9, και οι χαρακτήρες A εώς F (είτε εώς κεφαλαία ή μικρά). Η ακόλουθη υποέκφραση, {1,2} περιορίζει το μέγεθος της συμβολοσειράς εισόδου σε τουλάχιστον ένα ή το πολύ δύο χαρακτήρες.

 

Οι κανονικές εκφράσεις σε Qt σχετίζονται με αυτές στην Perl, αλλά υπάρχουν ορισμένες σημαντικές διαφορές. Για παράδειγμα, είναι αναγκαία η διαφυγή μιας ανάστροφης καθέτου () σε μια έκραση όπως στην Perl, μια κανονική έκφραση με μια επιπλέον ανάστροφη κάθετο για να έχουμε την αντίστοιχη έκφραση Qtstyle, καθώς η μονή λειτουργεί ήδη σαν χαρακτήρας διαφυγής στην C/C++. Τότε η QRegExp αναγνωρίζει την διπλή ανάστροφη κάθετο σαν απλή. Από αυτό προκύπτει ότι πρέπει να πληκτρολογήσουμε  τέσσερις ανάστροφες καθέτους έαν επιθυμούμε να ορίσουμε μια κανονική κάθετο μέσα σε μια έκφραση Qt-style.

 

Επιπλέον, κάνουμε χρήση ενός QRegExpValidator μαζί με την κανονική έκφραση [01]{1,8} σαν επικυρωτή για το πεδίο εισαγωγής που αφορά δυαδικούς αριθμούς. Η έκφραση αυτή επιτρέπει μόνο τους χαρακτήρες 0 και 1 στην συμβολοσειρά εισόδου, αλλά αυτή μπορεί να είναι οπουδήποτε απο ένα εώς οχτώ χαρακτήρες σε μήκος.

 

2.1.4 Υλοποίηση Υποδοχών (Slots)

 

Τέλος, είναι αναγκαίο να υλοποιήσουμε τις λειτουργικές διασυνδέσεις που καθιστούν το κουμπί Quit λειτουργικό και να συγχρονίσουμε τα τρία πεδία εισόδου μεταξύ τους.

 

Για να εξασφαλιστεί ότι κάνοντας κλικ στο κουμπί Quit θα κλείσει το παράθυρο διαλόγου μετατροπής byte, επεκτείνουμε τον κατασκευαστή ByteConverterDialog έτσι ώστε να συσχετίζει το σήμα clicked() του κουμπιού με τον υποδοχέα accept() του παραθύρου διαλόγου. Ο υποδοχέας αυτός που παρέχεται από το QDialog, τον οποίο η κλάση ByteConverterDialog κληρονομεί από:

 

// byteConverter/ByteConverterDialog.cpp (continued)

 

connect(exitButton, SIGNAL(clicked()),

this, SLOT(accept()));

 

Όταν καλείται η μέθοδος accept(), απλά κλείνει το παράθυρο του διαλόγου. Η χρήση της accept() εδώ ακολουθεί μια γενική σύμβαση: ένας μεγάλος αριθμός παραθύρων διαλόγων έχουν ένα κουμπί ΟΚ και ένα κουμπί Ακύρωσης στο κάτω μέρος: Το ΟΚ αντιστοιχεί στον υποδοχέα accept() και το Cancel στον υποδοχέα reject(). Και οι δύο κλείνουν το παράθυρο διαλόγου, το πρώτο με μια θετική τιμή επιστροφής και η δεύτερη με μια αρνητική τιμή (βλέπε Κεφάλαιο 6, σελίδα 161). Σε αυτό το παράδειγμα έχουμε ένα μόνο κουμπί και, εώς εκ τούτου, δεν μας απασχολεί η τιμή επιστροφής, παρά μόνο η ενέργεια.

 

Ωστόσο, η λογική κατά την εκτέλεση σε πραγματικό χρόνο της εφαρμογής μετατροπής byte, περιλαμβάνει την επαύξηση των σημάτων και υποδεχέων με αρκετές προσαρμοσμένες διασυνδέσεις που ειδικεύονται στην λειτουργία της κλάσης ByteConverterDialog. Αυτά τα σήματα/διασυνδέσεις πρέπει να έρχονται σε δράση όταν οποιοδήποτε από τα αντικείμενα QLineEdit εκπέμψει σήμα textChanged(), υποδηλώνοντας οτι το κείμενο στο συγκεκριμένο πεδίο εισόδου του εν λόγω αντικειμένου έχει αλλάξει. Για τον σκοπό αυτό, επεκτείνουμε τον ορισμό της κλάσης μας ως εξής:

 

// byteConverter/ByteConverterDialog.h (continued)

 

class ByteConverterDialog : public QDialog

{

 

private slots:

void decChanged(const QString&);

void hexChanged(const QString&);

void binChanged(const QString&);

};

 

Οι υποδοχές (slots) δηλώνονται με τον ίδιο τρόπο όπως οι κανονικές συναρτήσεις, με την διαφορά ότι για τον έλεγχο πρόσβασης κάνουμε χρήση των επισημασμένων δημόσιων υποδοχέων: προστατευμένες υποδοχές, τις ιδιωτικές υποδοχές, αντί των συνηθισμένων δημόσιων υποδοχών, προτατευμένους και ιδιωτικούς τρόπους προστασίας.

 

Κάθε μια από τις τρεις υποδοχές δέχεται ένα όρισμα τύπου const QString&. Με τον τρόπο αυτό το σήμα textChanged() της συνάρτησης μπορεί να περνά νέο κείμενο στην γραμμή επεξεργασίας.

 

Για τον καθορισμό του είδους ορισμάτων των σημάτων/υποδοχέων, δεν επιλέγουμε απλά το QString αλλά μια αναφορά στο const QString. Υπάρχουν δύο λόγοι για αυτό. Αρχικά, χρησιμοποιώντας την κλήση μέσω αναφοράς αντί της κλήσης μέσω τιμής, το αντικείμενο QString που εμπεριέχει την νέα είσοδο που θα περαστεί στα σήματα/υποδοχείς δεν θα αντιγραφεί όταν αυτά κληθούν, καθιστώντας τον κώδικα αποτελεσματικότερο. Ωστόσο, η κλήση μέσω αναφοράς επιτρέπει στη συνάρτηση να τροποποιεί την παράμετρο, κάτι που δεν  επιτρέπεται να κάνουν τα σήματα και υποδοχείς. Έτσι η παράμετρος δηλώνεται σαν αναφορά στο const data. Το δεύτερο βήμα συνίσταται σαν πρακτική “αμυντικού προγραματισμού” κάθε φορά που μια συνάρτηση δεν επιτρέπεται να αλλάξει μια υπάρχουσα παράμετρο που έχει περαστεί από αναφορά.

 

Ακόμη και αν η δήλωση του υποδοχέα διαφέρει ελαφρώς από άλλες συναρτήσεις, παραμένει μια καθορισμένη συνάρτηση, η οποία υλοποιείται και μπορεί να κληθεί με τον συνηθισμένο τρόπο. Εδώ βρίσκουμε τον ορισμό της υποδοχής decChanged() και του αρχείου ByteConverterDialog.cpp:

 

// byteConverter/ByteConverterDialog.cpp (continued)

 

void ByteConverterDialog::decChanged(const QString& newValue)

{

bool ok;

int num = newValue.toInt(&ok);

if (ok) {

hexEdit->setText(QString::number(num, 16));

binEdit->setText(QString::number(num, 2));

} else {

hexEdit->setText(“”);

binEdit->setText(“”);

}

}

 

Η συνάρτηση δέχεται σαν όρισμα μια συμβολοσειρά που εμφανίζεται απο το δεκαδικό widget γραμμής επεξεργασίας ως την πραγματική τιμή για την παράμετρο newValue, και ανανεώνει τις συμβολοσειρές που εμφανίζονται από τα δεκαεξαδικά και δυαδικά widgets. Αρχικά πρέπει να καθορίσουμε την αριθμητική τιμή που αντιστοιχεί στην συμβολοσειρά εισόδου. Ως αντικείμενο της κλάσης QString, η newValue διαθέτει πολλές συναρτήσεις που μετατρέπουν τις συμβολοσειρές σε αριθμητικές τιμές. Θα κάνουνε χρήση της συνάρτησης toInt(), εφόσον η είσοδος ειναι συμβολοσειρά που αναπαριστά μια ακέραια τιμή.

 

Η συνάρτηση toInt() δέχεται έναν δυαδικό δείκτη σαν προαιρετικό όρισμα: αν το όρισμα έχει καθοριστεί, η συνάρτηση ορίζει αληθή τη μεταβλητή στην οποία δείχνει σε τιμή, αν η συμβολοσειρά μετατραπεί επιτυχώς σε αριθμητική τιμή, και σε αναληθή σε περίπτωση που αποτύχει η μετατροπή, λαμβάνοντας υπόψιν ότι η συμβολοσειρά δεν αναπαριστά ακέραια τιμή.

 

Εάν η μετατροπή είναι επιτυχής, θέτουμε τα κείμενα που εμφανίζονται από τις γραμμές επεξεργασίας (hexEdit και binEdit) στα δεκαεξαδικά και δυαδικά αντίστοιχα της νέας τιμής. Για να γίνει αυτό, μετατρέπουμε τον αριθμό σε συμβολοσειρά που αναπαριστά τη νέα τιμή σε δεκαεξαδική μορφή καθώς και σε συμβολοσειρά που αναπαριστά τη νέα τιμή σε δυαδική μορφή. Για τον σκοπό αυτό, η κλάση QString έχει τη στατική συνάρτηση number(), η οποία επιστρέφει την αναπαράσταση του αριθμού σαν συμβολοσειρά. Ο ίδιος ο αριθμός είναι το πρώτο όρισμά του. Σαν δεύτερο όρισμα, η συνάρτηση number() αναμένει τη βάση για τον αριθμό συστήματος που χρησιμοποιήθηκε, στην περίπτωση μας 16 για δεκαεξαδικό και 2 για δυαδικό. Το δεύτερο όρισμα είναι προαιρετικό, και σε περίπτωση που δεν καθοριστεί, η συνάρτηση number() υποθέτει βάση 10 (δεκαδικό σύστημα), που είναι η συνηθέστερη περίπτωση.

 

Σε περίπτωση που η συνάρτηση toInt() δε δύναται να μετατρέψει τη συμβολοσειρά που εισήχθη στην widget δεκαδικής γραμμής επεξεργασίας σε αριθμό, γράφουμε ένα κενό κείμενο στα άλλα δύο widgets γραμμής επεξεργασίας, με χρήση της setText(). Χάρη στον μηχανισμό επικύρωσης που χρησιμοποιήσαμε για το αντικείμενο decEdit, το οποίο διασφαλίζει ότι μόνο οι αριθμοί εύρους 0 εώς 255 επιτρέπονται να εισαχθούν, η μετατροπή θα αποτύχει σε μια μοναδική περίπτωση: αν ο χρήστης διαγράψει εντελώς τις τιμές εισόδου.

 

Υλοποιούμε τις δύο εναπομείνασες υποδοχές με το ίδιο τρόπο:

 

// byteConverter/ByteConverterDialog.cpp (continued)

 

void ByteConverterDialog::hexChanged(const QString& newValue)

{

if (ok) {

decEdit->setText(QString::number(num));

binEdit->setText(QString::number(num, 2));

} else {

}

}

 

void ByteConverterDialog::binChanged(const QString& newValue)

{

if (ok) {

decEdit->setText(QString::number(num));

hexEdit->setText(QString::number(num, 16));

} else {

}

}

 

Στις συναρτήσεις αυτές και κατά την μετατροπή της συμβολοσειράς σε ακέραια τιμή, καθορίζουμε την βάση σε ένα δεύτερο προαιρετικό όρισμα στο toInt(); όπως η QString::number(), toInt() κάνει χρήση της βάσης του 10 εξ ορισμού όταν το όρισμα αυτό παραλείπεται.

 

Προκειμένου τα τμήματα αυτά της εφαρμογής μας να λειτουργούν μαζί σύμφωνα με τον σχεδιασμό μας, πρέπει να διασυνδέσουμε τα σήματα textChanged() για κάθε ένα από τα αντικείμενα μας QLineEdit με τον αντίστοιχο υποδοχέα. Για να γίνει αυτό επεκτείνουμε τον κατασκευαστή για τελευταία φορά:

 

// byteConverter/ByteConverterDialog.cpp (continued)

 

connect(decEdit, SIGNAL(textChanged(const QString&)),

this, SLOT(decChanged(const QString&)));

connect(hexEdit, SIGNAL(textChanged(const QString&)),

this, SLOT(hexChanged(const QString&)));

connect(binEdit, SIGNAL(textChanged(const QString&)),

this, SLOT(binChanged(const QString&)));

}

 

Ο κώδικας του κατασκευαστή της κλάσης ByteConverterDialog έχει ολοκληρωθεί και εκτελεί τρείς διαφορετικές λειτουργίες:

 

  • Δημιουργεί όλα τα widgets ενός παραθύρου διαλόγου, τα ενσωματώνει μέσα στις αντίστοιχες διατάξεις και καθορίζει την ιεραρχία αντικειμένων του παραθύρου.

 

  • Περιορίζει τις τιμές εισόδου σε επιθυμητές τιμές

 

  • Δημιουργεί όλες τις απαραίτητες συνδέσεις σημάτων/υποδοχών

 

Ολόκληρη η λογική του κώδικα της εφαρμογής εμπεριέχεται στον κώδικα των υποδοχέων και στις συνδέσεις των αντίστοιχων σήματων.

 

2.2 Διαχωρισμός του Γραφικού Κελύφους και της Λογικής Επεξεργασίας

 

2.2.1 Εναλλακτικός Σχεδιασμός

 

Στο προηγούμενο παράδειγμα εφαρμογής,  ορίσαμε την μοναδική κλάση ByteConverterDialog η οποία υλοποιεί το γραφικό περιβάλλον και την λογική δομή του προγράμματος: όταν ο χρήστης αλλάξει την τιμή στο πεδίο γραμμής επεξεργασίας του widget, η κλάση ByteConverterDialog καλεί την αντίστοιχη υποδοχή, η οποία προσαρμόζει την τιμή των άλλων δύο γραμμών επεξεργασίας. Βλέπε σχήμα 2.5

 

Μια τέτοια διασύνδεση του γραφικού περιβάλλοντος και της λογικής δομής της εφαρμογής δημιουργεί τον κίνδυνο ο σχεδιασμός του προγράμματος να προκαλεί σύγχυση και να είναι δύσκολος στη συντήρηση. Για παράδειγμα, αν η κρίσιμη λογική της εφαρμογής έχει ενσωματωθεί σε μεθόδους υπεύθυνες για τη δημιουργία των διατάξεων και των widgets, και θελήσουμε να αλλάξουμε την εμφάνιση της διεπαφής, τότε ο κώδικας υπεύθυνος για την λειτουργικότητα θα πρέπει να αλλάξει με ιδιαίτερη προσοχή.

 

Το πρόβλημα αυτό δεν μπορεί να αποφευχθεί εντελώς, αλλά μπορεί τουλάχιστον να ελαχιστοποιηθεί μέσω του διαχωρισμού του κώδικα διεπαφής χρήστη και του κώδικα επεξεργασίας δεδομένων όπως στο σχήμα 2.6. Η δυνατότητα χρήσης σημάτων και υποδοχέων απλοποιεί την αφαιρετικότητα εδώ, επειδή περιττές εξαρτήσεις του παραθύρου διαλόγου της κλάσης επεξεργασίας (και αντίστροφα) μπορούν να αποφευχθούν.

 

Στο σχέδιο αυτό, η κλάση ByteConverterDialog είναι υπεύθυνη μόνο για την γραφική διεπαφή GUI: Η μετατροπή των αριθμών διεκπεραιώνεται από μια επιπλεόν κλάση, την ByteConverter. Η κλάση αυτή διαθέτει τους υποδοχείς setDec(), setHex() και setBin(). Αν καλέσουμε την υποδοχή setDec() με χρήση συμβολοσειράς, η κλάση εκπέμπει τα σήματα hexChanged() και binChanged() με τις αντίστοιχες τιμές σε δεκαεξαδική ή δυαδική μορφή, και ομοίως για τους άλλους δύο υποδοχείς.

 

Μπορούμε να διασυνδέσουμε τα σήματα και τους υποδοχείς του widget γραμμής επεξεργασίας από το ByteConverterDialog με τα σήματα και υποδοχείς της κλάσης ByteConverter, για παράδειγμα, το σήμα της hexChanged() του αντικειμένου decEdit σε ένα παράθυρο διαλόγου στον υποδοχέα setDec() στο συσχετιζόμενο ByteConverter. Αν ο χρήστης εισάγει μια νέα δεκαδική τιμή, το widget γραμμής επεξεργασίας εκπέμπει σήμα textChanged() και κάνει εφαρμογή του setDec(). Η υποδοχή αυτή εκπέμπει τα σήματα hexChanged() και binChanged(). Δεδομένου οτι τα έχουμε συνδέσει στον υποδοχέα setText() του αντικειμένου hexEdit ή binEdit, το πρόγραμμα ενημερώνει τις δεκαεξαδικές και δυαδικές τιμές της γραφικής διεπαφής του χρήστη.

 

Η κλάση ByteConverter δεν “γνωρίζει” κάτι όσον αναφορά στα γραφικά στοιχεία (GUI components). Έχει μια σαφώς καθορισμένη διεπαφή και μπορεί ακόμη να χρησιμοποιηθεί αν η όψη της εφαρμογής αλλάξει.

 

Ο διαχωρισμός της επεξεργασίας δεδομένων από την γραφική διεπαφή χρήστη (GUI) με αυτόν τον τρόπο πρέπει να λαμβάνεται πάντα υπόψιν, αν η λογική επεξεργασίας μπορεί να διαχωριστεί φυσικά από την διεπαφή χρήστη. Από την άλλη πλευρά, αν επιθυμούμε να συγχρονίσουμε ξεχωριστά στοιχεία GUI μεταξύ τους, πρέπει να αποφασίσουμε κατά ενός τέτοιου διαχωρισμού. Στην περίπτωση αυτή, δεν θα επιτευχθεί καμία τέτοια ανεξαρτησία, αλλά μόνο η μεταβίβαση της ευθύνης σε μια νέα κλάση.

 

Το πρόγραμμα του παραδείγματός μας αποτελεί μια ακραία περίπτωση της άποψης αυτής: ΤΗν διαδικασία επεξεργασίας δεδομένων που περιλαμβάνει την μετατροπή αριθμών από το ένα αιρθιτικό (?)σύστημα στο άλλο—μια λειτουργία που δεν αντιστοιχεί σε κάποια διεπαφή χρήστη. Έαν επιθυμούσαμε να συνθέσουμε ένα πρόγραμμα επεξεργασίας δεκαεξαδικών αριθμών, από την άλλη πλευρά των οποίων τα εξαγώμενα δεδομένα μπορούν να εναλλαχθούν μεταξύ δεκαδικής, δεκαεξαδικής και δυαδικής μορφής, δεν θα ήταν δικαιολογημένο να διαχωρίσουμε την γραφική διεπαφή χρήστη απο την υπολογιστική λογική για τον συγχρονισμό των αντίστοιχων γραμμών επεξεργασίας.

 

Figure 2.6: Διαχωρισμός των στοιχείων GUI από την λογική επεξεργασίας

 

Είναι ήδη φανερό ότι δεν υπάρχει εύκολη απάντηση στο τι πρέπει να διαχωριστεί και στο τι πρέπει να μείνει μαζί. Ως ένα βαθμό η απάντηση εξαρτάται από το στυλ προγραμματισμού και την οργάνωση του έργου. Η Qt παρέχει την αναγκαία ελευθερία και για τις δύο μεθόδους.

 

2.2.2 Δηλώνοντας και Αποστέλοντας τα Σήματα

 

Η νέα κλάση ByteConverter διαθέτει σήματα και υποδοχείς, τα οποία ως εκ τούτου πρέπει να κληρονομεί από το QObject. Επειδή δεν προβάλλει τίποτα στην οθόνη, δεν αποτελεί ένα widget και δεν απαιτεί καμία άλλη λειτουργικότητα που κάνει διαθέσιμη την Qt σε άλλες υποκλάσεις της QObject, μπορεί να κληρονομήσει κατευθείαν απο το QObject:

 

The new ByteConverter class has signals and slots, and must therefore ultimately inherit from QObject. Since it displays nothing on the screen, is not a widget, and requires no other functionality that Qt makes available in other subclasses of QObject, it can inherit directly from QObject:

 

// byteConverter2/ByteConverter.h

 

#ifndef BYTECONVERTER_H

#define BYTECONVERTER_H

 

#include <QObject>

 

class ByteConverter : public QObject

{

Q_OBJECT

 

public:

ByteConverter(QObject* = 0);

 

public slots:

void setDec(const QString&);

void setHex(const QString&);

void setBin(const QString&);

 

signals:

void decChanged(const QString&);

void hexChanged(const QString&);

void binChanged(const QString&);

};

 

#endif

 

Και πάλι είναι σημαντικό να μην ξεχνάμε τον συμβολισμό Q_OBJECT, αλλιώς η Qt δεν θα γνωρίζει αναφορικά με τα σήματα και τους υποδοχείς που δηλώθηκαν.

 

Ο κατασκευαστής δέχεται ένα δείκτη σαν όρισμα σε ένα αντικείμενο QObject. Αυτός μετατρέπεται στον “πατέρα” το νέου αντικειμένου στην ιεραρχεία αντικειμένων. Ως προκαθορισμένη τιμή (ακριβώς όπως στην υπογραφή του κατασκευαστή QObject), το 0 γίνεται χρήση του μηδενικού δείκτη—το αντίστοιχο αντικείμενο ByteConverter δεν έχει γονέα.

 

Η κλάση διαθέτει τρείς υποδοχείς: setDec(), setHex() και setBin(). Αυτή τη φορά θέλουμε να αποκτήσουμε πρόσβαση σε αυτά τα εξωτερικά της κλάσης, από την ByteConverterDialog, και το επιτρέπουμε αυτό με την λέξη κλειδί public.

 

Τα σήματα δηλώνονται με τις σημάνσεις σημάτων. Δεν έχει καθοριστεί τρόπος ελέγχου πρόσβασης—είναι πάντα δημόσια (public). Κάθε ιδιωτικό (private) ή προστατευμένο (protected) σήμα θα ήταν μη ορατό εκτός της κλάσης, και εώς εκ τούτου θα ήταν άσχηστο για εποικοινωνία μεταξύ διαφορετικών κλάσεων. Μέσα σε μία μοναδική κλάση (όπως στις προηγούμενες μας υλοποιήσεις) μπορούν να χρησιμποιηθούν απλές κλήσεις. Εκτός από τις σημάνσεις των σημάτων, οι δηλώσεις τους μοιάζουν ακριβώς σαν τις δηλώσεις συναρτήσεων.

 

Σε αντίθεση με τις συναρτήσεις και τις υποδοχές των μελών, η υλοποίηση των κλάσεων παραλείπει τον καθορισμό των σημάτων, δεδομένου ότι το μόνο που κάνουν είναι να καλούν τις υποδοχές που συνδέονται7.

 

Ο κατασκευαστής ByteConverter υλοποιείται άμεσα:

 

The ByteConverter constructor is quickly implemented:

 

// byteConverter2/ByteConverter.cpp

 

#include “ByteConverter.h”

 

ByteConverter::ByteConverter(QObject* parent) :

QObject(parent)

{

}

 

Περνάμε μόνο το γονικό όρισμα στον κατασκευαστή QObject (δηλάδη τον κατασκευαστή της βασικής κλάσης).

 

7 Φυσικά τα σήματα υλοποιούνται αυτόματα απο τον moc. Ο παραγόμενος κώδικας για κλήση ενός σήματος στην αντίστοιχη υποδοχή, αλλά δεν είναι δυνατή η σύνταξη ξεχωριστής υλοποίησης για κάθε σήμα.

 

Η υλοποίηση των υποδοχέων αντιστοιχεί κυρίως στο Κεφάλαιο 2.1.4 στην σελίδα 70:

 

// byteConverter2/ByteConverter.cpp (continued)

 

void ByteConverter::setDec(const QString& newValue)

{

bool ok;

int num = newValue.toInt(&ok);

if (ok) {

emit hexChanged(QString::number(num, 16));

emit binChanged(QString::number(num, 2));

} else {

emit hexChanged(“”);

emit binChanged(“”);

}

}

 

void ByteConverter::setHex(const QString& newValue)

{

bool ok;

int num = newValue.toInt(&ok, 16);

if (ok) {

emit decChanged(QString::number(num));

emit binChanged(QString::number(num, 2));

} else {

emit decChanged(“”);

emit binChanged(“”);

}

}

 

void ByteConverter::setBin(const QString& newValue)

{

bool ok;

int num = newValue.toInt(&ok, 2);

if (ok) {

emit decChanged(QString::number(num));

emit hexChanged(QString::number(num, 16));

} else {

emit decChanged(“”);

emit hexChanged(“”);

}

}

 

Μετατρέψαμε την αριθμητική τιμή κάθε ενός από τα τρία αριθμητικά συστήματα με συναρτήσεις της QString  toInt() και number(). Ωστόσο οι υποδοχές δεν αλλάζουν την τιμή των widget γραμμών επεξεργασίας, απλά στέλνουν τα αντίστοιχα σήματα. Για να γίνει αυτό, χρειάζεται να καλέσουμε το σήμα σαν συνάρτηση.

 

Προκειμένου να καταστεί σαφές ότι δεν προκείται για μια συνηθισμένη κλήση συνάρτησης, βάζουμε ένα πρόθεμα και το ονομάζουμε ένδειξη εκπομπής (emit designator). Αυτό δεν είναι απαραίτητο, αλλά αποτελεί βοήθημα για τον προγραματιστή, ο οποίος μπορεί να διακρίνει αμέσως αν το σήμα έχει σταλεί. Αποτελεί μια καλή προγραμματιστική πρακτική να σηματοδοτούμε εκπομπές σημάτων με την ένδειξη εκπομπής emit.

 

Το μόνο που απαιτείται τώρα είναι η προσθήκη των νέων αρχείων κεφαλής και πηγαίου κειμένου στο .pro αρχείο έτσι ώστε το qmake να παράγει τις απαραίτητες moc κλήσεις:

 

#byteConverter2/byteConverter2.pro

 

TEMPLATE = app

 

SOURCES = main.cpp

ByteConverterDialog.cpp

ByteConverter.cpp

HEADERS = ByteConverterDialog.h

ByteConverter.h

 

Αν παραλείψουμε να επεξεργαστούμε ένα αρχείο που δηλώνει μια κλάση με σήματα χρησιμοποιώντας την moc, ο διασυνδετής (inker) θα βγάλει μήνυμα για απροσδιόριστα σύμβολα. Το GCC εκδίδει το ακόλουθο μύνημα λάθους, για παράδειγμα:

 

If you forget to process a file that declares a class with signals using moc, the linker will complain of undefined symbols; GCC issues the following error message, for example

 

ld: Undefined symbols:

ByteConverter::binChanged(QString const&)

ByteConverter::decChanged(QString const&)

ByteConverter::hexChanged(QString const&)

 

Αν η κλάση δηλώνει μόνο υποδοχείς και δεν τις επεξεργαστούμε με χρήση της moc, θα λάβουμε μόνο ένα μήνυμα λάθους κατά την εκτέλεση, ότι η σύνδεση σήμα/υποδεχέα δεν δύναται να δημιουργηθεί, εφόσον η υποδοχή δεν είναι γνωστή.

 

2.2.3 Κάνοντας χρήση δικών μας σημάτων

 

Με την κλάση ByteConverter, η κλάση ByteConverterDialog έχει απλουστευτεί— δεν απαιτούνται μεταβλητές κλάσεων ή υποδοχείς: ο κατασκευαστής είναι επαρκής.

 

// byteConverter2/ByteConverterDialog.h

#ifndef BYTECONVERTERDIALOG_H

#define BYTECONVERTERDIALOG_H

 

#include <<QDialog>

 

class ByteConverterDialog : public QDialog

{

Q_OBJECT

 

public:

ByteConverterDialog();

};

 

#endif

 

Έχουμε δημιουργήσει τα widgets όπως στο προηγούμενο παράδειγμα και τα περνάμε με τον ίδιο τρόπο στην επιμέλεια της διάταξης, χωρίς να υπάρχουν διαφορές στις προσαρμογές, συμπεριλαμβανομένου των επικυρωτών (validators). Εμείς απλώς απαιτούμε διαφορετικές συνδέσεις στα σήματα / υποδοχείς:

 

// byteConverter2/ByteConverterDialog.cpp

 

ByteConverterDialog::ByteConverterDialog()

{

// Signal/slot connections

connect(exitButton, SIGNAL(clicked()),

this, SLOT(accept()));

ByteConverter* bc=new ByteConverter(this);

connect(decEdit, SIGNAL(textChanged(const QString&)),

bc, SLOT(setDec(const QString&)));

connect(hexEdit, SIGNAL(textChanged(const QString&)),

bc, SLOT(setHex(const QString&)));

connect(binEdit, SIGNAL(textChanged(const QString&)),

bc, SLOT(setBin(const QString&)));

connect(bc, SIGNAL(decChanged(const QString&)),

decEdit, SLOT(setText(const QString&)));

connect(bc, SIGNAL(hexChanged(const QString&)),

hexEdit, SLOT(setText(const QString&)));

connect(bc, SIGNAL(binChanged(const QString&)),

binEdit, SLOT(setText(const QString&)));

}

 

Συνδέουμε το σήμα clicked() του κουμπιού εξόδου στην υποδοχή accept() του παραθύρου διαλόγου, το οποίο το κλείνει. Οι υπολοιπόμενα σήματα / υποδοχείς αντιστοιχούν σε αυτά που φαίνονται στο Σχήμα 2.6 της σελίδας 76.

 

Στον κατασκευαστή ByteConverter εισάγουμε έναν δείκτη σαν όρισμα έτσι ώστε το νέο αντικείμενο να γίνει το θυγατρικό του ByteConverterDialog. Αυτό προκαλεί την αυτόματη διαχείριση μνήμης να σβήσει το αντικείμενο ByteConverter μόλις διαγραφεί η γραφική διεπαφή χρήστη (GUI). Επιπροσθέτως, η σχέση γονένα/παιδιού εξασφαλίζει ότι το αντικείμενο ByteConverter είναι διαθέσιμο για ολόκληρη την διάρκεια ζωής του αντικειμένου ByteConverterDialog.

 

Το παράδειγμα δείχνει ότι η σύνδεση υποδοχέων με δικά μας σήματα ή σήματα των κλάσεων Qt, δεν κάνει καμία διαφορά αναφορικά με την σύνταξη.


Source: Quantaofnoise

Leave a Reply

Your email address will not be published. Required fields are marked *