БлогNot. Visual C++: построение графиков с интерпретацией введённой пользователем функции

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" малое и нужные пределы изменения аргумента вы можете не только задать константами, но и прочитать откуда-то (с той же консоли или из файла).

 Скачать архив .zip с консольной версией проекта построения графика произвольной функции, Visual Studio 2015 (6 Кб)

скриншот вывода консольного приложения
скриншот вывода консольного приложения

14.03.2015, 12:47 [45517 просмотров]


теги: программирование учебное c++ графика studio c++/cli

показать комментарии (2)