配方是給機器設定的一組運行參數,當機器要生產不同規格的產品時,可以給機器設定不同的參數。
配方的設計要點之一是如何保存配方。制葯行業要求對生產數據一般保存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

