Visual C++: построение графиков с интерпретацией введённой пользователем функции
В принципе, вся нужная информация есть вот здесь, сделаем по ней законченный проект.
Структура основной формы показана на рисунке, компоненты в panel1
перечислены по порядку в форме слева направо, что обеспечивает и нормальный порядок обхода полей по клавише табуляции.
основная форма приложения
Текстовым полям можно ограничить максимальный размер вводимой строки (свойство MaxLength
). Также panel1
расположена со свойством Dock=Top
, а chart1
со свойством Dock=Fill
. Это обеспечит нормальное взаимодействие компонент при изменении размеров окна. У самой формы выставлены Size
и MinimumSize
в значение 640; 400
- чтобы не "исчезали" кнопки при уменьшении окна.
Текстовым полям также даны значения по умолчанию, дробные части вещественных чисел при этом отделены точкой, а не запятой - будем так делать во всём проекте.
Как альтернатива, можно формировать вещественные значения полей
динамически в зависимости от текущего разделителя (например, по событию Load
формы 1):
double x1=3.14; this->x1->Text = x1.ToString();
В форму также добавлено глобальное свойство типа NumberFormatInfo
public: System::Globalization::NumberFormatInfo ^ nfi;
которое проинициализировано в её конструкторе:
Form1(void) { InitializeComponent(); nfi = gcnew System::Globalization::NumberFormatInfo(); nfi->NumberDecimalSeparator = "."; //"принудительная" точка разделителем целой и дробной части // //TODO: добавьте код конструктора // }
Основная работа выполняется по нажатию на кнопку OK (button1_Click
). Сначала проверяем допустимость введённых данных с помощью пары служебных методов Parse
(получить число) и Check
(проверить правильность записи функции, попробовав получить её значение от 1-го аргумента). Потом метод Go
делает цикл по нужным значениям аргумента, формируя диаграмму. Если возникает ошибка парсера, о ней выводится сообщение, но программа не завершается. Просто в данных не будет какой-то пары значений.
Парсер тот же, что по ссылке выше. Вот полный код фрагмента:
#pragma endregion private: bool Parse (String ^s, double &a) { System::IFormatProvider ^ provider = System::Globalization::CultureInfo::GetCultureInfo("en-US"); bool A = Double::TryParse(s,System::Globalization::NumberStyles::Number, provider,a); return A; } private: bool Check (String ^Str,double x1) { TParser *parser = new TParser(); try { double x=x1; Str=Str->Replace("x",x.ToString(nfi)); char *p = (char*) (Runtime::InteropServices::Marshal::StringToHGlobalAnsi (Str)).ToPointer (); parser->Compile(p); parser->Evaluate(); return true; } catch (...) { return false; } } private: void Go (String ^S,double x1, double x2, double dX) { TParser *parser = new TParser(); using namespace System::Windows::Forms::DataVisualization::Charting; using namespace System::Collections::Generic; using namespace System::Drawing::Drawing2D; using namespace System::Drawing; using namespace Runtime::InteropServices; Dictionary <double, double> f1 = gcnew Dictionary<double, double>(); String ^ Str; double x; char *p; chart1->Series[0]->ChartType = SeriesChartType::Line; chart1->Series[0]->MarkerStyle = MarkerStyle::Circle; ArrayList points = gcnew ArrayList(); try { for (x=x1; x<=x2; x+=dX) { Str=S->Replace("x",x.ToString(nfi)); p = (char*) (Marshal::StringToHGlobalAnsi(Str)).ToPointer(); parser->Compile(p); parser->Evaluate(); f1.Add(x, parser->GetResult()); } } catch(TError error) { System::String ^ str1 = gcnew System::String (error.error); System::String ^ str2 = gcnew System::String (error.pos.ToString()); MessageBox::Show ("Ошибка " + str1 + " в позиции строки разбора " + str2+" для x = "+x, "Ошибка парсера",MessageBoxButtons::OK); //return; } chart1->Series[0]->LegendText = S; chart1->Series[0]->Color = System::Drawing::Color::Green; chart1->Series[0]->BorderWidth = 2; chart1->Series[0]->Points->DataBindXY(f1.Keys, f1.Values); } private: System::Void button1_Click(System::Object^ sender, System::EventArgs^ e) { double x1,x2,dX; String ^ s = textBox1->Text; this->chart1->Visible=false; if (Parse(this->x1->Text,x1)==false || Parse(this->x2->Text,x2)==false || Parse(this->dX->Text,dX)==false) { MessageBox::Show ("Введите числовые значения x1,x2,dx","Ошибка",MessageBoxButtons::OK); return; } if (x1>x2 || x1+dX>x2) { MessageBox::Show ("Введите x1<x1+dX<x2","Ошибка",MessageBoxButtons::OK); return; } this->chart1->Visible=true; if (!Check (s,x1)) { MessageBox::Show ("Введите верную функцию, не могу взять значение от 1-го аргумента", "Ошибка",MessageBoxButtons::OK); return; } Go (s, x1, x2, dX); } private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) { Form2 ^F2 = gcnew Form2; using namespace System::Windows::Forms::DataVisualization::Charting; for each (DataPoint ^p in chart1->Series[0]->Points) { F2->Do (p->XValue, p->YValues[0],nfi); } F2->Show(); }
Единственная новая по отношению к статье мелочь -
Если национальные стандарты предполагают, что дробная часть вещественного числа отделяется от целой запятой, а не точкой, вместо оператора
Str=Str->Replace("x",x.ToString());используйте конструкцию
System::Globalization::NumberFormatInfo ^ nfi = gcnew System::Globalization::NumberFormatInfo(); nfi->NumberDecimalSeparator = "."; //для C++ нужна "принудительная" точка разделителем целой и дробной части //... Str=S->Replace("x",x.ToString(nfi));
Добавим в проект вторую форму, куда можно будет выводить таблицы данных из диаграммы. Для этого обратимся к меню Проект - Добавить новый элемент - Форма Windows Forms и назовём её Form2
. На вторую форму добавим DataGridView
, поставим ему свойства Dock=Fill
, ScrollBars=Vertical
и подготовим 2 столбца для вывода значений X и Y:
вторая форма - вывод таблицы значений функции
У этой формы будет единственный публичный метод - принять пару значений (x,y)
и добавить их в таблицу:
#pragma endregion public: void Do (double x, double y, System::Globalization::NumberFormatInfo ^ nfi) { dataGridView1->Rows->Add(1); int i=dataGridView1->RowCount-1; dataGridView1->Rows[i]->Cells[0]->Value = Math::Round(x,3).ToString(nfi); dataGridView1->Rows[i]->Cells[1]->Value = Math::Round(y,3).ToString(nfi); }
Такой код метода Do
работает при установке свойства
dataGridView1->AllowUserToAddRows = false
так как при значении true
в таблице есть "дополнительная" пустая строка, которая тоже участвует в нумерации.
А вызывать этот метод будет вторая кнопка tab
с первой формы (функция button2_Click
), при этом, сначала создастся новый экземпляр Form2
, чтобы можно было сравнить несколько таблиц:
private: System::Void button2_Click(System::Object^ sender, System::EventArgs^ e) { Form2 ^F2 = gcnew Form2; using namespace System::Windows::Forms::DataVisualization::Charting; for each (DataPoint ^p in chart1->Series[0]->Points) { F2->Do (p->XValue, p->YValues[0]); } F2->Show(); }
Чтобы это сработало, заинклудьте заголовки второй формы в начале кода Form1.h
:
#pragma once #include "parser.h" #include "Form2.h" namespace Lab4 { //...
Разумеется, сам парсер тоже подлючён. Это весь проект, можно собирать. Вот пример работы программы:
пример работы программы
Выражения в парсере пишутся "не совсем на C++", загляните в файл parser.cpp
и увидите это, ещё лучше, можете модифицировать код парсера под свои нужды. Ну и ещё много что можно улучшить, а я выложу проект в текущем "образовательном" состоянии.
Скачать этот проект Visual C++ в архиве .zip (21 Кб)
P.S. Для совместимости с Visual Studio 2015 достаточно сделать вот такой главный файл проекта Lab4.cpp
:
// Lab4.cpp: главный файл проекта. #define _CRT_SECURE_NO_WARNINGS #include "stdafx.h" #include "Form1.h" using namespace Lab4; [STAThreadAttribute] int main(void) { // Включение визуальных эффектов Windows XP до создания каких-либо элементов управления Application::EnableVisualStyles(); Application::SetCompatibleTextRenderingDefault(false); // Создание главного окна и его запуск Application::Run(gcnew Form1()); return 0; }
Самые очевидные улучшения:
- округлять вводимые и вычисляемые значения до некого удобного количества знаков в дробной части;
- ограничить максимальное количество узлов сетки, например, некой константой
maxCollectionSize
. При "слишком большом" размере коллекцииDictionary
приложение может зависнуть, а какой размер "слишком большой", знает только Studio; - найти минимальное и максимальное значения функции, назначив их затем меткам оси Y, выполнить ту же работу и для оси X;
- следить, не получилось ли при расчёте "не-число" Y с помощью
isnan(y) || isinf(y)
; - следить, не добавляются ли повторно в коллекцию элементы с тем же ключом, с помощью
ContainsKey
и т.д.
Вот набросок чуть "улучшенного" проекта для Studio 2015:
Скачать архив .zip с папкой этого проекта Visual Studio 2015 (21 Кб)
P.P.S. Решение едва ли предназначено для консольных приложений из-за не слишком удобных преобразований между строками библиотеки .NET и "классическими" строками std::string
или char *
. Тем не менее, поизвращаться, конечно, можно, скажем, вот такой код главного модуля проекта годится для консольного приложения Visual Studio 2015:
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <cstring> #include "parser.h" using namespace std; char *str_replace (char *orig, char *rep, char *with) { //Замена строки char * на char * char *result; char *ins; char *tmp; int len_rep; int len_with; int len_front; int count; if (!orig || !rep) return NULL; len_rep = strlen(rep); if (len_rep == 0) return NULL; if (!with) with = ""; len_with = strlen(with); ins = orig; for (count = 0; tmp = strstr(ins, rep); ++count) { ins = tmp + len_rep; } tmp = result = new char [strlen(orig) + (len_with - len_rep) * count + 1]; if (!result) return NULL; while (count--) { ins = strstr(orig, rep); len_front = ins - orig; tmp = strncpy(tmp, orig, len_front) + len_front; tmp = strcpy(tmp, with) + len_with; orig += len_front + len_rep; } strcpy (tmp, orig); return result; } void Go (char *p, double x1, double x2, double dX) { TParser *parser = new TParser(); char xstr[80]; try { for (double x = x1; x <= x2; x += dX) { sprintf (xstr,"%.3lf",x); char *p2 = str_replace(p, "x", xstr); parser->Compile(p2); parser->Evaluate(); cout << endl << x << " " << parser->GetResult(); delete p2; } } catch (TError error) { cout << "Error: " << error.error << " in position " << error.pos; return; } delete parser; } int main() { double x1 = 0, x2 = 2 * M_PI, dx = M_PI / 10; //Или ввести откуда-то границы по оси X и шаг char *p = "sin(x)+x/2"; //Или получить строку с выражением f(x) откуда-то Go (p, x1, x2, dx); cin.get(); return 0; }
Как видно из примера, нам пришлось дополнительно написать собственную функцию str_replace
для замены строки char *
на другую строку, чтобы обеспечить циклическую подстановку значений x
в табулируемую функцию f(x)
.
А вот архив с этим проектом Visual Studio 2015, с точностью до платформы (выбирается вверху из списков "Конфигурации решения", "Платформы решения") должно работать везде :) Конечно же, выражение для нужной функции от аргумента "x" малое и нужные пределы изменения аргумента вы можете не только задать константами, но и прочитать откуда-то (с той же консоли или из файла).
скриншот вывода консольного приложения
14.03.2015, 12:47 [45517 просмотров]