Dialog creation

In this page we will show how to build a simple Qt Dialog with Qt Designer, Qt's official tool for designing interfaces, then convert it to python code, then use it inside FreeCAD. I'll assume in the example that you know how to edit and run python scripts already, and that you can do simple things in a terminal window such as navigate, etc. You must also have, of course, pyqt installed.

Designing the dialog
In CAD applications, designing a good UI (User Interface) is very important. About everything the user will do will be through some piece of interface: reading dialog boxes, pressing buttons, choosing between icons, etc. So it is very important to think carefully to what you want to do, how you want the user to behave, and how will be the workflow of your action.

There are a couple of concepts to know when designing interface:
 * Modal/non-modal dialogs: A modal dialog appears in front of your screen, stopping the action of the main window, forcing the user to respond to the dialog, while a non-modal dialog doesn't stop you from working on the main window. In some case the first is better, in other cases not.
 * Identifying what is required and what is optional: Make sure the user knows what he must do. Label everything with proper description, use tooltips, etc.
 * Separating commands from parameters: This is usually done with buttons and text input fields. The user knows that clicking a button will produce an action while changing a value inside a text field will change a parameter somewhere. Nowadays, though, users usually know well what is a button, what is an input field, etc. The interface toolkit we are using, Qt, is a state-of-the-art toolkit, and we won't have to worry much about making things clear, since they will already be very clear by themselves.

So, now that we have well defined what we will do, it's time to open the qt designer. Let's design a very simple dialog, like this:



We will then use this dialog in FreeCAD to produce a nice rectangular plane. You might find it not very useful to produce nice rectangular planes, but it will be easy to change it later to do more complex things. When you open it, Qt Designer looks like this:



It is very simple to use. On the left bar you have elements that can be dragged on your widget. On the right side you have properties panels displaying all kinds of editable properties of selected elements. So, begin with creating a new widget. Select "Dialog without buttons", since we don't want the default Ok/Cancel buttons. Then, drag on your widget 3 labels, one for the title, one for writing "Height" and one for writing "Width". Labels are simple texts that appear on your widget, just to inform the user. If you select a label, on the right side will appear several properties that you can change if you want, such as font style, height, etc.

Then, add 2 LineEdits, which are text fields that the user can fill in, one for the height and one for the width. Here too, we can edit properties. For example, why not set a default value? For example 1.00 for each. This way, when the user will see the dialog, both values will be filled already and if he is satisfied he can directly press the button, saving precious time. Then, add a PushButton, which is the button the user will need to press after he filled the 2 fields.

Note that I choosed here very simple controls, but Qt has many more options, for example you could use Spinboxes instead of LineEdits, etc... Have a look at what is available, you will surely have other ideas.

That's about all we need to do in Qt Designer. One last thing, though, let's rename all our elements with easier names, so it will be easier to identify them in our scripts:



Converting our dialog to python
Now, let's save our widget somewhere. It will be saved as an .ui file, that we will easily convert to python script with pyuic. On windows, the pyuic program is bundled with pyqt (to be verified), on linux you probably will need to install it separately from your package manager (on debian-based systems, it is part of the pyqt4-dev-tools package). To do the conversion, you'll need to open a terminal window (or a command prompt window on windows), navigate to where you saved your .ui file, and issue:

pyuic mywidget.ui > mywidget.py

On some systems the program is called pyuic4 instead of pyuic. This will simply convert the .ui file into a python script. If we open the mywidget.py file, its contents are very easy to understand:

from PyQt4 import QtCore, QtGui class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") Dialog.resize(187, 178) self.title = QtGui.QLabel(Dialog) self.title.setGeometry(QtCore.QRect(10, 10, 271, 16)) self.title.setObjectName("title") self.label_width = QtGui.QLabel(Dialog) ...        self.retranslateUi(Dialog) QtCore.QMetaObject.connectSlotsByName(Dialog) def retranslateUi(self, Dialog): Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "Dialog", None, QtGui.QApplication.UnicodeUTF8)) self.title.setText(QtGui.QApplication.translate("Dialog", "Plane-O-Matic", None, QtGui.QApplication.UnicodeUTF8)) ...

As you see it has a very simple structure: a class named Ui_Dialog is created, that stores the interface elements of our widget. That class has two methods, one for setting up the widget, and one for translating its contents, that is part of the general Qt mechanism for translating interface elements. The setup method simply creates, one by one, the widgets as we defined them in Qt Designer, and sets their options as we decided earlier. Then, the whole interface gets translated, and finally, the slots get connected (we'll talk about that later).

We can now create a new widget, and use this class to create its interface. We can already see our widget in action, by putting our mywidget.py file in a place where FreeCAD will find it (in the FreeCAD bin directory, or in any of the Mod subdirectories), and, in the FreeCAD python interpreter, issue:

from PyQt4 import QtGui import mywidget d = QtGui.QWidget d.ui = mywidget.Ui_Dialog d.ui.setupUi(d) d.show And our dialog will appear! Note that our python interpreter is still working, we have a non-modal dialog. So, to close it, we can (apart from clicking its close icon, of course) issue: d.hide

Making our dialog do something
Now that we can show and hide our dialog, we just need to add one last part: To make it do something! If you play a bit with Qt designer, you'll quickly discover a whole section called "signals and slots". Basically, it works like this: elements on your widgets (in Qt terminology, those elements are themselves widgets) can send signals. Those signals differ according to the widget type. For example, a button can send a signal when it is pressed and when it is released. Those signals can be connected to slots, which can be special functionality of other widgets (for example a dialog has a "close" slot to which you can connect the signal from a close button), or can be custom functions. The PyQt Reference Documentation lists all the qt widgets, what they can do, what signals they can send, etc...

What we will do here, is to create a new function that will create a plane based on height and width, and to connect that function to the pressed signal emitted by our "Create!" button. So, let's begin with importing our FreeCAD modules, by putting the following line at the top of the script, where we already import QtCore and QtGui:

import FreeCAD, Part

Then, let's add a new function to our Ui_Dialog class:

def createPlane(self): try: # first we check if valid numbers have been entered w = float(self.width.text) h = float(self.height.text) except ValueError: print "Error! Width and Height values must be valid numbers!" else: # create a face from 4 points p1 = FreeCAD.Vector(0,0,0) p2 = FreeCAD.Vector(w,0,0) p3 = FreeCAD.Vector(w,h,0) p4 = FreeCAD.Vector(0,h,0) pointslist = [p1,p2,p3,p4,p1] mywire = Part.makePolygon(pointslist) myface = Part.Face(mywire) Part.show(myface) self.hide

Then, we need to inform Qt to connect the button to the function, by placing the following line just before QtCore.QMetaObject.connectSlotsByName(Dialog):

QtCore.QObject.connect(self.create,QtCore.SIGNAL("pressed"),self.createPlane)

This, as you see, connects the pressed signal of our create object (the "Create!" button), to a slot named createPlane, which we just defined. That's it! Now, as a final touch, we can add a little function to create the dialog, it will be easier to call. Outside the Ui_Dialog class, let's add this code:

class plane: d = QtGui.QWidget d.ui = Ui_Dialog d.ui.setupUi(d) d.show

Then, from FreeCAD, we only need to do:

import mywidget mywidget.plane

That's all Folks... Now you can try all kinds of things, like for example inserting your widget in the FreeCAD interface (see the Code snippets page), or making much more advanced custom tools, by using other elements on your widget.

The complete script
This is the complete script, for reference:

# # from PyQt4 import QtCore, QtGui import FreeCAD, Part class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") Dialog.resize(187, 178) self.title = QtGui.QLabel(Dialog) self.title.setGeometry(QtCore.QRect(10, 10, 271, 16)) self.title.setObjectName("title") self.label_width = QtGui.QLabel(Dialog) self.label_width.setGeometry(QtCore.QRect(10, 50, 57, 16)) self.label_width.setObjectName("label_width") self.label_height = QtGui.QLabel(Dialog) self.label_height.setGeometry(QtCore.QRect(10, 90, 57, 16)) self.label_height.setObjectName("label_height") self.width = QtGui.QLineEdit(Dialog) self.width.setGeometry(QtCore.QRect(60, 40, 111, 26)) self.width.setObjectName("width") self.height = QtGui.QLineEdit(Dialog) self.height.setGeometry(QtCore.QRect(60, 80, 111, 26)) self.height.setObjectName("height") self.create = QtGui.QPushButton(Dialog) self.create.setGeometry(QtCore.QRect(50, 140, 83, 26)) self.create.setObjectName("create") self.retranslateUi(Dialog) QtCore.QObject.connect(self.create,QtCore.SIGNAL("pressed"),self.createPlane) QtCore.QMetaObject.connectSlotsByName(Dialog) def retranslateUi(self, Dialog): Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "Dialog", None, QtGui.QApplication.UnicodeUTF8)) self.title.setText(QtGui.QApplication.translate("Dialog", "Plane-O-Matic", None, QtGui.QApplication.UnicodeUTF8)) self.label_width.setText(QtGui.QApplication.translate("Dialog", "Width", None, QtGui.QApplication.UnicodeUTF8)) self.label_height.setText(QtGui.QApplication.translate("Dialog", "Height", None, QtGui.QApplication.UnicodeUTF8)) self.create.setText(QtGui.QApplication.translate("Dialog", "Create!", None, QtGui.QApplication.UnicodeUTF8)) def createPlane(self): try: # first we check if valid numbers have been entered w = float(self.width.text) h = float(self.height.text) except ValueError: print "Error! Width and Height values must be valid numbers!" else: # create a face from 4 points p1 = FreeCAD.Vector(0,0,0) p2 = FreeCAD.Vector(w,0,0) p3 = FreeCAD.Vector(w,h,0) p4 = FreeCAD.Vector(0,h,0) pointslist = [p1,p2,p3,p4,p1] mywire = Part.makePolygon(pointslist) myface = Part.Face(mywire) Part.show(myface) class plane: d = QtGui.QWidget d.ui = Ui_Dialog d.ui.setupUi(d) d.show
 * 1) -*- coding: utf-8 -*-
 * 1) Form implementation generated from reading ui file 'mywidget.ui'
 * 1) Created: Mon Jun  1 19:09:10 2009
 * 2)      by: PyQt4 UI code generator 4.4.4
 * 1) WARNING! All changes made in this file will be lost!

Method 1
An example of a dialog box complete with its connections.


 * 1) -*- coding: utf-8 -*-
 * 2) Create by flachyjoe

from PyQt4 import QtCore, QtGui

try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: def _fromUtf8(s): return s

try: _encoding = QtGui.QApplication.UnicodeUTF8 def _translate(context, text, disambig): return QtGui.QApplication.translate(context, text, disambig, _encoding) except AttributeError: def _translate(context, text, disambig): return QtGui.QApplication.translate(context, text, disambig)

class Ui_MainWindow(object):

def __init__(self, MainWindow): self.window = MainWindow

MainWindow.setObjectName(_fromUtf8("MainWindow")) MainWindow.resize(400, 300) self.centralWidget = QtGui.QWidget(MainWindow) self.centralWidget.setObjectName(_fromUtf8("centralWidget"))

self.pushButton = QtGui.QPushButton(self.centralWidget) self.pushButton.setGeometry(QtCore.QRect(30, 170, 93, 28)) self.pushButton.setObjectName(_fromUtf8("pushButton")) self.pushButton.clicked.connect(self.on_pushButton_clicked) #connection pushButton

self.lineEdit = QtGui.QLineEdit(self.centralWidget) self.lineEdit.setGeometry(QtCore.QRect(30, 40, 211, 22)) self.lineEdit.setObjectName(_fromUtf8("lineEdit")) self.lineEdit.returnPressed.connect(self.on_lineEdit_clicked) #connection lineEdit

self.checkBox = QtGui.QCheckBox(self.centralWidget) self.checkBox.setGeometry(QtCore.QRect(30, 90, 81, 20)) self.checkBox.setChecked(True) self.checkBox.setObjectName(_fromUtf8("checkBoxON")) self.checkBox.clicked.connect(self.on_checkBox_clicked) #connection checkBox

self.radioButton = QtGui.QRadioButton(self.centralWidget) self.radioButton.setGeometry(QtCore.QRect(30, 130, 95, 20)) self.radioButton.setObjectName(_fromUtf8("radioButton")) self.radioButton.clicked.connect(self.on_radioButton_clicked) #connection radioButton

MainWindow.setCentralWidget(self.centralWidget)

self.menuBar = QtGui.QMenuBar(MainWindow) self.menuBar.setGeometry(QtCore.QRect(0, 0, 400, 26)) self.menuBar.setObjectName(_fromUtf8("menuBar")) MainWindow.setMenuBar(self.menuBar)

self.mainToolBar = QtGui.QToolBar(MainWindow) self.mainToolBar.setObjectName(_fromUtf8("mainToolBar")) MainWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.mainToolBar)

self.statusBar = QtGui.QStatusBar(MainWindow) self.statusBar.setObjectName(_fromUtf8("statusBar")) MainWindow.setStatusBar(self.statusBar)

self.retranslateUi(MainWindow)

def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow", None)) self.pushButton.setText(_translate("MainWindow", "OK", None)) self.lineEdit.setText(_translate("MainWindow", "tyty", None)) self.checkBox.setText(_translate("MainWindow", "CheckBox", None)) self.radioButton.setText(_translate("MainWindow", "RadioButton", None))

def on_checkBox_clicked(self): if self.checkBox.checkState==0: App.Console.PrintMessage(str(self.checkBox.checkState)+" CheckBox KO\r\n") else: App.Console.PrintMessage(str(self.checkBox.checkState)+" CheckBox OK\r\n") App.Console.PrintMessage(str(self.lineEdit.displayText)+" LineEdit\r\n")
 * 1)        App.Console.PrintMessage(str(self.lineEdit.setText("tititi"))+" LineEdit\r\n") #write text to the lineEdit window !
 * 2)        str(self.lineEdit.setText("tititi")) #écrit le texte dans la fenêtre lineEdit

def on_radioButton_clicked(self): if self.radioButton.isChecked: App.Console.PrintMessage(str(self.radioButton.isChecked)+" Radio OK\r\n") else: App.Console.PrintMessage(str(self.radioButton.isChecked)+" Radio KO\r\n")

def on_lineEdit_clicked(self): App.Console.PrintMessage(str(self.lineEdit.displayText)+" LineEdit Display\r\n")
 * 1)        if self.lineEdit.textChanged:

def on_pushButton_clicked(self): App.Console.PrintMessage("Terminé\r\n") self.window.hide

MainWindow = QtGui.QMainWindow ui = Ui_MainWindow(MainWindow) MainWindow.show

Here the same window but with an icon on each button.


 * 1) -*- coding: utf-8 -*-

from PyQt4 import QtCore, QtGui

try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: def _fromUtf8(s): return s

try: _encoding = QtGui.QApplication.UnicodeUTF8 def _translate(context, text, disambig): return QtGui.QApplication.translate(context, text, disambig, _encoding) except AttributeError: def _translate(context, text, disambig): return QtGui.QApplication.translate(context, text, disambig)

class Ui_MainWindow(object):

def __init__(self, MainWindow): self.window = MainWindow path = FreeCAD.ConfigGet("AppHomePath")
 * 1)        path = FreeCAD.ConfigGet("UserAppData")

MainWindow.setObjectName(_fromUtf8("MainWindow")) MainWindow.resize(400, 300) self.centralWidget = QtGui.QWidget(MainWindow) self.centralWidget.setObjectName(_fromUtf8("centralWidget"))

self.pushButton = QtGui.QPushButton(self.centralWidget) self.pushButton.setGeometry(QtCore.QRect(30, 170, 93, 28)) self.pushButton.setObjectName(_fromUtf8("pushButton")) self.pushButton.clicked.connect(self.on_pushButton_clicked) #connection pushButton

self.lineEdit = QtGui.QLineEdit(self.centralWidget) self.lineEdit.setGeometry(QtCore.QRect(30, 40, 211, 22)) self.lineEdit.setObjectName(_fromUtf8("lineEdit")) self.lineEdit.returnPressed.connect(self.on_lineEdit_clicked) #connection lineEdit

self.checkBox = QtGui.QCheckBox(self.centralWidget) self.checkBox.setGeometry(QtCore.QRect(30, 90, 100, 20)) self.checkBox.setChecked(True) self.checkBox.setObjectName(_fromUtf8("checkBoxON")) self.checkBox.clicked.connect(self.on_checkBox_clicked) #connection checkBox

self.radioButton = QtGui.QRadioButton(self.centralWidget) self.radioButton.setGeometry(QtCore.QRect(30, 130, 95, 20)) self.radioButton.setObjectName(_fromUtf8("radioButton")) self.radioButton.clicked.connect(self.on_radioButton_clicked) #connection radioButton

MainWindow.setCentralWidget(self.centralWidget)

self.menuBar = QtGui.QMenuBar(MainWindow) self.menuBar.setGeometry(QtCore.QRect(0, 0, 400, 26)) self.menuBar.setObjectName(_fromUtf8("menuBar")) MainWindow.setMenuBar(self.menuBar)

self.mainToolBar = QtGui.QToolBar(MainWindow) self.mainToolBar.setObjectName(_fromUtf8("mainToolBar")) MainWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.mainToolBar)

self.statusBar = QtGui.QStatusBar(MainWindow) self.statusBar.setObjectName(_fromUtf8("statusBar")) MainWindow.setStatusBar(self.statusBar)

self.retranslateUi(MainWindow)

# Affiche un icône sur le bouton PushButton # self.image_01 = "C:\Program Files\FreeCAD0.13\icone01.png" # adapt the icon name self.image_01 = path+"icone01.png" # adapt the name of the icon icon01 = QtGui.QIcon icon01.addPixmap(QtGui.QPixmap(self.image_01),QtGui.QIcon.Normal, QtGui.QIcon.Off) self.pushButton.setIcon(icon01) self.pushButton.setLayoutDirection(QtCore.Qt.RightToLeft) # This command reverses the direction of the button

# Affiche un icône sur le bouton RadioButton # self.image_02 = "C:\Program Files\FreeCAD0.13\icone02.png" # adapt the name of the icon self.image_02 = path+"icone02.png" # adapter le nom de l'icône icon02 = QtGui.QIcon icon02.addPixmap(QtGui.QPixmap(self.image_02),QtGui.QIcon.Normal, QtGui.QIcon.Off) self.radioButton.setIcon(icon02) # self.radioButton.setLayoutDirection(QtCore.Qt.RightToLeft) # This command reverses the direction of the button

# Affiche un icône sur le bouton CheckBox # self.image_03 = "C:\Program Files\FreeCAD0.13\icone03.png" # the name of the icon self.image_03 = path+"icone03.png" # adapter le nom de l'icône icon03 = QtGui.QIcon icon03.addPixmap(QtGui.QPixmap(self.image_03),QtGui.QIcon.Normal, QtGui.QIcon.Off) self.checkBox.setIcon(icon03) # self.checkBox.setLayoutDirection(QtCore.Qt.RightToLeft) # This command reverses the direction of the button

def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(_translate("MainWindow", "FreeCAD", None)) self.pushButton.setText(_translate("MainWindow", "OK", None)) self.lineEdit.setText(_translate("MainWindow", "tyty", None)) self.checkBox.setText(_translate("MainWindow", "CheckBox", None)) self.radioButton.setText(_translate("MainWindow", "RadioButton", None))

def on_checkBox_clicked(self): if self.checkBox.checkState==0: App.Console.PrintMessage(str(self.checkBox.checkState)+" CheckBox KO\r\n") else: App.Console.PrintMessage(str(self.checkBox.checkState)+" CheckBox OK\r\n") # App.Console.PrintMessage(str(self.lineEdit.setText("tititi"))+" LineEdit\r\n") # write text to the lineEdit window ! # str(self.lineEdit.setText("tititi")) #écrit le texte dans la fenêtre lineEdit App.Console.PrintMessage(str(self.lineEdit.displayText)+" LineEdit\r\n")

def on_radioButton_clicked(self): if self.radioButton.isChecked: App.Console.PrintMessage(str(self.radioButton.isChecked)+" Radio OK\r\n") else: App.Console.PrintMessage(str(self.radioButton.isChecked)+" Radio KO\r\n")

def on_lineEdit_clicked(self): # if self.lineEdit.textChanged: App.Console.PrintMessage(str(self.lineEdit.displayText)+" LineEdit Display\r\n")

def on_pushButton_clicked(self): App.Console.PrintMessage("Terminé\r\n") self.window.hide

MainWindow = QtGui.QMainWindow ui = Ui_MainWindow(MainWindow) MainWindow.show

Here the code to display the icon on the pushButton, change the name for another button, (radioButton, checkBox) and the path to the icon. # Affiche un icône sur le bouton PushButton # self.image_01 = "C:\Program Files\FreeCAD0.13\icone01.png" # the name of the icon self.image_01 = path+"icone01.png" # the name of the icon icon01 = QtGui.QIcon icon01.addPixmap(QtGui.QPixmap(self.image_01),QtGui.QIcon.Normal, QtGui.QIcon.Off) self.pushButton.setIcon(icon01) self.pushButton.setLayoutDirection(QtCore.Qt.RightToLeft) # This command reverses the direction of the button The command UserAppData gives the user path AppHomePath gives the installation path of FreeCAD path = FreeCAD.ConfigGet("AppHomePath") This command reverses the horizontal button, right to left. self.pushButton.setLayoutDirection(QtCore.Qt.RightToLeft) # This command reverses the direction of the button
 * 1)        path = FreeCAD.ConfigGet("UserAppData")

Method 2
Another method to display a window, here by creating a file QtForm.py which contains the header program (module called with import QtForm), and a second module that contains the code window all these accessories, and your code (the calling module).

This method requires two separate files, but allows to shorten your program using the file ' ' QtForm.py ' ' import. Then distribute the two files together, they are inseparable.

The file QtForm.py

from PyQt4 import QtCore, QtGui
 * 1) -*- coding: utf-8 -*-
 * 2) Create by flachyjoe

try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: def _fromUtf8(s): return s

try: _encoding = QtGui.QApplication.UnicodeUTF8 def _translate(context, text, disambig): return QtGui.QApplication.translate(context, text, disambig, _encoding) except AttributeError: def _translate(context, text, disambig): return QtGui.QApplication.translate(context, text, disambig)

class Form(object): def __init__(self, title, width, height): self.window = QtGui.QMainWindow self.title=title self.window.setObjectName(_fromUtf8(title)) self.window.setWindowTitle(_translate(self.title, self.title, None)) self.window.resize(width, height)

def show(self): self.createUI self.retranslateUI self.window.show def setText(self, control, text): control.setText(_translate(self.title, text, None))

The appellant, file that contains the window and your code.

The file my_file.py

The connections are to do, a good exercise.

from PyQt4 import QtCore, QtGui import QtForm
 * 1) -*- coding: utf-8 -*-
 * 2) Create by flachyjoe

class myForm(QtForm.Form): def createUI(self): self.centralWidget = QtGui.QWidget(self.window) self.window.setCentralWidget(self.centralWidget) self.pushButton = QtGui.QPushButton(self.centralWidget) self.pushButton.setGeometry(QtCore.QRect(30, 170, 93, 28)) self.pushButton.clicked.connect(self.on_pushButton_clicked) self.lineEdit = QtGui.QLineEdit(self.centralWidget) self.lineEdit.setGeometry(QtCore.QRect(30, 40, 211, 22)) self.checkBox = QtGui.QCheckBox(self.centralWidget) self.checkBox.setGeometry(QtCore.QRect(30, 90, 81, 20)) self.checkBox.setChecked(True) self.radioButton = QtGui.QRadioButton(self.centralWidget) self.radioButton.setGeometry(QtCore.QRect(30, 130, 95, 20)) def retranslateUI(self): self.setText(self.pushButton, "Fermer") self.setText(self.lineEdit, "essai de texte") self.setText(self.checkBox, "CheckBox") self.setText(self.radioButton, "RadioButton") def on_pushButton_clicked(self): self.window.hide

myWindow=myForm("Fenêtre de test",400,300) myWindow.show