Здравствуйте.
Эта тема уже поднималась, но заглохла, поэтому вынужден вернуться к ней снова.
Вот этим запросом:
SELECT date(timestamp), MAX(value) FROM oplax_sever_db.trends_data
WHERE (ID=31) and (timestamp >= "2019-04-01") and (timestamp < "2019-05-01")
GROUP BY date(timestamp) order by timestamp
я получаю двумерный массив такого вида:
2019-04-01 712
2019-04-02 693
2019-04-03 544
2019-04-04 571
2019-04-05 706
2019-04-06 777
.............................
Как сохранить результаты запроса в некие глобальные переменные, из которых затем построить диаграмму (по оси Х дата, по оси Y максимальное значение счетчика)?
Сам сообразить не могу, прошу подсказки.
P.S. Тут пришла идея поместить результаты запроса не в массив, а в пользовательскую таблицу, и затем из нее построить диаграмму. Но использование массива мне кажется рациональнее. Только вот как?
Здравствуйте.
Для работы с результатом пользовательского SQL-запроса нужно использовать скрипт с типом события "Выполнен SQL-запрос". В таких скриптах результат запроса хранится в параметре DataSet (набор данных). Пройти по набору данных можно в цикле, вот так:
begin
{ выполнять цикл пока не достигнем конца набора данных }
while not DataSet.EOF do
begin
// здесь работаем с текущей строкой
DataSet.Next; // переходим на следующую строку
end;
end.
А это тот же код, но добавлен проход по ячейкам каждой строки:
var
I: Integer;
aStr: string;
begin
{ выполнять цикл пока не достигнем конца набора данных }
while not DataSet.EOF do
begin
{ проходим по каждой ячейке текущей строки }
for I := 0 to DataSet.FieldCount - 1 do
aStr := DataSet[I].AsStr; // получаем значение текущей ячейки переведённое в строку и записываем его в переменную aStr.
DataSet.Next; // переходим на следующую строку
end;
end.
Для записи значений в глобальные переменные придется писать "неудобный" код в котором записывать каждое значение в отдельную переменную, например в зависимости от значения счетчика цикла. Если переменные имеют однотипные имена, то код можно сделать достаточно простым если искать переменные по имени через GetVariableByName (https://simple-scada.com/help/script/getvariablebyname.html).
P.S. Тут пришла идея поместить результаты запроса не в массив, а в пользовательскую таблицу, и затем из нее построить диаграмму. Но использование массива мне кажется рациональнее. Только вот как?
Массив сначала придётся заполнить, т.е. поместить в него нужные переменные в нужном порядке, поэтому проще сразу выполнять запись напрямую в нужные переменные. В пользовательскую таблицу тоже можно сохранить результат, но нужно будет сначала сформировать SQL-запрос на вставку данных (INSERT INTO (http://www.mysql.ru/docs/man/INSERT.html)) в БД, что является довольно обширной задачей. Также нужно отметить что не получится увидеть тренд по списку значений X / Y в скаде, т.к. в меню трендов на оси X всегда расположено время. По списку значений X / Y можно построить тренд (диаграмму) только в отчетах.
В пользовательскую таблицу тоже можно сохранить результат, но нужно будет сначала сформировать SQL-запрос на вставку данных (INSERT INTO (http://www.mysql.ru/docs/man/INSERT.html)) в БД, что является довольно обширной задачей. Также нужно отметить что не получится увидеть тренд по списку значений X / Y в скаде, т.к. в меню трендов на оси X всегда расположено время. По списку значений X / Y можно построить тренд (диаграмму) только в отчетах.
Вы так долго не отвечали, что отчет из пользовательской таблицы я уже сделал:
procedure btnGetChartPerform_OnClick(Sender:TM_Control)
var
aQuery: string;
aFrom, aTo: string;
aShift: integer;
begin
vrTimeBegin.Value := RecodeTime(vrTimeBegin.AsDateTime, 0, 0, 0, 0);
aFrom := MySQLDateTime(vrTimeBegin.AsDateTime, dttHour);
aTo := MySQLDateTime(RecodeTime(vrTimeEnd.AsDateTime + 1, 0, 0, 0, 0), dttHour);
aQuery := 'REPLACE INTO oplax_sever_db.report_values ' +
'SELECT date(a.timestamp), round(max(a.value/12)), b.shift FROM oplax_sever_db.trends_data a ' +
'LEFT JOIN oplax_sever_db.schedule b ON date(a.timestamp) = (b.date) ' +
'WHERE (date(a.Timestamp) >= ' + aFrom + ') AND (date(a.timestamp) < ' + aTo + ') AND (a.ID=31) ' +
'GROUP BY date(a.timestamp) ORDER BY date(a.timestamp)';
aShift := vrShift_2.AsInt;
if aShift = 0 then
aQuery := aQuery + ';'
else
begin
aQuery := aQuery + ' AND (b.shift = ' + IntToStr(aShift) + ');';
vrShiftStr.Value := 'Бригада № ' + IntToStr(vrShift_2.Value);
end;
RunSQL(aQuery, nil, 11);
ReportView(GetClientName, 'Диаграмма_производительности');
end.
Все работает, но мне не нравится, что при первом клике на кнопку просмотра отчета выходит старый отчет (как будто новый не успевает сформироваться), приходится кликать еще раз. Это нормально?
Мне кажется, что пользовательская таблица по результату запроса просто не успевает сформироваться
Да, верно, скорее всего так и происходит, отчет успевает построиться со старыми данными, и уже после его построения происходит обновление пользовательской таблицы.
У Вас пользовательская таблица формируется в процедуре btnGetChartPerform_OnClick через команду:
RunSQL(aQuery, nil, 11); // запрос помечается тегом равным 11
Нельзя назвать какое-то определённое время за которое выполнится RunSQL, скорость её выполнения зависит от нагрузки на СУБД и производительности ПК в момент вызова RunSQL. Поэтому фиксированная задержка перед построением отчета будет неправильным решением. Правильнее будет создать отдельный скрипт с типом события "Выполнен SQL-запрос" (https://simple-scada.com/help/script/sql-query-completed.html) и в нём вызывать построение отчета, когда пользовательская таблица уже гарантированно обновлена. Только в промежутке между обновлением таблицы и построением отчета придётся сохранить имя клиента для которого строится отчет. Для этого можно создать внутреннюю переменную vrClientName типа string.
procedure btnGetChartPerform_OnClick(Sender:TM_Control)
var
aQuery: string;
aFrom, aTo: string;
aShift: integer;
begin
vrTimeBegin.Value := RecodeTime(vrTimeBegin.AsDateTime, 0, 0, 0, 0);
aFrom := MySQLDateTime(vrTimeBegin.AsDateTime, dttHour);
aTo := MySQLDateTime(RecodeTime(vrTimeEnd.AsDateTime + 1, 0, 0, 0, 0), dttHour);
aQuery := 'REPLACE INTO oplax_sever_db.report_values ' +
'SELECT date(a.timestamp), round(max(a.value/12)), b.shift FROM oplax_sever_db.trends_data a ' +
'LEFT JOIN oplax_sever_db.schedule b ON date(a.timestamp) = (b.date) ' +
'WHERE (date(a.Timestamp) >= ' + aFrom + ') AND (date(a.timestamp) < ' + aTo + ') AND (a.ID=31) ' +
'GROUP BY date(a.timestamp) ORDER BY date(a.timestamp)';
aShift := vrShift_2.AsInt;
if aShift = 0 then
aQuery := aQuery + ';'
else
begin
aQuery := aQuery + ' AND (b.shift = ' + IntToStr(aShift) + ');';
vrShiftStr.Value := 'Бригада № ' + IntToStr(vrShift_2.Value);
end;
vrClientName.Value := GetClientName; // запоминаем имя клиента для которого нужно будет построить отчет
RunSQL(aQuery, nil, 11); // отправляем запрос на выполнение с тегом 11
end.
Скрипт "Выполнен SQL-запрос":
begin
if DataSet.Tag = 11 then // если запрос с тегом 11 выполнен, то строим отчет
ReportView(vrClientName.AsStr, 'Диаграмма_производительности');
end.
Типы данных Boolean Array, Byte Array, Word Array и т.п. используются для чтения переменных-массивов c OPC-DA /OPC-UA серверов, т.е. только для внешних тегов. Если нужна внутренняя переменная-массив, то её можно объявить в скрипте:
var
aArray: array[1..10] of integer;
begin
aArray[1] := 55;
end.
alan54, в обновлении 2.3.5.0 добавили новые функции с помощью которых можно решить описанную задачу. Вот как нужно изменить код:
В процедуре btnGetChartPerform_OnClick:
RunSQL(aQuery, GetClientData, 11); // помечаем запрос тегом равным 11 и передаём данные клиента
Скрипт "Выполнен SQL-запрос":
var
aClientData: TM_ClientData;
begin
if DataSet.Tag = 11 then // если запрос с тегом 11 выполнен, то строим отчет
if DataSet.Sender is TM_ClientData then
begin
aClientData := DataSet.Sender as TM_ClientData;
ReportViewEx(aClientData, 'Диаграмма_производительности');
end;
end.
Промежуточная переменная vrClientName типа string больше не нужна.
Компилятор Simple-Scada практически ничем не отличается от Delphi-компилятора. Если Вы в Delphi (кроме последней версии 10.3) таким образом попробуете инициализировать локальную переменную, то получите точно такую же ошибку: "Cannot initialize local variables", ведь инициализировать локальные переменные на этапе объявления нельзя. Инициализировать на этапе объявления можно только глобальные переменные.
В Simple-Scada всё то же самое. Если в меню скриптов создать "Глобальный модуль" (https://simple-scada.com/help/script/globalmodule.html), то в нём можно будет объявить глобальную переменную и сразу инициализировать её, например:
interface
var
ar: Array[1..4] of byte = (1,5,2,4);
implementation
end.
, затем эту переменную можно будет использовать в коде любого скрипта, т.к. она глобальная. А локальные переменные можно инициализировать только на этапе реализации (между begin и end), например так:
var
ar: Array[1..4] of byte;
begin
// сначала инициализируем массив
ar[1] := 1;
ar[2] := 5;
ar[3] := 2;
ar[4] := 4;
// далее работаем с массивом...
end.
Можете ли порекомендовать что-то для формирования combobox в зависимости от пользовательского выбора из другого combobox?
Через скрипты в любой момент можно очистить раскрывающийся список через метод ComboBox.Clear (https://simple-scada.com/help/script/cmbclear.html) и сразу добавить в список новые строки через ComboBox.AddItem (https://simple-scada.com/help/script/cmbadditem.html). Таким образом можно на ходу менять состав раскрывающихся списков.
Спасибо за объяснение.
Метод ComboBox.AddItem (https://simple-scada.com/help/script/cmbadditem.html) добавляет только по одной строке.
Можно ли как-то передавать в ComboBox весь массив или список, сформированный ранее?
Это возможно делать с помощью того же метода AddItem в цикле, но как тогда сформировать массив или список небольшого количества заданных значений, не используя "ручной" метод?
ar[1] := 1;
ar[2] := 5;
...
Другими словами, что-то по типу инициализации локального массива ar := (1,5,2,4) на этапе реализации (это также ручной метод, с более удобной записью). В delphi не силён, в примере могут быть синтаксические ошибки.
И на эту же тему...
В описании к "Глобальному модулю" (https://simple-scada.com/help/script/globalmodule.html) есть важное замечание по поводу объявления глобальных переменных строкового типа..
Важно! Использование таких глобальных переменных является плохим тоном. Вместо этого рекомендуется создавать глобальные переменные через меню редактирования переменных. Особенно не рекомендуется объявлять в глобальных модулях переменные строковых типов из-за особенностей их хранения в памяти.
Как быть в этом случае со строковыми переменными?
Можно ли как-то передавать в ComboBox весь массив или список, сформированный ранее?
Нет, для добавления строк в список через скрипты можно использовать только метод AddItem. При необходимости добавление можно сделать в цикле.
но как тогда сформировать массив или список небольшого количества заданных значений, не используя "ручной" метод?
Массивы бывают статические и динамические:
var
arStat: array[1..4] of byte; // статический массив
arDyn: array of integer; // динамический массив
Про статические массивы мы писали выше: если такой массив объявлен в глобальном модуле, то его можно инициализировать сразу в объявлении. Если статический массив описан не в глобальном модуле, то для его инициализации нужно либо пройти в цикле по элементам массива и заполнить каждый элемент, либо, если значения массива уникальные, то отдельной строкой кода инициализировать каждый элемент массива.
А с динамическими массивами инициализацию можно выполнить и в одну строку, вот пример объявление + инициализация + проход в цикле:
var
i: integer;
a: array of integer;
begin
a := [7, 8, 9]; // инициализируем массив тремя значениями
for i := 0 to Length(a) - 1 do // проходим по элементам массива в цикле
begin
// здесь работаем с каждым элементом массива
end;
end.
В описании к "Глобальному модулю" есть важное замечание по поводу объявления глобальных переменных строкового типа..
Это рекомендация. Ответ написан в процитированном Вами тексте: "... Вместо этого рекомендуется создавать глобальные переменные через меню редактирования переменных.". В целом, если без строк в глобальном модуле можно обойтись, то лучше их в нём не использовать. Если других вариантов нет, то можно использовать строковые переменные.
Создаётся впечатление, что Вы хотите решить какую-то странную задачу средствами скады, не совсем понятно зачем нужны эти массивы и строковые переменные, если со строками в списке можно работать напрямую. И почему Вы хотите передать в список числа типа byte, когда список работает со строками, а не с числами. Возможно, если бы Вы подробно описали конкретную задачу, то мы бы предложили оптимальное решение.
Обычно это делают одним OnDatachange скриптом. Пример: у нас есть два раскрывающихся списка ComboBox1 и ComboBox2. Нужно формировать разный набор строк в списке ComboBox2, в зависимости от того что выбрано в списке ComboBox1.
Решение: выделяем список ComboBox1 и на событие OnDataChange назначаем следующий скрипт:
begin
ComboBox2.Clear; // очищаем список ComboBox2
case ComboBox1.AsInt of
0: // выбрана первая строка в списке ComboBox1
begin
ComboBox2.AddItem('Красный');
ComboBox2.AddItem('Синий');
ComboBox2.AddItem('Зелёный');
end;
1: // выбрана вторая строка в списке ComboBox1
begin
ComboBox2.AddItem('Один');
ComboBox2.AddItem('Два');
ComboBox2.AddItem('Три');
end;
2: // выбрана третья строка в списке ComboBox1
begin
ComboBox2.AddItem('Вверх');
ComboBox2.AddItem('Вниз');
ComboBox2.AddItem('Влево');
ComboBox2.AddItem('Вправо');
end;
end;
end.
Задача решена.