Шаг 229.
Microsoft Visual C++ 2010. Язык С/С++.
Компоненты Windows Forms. Компонент Chart (и еще продолжение)

    На этом шаге мы рассмотрим создание приложения для построения графика введенной пользователем функции.


Содержание этого шага базируется на материале, взятом отсюда: http://blog.kislenko.net/show.php?id=1284.

    Создадим приложение, которое строит графики с интерпретацией введенной пользователем функции.

    Структура основной программы показана на рисунке 1, компоненты в panel1 перечислены по порядку в форме слева направо, что обеспечивает и нормальный порядок обхода полей по клавише табуляции.


Рис.1. Форма в режиме проектирования

    Текстовым полям можно ограничить максимальный размер вводимой строки (свойство MaxLength). Также panel1 расположена со свойством Dock=Top, а chart1 со свойством Dock=Fill. Это обеспечит нормальное взаимодействие компонент при изменении размеров окна. У самой формы выставлены Size и MinimumSize в значение 640; 400 - чтобы не "исчезали" кнопки при уменьшении окна.

    В форму также добавлено глобальное свойство типа NumberFormatInfo:

  public:
     System::Globalization::NumberFormatInfo ^ nfi;
которое проинициализировано в её конструкторе:
Form1(void)
{
	InitializeComponent();
	nfi = gcnew System::Globalization::NumberFormatInfo();
        //"принудительная" точка разделителем целой и дробной части
	nfi->NumberDecimalSeparator = "."; 
	//
	//TODO: добавьте код конструктора
	//
}

    Основная работа выполняется по нажатию на кнопку OK (button1_Click). Сначала проверяем допустимость введённых данных с помощью пары служебных методов Parse (получить число) и Check (проверить правильность записи функции, попробовав получить её значение от 1-го аргумента). Потом метод Go делает цикл по нужным значениям аргумента, формируя диаграмму. Если возникает ошибка парсера, о ней выводится сообщение, но программа не завершается. Просто в данных не будет какой-то пары значений.

    Парсер был взят отсюда: класс parser.cpp от Chaos Master. Вот полный код фрагмента:

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();
 }
Архив проекта можно взять здесь.

    Добавим в проект вторую форму, куда можно будет выводить таблицы данных из диаграммы. Для этого обратимся к меню Проект | Добавить новый элемент | Форма Windows Forms и назовём её Form2. На вторую форму добавим DataGridView, поставим ему свойства Dock=Fill, ScrollBars=Vertical и подготовим 2 столбца для вывода значений X и Y:


Рис.2. Вторая форма в режиме редактирования

    У этой формы будет единственный публичный метод - принять пару значений (x,y) и добавить их в таблицу:

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], nfi);
	 }
	 F2->Show();
 }

    Чтобы это сработало, не забудьте подключить заголовки в начале кода Form1.h:

  #include "parser.h"
  #include "Form2.h"

    Также нужно добавить в приложение сам парсер. Для этого поместите файлы parser.cpp и parser.h в папку с кодом (туда, где находится файл Form1.h). Выполним пункт меню Проект | Существующий элемент... и добавим файл parser.cpp (он добавится. в раздел "Файлы исходного кода" Обозревателя решений).

    Теперь в поле ввода можно писать любые допустимые парсером выражения с переменной x, например, cos(x)+1, текущее значение переменной x из программы подставится в выражение и его результат динамически подсчитается.

    Результат выполнения приложения приведен на рисунке 3.


Рис.3. Результат работы приложения

    На следующем шаге мы закончим изучение этого вопроса.




Предыдущий шаг Содержание Следующий шаг