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.