In my last article, I discussed why using Pywebview is a better alternative to Electron for Python programmers when it comes to building graphical user interfaces for cross-platform desktop applications.
To summarise
- Pywebview allows all design capability that comes with modern web design but allows the backend to be written in Python.
- Pywebview executables are also much smaller than the equivalent software written in Electron.
- Development is much easier because there is no complex IPC communication needed to pass information from the backend to the front end and visa-versa.
My favourite way of developing front-end projects is to use the Quasar framework which is based on Vue.js. Quasar is a combination of a library of material-design-based UI components as well as a suite of build tools that allow a single app to be turned into a web page, phone app, or even an electron application (although why would you). Quasar also provides hot-reloading which makes the development cycle super fast.
Pywebview provides methods for javascript to be called from Python-land and for Python methods to be called from Javascript-land.
To make Quasar and Pywebview work together even better a Quasar “boot” file can be written as an interface to Python methods. Quasar boot files are equivalent to adding custom logic to the main.js file in a regular Vue.js application.
The Quasr boot file allows methods to provide a nice interface between Python and Javascript, as well as to enable variables to be made reactive so that changes that originate in Python-land can effortlessly propagate to the user interface.
The goal is to provide an interface on the Vue.js instance which can be accessed with this.$myapp for example, from within a Vue component:
this.$myapp.pythonMethodWrapper(data)
While at the same time allowing data received from Python land to interact with the Vue context.
Step 1
Create a new boot file in the boot folder of the Quasar application (myapp.js)
Step 2
Enable the boot file in the quasar.config.js file. The Notify plugin is used later for error handling.
boot: ["notify", "myapp"],
Step 3
Populate the myapp.js file with this code:
Code Explanation
The start of the file includes the necessary dependencies for boot files and registers the $myapp namespace.
import {boot} from "quasar/wrappers"import {Notify} from "quasar"import {ref} from "vue"export default boot(({app}) => { app.config.globalProperties.$myapp = { }}
Next, some variables can be defined; reactive and non-reactive variables can be created. While both reactive and non-reactive variables can be referenced directly from Vue components, the difference is that whenever a reactive variable is updated, it can trigger a UI change with no additional coding.
app.config.globalProperties.$myapp = { // unreactive variables options: [], subscriptions: {},// reactive variables. Reactivity comes with a performance penalty so only use it when needed mode: ref(0),
A special setup method is needed to register the app into the global namespace. This is so it can easily be referenced from Python.
This is also where the methods are linked with the Vue context. Any methods which are to be called from Python need to be registered
first. This is achieved by calling .bind(this).
/** * Setup the global $myapp object which can easily be accessed from python land */ setup: function () { window.$myapp = {} // methods which can be called from Python land need to registered here // .bind(this) ensures that the function is called with the correct context (this) window.$myapp.doThing = this.doThing.bind(this) window.$myapp.addData = this.receiveData.bind(this) }, /** * Methods designed to be called from Python */ /** * Receive data from Python * @param data */ doThing: function (data) { console.log(data) }, /** * Methods designed to be called from inside the Quasar application */ /** * Example usage from within a vue component: this.$myapp.getMode() */ getMode: function () { window.pywebview.api.get_mode().then(res => { // call the python function get_mode() console.log(res) this.mode = res.mode // set the mode variable to the result of the python function }).catch(err => { this._error(err) }) },
Bonus: Error Handling
Javascript Exceptions propagate through to Javascript land and trigger the .catch handler.
The Quasar Notify method can be used to create an error popup.
/** * Display an error message. * * Ensure to enable the notification plugin in quasar.conf.js * @param message * @private */ _error: function (message) { Notify.create({ message: message.toString().replace("Exception: ", ""), // for Python exceptions replace 'Exce;ption' to make it display better color: "red" }) },
Finally, the setup method should be called.
export default boot(({app}) => { … app.config.globalProperties.$myapp.setup() // call the setup function to setup the $myapp object})
Bonus 2
A simple publish and subscribe interface can easily be included to allow data from Python to be reacted to. This is especially in a situation where Python is generating data that should be displayed in real time.
/** * Basic Pub/Sub functionality */ /** * Subscribe to events when new data is published. * * Example usage from within a vue component: this.$myapp.subscribeToData('uniquekey', function(data) { ... }) * * @param key Unique key * @param fn Function to call */ subscribeToData: function (key, fn) { this.subscriptions[key] = fn }, /** * Unsubscribe from data events. * * Example usage from within a vue component: this.$myapp.unsubscribeFromData('uniquekey') * @param key Key that was used to subscribe */ unsubscribeFromData: function (key) { delete this.subscriptions[key] }, } /** * Python calls: * js = f'window.$myapp.receiveData({json.dumps(data)})' * webview.windows[0].evaluate_js(js) * @param data */ receiveData: function (data) { for (const subscription in this.subscriptions) { this.subscriptions[subscription](data) } }
Conclusion
By creating a simple Qusar boot file, the Vue-world and Python world can be connected using a clean interface that allows for clean decoupled code, reactive variables, and a publish & subscribe model.