Уроки по Delphi

       

Корректное построение программного кода. Рисование. Построение графика функции


Бесконечные циклы, громоздкие операции и прочие зависшие программы.


Ну кто из вас хоть раз не завершал зависшее приложение через Ctrl+Alt+Del? А ведь в большинстве случаев в ситуации, когда программа зависает, виноват программист, а не пользователь, который ее довел до этого состояния. Зачастую программы имеют в себе до 30% различной проверочных команд. Когда проверяются переполнение списков, проверяется корректность созданного объекта, компонента, проверяется или запущено то или иное внешнее приложение. Именно от этих условий и зависит дальнейшее корректное выполнение вашего приложения.

Рассмотрим принцип работы следующей программы.

Допустим, в оконном модуле есть две процедуры. Первая процедура - реакция на событие нажатия на кнопку Button1, вторая процедура - реакция на событие нажатия на кнопку Button2. Исходный текст процедур:

procedure TForm1.Button1Click(Sender: TObject);

begin

while true do

   begin

      // бесконечный цикл

   end;

end;

procedure TForm1.Button2Click(Sender: TObject);

begin

ShowMessage('Нажата кнопка Button1');

end;

В первой процедуре мы видим типичный программный код, при выполнении которого программа зависнет. Или говоря языком юзера - бесконечно выполняет одно и то же. Естественно, что выхода из такой процедуры не существует, а сама программа ожидает выхода из нее, чтобы выполнять другие события, в том числе и системные.

Принцип работы операционной системы windows основан на посылке сообщений компонентам программы. События при этом становятся в очередь и ожидают своей обработки. Поскольку выхода из первой процедуры нами не предусмотрено, то очередь сообщений не будет обрабатываться, и при нажатии на кнопки Ctrl+Alt+Del через некоторое время, мы видим, что приложение "не отвечает на системные запросы".

В подобных случаях это совсем не означает, что программа не работает. Она может зациклиться на обработки одной или нескольких команд, ожидая выхода из цикла по определенному условию (например, прочтения данных с диска) или обрабатывать большой объем данных опять таки в одной процедуре. А, как известно, корректная работа программы забота программиста, то есть нас с вами. Мы должны учесть все возможные ситуации, просчитать приблизительное время обработки операций даже на слабых компьютерах, при необходимости применить компонент ProgressBar, для того, чтобы пользователь не скучал, глядя на "неотвечающую" программу.




В вышерассмотренном примере, если нажать на кнопку Button1, а потом Button2, то реакция на событие нажатия на вторую кнопку будет помещена в очередь сообщений, но само событие не будет выполнено никогда. Кроме того, приложение перестанет получать все системные сообщения, т.е. окно нельзя будет переместить, свернуть, закрыть. Окно автоматически перестанет перерисовываться (имею в виду старые операционные системы windows).

Есть выход. В автоматически создаваемом компоненте при запуске программы Application, есть такая команда ProcessMessages, которая приостанавливает выполнение текущей процедуры, пока не будет выполнена очередь всех сообщений, посланных в программу. Если вставить строку

Application.ProcessMessages;

внутри продолжительных операций, то гарантировано получим не полностью зависшее приложение, а при умелом написании программного кода, из подобного цикла можно выйти по желанию пользователя (кнопка "Стоп"). И без создания дополнительного потока (считаю, что создание второго потока в программе занятие трудное и в больших проектах с этим можно запутаться). Такой пример "вечного" цикла мы рассматриваем в этом уроке.

Исключительные ситуации.

Это другой неприятный момент для пользователя и программиста. Это когда появляются неожиданные англоязычные ошибки и выполняемая процедура завершает свою работу с места появления ошибки. Разработчики Borland Delphi таким контролем за ошибками и выходом из работающих модулей программы позаботились об остальных частях программы. Если ошибка появилась в первой строке, то имеет ли смысл выполнять вторую?

Рассмотрим такой пример:

procedure TForm1.Button1Click(Sender: TObject);

Var x,y,z:Double; // переменные.

begin

   x:=0;

   z:=1;

   y:=z/x; // деление на нуль

   ShowMessage(FloatToStr(y));

end;

При выполнении данного кода мы получаем сообщение "Floating point division by zero". И процедура дальше уже не обрабатывается. После команды деления мы не видим его результат ShowMessage(РЕЗУЛЬТАТ) Или еще такой пример:


procedure TForm1.Button1Click(Sender: TObject);

begin

   Form1.ShowModal; // открыть окно модально

   ShowMessage('Окно открыто модально');

end;

Если у нас окно Form1 уже открыто, то повторная попытка открыть его модально приведет к возникновению ошибки "Cannot make a visible window modal". И опять программа завершает обработку процедуры. Это приведены примитивные процедуры, но бывает такие, что даже сам программист не ожидает появления подобной ситуации, но нужно предусмотреть все.

Что же делать, если мы открыли несколько файлов, заполняем список, создали вручную некоторые объекты. Выход из процедуры приводит к тому, что файлы не закрыты и повторный вызов этой процедуры приведут к возникновению другой ошибки - открытие уже открытого файла. Созданные объекты мы теряем при выходе из процедуры, а при повторном их создании, если они глобальные, получаем еще одну ошибку. А эта трата драгоценной памяти, которая занимается объектом и остается даже после выхода их программы. И так далее со всеми вытекающими последствиями.

Во-первых, надо при всех операциях деления предусматривать что-то подобное

if x<>0 then y:=z/x; // если x не равно нулю, то делить.

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

Я имею в виду, что при всех арифметических операциях нужно проверять все возможные входящие данные на их совместимость. Например:

Var st:String;

y:Double;

...

InputQuery('Ввод числа','введите любое число',st);

y:=StrToFloat(st);

Если не проконтролировать корректность ввода числа в строковую переменную st, то при попытке преобразования его в переменную типа Double, произойдет ошибка.

При сомнениях, насчет отображения того или иного окна на экране использовать, например:

if not Form1.Visible then Form1.ShowModal; // если окна нет на экране, то открыть его модально

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


В уроке15 мы рассматривали пример, в котором есть директива компилятору об отключении контроля ошибок ввода-вывода. Мы открывали файл, при отключенном автоматическом контроле, после чего сами обрабатывали ошибку открытия.

{$I-} // отключение контроля ошибок ввода-вывода

Reset(f); // открытие файла для чтения

{$I+} // включение контроля ошибок ввода-вывода

if IOResult<>0 then // если есть ошибка открытия, то

   begin

      ShowMessage('Ошибка открытия файла C:\1.TXT');

      Exit; // выход из процедуры при ошибке открытия файла

   end;

Такой метод можно применять только с старыми форматами команд, совместимыми с языком программирования pascal и работающими только с устройствами ввода-вывода.

В delphi можно применять следующее:

try // начало опасно-ошибочной части процедуры:

   Form1.ShowModal;

except // если возникла ошибка, то выполняется следующее:

   ShowMessage('Ошибка открытия окна');

end; // конец try

ShowMessage('Дальнейшая обработка процедуры');

Заметьте, что этот кусок кода не вызовет ошибки выполнения. Программа просто вам сообщит об ошибке открытия окна, после чего процедура продолжит свое выполнение. Еще обратите внимание, что если в части, после except возникнет другая ошибка выполнения, то процедура таки точно завершит свою работу немедленно. Все команды, которые могут вызвать ошибку надо писать после try и до except.

Теперь можно без опаски потери данных, после except корректно закрыть все открытые файлы, при необходимости окна, сообщить пользователю о возникшей ошибке и самому выйти из процедуры с помощью Exit.

Аналогом try-except есть try-finally

procedure TForm1.Button1Click(Sender: TObject);

Var StringList:TStringList; // список строк

begin

try

   StringList:=TStringList.Create; // создание списка строк

finally // при успешной обработки создания списка:

   StringList.Free; // удалить и освободить память


end;

ShowMessage('Дальнейшая обработка процедуры');

end;

После finally следует строки программы, которые следует выполнять в случае успешной обработки команд, следующие после try.

Рисование на форме или на компоненте PaintBox. Генератор колебаний. Пример

Большое спасибо Александру, за идею примера, который мы рассмотрим в этом уроке. Извиняюсь за то, что перевел тему с генератора на построитель функции синуса и косинуса.

У формы и у компонента PaintBox
(страница System палитры компонентов) есть свойство Canvas. Это свойство представляет собой растровое изображение, на котором можно рисовать или в которое можно загрузить рисунок. Я не буду рассказывать подробно об особенностях рисования, тем более, что я в этом не силен, но основные сведения дам.

Свойство Canvas доступно только во время работы приложения и с помощью его можно:

* Canvas.Brush - свойство фона. У него можно установить свойство Canvas.Brush.Color в необходимый цвет и с помощью следующей за ней командой Canvas.FillRect(ClientRect) можно очистить всю рабочую область компонента под заданный цвет. С помощью свойтва Canvas.Brush можно в качестве фона установить рисунок. Для этого есть свойство Canvas.Brush.Bitmap, которому нужно присваивать переменную с растровым рисунком.

* Canvas.MoveTo(x,y) - устанавливает перо в заданную точку, где x и y - координаты точки, относительно компонента. Начало координат, точка [0,0] находится в верхнем левом углу. После этой команды перо установлено, но точка не нарисована. Чтобы провести линию от текущего положения пера до заданного Canvas.LineTo(x,y). Поставить точку определенного цвета на холсте Canvas.Pixels[x,y]:=ЦВЕТ_ТОЧКИ.

* Через Canvas можно писать текст, рисовать дуги, сектор круга, овал, прямоугольник, ломаную линию, кривую.

* Свойства пера содержатся в Canvas.Pen. Здесь можно задать толщину пера Canvas.Pen.Width:=ТОЛЩИНА_В_ТОЧКАХ. Задать цвет Canvas.Pen.Color:=ЦВЕТ.

Рассмотрим пример генератора колебаний, который можно скачать здесь (3,1КБ). Генератор сам по себе есть только на экране. Колебания, которые он создает, рассчитываются по формуле. Следовательно, он вам может оказаться полезным. Можно изменить рассчетную функцию и вы получите для этой функции график.


Рекомендую скачать пример, и уже на нем рассматривать работу. Тем не менее, даю основные сведения, которые вам понадобятся при самостоятельном написании этой программы.

1. Программа имеет только одно окно Form1, у которого сразу переименовываем заголовок на подходящее название.

2. Устанавливаем свойство Form1.Position в poDesktopCenter, чтобы окно при каждом запуске и при любом экранном разрешении всегда было ровно посередине экрана.

3. Устанавливаем свойство Form1.BorderStyle в bsSingle, для неизменяемого размера окна. Оставляем во вложенных свойствах BorderIcons только biSystemMenu в true, остальные в false. Это для того, чтобы окно нельзя было свернуть в значек, развернуть во весь экран и окно имело иконку в заголовке.

4. Устанавливаем в форму компонент PaintBox, два компонента RadioButton, CheckBox, три кнопки Button и TrackBar, расположенный на странице Win32.

5. RadioButton1.Caption переименовываем в "Sin". Этот флаг будет признаком рисования синусоиды. RadioButton2.Caption переименовываем в "Cos" - косинусоида. Начальное значение флага Checked для RadioButton1 в true.

6. CheckBox1.Caption переименовываем в "Все". Если флаг установлен, то будет рисоваться два графика.

7. Названия кнопок Button1 - "Старт", Button2 - "Стоп (пауза)" и Button3 - "Выход". Названия на кнопках меняются через свойство Caption. Теперь назначение этих кнопок понятно.

8. Компонент TrackBar1 свойство минимального значения Min устанавливаем в 1, максимальное значение Max - 50.

9. PaintBox1, на котором будет непосредственно рисоваться график размеры высоты Height=140, ширина Width=500.

Привожу текст модуля для окна Form1.

Сразу после слова implementation
в модуле окна объявляем глобальные переменные, которые будут доступны из любой процедуры в этом модуле.

Var stop:boolean; // признак рисования

x:Integer; // координата оси X

Реакция на событие нажатия на кнопку Button1 (Начало рисования)

procedure TForm1.Button1Click(Sender: TObject);


Var y:Integer; // ось Y

begin

if x=0 then // если точка в начале координат, то:

   begin

      PaintBox1.Canvas.Brush.Color:=clWhite; // цвет фона белый

      PaintBox1.Canvas.FillRect(ClientRect); // заливка всей рабочей области

   end;

stop:=false; // флаг старта процесса рисования

While not stop do // бесконечный цикл, пока флаг остановки не поднят:

   begin

      if (RadioButton1.Checked)or(CheckBox1.Checked) then // если установлен "Sin" или "Все", то:

         begin

            y:=Round(Sin(pi*x/100)*50)+70; // вычисление положения синусоиды

            PaintBox1.Canvas.Pixels[x,y]:=clBlack; // нарисовать черную точку

         end;

      if (RadioButton2.Checked)or(CheckBox1.Checked) then // если установлен "Cos" или "Все", то:

         begin

            y:=Round(Cos(pi*x/100)*50)+70; // вычисление положения косинусоиды

            PaintBox1.Canvas.Pixels[x,y]:=clBlack; // нарисовать черную точку

         end;

      inc(x); // увеличить значение X на едицину. Аналог X:=X+1

      if x>500 then // если X вышел за пределы PaintBox1, то:

         begin

            x:=0; // установить X на начало координат

            PaintBox1.Canvas.Brush.Color:=clWhite; // Цвет фона белый

            PaintBox1.Canvas.FillRect(ClientRect); // Очистка рабочей области PaintBox1


         end;

      Sleep(TrackBar1.Position); // Процедура "засыпает" на заданное время в миллисекундах

      Application.ProcessMessages; // Обработка всей очереди сообщений

   end;

end;

Коротко расскажу работу этой процедуры.

Как только нажата кнопка "Старт" Компонент PaintBox1 очищается и начинается бесконечный цикл While, выйти из которого можно только, пока переменная Stop не примет значение true. Это можно сделать кнопкой Button2, соответствующая процедура которой обработается во время Application.ProcessMessages. С помощью бегунка TrackBar1 можно менять скорость рисования кривой. Этот параметр передается в команду Sleep.

Процедура нажатия на кнопку остановки Button2:

procedure TForm1.Button2Click(Sender: TObject);

begin

Stop:=true; // установить флаг остановки процесса рисования

end;

Процедура создания окна Form1OnCreate:

procedure TForm1.FormCreate(Sender: TObject);

begin

x:=0; // начальное значение X

end;

Если нажата кнопка "Выход", то реакция на это событие будет таким:

procedure TForm1.Button3Click(Sender: TObject);

begin

Close; // закрыть окно

end;

И реакция перед закрытием окна OnClose. Без этой процедуры, если рисование включено, то окно не закроется.

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);

begin

Stop:=true; // остановить (если включен) цикл рисования

end;

После запуска программы, установки флажка "Все" и нажатии на кнопку "Старт" на экране отобразится этот график:



В принципе, с нашей программой можно отслеживать практически любые функции. Просто надо описать свою отслеживаемую функцию вместо моих строк Y:=Sin...

Если вы хотите убрать постоянную прорисовку графика функции, то следующий код в программе

      if x>500 then // если X вышел за пределы PaintBox1, то:

         begin


            x:=0; // установить X на начало координат

            PaintBox1.Canvas.Brush.Color:=clWhite; // Цвет фона белый

            PaintBox1.Canvas.FillRect(ClientRect); // Очистка рабочей области PaintBox1

         end;

измените на:

      if x>500 then // если X вышел за пределы PaintBox1, то:

         begin

            x:=0; // установить X на начало координат

            Stop:=true; // остановка рисования

         end;

Напомнню, что график функции вы видите перевернутым. Начало координат в мониторе находится в верхнем левом углу. Математики тут найдут выход. Для переворачивания функции нужно от значения рабочей высоты элемента, на котором рисуем, вычитать значение функции

Y:=140 - ФУНКЦИЯ;

Как видите, поле для экспериментов велико. Приступайте к самостоятельному изучению.
С уважением, ведущий уроков Semen semen@krovatka.net

Содержание раздела