FreeCAD Custom Property Editors Using PySide
This article explains how to build custom property editors in FreeCAD using Python and PySide. You will learn the architectural relationship between FreeCAD document objects, their ViewProviders, and the Qt-based user interface. The guide covers designing PySide input widgets, binding them to FreeCAD feature properties, handling two-way data synchronization, and embedding custom controls into FreeCAD's Task Panel to manage properties that standard input fields cannot effectively represent.
Understanding FreeCAD Properties and the GUI
In FreeCAD, objects in a document are divided into two components following the Model-View-Controller (MVC) pattern:
- App Object (
App::FeaturePython): Stores geometry, data, and parameters as properties (e.g.,App::PropertyFloat,App::PropertyString,App::PropertyEnumeration). - Gui Object
(
Gui::ViewProviderDocumentObject): Manages how the object is visualized in the 3D viewport and provides interface elements, including task panels and contextual actions.
While FreeCAD automatically generates controls in the Data tab of the Property View for standard property types, complex data structures, validated inputs, or interactive controls (such as sliders, file pickers, or color pickers) require custom GUI elements built with PySide.
Architecture of a Custom Property Editor
Because direct injection of custom PySide delegates into the native
FreeCAD Property View tree is constrained by C++ core internals, the
standard and supported method to provide a custom property editing
experience is through a Task Panel managed by the
object's ViewProvider.
When a user double-clicks an object in the Tree View, the
ViewProvider activates a custom PySide task panel that
serves as the dedicated editor.
A functional custom property editor requires:
- A PySide Widget containing the input controls.
- A Data Synchronization Mechanism to read property values from the object and write changes back.
- Transaction handling via
App.ActiveDocument.openTransaction()andcommitTransaction()to support Undo/Redo.
Step-by-Step Implementation
1. Define the Feature Object
Create an App::FeaturePython object with the properties
you want to manipulate:
import FreeCAD as App
class ParametricBox:
def __init__(self, obj):
obj.Proxy = self
# Add properties to be controlled by the custom editor
obj.addProperty("App::PropertyFloat", "Width", "Parameters", "Width of the box").Width = 10.0
obj.addProperty("App::PropertyFloat", "Height", "Parameters", "Height of the box").Height = 10.0
obj.addProperty("App::PropertyString", "LabelText", "Parameters", "Custom text").LabelText = "Default"
def execute(self, obj):
"""Recompute the feature geometry when properties change."""
pass2. Create the PySide Editor Widget
Design a PySide widget containing the custom controls. Use
PySide.QtWidgets controls such as QSlider,
QSpinBox, or custom layout managers:
from PySide import QtCore, QtWidgets
import FreeCAD as App
class PropertyEditorWidget(QtWidgets.QWidget):
def __init__(self, obj, parent=None):
super(PropertyEditorWidget, self).__init__(parent)
self.obj = obj
self.initUI()
self.loadProperties()
def initUI(self):
layout = QtWidgets.QFormLayout(self)
# Custom slider and spinbox for the 'Width' property
self.widthSlider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.widthSlider.setRange(1, 100)
self.widthSpin = QtWidgets.QDoubleSpinBox()
self.widthSpin.setRange(1.0, 100.0)
# Custom text field
self.textEdit = QtWidgets.QLineEdit()
layout.addRow(QtWidgets.QLabel("Width (Slider):"), self.widthSlider)
layout.addRow(QtWidgets.QLabel("Width (Exact):"), self.widthSpin)
layout.addRow(QtWidgets.QLabel("Custom Label:"), self.textEdit)
# Connect signals for two-way synchronization
self.widthSlider.valueChanged.connect(self.onSliderChanged)
self.widthSpin.valueChanged.connect(self.onSpinChanged)
self.textEdit.textChanged.connect(self.onTextChanged)
def loadProperties(self):
"""Populate widgets with current object property values."""
self.widthSpin.setValue(self.obj.Width)
self.widthSlider.setValue(int(self.obj.Width))
self.textEdit.setText(self.obj.LabelText)
def onSliderChanged(self, value):
self.widthSpin.blockSignals(True)
self.widthSpin.setValue(float(value))
self.widthSpin.blockSignals(False)
self.applyChanges()
def onSpinChanged(self, value):
self.widthSlider.blockSignals(True)
self.widthSlider.setValue(int(value))
self.widthSlider.blockSignals(False)
self.applyChanges()
def onTextChanged(self, text):
self.applyChanges()
def applyChanges(self):
"""Write values back to the FreeCAD object properties."""
App.ActiveDocument.openTransaction("Modify Properties")
self.obj.Width = self.widthSpin.value()
self.obj.LabelText = self.textEdit.text()
App.ActiveDocument.commitTransaction()
App.ActiveDocument.recompute()3. Integrate with ViewProvider and Task Panel
Embed the widget into FreeCAD's native Task Panel interface by
subclassing Gui::ViewProviderDocumentObject:
import FreeCADGui as Gui
class ViewProviderParametricBox:
def __init__(self, vobj):
vobj.Proxy = self
def doubleClicked(self, vobj):
"""Triggered when the user double-clicks the object in the tree."""
if not Gui.Control.activeDialog():
panel = BoxTaskPanel(vobj.Object)
Gui.Control.showDialog(panel)
return True
return False
def getIcon(self):
return """/* XPM */
static const char *test[] = {
"16 16 2 1",
" c None",
". c #0000FF",
"................",
"................",
".. ..",
".. ..",
".. ..",
".. ..",
".. ..",
".. ..",
".. ..",
".. ..",
".. ..",
".. ..",
".. ..",
".. ..",
"................",
"................"};"""
class BoxTaskPanel:
def __init__(self, obj):
self.obj = obj
self.form = PropertyEditorWidget(obj)
self.form.setWindowTitle("Custom Property Editor")
def accept(self):
"""Called when the user clicks 'OK' in the Task Panel."""
Gui.Control.closeDialog()
return True
def reject(self):
"""Called when the user clicks 'Cancel' in the Task Panel."""
Gui.Control.closeDialog()
return TrueInstantiation and Testing
To deploy and test the custom object and editor within an open FreeCAD session:
doc = App.activeDocument() or App.newDocument()
# Create the base FeaturePython object
box_obj = doc.addObject("App::FeaturePython", "CustomBox")
ParametricBox(box_obj)
# Attach the custom ViewProvider
ViewProviderParametricBox(box_obj.ViewObject)
doc.recompute()Double-clicking CustomBox in the Model Tree opens the
custom PySide editor in the Task Panel, providing dedicated interactive
control over Width and LabelText with
real-time viewport updates and undo history integration.