Qt сегодня стал крайне популярным SDK для разработки кросс-платформенных приложений. И это легко понять. Обеспечена поддержка всех основных операционных систем: *nix, Windows и MacOS, а мощные возможности библиотек позволяют решать даже сложные задачи минимальным количеством кода. Кроме того, организация проекта на базе pro-файла весьма удобна, поэтому я применяю ее даже для тех проектов на C/C++, которые напрямую не используют возможности Qt’а. О таком способе организации проектов мы и поговорим.
РЕКОМЕНДУЕМ: Пример использования QtConcurrent
- Дерево проекта
- Каталог с бинарниками — bin/
- Каталог сборки — build/
- Каталог с заголовочными файлами сторонних библиотек — import/
- Каталог с внешними заголовочными файлами нашего проекта — include/
- Каталог с библиотеками — lib.(linux|win32)/
- Каталог с исходниками — src/
- Каталог с тестами — tests/
- Создаем файлы проекта
- MyProject.pro
- common.pri
- app.pri
- lib.pri
- MyApp.pro
- MyLib.pro
- Здесь тоже все достаточно стандартно, но обратим внимание на следующие моменты:
- Заключение
Дерево проекта
Начнем сразу с общей структуры всего проекта с последовательным разбором его элементов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
. ├── bin │ ├── debug │ └── release ├── build │ ├── debug │ └── release ├── import ├── include ├── lib.linux ├── lib.win32 ├── src │ ├── include │ ├── MyApp │ └── MyLib └── tests └── MyLibTest |
На верхнем уровне у нас расположено 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.
1 2 3 4 5 |
TEMPLATE = subdirs SUBDIRS += \ src/MyApp \ src/MyLib |
Для него используется Qt-шаблон subdirs, что означает, что наш проект будет состоять из набора модулей-подпроектов. Кто-то может решить, что нет смысла заниматься компоновкой такой многоуровневой структуры и гораздо удобнее сделать приложение на базе единственного app-проекта. Возможно, что в некоторых случаях это так, но шаблон subdirs не запрещает использование одного модуля, да и много времени вы на этом не сэкономите. Зато в дальнейшем очень часто оказывается, что одного модуля было недостаточно и все равно приходится что-то менять.
В приведенном примере у нас всего два модуля: MyApp — исполняемое приложение и MyLib — вспомогательная библиотека. Но прежде чем спуститься на уровень ниже и посмотреть на то, как устроены MyApp и MyLib, рассмотрим несколько вспомогательных pri-файлов.
[crayon-6628b42fa23e4200335002-i/]
Общий для всех модулей файл с определениями путей и некоторых констант, задействованных при сборке:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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 |
Разберем отдельные блоки этого файла. В первой строке просто фиксируется путь к корневому каталогу проекта, относительно которого будем определять все остальные пути:
1 |
PROJECT_ROOT_PATH = $${PWD}/ |
Далее определяем то, под какой ОС происходит сборка, и устанавливаем соответствующим образом значение суффикса OS_SUFFIX. Значение этого суффикса будет использовано для ветвления по каталогам lib.*/.
1 2 |
win32: OS_SUFFIX = win32 linux-g++: OS_SUFFIX = linux |
В следующем фрагменте в зависимости от режима сборки ( debug или release) определяется значение BUILD_FLAG, которое будет указывать на версию используемого подкаталога в bin/ и build/:
1 2 3 4 5 6 |
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/:
1 2 3 4 |
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:
1 2 3 4 5 |
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 тоже в общем файле:
1 2 3 |
LIBS += -L$${LIBS_PATH}/ INCLUDEPATH += $${INC_PATH}/ INCLUDEPATH += $${IMPORT_PATH}/ |
Ключ -L перед $${LIBS_PATH} означает, что мы определяем каталог, в котором компоновщик должен искать библиотеки в процессе сборки. А чтобы добавить конкретную библиотеку, нужно использовать ключ -l. Например:
1 |
LIBS += -lMyLib |
При этом, как видно из примера, расширение файла указывать не требуется, поскольку в разных ОС и для разных компиляторов оно может быть разным.
Последняя строка не является обязательной для сборки, но если вы хотите задействовать в вашем приложении возможности C++11, то имеет смысл ее не забыть:
1 |
linux-g++: QMAKE_CXXFLAGS += -std=c++11 |
[crayon-6628b42fa240c630099437-i/]
Между всеми исполняемыми модулями есть что-то общее. Определим соответствующие настройки сборки в pri-файле:
1 2 |
DESTDIR = $${BIN_PATH}/ linux-g++: QMAKE_LFLAGS += -Wl,--rpath=\\\$\$ORIGIN/../../lib.$${OS_SUFFIX}/ |
Переменная DESTDIR указывает путь, в который будет помещен готовый исполняемый файл. Это окажется либо bin/debug/, либо bin/release/.
В следующей строке определяется путь поиска динамических библиотек по умолчанию. В Windows он работать не будет. А в Linux позволяет упростить запуск скомпонованного исполняемого файла. Если опустить эту строку, то приложение все равно можно будет запустить, но тогда:
- Либо библиотеки должны лежать в системных папках, по которым осуществляется поиск;
- Либо путь сборки библиотек должен быть добавлен к системным с помощью файла /etc/ld.so.conf;
- Либо должна быть определена переменная окружения LD_LIBRARY_PATH.
Вариант с LD_LIBRARY_PATH является самым простым, поскольку в этом случае вам не нужны root-права в системе. Удобно использовать для этого скрипт следующего вида:
1 2 3 4 |
#!/bin/sh export LD_LIBRARY_PATH=../../lib.linux/:../../lib.linux/import_dir/ ./MyApp |
[crayon-6628b42fa2426560346531-i/]
Как и для исполняемых файлов, для библиотек тоже удобно определить общий pri-файл:
1 2 3 4 |
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.
Заметим, что может появиться необходимость определить переменную VERSION для каждой библиотеки отдельно, если вы хотите иметь тонкий контроль над этим значением. Но я считаю, что версии всех библиотек в рамках одного проекта должны совпадать и не имеет смысла вносить излишние различия.
Кроме того, обратите внимание, что в описании библиотеки указаны не только «Авторские права», но и «Описание файла» с «Названием продукта». Эти два значения уже имеет смысл определять для каждой библиотеки в отдельности.
[crayon-6628b42fa2430264009691-i/]
Пришло время посмотреть на содержимое файла конкретного модуля:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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, поэтому здесь нам его дублировать не нужно.
[crayon-6628b42fa243d607520356-i/]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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 } |
[crayon-6628b42fa243f486171467-i/]Здесь тоже все достаточно стандартно, но обратим внимание на следующие моменты:
- Чтобы в переменной TARGET задействовать суффикс LIB_SUFFIX из common.pri подключаем его заранее в самом начале;
- Заголовочные файлы предполагается передать в дальнейшем кому-то другому, поэтому перенесем их в предназначенный для этого каталог include/;
- Название продукта и описание, про которые говорилось выше, мы описываем для каждой библиотеки с помощью переменных QMAKE_TARGET_PRODUCT и QMAKE_TARGET_DESCRIPTION.
Заключение
Вот мы и рассмотрели способ организации проекта на C++. Очевидно, что нет особой необходимости пользоваться утилитой qmake для того, чтобы ей следовать. Кроме того, с минимальными изменениями она вполне может подойти и для проектов на других языках программирования. Однако следует учитывать, что не существует идеального способа организации проекта, поэтому вы можете взять предложенный мной вариант в качестве основы и адаптировать его под свои нужды.