БлогNot. QT: виджет "суммирование стека чисел"

QT: виджет "суммирование стека чисел"

Сделаем виджет как в этой заметке, если в Вашем QT появились какие-то дополнительные шаги при создании проекта - оставляем выбор по умолчанию, нажимая кнопку "Далее".

Здесь сознательно не использовано назначение одного обработчика нескольким кнопкам, программное создание массива кнопок и т.п. приёмы из этой статьи, код максимально облегчен.

По нажатию кнопки "+" добавляем в стек то, что определяется как число в поле ввода, по "-" удаляем, по "=" суммируем числа из стека, по "C" очищаем стек.

Также код показывает следующие типовые моменты:

  • логическое создание несложного интерфейса "по строкам" сетки QGridLayout;
  • типовые настройки окна виджета, включая позиционирование по центру основного экрана. Для этого виджету по-прежнему нужно передавать ссылку на экземпляр родительского приложения QApplication (у нас parent), но часто применяемый способ центрирования
    parent->desktop()->availableGeometry()

    согласно документации по новым версиям, устарел, и теперь нужно

    parent->screens().first()->geometry()
  • базовое преобразование строки QString в вещественное число и вещественного числа в строку QString;
  • программное добавление сигналов и слотов, в том числе, обработка двух разных сигналов одним слотом (элемент в стек можно добавлять как кнопкой "+", так и нажатием Enter в поле ввода);
  • поддержка списка строк в QTextEdit без лёгких в написании, но трудоёмких при расчётах операций по преобразованию QTextEdit в QStringList и обратно, как здесь;
  • хотя мы поддерживаем "стекоподобную" функциональность, использовать именно QStack с его методами, автоматически удаляющими элементы при извлечении, незачем, обычный QVector гораздо удобнее;
  • с другой стороны, платой за отсутствие списка служит необходимость удалять "вручную" последнюю строку из QTextEdit, это делает метод removeLastLineFromQTextEdit, которому передаётся указатель на QTextEdit, чтобы метод можно было применять отдельно от класса. В реальности наши данные всё равно хранятся в векторе и текстовое поле служит лишь для иллюстрации. "Вытащить" из QVector stack все строки мы могли и простым
    QList <QString> ql = stack.toList();

    а объединить потом обратно и заменить содержимое QTextEdit stackList столь же лёгким

    QString qs = ql.join('\n');
    stackList->clear();
    stackList->append(qs);

Ниже прикреплены скриншот приложения и архив с проектом.

скриншот приложения в работе
скриншот приложения в работе

 Скачать этот проект QT 5.10+ в архиве .zip, папка уже создана внутри архива (3 Кб)

В качестве дополнения научим виджет реагировать на события Windows (нажатия клавиш и перемещение мыши), для этого добавим прототипы соответствующих методов в класс виджета:

protected:
 virtual void keyReleaseEvent(QKeyEvent *event);
 virtual void mousePressEvent (QMouseEvent *);
 virtual void mouseMoveEvent (QMouseEvent *);

В конструкторе виджета предусмотрим слежение за перемещением мыши (по умолчанию отслеживаются только нажатия кнопок):

setMouseTracking (true);

Обработчик нажатия клавиш будет получать их коды, обрабатывая только те, что дублируют действие кнопок виджета.

void Widget::keyReleaseEvent(QKeyEvent *event) {
 int key=event->key(); //целочисленный код клавиши
 switch (key) {
  case Qt::Key_Plus:
   event->ignore(); //Отменить дальнейшую обработку события
   this->deleteLastChar(displaystring); //Удалить завешающий символ из QLineEdit
   this->b1Clicked(); //Вызвать слот-обработчик соответствующнй кнопки
  break;
  case Qt::Key_Minus:
   event->ignore();
   this->deleteLastChar(displaystring);
   this->b2Clicked();
  break;
  case Qt::Key_Escape:
   event->ignore();
   this->deleteLastChar(displaystring);
   this->b3Clicked();
  break;
  case Qt::Key_Equal:
   event->ignore();
   this->deleteLastChar(displaystring);
   this->b4Clicked();
  break;
  default: //Здесь можно обрабатывать другие коды клавиш
  break;
 }
}

Метод для удаления последнего символа из QLineEdit пришлось написать отдельно, по умолчанию стандартный виджет просто так не "отдаст" своего символа.

void Widget::deleteLastChar (QLineEdit *q) {
 int len = q->text().length();
 q->setCursorPosition(len -1);
 q->setText(q->text().remove(len - 1,1));
}

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

void Widget::mousePressEvent(QMouseEvent *event) { //клик мышью
 //if (event->button()==Qt::LeftButton) { //Какая кнопка мыши нажата
  QPoint position = event->pos(); //Позиция нажатия
  int pos = this->windowTitle().indexOf(':');
  QString title = pos > -1 ? this->windowTitle().remove(pos,this->windowTitle().size() - pos) : this->windowTitle();
  this->setWindowTitle (title + ": " +
   QString::number(position.x())+","+QString::number(position.y()));
 //}
}
void Widget::mouseMoveEvent(QMouseEvent *event) { //перемещение курсора 
 this->mousePressEvent(event);
}

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

 Скачать этот проект QT 5.10+ в архиве .zip, папка уже создана внутри архива (4 Кб)

Ещё одно дополнение уже таким простым не будет.

При запуске предыдущего виджета легко увидеть, что главный виджет "видит" мышь только тогда, когда курсор находится "вне" всех компонент, например, "между кнопками".

Выход 1, простой - сделать все нужные виджеты "прозрачными" для нужного события, например,

stackList->setAttribute(Qt::WA_TransparentForMouseEvents,true);

Это будет работать, только если виджеты являются прямыми дочерними элементами виджета, обрабатывающего событие (в нашем случае - самого объекта класса Widget).

Выход 2, сложнее - пытаться фильтровать события. Например, добавив к списку protected-методов класса

bool eventFilter(QObject *obj, QEvent *ev) override;

фильтр можно назначить нужным элементам

stackList->installEventFilter(this);

и потом реализовать в классе виджета:

bool Widget::eventFilter(QObject *obj, QEvent *event){
 if (obj == stackList) { //Если это нужный объект
  if (event->type() QEvent::MouseMove) { //и это нужный тип события
   QMouseEvent *e = static_cast <QMouseEvent *> (event); //берем указатель на событие
   //что-то делаем, пользуясь нужными методами или свойствами e и obj
   return true; //не передавать событие дальше
  } 
  else {
   return false; //другие события передать дальше
  }
 } 
 else { //События от других объектов передать родительскому классу
  return QWidget::eventFilter(obj, event);
 }
}

Увы, не факт, что это будет работать из-за довольно сложной схемы делегирования событий в QT.

В принципе, в QT существует пять уровней, на которых событие может быть перехвачено и обработано:

1. Обработка событий в функциях-обработчиках. Перехват обработчиков событий, таких как mousePressEvent(), keyPressEvent(), paintEvent() - самый распространенный способ. Мы пользовались им и в этой заметке.

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

3. Установка фильтра событий для QObject. После того, как фильтр будет зарегистрирован функцией installEventFilter(), все события, предназначающиеся указанному объекту, сначала будут попадать в обработчик eventFilter(). Можно перехватывать так события и от других объектов, но результат не гарантирован.

4. Установка фильтра событий для QApplication. После регистрации фильтра, любое событие, предназначенное для любого объекта в приложении, будет сначала попадать в обработчик eventFilter(). Такой подход часто используется при отладке.

5. Создание дочернего класса от QApplication и перекрытие метода notify(). Qt вызывает QApplication::notify(), чтобы передать событие приложению. Так можно перехватить любое событие до того, как оно попадет в фильтр событий. Вообще, фильтры событий более удобны, поскольку допускается одновременное существование любого количества фильтров, а функция notify() может быть только одна.

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

Что касается "неудавшейся" попытки сделать для QTextEdit фильтр eventFilter для события от мыши в сочетании с имеющимися в приложении обработчиками событий мыши, пожалуй, проблему можно решить, только сделав для QTextEdit класс-потомок MouseMoveTextEdit, в конструкторе которого мы установим фильтр

QCoreApplication::instance()->installEventFilter(this);

и потом реализуем его в классе-потомке:

bool MouseMoveTextEdit::eventFilter(QObject *obj, QEvent *event) {
 if (obj == this) { //Если от списка
  if (event->type() == QEvent::MouseMove) { //и это нужный тип события
   QMouseEvent *e = static_cast <QMouseEvent *> (event); //берем указатель на событие
   this->setStyleSheet("MouseMoveTextEdit { background-color: yellow; }");
    //желтый цвет фона
   return true; //не передавать обработку дальше
  }
  else {
   return false; //другие события передать дальше
  }
 }
 else { //Передать событие вверх родительскому классу
  return QWidget::eventFilter(obj, event);
 }
}

Здесь после того, как курсор "побывал" на поле-потомке QTextEdit, его фон становится жёлтым. Однако с учётом всего сказанного это не означает, что пожелтеет немедленно при первом же наведении... подумайте, почему :)

 Скачать эту версию виджета, проект QT 5.10+ в архиве .zip, папка уже создана внутри архива (5 Кб)

P.S. Если виджету нужно "просто следить за мышью", как и сказано выше, не забываем про setMouseTracking.

13.02.2021, 14:11 [325 просмотров]


теги: список c++ программирование qt