feat(projects): 新增PySide综合案例示例代码
This commit is contained in:
253
projects/gui/pylash/pylash/main.py
Normal file
253
projects/gui/pylash/pylash/main.py
Normal file
@@ -0,0 +1,253 @@
|
||||
import dataclasses
|
||||
import logging
|
||||
import pathlib
|
||||
import shutil
|
||||
import sys
|
||||
import textwrap
|
||||
from pprint import pformat
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import Slot
|
||||
from PySide6.QtGui import QAction, QCloseEvent, QKeySequence, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QGraphicsScene,
|
||||
QGraphicsView,
|
||||
QHBoxLayout,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QStatusBar,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from pylash import settings, unsplash # isort: skip
|
||||
|
||||
# -------------
|
||||
# Prerequisites
|
||||
# -------------
|
||||
|
||||
logging.basicConfig(
|
||||
level=settings.DEBUG or logging.INFO,
|
||||
format=settings.LOG_FORMAT,
|
||||
style="{",
|
||||
)
|
||||
LOG = logging.getLogger("pylash.main")
|
||||
SECOND = 1000
|
||||
|
||||
|
||||
@dataclasses.dataclass()
|
||||
class Geometry:
|
||||
x: int
|
||||
y: int
|
||||
width: int
|
||||
height: int
|
||||
|
||||
|
||||
# -----------
|
||||
# Application
|
||||
# -----------
|
||||
|
||||
|
||||
class PylashWallpaperApp(QWidget):
|
||||
|
||||
STORE_DIR = pathlib.Path("~/Desktop/pylash").expanduser()
|
||||
|
||||
def __init__(self, client: unsplash.UnsplashClient, title: str = "") -> None:
|
||||
super().__init__()
|
||||
self.client = client
|
||||
self.ctx = {}
|
||||
self._current_image: Optional[pathlib.Path] = None
|
||||
|
||||
self.setup(title=title)
|
||||
|
||||
def setup(self, title: str):
|
||||
self.setWindowTitle(title)
|
||||
self.addAction(self._make_close_action(callback=self.close))
|
||||
self._viewer = self._make_viewer(name="viewer", geom=(30, 30, 960, 480))
|
||||
self._nextButton = self._make_button(
|
||||
name="nextButton",
|
||||
text="下一张",
|
||||
geom=(50, 520, 400, 100),
|
||||
callback=self.next,
|
||||
)
|
||||
|
||||
self._saveButton = self._make_button(
|
||||
name="saveButton",
|
||||
text="保存",
|
||||
geom=(550, 520, 400, 100),
|
||||
callback=self.save,
|
||||
)
|
||||
|
||||
self._status = self._make_status_bar()
|
||||
# main window layout
|
||||
layout = self._make_layout()
|
||||
self.setLayout(layout)
|
||||
|
||||
def _make_status_bar(self, css: str = ""):
|
||||
bar = QStatusBar()
|
||||
style = """
|
||||
background-color: none;
|
||||
color: 'grey';
|
||||
padding: 0;
|
||||
border: none;
|
||||
"""
|
||||
|
||||
if css:
|
||||
style = css
|
||||
bar.setStyleSheet(style)
|
||||
else:
|
||||
bar.setStyleSheet(style)
|
||||
|
||||
ctx = self.ctx.setdefault("statusBar", {})
|
||||
ctx.update(css=textwrap.dedent(style))
|
||||
|
||||
return bar
|
||||
|
||||
def _make_viewer(self, name: str, geom):
|
||||
viewer = QGraphicsView()
|
||||
viewer.setObjectName(name)
|
||||
viewer.setGeometry(*geom)
|
||||
|
||||
ctx = self.ctx.setdefault("viewer", {})
|
||||
ctx[name] = dict(name=name, geom=Geometry(*geom))
|
||||
return viewer
|
||||
|
||||
def _make_button(self, name: str, text: str, geom, callback=None):
|
||||
btn = QPushButton(text=text)
|
||||
btn.setObjectName(name)
|
||||
btn.setGeometry(*geom)
|
||||
|
||||
if callback:
|
||||
btn.clicked.connect(callback) # noqa: ignore
|
||||
|
||||
ctx = self.ctx.setdefault("button", {})
|
||||
ctx[name] = dict(
|
||||
name=name,
|
||||
text=text,
|
||||
geom=Geometry(*geom),
|
||||
callback=callback.__name__ if callback else None,
|
||||
)
|
||||
return btn
|
||||
|
||||
def _make_layout(self):
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self._viewer)
|
||||
|
||||
# button layout
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.addWidget(self._nextButton)
|
||||
btn_layout.addWidget(self._saveButton)
|
||||
|
||||
layout.addWidget(self._status)
|
||||
layout.addLayout(btn_layout)
|
||||
return layout
|
||||
|
||||
def _check_store_dir(self):
|
||||
if not self.STORE_DIR.exists():
|
||||
LOG.debug(f"initialization the directory to save a photo: {self.STORE_DIR}")
|
||||
self.STORE_DIR.mkdir()
|
||||
|
||||
def _make_image_layer(self, data):
|
||||
image = QPixmap()
|
||||
image.loadFromData(data)
|
||||
|
||||
scene = QGraphicsScene()
|
||||
scene.addPixmap(image)
|
||||
|
||||
return scene
|
||||
|
||||
@Slot()
|
||||
def save(self):
|
||||
"""save photo to specific directory."""
|
||||
self._check_store_dir() # create a directory or not before saving
|
||||
if not self._current_image:
|
||||
self._status.showMessage(
|
||||
"There isn't any image on the screen", timeout=5 * SECOND
|
||||
)
|
||||
return
|
||||
|
||||
name = self._current_image.name
|
||||
fpath = self.STORE_DIR.joinpath(name)
|
||||
if fpath.exists():
|
||||
self._status.showMessage(
|
||||
f"this image has saved to {fpath}", timeout=5 * SECOND
|
||||
)
|
||||
LOG.warning(f"{fpath} image has existed.")
|
||||
return
|
||||
|
||||
shutil.copy(self._current_image, fpath)
|
||||
self._status.showMessage(f"save to {fpath}")
|
||||
|
||||
@Slot()
|
||||
def next(self):
|
||||
"""fetch the next photo."""
|
||||
if self._status.currentMessage():
|
||||
self._status.clearMessage()
|
||||
|
||||
image = self.client.fetch()
|
||||
if not image:
|
||||
msg = "the api has been limited, please try it after next hour time."
|
||||
self._status.showMessage(msg)
|
||||
LOG.warning(msg)
|
||||
return
|
||||
|
||||
self._current_image = image
|
||||
|
||||
scene = self._make_image_layer(data=image.read_bytes())
|
||||
self._viewer.setScene(scene)
|
||||
|
||||
def _make_close_action(self, callback):
|
||||
key = "&Close"
|
||||
close = QAction(key, self)
|
||||
close.setShortcut(QKeySequence.Close)
|
||||
close.triggered.connect(callback) # noqa: ignore
|
||||
|
||||
ctx = self.ctx.setdefault("actions", {})
|
||||
|
||||
ctx["close"] = dict(
|
||||
key=key,
|
||||
shortcut=str(QKeySequence.Close),
|
||||
callback=callback.__name__ if callback else None,
|
||||
)
|
||||
|
||||
return close
|
||||
|
||||
def _close_event(self, event: QCloseEvent):
|
||||
ctx_msg = pformat(self.ctx, compact=True)
|
||||
LOG.debug(f"context: \n{ctx_msg}")
|
||||
box = QMessageBox()
|
||||
box.setIcon(QMessageBox.Question)
|
||||
box.setWindowTitle("Quit")
|
||||
box.setText("Are you sure to close window?")
|
||||
box.setStandardButtons(QMessageBox.No | QMessageBox.Yes)
|
||||
box.setDefaultButton(QMessageBox.No)
|
||||
|
||||
choice = box.exec()
|
||||
key = "No" if choice == 65536 else "Yes"
|
||||
LOG.debug(f"choice info(choice={key}, number={choice})")
|
||||
if choice == QMessageBox.No:
|
||||
event.ignore()
|
||||
else:
|
||||
event.accept()
|
||||
|
||||
def closeEvent(self, event: QCloseEvent):
|
||||
self._close_event(event)
|
||||
|
||||
|
||||
def main():
|
||||
# create application window loop.
|
||||
window = QApplication(sys.argv)
|
||||
|
||||
# show content.
|
||||
client = unsplash.UnsplashClient(token=settings.TOKEN)
|
||||
app = PylashWallpaperApp(client=client, title="Unsplash 随机壁纸获取器")
|
||||
app.resize(1440, 900)
|
||||
app.show()
|
||||
|
||||
# exit when application window closed.
|
||||
sys.exit(window.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
11
projects/gui/pylash/pylash/settings.py
Normal file
11
projects/gui/pylash/pylash/settings.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
DEBUG = 10 if os.getenv("PYLASH_DEBUG") else False
|
||||
TOKEN = os.getenv("PYLASH_TOKEN")
|
||||
|
||||
LOG_FORMAT = "[{asctime}] [{levelname}] [{name}] [{funcName}:{lineno}] - {message}"
|
||||
98
projects/gui/pylash/pylash/unsplash.py
Normal file
98
projects/gui/pylash/pylash/unsplash.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import logging
|
||||
import pathlib
|
||||
import tempfile
|
||||
from typing import Optional
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import requests
|
||||
|
||||
__all__ = "UnsplashClient"
|
||||
|
||||
# --------------
|
||||
# Prerequisites
|
||||
# --------------
|
||||
|
||||
API = "https://api.unsplash.com/photos/random"
|
||||
LOG = logging.getLogger(__name__)
|
||||
CACHED = pathlib.Path(tempfile.mkdtemp(prefix="pylash-"))
|
||||
|
||||
|
||||
class APIQueryException(Exception):
|
||||
def __init__(self, status_code: int, detail) -> None:
|
||||
self.status_code = status_code
|
||||
self.detail = detail
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"staus_code={self.status_code}, detail={self.detail}"
|
||||
|
||||
|
||||
# ---------------------
|
||||
# Unsplash Integration
|
||||
# ---------------------
|
||||
|
||||
|
||||
class UnsplashClient(object):
|
||||
def __init__(self, token: str) -> None:
|
||||
if not token:
|
||||
raise ValueError(f"UnsplashClient need a token for initialization.")
|
||||
self.token = token
|
||||
self._remaining = 50
|
||||
self._headers = {
|
||||
"Accept-Version": "v1",
|
||||
"Authorization": f"Client-ID {self.token}",
|
||||
}
|
||||
|
||||
@property
|
||||
def remaining(self) -> int:
|
||||
return self._remaining
|
||||
|
||||
def _download(self, url: str) -> pathlib.Path:
|
||||
"""download image from target url"""
|
||||
|
||||
# extract name from url query string.
|
||||
parser = urlparse(url)
|
||||
params = parse_qs(parser.query)
|
||||
|
||||
name = params['ixid'][0]
|
||||
file = CACHED.joinpath(f"{name}.jpg")
|
||||
LOG.debug(f"save image to cached dir in: {file}")
|
||||
|
||||
response = requests.get(url, stream=True, params=dict(w=1080, fm="jpg", q=80))
|
||||
total = 0
|
||||
with open(file, mode="wb") as f:
|
||||
for chunk in response.iter_content(1024):
|
||||
total += f.write(chunk)
|
||||
|
||||
LOG.debug(f"writing data: {total >> 10} KB.")
|
||||
|
||||
return file
|
||||
|
||||
def fetch(self) -> Optional[pathlib.Path]:
|
||||
if self._remaining == 0:
|
||||
LOG.warning(
|
||||
"the API request/usage has exhausted."
|
||||
"Please try it after next hour time."
|
||||
)
|
||||
return
|
||||
|
||||
response = requests.get(
|
||||
API, headers=self._headers, params={"orientation": "landscape"}
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
if response.status_code != 200:
|
||||
raise APIQueryException(status_code=response.status_code, detail=data)
|
||||
|
||||
# this property is not thread safety.
|
||||
self._remaining = int(response.headers.get("X-Ratelimit-Remaining"))
|
||||
|
||||
url = data["urls"].get("raw")
|
||||
image = self._download(url=url)
|
||||
return image
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from pylash import settings
|
||||
|
||||
client = UnsplashClient(token=settings.TOKEN)
|
||||
client.fetch()
|
||||
20
projects/gui/quickstart.py
Normal file
20
projects/gui/quickstart.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import sys
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication()
|
||||
|
||||
box = QMessageBox()
|
||||
box.setText("Do you attempt to close window?")
|
||||
|
||||
box.setStandardButtons(box.No | box.Yes)
|
||||
box.setDefaultButton(box.Yes)
|
||||
box.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
49
projects/gui/signals.py
Normal file
49
projects/gui/signals.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import sys
|
||||
|
||||
from PySide6.QtCore import Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QPushButton,
|
||||
QStatusBar,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
|
||||
class MyWidget(QWidget):
|
||||
|
||||
counter = 0
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.resize(200, 100)
|
||||
|
||||
btn = QPushButton()
|
||||
btn.setText("Click me")
|
||||
btn.clicked.connect(self.count)
|
||||
self.btn = btn
|
||||
|
||||
bar = QStatusBar()
|
||||
self.bar = bar
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self.btn)
|
||||
layout.addWidget(self.bar)
|
||||
self.setLayout(layout)
|
||||
|
||||
@Slot()
|
||||
def count(self):
|
||||
self.counter += 1
|
||||
self.bar.showMessage(f"you have clicked: {self.counter}")
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication()
|
||||
widget = MyWidget()
|
||||
widget.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
53
projects/gui/widget.py
Normal file
53
projects/gui/widget.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import sys
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QLabel, QLineEdit, QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class MyWidget(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.resize(200, 200)
|
||||
|
||||
label = QLabel()
|
||||
label.setText("Hello, World!")
|
||||
label.setStyleSheet(
|
||||
"""
|
||||
QLabel {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
margin: auto 50%;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.label = label
|
||||
|
||||
editor = QLineEdit()
|
||||
editor.setPlaceholderText("input your name here.")
|
||||
editor.textChanged.connect(self.echo)
|
||||
self.editor = editor
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self.label)
|
||||
layout.addWidget(self.editor)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def echo(self, name: str = ""):
|
||||
if name:
|
||||
msg = f"Hello, {name}!"
|
||||
else:
|
||||
msg = f"Hello, world!"
|
||||
self.label.setText(msg)
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication()
|
||||
widget = MyWidget()
|
||||
widget.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user