Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

OOP_C++ / 15

.htm
Скачиваний:
22
Добавлен:
02.02.2015
Размер:
30.26 Кб
Скачать

15 - Пример программы графического интерфейса пользователя Содержание     Предыдущее занятие     Следующее занятие

Занятие 15 Пример программы графического интерфейса пользователя 1 Постановка задачи Допустим, необходимо реализовать программу графического интерфейса пользователя, предназначенную для нахождения всех корней уравнения

f(x) = g(x)

на интервале от a к b с заданной точностью ε. Для решения уравнения воспользуемся методом хорд. Функцiї f(x) и g(x) заданы набором точек. Для получения промежуточных значений используются методы интерполяции с помощью полинома Лагранжа и линейной интерполяции соответственно. Необходимо реализовать ввод данных из файлов и генерацию отчета о результатах работы программы в виде файла. Программа должна быть реализована как приложение графического интерфейса пользователя.

2 Создание классов, обеспечивающих решение уравнения Основные сущности предметной области - уравнение и функции. Решение уравнения предусматривает вызов функций f(x) и g(x). Поэтому клас "Решение уравнения методом хорд" (Chords) должен знать об объектах классов "Функция f" (Lagrange) и "Функция g"(Linear). Поскольку необходимо обеспечить многоцелевое применение классов в разных проектах, следует избегать прямых связей между конкретными классами. Более целесообразно создать абстрактный класс (AFunction) и воспользоваться возможностями полиморфизма.

Для создания конкретных классов можно воспользоваться любой средой программирования, в частности, Microsoft Visual Studio .NET. Создаем абстрактный класс AFunction в заголовочном файле AFunction.h. Все функции-элементы данного класса - виртуальные. В том числе функция y() - чисто виртуальная. Функция describe() может пригодиться при генерации отчета. Кроме того, могут понадобиться классы-исключения BadFunction, FileMissing и WrongFormat. В нашем случае текст заголовочного файла будет иметь такой вид:

#ifndef AFunction_h #define AFunction_h #include <iostream> class BadFunction {}; class FileMissing {}; class WrongFormat {}; class AFunction { public: virtual double y(double x) const = 0; virtual ~AFunction() { } virtual void describe(std::ostream& out) { } }; #endif Далее целесообразно создать производные классы, которые реализуют конкретные функции. В классе Lagrange перегружаем функцию y(). Значения координат точек можно разместить в двух векторах. Заголовочный файл будет иметь вид:

#ifndef Lagrange_h #define Lagrange_h #include <vector> #include "AFunction.h" class Lagrange : public AFunction { std::vector<double> vx, vy; public: void clear() { vx.clear(); vy.clear(); } void addPoint(double x, double y) { vx.push_back(x); vy.push_back(y); } int pointsCount() const { return (int)vx.size(); } double getX(int i) const { return vx.at(i); } double getY(int i) const { return vx.at(i); } virtual double y(double x) const; void readFromFile(const char* fileName); virtual void describe(std::ostream& out); }; #endif В файле Lagrange.cpp осуществляем реализацию оставшихся функций-элементов:

#include <fstream> #include "Lagrange.h" double Lagrange::y(double x) const { double result = 0; for (int i = 0; i < pointsCount(); i++) { double L = 1; for (int j = 0; j < pointsCount(); j++) if (i != j) { if (vx[i] == vx[j]) throw BadFunction(); L *= (x - vx[j]) / (vx[i] - vx[j]); } result += L * vy[i]; } return result; } void Lagrange::readFromFile(const char* fileName) { std::ifstream in(fileName); if (in == 0) throw FileMissing(); clear(); double x, y; while (in >> x >> y) addPoint(x, y); if (pointsCount() <= 1) throw WrongFormat(); } void Lagrange::describe(std::ostream& out) { out << "полином Лагранжа, проведенный через следующие точки: " << "\n"; for (unsigned int i = 0; i < pointsCount(); i++) out << getX(i) << " " << getY(i) << "\n"; } Следующий класс, который следует создать - это класс Linear. Он также является производным от AFunction. Соответствующий заголовочний файл очень похож на предшествующий. Поскольку линейная интерполяция требует сортировки точек по возрастанию x, точки целесообразно хранить в структуре, для которой определена операция <. Теперь для сортировки точек удобно будет воспользоваться алгоритмом sort() стандартной библиотеки.

Заголовочный файл будет иметь вид:

#ifndef Linear_h #define Linear_h #include <vector> #include "AFunction.h" struct Point { double x, y; Point(double x, double y) { this->x = x; this->y = y; } bool operator<(const Point& p) const { return x < p.x; } }; class Linear : public AFunction { std::vector<Point> vp; public: void clear() { vp.clear(); } void addPoint(double x, double y); int pointsCount() const { return (int)vp.size(); } double getX(int i) const { return vp.at(i).x; } double getY(int i) const { return vp.at(i).y; } virtual double y(double x) const; void readFromFile(const char* fileName); virtual void describe(std::ostream& out); }; #endif Файл реализации:

#include <algorithm> #include <fstream> #include <functional> #include "Linear.h" void Linear::addPoint(double x, double y) { Point p(x, y); vp.push_back(p); } double Linear::y(double x) const { std::vector<Point> v = vp; std::sort(v.begin(), v.end()); if (x < v[0].x || x > v[v.size() - 1].x) throw BadFunction(); int i; for (i = 1; i < pointsCount(); i++) if (v[i].x > x) break; if (v[i].x == v[i - 1].x) throw BadFunction(); return v[i - 1].y + (v[i].y - v[i - 1].y) * (x - v[i - 1].x) / (v[i].x - v[i - 1].x); } void Linear::readFromFile(const char* fileName) { std::ifstream in(fileName); if (in == 0) throw FileMissing(); clear(); double x, y; while (in >> x >> y) addPoint(x, y); if (pointsCount() == 0) throw WrongFormat(); } void Linear::describe(std::ostream& out) { out << "линейная интерполяция функции, проведенной через следующие точки: " << "\n"; for (int i = 0; i < pointsCount(); i++) out << getX(i) << " " << getY(i) << "\n"; } После подготовки обеих функций можно создавать класс Chords (уравнение). Он будет содержать указатели на два объекта класса AFunction, а также вектор корней в качестве элементов данных. Вспомогательные данные - a и b сохраняют начало и конец интервала, на котором осуществлялся поиск корней. Поскольку уравнение без функций f(x) и g(x) не имеет смысла, соответствующие указатели следует устанавливать в конструкторе класса. Функции clearRoots(), getRoot() и rootsCount() обеспечивают доступ к вектору корней. Функция phi() применяется во время решения уравнения вместо разности  f(x) - g(x). Функциональность класса обеспечивается методом solve(). Эта функция требует определение начала и конца интервала, а также необходимой точности. Функция findRoot(), которая находит один корень на интервале - вспомогательная, поэтому она объявлена как закрытая. Функция generateReport() позволяет сохранить отчет о работе программы в виде текстового файла. Заголовочный файл будет иметь вид:

#ifndef Chords_h #define Chords_h #include <vector> #include "AFunction.h" class EquationError {}; class ReportError {}; class Chords { AFunction *pf, *pg; std::vector<double> roots; double findRoot(double x1, double x2, double eps) const; double a, b; public: Chords(AFunction *pf, AFunction *pg); void clearRoots() { roots.clear(); } double getRoot(int i) const { return roots.at(i); } int rootsCount() const { return (int)roots.size(); } double phi(double x) const { return pf->y(x) - pg->y(x); } void solve(double a, double b, double eps); void generateReport(const char* fileName) const; }; #endif Файл реализации:

#include <fstream> #include "Chords.h" Chords::Chords(AFunction *pf, AFunction *pg) { this->pf = pf; this->pg = pg; } void Chords::solve(double a, double b, double eps) { if (a >= b || eps <= 0) throw EquationError(); this->a = a; this->b = b; double h = (b - a) / 1000; clearRoots(); for (double x = a; x < b; x += h) if (phi(x) * phi(x + h) <= 0) roots.push_back(findRoot(x, x + h, eps)); } double Chords::findRoot(double x1, double x2, double eps) const { double x; do { if ((phi(x1) - phi(x2)) == 0) throw EquationError(); x = x1 - phi(x1) * (x1 - x2) / (phi(x1) - phi(x2)); if(phi(x) * phi(x1) < 0) x2 = x; else x1 = x; } while (phi(x) > eps || phi(x) <= -eps); return x; } void Chords::generateReport(const char* fileName) const { std::ofstream out(fileName); if (out) { out << "В результате решения уравнения\n"; out << " f(x) = g(x)\n"; out << "на интервале от " << a << " до " << b << "\n"; out << "где функция f(x) - "; pf->describe(out); out << "функция g(x) - "; pg->describe(out); if (rootsCount() > 0) { out << "получены следующие корни:\n"; for (int i = 0; i < rootsCount(); i++) out << getRoot(i) << "\n"; } else out << "установлено, что уравнение не имеет корней.\n"; } else throw ReportError(); } Можно создать небольшую консольную программу для тестирования классов. Данные о точках можно прочитать из следующих файлов. Файл f1.txt:

0 2 2 0 3 4 Файл g1.txt:

0 1 1 2 3 3 2 8 3 Проектирование графического интерфейса пользователя Главное окно программы должно содержать кнопки, которые позволяют загрузить данные для функций F и G соответственно, таблицы, в которые загружаются данные, строки для ввода начала и конца интервала, а также необходимой точности, кнопку нахождения корней, подокно для вывода списка корней, а также кнопки генерации отчета и выхода.

На следующем рисунке приведен возможный вариант интерфейса пользователя:

Рисунок 2.1 - Интерфейс пользователя

4 Реализация программы графического интерфейса пользователя Для реализации графического интерфейса пользователя к проекту необходимо добавить класс, производный от QWidget. QWidget - базовый класс для всех визуальных элементов библиотеки Qt. Новый класс QMainWindow должен содержать в своем описании макрос Q_OBJECT. Это означает, что необходимо выполнить обработку исходного текста данного класса специальной программой, именуемой "Компилятор макрообъектов" (MOC). Использование этой программы позволяет добавить в класс нестандартные ключевые слова, такие как signals (источники событий) и slots (обработчики событий). Наш класс будет содержать четыре таких обработчика. Каждому управляющему элементу соответствует указатель на объект определенного класса. Для использования этих классов необходимо подключить соответствующие заголовочные файлы. Кроме того, в качестве элементов данных включены объекты классов Lagrange, Linear и Chords. Заголовочный файл QMainWindow.h будет иметь следующий вид:

#ifndef QMainWindow_H #define QMainWindow_H #include <QWidget> #include <QGridLayout> #include <QPushButton> #include <QTableWidget> #include <QLabel> #include <QLineEdit> #include <QTextEdit> #include "AFunction.h" #include "Lagrange.h" #include "Linear.h" #include "Chords.h" class QMainWindow : public QWidget { Q_OBJECT public: QMainWindow(QWidget *parent = 0); private slots: void openF(); void openG(); void solve(); void generateReport(); private: QString fFileName, gFileName, reportFileName; QPushButton *openFButton, *openGButton, *quitButton, *runButton, *reportButton; QTableWidget *fTable, *gTable; QLabel *aLabel, *bLabel, *epsLabel, *resultsLabel; QLineEdit *aLine, *bLine, *epsLine; QTextEdit *resultsText; Lagrange f; Linear g; Chords e; }; #endif В файле реализации подключаются дополнительные заголовочные файлы, создается класс-исключение ConversionError и определяется функция ru(). Функция ru() предназначена для получения сторки типа QString из массива символов с учетом принятой кодовой таблицы. Поскольку сторки типа QString сохраняют символы Unicode, для занесения в них цепочек однобайтовых символов, так как иначе будут сохранены символы в кодировке Western European. Статическая функция codecForLocale() класса QTextCodec позволяет получить объект, описывающий кодировку, принятую для операционной системы на конкретном компьютере.

В списке инициализации конструктора класса QMainWindow инициализируется базовый класс (QWidget) и объект класса Chords. Далее в конструкторе определяется заголовок окна, создаются и настраиваются элементы управления. В частности, для объектов типа QTableWidget (таблица) указывается количество строк и столбцов, определяется шапка с использованием вспомогательных объектов типа QTableWidgetItem (ячейка таблицы). Компоненты типа QLabel (метка) определяют статический текст в окне. Окно вывода корней resultsText типа QTextEdit делается нередактируемым с помощью функции setReadOnly(). Для окна задаются начальные размеры с помощью функции setGeometry().

В конструкторе также осуществляется связывание событий с обработчиками. Для этого вызывается статическая функция connect() класса QObject. Четыре обработчика реализованы в данном классе, один - стандартный.

Размещение компонентов осуществляется с помощью объекта класса QGridLayout. Данный объект позволяет покрыть окно невидимой сеткой. Ячейки, задаваемые этой сеткой, могут иметь различные размеры. Эти размеры определяются размерами элементов, вставляемых в ячейки. Второй и третий параметры функции addWidget() задают соответствующие координаты. При необходимости элементы управления могут занимать более одной ячейки. Тогда четвертым и пятым параметром можно задать количество таких ячеек по вертикали и по горизонтали.

В функции openF() осуществляется вызов функции getOpenFileName() класса QFileDialog. Изображается стандартное диалоговое окно открытия файлов. Результат функции - имя выбранного файла или пустая строка. С помощью функции toStdString() строку типа QString можно перевести в std::string, а из этой строки получить указатель на char с помощью функции c_str(). В случае успешного открытия файла прочитанные значения записываются в соответствующие ячейки таблицы. Функция QString::number() переводит числа в их строковое представление. Если файл не был открыт, соответствующее сообщение выводится в специальном окне предупреждения (функция QMessageBox::warning()), а таблица приводится в исходное состояние. Функция openG() реализована аналогично.

В теле функции solve() осуществляется чтение содержимого строк ввода и перевод данных в числовое представление. Переменная ok используется для анализа успешности перевода. В случае неудачи генерируется исключение. Далее осуществляется поиск корней, очистка подокна редактирования resultsText и добавление корней с помощью функции append() с переводом в строковое представление. В случае отсутствия корней выдается соответствующее сообщение.

В функции generateReport() осуществляется вызов диалогового окна сохранения файла и создается отчет в файле с выбранным именем с выдачей соответствующего сообщения.

Файл реализации будет иметь вид:

#include <QMessageBox> #include <QTextCodec> #include <QApplication> #include <QTableWidgetItem> #include <QFileDialog> #include "QMainWindow.h" class ConversionError { }; QString ru(char *s) { return QTextCodec::codecForLocale()->toUnicode(s); } QMainWindow::QMainWindow(QWidget *parent) : QWidget(parent), e(&f, &g) { // Определение заголовка: setWindowTitle(ru("Уравнение")); // Создание элементов управления: openFButton = new QPushButton(ru("Загрузить данные\nдля функции F")); openGButton = new QPushButton(ru("Загрузить данные\nдля функции G")); fTable = new QTableWidget(1, 2); fTable->setHorizontalHeaderItem(0, new QTableWidgetItem("x")); fTable->setHorizontalHeaderItem(1, new QTableWidgetItem("y")); fTable->setMinimumWidth(240); gTable = new QTableWidget(1, 2); gTable->setHorizontalHeaderItem(0, new QTableWidgetItem("x")); gTable->setHorizontalHeaderItem(1, new QTableWidgetItem("y")); gTable->setMinimumWidth(240); aLabel = new QLabel("a"); bLabel = new QLabel("b"); epsLabel = new QLabel(ru("точность")); aLine = new QLineEdit("-1"); bLine = new QLineEdit("1"); epsLine = new QLineEdit("0.0001"); resultsLabel = new QLabel(ru("Корни:")); resultsText = new QTextEdit(); resultsText->setReadOnly(true); resultsText->setGeometry(0, 0, 340, 50); runButton = new QPushButton(ru("Найти корни")); reportButton = new QPushButton(ru("Сгенерировать отчет")); quitButton = new QPushButton(ru("Выход")); // Связывание событий с обработчиками: QObject::connect(openFButton, SIGNAL(clicked()), this, SLOT(openF())); QObject::connect(openGButton, SIGNAL(clicked()), this, SLOT(openG())); QObject::connect(quitButton, SIGNAL(clicked()), qApp, SLOT(quit())); QObject::connect(runButton, SIGNAL(clicked()), this, SLOT(solve())); QObject::connect(reportButton, SIGNAL(clicked()), this, SLOT(generateReport())); // Размещение компонентов: QGridLayout *layout = new QGridLayout; layout->addWidget(openFButton, 0, 0, 1, 2); layout->addWidget(openGButton, 0, 2, 1, 2); layout->addWidget(fTable, 1, 0, 1, 2); layout->addWidget(gTable, 1, 2, 1, 2); layout->addWidget(aLabel, 2, 0); layout->addWidget(aLine, 2, 1); layout->addWidget(bLabel, 3, 0); layout->addWidget(bLine, 3, 1); layout->addWidget(epsLabel, 4, 0); layout->addWidget(epsLine, 4, 1); layout->addWidget(runButton, 5, 0, 1, 2); layout->addWidget(resultsLabel, 2, 2, 1, 2); layout->addWidget(resultsText, 3, 2, 2, 2); layout->addWidget(reportButton, 5, 2, 1, 2); layout->addWidget(quitButton, 6, 0, 1, 4); setLayout(layout); } void QMainWindow::openF() { fFileName = QFileDialog::getOpenFileName(this); try { f.readFromFile(fFileName.toStdString().c_str()); fTable->setRowCount(f.pointsCount()); for (int i = 0; i < f.pointsCount(); i++) { QTableWidgetItem *xItem = new QTableWidgetItem(QString::number(f.getX(i))); fTable->setItem(i, 0, xItem); QTableWidgetItem *yItem = new QTableWidgetItem(QString::number(f.getY(i))); fTable->setItem(i, 1, yItem); } } catch (...) { QMessageBox::warning(this, ru("Файл не открыт"), ru("Файл ") + fFileName + ru(" не открыт!")); fTable->setRowCount(1); fTable->setItem(0, 0, new QTableWidgetItem("")); fTable->setItem(0, 1, new QTableWidgetItem("")); } } void QMainWindow::openG() { gFileName = QFileDialog::getOpenFileName(this); try { g.readFromFile(gFileName.toStdString().c_str()); gTable->setRowCount(g.pointsCount()); for (int i = 0; i < g.pointsCount(); i++) { QTableWidgetItem *xItem = new QTableWidgetItem(QString::number(g.getX(i))); gTable->setItem(i, 0, xItem); QTableWidgetItem *yItem = new QTableWidgetItem(QString::number(g.getY(i))); gTable->setItem(i, 1, yItem); } } catch (...) { QMessageBox::warning(this, ru("Файл не открыт"), ru("Файл ") + gFileName + ru(" не открыт!")); gTable->setRowCount(1); gTable->setItem(0, 0, new QTableWidgetItem("")); gTable->setItem(0, 1, new QTableWidgetItem("")); } } void QMainWindow::solve() { try { bool ok = true; double a = aLine->text().toDouble(&ok); if (!ok) throw ConversionError(); double b = bLine->text().toDouble(&ok); if (!ok) throw ConversionError(); double eps = epsLine->text().toDouble(&ok); if (!ok) throw ConversionError(); e.solve(a, b, eps); resultsText->clear(); if (e.rootsCount() > 0) for (int i = 0; i < e.rootsCount(); i++) resultsText->append(QString::number(e.getRoot(i))); else QMessageBox::information(this, ru("Нет корней"), ru("Нет корней!")); } catch (...) { QMessageBox::warning(this, ru("Неправильные данные"), ru("Неправильные данные!")); } } void QMainWindow::generateReport() { reportFileName = QFileDialog::getSaveFileName(this); try { e.generateReport(reportFileName.toStdString().c_str()); QMessageBox::information(this, ru("Отчет сгенерирован"), ru("Отчет сгенерирован!")); } catch (...) { QMessageBox::warning(this, ru("Отчет не сгенерирован"), ru("Отчет не сгенерирован!")); } } Основная программа будет иметь вид:

#include <QApplication> #include "QMainWindow.h" int main(int argc, char* argv[]) { QApplication app(argc, argv); QMainWindow mainWindow; mainWindow.show(); return app.exec(); } Создаем новую папку, далее помещаем в нее все исходные тексты (заголовочные файлы и файлы реализации). Файл с функцией main() должен быть ровно один. Создаем проект с помощью команд qmake -project и qmake и компилируем с помощью mingw32-make. В папке Debug находим и запускаем скомпилированную программу.

 

Содержание     Предыдущее занятие     Следующее занятие

 

© 2001 - 2006 Иванов Л.В.

Соседние файлы в папке OOP_C++