mirror of
https://github.com/pythonguis/pythonguis-examples.git
synced 2025-01-13 16:42:55 +08:00
Add images, start of per-app README.md files.
This commit is contained in:
parent
2564c8c42c
commit
074c4c9970
@ -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.
|
||||
|
@ -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
9
notepad/README.md
Normal 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
11
notes/README.md
Normal 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
48
paint/README.md
Normal 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.
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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__':
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user