| 
									
										
										
										
											2021-09-19 13:41:50 +02:00
										 |  |  | --- | 
					
						
							| 
									
										
										
										
											2021-10-30 10:33:38 +02:00
										 |  |  | title: "Binding QML with Python: PyViewer 👾" | 
					
						
							| 
									
										
										
										
											2021-09-19 13:41:50 +02:00
										 |  |  | date: 2021-08-29T12:53:19+02:00 | 
					
						
							|  |  |  | draft: false | 
					
						
							| 
									
										
										
										
											2021-10-03 11:42:10 +02:00
										 |  |  | toc: true | 
					
						
							| 
									
										
										
										
											2021-09-19 13:41:50 +02:00
										 |  |  | tags: | 
					
						
							|  |  |  |   - python | 
					
						
							|  |  |  |   - qml | 
					
						
							|  |  |  |   - gui | 
					
						
							|  |  |  |   - code | 
					
						
							|  |  |  | --- | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | [PyViewer](https://git.leene.dev/lieuwe/pyviewer) is a example project which | 
					
						
							|  |  |  | implements a simple image browser / viewer in a scrollable grid array. This main | 
					
						
							|  |  |  | objective here was using QML to define a graphical layout and bind it to a | 
					
						
							|  |  |  | python code-base. Note that this code base is compatible with both Pyside2 and | 
					
						
							|  |  |  | Pyside6. This is because while Pyside6 is preferred it is not readily available | 
					
						
							|  |  |  | on all platforms. Running Pyside6 instead only recommend the qml library version | 
					
						
							|  |  |  | requirements to omitted. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Please take a look at the git repository for exact implementation details. A | 
					
						
							|  |  |  | brief summary of this interaction is presented below. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Emitting QML Calls
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Creating a `QObject` and adding `PySide2.QtCore.Slot` decorators to its methods | 
					
						
							|  |  |  | will allow a python object to be added to the qml context as a referenceable | 
					
						
							|  |  |  | object. For example here we add "viewer" to the qml context which is a | 
					
						
							|  |  |  | "PyViewer" python object. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```Python | 
					
						
							|  |  |  | pyviewer = PyViewer() | 
					
						
							|  |  |  | engine.rootContext().setContextProperty("viewer", pyviewer) | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | This way we can call the object's python procedure "update_tag_filter" from | 
					
						
							|  |  |  | within the QML script as follows: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```QML | 
					
						
							|  |  |  | viewer.update_tag_filter(false); | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Further using the `PySide2.QtCore.Property` decorator further allows us to call | 
					
						
							|  |  |  | states in our python object and manipulate them as it were a qml object. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```QML | 
					
						
							|  |  |  | viewer.path.split("::") | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Emitting Python Calls
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Once this context is working we can create a `PySide2.QtCore.Signal` object to | 
					
						
							|  |  |  | call QML methods from within the python context. A python procedure could then | 
					
						
							|  |  |  | "emit" this signal and thereby prompt any connected qml methods. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```python | 
					
						
							|  |  |  | self.path_changed.emit() | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | In the qml contect we can connect the signals from the python "viewer" object | 
					
						
							|  |  |  | to a qml function call "swipe.update_paths" for example. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```qml | 
					
						
							|  |  |  | viewer.path_changed.connect(swipe.update_paths) | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-13 17:39:51 +02:00
										 |  |  | ## Example: passing images as bindary data
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | For reference the code below outlines a simple example that loads an image from | 
					
						
							|  |  |  | a zip archive and makes the binary data available for QML to source. This | 
					
						
							|  |  |  | avoids the need for explicit file handles when generating or deflating images | 
					
						
							|  |  |  | that are needed for the QML front-end. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```python | 
					
						
							|  |  |  | class Archive(ZipFile): | 
					
						
							|  |  |  |     """Simple archive handler for loading data.""" | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def binarydata(self) -> bytes: | 
					
						
							|  |  |  |         """Load file from archive by name.""" | 
					
						
							|  |  |  |         with self.open(self.source_file, "r") as file: | 
					
						
							|  |  |  |             return file.read() | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The example class above simply inherits from the zipfile standard library where | 
					
						
							|  |  |  | we read a image and store it as part of the `PyViewer` class shown below. This | 
					
						
							|  |  |  | class inherits from `QObject` such that the property is exposed to the qml | 
					
						
							|  |  |  | interface. In this case the `imageloader` is an `Archive` handler that is | 
					
						
							|  |  |  | shown above. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```python | 
					
						
							|  |  |  | class PyViewer(QObject): | 
					
						
							|  |  |  |     """QObject for binging user interface to python backend.""" | 
					
						
							|  |  |  |     @Property(QByteArray) | 
					
						
							|  |  |  |     def image(self) -> QByteArray: | 
					
						
							|  |  |  |         """Return an image at index.""" | 
					
						
							|  |  |  |         return QByteArray(self.imageloader.binarydata).toBase64() | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | This setup allows a relatively clean call to the `viewer.image` property within | 
					
						
							|  |  |  | the QML context as shown below. Other data types such as `int`, `string`, | 
					
						
							|  |  |  | `float`, and booleans can be passed as expected without requiring the | 
					
						
							|  |  |  | QByteArray container. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```qml | 
					
						
							|  |  |  | Image { | 
					
						
							|  |  |  |   anchors.fill: parent | 
					
						
							|  |  |  |   fillMode: Image.PreserveAspectFit | 
					
						
							|  |  |  |   mipmap: true | 
					
						
							|  |  |  |   source = "data:image;base64," + viewer.image | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-19 13:41:50 +02:00
										 |  |  | ## Downside
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Debugging and designing QML in this environment is limited since the pyside | 
					
						
							|  |  |  | python library does not support all available QML/QT6 functionality. In most | 
					
						
							|  |  |  | cases you are looking at C++ Qt documentation for how the pyside data-types | 
					
						
							| 
									
										
										
										
											2022-06-13 17:39:51 +02:00
										 |  |  | and methods are supposed to behave without good hinting. Having developed | 
					
						
							|  |  |  | native C++/QML projects previously helps a lot. The main advantage here is t | 
					
						
							|  |  |  | hat QML source code / frame-works can be reused. | 
					
						
							| 
									
										
										
										
											2021-09-19 13:41:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-13 17:39:51 +02:00
										 |  |  | ## Other Notes:
 | 
					
						
							| 
									
										
										
										
											2021-09-19 13:41:50 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | ```python | 
					
						
							|  |  |  | ImageCms.profileToProfile(img, 'USWebCoatedSWOP.icc', | 
					
						
							|  |  |  |     'sRGB Color Space Profile.icm', renderingIntent=0, outputMode='RGB') | 
					
						
							|  |  |  | ``` |