├── server
├── __init__.py
├── utils
│ ├── __init__.py
│ └── react.py
└── api
│ ├── __init__.py
│ ├── transactions.py
│ ├── foo.py
│ └── README.md
├── requirements.txt
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── .gitignore
├── .env.example
├── src
├── index.js
├── index.css
├── App.css
├── App.js
└── logo.svg
├── package.json
├── main.py
└── README.md
/server/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | python-dotenv
2 | PyQtWebEngine
3 |
--------------------------------------------------------------------------------
/server/api/__init__.py:
--------------------------------------------------------------------------------
1 | from .foo import *
2 | from .transactions import *
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivan0313/PyQt-React-Boilerplate/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivan0313/PyQt-React-Boilerplate/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivan0313/PyQt-React-Boilerplate/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | sample_code/data/**/
3 | sample_code/model
4 | .env
5 | venv
6 |
7 | /node_modules
8 | build
9 | **/__pycache__
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Make a copy of this file and rename as .env
2 |
3 | ENVIRONMENT=development
4 |
5 | QTWEBENGINE_REMOTE_DEBUGGING=3030
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/server/api/transactions.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import pyqtSlot, QObject
2 |
3 |
4 | class Transactions(QObject):
5 | def __init__(self, parent=None):
6 | super().__init__(parent)
7 |
8 | @pyqtSlot()
9 | def example_api(self):
10 | print('some data is being processed')
11 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/server/utils/react.py:
--------------------------------------------------------------------------------
1 | import http.server
2 | import socketserver
3 |
4 | PORT = 8000
5 | DIRECTORY = "build"
6 |
7 |
8 | class Handler(http.server.SimpleHTTPRequestHandler):
9 | def __init__(self, *args, **kwargs):
10 | super().__init__(*args, directory=DIRECTORY, **kwargs)
11 |
12 | def serve():
13 | with socketserver.TCPServer(("", PORT), Handler) as httpd:
14 | print("serving at port", PORT)
15 | httpd.serve_forever()
16 |
--------------------------------------------------------------------------------
/server/api/foo.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from PyQt5.QtCore import pyqtSlot, QObject
4 |
5 |
6 | class Foo(QObject):
7 | def __init__(self, parent=None):
8 | super().__init__(parent)
9 |
10 | @pyqtSlot(str, int, str, result=str)
11 | def foo(self, mystr, myint, myobjectjson):
12 | print('bar', mystr, myint)
13 | myobject = json.loads(myobjectjson)
14 |
15 | print('My Object', myobject)
16 |
17 | return mystr
18 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "react": "^17.0.2",
10 | "react-dom": "^17.0.2",
11 | "react-scripts": "4.0.3",
12 | "web-vitals": "^1.0.1"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import logo from './logo.svg';
3 | import './App.css';
4 |
5 | function App() {
6 | const [log, setLog] = useState('null')
7 |
8 | const handleClick = () => {
9 | const student = {name: 'Lucas', school: 'HKU'}
10 | window.server.foo.foo('test', 69, JSON.stringify(student)).then(res => {
11 | setLog(res);
12 | })
13 | }
14 |
15 | const handleClick2 = () => {
16 | window.server.transactions.example_api();
17 | }
18 |
19 | return (
20 |
38 | );
39 | }
40 |
41 | export default App;
42 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import threading
4 |
5 | from dotenv import load_dotenv
6 | from PyQt5.Qt import QUrl
7 | from PyQt5.QtWidgets import QApplication
8 | from PyQt5.QtWebChannel import QWebChannel
9 | from PyQt5.QtWebEngineWidgets import QWebEngineView
10 |
11 | from server.utils.react import serve
12 | from server.api import Foo, Transactions
13 |
14 | load_dotenv()
15 |
16 | ENVIRONMENT = os.environ['ENVIRONMENT']
17 |
18 |
19 | class WebEngineView(QWebEngineView):
20 |
21 | def __init__(self):
22 | super().__init__()
23 |
24 | if ENVIRONMENT == 'production':
25 | self.load(QUrl("http://localhost:8000"))
26 | elif ENVIRONMENT == 'development':
27 | self.load(QUrl("http://localhost:3000"))
28 | else:
29 | raise Exception(f"Unknown environment configuration: {ENVIRONMENT}")
30 |
31 | # setup channel
32 | self.channel = QWebChannel()
33 | self.channel.registerObject('transactions', Transactions(self))
34 | self.channel.registerObject('foo', Foo(self))
35 | self.page().setWebChannel(self.channel)
36 |
37 | self.show()
38 |
39 |
40 | if __name__ == "__main__":
41 | # Serve static files build using react
42 | if ENVIRONMENT == 'production':
43 | react = threading.Thread(target=serve, daemon=True)
44 | react.start()
45 |
46 | # Init PyQt application
47 | app = QApplication(sys.argv)
48 | view = WebEngineView()
49 | view.show()
50 |
51 | # Run Application
52 | sys.exit(app.exec_())
53 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PyQt React Boilerplate
2 |
3 | ## Project Structure
4 |
5 | .
6 | ├── public # Public files for react
7 | ├── server # PyQt Web Server
8 | │ ├── api
9 | │ └── utils
10 | ├── src # React application source code
11 | │ └── ...
12 | └── main.py # Application entry point
13 |
14 | ## Setting Up Development Environment
15 |
16 | ### Environment variables
17 |
18 | All environment variables are stored in `.env`.
19 | This is where we store sensitive information that we do not want to include in the source code, such as database login information.
20 |
21 | 1. Create a copy of `.env.example` and rename it as `.env`.
22 |
23 | 2. Fill in the missing information, such as database login username and password.
24 |
25 | To access variables defined in `.env`, package `python-dotenv` is used.
26 |
27 | ```py
28 | from dotenv import load_dotenv
29 |
30 | load_dotenv()
31 | ```
32 |
33 | Access ENVIRONMENT variable for example
34 |
35 | ```py
36 | ENVIRONMENT = os.environ['ENVIRONMENT']
37 | ```
38 |
39 | ### React GUI
40 |
41 | 1. Install yarn
42 |
43 |
44 | 2. Install node dependencies
45 |
46 | ```sh
47 | yarn
48 | ```
49 |
50 | 3. Run react app
51 |
52 | ```bash
53 | yarn start
54 | ```
55 |
56 | ### PyQt Web Server
57 |
58 | 1. Install virtualenv
59 |
60 | ```bash
61 | pip install virtualenv
62 | ```
63 |
64 | 2. Create virtualenv
65 |
66 | ```bash
67 | python -m venv ./venv
68 | ```
69 |
70 | 3. Activate virtualenv
71 |
72 | ```bash
73 | # win
74 | venv/Scripts/activate.bat
75 |
76 | # mac
77 | source venv/bin/activate
78 | ```
79 |
80 | 4. Install python dependencies
81 |
82 | ```bash
83 | pip install -r requirements.txt
84 | ```
85 |
86 | 5. Run Application
87 |
88 | ```bash
89 | python main.py
90 | ```
91 |
92 | ## Packaging for distribution
93 |
94 | ### Build react app
95 |
96 | Static files will be built in the `./build` directory
97 |
98 | ```bash
99 | yarn build
100 | ```
101 |
102 | ### Build PyQt app
103 |
104 | (I am still figuring this out)
105 |
--------------------------------------------------------------------------------
/server/api/README.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | ## Creating new API group
4 |
5 | 1. Create new file in this directory
6 |
7 | For example, `server/api/foo.py`
8 |
9 | 2. Create new PyQt Widget using the following template
10 |
11 | Rename Foo to the API group's name and foo to the API's name accordingly.
12 |
13 | ```py
14 | import json
15 |
16 | from PyQt5.QtCore import pyqtSlot, QObject
17 |
18 |
19 | class Foo(QObject):
20 | def __init__(self, parent=None):
21 | super().__init__(parent)
22 |
23 | @pyqtSlot()
24 | def foo(self):
25 | print('bar')
26 | ```
27 |
28 | 3. Export API group from api module
29 |
30 | ```py
31 | # server/api/__init__.py
32 |
33 | from .foo import *
34 | ```
35 |
36 | 4. Add API group as channel to PyQt Web Server
37 |
38 | ```py
39 | # main.py
40 |
41 | from server.api import Foo # Add this line
42 |
43 | class WebEngineView(QWebEngineView):
44 |
45 | def __init__(self):
46 | super().__init__()
47 |
48 | ...
49 |
50 | # setup channel
51 | self.channel = QWebChannel()
52 | self.channel.registerObject('foo', Foo(self)) # Add this line
53 | self.page().setWebChannel(self.channel)
54 |
55 | self.show()
56 | ```
57 |
58 | ## pyqtSlot
59 |
60 | There is not a lot of examples found online regarding pyqtSlot. You may reference to the following documentation.
61 |
62 |
63 | But for our purpose, the following guide should suffice.
64 |
65 | ### Add arguments
66 |
67 | To add a new argument, first specify the argument type in the pyqtSlot decorator `[float, int, str]`
68 |
69 | ```py
70 | @pyqtSlot(str, int)
71 | def foo(self, mystr):
72 | print(mystr)
73 | ```
74 |
75 | I haven't figured out how to pass python or node.js object into slots yet, for now, you may make use of json.
76 |
77 | ```py
78 | import json
79 |
80 | ...
81 |
82 | # Receive
83 |
84 | @pyqtSlot(str)
85 | def foo(self, myobjectjson):
86 |
87 | myobject = json.loads(myobjectjson)
88 | print('My Object', myobject)
89 |
90 |
91 | # Return
92 |
93 | @pyqtSlot(result=str)
94 | def foo(self, myobjectjson):
95 | myobject = {
96 | 'text': 'some text',
97 | 'list': ['1', 2],
98 | }
99 | myobjectjson = json.dump(myobject)
100 |
101 | return myobjectjson
102 | ```
103 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------