IT Notes

Структура Qt-проекта на C++

Введение

Qt сегодня стал крайне популярным SDK для разработки кросс-платформенных приложений. И это легко понять. Обеспечена поддержка всех основных операционных систем: *nix, Windows и MacOS, а мощные возможности библиотек позволяют решать даже сложные задачи минимальным количеством кода. Кроме того, организация проекта на базе pro-файла весьма удобна, поэтому я применяю ее даже для тех проектов на C/C++, которые напрямую не используют возможности Qt'а. О таком способе организации проектов мы и поговорим.

Дерево проекта

Начнем сразу с общей структуры всего проекта с последовательным разбором его элементов:

.
├── bin
│   ├── debug
│   └── release
├── build
│   ├── debug
│   └── release
├── import
├── include
├── lib.linux
├── lib.win32
├── src
│   ├── include
│   ├── MyApp
│   └── MyLib
└── tests
    └── MyLibTest

На верхнем уровне у нас расположено 8 каталогов. Разберем каждый из них в отдельности.

Каталог с бинарниками - bin/

Сюда будут складываться все наши исполняемые файлы. Для режимов сборки debug/ и release/ предусмотрены собственные подкаталоги, чтобы упростить переключение между отладочной и чистовой версиями.

Каталог сборки - build/

Чтобы не смешивать вспомогательные obj, moc, rcc и ui файлы с исходниками или готовыми бинарниками отведен отдельный каталог. По аналогии с bin/ он разбит на подкаталоги debug/ и release/ для соответствующих режимов сборки.

Каталог с заголовочными файлами сторонних библиотек - import/

Сейчас довольно сложно написать более или менее серьезное приложение, в котором не задействованы никакие сторонние библиотеки. Конечно, всегда есть вариант разместить эти зависимости в системных папках и настроить к ним пути, но это решение имеет множество недостатков. Лучше всегда держать все зависимости проекта в едином каталоге, который будет помещен под управление системы контроля версий, однако это уже тема отдельной заметки. Организация этого подкаталога может быть достаточно произвольной, но я предпочитаю для каждой отдельной библиотеки выделить свой подкаталог с h-файлами, чтобы избежать возможного конфликта имен.

Каталог с внешними заголовочными файлами нашего проекта - include/

Сюда мы будем помещать все наши h-файлы, которые соответствуют открытым частям интерфейса библиотек проекта. Вообще, этот каталог можно было бы назвать не include/, а export/, но это уже дело вкуса. Он вполне может оказаться пустым, если вы пишите небольшой одноразовый проект, код которого потом не будет повторно использоваться. Но если вы решите передать кому-то ваши наработки, то при наличии такого каталога для этого будет достаточно скопировать его вместе с содержимым lib.*/, о котором мы поговорим в следующем подразделе.

лето на алтае.

Каталог с библиотеками - lib.(linux|win32)/

Чтобы не плодить глубокую вложенную иерархию подкаталогов с ветвлением по версиям операционных систем, мы просто создаем необходимые каталоги верхнего уровня с нужным суффиксом linux или win32. Я не занимаюсь разработкой приложений для MacOS, но думаю, что вы без труда добавите нужный суффикс, если это понадобится. Сюда мы будем помещать как сторонние библиотеки с разбиением на подкаталоги по аналогии с заголовочными файлами в import/, так и наши собственные библиотеки, но уже непосредственно в сам каталог lib.*/.

Кроме того, заметим, что для win32-приложений, собираемых с помощью компилятора msvc из Visual Studio, динамические библиотеки разделяются на *.lib и *.dll файлы. Первые используются во время линковки компоновщиком, а вторые непосредственно во время работы приложения и должны находиться в одном каталоге с использующим их исполняемым файлом. Возникает вопрос о том, куда поместить эти файлы для используемых сторонних библиотек. Однозначно *.lib-файлы должны лежать по путям, параллельным *.so-шникам для линукс приложений. Но куда деть *.dll-ки? Возможно несколько вариантов. Один из них заключается в том, чтобы поместить их рядом с *.lib-файлами. Но тогда их придется вручную копировать в bin/. Если же поместить их сразу в bin/, то они будут засорять сборку под другими операционными системами или с компилятором gcc, поэтому я бы не стал рекомендовать этот способ. Отдельный каталог для этого заводить тоже смысла нет, поэтому с учетом всех плюсов и минусов я сам храню *.dll-файлы рядом с *.lib-ами.

Каталог с исходниками - src/

В нем для каждого модуля заводится отдельный подкаталог с его именем, в котором будут лежать cpp-файлы и закрытые h-файлы, которые нужны только внутри этого модуля. Не забудьте про каталог include/ верхнего уровня, в который идет на экспорт часть внешних заголовочных файлов нашего приложения. Но что делать с разделяемыми заголовочными файлами, которые нужны в нескольких наших модулях, но не имеющих такого большого значения, чтобы можно было их экспортировать? Для этого предназначен внутренний каталог src/include/. В него мы можем поместить наборы внутренних констант, объявлений классов и функций, а потом совместно использовать их в наших модулях, не нарушая инкапсуляцию.

Каталог с тестами - tests/

Последний по порядку, но не по значению. Тесты никогда не бывают лишними, однако нет особого смысла относиться к ним каким-то особым образом. Это тоже бинарники. Единственное отличие заключается в том, что их исходные коды должны быть логически отделены от основных, чтобы ничего не смешалось.

Создаем файлы проекта

А теперь посмотрим на файлы, определяющие структуру проекта для утилиты qmake, на основе которых будет создан набор Makefile'ов. Как говорилось во введении, утилита qmake позволяет управлять проектами для любых C/C++ приложений, поэтому если вы пишите программу на C++, но по какой-то причине для разработки графического интерфейса применяете модули GTK+ или Win32 API, то вас не должно это останавливать от применения приведенного ниже материала.

MyProject.pro

Главный pro-файл нашего проекта. В Visual Studio этот уровень называется Solution.

TEMPLATE = subdirs

SUBDIRS += \
    src/MyApp \
    src/MyLib

Для него используется Qt-шаблон subdirs, что означает, что наш проект будет состоять из набора модулей-подпроектов. Кто-то может решить, что нет смысла заниматься компоновкой такой многоуровневой структуры и гораздо удобнее сделать приложение на базе единственного app-проекта. Возможно, что в некоторых случаях это так, но шаблон subdirs не запрещает использование одного модуля, да и много времени вы на этом не сэкономите. Зато в дальнейшем очень часто оказывается, что одного модуля было недостаточно и все равно приходится что-то менять.

В приведенном примере у нас всего два модуля: MyApp - исполняемое приложение и MyLib - вспомогательная библиотека. Но прежде чем спуститься на уровень ниже и посмотреть на то, как устроены MyApp и MyLib, рассмотрим несколько вспомогательных pri-файлов.

common.pri

Общий для всех модулей файл с определениями путей и некоторых констант, задействованных при сборке:

PROJECT_ROOT_PATH = $${PWD}/

win32: OS_SUFFIX = win32
linux-g++: OS_SUFFIX = linux

CONFIG(debug, debug|release) {
    BUILD_FLAG = debug
    LIB_SUFFIX = d
} else {
    BUILD_FLAG = release
}

LIBS_PATH = $${PROJECT_ROOT_PATH}/lib.$${OS_SUFFIX}/
INC_PATH = $${PROJECT_ROOT_PATH}/include/
IMPORT_PATH = $${PROJECT_ROOT_PATH}/import/
BIN_PATH = $${PROJECT_ROOT_PATH}/bin/$${BUILD_FLAG}/

BUILD_PATH = $${PROJECT_ROOT_PATH}/build/$${BUILD_FLAG}/$${TARGET}/
RCC_DIR = $${BUILD_PATH}/rcc/
UI_DIR = $${BUILD_PATH}/ui/
MOC_DIR = $${BUILD_PATH}/moc/
OBJECTS_DIR = $${BUILD_PATH}/obj/

LIBS += -L$${LIBS_PATH}/
INCLUDEPATH += $${INC_PATH}/
INCLUDEPATH += $${IMPORT_PATH}/

linux-g++: QMAKE_CXXFLAGS += -std=c++11

Разберем отдельные блоки этого файла. В первой строке просто фиксируется путь к корневому каталогу проекта, относительно которого будем определять все остальные пути:

PROJECT_ROOT_PATH = $${PWD}/

Далее определяем то, под какой ОС происходит сборка, и устанавливаем соответствующим образом значение суффикса OS_SUFFIX. Значение этого суффикса будет использовано для ветвления по каталогам lib.*/.

win32: OS_SUFFIX = win32
linux-g++: OS_SUFFIX = linux

В следующем фрагменте в зависимости от режима сборки (debug или release) определяется значение BUILD_FLAG, которое будет указывать на версию используемого подкаталога в bin/ и build/:

CONFIG(debug, debug|release) {
    BUILD_FLAG = debug
    LIB_SUFFIX = d
} else {
    BUILD_FLAG = release
}

Кроме того, определяется вспомогательный суффикс LIB_SUFFIX. Мы будем использовать его для того, чтобы к именам библиотек в отладочном режиме присоединялся символ d. За счет этого мы можем иметь единый каталог для библиотек и не допускать конфликтов имен. Например, в lib.win32/ у нас может одновременно находиться обе версии MyLib.lib и MyLibd.lib.

Далее по порядку определяются пути к библиотекам lib.*/, к открытым заголовочным файлам include/, к импортируемым заголовочным файлам import/ и путь к каталогу с бинарниками bin/:

LIBS_PATH = $${PROJECT_ROOT_PATH}/lib.$${OS_SUFFIX}/
INC_PATH = $${PROJECT_ROOT_PATH}/include/
IMPORT_PATH = $${PROJECT_ROOT_PATH}/import/
BIN_PATH = $${PROJECT_ROOT_PATH}/bin/$${BUILD_FLAG}/

Заметим, что в конце определения LIBS_PATH мы воспользовались нашим OS_SUFFIX, а в конце BIN_PATH подставили BUILD_FLAG, чтобы привести пути в соответствие с нашей начальной задумкой по ветвлению конфигурации проекта на основании версии ОС и режиму сборки.

Ниже стоит блок, который задает пути сборки для файлов ресурсов rcc, файлов графического интерфейса ui, МОК-файлов moc и объектных файлов obj:

BUILD_PATH = $${PROJECT_ROOT_PATH}/build/$${BUILD_FLAG}/$${TARGET}/
RCC_DIR = $${BUILD_PATH}/rcc/
UI_DIR = $${BUILD_PATH}/ui/
MOC_DIR = $${BUILD_PATH}/moc/
OBJECTS_DIR = $${BUILD_PATH}/obj/

Каталог сборки для каждого подпроекта будет свой. При этом его расположение зависит от режима сборки и от имени самого подпроекта, которому соответствует переменная TARGET, определенная для каждого модуля.

Поскольку библиотеки и заголовочные файлы с большой вероятностью будут использоваться многими модулями совместно, то мы определяем ключевые переменные LIBS и INCLUDEPATH тоже в общем файле:

LIBS += -L$${LIBS_PATH}/
INCLUDEPATH += $${INC_PATH}/
INCLUDEPATH += $${IMPORT_PATH}/

Ключ -L перед $${LIBS_PATH} означает, что мы определяем каталог, в котором компоновщик должен искать библиотеки в процессе сборки. А чтобы добавить конкретную библиотеку, нужно использовать ключ -l. Например:

LIBS += -lMyLib

При этом, как видно из примера, расширение файла указывать не требуется, поскольку в разных ОС и для разных компиляторов оно может быть разным.

Последняя строка не является обязательной для сборки, но если вы хотите задействовать в вашем приложении возможности C++11, то имеет смысл ее не забыть:

linux-g++: QMAKE_CXXFLAGS += -std=c++11

app.pri

Между всеми исполняемыми модулями есть что-то общее. Определим соответствующие настройки сборки в pri-файле:

DESTDIR = $${BIN_PATH}/
linux-g++: QMAKE_LFLAGS += -Wl,--rpath=\\\$\$ORIGIN/../../lib.$${OS_SUFFIX}/

Переменная DESTDIR указывает путь, в который будет помещен готовый исполняемый файл. Это окажется либо bin/debug/, либо bin/release/.

В следующей строке определяется путь поиска динамических библиотек по умолчанию. В Windows он работать не будет. А в Linux позволяет упростить запуск скомпонованного исполняемого файла. Если опустить эту строку, то приложение все равно можно будет запустить, но тогда:

  1. Либо библиотеки должны лежать в системных папках, по которым осуществляется поиск;
  2. Либо путь сборки библиотек должен быть добавлен к системным с помощью файла /etc/ld.so.conf;
  3. Либо должна быть определена переменная окружения LD_LIBRARY_PATH.

Вариант с LD_LIBRARY_PATH является самым простым, поскольку в этом случае вам не нужны root-права в системе. Удобно использовать для этого скрипт следующего вида:

#!/bin/sh

export LD_LIBRARY_PATH=../../lib.linux/:../../lib.linux/import_dir/
./MyApp

lib.pri

Как и для исполняемых файлов, для библиотек тоже удобно определить общий pri-файл:

DESTDIR = $${LIBS_PATH}/
win32: DLLDESTDIR = $${BIN_PATH}/
VERSION = 1.0.0
QMAKE_TARGET_COPYRIGHT = (c) My Company Name

Переменная DESTDIR имеет такой же смысл, как и в app.pri.

Следующая строка будет работать только в Windows. Она удобна тем, что позволяет автоматически скопировать все *.dll-файлы в каталог к исполняемым файлам.

Определения переменных в конце указывают информацию о версии библиотеки и ваш копирайт. Например, в Linux при значении VERSION = 2.0.1 вы получите библиотеки с именем вида libMyLib.so.2.0.1. Но копирайт будет отображаться только в Windows, при этом имя библиотек будет выглядеть следующим образом: MyLib2.dll, а в свойствах вы увидите что-то подобное:

MyLib2

Заметим, что может появиться необходимость определить переменную VERSION для каждой библиотеки отдельно, если вы хотите иметь тонкий контроль над этим значением. Но я считаю, что версии всех библиотек в рамках одного проекта должны совпадать и не имеет смысла вносить излишние различия.

Кроме того, обратите внимание, что в описании библиотеки указаны не только "Авторские права", но и "Описание файла" с "Названием продукта". Эти два значения уже имеет смысл определять для каждой библиотеки в отдельности.

MyApp.pro

Пришло время посмотреть на содержимое файла конкретного модуля:

QT       += core gui

TARGET = MyApp
TEMPLATE = app

SOURCES += main.cpp\
        mainwindow.cpp

HEADERS  += mainwindow.h

FORMS    += mainwindow.ui

include( ../../common.pri )
include( ../../app.pri )

LIBS += -lMyLib$${LIB_SUFFIX}

Содержимое этого файла достаточно типично для Qt-проектов и не вызывает особых сложностей. Большую его часть может легко создать QtCreator, поэтому рассмотрим лишь последние 3 строки. Директива include позволяет включить содержимое наших pri-файлов, объявленных ранее. В зависимости от версии утилиты qmake мы могли бы вынести команду include( ../../common.pri ) в начало файла app.pri, чтобы уменьшить количество кода, поэтому проверьте, будет ли работать такой вариант у вас. В последней строке мы просто подключаем наш модуль MyLib с суффиксом LIB_SUFFIX. Заметим, что путь поиска библиотек компоновщиком уже был определен в common.pri, поэтому здесь нам его дублировать не нужно.

MyLib.pro

include( ../../common.pri )
include( ../../lib.pri )

QT       += core gui

TARGET = MyLib$${LIB_SUFFIX}
TEMPLATE = lib

DEFINES += MYLIB_LIBRARY

SOURCES += mylib.cpp

HEADERS += ../../include/mylib.h \
    ../../include/mylib_global.h

win32 {
    QMAKE_TARGET_PRODUCT = My Lib
    QMAKE_TARGET_DESCRIPTION = It is my library
}

Здесь тоже все достаточно стандартно, но обратим внимание на следующие моменты:

  1. Чтобы в переменной TARGET задействовать суффикс LIB_SUFFIX из common.pri подключаем его заранее в самом начале;
  2. Заголовочные файлы предполагается передать в дальнейшем кому-то другому, поэтому перенесем их в предназначенный для этого каталог include/;
  3. Название продукта и описание, про которые говорилось выше, мы описываем для каждой библиотеки с помощью переменных QMAKE_TARGET_PRODUCT и QMAKE_TARGET_DESCRIPTION.

Заключение

Вот мы и рассмотрели способ организации проекта на C++. Очевидно, что нет особой необходимости пользоваться утилитой qmake для того, чтобы ей следовать. Кроме того, с минимальными изменениями она вполне может подойти и для проектов на других языках программирования. Однако следует учитывать, что не существует идеального способа организации проекта, поэтому вы можете взять предложенный мной вариант в качестве основы и адаптировать его под свои нужды.

Похожие публикации

Комментарии

А можно где-то посмотреть исходный код проекта с описанной в статье структурой?

Anonymous:

А можно где-то посмотреть исходный код проекта с описанной в статье структурой?

В качестве примера можете посмотреть проект из статьи про разработку простого чата.

Представляется более рациональным вместо:

include( ../../common.pri )

include( ../../lib.pri )

использовать универсальный способ записи:

include($${_PRO_FILE_PWD_}/common.pri)

include($${_PRO_FILE_PWD_}/lib.pri)

где _PRO_FILE_PWD_ - встроенная переменная, которая содержит путь к каталогу самого первого файла, который начал обрабатывать qmake, т.е. в нашем случае к каталогу MyProject.pro файла. С ее помощью всегда можно вычислить абсолютный путь к файлам вроде common.pri и lib.pri. (http://blog.mgsxx.com/?p=1781). В приведенной записи эти файлы у нас расположены в одном каталоге с главным pro-файлом нашего проекта. В этом случае мы избавляемся от необходимости следить за структурой и глубиной вложенности используемых каталогов нашего проекта.

Здравствуйте, russich. Большое спасибо за такие развернутые комментарии.

Мне очень понравилась предложенная в статье структура проектов. Очень основательный подход к решению отнюдь не второстепенного вопроса организации проектов. Можно конечно же довольствоваться проектами, созданными по умолчанию мастером и валить в общую кучу билды всех своих проектов… Но с увеличением числа созданных проектов и решенных задач в этой файлопомойке все труднее будет ориентироваться самому автору. О сторонних разработчиках и вовсе не может быть речи. Не будет нормального решения для повторного использования уже разработанного кода. Четкая структуризация всех проектов - залог отсутствия дублирования кода и минимальных затрат на поиск необходимого модуля в будущем не только автором, но и его коллегами. Полная аналогия со структуризацией книг в библиотеке. Если у какого-либо буратины в личной библиотеке кроме букваря и пары рекламных буклетов ничего нет - нафига ему какая-то каталогизация? А если этот же буратина зайдет в любую большую библиотеку - то без каталогизации там делать нечего… Аналогичную каталогизацию программного кода предлагает программистам классовая методика в программировании. Это один из побудительных мотивов создания объектного подхода. Наследование и инкапсуляция являются хорошими инструментами для упорядочивания большого количества фрагментов программного кода. А хорошая структура проектов создает практическую реализацию систематизации разработки программного кода.

В нашем случае объектный подход при создании структуры взаимосвязанных проектов и предложен в данной статье. Потому что объектный подход это не только кодинг, с использованием правил записи объектноориентированного (ОО) языка, это прежде всего способность мыслить объектноориентированными категориями. Можно в синтаксисе ОО языка на деле писать код, который по своей сути не потянет даже на структурный подход…

И в заключении поста хочу добавить немного по существу тематики данной статьи. DEPENDPATH - эта переменная предназначена для оптимизации работы утилиты make (http://blog.mgsxx.com/?p=2070). Она призвана разделить стабильные и изменяемые файлы, чтобы не включать в список зависимостей стабильные файлы и тем самым сократить работу make. Поэтому при составлении списка путей включаемых заголовочных файлов можно сделать так:

оригинал:

LIBS += -L$${LIBS_PATH}/

INCLUDEPATH += $${INC_PATH}/

INCLUDEPATH += $${IMPORT_PATH}/

По условиям, оговоренным в статье, IMPORT_PATH - каталог с заголовочными файлами сторонних библиотек (import/), INC_PATH - каталог с внешними заголовочными файлами нашего проекта (include/). Следовательно в список построения зависимостей надо будет включать только заголовочные файлы из каталога INC_PATH. В итоге с учетом сказанного получим:

LIBS += -L$${LIBS_PATH}/

DEPENDPATH += $${INC_PATH}/

INCLUDEPATH += $${INC_PATH}/

INCLUDEPATH += $${IMPORT_PATH}/

Переменную DEPENDPATH имеет смысл использовать только при CONFIG -= depend_includepath. Но как замечено в http://blog.mgsxx.com/?p=2070 "практика показала, что от такой оптимизации больше вреда, чем пользы." Так что решение об использовании данной feature - дело вкуса разработчика…

Статья хорошая, но как уже было сказано не хватает какого-нибудь проекта на гитхабе(тенденция). Всё таки лучший пример это конкретный QtCreator-проект. Я не знаю, может вам надо чтобы мы почаще обращались к вашей статье, но всё равно большое спасибо.

Админ, что ты будешь делать, если у тебя в проекте окажется несколько десятков файлов *.cpp, *.h, разбросанных по разным директориям? Я пытался прописать в таком формате

INCLUDEPATH += foo_1/include/*.h \

foo_2/include/*.h \

SOURCES += foo_1/src/*.cpp \

foo_2/src/*.cpp \

но при парсинге .pro файла qt выдавал ошибку, что нет таких файлов. Некогда тогда было искать решение, пришлось руками прописать каждый файлик. Это было оч неудобно. Есть какое-нибудь решение этого вопроса?

Здравствуйте. Существует недокументированная функция qmake - $$files. Ваш код можно переписать с ее помощью следующим образом:

HEADERS += $$files(foo_1/include/*.h) \

$$files(foo_2/include/*.h)

SOURCES += $$files(foo_1/src/*.cpp) \

$$files(foo_2/src/*.cpp)

При этом для включения заголовочных файлов в проект достаточно обойтись INCLUDEPATH с указанием имени каталога без перечисления отдельных имен файлов.

export LD_LIBRARY_PATH=../lib.linux/:../lib.linux/import_dir/

в свете данного примера, тут видимо имелось ввиду

export LD_LIBRARY_PATH=../../lib.linux/:../../lib.linux/import_dir/

не так ли?

Zugr:

export LD_LIBRARY_PATH=../lib.linux/:../lib.linux/import_dir/

в свете данного примера, тут видимо имелось ввиду

export LD_LIBRARY_PATH=../../lib.linux/:../../lib.linux/import_dir/

не так ли?

Спасибо за комментарий. Внес поправку в текст статьи.

Нужно отметить, что если нет необходимости, qmake вообще лучше не использовать в качестве сборочной системы.

cmake куда гибче и проще.

У меня нужда появилась, и выяснилось, что почему-то он сам дописывает в название библиотеки её версию. причём не 0.0.1, а просто 0. И линковщик по имени без версии её не находит.

Очень благодарен Вам за такую полезную статью, а также авторам комментариев за не менее ценную информацию!