Programowanie w Qt - kurs
Sygnały i sloty
W tej lekcji dodamy do naszego poprzedniego programu możliwośc interackji
z użytkownikiem. Pokażę tutaj jak łączyć zdarzenia wciśnięcia przycisku, czy wybrania
opcji z menu z naszymi zdefiniowanymi funkcjami.
Czym są sygnały i sloty?
Gdy klikamy gdzieś na jakimś obiekcie Qt, np. formatce czy przycisku, jest
wówczas emitowany sygnał. W przypadku kliknięcia na przycisk sygnał ten nazywa się clicked()
(oczywiście, całej operacji wciśniecia przycisku myszy w ponad przyciskiem towarzyszy cała seria
sygnałów takich jak pressed(), released(), toggled(), a także tych które są towarzyszą klasie
do obsługi myszy mousePressEvent()). Sygnał ten możemy dzięki wbudowanym w Qt mechanizmom połączyć
z naszą zdefiniowaną metodą. W ten sposób programujemy w Qt obsługę graficznego interfejsu użytkownika.
Jak to działa w praktyce
Dodamy teraz do naszego programu z lekcji poprzedniej pewną podstawową funkcjonalność.
Na początek będzie ona pozwalała wykonywać operacje takie jak zaczytywanie i zapisywanie pliku. W tym celu
będziemy musieli rozbudować naszą klasę o trzy sloty, osbługujące takie funkcje jak: otwieranie pliku,
zapis i zapisywanie pod inną nazwą. Dodamy także zmienną prywatną, w której będziemy przechowywać
nazwę aktualnie otwartego pliku. Tak więc cała klasa wraz z deklaracją slotów, będzie wyglądała tak:
#include "ui_form1.h"
/* Pliki niezbedne dla GUI */
#include <QMainWindow>
#include <QMessageBox>
#include <QTextCodec>
#include <QToolBar>
#include <QMenuBar>
#include <QStatusBar>
#include <QLabel>
/* Pliki niezbedne dla otwierania i zapisywania plikow */
#include <QFileDialog>
#include <QTextStream>
using namespace Ui;
class Form1: public QMainWindow
{
Q_OBJECT
public:
Form1(QWidget * parent = 0, Qt::WFlags f = 0 );
~Form1(){};
void createMenu();
void createStatus();
QString fileName;
QLabel *fileNameLabel;
public slots:
void fileOpen();
void fileSave();
void fileSaveAs();
private:
MainWindow ui;
QToolBar *tBar;
QMenu *fileMenu, *editMenu, *helpMenu;
QStatusBar *stBar;
};
Jak widać dodałem tutaj dwa pliki nagłówkowe: QTextStream, który jest bardzo przydatny gdy
odczytujemy i zapisujemy pliki oraz QFileDialog, który jest odpowiedzialny za wyświetlenie
stosowenego okienka dialogowego służącego do wybrania nazwy pliku lub katalogu zapisania lub
otwarcia. Warto przy okazji zwrócić uwagę na makro Q_OBJECT użyte zaraz na początku klasy
Form1. Oznacza ono dla kompilatora, że w tej klasie będziemy korzystać ze slotów i sygnałów,
że dla tej klasy ma zostać wygenerowany specjalny plik moc implementujący mechanizm sygnałów
i slotów.
Definicja przedstawionych powyżej slotów przedstawia się następująco:
void Form1::fileOpen()
{
fileName = QFileDialog::getOpenFileName(this,
tr("Wybierz plik..."), ".",
tr("Pliki programu (*.h *.cpp *.pro *.ui *.txt)") );
if (!fileName.isEmpty ())
{
QFile file(fileName);
if (file.open (QFile::ReadOnly | QFile::Text))
ui.textEdit->setPlainText (file.readAll ());
}
fileNameLabel->setText( fileName );
}
W pierwszym etapie w tej funkcji wywołujemy metodę getOpenFileName z klasy QFileDialog, która
jako wynik swojego działania zwraca nazwę wybranego pliku. Następnie sprawdzamy czy użytkownik
coś wybrał czyli czy nazwa wybranego pliku jest różna od pustej. Gdy tak jest to tworzymy obiekt
typu QFile na podstawie nazwy wybranej przez usera. Następnie próbujemy otworzyć ten plik i jeśli
się to powiedzie jego zawartość w formie czystego tekstu ląduje do textEdit.
Przedstawiony tutaj zapis "ui.textEdit" oznacza odwołanie do obiektu klasy ui, która to z kolei
jest klasą MainWindow zadeklarowaną w pliku ui_form1.h. Całość może brzmi skomplikowanie jednak
jest to dość proste do zrozumienia. Szerzej o tym będzie w następnej lekcji.
Jeszcze jedna uwaga, aby zapisywany tekst miał nasze polskie kodowanie, lub jakiekolwiek inne
od domyślnego należy np. w konstruktorze zadeklarować domyślny kodek dla stringów:
QTextCodec::setCodecForTr (QTextCodec::codecForName ("ISO8859-2"));
QTextCodec::setCodecForCStrings (QTextCodec::codecForName ("ISO8859-2"));
Te dwie powyższe metody definiują kodowanie jakie będzie używane:
- We wszystkich (tłumaczalnych - tr) napisach używanych w programie.
- We wszystkich stringach na jakich będzie operował program.
Oczywiście nic nie stoi na przeszkodzie, żeby używać kodowania utf8 jednak poki co jest jeszcze
na to troche za wczesnie.
Zapis pliku:
void Form1::fileSave()
{
if (!fileName.isEmpty ())
{
QFile file(fileName);
if (file.open (QFile::WriteOnly | QFile::Text))
{
QTextStream out(&file);
out << ui.textEdit->toPlainText();
}
}
}
Jak widać do zapisu pliku użyłem strumienia tekstowego, jest to najprostsze rozwiązanie, które
jak widać zmieściło się w dwóch linijkach. Strumień został utworzony, a jego urządzenie określone jako
plik zapamiętany z otwarcia (lub funkcji fileSaveAs). Zapis do strumienia nie różni się niczym od
metody jaką stosujemy przy zwykłym programowaniu konsolowym cout. Funkcja fileSaveAs jest połączeniem
dwóch powyższych:
void Form1::fileSaveAs()
{
fileName = QFileDialog::getSaveFileName(this,
tr("Wybierz plik..."), ".",
tr("Pliki programu (*.h *.cpp *.pro *.ui *.txt)") );
if (!fileName.isEmpty ())
{
QFile file(fileName);
if (file.open (QFile::WriteOnly | QFile::Text))
{
QTextStream out(&file);
out << ui.textEdit->toPlainText();
}
}
fileNameLabel->setText( fileName );
}
Sygnały i sloty - łączymy wszystko do kupy
Aby połączyć sygnał jakiegos obiektu z naszym slotem, używa się do tego funkcji connect. W przykładzie
użyjemy go do połączenia naszych trzech slotów jednocześnie z akcjami menu i przyciskami, które
znajdują sie na toolbarze. Tak więc znana z poprzedniego przykładu funkcja createMenu() będzie teraz
wyglądać tak:
void Form1::createMenu()
{
tBar = addToolBar (tr ("Glowny toolbar"));
fileMenu = menuBar()->addMenu (tr ("&Plik"));
QAction *fileMenu1 = new QAction( QIcon("images/new.png"), tr("Nowy"), this);
fileMenu1->setStatusTip (tr ("Tworzy nowy plik"));
fileMenu->addAction (fileMenu1);
tBar->addAction( fileMenu1 );
QAction *fileMenu2 = new QAction ( QIcon("images/open.png"), tr("Otwórz"), this);
fileMenu2->setStatusTip ( tr("Otwiera już istniejący plik"));
fileMenu->addAction( fileMenu2 );
tBar->addAction( fileMenu2 );
connect(fileMenu2, SIGNAL(triggered()), this, SLOT(fileOpen()) );
QAction *fileMenu3 = new QAction ( QIcon("images/save.png"), tr("Zapisz"), this);
fileMenu3->setStatusTip (tr("Zapisuje bierzące zmiany"));
fileMenu->addAction (fileMenu3);
tBar->addAction( fileMenu3 );
connect(fileMenu3, SIGNAL(triggered()), this, SLOT(fileSave()) );
QAction *fileMenu4 = new QAction ( tr("Zapisz jako"), this);
fileMenu4->setStatusTip ( tr("Zapisuje jako nowy plik"));
fileMenu->addAction (fileMenu4);
connect(fileMenu4, SIGNAL(triggered()), this, SLOT(fileSaveAs()) );
fileMenu->addSeparator ();
tBar->addSeparator();
QAction *fileMenu5 = new QAction ( tr("Zakończ"), this);
fileMenu4->setStatusTip ( tr("Kończy pracę programu"));
fileMenu->addAction (fileMenu5);
connect(fileMenu5, SIGNAL(triggered()), qApp, SLOT(quit()) );
editMenu = menuBar()->addMenu( tr("&Edycja") );
QAction *editMenu1 = new QAction( QIcon("images/copy.png"), tr("Kopiuj"), this );
editMenu1->setStatusTip( tr("Kopiuje zaznaczony tekst") );
tBar->addAction( editMenu1 );
editMenu->addAction(editMenu1);
QAction *editMenu2 = new QAction (QIcon("images/cut.png"), tr ("Wytnij"), this );
editMenu2->setStatusTip( tr("Wycina zaznaczony tekst") );
editMenu->addAction(editMenu2);
tBar->addAction( editMenu2 );
QAction *editMenu3 = new QAction (QIcon("images/paste.png"), tr ("Wklej"), this );
editMenu3->setStatusTip( tr("Wkleja zaznaczony tekst") );
editMenu->addAction(editMenu3);
tBar->addAction( editMenu3 );
helpMenu = menuBar()->addMenu( tr("&Pomoc") );
QAction *helpMenu1 = new QAction( tr("O programie"), this );
helpMenu1->setStatusTip( tr("Pomoc do programu") );
connect(helpMenu1, SIGNAL(triggered()), qApp, SLOT(aboutQt()) );
helpMenu->addAction(helpMenu1);
}
W ten oto błyskotliwy sposób program zyskał niesamowicie wypasioną funkcjonalność. Funkcja connect,
jest działa zawsze w ten sam sposób wymaga czterech parametrów, z które odpowiadają kolejno za obiekt
który chcemy połączyć, sygnał który chcemy obsługiwać, nazwę obiektu, w którym znajduje się funkcja
użyta w momencie przechwycenia, i nazwa tej funkcji. W pseudokodzie może to wyglądać tak:
connect( co, sygnał(co_clicked()), this, slot(onCoClicked()) );
W zaprezentowanym powyżej przykładzie oprócz zdefiniowanych przez nas slotów, użyłem również
slotów zdefiniowanych w klasie QApplication i odwołałem się do niej przez obiekt qApp.
Oprócz funkcji connect istnieje również funkcja disconnect, która przydaje sie bardzo często
gdy łączymy obiekty w sposób dynamiczny i gdy jeden slot jest wykorzystywany przez wiele obiektów
lub gdy jeden obiekt korzysta z wielu slotów w zależności od sytucji w programie.