Files
awesome-copilot/skills/freecad-scripts/references/gui-and-interface.md
John Haugabook c037695901 new skill freecad-scripts (#1328)
* new skill freecad-scripts

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

* resolve: codepsellrc, readme

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add suggestions from review

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-10 11:02:57 +10:00

11 KiB

FreeCAD GUI and Interface

Reference guide for building FreeCAD user interfaces: PySide/Qt dialogs, task panels, Gui Commands, Coin3D scenegraph via Pivy.

Official Wiki References

Gui Command

The standard way to add toolbar buttons and menu items in FreeCAD:

import FreeCAD
import FreeCADGui

class MyCommand:
    """A registered FreeCAD command."""

    def GetResources(self):
        return {
            "Pixmap": ":/icons/Part_Box.svg",    # Icon (built-in or custom path)
            "MenuText": "My Command",
            "ToolTip": "Does something useful",
            "Accel": "Ctrl+Shift+M",             # Keyboard shortcut
            "CmdType": "ForEdit"                  # Optional: ForEdit, Alter, etc.
        }

    def IsActive(self):
        """Return True if command should be enabled."""
        return FreeCAD.ActiveDocument is not None

    def Activated(self):
        """Called when the command is triggered."""
        FreeCAD.Console.PrintMessage("Command activated!\n")
        # Open a task panel:
        panel = MyTaskPanel()
        FreeCADGui.Control.showDialog(panel)

# Register the command (name must be unique)
FreeCADGui.addCommand("My_Command", MyCommand())

Task Panel (Sidebar Integration)

Task panels appear in FreeCAD's left sidebar — the preferred way to build interactive tools:

import FreeCAD
import FreeCADGui
from PySide2 import QtWidgets, QtCore

class MyTaskPanel:
    """Task panel for the sidebar."""

    def __init__(self):
        # Build the widget
        self.form = QtWidgets.QWidget()
        self.form.setWindowTitle("My Tool")
        layout = QtWidgets.QVBoxLayout(self.form)

        # Input widgets
        self.length_spin = QtWidgets.QDoubleSpinBox()
        self.length_spin.setRange(0.1, 10000.0)
        self.length_spin.setValue(10.0)
        self.length_spin.setSuffix(" mm")
        self.length_spin.setDecimals(2)

        self.width_spin = QtWidgets.QDoubleSpinBox()
        self.width_spin.setRange(0.1, 10000.0)
        self.width_spin.setValue(10.0)
        self.width_spin.setSuffix(" mm")

        self.height_spin = QtWidgets.QDoubleSpinBox()
        self.height_spin.setRange(0.1, 10000.0)
        self.height_spin.setValue(5.0)
        self.height_spin.setSuffix(" mm")

        self.fillet_check = QtWidgets.QCheckBox("Apply fillet")

        # Form layout
        form_layout = QtWidgets.QFormLayout()
        form_layout.addRow("Length:", self.length_spin)
        form_layout.addRow("Width:", self.width_spin)
        form_layout.addRow("Height:", self.height_spin)
        form_layout.addRow(self.fillet_check)
        layout.addLayout(form_layout)

        # Live preview on value change
        self.length_spin.valueChanged.connect(self._preview)
        self.width_spin.valueChanged.connect(self._preview)
        self.height_spin.valueChanged.connect(self._preview)

    def _preview(self):
        """Update preview in 3D view."""
        pass  # Build and display temporary shape

    def accept(self):
        """Called when user clicks OK."""
        import Part
        doc = FreeCAD.ActiveDocument
        shape = Part.makeBox(
            self.length_spin.value(),
            self.width_spin.value(),
            self.height_spin.value()
        )
        Part.show(shape, "MyBox")
        doc.recompute()
        FreeCADGui.Control.closeDialog()
        return True

    def reject(self):
        """Called when user clicks Cancel."""
        FreeCADGui.Control.closeDialog()
        return True

    def getStandardButtons(self):
        """Which buttons to show."""
        return int(QtWidgets.QDialogButtonBox.Ok |
                   QtWidgets.QDialogButtonBox.Cancel)

    def isAllowedAlterSelection(self):
        return True

    def isAllowedAlterView(self):
        return True

    def isAllowedAlterDocument(self):
        return True

# Show:
# FreeCADGui.Control.showDialog(MyTaskPanel())

Task Panel with Multiple Widgets (Multi-Form)

class MultiFormPanel:
    def __init__(self):
        self.form = [self._buildPage1(), self._buildPage2()]

    def _buildPage1(self):
        w = QtWidgets.QWidget()
        w.setWindowTitle("Page 1")
        # ... add widgets ...
        return w

    def _buildPage2(self):
        w = QtWidgets.QWidget()
        w.setWindowTitle("Page 2")
        # ... add widgets ...
        return w

Standalone PySide Dialogs

import FreeCAD
import FreeCADGui
from PySide2 import QtWidgets, QtCore, QtGui

class MyDialog(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super().__init__(parent or (FreeCADGui.getMainWindow() if FreeCAD.GuiUp else None))
        self.setWindowTitle("My Dialog")
        self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)

        layout = QtWidgets.QVBoxLayout(self)

        # Combo box
        self.combo = QtWidgets.QComboBox()
        self.combo.addItems(["Option A", "Option B", "Option C"])
        layout.addWidget(self.combo)

        # Slider
        self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        self.slider.setRange(1, 100)
        self.slider.setValue(50)
        layout.addWidget(self.slider)

        # Text input
        self.line_edit = QtWidgets.QLineEdit()
        self.line_edit.setPlaceholderText("Enter a name...")
        layout.addWidget(self.line_edit)

        # Button box
        buttons = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addWidget(buttons)

Loading a .ui File

import os
from PySide2 import QtWidgets, QtUiTools, QtCore

def loadUiFile(ui_path):
    """Load a Qt Designer .ui file."""
    loader = QtUiTools.QUiLoader()
    file = QtCore.QFile(ui_path)
    file.open(QtCore.QFile.ReadOnly)
    widget = loader.load(file)
    file.close()
    return widget

# In a task panel:
class UiTaskPanel:
    def __init__(self):
        ui_path = os.path.join(os.path.dirname(__file__), "panel.ui")
        self.form = loadUiFile(ui_path)
        # Access widgets by objectName set in Qt Designer
        self.form.myButton.clicked.connect(self._onButton)

File Dialogs

# Open file
path, _ = QtWidgets.QFileDialog.getOpenFileName(
    FreeCADGui.getMainWindow(),
    "Open File",
    "",
    "STEP files (*.step *.stp);;All files (*)"
)

# Save file
path, _ = QtWidgets.QFileDialog.getSaveFileName(
    FreeCADGui.getMainWindow(),
    "Save File",
    "",
    "STL files (*.stl);;All files (*)"
)

# Select directory
path = QtWidgets.QFileDialog.getExistingDirectory(
    FreeCADGui.getMainWindow(),
    "Select Directory"
)

Message Boxes

QtWidgets.QMessageBox.information(None, "Info", "Operation completed.")
QtWidgets.QMessageBox.warning(None, "Warning", "Something may be wrong.")
QtWidgets.QMessageBox.critical(None, "Error", "An error occurred.")

result = QtWidgets.QMessageBox.question(
    None, "Confirm", "Are you sure?",
    QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
)
if result == QtWidgets.QMessageBox.Yes:
    pass  # proceed

Input Dialogs

text, ok = QtWidgets.QInputDialog.getText(None, "Input", "Enter name:")
value, ok = QtWidgets.QInputDialog.getDouble(None, "Input", "Value:", 10.0, 0, 1000, 2)
choice, ok = QtWidgets.QInputDialog.getItem(None, "Choose", "Select:", ["A","B","C"], 0, False)

Coin3D / Pivy Scenegraph

FreeCAD's 3D view uses Coin3D (Open Inventor). Pivy provides Python bindings.

from pivy import coin
import FreeCADGui

# Get the scenegraph root
sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()

# --- Basic shapes ---
sep = coin.SoSeparator()

# Material (color)
mat = coin.SoMaterial()
mat.diffuseColor.setValue(0.0, 0.8, 0.2)  # RGB 0-1
mat.transparency.setValue(0.3)             # 0=opaque, 1=invisible

# Transform
transform = coin.SoTransform()
transform.translation.setValue(10, 0, 0)
transform.rotation.setValue(coin.SbVec3f(0,0,1), 0.785)  # axis, angle(rad)
transform.scaleFactor.setValue(2, 2, 2)

# Shapes
sphere = coin.SoSphere()
sphere.radius.setValue(3.0)

cube = coin.SoCube()
cube.width.setValue(5)
cube.height.setValue(5)
cube.depth.setValue(5)

cylinder = coin.SoCylinder()
cylinder.radius.setValue(2)
cylinder.height.setValue(10)

# Assemble
sep.addChild(mat)
sep.addChild(transform)
sep.addChild(sphere)
sg.addChild(sep)

# --- Lines ---
line_sep = coin.SoSeparator()
coords = coin.SoCoordinate3()
coords.point.setValues(0, 3, [[0,0,0], [10,0,0], [10,10,0]])
line_set = coin.SoLineSet()
line_set.numVertices.setValue(3)
line_sep.addChild(coords)
line_sep.addChild(line_set)
sg.addChild(line_sep)

# --- Points ---
point_sep = coin.SoSeparator()
style = coin.SoDrawStyle()
style.pointSize.setValue(5)
coords = coin.SoCoordinate3()
coords.point.setValues(0, 3, [[0,0,0], [5,5,0], [10,0,0]])
points = coin.SoPointSet()
point_sep.addChild(style)
point_sep.addChild(coords)
point_sep.addChild(points)
sg.addChild(point_sep)

# --- Text ---
text_sep = coin.SoSeparator()
trans = coin.SoTranslation()
trans.translation.setValue(0, 0, 5)
font = coin.SoFont()
font.name.setValue("Arial")
font.size.setValue(16)
text = coin.SoText2()       # 2D screen-aligned text
text.string.setValue("Hello")
text_sep.addChild(trans)
text_sep.addChild(font)
text_sep.addChild(text)
sg.addChild(text_sep)

# --- Cleanup ---
sg.removeChild(sep)
sg.removeChild(line_sep)

View Manipulation

view = FreeCADGui.ActiveDocument.ActiveView

# Camera operations
view.viewIsometric()
view.viewFront()
view.viewTop()
view.viewRight()
view.fitAll()
view.setCameraOrientation(FreeCAD.Rotation(0, 0, 0))
view.setCameraType("Perspective")   # or "Orthographic"

# Save image
view.saveImage("/path/to/screenshot.png", 1920, 1080, "White")

# Get camera info
cam = view.getCameraNode()