Статья о том, как разработать собственное Android-приложение в среде QtCreator, шаг за шагом.
Создание проекта
Итак, всё начинается с запуска QtCreator, который мы установили и настроили в первой части нашей статьи. Для создания собственного проекта — кликаем на меню Файл в верхней левой части окна и выбираем пункт Создать файл или проект…. Вы можете также воспользоваться сочетанием клавиш Ctrl + N (только не забудьте переключить раскладку на английский язык перед этим).
После этого вам предстоит выполнить стандартную процедуру для создания нового проекта.
Новый файл или проект
Для создания проекта приложения вам необходимо выбрать пункт Приложение Qt Widgets, как показано на скриншоте:
Размещение проекта
Первое, что вам предложат сделать — выбрать название проекта и его расположение на жёстком диске. Вы можете последовать примеру на экране, затем кликаем Далее.
Выбор системы сборки
Затем вам будет нужно определиться с системой сборки. Что это такое?
Система сборки представляет собой отдельное приложение, которое выполняет инструкции по сборке исходного кода в готовое для запуска приложение. Иными словами она автоматизирует выполнение всех команд так или иначе связанных с компиляцией и сборкой вашего приложения из исходного кода. Для сборки приложения система сборки использует специальный сборочный файл, содержащий инструкции сборки. Синтаксис этого файла зависит от конкретной системы сборки. Существуют различные системы сборки, к наиболее распространённым из них относятся:
В нашем случае среда Qt разработки предоставляет собственную систему сборки — Qmake. Синтаксис сборочного файла qmake достаточно прост, чуть позже мы к этому вернёмся, а пока пропустим этот пункт (здесь нас всё устраивает) и продолжим настраивать наш проект.
Информация о классе
Далее мы настраиваем главный класс нашего приложения.
Класс MainWindow будет содержать UI приложения и интерфейсную логику.
UI (User Interface) — пользовательский интерфейс приложения, часть программы, через которую пользователь имеет возможность взаимодействовать с функционалом приложения, то есть её основными функциями. UI может быть консольным и тогда программа управляется посредством консольных команд. UI также может быть графическим, т.е. исполненным в виде графических элементов (кнопок, чекбоксов, окон ввода и т.д.) и тогда его называют GUI (Graphical UI). В Qt графические элементы приложения именуют виджетами и для их использования существует специальный заголовочный файл QtWidgets. Дальше мы познакомимся с ними подробнее.
Здесь мы тоже ничего не меняем, оставляем всё как есть.
Файл переводов
Файл переводов это отдельная тема касающаяся локализации вашего приложения. Используя возможности локализации Qt, можно создавать приложения, которые будут корректно отображать текстовую информацию на родном языке пользователя в зависимости от его географического расположения.
В этой статье мы не будем рассматривать эту тему, но все кто желает, могут ознакомиться с ней в официальной справке, либо на сайте Qt.
Здесь нам нужно выбрать из списка доступных языков свой родной — Великий и Могучий Русский язык.
Выбор комплекта
Итак, если вы хотите попробовать запустить приложение на своём компьютере, вам нужно выбрать пункт Desktop, иначе вам достаточно указать только пункт Android.
Управление проектом
Если вы используете систему контроля версий, такую как SVN или Git, то вы можете указать её в пункте Добавить под контроль версии. В этом случае QtCreator создаст для вас отдельный репозиторий.
Система контроля версий ( сокращённо СКВ) представляет собой машину времени для файлов вашего проекта — с ней вы всегда можете вернуть любой файл в любое фиксированное состояние.
Она сослужит вам огромную службу например, когда что-то пошло не так и вам срочно нужно вернуть рабочий исходный код для какого-либо класса, или когда вы экспериментируете с различными вариантами реализации программных функций или интерфейса, или работаете над одними и тем же проектом в команде разработчиков. В большинстве компаний это распространённая практика — использовать систему контроля версий для разработки достаточно сложных проектов.
Так что если вы ещё не установили себе СКВ, то я настоятельно рекомендую это сделать. Я например пользуюсь Git, т.к. она позволяет сохранять исходный код моих проектов и делиться им с помощью веб-сервиса GitHub. О том как пользоваться Git и GitHub вы можете прочитать здесь и здесь. Стоит уделить внимание этой теме так же потому, что знание хотя бы одной СКВ как правило требуется при устройстве на работу в любую уважающую себя IT компанию.
Итак, это последний пункт для настройки вашего проекта. Теперь нам предстоит заняться непосредственно процессом разработки.
Разработка приложения
Структура приложения
После создания и конфигурации вашего проекта он откроется в среде QtCreator. Это будет выглядеть примерно так:
Обратите внимание на панель справа. Здесь вы можете видеть структуру вашего проекта. Перед вами несколько файлов:
- programmer.pro файл с инструкциями сборки для qmake
- main.cpp главный файл, содержащий точку входа в ваше приложение
- mainwindow.h заголовочный файл главного окна вашего приложения
- mainwindow.cpp файл содержащий UI-логику вашего приложения
- mainwindow.ui файл QtDesigner, содержащий описание макета GUI
- programmer_ru_RU.ts файл локализации
В последствии к данной структуре добавятся ещё несколько файлов, о чём будет рассказано далее.
Главный файл
Давайте подробнее остановимся на главном файле нашего приложения. Откроем файл main.cpp Здесь всего 11 строк, как видно он совсем не большой:
# include "mainwindow.h"
# include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv); a.setStyleSheet(getCustomStyle());
MainWindow w;
w.show();
return a.exec();
}
- строка 1 подключает заголовочный файл главного виджета приложения
- строка 3 подключает стандартный класс приложения QApplication
- строка 7 создаёт объект приложения Qt с передачей аргументов командной строки
- строка 8 создаёт объект главного окна вашего приложения MainWindow
- строка 9 делает этот объект видимым
- строка 10 запускает ваше приложение с передачей кода возврата по завершении
Запуск приложения
После выполнения данного кода приложение будет запущено и перед пользователем появится главное окно.
Режим отладки
Среда QtCreator предоставляет возможность удобной отладки приложения в графическом режиме. Собственно она использует старый добрый GDB в качестве программы отладчика. С помощью него вы можете полностью контролировать ход выполнения вашего приложения по шагам, а также получать информацию о состоянии памяти вашей программы на каждом шаге. Это весьма полезно, когда вы сталкиваетесь с каким-либо неожиданным поведением вашего кода.
Нельзя теоретизировать, прежде чем появятся факты. Неизбежно начинаешь подстраивать факты под свою теорию, а не строить теорию на основе фактов.
цитата из к/ф «Шерлок Холмс»
Когда-то на заре компьютерной эры, когда компьютеры были большими, а программы к ним маленькими — сбои в работе программ возникали, каким бы странным это ни казалось, по причине поломок в компьютерном оборудовании, вызываемых в том числе и наличием внутри них живой органики в виде жуков-багов (от англ. bugs), вызывающих короткое замыкание при попадании на контакты радиоэлементов электронных плат…
Я констатировал факт. Факт при этом визжал и вырывался, но я его все-таки констатировал.
цитата из к/ф «Шерлок Холмс»
Соответственно, отлаживать программу означало то же самое, что и производить проверку компьютерного железа на предмет наличия багов для устранения последствий сбоев, что собственно из называли тогда de-bug. Затем эта терминология перешла в мир компьютерных программ и теперь мы имеем целый класс программ выполняющих данную функцию в области копьютерного кода — дебаггеров (от англ. слова debugger).
Перейдём к делу. Далее вам потребуется знание того, как запускать приложение в режиме отладки, потому как отладка занимает значительную часть рабочего времени в процессе разработки приложения. Для того, чтобы запустить ваше приложение в режиме отладки сперва вам нужно выбрать режим Отладка, воспользовавшись элементом на левой нижней панели QtCreator
Вам нужно выбрать пункт Отладка.
Это позволит Qt подготовить ваше приложение к процессу отладки, автоматически настроить нужные ключи компилятора для сбора отладочной информации. Если вы хотите заниматься отладкой на вашем компьютере, то вы также можете выбрать пункт Desktop (если вы указали поддержку Desktop на этапе создания проекта).
QtCreator запустит ваше приложение и автоматически подключит его к программе отладчика GDB, а так же немного изменит свой вид, чтобы сделать процесс отладки для вас более комфортным.
Здесь представлены кнопки управления процессом отладки:
- первая кнопка — приостановка процесса отладки — программа останавливается в произвольном месте, в котором она оказалась на текущий момент, эта же кнопка позволяет продолжить процесс отладки, в случае если программа уже остановлена
- вторая кнопка — остановка отладчика — выход из режима отладки и закрытие приложения
- кнопки 3, 4, 5 — кнопки управления ходом отладки — они отвечают за навигацию в коде приложения, позволяют переходить между функциональными блоками программы, входить и выходить из заданных блоков
- кнопка 6 — кнопка перезапуска — перезапускает сессию отладки
Подробнее рассмотрим процесс приостановки программы. Программа может быть приостановлена в 3-х случаях:
- Если вы нажали на кнопку приостановки процесса отладки
- Если программа получила сигнал аварийного останова типа SIGSEGV
Точка останова (break point)
В произвольной строке исходного кода вашей программы вы можете поставить точку останова. Чтобы это сделать вам достаточно кликнуть по области слева от номера выбранной строки как на примере ниже
При наведении курсора на область изображения точки останова, вы можете увидеть дополнительную информацию
На панели справа вы увидите информацию о состоянии переменных и объектов вашей программы на момент останова вашего приложения.
ФАКТЫ, ФАКТЫ, ФАКТЫ! Я не могу строить дом без цемента!
цитата из к/ф «Шерлок Холмс»
Для того чтобы понять, что же пошло не так, вам нужно будет внимательно изучить данную информацию. Вам также понадобиться информация о т.н. стеке вызовов ваших функций, которая указана на нижней панели под кнопками управления режимом отладки. Это позволит вам сориентироваться где именно в программе произошёл сбой.
После того, как у вас появилась адекватная гипотеза о том, что именно произошло и как это исправить, вы можете проверить её, сделав правку в исходном коде программы и запустив её снова в режиме в отладки. Если всё отработало так как вы это себе представляли, то ваша гипотеза верна, и вам больше ничего не нужно делать. Но как это часто бывает, гипотеза оказывается не верна, поэтому приходится повторять этот процесс до тех пор, пока верная гипотеза не будет найдена.
Не отчаивайтесь, если на начальных этапах разработки вам не удаётся сразу найти верное решение, потому что в этом и заключается искусство разработки. Помните, что ваше мастрество растёт с вашим опытом. Дам вам совет — читайте больше профессиональной литературы на тему разработки приложений — это позволит вам сократить путь к мастерству создания компьютерных программ. Это подобно ориентированию на местности — если у вас нет правильной карты (адекватной теории), то вам будет значительно труднее попасть в нужное место (написать правильный код).
Создание GUI в QtDesigner
Теперь настала пора заняться разработкой графического интерфейса для вашего приложения. Для этого QtCreator предоставляет собственный дизайнер интерфейсов QtDesigner, который позволяет скомпановать графические элементы вашего приложения не прибегая к работе с исходным кодом. Найдите кнопку Дизайн на левой верхней панели QtCreator
Перед вами должен открыться дизайнер интерфейсов QtDesigner
Здесь вы увидите 6 основных панелей:
- Центральная панель — макет интерфейса, на котором будет отображаться ваш интерфейс, куда вы можете добавлять графические элементы. Обратите внимание на маленькие синие квадратики по периметру вашего макета — с их помощью вы можете менять размеры макета вашего окна, это также относится и к любым другим графическим элементам.
- Левая панель — выбор графического элемента. Элементы добавляются по принципу drag-and-drop на макет интерфейса на центральной панели.
- Правая верхняя панель — иерархия вложенности графических элементов на вашем макете. Эта панель также взаимодействует с центральной панелью, так что вы можете перемещать элементы макета по тому же принципу drag-and-drop в произвольное место в иерархии графических элементов. Иногда это очень удобно, потому как некоторые элементы могут перекрывать друг друга. Здесь вы также можете выделять элементы макета для правки их свойств.
- Правая нижняя панель — свойства графического элемента. Для выделенного графического элемента вы можете изменить его свойства, например задав имя объекта, подпись, максимальный размер и т.д. Тоже самое можно сделать для некоторых основных свойств с помощью пунктов выпадающего меню по нажатии ПКМ (Правой Кнопки Мыши) на выделенном элементе на центральной панели.
- Нижняя панель — эта панель позволяет создавать и настраивать сигналы и слоты. Этой темы мы коснёмся в следующем разделе.
- Верхняя панель — совсем небольшая, на ней всего несколько кнопок:
Ну что ж, давайте с вами набросаем простенький интерфейс… Постойте ка мы же ведь даже не определились с тем, что за приложением мы будем разрабатывать… Предлагаю, так как наш сайт посвящен программированию, создать приложение, которое снискало популярность на заре компьютерной эры в нашей стране, а именно приложение «Программист». Точнее не его, а его аналог, что-нибудь вроде реплики с учётом современных реалий.
Концепт
Everything starts with your vision
Unknown author
Итак, давайте определимся с тем, что мы хотим получить от нашей программы, отталкиваясь от основной идеи — симулятора жизни программиста. В первую очередь это будет игра, от которой будет можно получить удовольствие, встречая в ней знакомые ситуации из жизни современного программиста. Итак, что нам известно, так это то, что у нас есть программист, и мы хотим управлять его действиями, создавая определенные игровые ситуации. Основной фактор с которым сталкивается любой программист в своей деятельности это время. Поэтому наша игра должна создавать иллюзию быстрого течения времени, за которое нужно успеть что-то сделать. Что ж, в целом у нас вырисовывается первый экран приложения
Главная
Давайте проведём декомпозицию этого макета… Здесь у нас есть множество различных элементов с надписями, а также статическое изображение, которые можно создать с помощью класса QLabel.
Затем у нас есть прогресс бар, реализуемый с помощью класса QProgressBar, который также входит в список стандартных виджетов Qt. И ещё у нас есть список навыков, который мы можем отобразить, использовав класс QListWidget. Не забудем также что, нам нужно упорядочить все эти элементы, чтобы они располагались строго на своих местах — для этого используются элементы QHBoxLayout и QVBoxLayout, они располагают элементы друг за другом по горизонтали и вертикали. Кажется это всё! Этих пяти элементов достаточно, чтобы реализовать макет данной страницы на практике.
Да, конечно мы кое что упустили… Это страницы Learning, Работа и Отдых. Чтобы их реализовать, нам нужно воспользоваться классом QTabWidget. Каждая из страниц будет отвечать за определённую область жизни программиста в соответствии с её названием. Давайте рассмотрим каждую из страниц в отдельности
Learning
В моей жизни как правило возникает три основных варианта, в которых я получаю новое знание. Они изображены на макете
Здесь всего три кнопки QPushButton, которые упорядочены с помощью элемента QVBoxLayout.
Работа
На этой странице я указал всего два варианта, во втором случае можно сделать переход на дополнительную страницу, где можно конкретизировать то, чем именно можно бы было заняться. Например так:
Всё это реальные проекты из жизни и кому-то уже повезло в них поучаствовать, в вашей жизни может произойти нечто подобное, если вы действительно увлекаетесь программированием!
Отдых
Так же как и на предыдущем макете, элементы данного макета нам уже встречались, поэтому я предлагаю вам проделать работу по его декомпозиции самостоятельно.
Ещё было бы неплохо иметь возможность смотреть картинки тех мест, в которых мы будем отдыхать по игре — так как это приносит большое удовольствие для нашего мозга и даёт предпосылку для того, чтобы помечтать о том светлом будущем, которое вас ожидает. Для этого нам понадобиться ещё один класс QMessageBox, он позволяет отображать как текстовую, так и графическую информацию в виде диалогового окна.
Итак это всё, что нам потребуется знать о приложении на текущий момент, для того, чтобы создать его GUI. Здесь я оставляю вам простор для экспериментирования, укажу лишь конечную цель, потому как для понимания следующих шагов разработки приложения вам потребуется иметь набор тех же самых виджетов, упорядоченных в иерархию подобно этой
Прежде чем приступать к следующему шагу, убедитесь, что вы получили что-то похожее, воспользовавшись средствами QtDesigner.
Модель приложения
Обычно при создании типичного приложения на Qt используется паттерн проектирования MVC
Шаблон проектирования или паттерн (англ. design pattern) в разработке программного обеспечение — повторяемая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста.
Model-View-Controller (MVC, «Модель-Представление-Контроллер») — схема разделения данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента: модель, представление и контроллер — таким образом, что модификация каждого компонента может осуществляться независимо.
Модель (Model) предоставляет данные и реагирует на команды контроллера, изменяя своё состояние.
Представление (View) отвечает за отображение данных модели пользователю, реагируя на изменения модели.
Контроллер (Controller) интерпретирует действия пользователя, оповещая модель о необходимости изменений.справка из Википедии
Мы не будем отходить от данного правила, поэтому нам потребуется создание собственной модели приложения. Пусть это будет класс, который будет содержать основной объект игры, который будет иметь, собственно: главный игровой цикл, модель игрока в виде списка параметров, а также все игровые объекты, так или иначе влияющие на эти параметры, при их взаимодействии с игроком. Обычно конкретные данные о игровых объектах хранят с помощью базы данных, но мы не будем этого делать а ограничимся лишь только текстовым файлом, т.к. у нас не слишком большое приложение.
Идея готова, теперь нам нужно перейти в редактор для того, чтобы заняться её реализацией. Кликаем на кнопку редактора на левой панели
Создание нового файла
Создадим файл для нашей модели, кликнув ПКМ на названии проекта на левой панели сверху и выбрав пункт Добавить новый…
Перед вами появиться окно, в котором нужно выбрать пункт Класс С++
Теперь осталось указать имя класса
Вы можете также сразу отметить галочки Подключить QObject и Добавить Q_OBJECT — это позволит вам использовать сигналы и слоты для взаимодействия вашей модели с UI-частью. Это можно также реализовать самостоятельно в коде, унаследовав класс от объекта QObject и добавив макрос Q_OBJECT сразу за названием в объявлении класса. В любом вам необходимо сделать одно из этих действий.
Если вы пользуетесь Git, то смело добавляйте.
На этом процесс создания файла для нового класса завершён, теперь можно приступать к его реализации.
Думаю не стоит подробно останавливаться на каждой строчке кода игровой модели, приведу его весь сразу
gamemodel.h
# ifndef GAMEMODEL_H
# define GAMEMODEL_H
# include <QString>
# include <QMap>
# include <QTextStream>
# include <QFile>
# include <QThread>
/*
* Player state
*/
struct Parameters
{
int time; // hours
float money; // $
int health;
int intellect;
int mood;
int reputation;
QMap<QString, uint> skills;
};
/*
* Sources of changes
*/
struct Phenomenon
{
QString type; // what?
QString label; // what exactly?
Parameters change; // how it changes parameters?
bool was; // it is already happend?
};
/*
* Game logic
*/
class GameModel : public QObject
{
Q_OBJECT
void init(); // setup game
void act(Phenomenon event); // run single event
float getSpeed(); // defined by intellect
int getScore(); // calculate game score
public:
GameModel();
~GameModel();
Parameters player; // player state
QList<Phenomenon> continuum; // all possible events
bool act(QString type, QString label = "", bool onlyNew = false); // single event
void loop(); // main game cycle
bool pause; // change this flag to pause game
// output
signals:
void actComplete(Phenomenon event);
void timeUpdate();
void gameEnd(int gameScore);
};
# endif // GAMEMODEL_H
gamemodel.cpp
#include "gamemodel.h"
GameModel::GameModel()
{
init();
}
GameModel::~GameModel()
{
player.skills.clear();
continuum.clear();
}
void GameModel::init()
{
/*
* init player
*/
player.mood = 1;
player.time = 876000; // 100 years in hours
player.money = 100.00; // $
player.health = 100;
player.intellect = 100;
player.reputation = 0;
player.skills = {};
/*
* init continuum
*/
QFile file(QString(":/res/gamedata.txt"));
if(file.open(QIODevice::ReadOnly))
{
QTextStream in(&file);
while(!in.atEnd())
{
// get fields
QStringList fields = in.readLine().split(",");
// parse skills
QMap<QString, uint> skills;
if(fields[8].trimmed() != "null")
{
foreach(QString str, fields[8].split(" "))
{
if(str != "")
{
QStringList strList = str.split(":");
if(strList.size() > 1)
skills[strList[0]] = strList[1].toUInt();
}
}
}
// setup parameters
Parameters pars = {
fields[2].toInt(),
fields[3].toFloat(),
fields[4].toInt(),
fields[5].toInt(),
fields[6].toInt(),
fields[7].toInt(),
skills};
// wrap phenomena
QStringList listLabel = fields[1].split(".");
QString formatedLabel = listLabel.size() > 1? listLabel[1].trimmed()
+ " (" + listLabel[0].trimmed() + ")" : fields[1];
Phenomenon phen = { fields[0], formatedLabel, pars, false };
// expand continuum
continuum.append(phen);
}
}
/*
* start game
*/
pause = false;
}
void GameModel::act(Phenomenon event)
{
/*
* update the parameters
*/
player.time -= event.change.time * 3 / (float(player.intellect) / 100.0);
player.money += event.change.money;
player.health += event.change.health;
player.intellect += event.change.intellect;
player.mood += event.change.mood;
player.reputation += event.change.reputation;
/*
* update skills
*/
foreach(QString skill, event.change.skills.keys())
{
if(skill == "*")
{
foreach(QString all, player.skills.keys())
{
player.skills[all] += 1;
}
}
else if(skill == ".")
{
int rand = qrand() % player.skills.keys().size();
QString any = player.skills.keys()[rand];
player.skills[any] += 1;
}
else
{
player.skills[skill] = event.change.skills[skill];
}
}
/*
* check parameters values
*/
bool gameNotEnd = true;
if(player.health > 100)
{
player.health = 100;
}
else if(player.health <= 0)
{
gameNotEnd = false;
}
if(player.intellect > 230) player.intellect = 230;
/*
* send signals to UI
*/
if(gameNotEnd)
{
emit actComplete(event);
}
else
{
emit gameEnd(getScore());
}
}
bool GameModel::act(QString type, QString label, bool onlyNew)
{
bool actSucceed = false;
// get first phenomen by condition
for(int i = 0; i < continuum.size(); i++)
{
if(continuum[i].type == type && (continuum[i].label == label || label == ""))
{
if((onlyNew == true && continuum[i].was == false) || !onlyNew)
{
act(continuum[i]);
continuum[i].was = true;
actSucceed = true;
break;
}
}
}
return actSucceed;
}
void GameModel::loop()
{
qsrand(42);
// main game cycle
while(player.time >= 0)
{
// simple delay for 2 ms
QThread::msleep(2);
// provide game pause
if(pause)
continue;
//
// tick time
player.time -= 2 / getSpeed();
player.money -= 0.01 / getSpeed();
// generate accident
int rand = qrand() % 1000;
if(rand <= 1)
{
act("Случай", "Болезнь");
}
else if(rand <= 2)
{
act("Случай", "Стресс");
}
else if(rand <= 3)
{
act("Случай", "Праздник");
}
// send signal to UI
emit timeUpdate();
}
player.time = 0;
emit gameEnd(getScore());
}
float GameModel::getSpeed()
{
return float(player.intellect)/100.0;
}
int GameModel::getScore()
{
return player.mood +
player.money +
player.health +
player.intellect +
player.reputation +
player.skills.size();
}
Код вполне очевидный, ознакомьтесь с ним самостоятельно. Отмечу лишь только, что здесь я активно использую возможности Qt в виде готовых классов:
- QString — основной класс для работы со строками
- QMap — класс для работы со структурой данных map, представляющая собой набор key-value пар — здесь я использую QString в качестве ключей для доступа к uint значениям, это очень удобно для хранения списка значений в случае наличия уникальных ключей, таких как названия конкретных навыков
- QFile — класс для работы с файлами, осуществления операций чтения/записи (я использую его, чтобы загружать игровые данные из текстового файла)
- QTextStream — для работы с потоком текстовых данных — я использую его в комбинации с QFile, чтобы последовательно считывать файл строка за строкой
- QList — класс реализующий структуру списка для хранения произвольных объектов (в данном случае игровых объектов)
- QThread — класс для работы с потоками — здесь я использую данный класс для того, чтобы реализовать временную задержку в игровом цикле, а практике его используют для того, чтобы обеспечить многопоточное исполнение кода на множестве процессорных ядер (с многопоточностью мы все равно столкнёмся, как мы бы этого не хотели, потому что наша игровая модель не может исполняться в одном потоке с UI, т.к. это привело бы его зависанию — игровой цикл должен работать в отдельном потоке и об этом будет сказано далее)
Вся остальная часть кода должна выглядеть вполне привычно для тех, кто уже занимался разработкой на C/C++ (здесь я использую две дополнительные структуры для модели игрока и для модели явления, которое влияет на состояние игрока). Но вы также можете заметить новое слово, которое не встречалось вами ранее в декларации класса — а именно слово signals, что же это такое?
Сигналы, на ряду со слотами представляют собой абстракцию, позволяющую осуществлять взаимодействие между объектами в приложении Qt. Любой объект унаследованный от класса QObject, имеет возможность передавать сигналы другим объектам, также унаследованным от класса QObject, а также принимать сигналы от других объектов с помощью слотов, которые по сути являются обычными методами, в которых можно производить обработку данных полученных от сигналов.
Я не буду вдаваться здесь в технические детали реализации подобного механизма, об этом вы можете прочитать в книге Макса Шлее «Qt 5.10 Программирование для профессионалов». Для нас важно, что с помощью данного механизма мы можем осуществлять взаимодействие различных объектов прямо из кода их классов, и это очень круто! Потому как нам не нужно будет писать общий код, который будет явно передавать данные от одного класса к другому и тому подобные вещи. Всё что нам будет нужно, так это создать сигналы в одних классах и соотвествующие им слоты в других, и соединить их с помощью специальной функции connect, подобно тому как это сделано в примере ниже…
Пример использования сигналов и слотов
Допустим у нас есть класс A унаследованный от QObject (при обязательном указании макроса Q_OBJECT) и содержащий объявление сигнала
signal:
void ring();
Тогда мы можем вызвать этот сигнал в любом методе данного класса с помощью специального слова emit как будто мы вызываем обычную функцию:
emit ring();
На принимающей стороне, а это может быть как тот же самый класс A, так и класс B, так же унаследованный от QObject и содержащий соответствующий слот для приёма данного сигнала, например
public slot:
void answer();
Для обеспечения соединения между сигналами используется функция connect, которая как правило вызывается из конструктора класса таким образом
connect(&a, SIGNAL(ring()), this, SLOT(answer()));
Здесь &a это адрес объекта класса A, содержащий объявление сигнала ring(). Таким образом мы говорим, что когда в объекте a класса A происходит событие ring(), то в данном объекте класса B произойдёт событие answer().
В данном примере между событиями не происходит передача каких-либо данных, но это также можно осуществить с помощью указания конкретных переменных, единственное, что интерфейсы сигналов и слотов должны полностью соответствовать друг другу. Например, если мы передаем int, то вызов функции connect будет выглядеть следующим образом
connect(&a, SIGNAL(ring(int)), this, SLOT(answer(int)));
В остальном сигналы и слоты можно рассматривать как обычные методы: сигналы похожи на чистые виртуальные методы не имеющие реализации, тогда как слоты обязательно должны иметь свою реализацию.
Пока нам этого будет достаточно, чтобы воспользоваться сигналами и слотами для связывания модели приложения его UI.
UI-логика
Под UI-логикой, или как её ещё называют интерфейсной логикой, как правило подразумевают логическую связь между компонентами элементов интерфейса и их взаимодействие с моделью приложения. Для того, чтобы обвязать наш интерфейс и модель логикой, нам нужно вернуться к классу MainWindow и добавить в него объект нашей игровой модели GameModel, а затем прописать каким образом будет меняться модель в зависимости от действий пользователя, а также каким образом интерфейс будет реагировать на эти изменения. Вообще модель MVC устроена таким образом, что действия пользователя посредством контроллера C влияют на изменение модели M, а та в свою очередь производит изменения в представлении V. При этом конечно возможен вариант, когда контроллер C передаёт эти изменения непосредственно в представление V.
Итак, код нашего GUI будет выглядеть следующим образом:
mainwindow.h
# ifndef MAINWINDOW_H
# define MAINWINDOW_H
# include <QMainWindow>
# include <QInputDialog>
# include <QMessageBox>
# include <QFuture>
# include <QtConcurrent/QtConcurrent>
# include <QPixmap>
# include <QDebug>
# include "gamemodel.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
GameModel gm; // game logic
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
/*
* GameModel process slots
*/
void processActComplete(Phenomenon event);
void processTimeUpdate();
void processGameEnd(int gameScore);
/*
* UI elements process slots
*/
void on_readBookButton_clicked();
void on_learnCourseButton_clicked();
void on_getAnswerButton_clicked();
void on_takeProjectButton_clicked();
void on_findJobButton_clicked();
void on_doJobButton_clicked();
void on_chooseRestButton_clicked();
void on_tabWidget_currentChanged(int index);
private:
Ui::MainWindow *ui;
/*
* UI inner methods
*/
void init();
void runStandartAct(QString type, bool onlyNew = true);
void updatePlayerInfo();
void updateSkillsList();
void updateJobList();
void updateRestList();
void showEventDialog(Phenomenon event);
void showDialog(QString message, QString text);
void showImageDialog(QString message, QString text);
QString unfoldProjectLabel(QString text);
};
# endif // MAINWINDOW_H
mainwindow.cpp
# include "mainwindow.h"
# include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
init();
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::init()
{
// show get player name dialog
bool ok;
QString text = QInputDialog::getText(this,
tr("Как вас зовут?"),
tr("Ваше имя:"),
QLineEdit::Normal,
QString("anon"),
&ok);
// show greetings
if(ok && !text.isEmpty())
ui->labelName->setText(tr("Привет, ") + text + "!");
// get player name age
int age = QInputDialog::getInt(this,
tr("Сколько вам лет?"),
tr("Ваш возраст:"),
23, 7, 42, 1, &ok);
// setup initial player time
int yearHours = 24 * 365;
gm.player.time = (42 - age) * yearHours; // 100 years minus age in hours
if(gm.player.time < 25 * yearHours)
{
gm.player.time = 25 * yearHours;
}
ui->progressTime->setMaximum(gm.player.time);
// update UI
updatePlayerInfo();
// clear UI list selection
ui->skillsList->clearSelection();
ui->jobList->clearSelection();
ui->restList->clearSelection();
// connect game model output signals with UI slots
connect(&gm, SIGNAL(actComplete(Phenomenon)), this, SLOT(processActComplete(Phenomenon)));
connect(&gm, SIGNAL(timeUpdate()), this, SLOT(processTimeUpdate()));
connect(&gm, SIGNAL(gameEnd(int)), this, SLOT(processGameEnd(int)));
// run game loop
QFuture<void> future = QtConcurrent::run([=](){
gm.loop();
});
}
void MainWindow::runStandartAct(QString type, bool onlyNew)
{
if(!gm.act(type, "", onlyNew))
{
showDialog("Сообщение","Ничего интересного!");
}
}
void MainWindow::processActComplete(Phenomenon event)
{
updatePlayerInfo();
showEventDialog(event);
}
void MainWindow::processTimeUpdate()
{
ui->progressTime->setValue(gm.player.time);
ui->progressTime->update();
ui->labelMoney->setText(QString("Деньги: ") + QString::number(gm.player.money,'f',2) + "$");
updateRestList();
}
void MainWindow::processGameEnd(int gameScore)
{
showDialog("Конец игры", QString("Ваш счёт: ") + QString::number(gameScore));
QCoreApplication::exit();
}
void MainWindow::updatePlayerInfo()
{
// update labels
QString moodText = "Нейтральное";
if(gm.player.mood > 0)
{
moodText = "Хорошее";
}
else if(gm.player.mood < 0)
{
moodText = "Плохое";
}
ui->labelMood->setText(QString("Настроение: ") + moodText);
ui->labelMoney->setText(QString("Деньги: ") + QString::number(gm.player.money,'f',2) + "$");
QString healthText = "Плохое";
if(gm.player.health > 66)
{
healthText = "Отличное";
}
else if(gm.player.health > 33)
{
healthText = "Нормальное";
}
ui->labelHealth->setText(QString("Здоровье: ") + healthText);
ui->labelIntellect->setText(QString("Интеллект (IQ): ") + QString::number(gm.player.intellect));
QString textReputation = "О вас никто не знает";
if(gm.player.reputation > 10)
{
textReputation = "Вам доверяют";
}
else if(gm.player.reputation > 1)
{
textReputation = "О вас знают";
}
else if(gm.player.reputation < 0)
{
textReputation = "Вам не доверяют";
}
ui->labelReputation->setText(QString("Репутация: ") + textReputation);
// update progress
ui->progressTime->setValue(gm.player.time);
// update lists
updateSkillsList();
updateJobList();
updateRestList();
}
void MainWindow::updateSkillsList()
{
// prepare skills list
QStringList keys = gm.player.skills.keys();
QList<uint> values = gm.player.skills.values();
QStringList skills;
for(int i = 0; i < keys.size(); i++)
{
QString level = "Junior";
if(values[i] >= 3 && values[i] <= 6)
{
level = "Middle";
}
else if(values[i] >= 7)
{
level = "Senior";
}
skills.append(keys[i] + "\t" + level);
}
skills.sort();
// update skills list
ui->skillsList->clear();
ui->skillsList->addItems(skills);
}
void MainWindow::updateJobList()
{
ui->jobList->clear();
foreach(Phenomenon phen, gm.continuum)
{
if(phen.type == "Работа")
{
ui->jobList->addItem(phen.label);
// modify item display settings
QListWidgetItem* item = ui->jobList->item(ui->jobList->count() - 1);
if(item != nullptr)
{
if(phen.was == true)
{
// hide job that was already done
item->setHidden(true);
}
else
{
// check is job allowed
bool jobAllowed = false;
if(gm.player.reputation >= phen.change.reputation)
{
QString mainSkill = phen.change.skills.firstKey();
if(gm.player.skills.contains(mainSkill))
{
if(gm.player.skills[mainSkill] >= phen.change.skills[mainSkill])
{
jobAllowed = true;
}
}
}
// set color gray for not allowed job
if(!jobAllowed)
item->setTextColor(QColor("gray"));
}
}
}
}
}
void MainWindow::updateRestList()
{
ui->restList->clear();
foreach(Phenomenon phen, gm.continuum)
{
if(phen.type == "Место")
{
ui->restList->addItem(phen.label);
if(gm.player.money < phen.change.money*(-1))
{
ui->restList->item(ui->restList->count() - 1)->setTextColor(QColor("gray"));
}
}
}
}
void MainWindow::showEventDialog(Phenomenon event)
{
QString message = "Сообщение";
if(event.type == "Книга")
{
message = "Вы прочитали книгу";
}
else if(event.type == "Курс")
{
message = "Вы прошли курс";
}
else if(event.type == "Вопрос")
{
message = "Вы исследовали вопрос";
}
else if(event.type == "Работа")
{
message = "Вы выполнили работу";
ui->followProjectLabel->setText(unfoldProjectLabel(event.label));
}
else if(event.type == "Проект")
{
message = "Вы выполнили проект";
ui->followProjectLabel->setText(unfoldProjectLabel(event.label));
}
else if(event.type == "Место")
{
message = "Вы отдохнули в месте";
}
else if(event.type == "Событие")
{
message = "Непредвиденное обстоятельство";
}
if(event.type == "Место")
{
showImageDialog(message, event.label);
}
else
{
showDialog(message, event.label);
}
}
QString MainWindow::unfoldProjectLabel(QString label)
{
QStringList stringList = label.split(" ");
return "Последний проект: " +
stringList[stringList.size() - 2].replace("(","").replace(")","") +
" " + stringList[stringList.size()-1];
}
void MainWindow::showDialog(QString message, QString text)
{
QMessageBox dialog;
dialog.setWindowTitle(message);
dialog.setText(text);
dialog.exec();
}
void MainWindow::showImageDialog(QString message, QString text)
{
QString place = text.split(" ")[0].trimmed();
QMessageBox dialog;
QPixmap pix(":/res/images/rest/"+place+".jpeg");
int scaleFactor = 800;
if(pix.size().width() > pix.size().height())
{
scaleFactor = (scaleFactor > pix.size().width()? pix.size().width() : scaleFactor);
dialog.setIconPixmap(pix.scaledToWidth(scaleFactor));
}
else
{
scaleFactor = (scaleFactor > pix.size().height()? pix.size().height() : scaleFactor);
dialog.setIconPixmap(pix.scaledToHeight(scaleFactor));
}
dialog.setWindowTitle(message);
dialog.setText(text);
dialog.exec();
}
void MainWindow::on_readBookButton_clicked()
{
runStandartAct("Книга");
}
void MainWindow::on_learnCourseButton_clicked()
{
runStandartAct("Курс");
}
void MainWindow::on_getAnswerButton_clicked()
{
runStandartAct("Вопрос", false);
}
void MainWindow::on_takeProjectButton_clicked()
{
if(gm.player.skills.size() > 0)
{
runStandartAct("Проект", false);
}
else
{
showDialog("Сообщение","Вы не можете выполнить проект. "
"\n Вы ничего не умеете.");
}
}
void MainWindow::on_findJobButton_clicked()
{
updateJobList();
ui->stackedWidget->setCurrentIndex(1);
}
void MainWindow::on_doJobButton_clicked()
{
if(ui->jobList->selectedItems().size() > 0)
{
if(ui->jobList->selectedItems()[0]->textColor() == QColor("gray"))
{
showDialog("Сообщение", "Вы не можете сделать эту работу. "
"\n У вас недостаточно навыка или репутации.");
}
else
{
gm.act("Работа", ui->jobList->selectedItems()[0]->text());
}
}
ui->stackedWidget->setCurrentIndex(0);
}
void MainWindow::on_chooseRestButton_clicked()
{
if(ui->restList->selectedItems().size() > 0)
{
if(ui->restList->selectedItems()[0]->textColor() != QColor("gray"))
{
gm.act("Место", ui->restList->selectedItems()[0]->text());
}
else
{
showDialog("Сообщение", "У вас недостаточно денег.");
}
}
}
void MainWindow::on_tabWidget_currentChanged(int index)
{
ui->stackedWidget->setCurrentIndex(0);
if(ui->tabWidget->currentWidget() != ui->tab_main)
{
gm.pause = true;
}
else
{
gm.pause = false;
}
}
Что ж, я так же не буду останавливаться подробно на каждой строчке исходного кода самого приложения, вы сможете прочитать всю необходимую информацию из комментариев, что действительно вам потребуется ещё в процессе работы, так это знание того, как создавать слоты для конкретных элементов GUI непосредственно из QtDesigner.
Создание слота средствами QtDesigner
Для создания слота, обработки события конкретного элемента пользовательского интерфеса (кнопки, чекбокса, и т.п.) зайдите в QtDesigner и выберете этот элемент, кликнув по нему ПКМ
Откройте его и выберете пункт Перейти к слоту… Перед вами появиться выбор вариантов типа события, которое вы хотите обработать
Выберете событие и нажмите кнопку OK. Это автоматически перенесёт вас в редактор к тому месту, где только что вы добавили слот. Посмотрите внимательно — был добавлен только интерфейс данного слота, тогда как прописать его тело вы должны своими руками.
Ресурсы Qt
Ещё одна тема которую вы должны знать для полноценной разработки приложений на Qt — это ресурсы приложения. В любом достаточно сложном приложении всегда есть файлы, которые хранят непосредственно данные приложения:
- это могут быть медиафайлы (картинки, анимация, аудио и т.д.),
- так же это могут быть текстовые файлы,
- либо файлы баз данных и т.п.
Для того, чтобы создать определённый ресурс для вашего приложения и использовать его в своей программе, вам потребуется выполнить следующий алгоритм:
Алгоритм создания ресурса для приложения Qt
I. Создайте файл ресурсов (делается только один раз)
Затем выберете пункт Добавить новый файл…
Выберете пункт Qt -> Файл ресурсов Qt
Выберете размещение файла ресурсов
Добавьте этот файл под контроль версий (если нужно)
II. Создайте папку с ресурсами в каталоге вашего приложения и скопируйте в него все необходимые для вашего приложения ресурсные файлы.
III. Зарегистрируйте ваши ресурсные файлы в файле ресурсов при помощи редактора ресурсов.
При создании файла ресурсов здесь будет доступна только одна кнопка Добавить префикс… Отредактируйте префикс как вам удобно в окне ввода ниже (очень удобно когда префикс состоит только из одного символа слэша, потому что затем к нему прибавляется путь к каталогу расположения вашего ресурса, а он может быть очень длинным). Затем кликнете на эту кнопку и ещё на кнопку Добавить файлы… После этого добавьте каталог с вашими ресурсами. Это автоматически зарегистрирует все доступные в данном каталоге ресурсы для вашей программы.
IV. Теперь осталось использовать ваш файл внутри вашей программы. На примере выше, вы можете увидеть полный путь к ресурсу, что-нибудь вроде:
/res/images/rest/Крым.jpeg
QPixmap pix(":/res/images/rest/Крым.jpeg");
Это позволит вам загрузить вашу картинку, без указания её расположения в файловой системе. Файл ресурсов это очень удобная вещь, потому как он позволяет организовать и в более удобной форме использовать данные вашего приложения, сэкономив ваше время.
Итак это всё на сегодня. Внимательно читайте исходный код и используйте справку Qt для лучшего понимания. И ещё, я выложил исходный код на GitHub для тех, кому не терпиться сразу испытать наше приложение боем! Так что дерзайте! Всё в ваших руках!