from Guide to Hacking on Jun 11, 2023
How a Python-only desktop app works
There's a new breed of cross-platform, browser-like wrappers that are both easy to build and feature-ful, supporting a number of native features such as offline support, deeplinking, notifications, and more.
Native desktop applications support a number of features — offline support, deeplinking, notifications, and more — but are not easy to build; they often require platform-specific development tools and languages, that aren't generalizable across platforms. On the other hand, web applications are easier to build but don't support these exclusive features.
Previously, this effectively meant that you traded developer time for native features. However, with a new set of browser-based cross-platform desktop applications, this calculus has changed.
Introducing browser-based desktop applications
One of the first mainstream projects to this effect was Electron, which enabled easy deployment of NodeJS-powered applications. In short, the application hosts a local web server and opens a browser.
A slew of other projects, such as neutralino or tauri, may instead open a webview, which you can consider a minimal browser to render webpages. As a result of this, we can create desktop applications with the ease of web applications.
These web-based desktop applications make native application features accessible, a select few of which we'll cover:
- offline: Be able to run the desktop application without an internet connection.
- deeplink: Support what is formally called a "custom URL protocol". In other words, support links that directly connect to a specific page or action in your desktop application. See How a Python-only deeplink works.
Let's now build one of these desktop applications.
Hello world desktop application
This tutorial will cover a Python-only desktop application. In particular, our application uses pywebview, which creates a minimal desktop application by launching both a web server and a webview. Create and navigate to a directory for your project.
mkdir ~/Desktop/desktopapp
cd ~/Desktop/desktopapp
Next, create a virtual environment in this directory. For this tutorial, don't use Anaconda or a Python from Anaconda. If you do, you'll encounter a launch error later when we attempt to build the application, which we'll bring up again when that happens.
python -m venv env
source env/bin/activate
Then, install the pywebview package.
pip install pywebview==4.2.2
For your hello world desktop application, create a webview that simply loads an external webpage, to start. Write this to a file called app.py
.
import webview
webview.create_window('Hello world', 'https://alvinwan.com/blog')
webview.start()
So far, this application simply instantiates a webview. Optionally, check this against our reference implementation. Let's try this now. Run the following to launch your desktop application.
python app.py
You should now see the following "hello world" desktop application.
You can close the application either by clicking on the window's "close" button or by killing the Python process using CTRL+K
in the terminal.
Offline desktop application
Next, we'll combine this with a local web server. For simplicity, we'll use the common web framework Flask; if you'd like to learn more, see How a web application works for how it works and basic usage. Install the Flask package.
pip install Flask==2.3.2
Below, in app.py
, combine the boilerplate pywebview code with boilerplate Flask code.
from flask import Flask
import webview
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
if __name__ == '__main__':
webview.create_window('Flask example', app)
webview.start()
Optionally, check this against our reference implementation. Run your desktop application again using the following.
python app.py
You should now see a "Hello world!" webpage in your desktop application.
This is now a web-based desktop application that can be run offline, without an internet connection.
Share-able desktop application
Next, note that this desktop application is not easily share-able; namely, the end user needs to have Python and the Python dependencies installed. That isn't a particularly easy-to-setup program, so let's now package this into a standalone program that doesn't rely on external binaries such as Python.
Start by installing another library called py2app
. This particular utility will convert our Python script into a standalone, runnable desktop application.
pip install py2app
Start by creating a new setup.py
file. This will contain some configuration options for how we build a share-able program.
from setuptools import setup
setup(
app=['app.py'], # your main application
setup_requires=['flask', 'py2app', 'pywebview'], # add other dependencies here
)
Let's now build our desktop application. Optionally, check with our reference implementation. To build the desktop application, use the py2app
command.
python setup.py py2app -A
Above, the -A
flag behaves like pip install
's -e
flag, meaning that the installation is linked to the original source code. No need to rebuild the application, after making source code changes[^3]. This is just for now, during development.
Next, open the application in the dist/
folder.
open dist/app.app
This should now launch your desktop application just like before.
With that said, this is not yet a standalone desktop application. To accomplish this, let's rebuild the desktop application but without the -A
alias flag this time.
python setup.py py2app
This build process will take slightly longer, but once complete, you'll have another desktop application available at dist/app.app
. This time, the application is a standalone program that you can share with anyone.
Let's open this application one more time.
open dist/app.app
You should see the same preview as before, completing your very first hello world, standalone desktop application. You can now share this app.app
with anyone, and even without the right Python version or Python dependencies, they should be able to run your application out of the box.
How does a browser-based native app work?
To understand how our browser-based desktop application works, we'll peel back the layers of abstraction one by one until we end up at code that compiles to a native desktop application.
- pywebview is a light wrapper around several different "engines". Engines are simply implementations of webviews that you can use as a browser to render content. On Mac, compatible engines include GTK, Qt, and the OS-default webkit.
- The pywebview Qt engine is written using qtpy, which abstracts away two popular Python bindings for the Qt library — both PyQt and PySide. In particular, pywebview uses qtpy's PyQt backend. As a result, to customize pywebview's Qt engine, we'd refer to PyQt's documentation.
- PyQt in turn is a set of Python bindings for Qt, an open-source C++ library that provides tools for creating GUIs across multiple platforms.
In summary, pywebview uses a qtpy engine, which wraps PyQt, which provides Python bindings for Qt. The entire stack is furthermore very mature tooling with a decade or more of development.
- pywebview, our highest layer of abstraction, started in 2014
- qtpy, the "engine" providing our webview, started in 2015
- PyQt, the underlying Python bindings, started in 1998
- Qt, the core GUI implementation, started in 1995
Above, we focus on Qt due to its clear preference over the years, per a Google Trend chart comparing Qt, GTK, and Objective-C. We repeat the same check with Python bindings, finding PyQt the winner among pyobjc, pyqt, pygtk.
Let's now tackle more advanced customizations of pywebview, using our knowledge of this above stack.
Advanced features via PyQt
Let's say we want to change the title bar for our application anytime it isn't in focus. Unfortunately, pywebview doesn't appear to provide this functionality, as pywebview only provides 9 window-based events. The closest one, on_shown
only triggers when the application launches for the first time.
However, the full list of possible events that the underlying PyQt supports is much more expansive, with 140 total events. Some notable but missing events include timezone changes, clipboard content changes, and most importantly for us — focus changes. In light of this, let's implement a Python-only desktop application using PyQt.
Create a new directory for this version of the project.
mkdir -p ~/Desktop/desktopapp/pyqt
cd ~/Desktop/desktopapp/pyqt
In the same virtual environment as before, install additional Qt libraries. We install the GUI library PyQt, as well as a thin abstraction layer qtpy.
pip install qtpy==2.3.1 pyqt6==6.5.1
Then, create a minimal PyQt application in app.py
. In particular, we'll use the thin qtpy
wrapper.
from qtpy.QtWidgets import QApplication, QMainWindow
from qtpy.QtCore import QEvent
class MyApp(QApplication):
def __init__(self, argv):
super().__init__(argv)
self.window = QMainWindow()
self.window.show()
def event(self, event: QEvent):
if event.type() == QEvent.Type.ApplicationActivate:
self.window.setWindowTitle("In focus")
if event.type() == QEvent.Type.ApplicationDeactivate:
self.window.setWindowTitle("Not in focus")
return super().event(event)
if __name__ == '__main__':
app = MyApp([])
app.exec()
Optionally, check against our reference implementation. Run your desktop app as you would any Python script.
python app.py
This should launch a small window with the title "In focus" when the window is focused and "Not in focus" when not.
We've now successfully implemented a desktop application that can respond to focus events, using PyQt directly.
However, as we discussed above, pywebview makes development easier by allowing us to build a GUI with web technologies. Ideally, then, we'd like to use pywebview instead of PyQt directly.
Advanced Customization of Pywebview
Ideally, we would register the same event handler with pywebview directly. Unfortunately, pywebview currently doesn't support extensions to its event handling ability. To hack around this, we can change pywebview's engine to PyQt and manually add an event handler to the underlying PyQt application.
Let's now integrate this back into pywebview, by manually modifying the underlying PyQt engine. Navigate back to your pywebview application.
cd ~/Desktop/desktopapp/
Pywebview relies on the PyQt web engine, which isn't included in the PyQt libraries we've installed so far. Install the web engine separately.
pip install PyQt6-WebEngine==6.5.0
Insert some new imports for the new GUI library PyQt that we adopted in the last step.
import sys
import webview
from qtpy.QtWidgets import QApplication
from qtpy.QtCore import QEvent
import webview.platforms.qt # needed for us to define a custom application
After your imports at the top of the file, define a custom application1 with a custom event handler.
class MyApp(QApplication):
def event(self, event: QEvent):
if event.type() == QEvent.Type.ApplicationActivate:
window.set_title("In focus")
if event.type() == QEvent.Type.ApplicationDeactivate:
window.set_title("Not in focus")
return super(QApplication, self).event(event)
Then, instantiate your application. This way, pywebview will use your custom application.
Additionally, modify your webview start call to change the GUI engine to Qt.
if __name__ == '__main__':
app = MyApp(sys.argv)
window = webview.create_window('Hello world', 'https://alvinwan.com/blog/')
webview.start(gui='qt')
Check against our reference implementation. Now, rerun your pywebview application.
python app.py
You should now see the same effect: While the application window is focused, the title bar will read "In focus" and "Not in focus" otherwise.
You now have successfully enhanced your browser-based desktop application with some advanced customization.
More broadly, you've implemented a fairly important and useful extension to your browser-based desktop application: Namely, your application can now handle a wide variety of events. We've only explored one here — focus changes — but you can now handle any native system event via PyQt. You can learn more about different types of events from the official qt documentation on PyQt6's Events and Filters.
Conclusion
In this tutorial, we built a minimal Python-only desktop application; the application is share-able, works offline and can even support an expanded set of events, making the app a very native-like experience.
Despite all this, we never left the comfort of Python. In fact, we used familiar web frameworks and technologies, instead of learning proprietary, platform-specific GUI libraries and languages.
With this launching point for a Python-only desktop application, you should be well-prepared to build a native desktop experience for any task.
You can find the source code for this tutorial, as well as the finished code, on my "Guide to Hacking" repository.
-
Notice we specifically patched the application-level object. Individual webviews and webpages within the application also have event handlers but won't be notified of application-level events like file opens. ↩
Want more tips? Drop your email, and I'll keep you in the loop.