Add images, start of per-app README.md files.

This commit is contained in:
Martin Fitzpatrick 2018-02-19 02:16:27 +01:00
parent 2564c8c42c
commit 074c4c9970
7 changed files with 157 additions and 42 deletions

View File

@ -4,7 +4,7 @@ A collection of 15 small — *minute* — desktop applications written in Python
using the PyQt framework. These apps are intended as examples from
which you can poke, hack and prod your way to writing your own tools.
If you find these apps interesting, or want to learn more about
> If you think this example app is neat and want to learn more about
PyQt in general, [take a look at my ebook & online course
"Create Simple GUI Applications"](https://martinfitzpatrick.name/create-simple-gui-applications)
which covers everything you need to know to start building your own applications with PyQt.

View File

@ -63,7 +63,7 @@ class WorkerSignals(QObject):
class UpdateWorker(QRunnable):
'''
Worker thread for unzipping.
Worker thread for updating currency.
'''
signals = WorkerSignals()
is_interrupted = False

9
notepad/README.md Normal file
View File

@ -0,0 +1,9 @@
# No2Pads — A Notepad clone in PyQt
A very simple notepad clone using the QTextEdit widget to handle more or less
everything. Supports file loading, saving and printing.
> If you think this example app is neat and want to learn more about
PyQt in general, [take a look at my ebook & online course
"Create Simple GUI Applications"](https://martinfitzpatrick.name/create-simple-gui-applications)
which covers everything you need to know to start building your own applications with PyQt.

11
notes/README.md Normal file
View File

@ -0,0 +1,11 @@
# Brown Note — A desktop Post-it note application in PyQt
Take temporary notes on your desktop, with this floating-note app. Notes
are stored locally in a SQLite database.
![Brown note](screenshot-notes.png)
> If you think this example app is neat and want to learn more about
PyQt in general, [take a look at my ebook & online course
"Create Simple GUI Applications"](https://martinfitzpatrick.name/create-simple-gui-applications)
which covers everything you need to know to start building your own applications with PyQt.

48
paint/README.md Normal file
View File

@ -0,0 +1,48 @@
# Piecasso — A desktop Paint application in PyQt
Express yourself with PieCasso, the only painting programme to feature
ready made pictures of pie.
Piecasso is a clone of the Paint programme from Windows 95 (ish) with a
few additions (and subtractions). The programme features standard
tools including pen, brush, fill, spray can, eraser, text and a number of
shape drawing widgets.
![Piecasso](screenshot-paint.png)
You can copy from the image, with a custom shape,
although pasting + floating is not supported. The canvas is a fixed size
and loaded images are adjusted to fit. A stamp tool is also included
which is pre-loaded with pictures of delicious pie.
![Piecasso](screenshot-paint2.png)
> If you think this example app is neat and want to learn more about
PyQt in general, [take a look at my ebook & online course
"Create Simple GUI Applications"](https://martinfitzpatrick.name/create-simple-gui-applications)
which covers everything you need to know to start building your own applications with PyQt.
## Code notes
### Event handling
All tools are implemented with nested event handlers, which forward
on events as appropriate. This allows for a lot of code re-used between
tools which have common behaviours (e.g. shape drawing). Adding the select
region animation requires a timer (to update the crawling ants) which
added some complexity.
### Flood fill
This was the trickiest part of this app from a performance point of view.
Checking pixels directly is far too slow (full-canvas fill
time of approx 10 seconds). Most code to achieve this in Python sensibly
uses numpy, but I didn't want to introduce a dependency for this alone.
By exporting the image as a bytestring, then down-sampling to a boolean
byte-per-pixel (for match/no-match) to simplify the comparison loop, I
could get it up to a reasonable speed.
The search-to-fill algorithm is still pretty dumb though.

View File

@ -356,7 +356,8 @@ class Canvas(QLabel):
self.current_text = ""
self.timer_event = self.text_timerEvent
elif e.button() == Qt.RightButton and self.current_pos:
elif e.button() == Qt.LeftButton:
self.timer_cleanup()
# Draw the text to the image
p = QPainter(self.pixmap())
@ -370,6 +371,9 @@ class Canvas(QLabel):
self.reset_mode()
elif e.button() == Qt.RightButton and self.current_pos:
self.reset_mode()
def text_timerEvent(self, final=False):
p = QPainter(self.pixmap())
p.setCompositionMode(QPainter.RasterOp_SourceXorDestination)
@ -536,7 +540,7 @@ class Canvas(QLabel):
self.timer_cleanup()
p = QPainter(self.pixmap())
p.setPen(QPen(self.primary_color, 5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
p.setPen(QPen(self.primary_color, self.config['size'], Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
p.drawLine(self.origin_pos, e.pos())
self.update()
@ -583,7 +587,7 @@ class Canvas(QLabel):
def generic_poly_mouseDoubleClickEvent(self, e):
self.timer_cleanup()
p = QPainter(self.pixmap())
p.setPen(QPen(self.primary_color, 5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
p.setPen(QPen(self.primary_color, self.config['size'], Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
# Note the brush is ignored for polylines.
if self.secondary_color:

View File

@ -19,6 +19,60 @@ application.
"""
def from_ts_to_time_of_day(ts):
dt = datetime.fromtimestamp(ts)
return dt.strftime("%I%p").lstrip("0")
class WorkerSignals(QObject):
'''
Defines the signals available from a running worker thread.
'''
finished = pyqtSignal()
error = pyqtSignal(str)
result = pyqtSignal(dict, dict)
class WeatherWorker(QRunnable):
'''
Worker thread for weather updates.
'''
signals = WorkerSignals()
is_interrupted = False
def __init__(self, location):
super(WeatherWorker, self).__init__()
self.location = location
@pyqtSlot()
def run(self):
try:
params = dict(
q=self.location,
appid=OPENWEATHERMAP_API_KEY
)
url = 'http://api.openweathermap.org/data/2.5/weather?%s&units=metric' % urlencode(params)
r = requests.get(url)
weather = json.loads(r.text)
# Check if we had a failure (the forecast will fail in the same way).
if weather['cod'] != 200:
raise Exception(weather['message'])
url = 'http://api.openweathermap.org/data/2.5/forecast?%s&units=metric' % urlencode(params)
r = requests.get(url)
forecast = json.loads(r.text)
self.signals.result.emit(weather, forecast)
except Exception as e:
self.signals.error.emit(str(e))
self.signals.finished.emit()
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, *args, **kwargs):
@ -26,42 +80,45 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.setupUi(self)
self.pushButton.pressed.connect(self.update_weather)
self.pushButton.pressed.connect(self.update_forecast)
self.threadpool = QThreadPool()
self.show()
def from_ts_to_time_of_day(self, ts):
dt = datetime.fromtimestamp(ts)
return dt.strftime("%I%p").lstrip("0")
def alert(self, message):
alert = QMessageBox.warning(self, "Warning", message)
def update_weather(self):
params = dict(
q=self.lineEdit.text(),
appid=OPENWEATHERMAP_API_KEY
)
worker = WeatherWorker(self.lineEdit.text())
worker.signals.result.connect(self.weather_result)
worker.signals.error.connect(self.alert)
self.threadpool.start(worker)
url = 'http://api.openweathermap.org/data/2.5/weather?%s&units=metric' % urlencode(params)
r = requests.get(url)
data = json.loads(r.text)
def weather_result(self, weather, forecasts):
self.latitudeLabel.setText("%.2f °" % weather['coord']['lat'])
self.longitudeLabel.setText("%.2f °" % weather['coord']['lon'])
self.latitudeLabel.setText("%.2f °" % data['coord']['lat'])
self.longitudeLabel.setText("%.2f °" % data['coord']['lon'])
self.windLabel.setText("%.2f m/s" % weather['wind']['speed'])
self.windLabel.setText("%.2f m/s" % data['wind']['speed'])
self.temperatureLabel.setText("%.1f °C" % weather['main']['temp'])
self.pressureLabel.setText("%d" % weather['main']['pressure'])
self.humidityLabel.setText("%d" % weather['main']['humidity'])
self.temperatureLabel.setText("%.1f °C" % data['main']['temp'])
self.pressureLabel.setText("%d" % data['main']['pressure'])
self.humidityLabel.setText("%d" % data['main']['humidity'])
self.sunriseLabel.setText(self.from_ts_to_time_of_day(data['sys']['sunrise']))
self.sunriseLabel.setText(from_ts_to_time_of_day(weather['sys']['sunrise']))
self.weatherLabel.setText("%s (%s)" % (
data['weather'][0]['main'],
data['weather'][0]['description']
)
weather['weather'][0]['main'],
weather['weather'][0]['description']
)
)
self.set_weather_icon(self.weatherIcon, data['weather'])
self.set_weather_icon(self.weatherIcon, weather['weather'])
for n, forecast in enumerate(forecasts['list'][:5], 1):
getattr(self, 'forecastTime%d' % n).setText(from_ts_to_time_of_day(forecast['dt']))
self.set_weather_icon(getattr(self, 'forecastIcon%d' % n), forecast['weather'])
getattr(self, 'forecastTemp%d' % n).setText("%.1f °C" % forecast['main']['temp'])
def set_weather_icon(self, label, weather):
label.setPixmap(
@ -72,20 +129,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
)
def update_forecast(self):
params = dict(
q=self.lineEdit.text(),
appid=OPENWEATHERMAP_API_KEY
)
url = 'http://api.openweathermap.org/data/2.5/forecast?%s&units=metric' % urlencode(params)
r = requests.get(url)
data = json.loads(r.text)
for n, forecast in enumerate(data['list'][:5], 1):
getattr(self, 'forecastTime%d' % n).setText(self.from_ts_to_time_of_day(forecast['dt']))
self.set_weather_icon(getattr(self, 'forecastIcon%d' % n), forecast['weather'])
getattr(self, 'forecastTemp%d' % n).setText("%.1f °C" % forecast['main']['temp'])
if __name__ == '__main__':