💾 Archived View for gluonspace.com › gemlog › glueather_a_python_qt_weather_application.gmi captured on 2022-04-28 at 18:04:41. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2022-03-01)
-=-=-=-=-=-=-
After the success of the weather service here at gluonspace, I've decided to reuse some of the code and the minimalist spirit of Gemini to create a simple GUI app called glueather. I'm mostly a command line kind of user, but I also have periods of my life when I think a GUI based environment, as long as you keep it relatively simple, can work equally well. I'm going through one of those phases, so I decided to implement a new app.
The code is simple enough to be followed like a real world kind of hands on tutorial to learn Python and the Qt library at an introductory level. I've been using Python for many years, but this was the first time I actually bothered creating an official PyPI package, a learning experience in itself. I've released the code as opensource on Github and PyPI:
I've used many different widget toolkits in the past and honestly I share the opinion that you should pick the right tool for the job. There's no right or wrong answer, just go with what you think makes sense. Since I picked Python for this project, Tkinter sounded like a natural choice, however, having created Tkinter apps in the past, I knew that although it feels very easy to use, the looks and consistency aren't its main features. wxWidgets is another one I know quite well from previous projects, but I also remember it had shortcomings.
As a fan of KDE Plasma and the looks as well as the consistency of their Qt apps, I've always wanted to code a Qt app but never really took the time to learn it. I knew beforehand it was a complex library, with many modules and a comprehensive hierarchy of classes, thus I've always sort of resisted using Qt. This time however, I dove right in. The Python bindings for Qt are quite mature, both in the form of PyQt and Pyside. Since Python's class syntax isn't too dissimilar to C++, you can even read Qt's C++ documentation, which is extremely well written.
Surprisingly, despite its size, I found Qt quite easy to learn and very predictable, something I always value when learning a new platform. In a couple of days I could already build a simple barebones app. I wanted to keep the output of my weather app simple, so instead of using many different widgets, I've opted to use just a QTextEdit, which supports both HTML and markup, a neat way to format the weather data, in a similar way to my Gemini service.
The only code which wasn't trivial to implement was a worker thread that I've ended up using simply to avoid freezing the GUI while fetching weather data from the Internet using the OpenWeather API. That being said, Qt has this very interesting mechanism that uses signals and slots to allow communication between widgets and your code. It's an alternative to the traditional callbacks that allows handling all sorts of events and message passing, including sending data from a thread to another in an easy way.
Below you can see the code for the worker thread. In reality, it's implemented as a class that inherits from a generic QObject that is later passed to a QThread object, the proper thread. Anyway, what's relevant here is the "finished" class variable, defined as "finished = pyqtSignal(object)". This creates a signal that I emit later on as my worker thread finishes its job sending any object I need as an argument. In this case I emit a finished signal with a dictionary containing the results by calling "self.finished.emit(result)". The result is sent to a slot (which is just a function called update_weather) in the main thread that handles the contents and updates th GUI.
class WeatherWorker(QObject): finished = pyqtSignal(object) def __init__(self, api_key, units, location, current, hourly, daily): super().__init__() self.api_key = api_key self.units = units self.location = location self.current = current self.hourly = hourly self.daily = daily def run(self): result = {} try: wm = WeatherManager(self.api_key, self.units) if self.current: current_weather = wm.current(self.location) result['current_weather'] = current_weather if self.hourly: hourly_weather = wm.hourly(self.location) result['hourly_weather'] = hourly_weather if self.daily: daily_weather = wm.daily(self.location) result['daily_weather'] = daily_weather self.finished.emit(result) except WeatherError as e: result['error'] = e.message self.finished.emit(result)
def update_weather(self, result): self.ui.lineEdit.setEnabled(True) self.ui.pushButton.setEnabled(True) if 'error' in result.keys(): QMessageBox.warning(self, "Error", result['error']) return ...
This is definitely my longest Gemini article since I've started my gemlog. Luckily I was kind enough to use the first paragraph as the gist and I even provided the link to the code right after that. Nevertheless, if you're still with me, congratulations on making it this far, hopefully I wasn't too boring. As a final note and bonus for the reader I'd like to share here a very simple, yet elegant function that I'm using to convert a wind direction angle to a wind cardinal direction without a train of if statements (as is often the case):
def _wind_cardinal(self, angle): try: angle = int(angle) if angle < 0 or angle > 360: return 'N/A' except: return 'N/A' index = math.floor((angle / 22.5) + 0.5) cardinal = ('⭣ N', '⭩ NNE', '⭩ NE', '⭩ ENE', '⭠ E', '⭦ ESE', '⭦ SE', '⭦ SSE', '⭡ S', '⭧ SSW', '⭧ SW', '⭧ WSW', '⭢ W', '⭨ WNW', '⭨ NW', '⭨ NNW') return cardinal[index % 16]