Как разбить проект CMake на подпроекты
Разделяем на подпроекты существующий проект C++ на CMake
В этой статье мы рассмотрим процесс создания простого оконного приложения с помощью библиотеки Qt в среде разработки QtCreator. Мы поработаем с лэйаутами, то есть с разметкой элементов окна, добавим в окно таблицу, несколько полей ввода данных и кнопку.
Библиотека Qt - это набор классов C++ для создания кросс-платформенных приложений с графическим интерфейсом. С помощью этой библиотеки созданы, например, среда разработки QtCreator и инструмент для записи видео OBS Studio.
Для работы потребуется: компилятор C++; библиотека Qt, соответствующая компилятору (неплохая инструкция по установке); среда разработки QtCreator (или любая другая).
Видео-версия текста статьи:
На макете ниже показано как будет выглядеть наше приложение. С левой стороны разместим таблицу со списком людей, определяемых именем, фамилией и возрастом. А справа - панель добавления данных в таблицу. Тут будут те же самые поля, что и в таблице, а также кнопка для добавления.
Для начала создадим новый проект Qt Widgets в QtCreator на базе CMake.
Обращаю внимание, что нам не нужно создавать файл MainWindow.ui
, так как весь интерфейс окна мы будем задавать в коде, избегая инструментов QtDesigner. Поэтому снимаем соответствующую галочку.
В результате работы мастера будет создан проект из 4-х файлов:
Файл проекта CMakeLists.txt
;
Главный файл программы с точкой входа main.cpp
;
Класс окна MainWindow с заголовочным файлом MainWindow.h
и реализацией MainWindow.cpp
.
#pragma once
#include <QMainWindow>
class QTableWidget;
class QLineEdit;
class QSpinBox;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void onAddClicked();
private:
QTableWidget* table;
QLineEdit* nameInput;
QLineEdit* secondNameInput;
QSpinBox* ageInput;
};
В заголовочном файле мы объявляем слот void onAddClicked();
, который будет содержать реализацию обработчика нажатия на кнопку. Слот в Qt - это специальный метод класса, участвующий в концепции сигнал/слот. Слоты всегда объявляются в заголовочных файлах с указанием ключевого слова slots
. Например, наш слот определен в закрытой секции класса private slots:
.
Если коротко, то концепция сигналов и слотов в Qt - это что-то вроде событийной системы. Сигналы - это методы классов, определяющие некоторое событие. У этих методов нет реализации, мы используем от них только имена и аргументы. Слоты же - это методы классов, отвечающие за обработку событий. Вызов конкретного сигнала всегда будет инициировать вызов присоединенного к нему слота. Об их соединении поговорим ниже.
Также в заголовочном файле мы объявляем указатели на те виджеты, к которым нам понадобится доступ внутри слота.
private:
QTableWidget* table;
QLineEdit* nameInput;
QLineEdit* secondNameInput;
QSpinBox* ageInput;
Весь исходный код реализации класса MainWindow
приведен ниже. Создание всего интерфейса окна происходит в конструкторе. Рассмотрим некоторые его моменты подробнее.
#include "MainWindow.h"
#include <QHBoxLayout>
#include <QTableWidget>
#include <QLabel>
#include <QLineEdit>
#include <QSpinBox>
#include <QPushButton>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QWidget* centralWidget = new QWidget;
setCentralWidget(centralWidget);
QHBoxLayout* mainLayout = new QHBoxLayout;
centralWidget->setLayout(mainLayout);
table = new QTableWidget;
table->setColumnCount(3);
table->setHorizontalHeaderLabels(QStringList() << "Name" << "Second name" << "Age");
mainLayout->addWidget(table);
QVBoxLayout* verticalLayout = new QVBoxLayout;
mainLayout->addLayout(verticalLayout);
verticalLayout->addWidget(new QLabel("Name:"));
nameInput = new QLineEdit;
verticalLayout->addWidget(nameInput);
verticalLayout->addWidget(new QLabel("Second name:"));
secondNameInput = new QLineEdit;
verticalLayout->addWidget(secondNameInput);
verticalLayout->addWidget(new QLabel("Age:"));
ageInput = new QSpinBox;
verticalLayout->addWidget(ageInput);
QPushButton* addButton = new QPushButton("Add");
connect(addButton, &QPushButton::clicked, this, &MainWindow::onAddClicked);
verticalLayout->addWidget(addButton);
verticalLayout->addStretch();
}
MainWindow::~MainWindow()
{
}
void MainWindow::onAddClicked()
{
const QString name = nameInput->text();
const QString secondName = secondNameInput->text();
const int age = ageInput->value();
const int rowCount = table->rowCount();
table->setRowCount(rowCount + 1);
table->setItem(rowCount, 0, new QTableWidgetItem(name));
table->setItem(rowCount, 1, new QTableWidgetItem(secondName));
table->setItem(rowCount, 2, new QTableWidgetItem(QString::number(age)));
}
В первую очередь мы создаем главный горизонтальный лэйаут mainLayout
(QHBoxLayout) нашего окна.
Лэйауты в Qt предназначены для управления размещением виджетов в окне. Он задают не только место и порядок размещения виджетов, но и их размер, который вычисляется динамически при изменении размеров окна. Виджетами в Qt называют все графические элементы, которые могут быть размещены в окне (поля ввода, кнопки, таблицы, списки и т.д.)
Горизонтальный лэйаут подразумевает, что все виджеты, добавленные на него, будут размещены по горизонтали друг за другом слева направо в порядке добавления.
QWidget* centralWidget = new QWidget;
setCentralWidget(centralWidget);
QHBoxLayout* mainLayout = new QHBoxLayout;
centralWidget->setLayout(mainLayout);
Дальше мы добавляем в лэйаут пустую таблицу QTableWidget с тремя колонками под именами “Name” (имя человека), “Second name” (его фамилия) и “Age” (возраст).
table = new QTableWidget;
table->setColumnCount(3);
table->setHorizontalHeaderLabels(QStringList() << "Name" << "Second name" << "Age");
mainLayout->addWidget(table);
Теперь дело за правой панелью. Как видно на макете выше виджеты правой панели размещены по вертикали. Чтобы этого добиться в Qt, мы должны использовать вертикальный лэйаут QVBoxLayout, который добавляем внутрь горизонтального. Да, мы можем вкладывать лэйауты друг в друга.
QVBoxLayout* verticalLayout = new QVBoxLayout;
mainLayout->addLayout(verticalLayout);
Как только вертикальный лэйаут создан, мы можем его заполнять виджетами сверху вниз.
В верхней части добавляем статический текст “Name:”
с помощью виджета QLabel. Сразу после него размещаем поле ввода однострочного текста QLineEdit, которое будет обрабатывать ввод имени.
verticalLayout->addWidget(new QLabel("Name:"));
nameInput = new QLineEdit;
verticalLayout->addWidget(nameInput);
Дальше идет похожий блок кода, только для ввода фамилии.
verticalLayout->addWidget(new QLabel("Second name:"));
secondNameInput = new QLineEdit;
verticalLayout->addWidget(secondNameInput);
Третий элемент ввода отличается от остальных, так как он отвечает за ввод целочисленного значения возраста человека. Ввод целочисленных значений выполним с помощью виджета QSpinBox.
verticalLayout->addWidget(new QLabel("Age:"));
ageInput = new QSpinBox;
verticalLayout->addWidget(ageInput);
И последний элемент окна - кнопка QPushButton.
Здесь как раз появляется вызов connect(), отвечающий за соединение сигналов и слотов. Этот вызов можно прочитать так: “Присоединить вызов сигнала &QPushButton::clicked
объекта addButton
к слоту &MainWindow::onAddClicked
объекта this
(то есть самого класса MainWindow
)”. Таким образом клик по кнопке испускает сигнал clicked()
, который инициирует вызов метода onAddClicked()
.
QPushButton* addButton = new QPushButton("Add");
connect(addButton, &QPushButton::clicked, this, &MainWindow::onAddClicked);
verticalLayout->addWidget(addButton);
В завершение мы добавляем в вертикальный лэйаут “пружинку”, которая как бы подожмет все элементы лэйаута к верхней его части. Если не добавить “пружинку”, то все элементы лэйаута равномерно распределятся по всей его площади. При больших размерах окна это будет смотреться несобранно, а это нам не подходит.
verticalLayout->addStretch();
Слот onAddClicked()
, реализующий обработку нажатия кнопки, включает три условных этапа. Сначала мы получаем текущие данные из полей ввода.
const QString name = nameInput->text();
const QString secondName = secondNameInput->text();
const int age = ageInput->value();
Затем увеличиваем количество строк в таблице на 1. Тем самым создаем новую пустую строку в конце таблицы.
const int rowCount = table->rowCount();
table->setRowCount(rowCount + 1);
И, наконец, заполняем эту строку данными путем добавления текстовых элементов QTableWidgetItem в каждую ячейку. При этом целочисленную переменную age
предварительно конвертируем в строку QString
с помощью вызова статического метода QString::number()
.
table->setItem(rowCount, 0, new QTableWidgetItem(name));
table->setItem(rowCount, 1, new QTableWidgetItem(secondName));
table->setItem(rowCount, 2, new QTableWidgetItem(QString::number(age)));
Главный файл программы остается практически без изменений. Добавляем только свое название окна вызовом setWindowTitle(“People”);
и изменяем изначальный размер окна - делаем его немного больше вызовом resize(800, 600);
.
#include "MainWindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.setWindowTitle("People");
w.resize(800, 600);
w.show();
return a.exec();
}
Файл проекта CMakeLists.txt
изменений не потребует и останется точно таким каким его создал мастер во время создания проекта. Например, его содержимое может выглядеть так:
cmake_minimum_required(VERSION 3.5)
project(qt_example VERSION 0.1 LANGUAGES CXX)
# Включаем генераторы Qt
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Находим библиотеку Qt
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)
# Перечисляем исходные файлы проекта
set(PROJECT_SOURCES
main.cpp
MainWindow.cpp
MainWindow.h
)
# Создаем исполняемый файл проекта
add_executable(qt_example ${PROJECT_SOURCES})
# Прилинковываем библиотеку QtWidgets к исполняемому файлу проекта
target_link_libraries(qt_example PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)
После сборки и запуска проекта мы увидим наше окно.
Вводим данные и нажимаем кнопку “Add”. Новая запись появляется в таблице.
По умолчанию данные таблицы можно редактировать. Делаем двойной клик на интересующей ячейке и вводим новые данные. Например, меняем имя с “Ivan” на “Petr”.
На этом все.
В статье мы рассмотрели самую верхушку айсберга Qt: горизонтальные и вертикальные лэйауты и несколько виджетов. Но принципы построения окна во всех остальных случаях будут точно такие же. Изучайте и пробуйте.
Разделяем на подпроекты существующий проект C++ на CMake
Учимся создавать свое первое оконное приложение на Qt с использованием QMainWindow в среде QtCreator
Учимся создавать свою первую 3D модель тора в OpenCASCADE
Учимся работать с пул-реквестами на GitHub в личных репозиториях
Простейший пример исследования и патчинга исполняемого файла с помощью Cutter