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

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

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

РЕКОМЕНДУЕМ: Пример использования QtConcurrent

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

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

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

Каталог с бинарниками — [crayon-6628b42fa23ad476236777-i/]

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

Каталог сборки — [crayon-6628b42fa23b0590235052-i/]

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

Каталог с заголовочными файлами сторонних библиотек — [crayon-6628b42fa23b8196060045-i/]

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

Каталог с внешними заголовочными файлами нашего проекта — [crayon-6628b42fa23bc681988710-i/]

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

Каталог с библиотеками — [crayon-6628b42fa23c1671717316-i/]

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

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

Каталог с исходниками — [crayon-6628b42fa23d0055212396-i/]

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

Каталог с тестами — [crayon-6628b42fa23d5081679349-i/]

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

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

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

[crayon-6628b42fa23d9647687133-i/]

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

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

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

[crayon-6628b42fa23e4200335002-i/]

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

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

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

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

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

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

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

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

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

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

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

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

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

[crayon-6628b42fa240c630099437-i/]

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

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

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

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

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

[crayon-6628b42fa2426560346531-i/]

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

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

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

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

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

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

[crayon-6628b42fa2430264009691-i/]

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

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

[crayon-6628b42fa243d607520356-i/]

[crayon-6628b42fa243f486171467-i/]Здесь тоже все достаточно стандартно, но обратим внимание на следующие моменты:

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

Заключение

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

Понравилась статья? Поделиться с друзьями:
Добавить комментарий