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:

  1. A PySide Widget containing the input controls.
  2. A Data Synchronization Mechanism to read property values from the object and write changes back.
  3. Transaction handling via App.ActiveDocument.openTransaction() and commitTransaction() 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."""
        pass

2. 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 True

Instantiation 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.