Overview
Siemens WinCC exposes a per-property C-Action that fires every time a configured object property changes value. The script in this reference is a canonical OnPropertyChanged handler that reads three user-defined configuration properties from a custom WinCC object, evaluates a boolean expression, and writes the result to the two visibility properties @On_Vis and @Off_Vis. The exact line that confuses engineers is the nested equality comparison:
Result = ((Neg == NegRes) == Signal);
The expression is valid ISO C, has no precedence trickery, and reduces to a single boolean that can be rendered in a truth table. This article decodes that line, explains why GetPropBOOL is used in this specific handler instead of GetTagBit, and provides a refactored version that reads an entire PLC error word at once instead of polling single bits. The target audience is the WinCC V7.x and WinCC TIA (Comfort/Professional) engineer who has just inherited a custom object library and needs to understand, modify, or troubleshoot visibility logic on HMI screens.
Prerequisites
- WinCC V7.4 SP3 / V7.5 SP2 / V7.5 SP4 or TIA Portal WinCC Professional V16-V18 with the C-Script option installed.
- Graphics Designer license with the C-Editor add-in enabled (Options > Settings > C-Script).
- A configured custom WinCC object exposing the user-defined properties
Signal,Sig_neg, andNegResas typeBOOL. - Knowledge of the C-Action trigger model: scripts fire on property change events, not on tag polling unless a trigger tag is explicitly configured.
- Knowledge of the WinCC tag namespace: internal tags, process tags, and structure tags (see the WinCC Information System under Configuration > Tags).
apdefap.h declares the WinCC API prototypes (GetPropBOOL, SetPropBOOL, GetTagBit, GetTagWord, SetTagBit, and so on). If the script is recompiled outside Graphics Designer, that header must be on the include path or the build will fail with unresolved externals.The C-Action Source Under Review
#include "apdefap.h"
void OnPropertyChanged(char* lpszPictureName,
char* lpszObjectName,
char* lpszPropertyName,
BOOL value)
{
BOOL Signal, Neg, NegRes, Result;
Signal = GetPropBOOL(lpszPictureName, lpszObjectName, "Signal");
Neg = GetPropBOOL(lpszPictureName, lpszObjectName, "Sig_neg");
NegRes = GetPropBOOL(lpszPictureName, lpszObjectName, "NegRes");
Result = ((Neg == NegRes) == Signal);
SetPropBOOL(lpszPictureName, lpszObjectName, "@On_Vis", Result);
SetPropBOOL(lpszPictureName, lpszObjectName, "@Off_Vis", !Result);
}
Three custom properties (Signal, Sig_neg, NegRes) act as configuration inputs. The script computes Result from those inputs and assigns it to the built-in visibility properties @On_Vis and @Off_Vis. The @ prefix is the WinCC convention for system properties; the suffix _Vis marks the visibility attribute of the picture's ON and OFF states.
C Operator Semantics: == vs =
The first source of confusion is operator choice. C has two completely different operators that look almost identical in proportional fonts:
| Operator | Name | Result | Typical Use |
|---|---|---|---|
= |
Assignment | Value of the right-hand side | Write a value into a variable |
== |
Equality comparison | 1 (TRUE) or 0 (FALSE) | Test whether two values match |
!= |
Inequality | 1 (TRUE) or 0 (FALSE) | Test whether two values differ |
In the line Result = ((Neg == NegRes) == Signal);, the inner and outer == are both comparisons that yield 0 or 1. The single = in the outermost position is the assignment that stores that 0 or 1 into Result. There is no precedence trick: == has left-to-right associativity, so the expression is parsed as ((Neg == NegRes) == Signal) exactly as written, with no implicit grouping surprise.
if (Signal = TRUE) pattern. In C, = is a valid expression that returns the assigned value, so the branch always fires. WinCC V7.x with the strict compiler option turned on (Project Properties > C-Editor > Compiler Options > Warning Level 3) will warn about this; the TIA Portal C-Editor by default does not.Decoding the Boolean Expression
The expression ((Neg == NegRes) == Signal) can be expanded into a truth table. The intermediate == produces a 1 when Neg and NegRes match, and a 0 when they differ. The outer == then tests whether that 1-or-0 matches the value of Signal. The two inputs Sig_neg and NegRes together behave like a logical XOR enable: they are typically wired to two separate "invert" configuration flags.
| Neg (Sig_neg) | NegRes | Signal | Inner (Neg == NegRes) | Result |
|---|---|---|---|---|
| 0 | 0 | 0 | 1 | 0 |
| 0 | 0 | 1 | 1 | 1 |
| 0 | 1 | 0 | 0 | 1 |
| 0 | 1 | 1 | 0 | 0 |
| 1 | 0 | 0 | 0 | 1 |
| 1 | 0 | 1 | 0 | 0 |
| 1 | 1 | 0 | 1 | 0 |
| 1 | 1 | 1 | 1 | 1 |
Reading the right-hand column: Result is TRUE only when Signal agrees with the agreement of Neg and NegRes. The pattern is the standard implementation of a configurable inverted logic gate. The four boolean inputs (Signal, Sig_neg, NegRes, Result) cover all 16 possible states of three inputs, of which 8 yield Result = TRUE and 8 yield Result = FALSE. The implementation is a multiplexer that picks between Signal and !Signal based on the XOR of Sig_neg and NegRes.
GetPropBOOL vs GetTagBit vs GetTagWord
The source uses GetPropBOOL, which is the correct choice for configuration data bound to a custom object's user-defined properties. GetTagBit is the right call when the script needs a runtime process value from a tag, and GetTagWord is the right call when many bits live inside a single WORD and the script wants to avoid a per-bit round trip to the data manager.
| Function | Reads From | Use When | Typical Cost |
|---|---|---|---|
GetPropBOOL |
Object's user-defined property | Reading static config such as invert flags | Local, no datamanager |
GetTagBit |
Single BOOL tag in datamanager | Reading one live process bit | 1 datamanager lookup |
GetTagByte |
BYTE tag | Reading 8 packed bits at once | 1 datamanager lookup |
GetTagWord |
WORD tag | Reading 16 packed bits at once | 1 datamanager lookup |
GetTagDWord |
DWORD tag | Reading 32 packed bits at once | 1 datamanager lookup |
Every call to GetTagBit enters the WinCC datamanager, acquires the data lock, copies the value, and releases the lock. In a script that runs every 250 ms and reads eight error bits, that is eight locks and copies per cycle. Reading the underlying WORD once and then masking the bits with C bitwise operators (&, |, <<, >>) collapses that into one lookup. The refactored block from the source is reproduced and expanded below.
The Property System Behind the Script
Three custom properties drive the logic, and two built-in system properties carry the output.
| Property Name | Direction | Type | Source | Purpose |
|---|---|---|---|---|
Signal |
Input | BOOL | User-defined (config) | The live or configuration signal value |
Sig_neg |
Input | BOOL | User-defined (config) | First inversion flag |
NegRes |
Input | BOOL | User-defined (config) | Second inversion flag |
@On_Vis |
Output | BOOL | WinCC system | Visibility of the ON-state graphics |
@Off_Vis |
Output | BOOL | WinCC system | Visibility of the OFF-state graphics |
User-defined properties are added in the Graphics Designer by opening the object's properties dialog and selecting New under the Properties tab. WinCC stores them in the picture's .pdl file as a serialized structure; GetPropBOOL is the only public C API to read them at runtime. The system properties @On_Vis and @Off_Vis are evaluated by the WinCC Picture Renderer after every cycle; assigning to them in a C-Action takes effect on the next screen update.
Best Practice Refactor: Read the Error Word Once
Where the source mentions the common case of "different error bits of the PLC occupying the same byte, word, or double word," the following pattern should be used. The script reads the entire error WORD from the PLC and tests individual bits with a mask.
#include "apdefap.h"
#define ERR_OVERVOLTAGE 0x0001u /* bit 0 */
#define ERR_OVERTEMP 0x0002u /* bit 1 */
#define ERR_OVERCURRENT 0x0004u /* bit 2 */
#define ERR_COMM_FAULT 0x0008u /* bit 3 */
#define ERR_SAFETY 0x0010u /* bit 4 */
BOOL bAnyError = FALSE;
WORD wError = 0;
/* One datamanager call instead of five GetTagBit() calls */
wError = GetTagWord("PLC_DB100_DBW0_ERRORS");
if (wError & ERR_OVERVOLTAGE) { /* overvoltage handling */ }
if (wError & ERR_OVERTEMP) { /* overtemperature */ }
if (wError & ERR_OVERCURRENT) { /* overcurrent */ }
if (wError & ERR_COMM_FAULT) { /* comm fault */ }
if (wError & ERR_SAFETY) { /* safety chain tripped */ }
bAnyError = (wError != 0);
SetPropBOOL(lpszPictureName, lpszObjectName, "@On_Vis", bAnyError);
SetPropBOOL(lpszPictureName, lpszObjectName, "@Off_Vis", !bAnyError);
For a 32-bit status word, use GetTagDWord and DWORD masks with the 0x00000001u notation. For a 64-bit status doubleword (WinCC V7.4 and later), use GetTagDWord on the low and high halves separately and combine. Avoid the deprecated GetTagBitWait / SetTagBitWait family for cyclic scripts, because the Wait variants block the Graphics Designer thread for up to the configured timeout and freeze the HMI when the PLC is offline.
Step-by-Step: Implementing the C-Action on a Custom Object
-
Open the object in Graphics Designer. Right-click the custom object and choose Configuration > Properties. Add three BOOL properties named exactly
Signal,Sig_neg, andNegRes(case-sensitive; WinCC matches property names literally). -
Wire the properties to source tags. For each new property, click the small lightbulb icon, select Tag, and pick the source. For
Signalwire a live process tag such asPLC_DB100.DBX0.0. ForSig_negandNegReseither wire to internal tags_InvertSignaland_InvertResultor set them to constant values in the object default dialog. -
Open the C-Editor on the
@On_Visproperty. Right-click@On_Visin the property list, choose C-Action. The editor opens with the signaturevoid OnPropertyChanged(char*, char*, char*, BOOL)prefilled. - Paste the script. Copy the source body, replacing the function signature with the prefilled one if needed. Save and recompile (F7 in V7.x, the hammer icon in TIA Portal).
-
Set the trigger. Under Trigger in the C-Editor, add the tag(s) that should cause this script to re-fire. For visibility logic, trigger on the source
Signaltag with cycle 250 ms or on a 1-second standard cycle. -
Compile and assign. Confirm there are no errors in the output pane. WinCC V7.x will report C1010 "compiler aborted" if
apdefap.his missing; the resolution is to add the WinCC include path under Options > Settings > C-Editor > Include Paths. -
Runtime check. Start Runtime (RT) and toggle the source
Signaltag. The ON-state graphics should appear and the OFF-state should hide, with both transitions completing within one RT cycle (typically 250 ms).
Verification and Debugging
When the C-Action does not behave as expected, work through the following matrix before assuming a logic bug.
| Symptom | Likely Root Cause | Diagnostic Step | Fix |
|---|---|---|---|
| Visibility never changes at runtime | Trigger tag missing or set to a tag that does not update | Open Tag Diagnosis in RT, confirm trigger tag is refreshing | Add the source tag to the trigger list or switch to a 1 s standard cycle |
| Visibility flickers | Script reads process value directly and trigger cycle is too fast | Check RT logging for re-fire rate | Add a debounce internal tag or use GetTagBit with a 500 ms cycle |
Compile error C2065: GetPropBOOL undeclared |
Missing apdefap.h include path |
Check include paths in C-Editor settings | Add %ProgramFiles(x86)%\Siemens\Automation\WinCC\aplib\ to include paths |
| Compile warning "function should return a value" | Missing return at end of script when not using void signature |
Inspect function signature | Match the signature: OnPropertyChanged is void and must not return a value |
| WinCC freezes when PLC is offline | Script uses GetTagBitWait
|
Grep the script for Wait
|
Replace with GetTagBit or wrap the call in a quality check |
Result always TRUE |
Property name typo, e.g. signal vs Signal
|
Watch the property name in Graphics Designer | Make the property name identical to the string in GetPropBOOL
|
| Object does not show up in library | Custom object saved without .pdl extension |
Check file type in library directory | Save the picture as a custom object via File > Save As Custom Object |
For deep debugging, add a temporary printf redirection through the WinCC logging system. WinCC V7.x routes printf output to the WinCC Diagnosis window when the project is started with the Diagnostic option enabled. In TIA Portal, use LCIDebugTrace() if available, or write a temporary log line into an internal text tag and display it on a diagnostics screen.
Edge Cases and Field-Proven Caveats
-
Boolean width. C's
BOOLin WinCC is a 32-bit integer typedef. Truth tables above used 0/1 notation, but the actual value in memory is 0 or 1, not a single bit. Use the value in integer comparisons if you ever cast toint. -
Property change events are level-triggered, not edge-triggered. If a downstream consumer of
Resultwatches for a transition, set@On_Visdirectly from a tag trigger instead of from this C-Action, because assigning the same value twice in a row does not produce a change event. -
Multi-user editing. When two engineers open the same picture in Graphics Designer, the second saver wins and the C-Action they did not modify is overwritten verbatim. The C-Action is stored as a separate
.pdl_actionrecord, not as part of the picture body, so diffs are not shown in the standard merge tool. Use WinCC's Project Differencer to compare C-Actions across project branches. -
Trigger cycle vs RT cycle. The script fires on the configured trigger (default 1 s). The picture is redrawn on the RT cycle (default 250 ms). The two are decoupled: it is possible to set
@On_Visfaster than the picture redraws, but the user will not see the change. The default values are already chosen to keep CPU load below 20 % on a TIA Runtime PC with a 4-core CPU and 8 GB of RAM. -
Unicode property names. If the project is migrated to WinCC V7.5 with Unicode project names enabled, property names are stored in UTF-16 and the
char*pointer inOnPropertyChangedis the ANSI representation. Custom property names should remain ASCII for the lifetime of the project to avoid migration risk. -
Return semantics in
void OnPropertyChanged. Some script templates show areturnat the end; this is legal in C but unnecessary for avoidfunction. The WinCC C compiler emits warning C4715 "not all control paths return a value" only for non-void functions.
Frequently Asked Questions
What does ((Neg == NegRes) == Signal) actually mean in plain English?
The expression is TRUE only when Signal matches the agreement of the two configuration flags Sig_neg and NegRes. If both flags are equal (both 0 or both 1) the inner == returns 1, and that 1 must equal Signal for the result to be TRUE. If the two flags differ, the inner == returns 0, and Signal must also be 0 for the result to be TRUE.
Why use GetPropBOOL instead of GetTagBit for configuration flags?
GetPropBOOL reads user-defined object properties that are stored inside the picture, not inside the WinCC datamanager. This avoids creating three internal tags purely to act as invert flags, and it makes the custom object portable: it can be dropped into any picture with the same three configuration properties and will work without tag configuration.
Can I read multiple error bits with a single call instead of multiple GetTagBit calls?
Yes. Define the error bits as a WORD or DWORD tag in the PLC, read it once with GetTagWord or GetTagDWord, and then test the individual bits with bitwise masks such as if (wError & 0x0001u). This reduces five datamanager lookups to one and improves RT performance measurably on large projects.
The script gives a compile error that apdefap.h is missing. Where is it?
On a default WinCC V7.5 SP4 install the header is at C:\Program Files (x86)\Siemens\Automation\WinCC\aplib\apdefap.h. In TIA Portal WinCC Professional the same header ships with the installation; add the path under Project > C-Editor > Include Paths. After the include path is set, do a full rebuild of the picture with F7 in V7.x or the hammer icon in TIA.
Is it safe to call SetPropBOOL("@On_Vis", TRUE) from a C-Action on a safety-related tag?
No. @On_Vis only affects graphics rendering, not the underlying safety chain. Use a non-safety display tag and read it from the C-Action; the safety PLC must enforce the actual safety function independently. The C-Action can be disabled at runtime by the operator, so any safety state displayed by it is purely informational.