配方是给机器设定的一组运行参数,当机器要生产不同规格的产品时,可以给机器设定不同的参数。
配方的设计要点之一是如何保存配方。制药行业要求对生产数据一般保存5年,生产数据包括生产时所用的配方,当打印报表时需要把对应的配方打印出来。这就要求已生产过的配方要能长期保存,不能被修改或删除。
以下内容介绍如何基于WinCC的用户归档存储配方。用户归档是对SQL数据库的一种封装,与直接读写数据库相比,用户归档可以满足双机热备的要求。
用户归档的读写要求
用户归档实质是存储在SQL Server中的数据库,优点是基于WinCC软件实现了两台电脑里数据库的双机热备。SQL Server也有一些热备方法,例如“数据库复制:发布-订阅”,但是因为WinCC的限制不能使用,用户归档的热备在此更合适。
要实现用户归档的热备冗余,不能直接写数据库,脚本中只能通过UA API函数、控制变量对用户归档写入,对用户归档读取可以直接读数据库。UA API函数只在C脚本中支持,用C脚本读写配方过于复杂,所以在VB脚本中使用控制变量操作配方。

配方的数据流向
用控制变量操作配方,只能将配方从变量写入到用户归档的数据库,或者将特定配方从用户归档的数据库写入到变量。实际使用中,需要先查看配方,然后再将配方下载到PLC,因此需要两套不同的变量,其中一组变量作为中间变量(内部变量)用于查看、编辑,另一组变量是PLC中的实际变量(外部变量),将中间变量的值写入到这组变量就意味着下发了配方。数据流图如下。

用户归档数据库中的特殊字段

红框中的字段是每个配方数据库表中都统一的字段,以下是这些字段的说明。
| 字段名称 | 类型 | 描述 |
| Recipe_ID | 字符串 | 通过Recipe_ID唯一标识一个配方,该字段由Recipe_No和Recipe_Edition拼接而成。 |
| Recipe_No | 字符串 | 新建配方时指定唯一配方编号,修改配方将生产新的配方版本,不会覆盖旧配方,新旧配方的配方编号相同。 |
| Recipe_Edition | 字符串 | 配方的版本,新建的配方的版本为1,修改并保存后配方版本自动加1,再与配方编号拼接成新的配方ID,在数据库中存储一条新的数据。 |
| Recipe_Name | 字符串 | 配方名称。 |
| Recipe_Description | 字符串 | 配方描述,同一配方的不同版本的描述可以不同。 |
| IsProduced | 数字(整型) | 已生产标志位,当配方已下发并执行了生产,将这个配方的标志位置1,否则这个标志位为0。 |
| IsDeleted | 数字(整型) | 当删除一个配方时,如果IsProduced为1,则把IsDeleted标志位置1,表示已删除,查看配方时不再显示这条配方,但配方依然存储在数据库中,打印报表时依然可以调出这个配方的数据;如果IsProduced为0,说明这个配方没有生产过,将会从数据库中删除。 |
使用控制变量操作用户归档,则必须给每个字段值绑定参数变量,然后将参数变量的值写入到用户归档。
绑定的参数变量如下,“XX_”代表设备前缀,不同的设备指定不同的设备前缀。设备前缀之后的变量名在所有设备中都是统一的,之后的代码根据这些变量名传递配方信息。“M_”代表这是中间变量(内部变量),还有一组不带“M_”中缀的同名变量,那些是外部变量,下载配方时通过中间变量向外部变量写值完成。
| 变量名 | 类型 |
| XX_M_RecipeID | 文本变量8位字符集 |
| XX_M_RecipeNo | 文本变量8位字符集 |
| XX_M_RecipeEdition | 文本变量8位字符集 |
| XX_M_RecipeName | 文本变量8位字符集 |
| XX_M_RecipeDescription | 文本变量8位字符集 |
| XX_M_IsProduced | 二进制变量 |
| XX_M_IsDeleted | 二进制变量 |
如何用控制变量操作用户归档

用户归档需要绑定四个变量对其进行操作,这四个变量分别为ID、Job、Field、Value,控制变量的说明如下:
|
控制变量 |
功能 |
数据类型 |
|---|---|---|
|
ID |
用户归档的数据记录编号 |
有符号 32 位数 |
|
Job |
可能存在下列作业: “6”= 读取变量写入到用户归档中的数据记录 “7”= 将数据记录从用户归档写入变量 “8”= 删除用户归档中的数据记录 执行作业后,“作业”变量将变为以下数值: “0”= 无错误 “-1”= 有错误 |
有符号 32 位数 |
|
Field |
用户归档的特定字段 |
文本变量,8 位 |
|
Value |
特定用户归档字段的值 |
文本变量,8 位 |
控制变量“ID”和“作业”的组合:
|
ID |
作业 =“6” |
作业 =“7” |
作业 =“8” |
|
-1 |
读取变量向用户归档中新增数据记录 |
- |
删除最低 ID 的数据记录 |
|
-6 |
读取变量写入到最低 ID 的数据记录 |
读取最低 ID 的数据记录写入到变量 |
删除最低 ID 的数据记录 |
|
-9 |
读取变量写入到最高 ID 的数据记录 |
读取最高 ID 的数据记录写入到变量 |
删除最高 ID 的数据记录 |
|
>0 |
读取变量写入到ID变量指定的数据记录 |
读取ID变量指定的数据记录写入到变量 |
删除ID变量指定的数据记录 |
|
0 |
读取变量写入到字段变量和值变量指定的数据记录 | 读取字段变量和值变量指定的数据记录写入到变量 | 删除字段变量和值变量指定的数据记录 |
操作用户归档的全局函数
用控制变量操作用户归档的方式不太明晰,写成全局变量可以使该过程更加简便,常用的方式是用Field变量和Value变量确定一条记录,以下是写在全局脚本中操作用户归档的函数。用控制变量操作用户归档是一个异步的过程,函数中添加了检查Job变量返回值的代码,变成了同步过程,函数返回0表示执行成功,返回-1表示执行失败。
注意:虽然是全局函数,但是在设定了前缀的画面窗口中调用时,全局函数中使用的变量也会被加上前缀。
Function Delete_UA(strUAName,strField,strValue)'删除一条信息 HMIRuntime.Tags(strUAName&"_ID").Write 0 HMIRuntime.Tags(strUAName&"_Field").Write strField HMIRuntime.Tags(strUAName&"_Value").Write strValue HMIRuntime.Tags(strUAName&"_Job").Write 8 Do Delete_UA = HMIRuntime.Tags(strUAName&"_Job").Read Loop Until Delete_UA=0 Or Delete_UA=-1 End Function Function Select_UA(strUAName,strField,strValue) '下载一条信息 HMIRuntime.Tags(strUAName&"_ID").Write 0 HMIRuntime.Tags(strUAName&"_Field").Write strField HMIRuntime.Tags(strUAName&"_Value").Write strValue HMIRuntime.Tags(strUAName&"_Job").Write 7 Do Select_UA = HMIRuntime.Tags(strUAName&"_Job").Read Loop Until Select_UA=0 Or Select_UA=-1 End Function Function Insert_UA(strUAName)'添加一条信息 HMIRuntime.Tags(strUAName&"_ID").Write -1 HMIRuntime.Tags(strUAName&"_Field").Write "" HMIRuntime.Tags(strUAName&"_Value").Write "" HMIRuntime.Tags(strUAName&"_Job").Write 6 Do Insert_UA = HMIRuntime.Tags(strUAName&"_Job").Read Loop Until Insert_UA=0 Or Insert_UA=-1 End Function Function Update_UA(strUAName,strField,strValue)'修改一条信息 HMIRuntime.Tags(strUAName&"_ID").Write 0 HMIRuntime.Tags(strUAName&"_Field").Write strField HMIRuntime.Tags(strUAName&"_Value").Write strValue HMIRuntime.Tags(strUAName&"_Job").Write 6 Do Update_UA = HMIRuntime.Tags(strUAName&"_Job").Read Loop Until Update_UA=0 Or Update_UA=-1 End Function
配方界面概览

操作配方的全局函数
上图的新建、保存、下载、上载和删除按钮中会用到许多脚本,这些脚本并不直接写在控件中,而是以函数形式定义在VBS全局项目模块中,然后在控件中调用函数。一个WinCC程序可能要管理许多台设备的配方,也就有许多张配方画面,假如直接将脚本写在控件中,当需要修改脚本时,会要修改很多遍。所以将脚本集中定义在全局项目模块,便于维护代码。
这些函数中通过控件名称调用控件,必须在配方画面中才能使用,并且控件的命名必须依照给定的名称。
调用函数会用到三个参数:
- tagPrefix:变量前缀。不同设备的变量都带有不同的变量前缀,避免变量名冲突。
- DSN:数据源名称。配方数据表的名称,建议用户归档以“变量前缀+Recipe”命名,实际在数据库中还会加上“UA#”前缀。例如,假设变量前缀是“XX_”,则DSN为“UA#XX_Recipe”。
- objNameList:这是一个二维数组,第一列是配方数据表字段名和画面控件的名称,要求配方数据表字段名和被编辑的控件的名称一致;第二列是变量名。
全局项目模块中的代码如下(建议文件命名为_Recipe.bmo):
'该文件中的函数仅用于当前项目的recipe,目的是为了方便编辑,不具有移植性。
'新建配方
Sub NewRecipe(tagPrefix, DSN, objNameList)
'-----------------------------------------------------
' 新建配方
' 输入配方编号、配方名和配方描述,
' 检查配方编号是否与已有的配方重复,
' 将配方信息写入到界面控件中显示。
'-----------------------------------------------------
'询问是否新建配方
Dim RecipeNo,RecipeName,RecipeDescription
If ScreenItems("BT_Save").Enabled=True Then '检查是否有配方正在编辑
If Msgbox (TranslateText("配方正在编辑,确定不保存该配方,继续新建配方吗?", 2052),vbOKCancel+vbQuestion,"Note") = vbCancel Then
Exit Sub
End If
Else
If Msgbox (TranslateText("新建配方吗?",2052),vbOKCancel+vbQuestion,"Note") = vbCancel Then
Exit Sub
End If
End If
'创建数据库对象
Dim cnn, rs
Set cnn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
'输入配方编号
Do While True
RecipeNo = Trim(InputBox(TranslateText("请输入新配方编号:", 2052),"Note"))
'检查输入的配方编号是否符合要求
If RecipeNo="" Or Check_LawlessChar(RecipeNo)=True Then
If MsgBox (TranslateText("配方编号只能包含字母和数字,请重新输入!", 2052),vbRetryCancel + vbInformation,"Note") = vbCancel Then
Exit Sub
Else
'如果选择重试则继续执行循环
End If
else
'检查配方编号是否重复
Dim SQL
SQL = "select Recipe_No from "& DSN &" where Recipe_No='" & RecipeNo & "' AND IsDeleted = 0"
ConnectDatabase cnn,rs,SQL
If rs.recordcount>0 Then
If MsgBox (TranslateText("配方编号重复!",2052),vbRetryCancel + vbInformation,"Note") = vbCancel Then
rs.Close
cnn.Close
Exit Sub
else
'如果选择重试则继续执行循环
End If
Else
rs.Close
cnn.Close
Exit Do
End If
End If
Loop
Set rs = Nothing
Set cnn = Nothing
'输入配方名称
Do While True
RecipeName = Trim(InputBox(TranslateText("请输入新配方名称:",2052),"Note"))
If RecipeName="" Then
If MsgBox (TranslateText("配方名称不能为空!",2052),vbRetryCancel + vbInformation,"Note") = vbCancel Then
Exit Sub
else
'如果选择重试则继续执行循环
End If
Else
Exit Do
End If
Loop
'输入配方描述
RecipeDescription = Trim(InputBox(TranslateText("请输入新配方描述(可以为空):",2052),"Note"))
'清空控件内容
Dim objName
For Each objName In objNameList
ScreenItems(objName(0)).text = ""
Next
'向控件填入配方信息
ScreenItems("ComboRecipeList").text = RecipeNo
ScreenItems("ComboEditionList").text = "0"
ScreenItems("Recipe_Name").text = RecipeName
ScreenItems("Recipe_Description").text = RecipeDescription
'设置按钮状态
ScreenItems("BT_New").Enabled = False
ScreenItems("BT_Download").Enabled = False
ScreenItems("BT_Save").Enabled = True
ScreenItems("BT_Delete").Enabled = False
ScreenItems("ComboRecipeList").Enabled = True
End Sub
'*******************************************************************************************************************************************
'读取数据库中的配方编号写入到ComboBox控件
Sub WriteRecipeNoToComboBox(DSN, objName)
'创建数据库对象
Dim cnn, rs
Set cnn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
'查询不为空并且未删除的配方编号
Dim SQL
SQL = "Select distinct Recipe_No FROM "& DSN &" where Recipe_No<>'' AND Recipe_No IS NOT NULL AND IsDeleted = 0"
ConnectDatabase cnn,rs,SQL
'将配方编号写入到控件
ScreenItems(objName).clear
If rs.RecordCount>0 Then
rs.MoveFirst
Dim i
For i = 1 To rs.RecordCount
ScreenItems(objName).AddItem rs("Recipe_No")
rs.MoveNext
Next
End If
rs.Close
Set rs = Nothing
cnn.Close
Set cnn = Nothing
End Sub
'*******************************************************************************************************************************************
'保存配方
Sub SaveRecipe(tagPrefix, DSN, objNameList)
'--------------------------
' 保存配方:
' 查找配方编号的最大版本号,
' 将最大版本号递增置为下一版本号,
' 将配方信息写入到中间变量,
' 将界面上的配方参数写入到中间变量,
' 将中间变量的值写入到用户归档中新增一条记录,
' 保存配方并不会修改已存在的配方,始终是增加一个新版本的配方。
'--------------------------
'检查是否选择配方
If ScreenItems("ComboRecipeList").text="" Or ScreenItems("ComboEditionList").text="" Then
MsgBOX TranslateText("没有选择正确的配方,不能保存",2052)
Exit Sub
End If
'电子签名
Dim sComments
If EsigDialog(sComments,False,"") <> 1 Then
Exit Sub
End If
'创建数据库对象
Dim cnn, rs
Set cnn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
'查找数据库中配方的最大版本号
Dim intMaxID,i
intMaxID=0
Dim SQL
SQL = "select Recipe_Edition from "& DSN &" where Recipe_No='" & RecipeNo & "'"
ConnectDatabase cnn,rs,SQL
If rs.recordcount>0 Then
rs.MoveFirst
For i = 1 To rs.recordcount
If intMaxID<CInt(Mid(rs("Recipe_Edition"),1)) Then ' 查找最大配方版本号
intMaxID=CInt(Mid(rs("Recipe_Edition"),1))
End If
rs.MoveNext
Next
End If
'关闭数据库连接
rs.Close
cnn.Close
Set rs = Nothing
Set cnn = Nothing
'计算配方下一版本号
NextRecipeEdition = intMaxID + 1
'清空中间变量的已删除和已生产标志
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_IsProduced").Write 0
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_IsDeleted").Write 0
'获取配方ID
Dim recipeID
recipeID = ScreenItems("ComboRecipeList").text &"_"& NextRecipeEdition
'将界面显示的配方信息写入内部变量
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeNo").Write ScreenItems("ComboRecipeList").text
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeEdition").Write NextRecipeEdition
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeName").Write ScreenItems("Recipe_Name").text
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeDescription").Write ScreenItems("Recipe_Description").text
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeID").Write recipeID
'将界面显示的配方参数写入内部变量
Dim objName
For Each objName In objNameList
If Trim(ScreenItems(objName(0)).text) = "" Then
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_"& objName(1)).Write 0
Else
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_"& objName(1)).Write ScreenItems(objName(0)).text
End If
Next
'保存配方,把变量值写入到数据库
If Insert_UA("@NOP::" &tagPrefix& "UA_Recipe") <> 0 Then
Msgbox TranslateText("保存失败!",2052)
Exit Sub
End If
'记录Audit
CreateOpMsg PrefixToName(tagPrefix), TranslateText("保存配方",2052)&recipeID , "", recipeID, sComments
'更新配方编号控件中的内容
Call WriteRecipeNoToComboBox(DSN, "ComboRecipeList")
'向界面控件添加配方版本
ScreenItems("ComboEditionList").AddItem NextRecipeEdition
ScreenItems("ComboEditionList").text = NextRecipeEdition
Msgbox TranslateText("保存升级成功",2052)
'设置按钮状态
ScreenItems("BT_New").Enabled=True
ScreenItems("BT_Download").Enabled=True
ScreenItems("BT_Delete").Enabled=True
ScreenItems("BT_Save").Enabled=False
ScreenItems("ComboRecipeList").Enabled=True
End Sub
'*******************************************************************************************************************************************
'下载配方
Sub DownloadRecipe(tagPrefix, DSN, objNameList)
'---------------------------------------
' 下载配方:
' 从用户归档中读取配方到中间变量,
' 再把配方从中间变量写入到外部变量。
'---------------------------------------
' If HMIRuntime.Tags("A_CommunicationFail").Read=1 Then
' MsgBOX "通讯失败,不能下载"
' Exit Sub
' End If
'检查是否选择配方
If ScreenItems("ComboRecipeList").text="" Or ScreenItems("ComboEditionList").text="" Then
MsgBOX TranslateText("没有选择正确的配方,不能下载!",2052)
Exit Sub
End If
'电子签名
Dim sComments
If EsigDialog(sComments,False,"") <> 1 Then
Exit Sub
End If
'计算配方ID
Dim RecipeID
RecipeID = ScreenItems("ComboRecipeList").text&"_"&ScreenItems("ComboEditionList").text
'更新中间变量
If Select_UA( "@NOP::" &tagPrefix& "UA_Recipe","Recipe_ID",RecipeID) <> 0 Then
MsgBOX TranslateText("获取配方失败!",2052)
Exit Sub
end if
'将配方从中间变量写入到外部变量
Dim objName
For Each objName In objNameList
HMIRuntime.Tags("@NOP::" &tagPrefix& objName(1)).Write HMIRuntime.Tags("@NOP::" &tagPrefix& "M_"&objName(1)).Read
Next
HMIRuntime.Tags("@NOP::" &tagPrefix& "RecipeNo").Write HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeNo").Read
HMIRuntime.Tags("@NOP::" &tagPrefix& "RecipeEdition").Write HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeEdition").Read
HMIRuntime.Tags("@NOP::" &tagPrefix& "RecipeName").Write HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeName").Read
HMIRuntime.Tags("@NOP::" &tagPrefix& "RecipeDescription").Write HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeDescription").Read
HMIRuntime.Tags("@NOP::" &tagPrefix& "RecipeID").Write HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeID").Read
'记录Audit
CreateOpMsg PrefixToName(tagPrefix), TranslateText("下载配方",2052)&recipeID , "", recipeID, sComments
'设置按钮状态
ScreenItems("BT_New").Enabled=True
ScreenItems("BT_Download").Enabled=True
ScreenItems("BT_Delete").Enabled=True
ScreenItems("ComboRecipeList").Enabled=True
Msgbox TranslateText("下载成功",2052)
End Sub
'*******************************************************************************************************************************************
'上载配方
Sub Upload(tagPrefix, DSN, objNameList)
'检查是否有配方正在编辑
If ScreenItems("BT_Save").Enabled=True Then
If Msgbox (TranslateText("配方正在编辑,确定不保存该配方,继续上载配方吗?",2052),vbQuestion+vbOKCancel,"Note") = vbCancel Then
Exit Sub
End If
End If
'检查是否选择了配方编号,询问是否上传到当前配方编号并新建配方版本。
Dim NewRecipeNoFlag
If Trim(ScreenItems("ComboRecipeList").text) <> "" And _
Trim(ScreenItems("Recipe_Name").text) <> "" Then ' 判断是否沿用当前选择的配方编号
Select Case MsgBOX( TranslateText("是否上传到当前配方编号并更新配方版本?",2052), vbYesNoCancel + vbQuestion)
Case vbYes
NewRecipeNoFlag = False
Case vbno
NewRecipeNoFlag = True
Case Else
Exit Sub
End Select
Else
NewRecipeNoFlag = True
End If
'电子签名
Dim sComments
If EsigDialog(sComments,False,"") <> 1 Then
Exit Sub
End If
'创建数据库对象
Dim cnn, rs, SQL
Set cnn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
Dim RecipeNo, RecipeName, RecipeDescription
If NewRecipeNoFlag = True Then ' 输入新配方编号和配方名称
Do While True
RecipeNo = Trim(InputBox(TranslateText("请输入新配方编号:",2052),"Note"))
'检查输入的配方编号是否符合要求
If RecipeNo="" Or Check_LawlessChar(RecipeNo)=True Then
If MsgBox (TranslateText("配方编号只能包含字母和数字,请重新输入!",2052),vbRetryCancel + vbInformation,"Note") = vbCancel Then
Exit Sub
Else
'如果选择重试则继续执行循环
End If
else
'检查配方编号是否重复
SQL = "select Recipe_No from "& DSN &" where Recipe_No='" & RecipeNo & "' AND IsDeleted = 0"
ConnectDatabase cnn,rs,SQL
If rs.recordcount>0 Then
If MsgBox (TranslateText("配方编号重复!",2052),vbRetryCancel + vbInformation,"Note") = vbCancel Then
rs.Close
cnn.Close
Exit Sub
else
'如果选择重试则继续执行循环
End If
Else
rs.Close
cnn.Close
Exit Do
End If
End If
Loop
'输入配方名称
Do While True
RecipeName= Trim(InputBox(TranslateText("请输入新配方名称:",2052),"Note"))
If RecipeName="" Then
If MsgBox (TranslateText("配方名称不能为空!",2052),vbRetryCancel + vbInformation,"Note") = vbCancel Then
Exit Sub
else
'如果选择重试则继续执行循环
End If
Else
Exit Do
End If
Loop
Else '使用当前配方编号和配方名称
RecipeNo = Trim(ScreenItems("ComboRecipeList").text)
RecipeName = Trim(ScreenItems("Recipe_Name").text)
End If
'输入配方描述
RecipeDescription = Trim(InputBox(TranslateText("请输入新配方描述(可以为空):",2052),"Note"))
'查找特定配方编号下最大版本号
Dim intMaxID,i
intMaxID = 0
SQL = "select Recipe_Edition from "& DSN &" where Recipe_No='" & RecipeNo & "'"
ConnectDatabase cnn,rs,SQL
If rs.recordcount>0 Then
rs.MoveFirst
For i = 1 To rs.recordcount
If intMaxID<CInt(Mid(rs("Recipe_Edition"),1)) Then ' 查找最大配方版本号
intMaxID=CInt(Mid(rs("Recipe_Edition"),1))
End If
rs.MoveNext
Next
End If
'设置下一版本号
NextRecipeEdition = intMaxID + 1
'计算配方ID
Dim recipeID
recipeID = RecipeNo &"_"& NextRecipeEdition
'将配方参数从外部变量写入到中间变量
Dim objName
For Each objName In objNameList
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_"&objName(1)).Write HMIRuntime.Tags("@NOP::" &tagPrefix& objName(1)).Read
Next
'清空中间变量已删除和已生产标志
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_IsProduced").Write 0
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_IsDeleted").Write 0
'将输入的配方信息写入内部变量
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeNo").Write RecipeNo
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeEdition").Write NextRecipeEdition
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeName").Write RecipeName
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeDescription").Write RecipeDescription
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeID").Write recipeID
'从中间变量写入到用户归档
If Insert_UA("@NOP::" &tagPrefix& "UA_Recipe") <> 0 Then
Msgbox TranslateText("上载失败!",2052)
Exit Sub
End If
'记录Audit
CreateOpMsg PrefixToName(tagPrefix), TranslateText("上载配方",2052)&recipeID , "", recipeID, sComments
'更新控件
Call WriteRecipeNoToComboBox(DSN, "ComboRecipeList")
ScreenItems("ComboRecipeList").text = RecipeNo
ScreenItems("ComboEditionList").text = ""
Msgbox TranslateText("上载成功并已保存",2052)
ScreenItems("BT_New").Enabled=True
ScreenItems("BT_Download").Enabled=True
ScreenItems("BT_Delete").Enabled=True
ScreenItems("ComboRecipeList").Enabled=True
ScreenItems("BT_Save").Enabled=False
End Sub
'*******************************************************************************************************************************************
'删除配方
Sub DeleteRecipe(tagPrefix, DSN, objNameList)
'--------------------------------------
' 删除配方:
' 检查需删除的配方是否生产过,
' 生产过的配方将其IsDeleted字段置1,
' 之后查询时不再显示该配方,
' 打印报表时依然可以打印该配方;
' 未生产过的配方则被直接删除。
'-------------------------------------
'检查是否选择配方
If ScreenItems("ComboRecipeList").text="" Or ScreenItems("ComboEditionList").text="" Then
MsgBOX TranslateText("没有选择正确的配方!",2052)
Exit Sub
End If
'电子签名
Dim sComments
If EsigDialog(sComments,False,"") <> 1 Then
Exit Sub
End If
'计算配方ID
Dim RecipeID
RecipeID = ScreenItems("ComboRecipeList").text&"_"&ScreenItems("ComboEditionList").text
'更新中间变量
If Select_UA ("@NOP::" &tagPrefix& "UA_Recipe","Recipe_ID", RecipeID ) <> 0 Then
MsgBOX TranslateText("读取配方失败!",2052)
exit Sub
end if
'删除配方
If HMIRuntime.Tags("@NOP::" &tagPrefix& "M_IsProduced").Read = 1 Then
HMIRuntime.Tags("@NOP::" &tagPrefix& "M_IsDeleted").Write 1
If Update_UA ("@NOP::" &tagPrefix& "UA_Recipe", "Recipe_ID", RecipeID ) <> 0 Then
Msgbox TranslateText("删除配方失败!",2052)
exit sub
end if
Else
If Delete_UA ("@NOP::" &tagPrefix& "UA_Recipe","Recipe_ID",RecipeID ) <> 0 Then
Msgbox TranslateText("删除配方失败!",2052)
exit sub
end if
End If
'记录Audit
CreateOpMsg PrefixToName(tagPrefix), TranslateText("删除配方",2052)&recipeID , "", recipeID, sComments
'清空配方控件内容
Dim objName
For Each objName In objNameList
ScreenItems(objName(0)).text = ""
Next
ScreenItems("Recipe_Name").text = ""
ScreenItems("Recipe_Description").text = ""
ScreenItems("ComboRecipeList").text = ""
ScreenItems("ComboEditionList").clear
ScreenItems("ComboEditionList").text = ""
'更新控件中的配方编号
Call WriteRecipeNoToComboBox(DSN, "ComboRecipeList")
'设置按钮状态
ScreenItems("BT_New").Enabled=True
ScreenItems("BT_Download").Enabled=False
ScreenItems("BT_Delete").Enabled=False
Msgbox TranslateText("删除成功",2052)
End Sub
'*******************************************************************************************************************************************
'获取配方版本
Sub GetRecipeEdition(tagPrefix, DSN, objNameList)
'------------------------------------------------
' 查询数据库中特定配方编号的版本,写入到下拉控件。
'------------------------------------------------
'创建数据库对象
Dim cnn,rs,cnnStr, SQL,i
Set cnn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
'查询特定配方编号的数据
SQL = "select Recipe_Edition from "& DSN &" where Recipe_No='" & ScreenItems("ComboRecipeList").text & "' AND IsDeleted = 0"
ConnectDatabase cnn,rs,SQL
'将数据库中存在的配方版本填入控件
ScreenItems("ComboEditionList").clear
If rs.RecordCount>0 Then
rs.MoveFirst
For i = 1 To rs.RecordCount
ScreenItems("ComboEditionList").AddItem rs("Recipe_Edition")
rs.MoveNext
Next
End If
'关闭数据库连接
rs.Close
Set rs = Nothing
cnn.Close
Set cnn = Nothing
'清空参数控件内容
For Each objName In objNameList
ScreenItems(objName(0)).text = ""
Next
'清空配方信息控件内容
ScreenItems("Recipe_Name").text = ""
ScreenItems("Recipe_Description").text = ""
'设置按钮状态
ScreenItems("BT_New").Enabled=True
ScreenItems("BT_Delete").Enabled=False
ScreenItems("BT_Save").Enabled=False
ScreenItems("BT_Download").Enabled=False
End Sub
'*******************************************************************************************************************************************
'获取配方
Sub GetRecipe(tagPrefix, DSN, objNameList)
'----------------------------------------------
' 用配方编号和配方版本拼接得到配方ID,
' 用配方ID指定配方写入到中间变量,
' 再将中间变量的内容写入到控件去显示。
'----------------------------------------------
'获取配方ID
Dim RecipeID
RecipeID = ScreenItems("ComboRecipeList").text &"_"& ScreenItems("ComboEditionList").text
'将配方从数据库写入到中间变量
If Select_UA("@NOP::" &tagPrefix& "UA_Recipe","Recipe_ID",RecipeID) <> 0 Then
Msgbox TranslateText("读取配方失败!",2052)
Exit Sub
End If
'将配方从中间变量写入到控件
Dim objName
For Each objName In objNameList
ScreenItems(objName(0)).text = HMIRuntime.Tags("@NOP::" &tagPrefix& "M_"&objName(1)).Read
Next
'将配方信息从中间变量写入到控件
ScreenItems("Recipe_Name").text = HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeName").Read
ScreenItems("Recipe_Description").text = HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeDescription").Read
'设置按钮状态
ScreenItems("BT_New").Enabled=True
ScreenItems("BT_Delete").Enabled=True
ScreenItems("BT_Save").Enabled=False
ScreenItems("BT_Download").Enabled=True
End Sub
依赖函数
上述代码中依赖一些其他的函数。
TranslateText()
TranslateText()函数用于脚本内文本多语言翻译,函数的设计和使用详见:WinCC脚本内文本多语言化的一种方法
Check_LawlessChar()
输入配方编号要求为字母和数据,Check_LawlessChar()函数通过正则表达式判断输入的字符是否符合要求,符合则返回1,不符合返回0。代码如下:
Function Check_LawlessChar(strName)
Dim regEx
Set regEx = New RegExp
regEx.Pattern = "^[a-z0-9A-Z]+$"
regEx.IgnoreCase = True
regEx.Global = True
Validate = regEx.test(strName)
Set regEx = Nothing
Check_LawlessChar = Not Validate
End Function
ConnectDatabase()
ConnectDatabase()函数用于简化数据库连接,代码如下:
Function ConnectDatabase(cnn,rs,SQL)
Dim cnnStr
cnnStr = "Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog="& HMIRuntime.Tags("@NOTP::@DatasourceNameRT").Read &";Data Source=" & HMIRuntime.Tags("@NOTP::@ServerName").Read & "WINCC"
cnn.ConnectionString = cnnStr
cnn.CursorLocation = 3
cnn.Open
rs.Open SQL,cnn,1,3
End Function
EsigDialog()和CreateOpMsg()
EsigDialog()和CreateOpMsg()分别用于生产电子签名和记录审计追踪,详见:WinCC的电子签名与审计追踪 2.0
PrefixToName()
审计追踪需要记录设备名称,已知设备的变量前缀,可以得到对应的变量名称,PrefixToName()就是为了实现这个作用,但要根据项目修改和添加一下代码:
Function PrefixToName(Prefix)
Select Case Trim(Prefix)
Case "XX_"
PrefixToName = TranslateText("设备1", 2052)
Case "XX1_"
PrefixToName = TranslateText("设备2", 2052)
Case ""
PrefixToName = "System"
Case Else
PrefixToName = Prefix
End Select
End Function
画面全局声明区定义
将函数所需要的三个参数定义在画面的全局声明区中,在控件的脚本中就可以直接使用。
打开全局声明区只需打开画面中任意一个VBS编辑器,点击下图中红色方框的按钮,就可显示全局声明区。

全局声明区代码示例如下:
'数据库名称
Dim DSN
DSN = "UA#XX_Recipe"
'控件名称数据库字段名,变量名称
Dim objNameList
objNameList = Array(_
Array("i16_Set_VfFaninfeed", "i16_Set_VfFaninfeed"),_
Array("i16_Set_VfFanPreheat", "i16_Set_VfFanPre-heat"),_
Array("i16_Set_VfFan1Heat", "i16_Set_VfFan1#Heat"),_
Array("i16_Set_VfFan1Cool", "i16_Set_VfFan1Cool"),_
Array("i16_Set_VfFan2Cool", "i16_Set_VfFan2Cool"),_
Array("Recipe_Setbeltspeed", "Recipe_Setbeltspeed"),_
Array("Recipe_Setheat2", "Recipe_Setheat2"),_
Array("Sv1Height_Upperlimit", "r32_Set_Sv1Height_Upperlimit"),_
Array("Sv2Height_Upperlimit", "r32_Set_Sv2Height_Upperlimit"),_
Array("Sv3Height_Upperlimit", "r32_Set_Sv3Height_Upperlimit") _
)
'变量前缀
Dim TagPrefix
TagPrefix = "XX_"
操作批次的函数执行过程
点击新建、保存、下载、上载、删除按钮以及查询配方,执行流程如下:

新建配方
新建按钮中的脚本如下:
Sub OnClick(Byval Item)
NewRecipe TagPrefix, DSN, objNameList
End Sub
点击“新建”按钮,调用NewRecipe()函数,将会执行以下操作:
- 检查“BT_Save”按钮是否使能,判断当前是否有正在编辑的配方没保存,弹出提示框询问用户是否继续操作;
- 弹出对话框要求输入配方编号、配方名称和配方描述,检查输入的配方编号是否符合要求,和是否与现有的配方编号重复;
- 清空界面中参数控件的值,向配方信息控件写入用户输入的值;
- 更新按钮的使能状态。
保存配方
保存按钮中的脚本如下:
Sub OnClick(Byval Item)
SaveRecipe TagPrefix, DSN, objNameList
End Sub
点击“保存”按钮,调用SaveRecipe()函数,将会执行以下操作:
- 检查是否有配方编号和配方版本;
- 验证电子签名;
- 查找数据库中该配方编号的最大版本,计算得到下一配方版本;
- 清空中间变量;
- 向中间变量写入界面上显示的值;
- 将中间变量的值保存到用户归档;
- 记录审计追踪;
- 更新界面上配方版本控件的值;
- 更新按钮的使能状态。
下载配方
下载按钮中的脚本如下:
Sub OnClick(Byval Item)
DownloadRecipe TagPrefix, DSN, objNameList
End Sub
点击“下载”按钮,调用DownloadRecipe()函数,将会执行以下操作:
- 检查是否选择了一个配方;
- 验证电子签名;
- 通过配方编号和配方版本计算得到配方ID;
- 用配方ID从用户归档查询配方值写入到中间变量;
- 将中间变量的写入到外部变量;
- 记录审计追踪;
- 更新按钮的使能状态。
上载配方
上载按钮中的脚本如下:
Sub OnClick(Byval Item)
Upload TagPrefix, DSN, objNameList
End Sub
点击“上载”按钮,调用Upload()函数,将会执行以下操作:
- 检查“BT_Save”按钮是否使能,判断当前是否有正在编辑的配方没保存,弹出提示框询问用户是否继续操作;
- 如果当前界面已选择了配方,询问用户是否上载到当前配方编号;
- 验证电子签名;
- 如果需新建配方编号,弹出对话框要求输入配方编号、配方名称和配方描述,检查输入的配方编号是否符合要求,和是否与现有的配方编号重复;
- 如果使用当前配方编号,则只需输入新的配方描述;
- 查找数据库中该配方编号的最大版本,计算得到下一配方版本;
- 把外部变量的值写入到中间变量;
- 把控件中的配方信息写入到中间变量;
- 把中间变量的值保存到用户归档;
- 记录审计追踪;
- 更新配方编号和配方版本控件的值;
- 更新按钮的使能状态。
删除配方
删除按钮中的脚本如下:
Sub OnClick(Byval Item)
DeleteRecipe TagPrefix, DSN, objNameList
End Sub
点击“删除”按钮,调用DeleteRecipe()函数,将会执行以下操作:
- 检查是否选择了一个配方;
- 验证电子签名;
- 通过配方编号和配方版本计算得到配方ID;
- 用配方ID从用户归档查询配方值写入到中间变量;
- 检查“M_IsProduced”是否为1,判断该配方是否被生产过,以确定是否要删除该配方;
- 如果“M_IsProduced”是否为1,则把“M_IsDeleted”变量置1,更新用户归档中的值;
- 如果“M_IsProduced”是否为0;则直接在用户归档中删除该配方;
- 记录审计追踪;
- 更新配方编号控件的值。
- 更新按钮的使能状态。
编辑配方
编辑配方有以下2点要求:
- 能告知被编辑的参数的最大值和最小值,输入的值不能超过最大值和最小值;
- 编辑在线配方值时要求有电子签名和审计追踪。
要满足上述要求,就不能直接在Wincc的输入输出域控件里输入值,需要设计一个输入界面显示参数的最大值和最小值,用脚本检查输入的是否在范围之内,编辑在线配方值时还要进行电子签名。输入界面如下,点击数值控件时以对话框形式弹出,类似于屏幕键盘(以下称作屏幕键盘界面)。

屏幕键盘界面是个通过画面窗口调用的画面,这个画面窗口平常隐藏,需要输入配方值时通过调用函数显示画面窗口,编辑数据库配方值和在线配方值用的是不同的调用函数。
编辑数据库配方值时调用KeyboardDispScreen()函数显示屏幕键盘界面,示例如下:
Sub OnClick(ByVal Item)
KeyboardDispScreen AccessPath, Item.ObjectName, 0, 50, "配方变量:进瓶风机频率设定(Hz)"
End Sub
编辑在线配方值时调用KeyboardDispTag()函数显示屏幕键盘界面,示例如下:
Sub OnClick(Byval Item)
KeyboardDispTag parent.TagPrefix, "i16_Set_VfFaninfeed", 0, 50, Item.TooltipText
End Sub
屏幕键盘界面通过位于根画面的画面窗口控件引用,画面窗口控件名为“Keyboard”。屏幕键盘界面中有四个隐藏的文本控件,分别用于存储变量前缀、变量名、画面路径、控件名称,然后屏幕键盘界面里的代码就可以使用这几个值。KeyboardDispScreen()函数只传递画面路径和控件名称,KeyboardDispTag()函数只传递变量前缀和变量名。
KeyboardDispScreen()函数代码如下:
'---------------------------------------------------------------------------------
' ScreenPath:调用函数的控件所在的画面路径
' ItemName :调用控件的控件名
' MinVal :最小值
' MaxVal :最大值
' TagNote :被编辑的参数的描述
'---------------------------------------------------------------------------------
Function KeyboardDispScreen(ScreenPath,ItemName,MinVal,MaxVal,TagNote)
Dim objScrKeyboard,objScrWindow
Set objScrKeyboard=HMIRuntime.Screens(BaseScreenName).ScreenItems("Keyboard")
objScrKeyboard.CaptionText=TagNote'TagName
objScrKeyboard.Visible=True
Set objScrWindow = objScrKeyboard.Screen
objScrWindow.ScreenItems("ScreenPath").Text = ScreenPath
objScrWindow.ScreenItems("ItemName").Text = ItemName
objScrWindow.ScreenItems("ET_MinValue").OutputValue = MinVal
objScrWindow.ScreenItems("ET_MaxValue").OutputValue = MaxVal
End Function
KeyboardDispTag()函数代码如下:
'---------------------------------------------------------------------------------
' Prefix :变量的前缀
' ItemName :变量名称
' MinVal :最小值
' MaxVal :最大值
' TagNote :被编辑的变量的描述
'---------------------------------------------------------------------------------
Function KeyboardDispTag(Prefix,TagName,MinVal,MaxVal,TagNote)
Dim objScrKeyboard,objScrWindow
Set objScrKeyboard=HMIRuntime.Screens(BaseScreenName).ScreenItems("Keyboard")
objScrKeyboard.CaptionText=TagNote'TagName
objScrKeyboard.Visible=True
Set objScrWindow = objScrKeyboard.Screen
objScrWindow.ScreenItems("Prefix").Text = Prefix
objScrWindow.ScreenItems("TagName").Text = TagName
objScrWindow.ScreenItems("ET_MinValue").OutputValue = MinVal
objScrWindow.ScreenItems("ET_MaxValue").OutputValue = MaxVal
End Function
存储变量前缀、变量名、画面路径、控件名称的四个控件获得值后,就会脚本写入到全局声明的变量,便于之后使用。

“确认”按钮中的代码如下,如果控件名称(ItemName)变量不为空,就执行写入到界面控件的脚本,其中第36行至53行代码会将调用控件同一个画面的某些按钮设为启用或禁用。
如果变量名称(TagName)变量不为空,验证电子签名后就会执行写入到变量的代码。
Sub OnClick(Byval Item)
Dim objScrKeyboard, InputValue, MaxValue, MinValue, OutputValue
' 获取画面窗口控件
Set objScrKeyboard = Parent
'检查输入内容是否为空
If Trim(ScreenItems("Txt_Value").Text)="" Then
objScrKeyboard.Visible=False
Exit Sub
End If
'获取数据值和限制值
InputValue=CSng(ScreenItems("Txt_Value").Text)
MaxValue=CSng(ScreenItems("ET_MaxValue").OutputValue)
MinValue=CSng(ScreenItems("ET_MinValue").OutputValue)
'检查值范围
If InputValue>MaxValue Then
OutputValue=MaxValue
Elseif InputValue<MinValue Then
OutputValue=MinValue
Else
OutputValue=InputValue
End If
If ItemName <> "" Then
Dim objScrEdit
'获取画面对象
Set objScrEdit=HMIRuntime.Screens(ScreenPath)
'写入值
objScrEdit.ScreenItems(ItemName).text = CStr(OutputValue)
'更新按钮状态
Dim EnableButtuns, DisableButtuns, ButttunName
EnableButtuns = Array("BT_Save")
DisableButtuns = Array("BT_Download", "BT_Delete")
Dim ItemNameTemp
For Each ItemNameTemp In objScrEdit.ScreenItems
If ItemNameTemp.Type = "HMIButton" Then
For Each ButttunName In EnableButtuns
If ItemNameTemp.ObjectName = ButttunName Then
ItemNameTemp.Enabled = True
End If
Next
For Each ButttunName In DisableButtuns
If ItemNameTemp.ObjectName = ButttunName Then
ItemNameTemp.Enabled = False
End If
Next
End If
Next
End If
If TagName <> "" Then
'电子签名,写入值
Dim TagNote
TagNote = parent.CaptionText
TagNewValueES PrefixToName(Prefix), TagNote, Prefix & TagName, OutputValue
'HMIRuntime.Tags(Prefix & TagName).Write OutputValue
End If
objScrKeyboard.Visible=False
End Sub
