Начало работы с Qt

Добро пожаловать в мир Qt — кроссплатформенного набора инструментов (toolkit) для разработки графического интерфейса пользователя (GUI). В данном руководстве мы изучим основы Qt, написав простое приложение “Блокнот”. После прочтения этого руководства вы сможете лучше ориентироваться в документации и найти там всю необходимую информацию для разработки своих приложений.

Привет, Блокнот!

В качестве первого примера создадим и отобразим текстовый редактор (QTextEdit [doc.qt.nokia.com]). Это, пожалуй, самая простая программа на Qt с графическим интерфейсом пользователя, которая только может существовать.

Самое простое приложение на Qt

Исходный код:

  1. #include <QApplication>
  2. #include <QTextEdit>
  3.  
  4. int main(int argv, char **args)
  5. {
  6.     QApplication app(argv, args);
  7.    
  8.     QTextEdit textEdit;
  9.     textEdit.show();
  10.    
  11.     return app.exec();;
  12. }

Давайте рассмотрим каждую строку кода. На первых двух строках мы подключаем заголовочные файлы для QApplication [doc.qt.nokia.com] и QTextEdit [doc.qt.nokia.com] – двух классов которые потребуются для этого примера. Для каждого класса Qt есть заголовочный файл с соответствующим именем.

На строке 6 создается экземпляр QApplication [doc.qt.nokia.com]. Этот объект управляет ресурсами уровня приложения и необходим для запуска любой программы на Qt с графическим интерфейсом. Ему необходимо передать argv и args, так как Qt может принимать некоторые параметры командной строки.

На строке 8 создаётся экземпляр текстового редактора QTextEdit [doc.qt.nokia.com]. Текстовый редактор – это графический элемент интерфейса пользователя. В Qt мы называем такие элементы виджетами. Примерами других виджетов являются полосы прокрутки (scroll bars), метки (labels) и переключатели (radio buttons). Виджет также может быть контейнером для других виджетов, например, диалог или главное окно приложения.

На строке 9 текстовый редактор отображается на экране. Так как виджеты также служат контейнерами (например, QMainWindow [doc.qt.nokia.com], у которого есть панели инструментов (toolbars), меню, строка состояния (status bar) и другие виджеты), то можно показать отдельный виджет как полноценное окно. По умолчанию виджеты скрыты; метод show() [doc.qt.nokia.com] делает их видимыми.

На строке 11 QApplication [doc.qt.nokia.com] входит в цикл событий (eventloop). Во время работы приложения на Qt события генерируются и отправляются виджетам. Примерами событий могут служить нажатия кнопок мыши или ввод с клавиатуры. Когда пользователь вводит текст в текстовый редактор, этот виджет получает события нажатий клавиш и отображает набранный текст на экране.

Чтобы запустить приложение нужно в командной строке перейти в каталог с .cpp-файлом и выполнить следующие команды:

  1. qmake -project
  2. qmake
  3. make

Так как утилита qmake может работать в двух режимах – для создания Makefile или для создания .pro файла, мы должны импользовать ее два раза.

После этого в каталоге появится исполняемый файл (обратите внимание, что в Windows вместо make необходимо выполнить nmake и исполняемый файл будет в подкаталоге debug или release). qmake – это инструмент Qt для сборки приложения, использующий конфигурационный файл, который создается при запуске qmake с ключом -project. Используя заданный конфигурационный файл (файл с расширением .pro), qmake создаёт файл для утилиты make, которая будет собирать ваше приложение. Мы рассмотрим написание собственных .pro файлов позже.

Дополнительная информация

ТемаСтатья
Виджеты и геометрия окнаWindow and Dialog Widgets [doc.qt.nokia.com]
События и их обработкаThe Event System [doc.qt.nokia.com]

Добавление кнопки выхода

В настоящем приложении обычно требуется более одного виджета. Добавим кнопку (QPushButton [doc.qt.nokia.com]) под текстовым редактором. Кнопка будет закрывать Блокнот при нажатии на неё.

Кнопка выход

Давайте взглянем на код.

  1. #include <QtGui>
  2.  
  3. int main(int argv, char **args)
  4. {
  5.     QApplication app(argv, args);
  6.  
  7.     QTextEdit textEdit;
  8.     QPushButton quitButton("Quit");
  9.  
  10.     QObject::connect(&quitButton, SIGNAL(clicked()), qApp, SLOT(quit()));
  11.  
  12.     QVBoxLayout layout;
  13.     layout.addWidget(&textEdit);
  14.     layout.addWidget(&quitButton);
  15.  
  16.     QWidget window;
  17.     window.setLayout(&layout);
  18.  
  19.     window.show();
  20.  
  21.     return app.exec();
  22. }

На первой строке мы включаем QtGui [doc.qt.nokia.com] – заголовочный файл, содержащий все классы графического интерфейса пользователя Qt.

На строке 10 используется механизм сигналов и слотов Qt для выхода из приложения при нажатии кнопки Quit. Слот это функция, которая может быть вызвана во время выполнения программы (runtime) с помощью её имени (в виде строки). Сигнал это функция, которая вызывает связанные с ней слоты; мы называем это подключением слота к сигналу (connect) и отправкой сигнала (emit).

quit() [doc.qt.nokia.com] — это слот QApplication [doc.qt.nokia.com], который закрывает приложение. clicked() [doc.qt.nokia.com] – это сигнал, посылаемый кнопкой (QPushButton [doc.qt.nokia.com]) при нажатии. Статическая функция QObject::connect() [doc.qt.nokia.com] соединяет сигнал со слотом. SIGNAL и SLOT это два макроса, которые принимают сигнатуры соединяемых функций сигнала и слота. Также необходимо передать указатели на соединяемые объекты.

На строке 12 создаётся вертикальный компоновщик (QVBoxLayout [doc.qt.nokia.com]). Как было сказано выше, виджеты могут содержать другие виджеты. Можно явно указать границы (расположение и размер) дочерних виджетов, но удобнее использовать компоновщик (layout). Компоновщик управляет границами дочерних виджетов. QVBoxLayout [doc.qt.nokia.com], например, размещает дочерние виджеты вертикально.

На строках 13 и 14 текстовый редактор и кнопка добавляются в компоновщик. На строке 17 компоновщик устанавливается на виджет.

Дополнительная информация

ТемаСтатья
Сигналы и слотыSignals & Slots [doc.qt.nokia.com]
КомпоновкиLayout Management [doc.qt.nokia.com], Widgets and Layouts [doc.qt.nokia.com], Layout Examples [doc.qt.nokia.com]
Виджеты QtQt Widget Gallery [doc.qt.nokia.com], Widget Examples [doc.qt.nokia.com]

Наследование QWidget

Представим, что при выходе пользователя из приложения нам надо показать диалог для подтверждения выхода. В этом примере мы наследуем QWidget [doc.qt.nokia.com] и добавим слот, который соединим с кнопкой выхода Quit.

Наследуемся от QWidget

Давайте взглянем на код:

  1. class Notepad : public QWidget
  2. {
  3.     Q_OBJECT
  4.  
  5. public:
  6.     Notepad();
  7.  
  8. private slots:
  9.     void quit();
  10.  
  11. private:
  12.     QTextEdit *textEdit;
  13.     QPushButton *quitButton;
  14. };

Макрос Q_OBJECT должен быть первой инструкцией в объявлении класса. Этот макрос объявляет наш класс как QObject [doc.qt.nokia.com] (естественно, класс должен быть унаследован от QObject [doc.qt.nokia.com]). QObject [doc.qt.nokia.com] добавляет несколько возможностей обычным классам C++. Например, имя класса и имена его слотов могут быть запрошены во время выполнения программы. Также можно вызывать слоты и узнавать типы их параметров.

Строка 13 объявляет слот quit(). Для объявления слотов используется макрос slots. Теперь этот слот может быть подключен к сигналам с подходящей сигнатурой (то есть к сигналам без параметров).

Вместо настройки графического интерфейса пользователя и соединения слота в функции main() мы используем конструктор класса Notepad.

  1. Notepad::Notepad()
  2. {
  3.     textEdit = new QTextEdit;
  4.     quitButton = new QPushButton(tr("Quit"));
  5.  
  6.     connect(quitButton, SIGNAL(clicked()), this, SLOT(quit()));
  7.  
  8.     QVBoxLayout *layout = new QVBoxLayout;
  9.     layout->addWidget(textEdit);
  10.     layout->addWidget(quitButton);
  11.  
  12.     setLayout(layout);
  13.  
  14.     setWindowTitle(tr("Notepad"));
  15. }

Как видно из определения класса, используются указатели на экземпляры QObject [doc.qt.nokia.com] (textEdit и quitButton). Как правило, вы всегда должны создавать наследников QObject [doc.qt.nokia.com] в динамической памяти (heap) и никогда не копировать их.

Также мы используем функцию tr() [doc.qt.nokia.com] для видимых пользователю строк. Эта функция нужна когда приложение должно быть более чем на одном языке (например, на китайском и английском). Мы не будем углубляться в детали, но все подробности можно посмотреть по ссылке о Qt Linguist в дополнительной информации.

Дополнительная информация

ТемаСтатья
tr() и интернационализацияQt Linguist Manual [doc.qt.nokia.com], Writing Source Code for Translation [doc.qt.nokia.com], Hello tr() Example [doc.qt.nokia.com], Internationalization with Qt [doc.qt.nokia.com]
QObject [doc.qt.nokia.com] и объектная модель Qt (необходимо для понимания Qt)Object Model [doc.qt.nokia.com]
qmake и система сборки Qtqmake Manual [doc.qt.nokia.com]

Создание .pro файла

Для этого примера мы напишем собственный .pro файл вместо использования опции qmake -project.

  1. HEADERS =  notepad.h
  2. SOURCES =  notepad.cpp \
  3.            main.cpp

Соберем приложение:

  1. qmake
  2. make

Использование QMainWindow

Многие приложения могут выиграть от использования QMainWindow [doc.qt.nokia.com], у которого есть собственный компоновщик куда вы можете добавить меню, прикрепляемые виджеты (dock widgets), панель инструментов (tool bars) и строку состояния (status bar). У QMainWindow [doc.qt.nokia.com] есть центральная область где можно разместить любой виджет. В нашем случае мы разместим там наш текстовый редактор.

Используем QMainWindow

Давайте взглянем на новое определение класса Notepad.

  1. #include <QtGui>
  2.  
  3. class Notepad : public QMainWindow
  4. {
  5.     Q_OBJECT
  6.  
  7. public:
  8.     Notepad();
  9.  
  10. private slots:
  11.     void open();
  12.     void save();
  13.     void quit();
  14.  
  15. private:
  16.     QTextEdit *textEdit;
  17.  
  18.     QAction *openAction;
  19.     QAction *saveAction;
  20.     QAction *exitAction;
  21.  
  22.     QMenu *fileMenu;
  23. };

Здесь появились еще два слота для сохранения и загрузки файла. Они будут реализованы в следующем разделе.

Часто в главном окне один и тот же слот должен быть вызван сразу несколькими виджетами. Например, элементами меню и кнопками на панели инструментов. Для упрощения подобных вещей в Qt есть QAction [doc.qt.nokia.com], который может быть использован несколькими виджетами и который может быть соединён со слотом. Например, и QMenu [doc.qt.nokia.com], и QToolBar [doc.qt.nokia.com] могут создавать элементы меню и кнопки инструментов из одного QAction [doc.qt.nokia.com]. Мы скоро увидим как это работает.
Как и ранее, для настройки графического интерфейса пользователя мы используем конструктор Notepad.

  1. Notepad::Notepad()
  2. {
  3.     saveAction = new QAction(tr("&Open"), this);
  4.     saveAction = new QAction(tr("&Save"), this);
  5.     exitAction = new QAction(tr("E&xit"), this);
  6.  
  7.     connect(openAction, SIGNAL(triggered()), this, SLOT(open()));
  8.     connect(saveAction, SIGNAL(triggered()), this, SLOT(save()));
  9.     connect(exitAction, SIGNAL(triggered()), qApp, SLOT(quit()));
  10.  
  11.     fileMenu = menuBar()->addMenu(tr("&File"));
  12.     fileMenu->addAction(openAction);
  13.     fileMenu->addAction(saveAction);
  14.     fileMenu->addSeparator();
  15.     fileMenu->addAction(exitAction);
  16.  
  17.     textEdit = new QTextEdit;
  18.     setCentralWidget(textEdit);
  19.  
  20.     setWindowTitle(tr("Notepad"));
  21. }

Экземпляры QAction [doc.qt.nokia.com] создаются с указанием текста, который будет отображен на виджетах (в нашем случае, на элементах меню). Если мы хотим добавить их на панель инструментов, то нам может потребоваться задать ещё и иконки [doc.qt.nokia.com].

Теперь, когда будет выбран пункт меню, он активирует действие и вызовется соответствующий слот.

Дополнительная информация

ТемаСтатья
Главные окна и классы главного окнаApplication Main Window [doc.qt.nokia.com], Main Window Examples [doc.qt.nokia.com]
Приложения с многодокументным интерфейсом (MDI)QMdiArea [doc.qt.nokia.com], MDI Example [doc.qt.nokia.com]

Сохранение и загрузка

В этом примере мы реализуем слоты open() и save(), которые были добавлены в предыдущем примере.

Открытие файла в Kubuntu

Начнем со слота open():

  1. QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), "",
  2.                                     tr("Text Files (*.txt);;C++ Files (*.cpp *.h)"));
  3.  
  4. if (fileName != "") {
  5.     QFile file(fileName);
  6.     if (!file.open(QIODevice::ReadOnly)) {
  7.         QMessageBox::critical(this, tr("Error"),
  8.                         tr("Could not open file"));
  9.         return;
  10.     }
  11.     QString contents = file.readAll().constData();
  12.     textEdit->setPlainText(contents);
  13.     file.close();
  14. }

Сначала у пользователя запрашивается имя открываемого файла. В Qt есть класс QFileDialog [doc.qt.nokia.com], который является диалогом выбора файла. Выше показан такой диалог в Kubuntu. Статический метод getOpenFileName() [doc.qt.nokia.com] отображает модальный диалог выбора файла и не завершается, пока пользователь не выберет файл. Он возвращает путь к выбранному файлу или пустую строку, если пользователь отменил диалог.

Если пользователь выбрал файл, то попробуем его открыть с помощью метода open() [doc.qt.nokia.com], который возвращает истину в случае успеха. Мы не будем углубляться в обработку ошибок (подробнее про них можно почитать в дополнительной информации). Если не удалось открыть файл, то отображается диалог QMessageBox [doc.qt.nokia.com] с сообщением об ошибке.

Фактически, чтение данных выполняется с помощью одного метода readAll() [doc.qt.nokia.com], который возвращает все содержимое файла в QByteArray [doc.qt.nokia.com]. Метод constData() [doc.qt.nokia.com] возвращает содержимое QByteArray [doc.qt.nokia.com] в виде const char *, для которого у QString [doc.qt.nokia.com] есть конструктор. Затем содержимое файла отображается в текстовом редакторе. В конце работы вызывается метод close() [doc.qt.nokia.com] который закроет файл и вернет файловый дескриптор операционной системе.

Рассмотрим слот save():

  1. QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"), "",
  2.                                     tr("Text Files (*.txt);;C++ Files (*.cpp *.h)"));
  3.  
  4. if (fileName != "") {
  5.     QFile file(fileName);
  6.     if (!file.open(QIODevice::WriteOnly)) {
  7.         // error message
  8.     } else {
  9.         QTextStream stream(&file);
  10.         stream << textEdit->toPlainText();
  11.         stream.flush();
  12.         file.close();
  13.     }
  14. }

Для записи данных в файл воспользуемся классом QTextStream [doc.qt.nokia.com] (текстовый поток), который в нашем случае будет оберткой над экземпляром QFile [doc.qt.nokia.com]. Текстовый поток может записывать QString [doc.qt.nokia.com] непосредственно в файл; QFile [doc.qt.nokia.com] же принимает лишь сырые данные (char*) или QByteArray [doc.qt.nokia.com] с помощью методов write() [doc.qt.nokia.com] класса-предка QIODevice [doc.qt.nokia.com].

Дополнительная информация

ТемаСтатья
Файлы и устройства ввода-выводаQFile [doc.qt.nokia.com], QIODevice [doc.qt.nokia.com]