Hard-Soft News

Новости железа и софта

События Qt

Взаимодействие объектов Qt между собой и с внешним окружением происходит немного по-разному. Информацию о том, что происходит во внешнем окружении, программа получает, в основном в виде событий. Обработчики событий – это обычные методы классов Qt, которым в качестве аргумента передается указатель на объект, описывающий событие. Эти объекты являются потомками объекта QEvent.

Методы-обработчики событий располагаются в разделах protected. Это означает, что вам придется напрямую иметь дело с событиями в тех случаях, когда вы создаете классы-потомки стандартных классов Qt.

Покажем это на простом, но полезном примере (одна из причин, по которой я в свое время перешел на Qt, заключалась именно в том, что в Qt можно очень просто сделать некоторые действительно полезные вещи). Мы модифицируем компонент QLineEdit таким образом, чтобы в нем работало автоматическое завершение строки в стиле оболочки bash (для тех, кто не знаком с bash поясню: вы набираете начало строки, нажимаете клавишу табуляции, и bash завершает строку, как умеет). Впрочем, последние выпуски консоли Windows тоже научились чему-то подобному.

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

В окне Qt Designer поместим компонент QLineEdit, а затем, с помощью команды контекстного меню Преобразовать в… создадим его потомка класс AutoCompleteEdit.

Теперь нам требуется объявление и реализация класса AutoCompleteEdit. Я совместил их в файле autocompleteedit.h.

class AutoCompleteEdit: public QLineEdit
{
    Q_OBJECT
public:
    explicit AutoCompleteEdit(QWidget* parent=0) : QLineEdit(parent)
    {
        autoCompleteList = new QStringList();
    }
    ~AutoCompleteEdit()
    {
        delete autoCompleteList;
    }
    void setAutoCompleteList(const QStringList &list)
    {
        (*autoCompleteList) = list;
    }
protected:
    void keyPressEvent(QKeyEvent * event)
    {
        if (event->key() == Qt::Key_Tab) {
	    for (int i =  0; i < autoCompleteList->count(); i++)
   if (autoCompleteList->at(i).startsWith(text())) {
       setText(autoCompleteList->at(i));
       break;
   }
            event->accept();
            return;
        }
        QLineEdit::keyPressEvent(event);
    }
private:
        QStringList * autoCompleteList;
};

С точки зрения программиста, которому захочется использовать класс AutoCompleteEdit в своей программе, этот класс отличается от QLineEdit только наличием метода setAutoCompleteList(). Аргументом этого метода является ссылка на объект QStringList, который содержит список строк для автоматического завершения. Внутри же нам, помимо реализации конструктора, деструктора и метода setAutoCompleteList(), потребуется перекрыть виртуальный метод keyPressEvent(), который обрабатывает события, вызванные нажатием клавиш, когда наш компонент обладает фокусом ввода.

Теперь посмотрим на гораздо более интересный метод keyPressEvent(). Информация о событии передается обработчику в параметре event, который указывает на экземпляр класса QKeyEvent. Этот объект содержит всю информацию о событии, связанном с нажатием клавиши на клавиатуре. В частности, метод event->key() содержит код нажатой клавиши. Мы проверяем, не нажата ли клавиша табуляции. Если нажата именно она, мы ищем в списке autoCompleteList строки, которые начинаются с текста, введенного в компоненте (получить этот текст можно с помощью метода text()). Если такая строка найдена, мы присваиваем ее компоненту (метод setText()) и выходим из цикла.

Вызов метода event->accept(); означает, что мы обработали событие (иначе оно может быть передано родительскому виджету, а нам это не требуется). Если нажата какая-то другая клавиша, мы вызываем метод keyPressEvent() базового класса QLineEdit, передавая ему, по эстафете, переменную event. В результате  класс AutoCompleteEdit будет вести себя точно так же как QLineEdit, за исключением обработки клавиши табуляции.

Клавиша табуляции обычно используется для переключения фокуса ввода. Как наш виджет разрешает этот конфликт? Благодаря нашему методу обработки события и особенностям Qt library, конфликт разрешается вполне разумно: если наш виджет единственный потенциальный акцептор фокуса ввода в окне и переключаться с помощью клавиши табуляции просто некуда, то клавиша табуляции будет работать так, как описано выше. Если кроме нашего виджета в коне есть и другие, которые могут получить фокус ввода, то простое нажатие клавиши табуляции приведет к переключению фокуса ввода (этим управляет другой механизм, а наш виджет в этом случае просто не получит информацию о нажатии клавиши). Но выполнять автоматическое завершение в строке ввода все равно можно, только для этого клавишу табуляции придется применять с модификатором (Ctrl или Alt, не важно).

Существует и другой способ разрешения конфликта. Допустим, что кроме строки ввода окно содержит еще один виджет-акцептор ввода – кнопку QPushButton (в примере на сайте это так и есть). В этом случае мы можем в редакторе свойств объекта класса QPushButton изменить значение свойства focusPolicy со StrongFocus на ClickFocus. Теперь кнопка сможет получать фокус ввода только при щелчке мышью, но не при нажатии клавиши табуляции. В результате строка ввода снова останется единственным «переключаемым» элементом, и снова сможет обрабатывать нажатие клавиши табуляции так, как ей хочется.

Еще один способ разрешения конфликта, правда, более трудоемкий, предлагает документация по Qt. Мы бегло рассмотрим этот способ (подробное описание вы найдете в документации), главным образом, потому, что он позволяет понять, как работает механизм событий Qt на низком уровне. Выше я написал о том, что «система» скрывает от нашего виджета нажатие клавиши табуляции, если это нажатие может быть использовано для переключения фокуса ввода. Сослаться на систему, конечно, удобно, но лучше разобраться, как она работает. У каждого объекта-потомка QObject есть метод-диспетчер событий. Этот метод называется просто event(). Его единственным аргументом является указатель на объект класса QEvent (или на объект любого потомка этого класса). Иначе говоря, функции event() можно передать указатель на объект, соответствующий любому событию Qt. Метод-диспетчер изучает тип переданного ему объекта, несущего информацию о событии и решает, какая именно функция-обработчик должна быть вызвана для обработки данного события. Если функция event() смогла отправить событие «по назначению» и событие было корректно обработано, функция возвращает значение true. В противном случае – естественно – false. Рассмотрим возможный вариант функции event() для нашего примера.

bool event(QEvent *event)
{
    if (event->type() == QEvent::KeyPress) {
        QKeyEvent *ke = static_cast(event);
        if (ke->key() == Qt::Key_Tab) {
        for (int i =  0; i < autoCompleteList->count(); i++)
            if (autoCompleteList->at(i).startsWith(text())) {
                setText(autoCompleteList->at(i));
                break;
            }
            return true;
        }
    }
    return QLineEdit::event(event);
}

У каждого объекта-потомка QEvent есть метод type(), который возвращает идентификатор типа события. Мы вызываем этот метод для того чтобы определить, имеем ли мы дело с событием QKeyEvent или с каким-то другим. Если это событие QKeyEvent и была нажата клавиша табуляции, мы выполняем уже знакомую вам последовательность действий, после чего возвращаем true (событие распознано и обработано). Если одно из перечисленных условий не выполняется, мы просто передаем параметр event методу event() базового класса. Теперь клавиша табуляции в строке ввода будет делать то, что нужно нам, независимо от внешних условий.

Думаю, вы понимаете, какую мощь дает нам в руки перекрытие функции event(). Мы можем блокировать события или заменять одно событие другим по своему усмотрению. Единственный недостаток такого подхода заключается в том, что для перекрытия стандартного метода event() того или иного виджета нам придется создавать класс-потомок этого виджета. Далее мы узнаем, как манипулировать событиями классов извне, не создавая их потомков.

Еще одна возможность, которую дает нам знание о функции event() – это возможность эмулировать события. Например, если мы хотим программно эмулировать щелчок мышью по виджету, нам требуется создать объект класса QMouseEvent и передать указатель на этот объект функции event() соответствующего виджета. Дальше все будет выглядеть так, как будто мы действительно выполнили некоторое действие с помощью мыши. После возвращения из метода event() объект-событие может быть уничтожен. Обратите внимание на то, что ни метод event(), ни методы-обработчики событий никогда не уничтожают объект, указатель на который им передан. Вы в своих реализациях методов event() и обработчиков событий тоже ни в коем случае не должны этого делать. В этом случае действует известное «правило хорошего тона» при программировании на C++: объект уничтожается на том уровне вложенности, на котором он был создан (или, по-другому: «не ты создавал, – не ты уничтожаешь»).

Раз уж зашла речь о вызове event() для эмуляции событий, то стоит отметить, что это не единственная возможность эмуляции. К методу event() конкретного объекта мы обращаемся в том случае, если мы хотим, чтобы именно этот объект обработал именно это событие. Но такая эмуляция не всегда будет выглядеть правдоподобно. Некоторые события обрабатываются цепочками дочерних объектов. Очень часто, если объект не может сам обработать событие, он передает его родительскому объекту. Для того чтобы эмулировать стандартный процесс обработки события приложением, необходимо воспользоваться методом QCoreApplication::notify(). Напомню, что класс QCoreApplication является предком класса QApplication, с которым мы уже знакомы. Метод notify() этого  класса имеет два параметра. В параметре receiver передается указатель на объект, который должен обработать событие. В параметре event передается указатель на объект QEvent или его потомка. Метод возвращает значение типа bool, которое имеет тот же смысл, что и значение, возвращаемое методом event(). В простейшем случае метод notify() делает вызов

return receiver->event(event);

В более сложных случаях может произойти передача события дочернему или родительскому объектам.

Нам осталось посмотреть на наш виджет в работе. Все, что нам требуется – инициализировать его списком строк для автоматического завершения.

Dialog::Dialog(QWidget *parent) : QDialog(parent), ui(new Ui::Dialog)
{
    ui->setupUi(this);
    QStringList sl;
    sl << "FreeBSD" << "Linux" << "MacOS X" << "QNX" << "Symbian" << "Windows";
    ui->lineEdit->setAutoCompleteList(sl);
}

Мы заполняем список авто-завершения именами операционных систем, которые поддерживает Qt library. Так что, если вы наберете в строке ввода «Wi», а затем нажмете клавишу табуляции (возможно – с модификатором), в строке ввода появится имя нашей самой любимой операционной системы.

Еще один случай, когда вам может потребоваться иметь дело с событиями – реализация механизма Drag and Drop (перетащить и бросить) в окне, которое по умолчанию для этого не предназначено. Для демонстрации того, как это работает, мы модифицируем программу designerdemo2. Этот модифицированный вариант вы найдете под именем dragndropdemo. Суть нашей модификации заключается в том, чтобы открывать в программе графические файлы, просто перетаскивая их в окно программы мышью. Для того чтобы добавить такую полезную функциональность в нашу программу, нам потребуется внести совсем немного изменений. А именно, перекрыть в классе MainWindow функции dragEnterEvent() и dropEvent(). Сначала в заголовочном файле.

class QDragEnterEvent;
class QDropEvent;
class MainWindow : public QMainWindow
{
    Q_OBJECT
 private:
    Ui::MainWindow * ui;
public:
    explicit MainWindow(QWidget *parent = 0);
protected:
    void dragEnterEvent(QDragEnterEvent *event);
    void dropEvent(QDropEvent *event);
signals:
private slots:
    void openFile();
};

А затем и в файле реализации

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent)
{
    ui = new Ui::MainWindow();
    ui->setupUi(this);
    connect(ui->actionOpen_File, SIGNAL(triggered()), this, SLOT(openFile()));
    setAcceptDrops(true);
}
void MainWindow::openFile()
{
    QPixmap pm;
    QString fn = QFileDialog::getOpenFileName(this, trUtf8("Open image"));
    if (fn != "") {
        if (!pm.load(fn))
            QMessageBox::critical(this, trUtf8("Error"),trUtf8("Unable to load image from the selected file."));
        else {
            ui->label->setPixmap(pm);
            setWindowTitle(trUtf8("Image Viewer - ") + fn);
        }
    }
}
void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
    if (event->mimeData()->hasUrls())
        event->accept();
}
void MainWindow::dropEvent(QDropEvent *event)
{
    if (event->mimeData()->hasUrls()) {
        QPixmap pm;
        QString fn = event->mimeData()->urls().at(0).toLocalFile();
        if (pm.load(fn)) {
            ui->label->setPixmap(pm);
            setWindowTitle(trUtf8("Image Viewer - ") + fn);
        }
        event->accept();
    }
}

В конструкторе MainWindow мы вызываем метод setAcceptDrops() с параметром true. Тем самым мы объявляем системе, что окно MainWindow может служить приемником для перетаскиваемых мышью объектов. Событие QDragEnterEvent оповещает нас о том, что курсор мыши вошел в область виджета (в нашем примере – главного окна) в режиме перетаскивания. При этом нам, прежде всего, необходимо определить, что именно «тащит» мышь. У объектов QDragEnterEvent и QDropEvent есть метод mimeData(), который возвращает указатель на объект QMimeData. Этот объект позволяет определить формат перетаскиваемых данных и получить доступ к самим данным.

Наша программа принимает ссылки на файлы. Все подобные ссылки, независимо от их источника, преобразуются системой Qt Drag and Drop в универсальные ссылки URL. Метод hasUrls() объекта QMimeData позволяет нам определить, содержат ли перетаскиваемые данные ссылки URL. Если перетаскиваемые содержат ссылки URL, мы вызываем метод accept() объекта event. В этом примере метод accept() играет более важную роль, чем в предыдущем. Он не только сообщает Qt, что мы готовы обработать событие, но и сообщает системе, что наша программа готова принять перетаскиваемый объект.

Если метод accept() вызван, курсор мыши принимает соответствующий вид («сюда можно бросать перетаскиваемый объект»), в противном случае курсор примет вид запрещающего знака.

Метод dropEvent() вызывается в том случае, если пользователь отпустил кнопку мыши в режиме перетаскивания объектов над нашим окном, и мы готовы принять соответствующий объект. В этом методе мы снова проверяем, содержит ли перетаскиваемый объект ссылки URL. В принципе, в этом нет необходимости, так как если мы не вызвали метод accept() в обработчике dragEnterEvent(), то обработчик dropEvent() все равно не будет вызван, но лишняя проверка никогда не мешает. Метод event->mimeData()->urls() возвращает список перетаскиваемых ссылок (QList<QUrl>). Из этого списка мы берем самую первую ссылку (все равно наша программа может показать за раз только одно изображение) и с помощью метода toLocalFile() преобразуем ее в ссылку на файл на диске. Дальнейшие действия должны быть вам уже знакомы.

Почему мы занимаемся обработкой событий Drag and Drop в классе MainWindow, а не в классе QLabel, и как MainWindow «узнает» об этих событиях, если все его активное пространство закрыто объектом label? Мы, конечно, могли бы создать потомка класса QLabel и организовать обработку событий Drag and Drop в этом классе, но это только усложнило бы наш пример без всякой на то необходимости. Дело в том, что встречая событие, которое он не может обработать, например, событие Drag and Drop, объект QLabel просто передаст его родительскому виджету, то есть, классу MainWindow.

Исходные тексты примеров


© 2011 Андрей Боровский

 

Комментировать