Здравствуйте.
Эта тема уже поднималась, но заглохла, поэтому вынужден вернуться к ней снова.
Вот этим запросом:
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 можно построить тренд (диаграмму) только в отчетах.
Цитата: Simple-Scada от 07 мая 2019, 16:17:07Для записи значений в глобальные переменные придется писать "неудобный" код в котором записывать каждое значение в отдельную переменную, например в зависимости от значения счетчика цикла. Если переменные имеют однотипные имена, то код можно сделать достаточно простым если искать переменные по имени через GetVariableByName (https://simple-scada.com/help/script/getvariablebyname.html).
Что тогда представляют собой переменные типа Boolean Array, Byte Array, Word Array и т.п.?
Цитата: Simple-Scada от 07 мая 2019, 16:17:07В пользовательскую таблицу тоже можно сохранить результат, но нужно будет сначала сформировать 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.Все работает, но мне не нравится, что при первом клике на кнопку просмотра отчета выходит старый отчет (как будто новый не успевает сформироваться), приходится кликать еще раз. Это нормально?
ЦитироватьВсе работает, но мне не нравится, что при первом клике на кнопку просмотра отчета выходит старый отчет (как будто новый не успевает сформироваться), приходится кликать еще раз.
Может быть у Вас отчет строится долго и Вы не дожидаясь нажимаете кнопку построения снова? Тогда возможна ситуация когда второй отчет заменит первый.
Если только один раз кликнуть кнопку построения отчета и ждать, то отчет не строится? Или строится через длительное время? Если не строится, то есть ли какие-нибудь предупреждения в журнале сервера (https://simple-scada.com/help/manual/server-journal.html)?
Цитата: Simple-Scada от 07 мая 2019, 17:41:48Может быть у Вас отчет строится долго и Вы не дожидаясь нажимаете кнопку построения снова? Тогда возможна ситуация когда второй отчет заменит первый.
Если только один раз кликнуть кнопку построения отчета и ждать, то отчет не строится? Или строится через длительное время?
Мне кажется, что пользовательская таблица по результату запроса просто не успевает сформироваться, и на просмотр выходит старый отчет. Какой командой можно сделать небольшую задержку перед ReportView?
ЦитироватьМне кажется, что пользовательская таблица по результату запроса просто не успевает сформироваться
Да, верно, скорее всего так и происходит, отчет успевает построиться со старыми данными, и уже после его построения происходит обновление пользовательской таблицы.
У Вас пользовательская таблица формируется в процедуре 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.
Спасибо, с отчетом из пользовательской таблицы все получилось. Но вот как с моим предыдущим вопросом насчет переменных типа Array? Вы так и не ответили...
Типы данных Boolean Array, Byte Array, Word Array и т.п. используются для чтения переменных-массивов c OPC-DA /OPC-UA серверов, т.е. только для внешних тегов. Если нужна внутренняя переменная-массив, то её можно объявить в скрипте:
var
aArray: array[1..10] of integer;
begin
aArray[1] := 55;
end.
Почему-то отчеты с сохранением имени клиента во внутреннюю переменную и ее передачей в скрипт "Выполнен SQL-запрос", где вызывается команда ReportView, не работают в WEB-клиенте...
Здесь проблема. Выполняя ReportView из скрипта "Выполнен SQL-запрос" сервер знает только имя клиента (из переменной vrClientName), но не знает обычный это клиент или web, поэтому строит по-умолчанию для обычного клиента. А должен строить разные типы отчетов, для web это .pdf-документ, для обычных клиентов .mdc файл отчета. Подумаем как можно решить эту проблему. Возможно получится сделать сохранение всех данных о клиенте, а не только имени, чтобы сервер знал тип клиента.
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 от 08 мая 2019, 15:29:43Типы данных Boolean Array, Byte Array, Word Array и т.п. используются для чтения переменных-массивов c OPC-DA /OPC-UA серверов, т.е. только для внешних тегов. Если нужна внутренняя переменная-массив, то её можно объявить в скрипте:
var
aArray: array[1..10] of integer;
begin
aArray[1] := 55;
end.
Каким образом можно заполнить массив определёнными значениями для передачи в combobox?
На delphi это делается, к примеру:
var ar : Array[1..4] of byte = (1,5,2,4);
Simple-Scada такой код компилирует, но при сохранении выдаёт ошибку "Cannot initialize local variables".
Как быть в этом случае?
Можете ли порекомендовать что-то для формирования combobox в зависимости от пользовательского выбора из другого combobox?
Компилятор 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, когда список работает со строками, а не с числами. Возможно, если бы Вы подробно описали конкретную задачу, то мы бы предложили оптимальное решение.
Спасибо за ответ и напоминание, что со строками в списке можно работать напрямую.
Задача простая, но из-за долгого перерыва и недостатке знаний, завис на простых моментах.
Есть два ComboBox'а, один состоит из 4-х элементов. Для второго имеется n строковых элементов, по разному набору с разными значениями для каждого из 4-х вариантов. Необходимо заполнять второй ComboBox в зависимости от выбора в первом.
Решил вначале объявлением строкового массива в глобальном модуле, инициализации и заполнением его в 'Проект запущен' и использованием в коде.
После переделал на работу со строкой путём вытягивание необходимых элементов по заданным правилам. Сложновато, но работает.
Как альтернатива - можно создать 4 дополнительных ComboBox'а и переносить значения по заданным правилам во второй ComboBox. Или в таблицу занести всё и из неё тянуть. Не знаю, что будет по памяти/нагрузке на систему сложнее.
// Тип 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.Задача решена.
Тот же способ и использовал.
Вместо ComboBox1.AsInt брал ComboBox1.ItemIndex, а элементы загнал в строку и через обработчик вытягивал нужные значения и добавлял их цикле через ComboBox2.AddItem.
Спасибо за помощь :-)