├── 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 |
21 |
22 | logo 23 |

24 | {log} 25 |

26 | 27 | 28 | 34 | Learn React 35 | 36 |
37 |
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 | --------------------------------------------------------------------------------