Как Qt-сигналы/слоты фактически связаны с элементами в файле .ui?

В настоящее время я работаю над проектом Qt и несколько запутался с механизмом сигналов и слотов. Я чувствую, однако, что у меня есть некоторое хорошее понимание различий между QObject и формой пользовательского интерфейса.

Форма пользовательского интерфейса (описываемая файлом .ui) подается в компилятор пользовательского интерфейса (uic) и создает связанный файл заголовка. Этот заголовочный файл не просто содержит информацию о интерфейсе, но вместо этого содержит детали реализации в QObject, которые должны быть отформатированы. QObject, с другой стороны, является базовым классом, на котором построена много основы Qt. Системы сигналов и слотов полностью основаны на QObject.

При расширении класса QObject (или из производного класса) вы фактически определяете объект, в котором могут генерироваться сигналы и слоты. Чтобы отформатировать этот объект так, как пользовательский интерфейс, который вы только что создали в Qt Designer, вы создаете экземпляр класса ui (через заголовок ui, сгенерированный uic). Вы вызываете метод установки этого класса и передаете ему новый объект QObject, который вы создаете.

Все это имеет смысл.

Однако, что не имеет смысла, это когда вы начинаете фактически определять сигналы. С помощью Qt Designer у меня есть возможность щелкнуть правой кнопкой мыши и выбрать "сигнал". Когда я выбираю сигнал (будь то щелчок мышью по кнопке или изменение индикатора выполнения), он заканчивает добавление этого в раздел сигналов QObjects. Мой вопрос: почему и как? Как именно мои действия в Qt Designer (программа, которая генерирует XML для uic), связаны с моим QObject в Qt Creator? В частности, как узнать, какой QObject добавить этот слот?

Этот вопрос может быть несколько неясным, поэтому я приведу здесь пример.

Скажем, например, я хочу создать новый интерактивный экран. Позвольте называть его "MyScreen". Для создания этого интерфейса у меня, скорее всего, будет 3 файла: "myscreen.h", "myscreen.cpp" и "myscreen.ui". "myscreen.h" отвечает за объявление свойств и методов QObjects, а также сигналов и слотов. "myscreen.cpp" отвечает за определение методов, сигналов и слотов. "myscreen.ui" является копией для создания макета пользовательского интерфейса, который будет использоваться для форматирования экземпляра MyScreen.

Но из-за того, что я уже говорил ранее, что ui используется только для генерации заголовка, почему именно он связан с "myscreen.cpp" ? То есть, я могу щелкнуть правой кнопкой мыши по макету .ui, создать новый тип сигнала, и этот тип сигнала будет добавлен в myscreen.h и myscreen.cpp. Как это произошло? Как это сочетается? Работает ли Qt так, что должно существовать 3 файла (xxx.h, xxx.cpp, xxx.ui), которые должны существовать?

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

TL;DR - Как сигнал/слот от .h и .cpp на самом деле ссылаются на элементы, определенные в файле .ui?

Ответ 1

Мой вопрос: почему и как? Как именно мои действия в Qt Designer (программа, которая генерирует XML для uic), связаны с моим QObject в Qt Creator? В частности, как узнать, какой QObject добавить этот слот?

Короткий ответ: magic.

Реальный ответ: он фактически не имеет ничего общего с вашими действиями в Designer, по крайней мере, не напрямую. На самом деле это даже не специфичная для пользовательского интерфейса вещь, а просто тот дизайнер хорошо ее использует. Что происходит:

Прежде всего, всякий раз, когда вы компилируете файл, который использует макрос Q_OBJECT, это запускает инструмент Qt moc для запуска во время компиляции ( "триггеры" на самом деле не самое точное описание: точнее, когда qmake запускается для создания make файлов, он добавляет правила сборки для запуска moc, когда Q_OBJECT обнаружен в заголовке, но это рядом с точкой). Детали немного выходят за рамки этого ответа, но короткая история в конечном итоге генерирует те файлы moc_*.cpp, которые вы увидите после компиляции, которые содержат целую кучу скомпилированной метаинформации. Важная часть этого - это то, где он генерирует доступную информацию о различных сигналах и слотах, которые вы указали. Вы увидите, как это важно ниже.

Другое важное: все QObject имеют имя. Это имя доступно во время выполнения. Имя, которое вы даете своим виджетам в дизайнере, задается как имя объекта виджета. Причина, по которой это важно, также станет понятным ниже.

И последнее важное: QObject имеет иерархическую структуру; при создании QObject вы можете установить родительский элемент. Все виджеты вашей формы в конечном итоге имеют форму (например, ваш QMainWindow -derived класс) в качестве родителя.

Хорошо, так что...

Вы заметите, что в исходном коде Qt для ваших окон всегда есть вызов ui->setupUi(this) в конструкторе. Реализация этой функции генерируется инструментом Qt uic во время компиляции, например. ui_mainwindow.h. Эта функция позволяет установить все свойства, созданные вами в дизайнере, хранящиеся в файле .ui, включая имена объектов виджетов. Теперь последней строкой сгенерированной реализации setupUI() является ключ:

void setupUi(QMainWindow *MainWindow) {
    ...
    // Object properties, including names, are set here. This is 
    // generated from the .ui file. For example:
    pushButton = new QPushButton(centralWidget);
    pushButton->setObjectName(QString::fromUtf8("pushButton"));
    pushButton->setGeometry(QRect(110, 70, 75, 23));
    ...
    // This is the important part for the signal/slot binding:
    QMetaObject::connectSlotsByName(MainWindow);
}

Эта функция connectSlotsByName - это то, где волшебство происходит для всех этих сигналов виджетов.

В основном эта функция выполняет следующие действия:

  • Итерации через все объявленные имена слотов, которые он может сделать, потому что все это было упаковано в строки, доступные во время выполнения, через moc в информации метаобъекта.
  • Когда он находит слот, имя которого соответствует шаблону on_<objectname>_<signalname>, он рекурсивно ищет иерархию объектов для объекта, имя которого < имя_объектa > , например. один из ваших названных виджетов или любые другие именованные объекты, которые вы, возможно, создали. Затем он соединяет этот объект с сигналом <signalname> с найденным слотом (он в основном автоматизирует вызовы QObject::connect(...)).

И что это.

Итак, что это означает, ничего особенного на самом деле не происходит в дизайнере, когда вы переходите к слоту. Все, что происходит, - это конструктор вставляет в свой заголовок слот с именем on_widgetName_signalName с соответствующими параметрами и генерирует для него пустой элемент функции. (thuga добавил отличное объяснение этого). Этого достаточно для QMetaObject::connectSlotsByName, чтобы найти его во время выполнения и настроить соединение.

Последствия здесь очень удобны:

  • Вы можете удалить обработчик сигнала виджетов, не делая ничего особенного в дизайнере. Вы просто удаляете слот и его определение из исходного файла, все полностью автономно.
  • Вы можете добавлять обработчики сигналов виджетов вручную, не переходя через интерфейс дизайнера, который иногда может сэкономить вам. Все, что вам нужно сделать, это создать слот в вашем окне, например. on_lineEdit1_returnPressed() и вуаля, это работает. Вы просто должны быть осторожны, так как hyde напоминает нам в комментарии: если вы допустили ошибку здесь, вы не получите ошибку времени компиляции, вы получите только предупреждение о времени выполнения, напечатанное в stderr, и соединение не будет быть создан; поэтому важно обратить внимание на вывод консоли, если вы внесете какие-либо изменения здесь.

Другим следствием здесь является то, что эта функциональность фактически не ограничена компонентами пользовательского интерфейса. moc работает для всех ваших QObject s, а connectSlotsByName всегда доступен. Таким образом, любая иерархия объектов, которую вы создаете, если вы называете объекты, затем объявляйте слоты с соответствующими именами, вы можете вызвать connectSlotsByName для автоматической настройки соединений; так получилось, что uic придерживается этого вызова в конце setupUi, потому что это удобное место для его размещения. Но магия не специфична для UI и не полагается на uic, только на метаинформацию. Это общедоступный механизм. Это довольно аккуратно.

В стороне: я приложил быстрый пример QMetaObject::connectSlotsByName в GitHub.

Ответ 2

Что происходит, когда вы добавляете слот из пункта контекстного меню Go to slot..., проходит ли он через ваш проект и ищет файлы, содержащие ui_myclass.h. Когда он находит этот файл, он затем гарантирует, что объект Ui::MyClass объявлен либо в этом файле, либо в файлах, которые включены напрямую. Это будут ваши файлы myclass.cpp и myclass.h. Если он найдет эти файлы, он затем добавит объявление слота и определение в эти файлы.

Вы можете проверить это, удалив объявление Ui::MyClass (обычно называемое ui) из вашего файла myclass.h, а затем добавив слот, используя функцию Go to slot... в конструкторе. Вы должны получить сообщение об ошибке, заявив, что не может найти объявление объекта Ui::MyClass. Вы также можете попробовать удалить включение ui_myclass.h из вашего файла myclass.cpp. Если вы попробуете добавить слот от дизайнера сейчас, вы получите сообщение об ошибке, в котором указано, что он не может найти ui_myclass.h включенного в любом месте или что-то в этом роде.

Если вы определяете все только внутри вашего файла myclass.h и удаляете myclass.cpp, оно должно дать вам сообщение об ошибке, в котором указано, что он не может добавить определение слота.

Вы можете проверить, как это работает более подробно, просмотрев эту часть исходного кода.

Но в конце концов, действительно важно, чтобы вы объявляли и определяли методы класса в отдельных файлах .h и .cpp и включали ui_xxx.h и что в этих файлах был объявлен объект Ui::xxx, Конечно, это то, что Qt Creator делает для вас автоматически, когда вы добавляете новый класс формы, поэтому вам не нужно беспокоиться об этом.