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

 

Hi chozo,

Thank you for the follow-up and for sharing these excellent refinements! Your points about avoiding generic names and leveraging Igor’s newer features (like for (str : list) in Igor 9) are incredibly valuable—I especially appreciate how you’ve:

  1. Eliminated naming conflicts by using descriptive prefixes (OrbitalSelect_*) and avoiding globals.

  2. Streamlined the panel setup with ModifyControlList, making it more maintainable.

  3. Highlighted Igor 9 optimizations like the modern loop syntax, which I’ll adopt moving forward.

Your "boiled-down" version is a masterclass in clean Igor Pro coding! I’ve already incorporated these changes into my project and will definitely use this approach for future panels.

Thanks again for taking the time to share your expertise—it’s made a real difference in the quality of my code.

Programming is an intellectual labor that brings physical and mental pleasure to people.