Skip to content

4. GUI Application with attributes, properties, quantities and type information

stefkeB edited this page Apr 1, 2021 · 3 revisions

Getting a step further

In this example, we will expand on our Basic GUI example, using the same PyQt5 libraries. In our main __init__ method, we want to display everything we can think of for a single object: properties, quantities, the type and also the core attributes. To keep it convenient, we will put that in a second QTreeWidget.

	def __init__(self):
		QWidget.__init__(self)
		# Prepare Tree Widgets in a stretchable layout
		vbox = QVBoxLayout()
		self.setLayout(vbox)
		# Object Tree
		self.object_tree = QTreeWidget()
		vbox.addWidget(self.object_tree)
		self.object_tree.setColumnCount(3)
		self.object_tree.setHeaderLabels(["Name", "Class", "ID"])

This was what we already had before. We now add the possibility to select multiple objects, by setting the SelectionModel to ExtendedSelection and by linking any change of our selection to a new .add_data method.

		self.object_tree.setSelectionMode(QAbstractItemView.ExtendedSelection)
		self.object_tree.selectionModel().selectionChanged.connect(self.add_data)

And then we add a second QTreeWidget and add it inside the same vertical layout, so everything stretches nicely.

		# Property Tree
		self.property_tree = QTreeWidget()
		vbox.addWidget(self.property_tree)
		self.property_tree.setColumnCount(3)
		self.property_tree.setHeaderLabels(["Name", "Value", "ID/Type"])

The load_file and add_object_in_tree and even the main methods are exactly the same.

Fill the second tree

For the second tree, we create a new add_data method, which has no particular arguments, as it will retrieve the selection from the first tree automatically. In this method, we clear the tree, so it is ready to be filled in again, based on the current selection. As we may have multiple items selected, we run through all of them in a for loop.

	def add_data(self):
		self.property_tree.clear()
		items = self.object_tree.selectedItems()
		for item in items:

We then check the string in the third column, that is item.text(2) since we always count from zero. If it is empty, there is nothing we can do but break. Otherwise, we assume that it contains a GUID (normally the .GlobalId attribute that the object has inherited from IfcRoot. .by_guid() can be used to retrieve the actual entity from the IFC file. Again, break when we have no result.

			# the GUID is in the third column
			buffer = item.text(2)
			if not buffer:
				break
			# find the related object in our IFC file
			ifc_object = self.ifc_file.by_guid(buffer)
			if ifc_object is None:
				break

Beware that this code will only work for so-called rooted classes. So we may eventually add additional support for other classes.

When we test the application now, we get the second tree, but it is still empty.

Second Tree Empty Window

Filling the second tree

Within the add_data method, we start by adding type, properties and quantities, using almost exactly the same code as in the console application. But instead of dumping it in the console, we place them in a QTreeWidgetItem. For the column values, we pass the .Name, class (.is_a()) and the GUID (.GlobalId) inside a Tuple. The tree item is placed at the top of the tree (.addTopLevelItem).

			# Properties & Quantities
			if hasattr(ifc_object, 'IsDefinedBy'):
				for definition in ifc_object.IsDefinedBy:
					if definition.is_a('IfcRelDefinesByType'):
						type_object = definition.RelatingType
						type_item = QTreeWidgetItem([type_object.Name,
									type_object.is_a(),
									type_object.GlobalId])
						self.property_tree.addTopLevelItem(type_item)

For the type that suffices, but the property set (which could also be a quantity set) still needs to add the individual properties, which we'll do in a separate method .add_properties_in_tree or .add_quantities_in_tree which receive the set and the tree item containing the set.

					if definition.is_a('IfcRelDefinesByProperties'):
						property_set = definition.RelatingPropertyDefinition
						# the individual properties/quantities
						if property_set.is_a('IfcPropertySet'):
							properties_item = QTreeWidgetItem([property_set.Name,
									property_set.is_a(),
									property_set.GlobalId])
							self.property_tree.addTopLevelItem(properties_item)
							self.add_properties_in_tree(property_set, properties_item)
						elif property_set.is_a('IfcElementQuantity'):
							quantities_item = QTreeWidgetItem([property_set.Name,
									property_set.is_a(),
									property_set.GlobalId])
							self.property_tree.addTopLevelItem(quantities_item)
							self.add_quantities_in_tree(property_set, quantities_item)

Adding properties

The code is again the same as in the console app, with the only difference that we have to fill a tree item and place it underneath its parent.

	# PropertySet
	def add_properties_in_tree(self, property_set, parent_item):
		# the individual properties
		for prop in property_set.HasProperties:
			unit = str(prop.Unit) if hasattr(prop, 'Unit') else ''
			prop_value = '<not handled>'
			if prop.is_a('IfcPropertySingleValue'):
				prop_value = str(prop.NominalValue.wrappedValue)
			parent_item.addChild(QTreeWidgetItem([prop.Name, prop_value, unit]))

Adding quantities

Again, very much the same code as before.

	# QuantitySet
	def add_quantities_in_tree(self, quantity_set, parent_item):
		# the individual quantities
		for quantity in quantity_set.Quantities:
			unit = str(quantity.Unit) if hasattr(quantity, 'Unit') else ''
			quantity_value = '<not handled>'
			if quantity.is_a('IfcQuantityLength'):
				quantity_value = str(quantity.LengthValue)
			elif quantity.is_a('IfcQuantityArea'):
				quantity_value = str(quantity.AreaValue)
			elif quantity.is_a('IfcQuantityVolume'):
				quantity_value = str(quantity.VolumeValue)
			elif quantity.is_a('IfcQuantityCount'):
				quantity_value = str(quantity.CountValue)
			parent_item.addChild(QTreeWidgetItem([quantity.Name, quantity_value, unit]))

With the IfcOpenHouse.ifc example, it works fine, but beware that only the IfcSite entity has a property set available.

Second Tree with property

Adding attributes

Almost done. We need to add the attributes too. Start by adding a container tree item in the add_data method:

	def add_data(self):
		self.property_tree.clear()
		items = self.object_tree.selectedItems()
		for item in items:
			[...]
			# Attributes
			attributes_item = QTreeWidgetItem(["Attributes", "", ifc_object.GlobalId])
			self.property_tree.addTopLevelItem(attributes_item)
			self.add_attributes_in_tree(ifc_object, attributes_item)

And finally we need the method .add_attributes_in_tree as well. This is done from a for loop: we go attribute by attribute, using a counter or index att_idx as we know the total amount of attributes by asking the len (length) of the object.

Using the index, we receive the name of the attribute (.attribute_name(att_idx)), its value as a string (str(ifc_object[att_idx])) and using the name we just received, we can request the type of the attribute (.attribute_type(att_name)).

Use these three strings to fill a new tree item and put it underneath the parent.

	# Attributes
	def add_attributes_in_tree(self, ifc_object, parent_item):
		# the individual attributes
		for att_idx in range(0, len(ifc_object)):
			att_name = ifc_object.attribute_name(att_idx)
			att_value = str(ifc_object[att_idx])
			att_type = ifc_object.attribute_type(att_name)
			attribute_item = QTreeWidgetItem([att_name, att_value, att_type])
			parent_item.addChild(attribute_item)

Second Tree now completed

Beware that the attributes are in a fixed order and that some of them are values (e.g., STRING, DOUBLE), while others may refer to other entities (ENTITY INSTANCE). In that case, we get a fragment that looks exactly like a line in a STEP file.

You can download the file here.