Auto-Syncing Checkbox States Using Dependent Waves in Igor Pro

Question
I have a panel with four checkboxes: `p` (master), `px`, `py`, and `pz`. The `p` checkbox acts as a **master checkbox**, which controls the others as follows:

  1. Master control: Checking/unchecking `p` should check/uncheck all dependent checkboxes (`px`, `py`, `pz`).
  2. Auto-update master:
  • If any dependent checkbox is unchecked, `p` should uncheck.
  • If all dependent checkboxes are checked, `p` should check.

My current implementation achieves this logic but uses a wave (`pDependentCheckBoxeValues`) to track the dependent checkbox states. However, this wave requires manual updates when the dependent checkboxes change.

How can I make `pDependentCheckBoxeValues` a _dependent wave_ that automatically updates when its source variables (`pxCheckBoxValue`, `pyCheckBoxValue`, `pzCheckBoxValue`) change, without explicitly rewriting it each time?

Current Code:

#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3             // Use modern global access method and strict wave access
#pragma DefaultTab={3,20,4}     // Set default tab width in Igor Pro 9 and later
 
Function Main()
    Initialize()
    Execute "OrbitalSelectionPanel()"
End
 
Function Initialize()
    Variable/G pCheckBoxValue = 0
    Variable/G pxCheckBoxValue = 0
    Variable/G pyCheckBoxValue = 0
    Variable/G pzCheckBoxValue = 0
    Make/O pDependentCheckBoxeValues = {pxCheckBoxValue, pyCheckBoxValue, pzCheckBoxValue}
End
 
Function pCheckBoxProc(cba) : CheckBoxControl
    STRUCT WMCheckboxAction &cba
 
    switch( cba.eventCode )
        case 2: // mouse up
            Variable checked = cba.checked
            NVAR pxCheckBoxValue = root:pxCheckBoxValue
            NVAR pyCheckBoxValue = root:pyCheckBoxValue
            NVAR pzCheckBoxValue = root:pzCheckBoxValue
            WAVE pDependentCheckBoxeValues = pDependentCheckBoxeValues
            pxCheckBoxValue = checked
            pyCheckBoxValue = checked
            pzCheckBoxValue = checked
            pDependentCheckBoxeValues = {pxCheckBoxValue, pyCheckBoxValue, pzCheckBoxValue}
            break
        case -1: // control being killed
            break
    endswitch
 
    return 0
End
 
Function pCheckBoxSync(cba) : CheckBoxControl
    STRUCT WMCheckboxAction &cba
 
    switch( cba.eventCode )
        case 2: // mouse up
            Variable checked = cba.checked
            NVAR pxCheckBoxValue = pxCheckBoxValue
            NVAR pyCheckBoxValue = pyCheckBoxValue
            NVAR pzCheckBoxValue = pzCheckBoxValue
            NVAR pCheckBoxValue = pCheckBoxValue
            WAVE pDependentCheckBoxeValues = pDependentCheckBoxeValues
            pDependentCheckBoxeValues = {pxCheckBoxValue, pyCheckBoxValue, pzCheckBoxValue}
            pCheckBoxValue = sum(pDependentCheckBoxeValues) == numpnts(pDependentCheckBoxeValues)
            break
        case -1: // control being killed
            break
    endswitch
 
    return 0
End
 
Window OrbitalSelectionPanel() : Panel
    PauseUpdate; Silent 1       // building window...
    Variable CheckBoxwidth = 40.00
    Variable CheckBoxheight = 12.00
    Variable VerticalDistance = 20.00
    Variable HorizontalDistance = 40.00;
    Variable XPosition = 20.00
    Variable YPosition = 20.00
    NewPanel /W=(500.0,300.0,800.0,400.0)
    SetDrawLayer UserBack
    CheckBox pCheckBox, pos={XPosition+0*HorizontalDistance,YPosition+1*VerticalDistance},size={CheckBoxwidth,CheckBoxheight},proc=pCheckBoxProc,title="p", variable=pCheckBoxValue,help={"Select all p orbitals"}
    CheckBox pxCheckBox,pos={XPosition+1*HorizontalDistance,YPosition+1*VerticalDistance},size={CheckBoxwidth,CheckBoxheight},proc=pCheckBoxSync,title="px",variable=pxCheckBoxValue
    CheckBox pyCheckBox,pos={XPosition+2*HorizontalDistance,YPosition+1*VerticalDistance},size={CheckBoxwidth,CheckBoxheight},proc=pCheckBoxSync,title="py",variable=pyCheckBoxValue
    CheckBox pzCheckBox,pos={XPosition+3*HorizontalDistance,YPosition+1*VerticalDistance},size={CheckBoxwidth,CheckBoxheight},proc=pCheckBoxSync,title="pz",variable=pzCheckBoxValue
EndMacro

First, I would avoid using global variables and especially waves (which are definitely visible to users) to store control variables. This is not necessary unless you explicitly use the variables / waves for some other task unrelated to the panel. Use

ControlInfo/W=PanelName CtrlName

to inquire the state of each checkbox instead. Call

CheckBox CrtlName ,win=PanelName ,value=setTheValue

to set the value in the panel.

Otherwise, I find no problem with your approach. pCheckBoxSync() is exactly the way to go about this and to implement your conditions. Just set the value of each Checkbox within the mouse-up even while doing (p was pressed => check/uncheck all px,py,pz) and (all px,py,pz active / inactive => adjust p). I think doing it 'automatically' (whatever that means) will just lead to much pain down the road. To discern which of the check-boxes was pressed you can...

  • use StringMatch(cba.ctrlName,XXX)
  • use StringSwitch(cba.ctrlName)
  • use different CheckBoxControl functions for p and all px,py,pz

Hi chozo,

Thank you for taking the time to review my implementation and for your insightful suggestions! Your advice on avoiding global variables/waves and directly querying/setting control states via ControlInfo and CheckBox commands was exactly what I needed to streamline the code. I’ve refactored the solution to:

  1. Eliminate global variables and use panel control states directly.

  2. Simplify logic by unifying checkbox handlers and leveraging WhichListItem() for dynamic checks.

  3. Improve robustness by enforcing panel-name constants and consistent loops.

Your point about avoiding "automatic" dependencies (like wave-based tracking) was particularly valuable—it led me to a cleaner, more maintainable approach. The final version now handles all sync logic within pCheckBoxSync(), triggered by a single CheckBoxControl function.

I’ve shared the refined code below in case it’s useful for others. Thanks again for your guidance—it’s greatly appreciated!

 

#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3             // Use modern global access method and strict wave access
#pragma DefaultTab={3,20,4}     // Set default tab width in Igor Pro 9 and later
 
// Store panel name globally to avoid typos
StrConstant panelName = "OrbitalSelectionPanel"
 
Function Main()
    Execute panelName + "()"
End
 
// Unified handler for dependent checkboxes ("p", "px", "py", "pz")
Function pCheckBoxProc(cba) : CheckBoxControl
    STRUCT WMCheckboxAction &cba
 
    switch( cba.eventCode )
        case 2: // mouse up
            Variable checked = cba.checked
            pCheckBoxSync(cba.ctrlName) // Sync dependents
            break
        case -1: // control being killed
            break
    endswitch
 
    return 0
End
 
// Core sync logic
Function pCheckBoxSync(controlName)
    String controlName
    String pCheckBoxList = "pxCheckBox;pyCheckBox;pzCheckBox"
    Variable i, allChecked
    
    allChecked = 1
    if (WhichListItem(controlName, pCheckBoxList) != -1)
        // A dependent checkbox changed ==> sync its master
        for (i = 0; i < ItemsInList(pCheckBoxList); i += 1)
            ControlInfo /W=$panelName $StringFromList(i, pCheckBoxList)
            allChecked *= V_Value
        endfor
        CheckBox pCheckBox, win=$panelName, value=allChecked
    endif
 
    if (CmpStr(controlName, "pCheckBox") == 0)
        // Master checkbox changed ==> sync all its dependents
        ControlInfo /W=$panelName pCheckBox
        for (i = 0; i < ItemsInList(pCheckBoxList); i += 1)
            CheckBox $StringFromList(i, pCheckBoxList), win=OrbitalSelectionPanel, value=V_Value
        endfor
    endif
 
    return 0
End
 
Window OrbitalSelectionPanel() : Panel
    PauseUpdate; Silent 1       // building window...
    Variable cbWidth = 40.00, cbHeight = 12.00
    Variable vDist = 20.00, hDist = 40.00;
    Variable xPos = 20.00, yPos = 20.00
    NewPanel /W=(500.0,300.0,800.0,400.0)
    SetDrawLayer UserBack
    CheckBox pCheckBox, pos={xPos+0*hDist,yPos+1*vDist},size={cbWidth,cbHeight},proc=pCheckBoxProc,title="p",help={"Select all p orbitals"}
    CheckBox pxCheckBox,pos={xPos+1*hDist,yPos+1*vDist},size={cbWidth,cbHeight},proc=pCheckBoxProc,title="px"
    CheckBox pyCheckBox,pos={xPos+2*hDist,yPos+1*vDist},size={cbWidth,cbHeight},proc=pCheckBoxProc,title="py"
    CheckBox pzCheckBox,pos={xPos+3*hDist,yPos+1*vDist},size={cbWidth,cbHeight},proc=pCheckBoxProc,title="pz"
EndMacro

 

Great that it worked for you! :-) Maybe one more thing you want to change, especially if you are sharing your code with other users: Avoid giving generic names to functions and global constants such as "panelName" and "Main". As soon as another function / constant / wave with this name exists anywhere in your current Igor session you will get an error. You may use the 'static' prefix for functions / constants which are never exposed to users to prevent this. Here is your code boiled down to the essentials:

#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3          // Use modern global access method and strict wave access
#pragma DefaultTab={3,20,4}  // Set default tab width in Igor Pro 9 and later
 
Function buildOrbitalSelectionPanel()
    Variable vW = 40, hW = 12
    Variable vD = 20, hD = 40
    Variable vP = 20, hP = 20
    NewPanel/W=(500,300,800,400)/N=OrbitalSelectionPanel
    CheckBox pCheckBox, pos={hP+0*hD, hP+1*vD} ,size={hW,vW} ,title="p" ,help={"Select all p orbitals"}
    CheckBox pxCheckBox,pos={hP+1*hD, hP+1*vD} ,size={hW,vW} ,title="px"
    CheckBox pyCheckBox,pos={hP+2*hD, hP+1*vD} ,size={hW,vW} ,title="py"
    CheckBox pzCheckBox,pos={hP+3*hD, hP+1*vD} ,size={hW,vW} ,title="pz"
    ModifyControlList ControlNameList("OrbitalSelectionPanel",";","*CheckBox") proc=OrbitalSelect_pCheck
End
 
// Unified handler for dependent checkboxes ("p", "px", "py", "pz")
Function OrbitalSelect_pCheck(STRUCT WMCheckboxAction &cba) : CheckBoxControl
    if (cba.eventCode != 2)     // get out if not mouse-up
        return 0
    endif
    
    String list = "pxCheckBox;pyCheckBox;pzCheckBox"
    Variable i, allChecked = 1
    
    if (!CmpStr(cba.ctrlName, "pCheckBox"))     // Master checkbox changed ==> sync all its dependents
        for (i = 0; i < ItemsInList(list); i++)
            CheckBox $StringFromList(i, list), win=$cba.win, value=cba.checked
        endfor
    else                                        // A dependent checkbox changed ==> sync its master
        for (i = 0; i < ItemsInList(list); i++)
            ControlInfo/W=$cba.win $StringFromList(i, list)
            allChecked *= V_Value
        endfor
        CheckBox pCheckBox, win=$cba.win, value=allChecked  
    endif
    
    return 0
End

Or if you have Igor 9:

Function buildOrbitalSelectionPanel()
    Variable vW = 40, hW = 12
    Variable vD = 20, hD = 40
    Variable vP = 20, hP = 20
    NewPanel/W=(500,300,800,400)/N=OrbitalSelectionPanel
    CheckBox pCheckBox, pos={hP+0*hD, hP+1*vD} ,size={hW,vW} ,title="p" ,help={"Select all p orbitals"}
    CheckBox pxCheckBox,pos={hP+1*hD, hP+1*vD} ,size={hW,vW} ,title="px"
    CheckBox pyCheckBox,pos={hP+2*hD, hP+1*vD} ,size={hW,vW} ,title="py"
    CheckBox pzCheckBox,pos={hP+3*hD, hP+1*vD} ,size={hW,vW} ,title="pz"
    ModifyControlList ControlNameList("OrbitalSelectionPanel",";","*CheckBox") proc=OrbitalSelect_pCheck
End
 
// Unified handler for dependent checkboxes ("p", "px", "py", "pz")
Function OrbitalSelect_pCheck(STRUCT WMCheckboxAction &cba) : CheckBoxControl
    if (cba.eventCode != 2)     // get out if not mouse-up
        return 0
    endif
    
    String str
    Make/Free/T list = {"pxCheckBox","pyCheckBox","pzCheckBox"}
    Variable allChecked = 1
    
    if (!CmpStr(cba.ctrlName, "pCheckBox"))     // Master checkbox changed ==> sync all its dependents
        for (str : list)
            CheckBox $str, win=$cba.win, value=cba.checked
        endfor
    else                                        // A dependent checkbox changed ==> sync its master
        for (str : list)
            ControlInfo/W=$cba.win $str
            allChecked *= V_Value
        endfor
        CheckBox pCheckBox, win=$cba.win, value=allChecked  
    endif
    
    return 0
End