` element in HTML.).
+
+To tell Qt to arrange our buttons vertically, we create a `QVBoxLayout`:
+
+ layout = QVBoxLayout()
+
+Then, we add the two buttons to it:
+
+ layout.addWidget(QPushButton('Top'))
+ layout.addWidget(QPushButton('Bottom'))
+
+Finally, we add the layout - and thus its contents - to the `window` we created above:
+
+ window.setLayout(layout)
+
+We conclude by showing the window and (as is required) handing control over to Qt:
+
+ window.show()
+ app.exec_()
+
+For instructions how you can run this example yourself, please see [here](https://github.com/1mh/pyqt-examples#running-the-examples).
+
+The related [`QHBoxLayout`](https://doc.qt.io/qt-5/qhboxlayout.html) positions items horizontally. For an even more powerful approach, see [`QGridLayout`](https://doc.qt.io/qt-5/qgridlayout.html).
diff --git a/src/03 QVBoxLayout PyQt5/main.py b/src/03 QVBoxLayout PyQt5/main.py
new file mode 100644
index 0000000..b5550a4
--- /dev/null
+++ b/src/03 QVBoxLayout PyQt5/main.py
@@ -0,0 +1,9 @@
+from PyQt5.QtWidgets import *
+app = QApplication([])
+window = QWidget()
+layout = QVBoxLayout()
+layout.addWidget(QPushButton('Top'))
+layout.addWidget(QPushButton('Bottom'))
+window.setLayout(layout)
+window.show()
+app.exec_()
\ No newline at end of file
diff --git a/src/03 QVBoxLayout PyQt5/qvboxlayout-pyqt5.png b/src/03 QVBoxLayout PyQt5/qvboxlayout-pyqt5.png
new file mode 100644
index 0000000..19882ba
Binary files /dev/null and b/src/03 QVBoxLayout PyQt5/qvboxlayout-pyqt5.png differ
diff --git a/src/04 PyQt Signals and Slots/README.md b/src/04 PyQt Signals and Slots/README.md
new file mode 100644
index 0000000..682aae0
--- /dev/null
+++ b/src/04 PyQt Signals and Slots/README.md
@@ -0,0 +1,32 @@
+# PyQt Signals and Slots
+
+PyQt Signals let you react to user input such as mouse clicks. A *slot* is a function that gets called when such an event occurs. The file [`main.py`](main.py) in this directory shows this in action: When the user clicks a button, a popup appears:
+
+

+
+The code begins in the usual way. First, we import PyQt5 and create a `QApplication`:
+
+ from PyQt5.QtWidgets import *
+ app = QApplication([])
+
+Next, we create a button:
+
+ button = QPushButton('Click')
+
+Then we define a function. It will be called when the user clicks the button. You can see that it shows an alert:
+
+ def on_button_clicked():
+ alert = QMessageBox()
+ alert.setText('You clicked the button!')
+ alert.exec_()
+
+And here is where signals and slots come into play: We instruct Qt to invoke our function by _connecting_ it to the `.clicked` signal of our button:
+
+ button.clicked.connect(on_button_clicked)
+
+Finally, we show the button on the screen and hand control over to Qt:
+
+ button.show()
+ app.exec_()
+
+For instructions how you can run this example yourself, please see [here](https://github.com/1mh/pyqt-examples#running-the-examples).
diff --git a/src/04 PyQt Signals and Slots/main.py b/src/04 PyQt Signals and Slots/main.py
new file mode 100644
index 0000000..279b349
--- /dev/null
+++ b/src/04 PyQt Signals and Slots/main.py
@@ -0,0 +1,13 @@
+from PyQt5.QtWidgets import *
+
+app = QApplication([])
+button = QPushButton('Click')
+
+def on_button_clicked():
+ alert = QMessageBox()
+ alert.setText('You clicked the button!')
+ alert.exec_()
+
+button.clicked.connect(on_button_clicked)
+button.show()
+app.exec_()
\ No newline at end of file
diff --git a/src/04 PyQt Signals and Slots/pyqt-signals-and-slots.jpg b/src/04 PyQt Signals and Slots/pyqt-signals-and-slots.jpg
new file mode 100644
index 0000000..98e1ba1
Binary files /dev/null and b/src/04 PyQt Signals and Slots/pyqt-signals-and-slots.jpg differ
diff --git a/src/05 Qt Designer Python/README.md b/src/05 Qt Designer Python/README.md
new file mode 100644
index 0000000..da9cb26
--- /dev/null
+++ b/src/05 Qt Designer Python/README.md
@@ -0,0 +1,47 @@
+# Qt Designer Python
+
+[Qt Designer](https://build-system.fman.io/qt-designer-download) is a graphical tool for building Qt GUIs:
+
+

+
+It produces `.ui` files. You can load these files from C++ or Python to display the GUI.
+
+The dialog in the following screenshot comes from the file [`dialog.ui`](dialog.ui) in this directory:
+
+

+
+The [`main.py`](main.py) script (also in this directory) loads and invokes `dialog.ui` from Python. The steps with which it does this are quite easy.
+
+First, [`main.py`](main.py) imports the `uic` module from PyQt5:
+
+ from PyQt5 import uic
+
+It also imports `QApplication`. Like all (Py)Qt apps, we must create an instance of this class.
+
+ from PyQt5.QtWidgets import QApplication
+
+Then, we use [`uic.loadUiType(...)`](https://www.riverbankcomputing.com/static/Docs/PyQt5/designer.html#PyQt5.uic.loadUiType) to load the `.ui` file. This returns two classes, which we call `Form` and `Window`:
+
+ Form, Window = uic.loadUiType("dialog.ui")
+
+The first is an ordinary Python class. It has a `.setupUi(...)` method which takes a single parameter, the [widget](../02%20PyQt%20Widgets) in which the UI should be displayed. The type of this parameter is given by the second class, `Window`. This is configured in Qt Designer and is usually one of `QDialog`, `QMainWindow` or `QWidget`.
+
+To show the UI, we thus proceed as follows. First, we create the necessary `QApplication`:
+
+ app = QApplication([])
+
+Then, we instantiate the `Window` class. It will act as the container for our user interface:
+
+ window = Window()
+
+Next, we instantiate the `Form`. We invoke its `.setupUi(...)` method, passing the window as a parameter:
+
+ form = Form()
+ form.setupUi(window)
+
+We've now connected the necessary components for displaying the user interface given in the `.ui` file. All that remains is to `.show()` the window and kick off Qt's event processing mechanism:
+
+ window.show()
+ app.exec_()
+
+For instructions how to run this example yourself, please see [here](https://github.com/1mh/pyqt-examples#running-the-examples).
diff --git a/src/05 Qt Designer Python/dialog.ui b/src/05 Qt Designer Python/dialog.ui
new file mode 100644
index 0000000..a79c9ff
--- /dev/null
+++ b/src/05 Qt Designer Python/dialog.ui
@@ -0,0 +1,68 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 197
+ 72
+
+
+
+ Dialog
+
+
+
+
+ -160
+ 20
+ 341
+ 32
+
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Dialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Dialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/05 Qt Designer Python/main.py b/src/05 Qt Designer Python/main.py
new file mode 100644
index 0000000..7626b78
--- /dev/null
+++ b/src/05 Qt Designer Python/main.py
@@ -0,0 +1,11 @@
+from PyQt5 import uic
+from PyQt5.QtWidgets import QApplication
+
+Form, Window = uic.loadUiType("dialog.ui")
+
+app = QApplication([])
+window = Window()
+form = Form()
+form.setupUi(window)
+window.show()
+app.exec_()
\ No newline at end of file
diff --git a/src/05 Qt Designer Python/qt-designer-python.png b/src/05 Qt Designer Python/qt-designer-python.png
new file mode 100644
index 0000000..13568eb
Binary files /dev/null and b/src/05 Qt Designer Python/qt-designer-python.png differ
diff --git a/src/05 Qt Designer Python/qt-designer-windows.png b/src/05 Qt Designer Python/qt-designer-windows.png
new file mode 100644
index 0000000..dee8c10
Binary files /dev/null and b/src/05 Qt Designer Python/qt-designer-windows.png differ
diff --git a/src/06 QML Python example/LICENSE.md b/src/06 QML Python example/LICENSE.md
new file mode 100644
index 0000000..61345cd
--- /dev/null
+++ b/src/06 QML Python example/LICENSE.md
@@ -0,0 +1,35 @@
+This notice pertains to the following files:
+
+ * main.qml
+ * pinwheel.png
+ * background.png
+
+They are modified versions of code / images which are originally:
+
+Copyright (c) 2012-2014, Juergen Bocklage Ryannel and Johan Thelin
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/src/06 QML Python example/README.md b/src/06 QML Python example/README.md
new file mode 100644
index 0000000..5b36be7
--- /dev/null
+++ b/src/06 QML Python example/README.md
@@ -0,0 +1,58 @@
+# QML Python example
+
+Qt can be broadly split into two technologies: _Qt Widgets_ is the old core. It displays GUI elements in a way that is typical for operating systems such as Windows or macOS. A more recent alternative is _Qt Quick_. This technology is optimized for mobile and touch screen devices. It is better suited for very custom graphics and fluid animations.
+
+Qt Quick uses a markup language called QML. This example shows how you can combine QML with Python.
+
+

+
+The sample application displays a pin wheel in front of some hills. When you click with the mouse, the wheel rotates.
+
+The QML code lies in [`main.qml`](main.qml). It's a testament to QML that it is quite easy to read:
+
+```
+import QtQuick 2.2
+import QtQuick.Window 2.2
+
+Window {
+ Image {
+ id: background
+ source: "background.png"
+ }
+ Image {
+ id: wheel
+ anchors.centerIn: parent
+ source: "pinwheel.png"
+ Behavior on rotation {
+ NumberAnimation {
+ duration: 250
+ }
+ }
+ }
+ MouseArea {
+ anchors.fill: parent
+ onPressed: {
+ wheel.rotation += 90
+ }
+ }
+ visible: true
+ width: background.width
+ height: background.height
+}
+```
+
+Executing the QML from Python is even easier. The code is in [`main.py`](main.py):
+
+```
+from PyQt5.QtQml import QQmlApplicationEngine
+from PyQt5.QtWidgets import QApplication
+
+app = QApplication([])
+engine = QQmlApplicationEngine()
+engine.load("main.qml")
+app.exec_()
+```
+
+If you'd like further instructions how you can run this code for yourself, please see [here](https://github.com/1mh/pyqt-examples#running-the-examples).
+
+Some code in this directory has special license requirements. For more information, please see [`LICENSE.md`](LICENSE.md).
diff --git a/src/06 QML Python example/background.png b/src/06 QML Python example/background.png
new file mode 100644
index 0000000..d36e778
Binary files /dev/null and b/src/06 QML Python example/background.png differ
diff --git a/src/06 QML Python example/main.py b/src/06 QML Python example/main.py
new file mode 100644
index 0000000..bb25aea
--- /dev/null
+++ b/src/06 QML Python example/main.py
@@ -0,0 +1,7 @@
+from PyQt5.QtQml import QQmlApplicationEngine
+from PyQt5.QtWidgets import QApplication
+
+app = QApplication([])
+engine = QQmlApplicationEngine()
+engine.load("main.qml")
+app.exec_()
\ No newline at end of file
diff --git a/src/06 QML Python example/main.qml b/src/06 QML Python example/main.qml
new file mode 100644
index 0000000..495add7
--- /dev/null
+++ b/src/06 QML Python example/main.qml
@@ -0,0 +1,28 @@
+import QtQuick 2.2
+import QtQuick.Window 2.2
+
+Window {
+ Image {
+ id: background
+ source: "background.png"
+ }
+ Image {
+ id: wheel
+ anchors.centerIn: parent
+ source: "pinwheel.png"
+ Behavior on rotation {
+ NumberAnimation {
+ duration: 250
+ }
+ }
+ }
+ MouseArea {
+ anchors.fill: parent
+ onPressed: {
+ wheel.rotation += 90
+ }
+ }
+ visible: true
+ width: background.width
+ height: background.height
+}
\ No newline at end of file
diff --git a/src/06 QML Python example/pinwheel.png b/src/06 QML Python example/pinwheel.png
new file mode 100644
index 0000000..e70b977
Binary files /dev/null and b/src/06 QML Python example/pinwheel.png differ
diff --git a/src/06 QML Python example/qml-python-example.png b/src/06 QML Python example/qml-python-example.png
new file mode 100644
index 0000000..10b68c8
Binary files /dev/null and b/src/06 QML Python example/qml-python-example.png differ
diff --git a/src/07 Qt Text Editor/README.md b/src/07 Qt Text Editor/README.md
new file mode 100644
index 0000000..dfaeb49
--- /dev/null
+++ b/src/07 Qt Text Editor/README.md
@@ -0,0 +1,18 @@
+# Qt Text Editor
+
+This example implements a simple text editor with (Py)Qt.
+
+ 
+
+ 
+
+
+
+It has a surprising number of features:
+
+ * A *File* menu for opening and saving files.
+ * Keyboard shortcuts.
+ * An *About* dialog.
+ * A warning *Do you want to save before quitting?* if there are unmodified changes.
+
+The full source code is in [`main.py`](main.py). For instructions on how to run it, please see [here](https://github.com/1mh/pyqt-examples#running-the-examples).
diff --git a/src/07 Qt Text Editor/icon.svg b/src/07 Qt Text Editor/icon.svg
new file mode 100644
index 0000000..21e838a
--- /dev/null
+++ b/src/07 Qt Text Editor/icon.svg
@@ -0,0 +1,88 @@
+
+
\ No newline at end of file
diff --git a/src/07 Qt Text Editor/main.py b/src/07 Qt Text Editor/main.py
new file mode 100644
index 0000000..06caccf
--- /dev/null
+++ b/src/07 Qt Text Editor/main.py
@@ -0,0 +1,79 @@
+from PyQt5.QtWidgets import *
+from PyQt5.QtGui import QKeySequence
+
+class MainWindow(QMainWindow):
+ def closeEvent(self, e):
+ if not text.document().isModified():
+ return
+ answer = QMessageBox.question(
+ window, None,
+ "You have unsaved changes. Save before closing?",
+ QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
+ )
+ if answer & QMessageBox.Save:
+ save()
+ elif answer & QMessageBox.Cancel:
+ e.ignore()
+
+app = QApplication([])
+app.setApplicationName("Text Editor")
+text = QPlainTextEdit()
+window = MainWindow()
+window.setCentralWidget(text)
+
+file_path = None
+
+menu = window.menuBar().addMenu("&File")
+open_action = QAction("&Open")
+def open_file():
+ global file_path
+ path = QFileDialog.getOpenFileName(window, "Open")[0]
+ if path:
+ text.setPlainText(open(path).read())
+ file_path = path
+open_action.triggered.connect(open_file)
+open_action.setShortcut(QKeySequence.Open)
+menu.addAction(open_action)
+
+save_action = QAction("&Save")
+def save():
+ if file_path is None:
+ save_as()
+ else:
+ with open(file_path, "w") as f:
+ f.write(text.toPlainText())
+ text.document().setModified(False)
+save_action.triggered.connect(save)
+save_action.setShortcut(QKeySequence.Save)
+menu.addAction(save_action)
+
+save_as_action = QAction("Save &As...")
+def save_as():
+ global file_path
+ path = QFileDialog.getSaveFileName(window, "Save As")[0]
+ if path:
+ file_path = path
+ save()
+save_as_action.triggered.connect(save_as)
+menu.addAction(save_as_action)
+
+close = QAction("&Close")
+close.triggered.connect(window.close)
+menu.addAction(close)
+
+help_menu = window.menuBar().addMenu("&Help")
+about_action = QAction("&About")
+help_menu.addAction(about_action)
+def show_about_dialog():
+ text = "
" \
+ "Text Editor
" \
+ "" \
+ "
" \
+ "" \
+ "
Version 31.4.159.265358
" \
+ "Copyright © Company Inc.
"
+ QMessageBox.about(window, "About Text Editor", text)
+about_action.triggered.connect(show_about_dialog)
+
+window.show()
+app.exec_()
\ No newline at end of file
diff --git a/src/07 Qt Text Editor/screenshots/qdialog-example.png b/src/07 Qt Text Editor/screenshots/qdialog-example.png
new file mode 100644
index 0000000..35e961c
Binary files /dev/null and b/src/07 Qt Text Editor/screenshots/qdialog-example.png differ
diff --git a/src/07 Qt Text Editor/screenshots/qmenu-example.png b/src/07 Qt Text Editor/screenshots/qmenu-example.png
new file mode 100644
index 0000000..b64b453
Binary files /dev/null and b/src/07 Qt Text Editor/screenshots/qmenu-example.png differ
diff --git a/src/07 Qt Text Editor/screenshots/qmessagebox-example.png b/src/07 Qt Text Editor/screenshots/qmessagebox-example.png
new file mode 100644
index 0000000..ebe5709
Binary files /dev/null and b/src/07 Qt Text Editor/screenshots/qmessagebox-example.png differ
diff --git a/src/07 Qt Text Editor/screenshots/qt-qmenu.png b/src/07 Qt Text Editor/screenshots/qt-qmenu.png
new file mode 100644
index 0000000..05b62d1
Binary files /dev/null and b/src/07 Qt Text Editor/screenshots/qt-qmenu.png differ
diff --git a/src/07 Qt Text Editor/screenshots/qt-text-editor-windows.png b/src/07 Qt Text Editor/screenshots/qt-text-editor-windows.png
new file mode 100644
index 0000000..6b50292
Binary files /dev/null and b/src/07 Qt Text Editor/screenshots/qt-text-editor-windows.png differ
diff --git a/src/07 Qt Text Editor/screenshots/qt-text-editor.png b/src/07 Qt Text Editor/screenshots/qt-text-editor.png
new file mode 100644
index 0000000..e8adf39
Binary files /dev/null and b/src/07 Qt Text Editor/screenshots/qt-text-editor.png differ
diff --git a/src/08 PyQt5 exe/README.md b/src/08 PyQt5 exe/README.md
new file mode 100644
index 0000000..5edf22a
--- /dev/null
+++ b/src/08 PyQt5 exe/README.md
@@ -0,0 +1,53 @@
+# PyQt5 exe
+
+Once you have a PyQt5 application, you want to compile your Python source code into a standalone executable. Furthermore, you normally want to create an installer so your users can easily set up your app.
+
+This example uses [fbs](https://build-system.fman.io) to create a standalone executable and an installer for the text editor in [example 07](../07%20Qt%20Text%20Editor).
+
+

+
+You can find a modified version of the ["old" main.py](../07%20Qt%20Text%20Editor/main.py) in [`src/main/python/main.py`](src/main/python/main.py). It only has a few extra lines:
+
+We import fbs's `ApplicationContext`:
+
+ from fbs_runtime.application_context.PyQt5 import ApplicationContext
+
+Further down, we instantiate it:
+
+ appctxt = ApplicationContext()
+
+We no longer need to create a `QApplication`. This is done automatically by fbs.
+
+The editor's _About_ dialog shows an icon:
+
+
+
+This is done in the original code as follows:
+
+ text = "...

..."
+
+The new code however needs to be more flexible with regard to the icon's path. When running from source, the icon lies in [`src/main/resources/base/icon.svg`](src/main/resources/base/icon.svg). When running on the user's system however, it lies in the installation directory.
+
+To handle this, the new code uses fbs's [`ApplicationContext.get_resource(...)`](https://build-system.fman.io/manual/#get_resource) method:
+
+ text = "...

..." % appctxt.get_resource("icon.svg")
+
+This automatically handles the different possible locations of the image.
+
+Because we didn't create the `QApplication` ourselves, we finally use the following call instead of only `app.exec_()`:
+
+ appctxt.app.exec_()
+
+To run this example yourself, you need fbs installed as per the instructions [here](https://github.com/1mh/pyqt-examples#running-the-examples). Then, you can do use the following command to run the text editor:
+
+ fbs run
+
+The following command then compiles the Python source code into a standalone executable in your `target/` directory:
+
+ fbs freeze
+
+Finally, the following creates an installer that you can distribute to other people:
+
+ fbs installer
+
+Please note that this last command requires that you have [NSIS](https://nsis.sourceforge.io/Main_Page) installed and on your `PATH` on Windows, or [`fpm`](https://github.com/jordansissel/fpm) on Linux.
diff --git a/src/08 PyQt5 exe/pyqt5-exe.png b/src/08 PyQt5 exe/pyqt5-exe.png
new file mode 100644
index 0000000..0d4aaad
Binary files /dev/null and b/src/08 PyQt5 exe/pyqt5-exe.png differ
diff --git a/src/08 PyQt5 exe/pyqt5-installer-mac.png b/src/08 PyQt5 exe/pyqt5-installer-mac.png
new file mode 100644
index 0000000..50aabec
Binary files /dev/null and b/src/08 PyQt5 exe/pyqt5-installer-mac.png differ
diff --git a/src/08 PyQt5 exe/src/build/settings/base.json b/src/08 PyQt5 exe/src/build/settings/base.json
new file mode 100644
index 0000000..aa02eec
--- /dev/null
+++ b/src/08 PyQt5 exe/src/build/settings/base.json
@@ -0,0 +1,6 @@
+{
+ "app_name": "Text Editor",
+ "author": "Michael",
+ "main_module": "src/main/python/main.py",
+ "version": "0.0.0"
+}
\ No newline at end of file
diff --git a/src/08 PyQt5 exe/src/build/settings/linux.json b/src/08 PyQt5 exe/src/build/settings/linux.json
new file mode 100644
index 0000000..7a64c95
--- /dev/null
+++ b/src/08 PyQt5 exe/src/build/settings/linux.json
@@ -0,0 +1,6 @@
+{
+ "categories": "Utility;",
+ "description": "",
+ "author_email": "",
+ "url": ""
+}
\ No newline at end of file
diff --git a/src/08 PyQt5 exe/src/build/settings/mac.json b/src/08 PyQt5 exe/src/build/settings/mac.json
new file mode 100644
index 0000000..f7bd610
--- /dev/null
+++ b/src/08 PyQt5 exe/src/build/settings/mac.json
@@ -0,0 +1,3 @@
+{
+ "mac_bundle_identifier": ""
+}
\ No newline at end of file
diff --git a/src/08 PyQt5 exe/src/main/icons/Icon.ico b/src/08 PyQt5 exe/src/main/icons/Icon.ico
new file mode 100644
index 0000000..45e9d13
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/Icon.ico differ
diff --git a/src/08 PyQt5 exe/src/main/icons/README.md b/src/08 PyQt5 exe/src/main/icons/README.md
new file mode 100644
index 0000000..c6c4194
--- /dev/null
+++ b/src/08 PyQt5 exe/src/main/icons/README.md
@@ -0,0 +1,11 @@
+
+
+This directory contains the icons that are displayed for your app. Feel free to
+change them.
+
+The difference between the icons on Mac and the other platforms is that on Mac,
+they contain a ~5% transparent margin. This is because otherwise they look too
+big (eg. in the Dock or in the app switcher).
+
+You can create Icon.ico from the .png files with
+[an online tool](http://icoconvert.com/Multi_Image_to_one_icon/).
\ No newline at end of file
diff --git a/src/08 PyQt5 exe/src/main/icons/base/16.png b/src/08 PyQt5 exe/src/main/icons/base/16.png
new file mode 100755
index 0000000..ba99526
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/base/16.png differ
diff --git a/src/08 PyQt5 exe/src/main/icons/base/24.png b/src/08 PyQt5 exe/src/main/icons/base/24.png
new file mode 100755
index 0000000..fee419a
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/base/24.png differ
diff --git a/src/08 PyQt5 exe/src/main/icons/base/32.png b/src/08 PyQt5 exe/src/main/icons/base/32.png
new file mode 100755
index 0000000..b6e6f2d
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/base/32.png differ
diff --git a/src/08 PyQt5 exe/src/main/icons/base/48.png b/src/08 PyQt5 exe/src/main/icons/base/48.png
new file mode 100755
index 0000000..b008c31
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/base/48.png differ
diff --git a/src/08 PyQt5 exe/src/main/icons/base/64.png b/src/08 PyQt5 exe/src/main/icons/base/64.png
new file mode 100755
index 0000000..3616ba1
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/base/64.png differ
diff --git a/src/08 PyQt5 exe/src/main/icons/linux/1024.png b/src/08 PyQt5 exe/src/main/icons/linux/1024.png
new file mode 100755
index 0000000..51cb359
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/linux/1024.png differ
diff --git a/src/08 PyQt5 exe/src/main/icons/linux/128.png b/src/08 PyQt5 exe/src/main/icons/linux/128.png
new file mode 100755
index 0000000..a1bc2d5
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/linux/128.png differ
diff --git a/src/08 PyQt5 exe/src/main/icons/linux/256.png b/src/08 PyQt5 exe/src/main/icons/linux/256.png
new file mode 100755
index 0000000..42b01e9
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/linux/256.png differ
diff --git a/src/08 PyQt5 exe/src/main/icons/linux/512.png b/src/08 PyQt5 exe/src/main/icons/linux/512.png
new file mode 100755
index 0000000..3ec71ae
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/linux/512.png differ
diff --git a/src/08 PyQt5 exe/src/main/icons/mac/1024.png b/src/08 PyQt5 exe/src/main/icons/mac/1024.png
new file mode 100644
index 0000000..3b541c2
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/mac/1024.png differ
diff --git a/src/08 PyQt5 exe/src/main/icons/mac/128.png b/src/08 PyQt5 exe/src/main/icons/mac/128.png
new file mode 100644
index 0000000..cce4e9d
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/mac/128.png differ
diff --git a/src/08 PyQt5 exe/src/main/icons/mac/256.png b/src/08 PyQt5 exe/src/main/icons/mac/256.png
new file mode 100644
index 0000000..e986078
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/mac/256.png differ
diff --git a/src/08 PyQt5 exe/src/main/icons/mac/512.png b/src/08 PyQt5 exe/src/main/icons/mac/512.png
new file mode 100644
index 0000000..fb7be82
Binary files /dev/null and b/src/08 PyQt5 exe/src/main/icons/mac/512.png differ
diff --git a/src/08 PyQt5 exe/src/main/python/main.py b/src/08 PyQt5 exe/src/main/python/main.py
new file mode 100644
index 0000000..7c3ff21
--- /dev/null
+++ b/src/08 PyQt5 exe/src/main/python/main.py
@@ -0,0 +1,89 @@
+from fbs_runtime.application_context.PyQt5 import ApplicationContext
+from PyQt5.QtWidgets import QMainWindow
+
+import sys
+
+appctxt = ApplicationContext() # 1. Instantiate ApplicationContext
+
+from PyQt5.QtWidgets import *
+from PyQt5.QtGui import QKeySequence
+
+class MainWindow(QMainWindow):
+ def closeEvent(self, e):
+ if not text.document().isModified():
+ return
+ answer = QMessageBox.question(
+ window, None,
+ "You have unsaved changes. Save before closing?",
+ QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
+ )
+ if answer & QMessageBox.Save:
+ save()
+ elif answer & QMessageBox.Cancel:
+ e.ignore()
+
+text = QPlainTextEdit()
+window = MainWindow()
+window.setCentralWidget(text)
+
+file_path = None
+
+menu = window.menuBar().addMenu("&File")
+open_action = QAction("&Open")
+def open_file():
+ global file_path
+ path = QFileDialog.getOpenFileName(window, "Open")[0]
+ if path:
+ text.setPlainText(open(path).read())
+ file_path = path
+open_action.triggered.connect(open_file)
+open_action.setShortcut(QKeySequence.Open)
+menu.addAction(open_action)
+
+save_action = QAction("&Save")
+def save():
+ if file_path is None:
+ save_as()
+ else:
+ with open(file_path, "w") as f:
+ f.write(text.toPlainText())
+ text.document().setModified(False)
+save_action.triggered.connect(save)
+save_action.setShortcut(QKeySequence.Save)
+menu.addAction(save_action)
+
+save_as_action = QAction("Save &As...")
+def save_as():
+ global file_path
+ path = QFileDialog.getSaveFileName(window, "Save As")[0]
+ if path:
+ file_path = path
+ save()
+save_as_action.triggered.connect(save_as)
+menu.addAction(save_as_action)
+
+close = QAction("&Close")
+close.triggered.connect(window.close)
+menu.addAction(close)
+
+help_menu = window.menuBar().addMenu("&Help")
+about_action = QAction("&About")
+help_menu.addAction(about_action)
+def show_about_dialog():
+ text = "
" \
+ "Text Editor
" \
+ "" \
+ "
" \
+ "" \
+ "
Version 31.4.159.265358
" \
+ "Copyright © Company Inc.
" \
+ % appctxt.get_resource("icon.svg")
+ about_dialog = QMessageBox(window)
+ about_dialog.setText(text)
+ about_dialog.exec_()
+about_action.triggered.connect(show_about_dialog)
+
+window.show()
+
+exit_code = appctxt.app.exec_() # 2. Invoke appctxt.app.exec_()
+sys.exit(exit_code)
\ No newline at end of file
diff --git a/src/08 PyQt5 exe/src/main/resources/base/icon.svg b/src/08 PyQt5 exe/src/main/resources/base/icon.svg
new file mode 100644
index 0000000..21e838a
--- /dev/null
+++ b/src/08 PyQt5 exe/src/main/resources/base/icon.svg
@@ -0,0 +1,88 @@
+
+
\ No newline at end of file
diff --git a/src/09 Qt dark theme/README.md b/src/09 Qt dark theme/README.md
new file mode 100644
index 0000000..61f806b
--- /dev/null
+++ b/src/09 Qt dark theme/README.md
@@ -0,0 +1,21 @@
+# Qt Dark Theme
+
+This example shows how Qt's style mechanisms can be used to set a dark theme. It adapts the text editor from [example 7](../07%20Qt%20Text%20Editor).
+
+
+
+As you can see in [`main.py`](main.py), this example uses `QApplication.setStyle(...)` and a `QPalette` to change the application's colors:
+
+ # Force the style to be the same on all OSs:
+ app.setStyle("Fusion")
+
+ # Now use a palette to switch to dark colors:
+ palette = QPalette()
+ palette.setColor(QPalette.Window, QColor(53, 53, 53))
+ palette.setColor(QPalette.WindowText, Qt.white)
+ ...
+ app.setPalette(palette)
+
+The rest of the code is the same as for the [original version of the text editor](../07%20Qt%20Text%20Editor).
+
+To run this example yourself, please follow the [instructions in the README of this repository](https://github.com/1mh/pyqt-examples#running-the-examples).
diff --git a/src/09 Qt dark theme/icon.svg b/src/09 Qt dark theme/icon.svg
new file mode 100644
index 0000000..21e838a
--- /dev/null
+++ b/src/09 Qt dark theme/icon.svg
@@ -0,0 +1,88 @@
+
+
\ No newline at end of file
diff --git a/src/09 Qt dark theme/main.py b/src/09 Qt dark theme/main.py
new file mode 100644
index 0000000..ee1105e
--- /dev/null
+++ b/src/09 Qt dark theme/main.py
@@ -0,0 +1,105 @@
+from PyQt5.QtWidgets import *
+from PyQt5.QtGui import QKeySequence, QPalette, QColor
+from PyQt5.QtCore import Qt
+
+app = QApplication([])
+
+# Force the style to be the same on all OSs:
+app.setStyle("Fusion")
+
+# Now use a palette to switch to dark colors:
+palette = QPalette()
+palette.setColor(QPalette.Window, QColor(53, 53, 53))
+palette.setColor(QPalette.WindowText, Qt.white)
+palette.setColor(QPalette.Base, QColor(25, 25, 25))
+palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53))
+palette.setColor(QPalette.ToolTipBase, Qt.white)
+palette.setColor(QPalette.ToolTipText, Qt.white)
+palette.setColor(QPalette.Text, Qt.white)
+palette.setColor(QPalette.Button, QColor(53, 53, 53))
+palette.setColor(QPalette.ButtonText, Qt.white)
+palette.setColor(QPalette.BrightText, Qt.red)
+palette.setColor(QPalette.Link, QColor(42, 130, 218))
+palette.setColor(QPalette.Highlight, QColor(42, 130, 218))
+palette.setColor(QPalette.HighlightedText, Qt.black)
+app.setPalette(palette)
+
+# The rest of the code is the same as for the "normal" text editor.
+
+app.setApplicationName("Text Editor")
+
+text = QPlainTextEdit()
+
+class MainWindow(QMainWindow):
+ def closeEvent(self, e):
+ if not text.document().isModified():
+ return
+ answer = QMessageBox.question(
+ window, None,
+ "You have unsaved changes. Save before closing?",
+ QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
+ )
+ if answer & QMessageBox.Save:
+ save()
+ elif answer & QMessageBox.Cancel:
+ e.ignore()
+
+window = MainWindow()
+window.setCentralWidget(text)
+
+file_path = None
+
+menu = window.menuBar().addMenu("&File")
+open_action = QAction("&Open")
+def open_file():
+ global file_path
+ path = QFileDialog.getOpenFileName(window, "Open")[0]
+ if path:
+ text.setPlainText(open(path).read())
+ file_path = path
+open_action.triggered.connect(open_file)
+open_action.setShortcut(QKeySequence.Open)
+menu.addAction(open_action)
+
+save_action = QAction("&Save")
+def save():
+ if file_path is None:
+ save_as()
+ else:
+ with open(file_path, "w") as f:
+ f.write(text.toPlainText())
+ text.document().setModified(False)
+save_action.triggered.connect(save)
+save_action.setShortcut(QKeySequence.Save)
+menu.addAction(save_action)
+
+save_as_action = QAction("Save &As...")
+def save_as():
+ global file_path
+ path = QFileDialog.getSaveFileName(window, "Save As")[0]
+ if path:
+ file_path = path
+ save()
+save_as_action.triggered.connect(save_as)
+menu.addAction(save_as_action)
+
+close = QAction("&Close")
+close.triggered.connect(window.close)
+menu.addAction(close)
+
+help_menu = window.menuBar().addMenu("&Help")
+about_action = QAction("&About")
+help_menu.addAction(about_action)
+def show_about_dialog():
+ text = "
" \
+ "Text Editor
" \
+ "" \
+ "
" \
+ "" \
+ "
Version 31.4.159.265358
" \
+ "Copyright © Company Inc.
"
+ QMessageBox.about(window, "About Text Editor", text)
+about_action.triggered.connect(show_about_dialog)
+
+window.show()
+app.exec_()
\ No newline at end of file
diff --git a/src/09 Qt dark theme/qt-dark-theme.png b/src/09 Qt dark theme/qt-dark-theme.png
new file mode 100644
index 0000000..c19f6f1
Binary files /dev/null and b/src/09 Qt dark theme/qt-dark-theme.png differ
diff --git a/src/10 QPainter Python example/README.md b/src/10 QPainter Python example/README.md
new file mode 100644
index 0000000..cba69db
--- /dev/null
+++ b/src/10 QPainter Python example/README.md
@@ -0,0 +1,9 @@
+# QPainter Python example
+
+This example application demonstrates how you can use [`QPainter`](https://doc.qt.io/qt-5/qpainter.html) to perform custom rendering in a widget. It turns the text editor from [example 7](../07%20Qt%20Text%20Editor) into an action shooter: When you click inside the editor with the mouse, bullet holes appear.
+
+

+
+The crucial steps of this example are to [override `mousePressEvent(...)`](main.py#L13-L17) to handle the user's clicks, and [`paintEvent(...)`](main.py#L18-L22) to draw the bullets. See the top of [`main.py`](main.py) for how these features work in detail.
+
+To run this example yourself, please follow [these instructions](https://github.com/1mh/pyqt-examples#running-the-examples).
diff --git a/src/10 QPainter Python example/bullet.png b/src/10 QPainter Python example/bullet.png
new file mode 100644
index 0000000..80e9387
Binary files /dev/null and b/src/10 QPainter Python example/bullet.png differ
diff --git a/src/10 QPainter Python example/icon.svg b/src/10 QPainter Python example/icon.svg
new file mode 100644
index 0000000..21e838a
--- /dev/null
+++ b/src/10 QPainter Python example/icon.svg
@@ -0,0 +1,88 @@
+
+
\ No newline at end of file
diff --git a/src/10 QPainter Python example/main.py b/src/10 QPainter Python example/main.py
new file mode 100644
index 0000000..484901e
--- /dev/null
+++ b/src/10 QPainter Python example/main.py
@@ -0,0 +1,103 @@
+from PyQt5.QtWidgets import *
+from PyQt5.QtGui import *
+from PyQt5.QtCore import *
+from PyQt5.QtMultimedia import QSound
+
+class PlainTextEdit(QPlainTextEdit):
+ def __init__(self):
+ super().__init__()
+ self._holes = []
+ self._bullet = QPixmap("bullet.png")
+ size = self._bullet.size()
+ self._offset = QPoint(size.width() / 2, size.height() / 2)
+ def mousePressEvent(self, e):
+ self._holes.append(e.pos())
+ super().mousePressEvent(e)
+ self.viewport().update()
+ QSound.play("shot.wav")
+ def paintEvent(self, e):
+ super().paintEvent(e)
+ painter = QPainter(self.viewport())
+ for hole in self._holes:
+ painter.drawPixmap(hole - self._offset, self._bullet)
+
+app = QApplication([])
+text = PlainTextEdit()
+text.setPlainText("Click with the mouse below to shoot ;-)")
+
+# The rest of the code is as for the normal version of the text editor.
+
+class MainWindow(QMainWindow):
+ def closeEvent(self, e):
+ if not text.document().isModified():
+ return
+ answer = QMessageBox.question(
+ window, None,
+ "You have unsaved changes. Save before closing?",
+ QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
+ )
+ if answer & QMessageBox.Save:
+ save()
+ elif answer & QMessageBox.Cancel:
+ e.ignore()
+
+app.setApplicationName("Text Editor")
+window = MainWindow()
+window.setCentralWidget(text)
+
+file_path = None
+
+menu = window.menuBar().addMenu("&File")
+open_action = QAction("&Open")
+def open_file():
+ global file_path
+ path = QFileDialog.getOpenFileName(window, "Open")[0]
+ if path:
+ text.setPlainText(open(path).read())
+ file_path = path
+open_action.triggered.connect(open_file)
+open_action.setShortcut(QKeySequence.Open)
+menu.addAction(open_action)
+
+save_action = QAction("&Save")
+def save():
+ if file_path is None:
+ save_as()
+ else:
+ with open(file_path, "w") as f:
+ f.write(text.toPlainText())
+ text.document().setModified(False)
+save_action.triggered.connect(save)
+save_action.setShortcut(QKeySequence.Save)
+menu.addAction(save_action)
+
+save_as_action = QAction("Save &As...")
+def save_as():
+ global file_path
+ path = QFileDialog.getSaveFileName(window, "Save As")[0]
+ if path:
+ file_path = path
+ save()
+save_as_action.triggered.connect(save_as)
+menu.addAction(save_as_action)
+
+close = QAction("&Close")
+close.triggered.connect(window.close)
+menu.addAction(close)
+
+help_menu = window.menuBar().addMenu("&Help")
+about_action = QAction("&About")
+help_menu.addAction(about_action)
+def show_about_dialog():
+ text = "
" \
+ "Text Editor
" \
+ "" \
+ "
" \
+ "" \
+ "
Version 31.4.159.265358
" \
+ "Copyright © Company Inc.
"
+ QMessageBox.about(window, "About Text Editor", text)
+about_action.triggered.connect(show_about_dialog)
+
+window.show()
+app.exec_()
\ No newline at end of file
diff --git a/src/10 QPainter Python example/qpainter-python-example.png b/src/10 QPainter Python example/qpainter-python-example.png
new file mode 100644
index 0000000..3670444
Binary files /dev/null and b/src/10 QPainter Python example/qpainter-python-example.png differ
diff --git a/src/10 QPainter Python example/shot.wav b/src/10 QPainter Python example/shot.wav
new file mode 100644
index 0000000..0228460
Binary files /dev/null and b/src/10 QPainter Python example/shot.wav differ
diff --git a/src/11 PyQt Thread example/01_single_threaded.py b/src/11 PyQt Thread example/01_single_threaded.py
new file mode 100644
index 0000000..7e02378
--- /dev/null
+++ b/src/11 PyQt Thread example/01_single_threaded.py
@@ -0,0 +1,37 @@
+from PyQt5.QtCore import *
+from PyQt5.QtWidgets import *
+from requests import Session
+
+name = input("Please enter your name: ")
+chat_url = "https://build-system.fman.io/chat"
+server = Session()
+
+# GUI:
+app = QApplication([])
+text_area = QPlainTextEdit()
+text_area.setFocusPolicy(Qt.NoFocus)
+message = QLineEdit()
+layout = QVBoxLayout()
+layout.addWidget(text_area)
+layout.addWidget(message)
+window = QWidget()
+window.setLayout(layout)
+window.show()
+
+# Event handlers:
+def display_new_messages():
+ new_message = server.get(chat_url).text
+ if new_message:
+ text_area.appendPlainText(new_message)
+
+def send_message():
+ server.post(chat_url, {"name": name, "message": message.text()})
+ message.clear()
+
+# Signals:
+message.returnPressed.connect(send_message)
+timer = QTimer()
+timer.timeout.connect(display_new_messages)
+timer.start(1000)
+
+app.exec_()
\ No newline at end of file
diff --git a/src/11 PyQt Thread example/02_multithreaded.py b/src/11 PyQt Thread example/02_multithreaded.py
new file mode 100644
index 0000000..9bf788f
--- /dev/null
+++ b/src/11 PyQt Thread example/02_multithreaded.py
@@ -0,0 +1,49 @@
+from PyQt5.QtCore import *
+from PyQt5.QtWidgets import *
+from requests import Session
+from threading import Thread
+from time import sleep
+
+name = input("Please enter your name: ")
+chat_url = "https://build-system.fman.io/chat"
+server = Session()
+
+# GUI:
+app = QApplication([])
+text_area = QPlainTextEdit()
+text_area.setFocusPolicy(Qt.NoFocus)
+message = QLineEdit()
+layout = QVBoxLayout()
+layout.addWidget(text_area)
+layout.addWidget(message)
+window = QWidget()
+window.setLayout(layout)
+window.show()
+
+# Event handlers:
+new_messages = []
+def fetch_new_messages():
+ while True:
+ response = server.get(chat_url).text
+ if response:
+ new_messages.append(response)
+ sleep(.5)
+
+thread = Thread(target=fetch_new_messages, daemon=True)
+thread.start()
+
+def display_new_messages():
+ while new_messages:
+ text_area.appendPlainText(new_messages.pop(0))
+
+def send_message():
+ server.post(chat_url, {"name": name, "message": message.text()})
+ message.clear()
+
+# Signals:
+message.returnPressed.connect(send_message)
+timer = QTimer()
+timer.timeout.connect(display_new_messages)
+timer.start(1000)
+
+app.exec_()
\ No newline at end of file
diff --git a/src/11 PyQt Thread example/03_with_threadutil.py b/src/11 PyQt Thread example/03_with_threadutil.py
new file mode 100644
index 0000000..2d49dc4
--- /dev/null
+++ b/src/11 PyQt Thread example/03_with_threadutil.py
@@ -0,0 +1,43 @@
+from PyQt5.QtCore import *
+from PyQt5.QtWidgets import *
+from requests import Session
+from threading import Thread
+from threadutil import run_in_main_thread
+from time import sleep
+
+name = input("Please enter your name: ")
+chat_url = "https://build-system.fman.io/chat"
+server = Session()
+
+# GUI:
+app = QApplication([])
+text_area = QPlainTextEdit()
+text_area.setFocusPolicy(Qt.NoFocus)
+message = QLineEdit()
+layout = QVBoxLayout()
+layout.addWidget(text_area)
+layout.addWidget(message)
+window = QWidget()
+window.setLayout(layout)
+window.show()
+
+append_message = run_in_main_thread(text_area.appendPlainText)
+
+def fetch_new_messages():
+ while True:
+ response = server.get(chat_url).text
+ if response:
+ append_message(response)
+ sleep(.5)
+
+def send_message():
+ server.post(chat_url, {"name": name, "message": message.text()})
+ message.clear()
+
+# Signals:
+message.returnPressed.connect(send_message)
+
+thread = Thread(target=fetch_new_messages, daemon=True)
+thread.start()
+
+app.exec_()
\ No newline at end of file
diff --git a/src/11 PyQt Thread example/README.md b/src/11 PyQt Thread example/README.md
new file mode 100644
index 0000000..e389c19
--- /dev/null
+++ b/src/11 PyQt Thread example/README.md
@@ -0,0 +1,15 @@
+# PyQt Thread example
+
+This example shows how you can use threads to make your PyQt application more responsive. It's a fully functional chat client.
+
+

+
+To run this example, please follow [the instructions in the README of this repository](https://github.com/1mh/pyqt-examples#running-the-examples). Instead of `python main.py`, use `python` to execute one of the scripts described below. Eg. `python 01_single_threaded.py`.
+
+To demonstrate the utility of threads, this directory contains multiple implementations of the chat client:
+
+ * [`01_single_threaded.py`](01_single_threaded.py) does not use threads. Once per second, it fetches the latest messages from the server. It does this in the main thread. While fetching messages, it's unable to process your key strokes. As a result, it sometimes lags a little as you type.
+ * [`02_multithreaded.py`](02_multithreaded.py) uses threads to fetch new messages in the background. It is considerably more responsive than the single threaded version.
+ * [`03_with_threadutil.py`](03_with_threadutil.py) is a variation of the multithreaded version. It extracts the logic necessary for communicating between threads into a separate module that you can use in your own apps, [`threadutil.py`](threadutil.py). For an even more powerful implementation, see [`threadutil_blocking.py`](threadutil_blocking.py). This is the code which [fman](https://fman.io) uses.
+
+Most of the added complexity of the multithreaded versions comes from having to synchronize the main and background threads. In more detail: The _main thread_ is the thread in which Qt draws pixels on the screen, processes events such as mouse clicks, etc. In the examples here, there is a single background thread which fetches messages from the server. But what should happen when a new message arrives? The background thread can't just draw the text on the screen, because Qt might just be in the process of drawing itself. The answer is that the background thread must somehow get Qt to draw the text in the main thread. The second and third examples presented here ([`02_multithreaded.py`](02_multithreaded.py) and [`03_with_threadutil.py`](03_with_threadutil.py)) use different ways of achieving this. In the former, the background thread appends messages to a list, which is then processed in the main thread. The latter uses a custom mechanism that lets the background thread execute arbitrary code in the main thread. In this case, the "arbitrary code" draws the text for the new message on the screen.
diff --git a/src/11 PyQt Thread example/pyqt-thread-example.png b/src/11 PyQt Thread example/pyqt-thread-example.png
new file mode 100644
index 0000000..f735dea
Binary files /dev/null and b/src/11 PyQt Thread example/pyqt-thread-example.png differ
diff --git a/src/11 PyQt Thread example/threadutil.py b/src/11 PyQt Thread example/threadutil.py
new file mode 100644
index 0000000..7a4d3ea
--- /dev/null
+++ b/src/11 PyQt Thread example/threadutil.py
@@ -0,0 +1,22 @@
+from PyQt5.QtCore import QObject, pyqtSignal
+
+class CurrentThread(QObject):
+
+ _on_execute = pyqtSignal(object, tuple, dict)
+
+ def __init__(self):
+ super(QObject, self).__init__()
+ self._on_execute.connect(self._execute_in_thread)
+
+ def execute(self, f, args, kwargs):
+ self._on_execute.emit(f, args, kwargs)
+
+ def _execute_in_thread(self, f, args, kwargs):
+ f(*args, **kwargs)
+
+main_thread = CurrentThread()
+
+def run_in_main_thread(f):
+ def result(*args, **kwargs):
+ main_thread.execute(f, args, kwargs)
+ return result
\ No newline at end of file
diff --git a/src/11 PyQt Thread example/threadutil_blocking.py b/src/11 PyQt Thread example/threadutil_blocking.py
new file mode 100644
index 0000000..716e9f4
--- /dev/null
+++ b/src/11 PyQt Thread example/threadutil_blocking.py
@@ -0,0 +1,112 @@
+"""
+A more powerful, synchronous implementation of run_in_main_thread(...).
+It allows you to receive results from the function invocation:
+
+ @run_in_main_thread
+ def return_2():
+ return 2
+
+ # Runs the above function in the main thread and prints '2':
+ print(return_2())
+"""
+
+from functools import wraps
+from PyQt5.QtCore import pyqtSignal, QObject, QThread
+from PyQt5.QtWidgets import QApplication
+from threading import Event, get_ident
+
+def run_in_thread(thread_fn):
+ def decorator(f):
+ @wraps(f)
+ def result(*args, **kwargs):
+ thread = thread_fn()
+ return Executor.instance().run_in_thread(thread, f, args, kwargs)
+ return result
+ return decorator
+
+def _main_thread():
+ app = QApplication.instance()
+ if app:
+ return app.thread()
+ # We reach here in tests that don't (want to) create a QApplication.
+ if int(QThread.currentThreadId()) == get_ident():
+ return QThread.currentThread()
+ raise RuntimeError('Could not determine main thread')
+
+run_in_main_thread = run_in_thread(_main_thread)
+
+def is_in_main_thread():
+ return QThread.currentThread() == _main_thread()
+
+class Executor:
+
+ _INSTANCE = None
+
+ @classmethod
+ def instance(cls):
+ if cls._INSTANCE is None:
+ cls._INSTANCE = cls(QApplication.instance())
+ return cls._INSTANCE
+ def __init__(self, app):
+ self._pending_tasks = []
+ self._app_is_about_to_quit = False
+ app.aboutToQuit.connect(self._about_to_quit)
+ def _about_to_quit(self):
+ self._app_is_about_to_quit = True
+ for task in self._pending_tasks:
+ task.set_exception(SystemExit())
+ task.has_run.set()
+ def run_in_thread(self, thread, f, args, kwargs):
+ if QThread.currentThread() == thread:
+ return f(*args, **kwargs)
+ elif self._app_is_about_to_quit:
+ # In this case, the target thread's event loop most likely is not
+ # running any more. This would mean that our task (which is
+ # submitted to the event loop via signals/slots) is never run.
+ raise SystemExit()
+ task = Task(f, args, kwargs)
+ self._pending_tasks.append(task)
+ try:
+ receiver = Receiver(task)
+ receiver.moveToThread(thread)
+ sender = Sender()
+ sender.signal.connect(receiver.slot)
+ sender.signal.emit()
+ task.has_run.wait()
+ return task.result
+ finally:
+ self._pending_tasks.remove(task)
+
+class Task:
+ def __init__(self, fn, args, kwargs):
+ self._fn = fn
+ self._args = args
+ self._kwargs = kwargs
+ self.has_run = Event()
+ self._result = self._exception = None
+ def __call__(self):
+ try:
+ self._result = self._fn(*self._args, **self._kwargs)
+ except Exception as e:
+ self._exception = e
+ finally:
+ self.has_run.set()
+ def set_exception(self, exception):
+ self._exception = exception
+ @property
+ def result(self):
+ if not self.has_run.is_set():
+ raise ValueError("Hasn't run.")
+ if self._exception:
+ raise self._exception
+ return self._result
+
+class Sender(QObject):
+ signal = pyqtSignal()
+
+class Receiver(QObject):
+ def __init__(self, callback, parent=None):
+ super().__init__(parent)
+ self.callback = callback
+ def slot(self):
+ self.callback()
\ No newline at end of file
diff --git a/src/12 QTreeView example in Python/README.md b/src/12 QTreeView example in Python/README.md
new file mode 100644
index 0000000..7a700c0
--- /dev/null
+++ b/src/12 QTreeView example in Python/README.md
@@ -0,0 +1,23 @@
+# QTreeView example in Python
+
+A _tree view_ is what's classicaly used to display files and folders: A hierarchical structure where items can be expanded. This example application shows how PyQt5's [`QTreeView`](https://doc.qt.io/qt-5/qtreeview.html) can be used to display your local files.
+
+

+
+As for the other examples in this repository, the code lies in [`main.py`](main.py). The important steps are:
+
+ model = QDirModel()
+ view = QTreeView()
+ view.setModel(model)
+ view.setRootIndex(model.index(home_directory))
+ view.show()
+
+Both [`QDirModel`](https://doc.qt.io/qt-5/qdirmodel.html) and [`QTreeView`](https://doc.qt.io/qt-5/qtreeview.html) are a part of Qt's [Model/View framework](https://doc.qt.io/qt-5/model-view-programming.html). The idea is that the model provides data to the view, which then displays it. As you can see above, we first instantiate the model and the view, then connect the two via `.setModel(...)`. The `.setRootIndex(...)` call instructs the view to display the files in your home directory.
+
+The nice thing about the Model/View distinction is that it lets you visualize the same data in different ways. For instance, you could replace the line `view = QTreeView()` above by the following to display a flat _list_ of your files instead:
+
+ view = QListView()
+
+The next example, [PyQt5 QListview](../13%20PyQt5%20QListView), shows another way of using `QListView`.
+
+To run this example yourself, please follow [the instructions in the README of this repository](https://github.com/1mh/pyqt-examples#running-the-examples).
diff --git a/src/12 QTreeView example in Python/main.py b/src/12 QTreeView example in Python/main.py
new file mode 100644
index 0000000..5b69c15
--- /dev/null
+++ b/src/12 QTreeView example in Python/main.py
@@ -0,0 +1,12 @@
+from os.path import expanduser
+from PyQt5.QtWidgets import *
+
+home_directory = expanduser('~')
+
+app = QApplication([])
+model = QDirModel()
+view = QTreeView()
+view.setModel(model)
+view.setRootIndex(model.index(home_directory))
+view.show()
+app.exec_()
\ No newline at end of file
diff --git a/src/12 QTreeView example in Python/qtreeview-example-in-python.png b/src/12 QTreeView example in Python/qtreeview-example-in-python.png
new file mode 100644
index 0000000..72816dc
Binary files /dev/null and b/src/12 QTreeView example in Python/qtreeview-example-in-python.png differ
diff --git a/src/13 PyQt5 QListView/README.md b/src/13 PyQt5 QListView/README.md
new file mode 100644
index 0000000..51245b0
--- /dev/null
+++ b/src/13 PyQt5 QListView/README.md
@@ -0,0 +1,18 @@
+# PyQt5 QListView
+
+This example shows how you can use a PyQt5 [`QListView`](https://doc.qt.io/qt-5/qlistview.html) to display a list.
+
+

+
+It simply shows a static list of strings. Technically, the data is managed by Qt's [`QStringListModel`](https://doc.qt.io/qt-5/qstringlistmodel.html). The important steps of the [code](main.py) are:
+
+```
+model = QStringListModel(["An element", "Another element", "Yay! Another one."])
+view = QListView()
+view.setModel(model)
+view.show()
+```
+
+This is very similar to the [previous example](../12%20QTreeView%20example%20in%20Python), where we displayed a tree view of files. The reason for this similarity is that both examples use Qt's Model/View framework. As an exercise for yourself, you might want to try using `QListView` instead of `QTreeView` in the previous example.
+
+To run this example, please follow [the instructions in the README of this repository](https://github.com/1mh/pyqt-examples#running-the-examples).
diff --git a/src/13 PyQt5 QListView/main.py b/src/13 PyQt5 QListView/main.py
new file mode 100644
index 0000000..3d80515
--- /dev/null
+++ b/src/13 PyQt5 QListView/main.py
@@ -0,0 +1,11 @@
+from PyQt5.QtWidgets import *
+from PyQt5.QtCore import QStringListModel
+
+app = QApplication([])
+model = QStringListModel([
+ "An element", "Another element", "Yay! Another one."
+])
+view = QListView()
+view.setModel(model)
+view.show()
+app.exec_()
\ No newline at end of file
diff --git a/src/13 PyQt5 QListView/pyqt5-qlistview.png b/src/13 PyQt5 QListView/pyqt5-qlistview.png
new file mode 100644
index 0000000..33c8c1c
Binary files /dev/null and b/src/13 PyQt5 QListView/pyqt5-qlistview.png differ
diff --git a/src/14 QAbstractTableModel example/README.md b/src/14 QAbstractTableModel example/README.md
new file mode 100644
index 0000000..2634704
--- /dev/null
+++ b/src/14 QAbstractTableModel example/README.md
@@ -0,0 +1,52 @@
+# QAbstractTableModel example
+
+This [`QAbstractTableModel`](https://doc.qt.io/qt-5/qabstracttablemodel.html) example shows how you can define a custom Qt _model_ to display tabular data.
+
+

+
+The data is a table of famous scientists. In Python, it can be written as follows:
+
+```
+headers = ["Scientist name", "Birthdate", "Contribution"]
+rows = [("Newton", "1643-01-04", "Classical mechanics"),
+ ("Einstein", "1879-03-14", "Relativity"),
+ ("Darwin", "1809-02-12", "Evolution")]
+```
+
+To make Qt display these data in a table, we need to answer the following questions:
+
+ 1. How many rows are there?
+ 2. How many columns?
+ 3. What's the value of each cell?
+ 4. What are the (column) headers?
+
+We do this by subclassing `QAbstractTableModel`. This lets us answer each of the above questions by implementing a corresponding method:
+
+```
+class TableModel(QAbstractTableModel):
+ def rowCount(self, parent):
+ # How many rows are there?
+ return len(rows)
+ def columnCount(self, parent):
+ # How many columns?
+ return len(headers)
+ def data(self, index, role):
+ if role != Qt.DisplayRole:
+ return QVariant()
+ # What's the value of the cell at the given index?
+ return rows[index.row()][index.column()]
+ def headerData(self, section, orientation, role:
+ if role != Qt.DisplayRole or orientation != Qt.Horizontal:
+ return QVariant()
+ # What's the header for the given column?
+ return headers[section]
+```
+
+Once we have this model, we can instantiate it, connect it to a `QTableView` and show it in a window:
+
+ model = TableModel()
+ view = QTableView()
+ view.setModel(model)
+ view.show()
+
+The full code is in [`main.py`](main.py). For instructions how to run it, please see [the instructions in the README of this repository](https://github.com/1mh/pyqt-examples#running-the-examples).
diff --git a/src/14 QAbstractTableModel example/main.py b/src/14 QAbstractTableModel example/main.py
new file mode 100644
index 0000000..5f1edba
--- /dev/null
+++ b/src/14 QAbstractTableModel example/main.py
@@ -0,0 +1,28 @@
+from PyQt5.QtWidgets import *
+from PyQt5.QtCore import *
+
+headers = ["Scientist name", "Birthdate", "Contribution"]
+rows = [("Newton", "1643-01-04", "Classical mechanics"),
+ ("Einstein", "1879-03-14", "Relativity"),
+ ("Darwin", "1809-02-12", "Evolution")]
+
+class TableModel(QAbstractTableModel):
+ def rowCount(self, parent):
+ return len(rows)
+ def columnCount(self, parent):
+ return len(headers)
+ def data(self, index, role):
+ if role != Qt.DisplayRole:
+ return QVariant()
+ return rows[index.row()][index.column()]
+ def headerData(self, section, orientation, role):
+ if role != Qt.DisplayRole or orientation != Qt.Horizontal:
+ return QVariant()
+ return headers[section]
+
+app = QApplication([])
+model = TableModel()
+view = QTableView()
+view.setModel(model)
+view.show()
+app.exec_()
\ No newline at end of file
diff --git a/src/14 QAbstractTableModel example/qabstracttablemodel-example.png b/src/14 QAbstractTableModel example/qabstracttablemodel-example.png
new file mode 100644
index 0000000..5abf7c0
Binary files /dev/null and b/src/14 QAbstractTableModel example/qabstracttablemodel-example.png differ
diff --git a/src/15 PyQt database example/.gitignore b/src/15 PyQt database example/.gitignore
new file mode 100644
index 0000000..98e6ef6
--- /dev/null
+++ b/src/15 PyQt database example/.gitignore
@@ -0,0 +1 @@
+*.db
diff --git a/src/15 PyQt database example/README.md b/src/15 PyQt database example/README.md
new file mode 100644
index 0000000..a3eda8a
--- /dev/null
+++ b/src/15 PyQt database example/README.md
@@ -0,0 +1,40 @@
+# PyQt database example
+
+This example shows how you can connect to a database from a PyQt application.
+
+

+
+The screenshot shows a table of project data that comes from a SQL database. One of the projects is real ;-) (Though the income is made up.)
+
+There are many different database systems: MySQL, PostgreSQL, etc. For simplicity, this example uses SQLite because it ships with Python and doesn't require separate installation.
+
+The default way of connecting to a database in Python is the [Database API v2.0](https://www.python.org/dev/peps/pep-0249/). You can see an example of its use in [`initdb.py`](initdb.py). Essentially, you use `.connect(...)` to connect to a database, `.cursor()` to obtain a cursor for data querying / manipulation, and `.commit()` to save any changes you made:
+
+ import sqlite3
+ connection = sqlite3.connect("projects.db")
+ cursor = connection.cursor()
+ cursor.execute("CREATE TABLE projects ...")
+ cursor.execute("INSERT INTO projects ...")
+ connection.commit()
+
+The above code creates the SQLite file `projects.db` with a copy of the data shown in the screenshot.
+
+Qt also has its own facilities for connecting to a database. You can see this in [`main.py`](main.py), where we open the `projects.db` file created above and display its data:
+
+```
+db = QSqlDatabase.addDatabase("QSQLITE")
+db.setDatabaseName("projects.db")
+db.open()
+model = QSqlTableModel(None, db)
+model.setTable("projects")
+model.select()
+view = QTableView()
+view.setModel(model)
+view.show()
+```
+
+As in [previous examples](../12%20QTreeView%20example%20in%20Python), this uses Qt's Model/View framework to separate the two concerns of obtaining and displaying the data: We use `model` to load the database, and `view` to display it.
+
+To run this example yourself, first follow [these instructions](https://github.com/1mh/pyqt-examples#running-the-examples). Then invoke `python initdb.py` to initialize the database. After that, you can execute `python main.py` to start the sample application.
+
+While we use SQLite here, you can easily use other database systems as well. For instance, you could use PostgreSQL via the [psycopg2](http://initd.org/psycopg/) library.
diff --git a/src/15 PyQt database example/initdb.py b/src/15 PyQt database example/initdb.py
new file mode 100644
index 0000000..ceff98e
--- /dev/null
+++ b/src/15 PyQt database example/initdb.py
@@ -0,0 +1,13 @@
+import sqlite3
+connection = sqlite3.connect("projects.db")
+cursor = connection.cursor()
+cursor.execute("""
+ CREATE TABLE projects
+ (url TEXT, descr TEXT, income INTEGER)
+""")
+cursor.execute("""INSERT INTO projects VALUES
+ ('giraffes.io', 'Uber, but with giraffes', 1900),
+ ('dronesweaters.com', 'Clothes for cold drones', 3000),
+ ('hummingpro.io', 'Online humming courses', 120000)
+""")
+connection.commit()
\ No newline at end of file
diff --git a/src/15 PyQt database example/main.py b/src/15 PyQt database example/main.py
new file mode 100644
index 0000000..c4cf692
--- /dev/null
+++ b/src/15 PyQt database example/main.py
@@ -0,0 +1,21 @@
+from os.path import exists
+from PyQt5.QtWidgets import *
+from PyQt5.QtSql import *
+
+import sys
+
+if not exists("projects.db"):
+ print("File projects.db does not exist. Please run initdb.py.")
+ sys.exit()
+
+app = QApplication([])
+db = QSqlDatabase.addDatabase("QSQLITE")
+db.setDatabaseName("projects.db")
+db.open()
+model = QSqlTableModel(None, db)
+model.setTable("projects")
+model.select()
+view = QTableView()
+view.setModel(model)
+view.show()
+app.exec_()
\ No newline at end of file
diff --git a/src/15 PyQt database example/pyqt-database-example.png b/src/15 PyQt database example/pyqt-database-example.png
new file mode 100644
index 0000000..76174c9
Binary files /dev/null and b/src/15 PyQt database example/pyqt-database-example.png differ
diff --git a/src/requirements.txt b/src/requirements.txt
new file mode 100644
index 0000000..94511a5
--- /dev/null
+++ b/src/requirements.txt
@@ -0,0 +1,3 @@
+fbs==0.8.3
+PyQt5==5.9.2
+requests
\ No newline at end of file