mirror of
https://github.com/pythonguis/pythonguis-examples.git
synced 2025-01-20 16:52:54 +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
|
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.
|
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
|
PyQt in general, [take a look at my ebook & online course
|
||||||
"Create Simple GUI Applications"](https://martinfitzpatrick.name/create-simple-gui-applications)
|
"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.
|
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):
|
class UpdateWorker(QRunnable):
|
||||||
'''
|
'''
|
||||||
Worker thread for unzipping.
|
Worker thread for updating currency.
|
||||||
'''
|
'''
|
||||||
signals = WorkerSignals()
|
signals = WorkerSignals()
|
||||||
is_interrupted = False
|
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.current_text = ""
|
||||||
self.timer_event = self.text_timerEvent
|
self.timer_event = self.text_timerEvent
|
||||||
|
|
||||||
elif e.button() == Qt.RightButton and self.current_pos:
|
elif e.button() == Qt.LeftButton:
|
||||||
|
|
||||||
self.timer_cleanup()
|
self.timer_cleanup()
|
||||||
# Draw the text to the image
|
# Draw the text to the image
|
||||||
p = QPainter(self.pixmap())
|
p = QPainter(self.pixmap())
|
||||||
@ -370,6 +371,9 @@ class Canvas(QLabel):
|
|||||||
|
|
||||||
self.reset_mode()
|
self.reset_mode()
|
||||||
|
|
||||||
|
elif e.button() == Qt.RightButton and self.current_pos:
|
||||||
|
self.reset_mode()
|
||||||
|
|
||||||
def text_timerEvent(self, final=False):
|
def text_timerEvent(self, final=False):
|
||||||
p = QPainter(self.pixmap())
|
p = QPainter(self.pixmap())
|
||||||
p.setCompositionMode(QPainter.RasterOp_SourceXorDestination)
|
p.setCompositionMode(QPainter.RasterOp_SourceXorDestination)
|
||||||
@ -536,7 +540,7 @@ class Canvas(QLabel):
|
|||||||
self.timer_cleanup()
|
self.timer_cleanup()
|
||||||
|
|
||||||
p = QPainter(self.pixmap())
|
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())
|
p.drawLine(self.origin_pos, e.pos())
|
||||||
self.update()
|
self.update()
|
||||||
@ -583,7 +587,7 @@ class Canvas(QLabel):
|
|||||||
def generic_poly_mouseDoubleClickEvent(self, e):
|
def generic_poly_mouseDoubleClickEvent(self, e):
|
||||||
self.timer_cleanup()
|
self.timer_cleanup()
|
||||||
p = QPainter(self.pixmap())
|
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.
|
# Note the brush is ignored for polylines.
|
||||||
if self.secondary_color:
|
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):
|
class MainWindow(QMainWindow, Ui_MainWindow):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -26,42 +80,45 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
|
|
||||||
self.pushButton.pressed.connect(self.update_weather)
|
self.pushButton.pressed.connect(self.update_weather)
|
||||||
self.pushButton.pressed.connect(self.update_forecast)
|
|
||||||
|
self.threadpool = QThreadPool()
|
||||||
|
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def from_ts_to_time_of_day(self, ts):
|
|
||||||
dt = datetime.fromtimestamp(ts)
|
def alert(self, message):
|
||||||
return dt.strftime("%I%p").lstrip("0")
|
alert = QMessageBox.warning(self, "Warning", message)
|
||||||
|
|
||||||
def update_weather(self):
|
def update_weather(self):
|
||||||
params = dict(
|
worker = WeatherWorker(self.lineEdit.text())
|
||||||
q=self.lineEdit.text(),
|
worker.signals.result.connect(self.weather_result)
|
||||||
appid=OPENWEATHERMAP_API_KEY
|
worker.signals.error.connect(self.alert)
|
||||||
)
|
self.threadpool.start(worker)
|
||||||
|
|
||||||
url = 'http://api.openweathermap.org/data/2.5/weather?%s&units=metric' % urlencode(params)
|
def weather_result(self, weather, forecasts):
|
||||||
r = requests.get(url)
|
self.latitudeLabel.setText("%.2f °" % weather['coord']['lat'])
|
||||||
data = json.loads(r.text)
|
self.longitudeLabel.setText("%.2f °" % weather['coord']['lon'])
|
||||||
|
|
||||||
self.latitudeLabel.setText("%.2f °" % data['coord']['lat'])
|
self.windLabel.setText("%.2f m/s" % weather['wind']['speed'])
|
||||||
self.longitudeLabel.setText("%.2f °" % data['coord']['lon'])
|
|
||||||
|
|
||||||
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.sunriseLabel.setText(from_ts_to_time_of_day(weather['sys']['sunrise']))
|
||||||
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.weatherLabel.setText("%s (%s)" % (
|
self.weatherLabel.setText("%s (%s)" % (
|
||||||
data['weather'][0]['main'],
|
weather['weather'][0]['main'],
|
||||||
data['weather'][0]['description']
|
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):
|
def set_weather_icon(self, label, weather):
|
||||||
label.setPixmap(
|
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__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user