Add versions for PySide6, PyQt6 & PySide2.

Break down examples into module files to make easier to read. Use
full-definitions on Enums (PyQt6 compatible, better documenting).
Add fixes for Qt6 versions & some general bugfixes.
This commit is contained in:
Martin Fitzpatrick
2024-02-19 13:36:32 +01:00
parent 38118a64a4
commit b74592ea41
1448 changed files with 146610 additions and 27745 deletions

View File

@@ -0,0 +1,15 @@
# Mooseache — How web browsers would be if they'd just been invented
This is an example web browser built with Python and Qt. Using the
QtWebEngineWidgets system introduced in Qt5.6, this provides a single-window
browsing experience with the usual controls, as well as saving and loading HTML.
![Browser](screenshot-browser.jpg)
> If you want to learn more about build GUI applications with Python,
take a look at [Python GUIs](https://www.pythonguis.com/)
which covers everything you need to know to start building your own applications with PyQt5.
## Other licenses
Icons used in the application are by [Yusuke Kamiyaman](http://p.yusukekamiyamane.com/).

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

257
pyqt6/demos/browser/main.py Normal file
View File

@@ -0,0 +1,257 @@
import os
import sys
from PyQt6.QtCore import QSize, Qt, QUrl
from PyQt6.QtGui import QAction, QIcon, QKeySequence, QPixmap, QShortcut
from PyQt6.QtPrintSupport import QPrintPreviewDialog
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QFileDialog,
QLabel,
QLineEdit,
QMainWindow,
QStatusBar,
QToolBar,
QVBoxLayout,
)
class AboutDialog(QDialog):
def __init__(self):
super().__init__()
QBtn = QDialogButtonBox.StandardButton.Ok # No cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout = QVBoxLayout()
title = QLabel("MooseAche")
font = title.font()
font.setPointSize(20)
title.setFont(font)
layout.addWidget(title)
logo = QLabel()
logo.setPixmap(QPixmap(os.path.join("images", "ma-icon-128.png")))
layout.addWidget(logo)
layout.addWidget(QLabel("Version 23.35.211.233232"))
layout.addWidget(QLabel("Copyright 2015 MooseAche Inc."))
for i in range(0, layout.count()):
layout.itemAt(i).setAlignment(Qt.AlignmentFlag.AlignHCenter)
layout.addWidget(self.buttonBox)
self.setLayout(layout)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.browser = QWebEngineView()
self.browser.setUrl(QUrl("http://google.com"))
self.browser.urlChanged.connect(self.update_urlbar)
self.browser.loadFinished.connect(self.update_title)
self.setCentralWidget(self.browser)
self.status = QStatusBar()
self.setStatusBar(self.status)
navtb = QToolBar("Navigation")
navtb.setIconSize(QSize(16, 16))
self.addToolBar(navtb)
self.shortcut_open = QShortcut(QKeySequence("F5"), self)
self.shortcut_open.activated.connect(self.browser.reload)
back_btn = QAction(QIcon(os.path.join("images", "arrow-180.png")), "Back", self)
back_btn.setStatusTip("Back to previous page")
back_btn.triggered.connect(self.browser.back)
navtb.addAction(back_btn)
next_btn = QAction(
QIcon(os.path.join("images", "arrow-000.png")),
"Forward",
self,
)
next_btn.setStatusTip("Forward to next page")
next_btn.triggered.connect(self.browser.forward)
navtb.addAction(next_btn)
reload_btn = QAction(
QIcon(os.path.join("images", "arrow-circle-315.png")),
"Reload",
self,
)
reload_btn.setStatusTip("Reload page")
reload_btn.triggered.connect(self.browser.reload)
navtb.addAction(reload_btn)
home_btn = QAction(QIcon(os.path.join("images", "home.png")), "Home", self)
home_btn.setStatusTip("Go home")
home_btn.triggered.connect(self.navigate_home)
navtb.addAction(home_btn)
navtb.addSeparator()
self.httpsicon = QLabel() # Yes, really!
self.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-nossl.png")))
navtb.addWidget(self.httpsicon)
self.urlbar = QLineEdit()
self.urlbar.returnPressed.connect(self.navigate_to_url)
navtb.addWidget(self.urlbar)
stop_btn = QAction(
QIcon(os.path.join("images", "cross-circle.png")),
"Stop",
self,
)
stop_btn.setStatusTip("Stop loading current page")
stop_btn.triggered.connect(self.browser.stop)
navtb.addAction(stop_btn)
# Uncomment to disable native menubar on Mac
# self.menuBar().setNativeMenuBar(False)
file_menu = self.menuBar().addMenu("&File")
open_file_action = QAction(
QIcon(os.path.join("images", "disk--arrow.png")),
"Open file...",
self,
)
open_file_action.setStatusTip("Open from file")
open_file_action.triggered.connect(self.open_file)
file_menu.addAction(open_file_action)
save_file_action = QAction(
QIcon(os.path.join("images", "disk--pencil.png")),
"Save Page As...",
self,
)
save_file_action.setStatusTip("Save current page to file")
save_file_action.triggered.connect(self.save_file)
file_menu.addAction(save_file_action)
print_action = QAction(
QIcon(os.path.join("images", "printer.png")),
"Print...",
self,
)
print_action.setStatusTip("Print current page")
print_action.triggered.connect(self.print_page)
file_menu.addAction(print_action)
help_menu = self.menuBar().addMenu("&Help")
about_action = QAction(
QIcon(os.path.join("images", "question.png")),
"About MooseAche",
self,
)
about_action.setStatusTip("Find out more about MooseAche") # Hungry!
about_action.triggered.connect(self.about)
help_menu.addAction(about_action)
navigate_mozarella_action = QAction(
QIcon(os.path.join("images", "lifebuoy.png")),
"MooseAche Homepage",
self,
)
navigate_mozarella_action.setStatusTip("Go to MooseAche Homepage")
navigate_mozarella_action.triggered.connect(self.navigate_mozarella)
help_menu.addAction(navigate_mozarella_action)
self.show()
self.setWindowIcon(QIcon(os.path.join("images", "ma-icon-64.png")))
def update_title(self):
title = self.browser.page().title()
self.setWindowTitle("%s - MooseAche" % title)
def navigate_mozarella(self):
self.browser.setUrl(QUrl("https://www.pythonguis.com/"))
def about(self):
dlg = AboutDialog()
dlg.exec()
def open_file(self):
filename, _ = QFileDialog.getOpenFileName(
self,
"Open file",
"",
"Hypertext Markup Language (*.htm *.html);;" "All files (*.*)",
)
if filename:
with open(filename, "r") as f:
html = f.read()
self.browser.setHtml(html)
self.urlbar.setText(filename)
def save_html(self, html):
with open(self.save_file, "w") as f:
f.write(html)
def save_file(self):
filename, _ = QFileDialog.getSaveFileName(
self,
"Save Page As",
"",
"Hypertext Markup Language (*.htm *html);;" "All files (*.*)",
)
if filename:
self.save_file = filename
self.browser.page().toHtml(self.save_html)
def print_page(self):
dlg = QPrintPreviewDialog()
dlg.paintRequested.connect(self.browser.print_)
dlg.exec()
def navigate_home(self):
self.browser.setUrl(QUrl("http://www.google.com"))
def navigate_to_url(self): # Does not receive the Url
q = QUrl(self.urlbar.text())
if q.scheme() == "":
q.setScheme("http")
self.browser.setUrl(q)
def update_urlbar(self, q):
if q.scheme() == "https":
# Secure padlock icon
self.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-ssl.png")))
else:
# Insecure padlock icon
self.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-nossl.png")))
self.urlbar.setText(q.toString())
self.urlbar.setCursorPosition(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setApplicationName("MooseAche")
app.setOrganizationName("MooseAche")
app.setOrganizationDomain("MooseAche.org")
window = MainWindow()
app.exec()

View File

@@ -0,0 +1,277 @@
import os
import sys
from PyQt6.QtCore import QSize, Qt, QUrl
from PyQt6.QtGui import QAction, QIcon, QPixmap
from PyQt6.QtPrintSupport import QPrintPreviewDialog
from PyQt6.QtWebEngineCore import QWebEnginePage
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QFileDialog,
QLabel,
QLineEdit,
QMainWindow,
QStatusBar,
QToolBar,
QVBoxLayout,
)
class AboutDialog(QDialog):
def __init__(self):
super().__init__()
QBtn = QDialogButtonBox.StandardButton.Ok # No cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout = QVBoxLayout()
title = QLabel("MooseAche")
font = title.font()
font.setPointSize(20)
title.setFont(font)
layout.addWidget(title)
logo = QLabel()
logo.setPixmap(QPixmap(os.path.join("images", "ma-icon-128.png")))
layout.addWidget(logo)
layout.addWidget(QLabel("Version 23.35.211.233232"))
layout.addWidget(QLabel("Copyright 2015 MooseAche Inc."))
for i in range(0, layout.count()):
layout.itemAt(i).setAlignment(Qt.AlignmentFlag.AlignHCenter)
layout.addWidget(self.buttonBox)
self.setLayout(layout)
class WebEnginePage(QWebEnginePage):
# Store externally created windows.
external_windows = []
def acceptNavigationRequest(self, url, _type, isMainFrame):
print(url, _type, isMainFrame)
if _type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
w = QWebEngineView()
w.setUrl(url)
w.show()
# Keep reference to external window, so it isn't cleared up.
self.external_windows.append(w)
return False
return super().acceptNavigationRequest(url, _type, isMainFrame)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.browser = QWebEngineView()
self.browser.setPage(WebEnginePage(self))
self.browser.setUrl(QUrl("http://google.com"))
self.browser.urlChanged.connect(self.update_urlbar)
self.browser.loadFinished.connect(self.update_title)
self.setCentralWidget(self.browser)
self.status = QStatusBar()
self.setStatusBar(self.status)
navtb = QToolBar("Navigation")
navtb.setIconSize(QSize(16, 16))
self.addToolBar(navtb)
back_btn = QAction(QIcon(os.path.join("images", "arrow-180.png")), "Back", self)
back_btn.setStatusTip("Back to previous page")
back_btn.triggered.connect(self.browser.back)
navtb.addAction(back_btn)
next_btn = QAction(
QIcon(os.path.join("images", "arrow-000.png")),
"Forward",
self,
)
next_btn.setStatusTip("Forward to next page")
next_btn.triggered.connect(self.browser.forward)
navtb.addAction(next_btn)
reload_btn = QAction(
QIcon(os.path.join("images", "arrow-circle-315.png")),
"Reload",
self,
)
reload_btn.setStatusTip("Reload page")
reload_btn.triggered.connect(self.browser.reload)
navtb.addAction(reload_btn)
home_btn = QAction(QIcon(os.path.join("images", "home.png")), "Home", self)
home_btn.setStatusTip("Go home")
home_btn.triggered.connect(self.navigate_home)
navtb.addAction(home_btn)
navtb.addSeparator()
self.httpsicon = QLabel() # Yes, really!
self.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-nossl.png")))
navtb.addWidget(self.httpsicon)
self.urlbar = QLineEdit()
self.urlbar.returnPressed.connect(self.navigate_to_url)
navtb.addWidget(self.urlbar)
stop_btn = QAction(
QIcon(os.path.join("images", "cross-circle.png")),
"Stop",
self,
)
stop_btn.setStatusTip("Stop loading current page")
stop_btn.triggered.connect(self.browser.stop)
navtb.addAction(stop_btn)
# Uncomment to disable native menubar on Mac
# self.menuBar().setNativeMenuBar(False)
file_menu = self.menuBar().addMenu("&File")
open_file_action = QAction(
QIcon(os.path.join("images", "disk--arrow.png")),
"Open file...",
self,
)
open_file_action.setStatusTip("Open from file")
open_file_action.triggered.connect(self.open_file)
file_menu.addAction(open_file_action)
save_file_action = QAction(
QIcon(os.path.join("images", "disk--pencil.png")),
"Save Page As...",
self,
)
save_file_action.setStatusTip("Save current page to file")
save_file_action.triggered.connect(self.save_file)
file_menu.addAction(save_file_action)
print_action = QAction(
QIcon(os.path.join("images", "printer.png")),
"Print...",
self,
)
print_action.setStatusTip("Print current page")
print_action.triggered.connect(self.print_page)
file_menu.addAction(print_action)
help_menu = self.menuBar().addMenu("&Help")
about_action = QAction(
QIcon(os.path.join("images", "question.png")),
"About MooseAche",
self,
)
about_action.setStatusTip("Find out more about MooseAche") # Hungry!
about_action.triggered.connect(self.about)
help_menu.addAction(about_action)
navigate_mozarella_action = QAction(
QIcon(os.path.join("images", "lifebuoy.png")),
"MooseAche Homepage",
self,
)
navigate_mozarella_action.setStatusTip("Go to MooseAche Homepage")
navigate_mozarella_action.triggered.connect(self.navigate_mozarella)
help_menu.addAction(navigate_mozarella_action)
self.show()
self.setWindowIcon(QIcon(os.path.join("images", "ma-icon-64.png")))
def acceptNavigationRequest(self, url, navigation_type, is_main_frame):
self._externalview = QWebEngineView(url)
return False
def linkClicked(self, e):
print(e)
def update_title(self):
title = self.browser.page().title()
self.setWindowTitle("%s - MooseAche" % title)
def navigate_mozarella(self):
self.browser.setUrl(QUrl("https://www.pythonguis.com/"))
def about(self):
dlg = AboutDialog()
dlg.exec()
def open_file(self):
filename, _ = QFileDialog.getOpenFileName(
self,
"Open file",
"",
"Hypertext Markup Language (*.htm *.html);;" "All files (*.*)",
)
if filename:
with open(filename, "r") as f:
html = f.read()
self.browser.setHtml(html)
self.urlbar.setText(filename)
def save_file(self):
filename, _ = QFileDialog.getSaveFileName(
self,
"Save Page As",
"",
"Hypertext Markup Language (*.htm *html);;" "All files (*.*)",
)
if filename:
html = self.browser.page().toHtml()
with open(filename, "w") as f:
f.write(html)
def print_page(self):
dlg = QPrintPreviewDialog()
dlg.paintRequested.connect(self.browser.print_)
dlg.exec()
def navigate_home(self):
self.browser.setUrl(QUrl("http://www.google.com"))
def navigate_to_url(self): # Does not receive the Url
q = QUrl(self.urlbar.text())
if q.scheme() == "":
q.setScheme("http")
self.browser.setUrl(q)
def update_urlbar(self, q):
if q.scheme() == "https":
# Secure padlock icon
self.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-ssl.png")))
else:
# Insecure padlock icon
self.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-nossl.png")))
self.urlbar.setText(q.toString())
self.urlbar.setCursorPosition(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setApplicationName("MooseAche")
app.setOrganizationName("MooseAche")
app.setOrganizationDomain("MooseAche.org")
window = MainWindow()
app.exec()

View File

@@ -0,0 +1,277 @@
import os
import sys
from PyQt6.QtCore import QSize, Qt, QUrl
from PyQt6.QtGui import QAction, QIcon, QPixmap
from PyQt6.QtPrintSupport import QPrintPreviewDialog
from PyQt6.QtWebEngineCore import QWebEnginePage
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QFileDialog,
QLabel,
QLineEdit,
QMainWindow,
QStatusBar,
QToolBar,
QVBoxLayout,
)
class AboutDialog(QDialog):
def __init__(self):
super().__init__()
QBtn = QDialogButtonBox.StandardButton.Ok # No cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout = QVBoxLayout()
title = QLabel("MooseAche")
font = title.font()
font.setPointSize(20)
title.setFont(font)
layout.addWidget(title)
logo = QLabel()
logo.setPixmap(QPixmap(os.path.join("images", "ma-icon-128.png")))
layout.addWidget(logo)
layout.addWidget(QLabel("Version 23.35.211.233232"))
layout.addWidget(QLabel("Copyright 2015 MooseAche Inc."))
for i in range(0, layout.count()):
layout.itemAt(i).setAlignment(Qt.AlignmentFlag.AlignHCenter)
layout.addWidget(self.buttonBox)
self.setLayout(layout)
class WebEnginePage(QWebEnginePage):
# Store externally created window.
external_window = None
def acceptNavigationRequest(self, url, _type, isMainFrame):
print(url, _type, isMainFrame)
if _type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
if not self.external_window:
self.external_window = QWebEngineView()
self.external_window.setUrl(url)
self.external_window.show()
return False
return super().acceptNavigationRequest(url, _type, isMainFrame)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.browser = QWebEngineView()
self.browser.setPage(WebEnginePage(self))
self.browser.setUrl(QUrl("http://google.com"))
self.browser.urlChanged.connect(self.update_urlbar)
self.browser.loadFinished.connect(self.update_title)
self.setCentralWidget(self.browser)
self.status = QStatusBar()
self.setStatusBar(self.status)
navtb = QToolBar("Navigation")
navtb.setIconSize(QSize(16, 16))
self.addToolBar(navtb)
back_btn = QAction(QIcon(os.path.join("images", "arrow-180.png")), "Back", self)
back_btn.setStatusTip("Back to previous page")
back_btn.triggered.connect(self.browser.back)
navtb.addAction(back_btn)
next_btn = QAction(
QIcon(os.path.join("images", "arrow-000.png")),
"Forward",
self,
)
next_btn.setStatusTip("Forward to next page")
next_btn.triggered.connect(self.browser.forward)
navtb.addAction(next_btn)
reload_btn = QAction(
QIcon(os.path.join("images", "arrow-circle-315.png")),
"Reload",
self,
)
reload_btn.setStatusTip("Reload page")
reload_btn.triggered.connect(self.browser.reload)
navtb.addAction(reload_btn)
home_btn = QAction(QIcon(os.path.join("images", "home.png")), "Home", self)
home_btn.setStatusTip("Go home")
home_btn.triggered.connect(self.navigate_home)
navtb.addAction(home_btn)
navtb.addSeparator()
self.httpsicon = QLabel() # Yes, really!
self.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-nossl.png")))
navtb.addWidget(self.httpsicon)
self.urlbar = QLineEdit()
self.urlbar.returnPressed.connect(self.navigate_to_url)
navtb.addWidget(self.urlbar)
stop_btn = QAction(
QIcon(os.path.join("images", "cross-circle.png")),
"Stop",
self,
)
stop_btn.setStatusTip("Stop loading current page")
stop_btn.triggered.connect(self.browser.stop)
navtb.addAction(stop_btn)
# Uncomment to disable native menubar on Mac
# self.menuBar().setNativeMenuBar(False)
file_menu = self.menuBar().addMenu("&File")
open_file_action = QAction(
QIcon(os.path.join("images", "disk--arrow.png")),
"Open file...",
self,
)
open_file_action.setStatusTip("Open from file")
open_file_action.triggered.connect(self.open_file)
file_menu.addAction(open_file_action)
save_file_action = QAction(
QIcon(os.path.join("images", "disk--pencil.png")),
"Save Page As...",
self,
)
save_file_action.setStatusTip("Save current page to file")
save_file_action.triggered.connect(self.save_file)
file_menu.addAction(save_file_action)
print_action = QAction(
QIcon(os.path.join("images", "printer.png")),
"Print...",
self,
)
print_action.setStatusTip("Print current page")
print_action.triggered.connect(self.print_page)
file_menu.addAction(print_action)
help_menu = self.menuBar().addMenu("&Help")
about_action = QAction(
QIcon(os.path.join("images", "question.png")),
"About MooseAche",
self,
)
about_action.setStatusTip("Find out more about MooseAche") # Hungry!
about_action.triggered.connect(self.about)
help_menu.addAction(about_action)
navigate_mozarella_action = QAction(
QIcon(os.path.join("images", "lifebuoy.png")),
"MooseAche Homepage",
self,
)
navigate_mozarella_action.setStatusTip("Go to MooseAche Homepage")
navigate_mozarella_action.triggered.connect(self.navigate_mozarella)
help_menu.addAction(navigate_mozarella_action)
self.show()
self.setWindowIcon(QIcon(os.path.join("images", "ma-icon-64.png")))
def acceptNavigationRequest(self, url, navigation_type, is_main_frame):
self._externalview = QWebEngineView(url)
return False
def linkClicked(self, e):
print(e)
def update_title(self):
title = self.browser.page().title()
self.setWindowTitle("%s - MooseAche" % title)
def navigate_mozarella(self):
self.browser.setUrl(QUrl("https://www.pythonguis.com/"))
def about(self):
dlg = AboutDialog()
dlg.exec()
def open_file(self):
filename, _ = QFileDialog.getOpenFileName(
self,
"Open file",
"",
"Hypertext Markup Language (*.htm *.html);;" "All files (*.*)",
)
if filename:
with open(filename, "r") as f:
html = f.read()
self.browser.setHtml(html)
self.urlbar.setText(filename)
def save_file(self):
filename, _ = QFileDialog.getSaveFileName(
self,
"Save Page As",
"",
"Hypertext Markup Language (*.htm *html);;" "All files (*.*)",
)
if filename:
html = self.browser.page().toHtml()
with open(filename, "w") as f:
f.write(html)
def print_page(self):
dlg = QPrintPreviewDialog()
dlg.paintRequested.connect(self.browser.print_)
dlg.exec()
def navigate_home(self):
self.browser.setUrl(QUrl("http://www.google.com"))
def navigate_to_url(self): # Does not receive the Url
q = QUrl(self.urlbar.text())
if q.scheme() == "":
q.setScheme("http")
self.browser.setUrl(q)
def update_urlbar(self, q):
if q.scheme() == "https":
# Secure padlock icon
self.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-ssl.png")))
else:
# Insecure padlock icon
self.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-nossl.png")))
self.urlbar.setText(q.toString())
self.urlbar.setCursorPosition(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setApplicationName("MooseAche")
app.setOrganizationName("MooseAche")
app.setOrganizationDomain("MooseAche.org")
window = MainWindow()
app.exec()

View File

@@ -0,0 +1,2 @@
PyQt5>=5.6
PyQtWebEngine

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -0,0 +1,28 @@
# Mozzarella Ashbadger — Upgrade your browsing with tabs
Mozarella Ashbadger is the latest revolution in web
browsing! Go back and forward! Print! Save files! Get help!
(youll need it). Any similarity to other browsers is entirely
coincidental.
![Browser tabbed](screenshot-browser-tabbed.jpg)
> If you want to learn more about build GUI applications with Python,
take a look at my [PyQt5 tutorials](https://www.pythonguis.com)
which covers everything you need to know to start building your own applications with PyQt5.
## Code notes
### Tabbing
Adding tab support complicates the internals of the browser a bit, since we
now need to keep track of the currently active browser view, both to update
UI elements (URL bar, HTTPs icon) to changing state in the currently active
window, and to ensure the UI events are dispatched to the correct web view.
This is achieved by using intermediate slots which filter events, and by
adding signal redirection (using lamba functions to keep it short).
## Other licenses
Icons used in the application are by [Yusuke Kamiyaman](http://p.yusukekamiyamane.com/).

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

View File

@@ -0,0 +1,326 @@
import os
import sys
from PyQt6.QtCore import QSize, Qt, QUrl
from PyQt6.QtGui import QAction, QIcon, QKeySequence, QPixmap, QShortcut
from PyQt6.QtPrintSupport import QPrintPreviewDialog
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QFileDialog,
QLabel,
QLineEdit,
QMainWindow,
QStatusBar,
QTabWidget,
QToolBar,
QVBoxLayout,
)
class AboutDialog(QDialog):
def __init__(self):
super().__init__()
QBtn = QDialogButtonBox.StandardButton.Ok # No cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout = QVBoxLayout()
title = QLabel("Mozarella Ashbadger")
font = title.font()
font.setPointSize(20)
title.setFont(font)
layout.addWidget(title)
logo = QLabel()
logo.setPixmap(QPixmap(os.path.join("images", "ma-icon-128.png")))
layout.addWidget(logo)
layout.addWidget(QLabel("Version 23.35.211.233232"))
layout.addWidget(QLabel("Copyright 2015 Mozarella Inc."))
for i in range(0, layout.count()):
layout.itemAt(i).setAlignment(Qt.AlignmentFlag.AlignHCenter)
layout.addWidget(self.buttonBox)
self.setLayout(layout)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.tabs = QTabWidget()
self.tabs.setDocumentMode(True)
self.tabs.tabBarDoubleClicked.connect(self.tab_open_doubleclick)
self.tabs.currentChanged.connect(self.current_tab_changed)
self.tabs.setTabsClosable(True)
self.tabs.tabCloseRequested.connect(self.close_current_tab)
self.setCentralWidget(self.tabs)
self.status = QStatusBar()
self.setStatusBar(self.status)
navtb = QToolBar("Navigation")
navtb.setIconSize(QSize(16, 16))
self.addToolBar(navtb)
"""Shortcuts"""
self.shortcut_open = QShortcut(QKeySequence("F5"), self)
self.shortcut_open.activated.connect(lambda: self.tabs.currentWidget().reload())
back_btn = QAction(QIcon(os.path.join("images", "arrow-180.png")), "Back", self)
back_btn.setStatusTip("Back to previous page")
back_btn.triggered.connect(lambda: self.tabs.currentWidget().back())
navtb.addAction(back_btn)
next_btn = QAction(
QIcon(os.path.join("images", "arrow-000.png")),
"Forward",
self,
)
next_btn.setStatusTip("Forward to next page")
next_btn.triggered.connect(lambda: self.tabs.currentWidget().forward())
navtb.addAction(next_btn)
reload_btn = QAction(
QIcon(os.path.join("images", "arrow-circle-315.png")),
"Reload",
self,
)
reload_btn.setStatusTip("Reload page")
reload_btn.triggered.connect(lambda: self.tabs.currentWidget().reload())
navtb.addAction(reload_btn)
home_btn = QAction(QIcon(os.path.join("images", "home.png")), "Home", self)
home_btn.setStatusTip("Go home")
home_btn.triggered.connect(self.navigate_home)
navtb.addAction(home_btn)
navtb.addSeparator()
self.httpsicon = QLabel() # Yes, really!
self.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-nossl.png")))
navtb.addWidget(self.httpsicon)
self.urlbar = QLineEdit()
self.urlbar.returnPressed.connect(self.navigate_to_url)
navtb.addWidget(self.urlbar)
stop_btn = QAction(
QIcon(os.path.join("images", "cross-circle.png")),
"Stop",
self,
)
stop_btn.setStatusTip("Stop loading current page")
stop_btn.triggered.connect(lambda: self.tabs.currentWidget().stop())
navtb.addAction(stop_btn)
# Uncomment to disable native menubar on Mac
# self.menuBar().setNativeMenuBar(False)
file_menu = self.menuBar().addMenu("&File")
new_tab_action = QAction(
QIcon(os.path.join("images", "ui-tab--plus.png")),
"New Tab",
self,
)
new_tab_action.setStatusTip("Open a new tab")
new_tab_action.triggered.connect(lambda _: self.add_new_tab())
file_menu.addAction(new_tab_action)
open_file_action = QAction(
QIcon(os.path.join("images", "disk--arrow.png")),
"Open file...",
self,
)
open_file_action.setStatusTip("Open from file")
open_file_action.triggered.connect(self.open_file)
file_menu.addAction(open_file_action)
save_file_action = QAction(
QIcon(os.path.join("images", "disk--pencil.png")),
"Save Page As...",
self,
)
save_file_action.setStatusTip("Save current page to file")
save_file_action.triggered.connect(self.save_file)
file_menu.addAction(save_file_action)
print_action = QAction(
QIcon(os.path.join("images", "printer.png")),
"Print...",
self,
)
print_action.setStatusTip("Print current page")
print_action.triggered.connect(self.print_page)
file_menu.addAction(print_action)
help_menu = self.menuBar().addMenu("&Help")
about_action = QAction(
QIcon(os.path.join("images", "question.png")),
"About Mozarella Ashbadger",
self,
)
about_action.setStatusTip("Find out more about Mozarella Ashbadger") # Hungry!
about_action.triggered.connect(self.about)
help_menu.addAction(about_action)
navigate_mozarella_action = QAction(
QIcon(os.path.join("images", "lifebuoy.png")),
"Mozarella Ashbadger Homepage",
self,
)
navigate_mozarella_action.setStatusTip("Go to Mozarella Ashbadger Homepage")
navigate_mozarella_action.triggered.connect(self.navigate_mozarella)
help_menu.addAction(navigate_mozarella_action)
self.add_new_tab(QUrl("http://www.google.com"), "Homepage")
self.add_new_tab(QUrl("http://www.pythonguis.com"), "PythonGUIs")
self.show()
self.setWindowTitle("Mozarella Ashbadger")
self.setWindowIcon(QIcon(os.path.join("images", "ma-icon-64.png")))
def add_new_tab(self, qurl=None, label="Blank"):
if qurl is None:
qurl = QUrl("")
browser = QWebEngineView()
browser.setUrl(qurl)
i = self.tabs.addTab(browser, label)
self.tabs.setCurrentIndex(i)
# More difficult! We only want to update the url when it's from the
# correct tab
browser.urlChanged.connect(
lambda qurl, browser=browser: self.update_urlbar(qurl, browser)
)
browser.titleChanged.connect(
lambda _, i=i, browser=browser: self.tabs.setTabText(
i, browser.page().title()
)
)
browser.titleChanged.connect(
lambda _, i=i, browser=browser: self.tabs.setTabToolTip(
i, browser.page().title()
)
)
browser.loadFinished.connect(
lambda _, i=i, browser=browser: self.tabs.setTabText(
i, browser.page().title()
)
)
def tab_open_doubleclick(self, i):
if i == -1: # No tab under the click
self.add_new_tab()
def current_tab_changed(self, i):
qurl = self.tabs.currentWidget().url()
self.update_urlbar(qurl, self.tabs.currentWidget())
self.update_title(self.tabs.currentWidget())
def close_current_tab(self, i):
if self.tabs.count() < 2:
return
self.tabs.removeTab(i)
def update_title(self, browser):
if browser != self.tabs.currentWidget():
# If this signal is not from the current tab, ignore
return
title = self.tabs.currentWidget().page().title()
self.setWindowTitle("%s - Mozarella Ashbadger" % title)
def navigate_mozarella(self):
self.tabs.currentWidget().setUrl(QUrl("https://www.pythonguis.com/"))
def about(self):
dlg = AboutDialog()
dlg.exec()
def open_file(self):
filename, _ = QFileDialog.getOpenFileName(
self,
"Open file",
"",
"Hypertext Markup Language (*.htm *.html);;" "All files (*.*)",
)
if filename:
with open(filename, "r") as f:
html = f.read()
self.tabs.currentWidget().setHtml(html)
self.urlbar.setText(filename)
def save_file(self):
filename, _ = QFileDialog.getSaveFileName(
self,
"Save Page As",
"",
"Hypertext Markup Language (*.htm *html);;" "All files (*.*)",
)
if filename:
html = self.tabs.currentWidget().page().toHtml()
with open(filename, "w") as f:
f.write(html.encode("utf8"))
def print_page(self):
dlg = QPrintPreviewDialog()
dlg.paintRequested.connect(self.browser.print_)
dlg.exec()
def navigate_home(self):
self.tabs.currentWidget().setUrl(QUrl("http://www.google.com"))
def navigate_to_url(self): # Does not receive the Url
q = QUrl(self.urlbar.text())
if q.scheme() == "":
q.setScheme("http")
self.tabs.currentWidget().setUrl(q)
def update_urlbar(self, q, browser=None):
if browser != self.tabs.currentWidget():
# If this signal is not from the current tab, ignore
return
if q.scheme() == "https":
# Secure padlock icon
self.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-ssl.png")))
else:
# Insecure padlock icon
self.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-nossl.png")))
self.urlbar.setText(q.toString())
self.urlbar.setCursorPosition(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setApplicationName("Mozarella Ashbadger")
app.setOrganizationName("Mozarella")
app.setOrganizationDomain("mozarella.org")
window = MainWindow()
app.exec()

View File

@@ -0,0 +1,2 @@
PyQt5>=5.6
PyQtWebEngine

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@@ -0,0 +1,314 @@
# Form implementation generated from reading ui file 'mainwindow.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(484, 433)
self.centralWidget = QtWidgets.QWidget(parent=MainWindow)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Maximum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.centralWidget.sizePolicy().hasHeightForWidth())
self.centralWidget.setSizePolicy(sizePolicy)
self.centralWidget.setObjectName("centralWidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralWidget)
self.verticalLayout.setContentsMargins(11, 11, 11, 11)
self.verticalLayout.setSpacing(6)
self.verticalLayout.setObjectName("verticalLayout")
self.lcdNumber = QtWidgets.QLCDNumber(parent=self.centralWidget)
self.lcdNumber.setDigitCount(10)
self.lcdNumber.setObjectName("lcdNumber")
self.verticalLayout.addWidget(self.lcdNumber)
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setSpacing(6)
self.gridLayout.setObjectName("gridLayout")
self.pushButton_n4 = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_n4.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(True)
font.setWeight(75)
self.pushButton_n4.setFont(font)
self.pushButton_n4.setStyleSheet("QPushButton {\n"
"color: #1976D2;\n"
"}")
self.pushButton_n4.setObjectName("pushButton_n4")
self.gridLayout.addWidget(self.pushButton_n4, 3, 0, 1, 1)
self.pushButton_n1 = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_n1.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(True)
font.setWeight(75)
self.pushButton_n1.setFont(font)
self.pushButton_n1.setStyleSheet("QPushButton {\n"
"color: #1976D2;\n"
"}")
self.pushButton_n1.setObjectName("pushButton_n1")
self.gridLayout.addWidget(self.pushButton_n1, 4, 0, 1, 1)
self.pushButton_n8 = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_n8.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(True)
font.setWeight(75)
self.pushButton_n8.setFont(font)
self.pushButton_n8.setStyleSheet("QPushButton {\n"
"color: #1976D2;\n"
"}")
self.pushButton_n8.setObjectName("pushButton_n8")
self.gridLayout.addWidget(self.pushButton_n8, 2, 1, 1, 1)
self.pushButton_mul = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_mul.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(False)
font.setWeight(50)
self.pushButton_mul.setFont(font)
self.pushButton_mul.setObjectName("pushButton_mul")
self.gridLayout.addWidget(self.pushButton_mul, 2, 3, 1, 1)
self.pushButton_n7 = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_n7.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(True)
font.setWeight(75)
self.pushButton_n7.setFont(font)
self.pushButton_n7.setStyleSheet("QPushButton {\n"
"color: #1976D2;\n"
"}")
self.pushButton_n7.setObjectName("pushButton_n7")
self.gridLayout.addWidget(self.pushButton_n7, 2, 0, 1, 1)
self.pushButton_n6 = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_n6.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(True)
font.setWeight(75)
self.pushButton_n6.setFont(font)
self.pushButton_n6.setStyleSheet("QPushButton {\n"
"color: #1976D2;\n"
"}")
self.pushButton_n6.setObjectName("pushButton_n6")
self.gridLayout.addWidget(self.pushButton_n6, 3, 2, 1, 1)
self.pushButton_n5 = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_n5.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(True)
font.setWeight(75)
self.pushButton_n5.setFont(font)
self.pushButton_n5.setStyleSheet("QPushButton {\n"
"color: #1976D2;\n"
"}")
self.pushButton_n5.setObjectName("pushButton_n5")
self.gridLayout.addWidget(self.pushButton_n5, 3, 1, 1, 1)
self.pushButton_n0 = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_n0.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(True)
font.setWeight(75)
self.pushButton_n0.setFont(font)
self.pushButton_n0.setStyleSheet("QPushButton {\n"
"color: #1976D2;\n"
"}")
self.pushButton_n0.setObjectName("pushButton_n0")
self.gridLayout.addWidget(self.pushButton_n0, 5, 0, 1, 1)
self.pushButton_n2 = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_n2.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(True)
font.setWeight(75)
self.pushButton_n2.setFont(font)
self.pushButton_n2.setStyleSheet("QPushButton {\n"
"color: #1976D2;\n"
"}")
self.pushButton_n2.setObjectName("pushButton_n2")
self.gridLayout.addWidget(self.pushButton_n2, 4, 1, 1, 1)
self.pushButton_n9 = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_n9.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(True)
font.setWeight(75)
self.pushButton_n9.setFont(font)
self.pushButton_n9.setStyleSheet("QPushButton {\n"
"color: #1976D2;\n"
"}")
self.pushButton_n9.setObjectName("pushButton_n9")
self.gridLayout.addWidget(self.pushButton_n9, 2, 2, 1, 1)
self.pushButton_n3 = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_n3.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(True)
font.setWeight(75)
self.pushButton_n3.setFont(font)
self.pushButton_n3.setStyleSheet("QPushButton {\n"
"color: #1976D2;\n"
"}")
self.pushButton_n3.setObjectName("pushButton_n3")
self.gridLayout.addWidget(self.pushButton_n3, 4, 2, 1, 1)
self.pushButton_div = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_div.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(False)
font.setWeight(50)
self.pushButton_div.setFont(font)
self.pushButton_div.setObjectName("pushButton_div")
self.gridLayout.addWidget(self.pushButton_div, 1, 3, 1, 1)
self.pushButton_sub = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_sub.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(False)
font.setWeight(50)
self.pushButton_sub.setFont(font)
self.pushButton_sub.setObjectName("pushButton_sub")
self.gridLayout.addWidget(self.pushButton_sub, 3, 3, 1, 1)
self.pushButton_add = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_add.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(False)
font.setWeight(50)
self.pushButton_add.setFont(font)
self.pushButton_add.setObjectName("pushButton_add")
self.gridLayout.addWidget(self.pushButton_add, 4, 3, 1, 1)
self.pushButton_ac = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_ac.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(False)
font.setWeight(50)
self.pushButton_ac.setFont(font)
self.pushButton_ac.setStyleSheet("QPushButton {\n"
" color: #f44336;\n"
"}")
self.pushButton_ac.setObjectName("pushButton_ac")
self.gridLayout.addWidget(self.pushButton_ac, 1, 0, 1, 1)
self.pushButton_mr = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_mr.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(False)
font.setWeight(50)
self.pushButton_mr.setFont(font)
self.pushButton_mr.setStyleSheet("QPushButton {\n"
" color: #FFC107;\n"
"}")
self.pushButton_mr.setObjectName("pushButton_mr")
self.gridLayout.addWidget(self.pushButton_mr, 1, 2, 1, 1)
self.pushButton_m = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_m.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(False)
font.setWeight(50)
self.pushButton_m.setFont(font)
self.pushButton_m.setStyleSheet("QPushButton {\n"
" color: #FFC107;\n"
"}")
self.pushButton_m.setObjectName("pushButton_m")
self.gridLayout.addWidget(self.pushButton_m, 1, 1, 1, 1)
self.pushButton_pc = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_pc.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(False)
font.setWeight(50)
self.pushButton_pc.setFont(font)
self.pushButton_pc.setObjectName("pushButton_pc")
self.gridLayout.addWidget(self.pushButton_pc, 5, 1, 1, 1)
self.pushButton_eq = QtWidgets.QPushButton(parent=self.centralWidget)
self.pushButton_eq.setMinimumSize(QtCore.QSize(0, 50))
font = QtGui.QFont()
font.setPointSize(27)
font.setBold(True)
font.setWeight(75)
self.pushButton_eq.setFont(font)
self.pushButton_eq.setStyleSheet("QPushButton {\n"
"color: #4CAF50;\n"
"}")
self.pushButton_eq.setObjectName("pushButton_eq")
self.gridLayout.addWidget(self.pushButton_eq, 5, 2, 1, 2)
self.verticalLayout.addLayout(self.gridLayout)
MainWindow.setCentralWidget(self.centralWidget)
self.menuBar = QtWidgets.QMenuBar(parent=MainWindow)
self.menuBar.setGeometry(QtCore.QRect(0, 0, 484, 22))
self.menuBar.setObjectName("menuBar")
self.menuFile = QtWidgets.QMenu(parent=self.menuBar)
self.menuFile.setObjectName("menuFile")
MainWindow.setMenuBar(self.menuBar)
self.statusBar = QtWidgets.QStatusBar(parent=MainWindow)
self.statusBar.setObjectName("statusBar")
MainWindow.setStatusBar(self.statusBar)
self.actionExit = QtGui.QAction(parent=MainWindow)
self.actionExit.setObjectName("actionExit")
self.actionReset = QtGui.QAction(parent=MainWindow)
self.actionReset.setObjectName("actionReset")
self.menuFile.addAction(self.actionReset)
self.menuFile.addAction(self.actionExit)
self.menuBar.addAction(self.menuFile.menuAction())
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Calculon"))
self.pushButton_n4.setText(_translate("MainWindow", "4"))
self.pushButton_n4.setShortcut(_translate("MainWindow", "4"))
self.pushButton_n1.setText(_translate("MainWindow", "1"))
self.pushButton_n1.setShortcut(_translate("MainWindow", "1"))
self.pushButton_n8.setText(_translate("MainWindow", "8"))
self.pushButton_n8.setShortcut(_translate("MainWindow", "8"))
self.pushButton_mul.setText(_translate("MainWindow", "x"))
self.pushButton_mul.setShortcut(_translate("MainWindow", "*"))
self.pushButton_n7.setText(_translate("MainWindow", "7"))
self.pushButton_n7.setShortcut(_translate("MainWindow", "7"))
self.pushButton_n6.setText(_translate("MainWindow", "6"))
self.pushButton_n6.setShortcut(_translate("MainWindow", "6"))
self.pushButton_n5.setText(_translate("MainWindow", "5"))
self.pushButton_n5.setShortcut(_translate("MainWindow", "5"))
self.pushButton_n0.setText(_translate("MainWindow", "0"))
self.pushButton_n0.setShortcut(_translate("MainWindow", "0"))
self.pushButton_n2.setText(_translate("MainWindow", "2"))
self.pushButton_n2.setShortcut(_translate("MainWindow", "2"))
self.pushButton_n9.setText(_translate("MainWindow", "9"))
self.pushButton_n9.setShortcut(_translate("MainWindow", "9"))
self.pushButton_n3.setText(_translate("MainWindow", "3"))
self.pushButton_n3.setShortcut(_translate("MainWindow", "3"))
self.pushButton_div.setText(_translate("MainWindow", "÷"))
self.pushButton_div.setShortcut(_translate("MainWindow", "/"))
self.pushButton_sub.setText(_translate("MainWindow", "-"))
self.pushButton_sub.setShortcut(_translate("MainWindow", "-"))
self.pushButton_add.setText(_translate("MainWindow", "+"))
self.pushButton_add.setShortcut(_translate("MainWindow", "+"))
self.pushButton_ac.setText(_translate("MainWindow", "AC"))
self.pushButton_ac.setShortcut(_translate("MainWindow", "Esc"))
self.pushButton_mr.setText(_translate("MainWindow", "MR"))
self.pushButton_mr.setShortcut(_translate("MainWindow", "R"))
self.pushButton_m.setText(_translate("MainWindow", "M"))
self.pushButton_m.setShortcut(_translate("MainWindow", "M"))
self.pushButton_pc.setText(_translate("MainWindow", "%"))
self.pushButton_pc.setShortcut(_translate("MainWindow", "%"))
self.pushButton_eq.setText(_translate("MainWindow", "="))
self.pushButton_eq.setShortcut(_translate("MainWindow", "Return"))
self.menuFile.setTitle(_translate("MainWindow", "File"))
self.actionExit.setText(_translate("MainWindow", "Exit"))
self.actionExit.setShortcut(_translate("MainWindow", "Ctrl+Q"))
self.actionReset.setText(_translate("MainWindow", "Reset"))
self.actionReset.setShortcut(_translate("MainWindow", "Ctrl+R"))

View File

@@ -0,0 +1,11 @@
# Calculon - A desktop calculator in PyQt
A simple calculator application implemented in Python using PyQt. The UI was designed in Qt Designer and the
calculator operations are implemented using simple stack-based logic.
![Calculon](screenshot-calculator.jpg)
> If you think this example app is neat and want to learn more about
PyQt5, [take a look at my ebook & online course
"Create Simple GUI Applications"](https://martinfitzpatrick.name/create-simple-gui-applications)
which covers everything you need to know to start building your own applications with PyQt5.

View File

@@ -0,0 +1,110 @@
import operator
import sys
from MainWindow import Ui_MainWindow
from PyQt6.QtWidgets import QApplication, QMainWindow
# Calculator state.
READY = 0
INPUT = 1
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
# Setup numbers.
for n in range(0, 10):
getattr(self, "pushButton_n%s" % n).pressed.connect(lambda v=n: self.input_number(v))
# Setup operations.
self.pushButton_add.pressed.connect(lambda: self.operation(operator.add))
self.pushButton_sub.pressed.connect(lambda: self.operation(operator.sub))
self.pushButton_mul.pressed.connect(lambda: self.operation(operator.mul))
self.pushButton_div.pressed.connect(lambda: self.operation(operator.truediv)) # operator.div for Python2.7
self.pushButton_pc.pressed.connect(self.operation_pc)
self.pushButton_eq.pressed.connect(self.equals)
# Setup actions
self.actionReset.triggered.connect(self.reset)
self.pushButton_ac.pressed.connect(self.reset)
self.actionExit.triggered.connect(self.close)
self.pushButton_m.pressed.connect(self.memory_store)
self.pushButton_mr.pressed.connect(self.memory_recall)
self.memory = 0
self.reset()
self.show()
def display(self):
self.lcdNumber.display(self.stack[-1])
def reset(self):
self.state = READY
self.stack = [0]
self.last_operation = None
self.current_op = None
self.display()
def memory_store(self):
self.memory = self.lcdNumber.value()
def memory_recall(self):
self.state = INPUT
self.stack[-1] = self.memory
self.display()
def input_number(self, v):
if self.state == READY:
self.state = INPUT
self.stack[-1] = v
else:
self.stack[-1] = self.stack[-1] * 10 + v
self.display()
def operation(self, op):
if self.current_op: # Complete the current operation
self.equals()
self.stack.append(0)
self.state = INPUT
self.current_op = op
def operation_pc(self):
self.state = INPUT
self.stack[-1] *= 0.01
self.display()
def equals(self):
# Support to allow '=' to repeat previous operation
# if no further input has been added.
if self.state == READY and self.last_operation:
s, self.current_op = self.last_operation
self.stack.append(s)
if self.current_op:
self.last_operation = self.stack[-1], self.current_op
try:
self.stack = [self.current_op(*self.stack)]
except Exception:
self.lcdNumber.display("Err")
self.stack = [0]
else:
self.current_op = None
self.state = READY
self.display()
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setApplicationName("Calculon")
window = MainWindow()
app.exec()

View File

@@ -0,0 +1,583 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>484</width>
<height>534</height>
</rect>
</property>
<property name="windowTitle">
<string>Calculon</string>
</property>
<widget class="QWidget" name="centralWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0">
<widget class="QPushButton" name="pushButton_n4">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>4</string>
</property>
<property name="shortcut">
<string>4</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QPushButton" name="pushButton_n1">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>1</string>
</property>
<property name="shortcut">
<string>1</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="pushButton_n8">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>8</string>
</property>
<property name="shortcut">
<string>8</string>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QPushButton" name="pushButton_mul">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>x</string>
</property>
<property name="shortcut">
<string>*</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="pushButton_n7">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>7</string>
</property>
<property name="shortcut">
<string>7</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="pushButton_n6">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>6</string>
</property>
<property name="shortcut">
<string>6</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="pushButton_n5">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>5</string>
</property>
<property name="shortcut">
<string>5</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QPushButton" name="pushButton_n0">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>0</string>
</property>
<property name="shortcut">
<string>0</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="pushButton_n2">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>2</string>
</property>
<property name="shortcut">
<string>2</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="pushButton_n9">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>9</string>
</property>
<property name="shortcut">
<string>9</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QPushButton" name="pushButton_n3">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>3</string>
</property>
<property name="shortcut">
<string>3</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="pushButton_div">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>÷</string>
</property>
<property name="shortcut">
<string>/</string>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QPushButton" name="pushButton_sub">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>-</string>
</property>
<property name="shortcut">
<string>-</string>
</property>
</widget>
</item>
<item row="4" column="3">
<widget class="QPushButton" name="pushButton_add">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>+</string>
</property>
<property name="shortcut">
<string>+</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="pushButton_ac">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #f44336;
}</string>
</property>
<property name="text">
<string>AC</string>
</property>
<property name="shortcut">
<string>Esc</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="pushButton_mr">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #FFC107;
}</string>
</property>
<property name="text">
<string>MR</string>
</property>
<property name="shortcut">
<string>R</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="pushButton_m">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #FFC107;
}</string>
</property>
<property name="text">
<string>M</string>
</property>
<property name="shortcut">
<string>M</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QPushButton" name="pushButton_pc">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>%</string>
</property>
<property name="shortcut">
<string>%</string>
</property>
</widget>
</item>
<item row="5" column="2" colspan="2">
<widget class="QPushButton" name="pushButton_eq">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #4CAF50;
}</string>
</property>
<property name="text">
<string>=</string>
</property>
<property name="shortcut">
<string>Return</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLCDNumber" name="lcdNumber">
<property name="digitCount">
<number>10</number>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>484</width>
<height>26</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<addaction name="actionReset"/>
<addaction name="actionExit"/>
</widget>
<addaction name="menuFile"/>
</widget>
<widget class="QStatusBar" name="statusBar"/>
<action name="actionExit">
<property name="text">
<string>Exit</string>
</property>
<property name="shortcut">
<string>Ctrl+Q</string>
</property>
</action>
<action name="actionReset">
<property name="text">
<string>Reset</string>
</property>
<property name="shortcut">
<string>Ctrl+R</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,583 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>484</width>
<height>433</height>
</rect>
</property>
<property name="windowTitle">
<string>Calculon</string>
</property>
<widget class="QWidget" name="centralWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLCDNumber" name="lcdNumber">
<property name="digitCount">
<number>10</number>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0">
<widget class="QPushButton" name="pushButton_n4">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>4</string>
</property>
<property name="shortcut">
<string>4</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QPushButton" name="pushButton_n1">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>1</string>
</property>
<property name="shortcut">
<string>1</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="pushButton_n8">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>8</string>
</property>
<property name="shortcut">
<string>8</string>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QPushButton" name="pushButton_mul">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>x</string>
</property>
<property name="shortcut">
<string>*</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="pushButton_n7">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>7</string>
</property>
<property name="shortcut">
<string>7</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="pushButton_n6">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>6</string>
</property>
<property name="shortcut">
<string>6</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="pushButton_n5">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>5</string>
</property>
<property name="shortcut">
<string>5</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QPushButton" name="pushButton_n0">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>0</string>
</property>
<property name="shortcut">
<string>0</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="pushButton_n2">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>2</string>
</property>
<property name="shortcut">
<string>2</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="pushButton_n9">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>9</string>
</property>
<property name="shortcut">
<string>9</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QPushButton" name="pushButton_n3">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #1976D2;
}</string>
</property>
<property name="text">
<string>3</string>
</property>
<property name="shortcut">
<string>3</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="pushButton_div">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>÷</string>
</property>
<property name="shortcut">
<string>/</string>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QPushButton" name="pushButton_sub">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>-</string>
</property>
<property name="shortcut">
<string>-</string>
</property>
</widget>
</item>
<item row="4" column="3">
<widget class="QPushButton" name="pushButton_add">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>+</string>
</property>
<property name="shortcut">
<string>+</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="pushButton_ac">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #f44336;
}</string>
</property>
<property name="text">
<string>AC</string>
</property>
<property name="shortcut">
<string>Esc</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="pushButton_mr">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #FFC107;
}</string>
</property>
<property name="text">
<string>MR</string>
</property>
<property name="shortcut">
<string>R</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="pushButton_m">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #FFC107;
}</string>
</property>
<property name="text">
<string>M</string>
</property>
<property name="shortcut">
<string>M</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QPushButton" name="pushButton_pc">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>%</string>
</property>
<property name="shortcut">
<string>%</string>
</property>
</widget>
</item>
<item row="5" column="2" colspan="2">
<widget class="QPushButton" name="pushButton_eq">
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="font">
<font>
<pointsize>27</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #4CAF50;
}</string>
</property>
<property name="text">
<string>=</string>
</property>
<property name="shortcut">
<string>Return</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>484</width>
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<addaction name="actionReset"/>
<addaction name="actionExit"/>
</widget>
<addaction name="menuFile"/>
</widget>
<widget class="QStatusBar" name="statusBar"/>
<action name="actionExit">
<property name="text">
<string>Exit</string>
</property>
<property name="shortcut">
<string>Ctrl+Q</string>
</property>
</action>
<action name="actionReset">
<property name="text">
<string>Reset</string>
</property>
<property name="shortcut">
<string>Ctrl+R</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1 @@
PyQt5>=5.6

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -0,0 +1,16 @@
# NSAViewer — Somebody's watching you.
With this webcam snapshot application you can take photos of what is currently
being viewed by your webcam. Uses the Qt QtMultimedia framework for handling
all the interaction with the camera, and supports multiple cameras if you have
them.
![Camera](screenshot-camera.jpg)
> If you want to learn more about build GUI applications with Python,
take a look at my [PyQt5 tutorials](https://www.pythonguis.com)
which covers everything you need to know to start building your own applications with PyQt5.
## Other licenses
Icons used in the application are by [Yusuke Kamiyaman](http://p.yusukekamiyamane.com/).

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

117
pyqt6/demos/camera/main.py Normal file
View File

@@ -0,0 +1,117 @@
import os
import sys
import time
from PyQt6.QtCore import QSize
from PyQt6.QtGui import QAction, QIcon
from PyQt6.QtMultimedia import QCamera, QCameraImageCapture, QCameraInfo
from PyQt6.QtMultimediaWidgets import QCameraViewfinder
from PyQt6.QtWidgets import (
QApplication,
QComboBox,
QErrorMessage,
QFileDialog,
QMainWindow,
QStatusBar,
QToolBar,
)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.available_cameras = QCameraInfo.availableCameras()
if not self.available_cameras:
pass # quit
self.status = QStatusBar()
self.setStatusBar(self.status)
self.save_path = ""
self.viewfinder = QCameraViewfinder()
self.viewfinder.show()
self.setCentralWidget(self.viewfinder)
# Set the default camera.
self.select_camera(0)
# Setup tools
camera_toolbar = QToolBar("Camera")
camera_toolbar.setIconSize(QSize(14, 14))
self.addToolBar(camera_toolbar)
photo_action = QAction(
QIcon(os.path.join("images", "camera-black.png")),
"Take photo...",
self,
)
photo_action.setStatusTip("Take photo of current view")
photo_action.triggered.connect(self.take_photo)
camera_toolbar.addAction(photo_action)
change_folder_action = QAction(
QIcon(os.path.join("images", "blue-folder-horizontal-open.png")),
"Change save location...",
self,
)
change_folder_action.setStatusTip("Change folder where photos are saved.")
change_folder_action.triggered.connect(self.change_folder)
camera_toolbar.addAction(change_folder_action)
camera_selector = QComboBox()
camera_selector.addItems([c.description() for c in self.available_cameras])
camera_selector.currentIndexChanged.connect(self.select_camera)
camera_toolbar.addWidget(camera_selector)
self.setWindowTitle("NSAViewer")
self.show()
def select_camera(self, i):
self.camera = QCamera(self.available_cameras[i])
self.camera.setViewfinder(self.viewfinder)
self.camera.setCaptureMode(QCamera.CaptureStillImage)
self.camera.error.connect(lambda: self.alert(self.camera.errorString()))
self.camera.start()
self.capture = QCameraImageCapture(self.camera)
self.capture.error.connect(lambda i, e, s: self.alert(s))
self.capture.imageCaptured.connect(
lambda d, i: self.status.showMessage("Image %04d captured" % self.save_seq)
)
self.current_camera_name = self.available_cameras[i].description()
self.save_seq = 0
def take_photo(self):
timestamp = time.strftime("%d-%b-%Y-%H_%M_%S")
self.capture.capture(
os.path.join(
self.save_path,
"%s-%04d-%s.jpg" % (self.current_camera_name, self.save_seq, timestamp),
)
)
self.save_seq += 1
def change_folder(self):
path = QFileDialog.getExistingDirectory(self, "Snapshot save location", "")
if path:
self.save_path = path
self.save_seq = 0
def alert(self, s):
"""
Handle errors coming from QCamera dn QCameraImageCapture by displaying alerts.
"""
err = QErrorMessage(self)
err.showMessage(s)
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setApplicationName("NSAViewer")
window = MainWindow()
app.exec()

View File

@@ -0,0 +1 @@
PyQt5>=5.6

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,72 @@
import os
import sys
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import (
QAction,
QApplication,
QColorDialog,
QMenu,
QSystemTrayIcon,
)
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
# Create the icon
icon = QIcon(os.path.join("images", "color.png"))
clipboard = QApplication.clipboard()
dialog = QColorDialog()
def copy_color_hex():
if dialog.exec():
color = dialog.currentColor()
clipboard.setText(color.name())
def copy_color_rgb():
if dialog.exec():
color = dialog.currentColor()
clipboard.setText(
"rgb(%d, %d, %d)" % (color.red(), color.green(), color.blue())
)
def copy_color_hsv():
if dialog.exec():
color = dialog.currentColor()
clipboard.setText(
"hsv(%d, %d, %d)" % (color.hue(), color.saturation(), color.value())
)
# Create the tray
tray = QSystemTrayIcon()
tray.setIcon(icon)
tray.setVisible(True)
# Create the menu
menu = QMenu()
action1 = QAction("Hex")
action1.triggered.connect(copy_color_hex)
menu.addAction(action1)
action2 = QAction("RGB")
action2.triggered.connect(copy_color_rgb)
menu.addAction(action2)
action3 = QAction("HSV")
action3.triggered.connect(copy_color_hsv)
menu.addAction(action3)
action4 = QAction("Exit")
action4.triggered.connect(app.quit)
menu.addAction(action4)
# Add the menu to the tray
tray.setContextMenu(menu)
app.exec()

View File

@@ -0,0 +1,35 @@
# Doughnut — An exchange rate tracker for people nuts about dough, in PyQt.
This is a simple currency exchange rate tracker implemented in PyQt, using the [fixer.io](http://fixer.io) API
for data. The default setup shows currency data for the preceding 180 days.
![Doughnut](screenshot-currency1.jpg)
Data is loaded progressively, with increasing resolution. Currency rates for a given date are shown in the right
hand panel and updated to follow the position of the mouse.
![Doughnut](screenshot-currency2.jpg)
> If you want to learn more about build GUI applications with Python,
take a look at my [PyQt5 tutorials](https://www.pythonguis.com)
which covers everything you need to know to start building your own applications with PyQt5.
## Code notes
### Data handling
The interface presents a tracking plot (using PyQtGraph) of rates over the past 180 days. Since we don't want to
spam a free service, requests to the API are rate-limited to 1-per-second, giving a full-data-load time of 180s (3 min).
To avoid waiting each time, we use `requests_cache` which uses a local sqlite database to store the result of recent
requests. The requests for data use a progressive 'search' approach: where there is a gap in the data, the middle
point is filled first, and it prefers to load the most recent timepoints first. This means the whole plot gradually
increases in resolution over time, rather than working backwards only.
### Conversions
By default the app retrieves EUR rates and shows conversions to this base currency. If you change base currency
it will retrieve all data again for that new currency. This is daft, since if we have rates vs. EUR we can calculate
any other currency->currency conversion via EUR (with a small loss of accuracy).

View File

@@ -0,0 +1,38 @@
# CryptoCompare.com API Key
CRYPTOCOMPARE_API_KEY = ""
# Base currency is used to retrieve rates from bitcoinaverage.
DEFAULT_BASE_CURRENCY = "USD"
AVAILABLE_BASE_CURRENCIES = ["USD", "EUR", "GBP"]
# The crypto currencies to retrieve data about.
AVAILABLE_CRYPTO_CURRENCIES = [
"BTC",
"ETH",
"LTC",
"EOS",
"XRP",
"BCH",
] #
DEFAULT_DISPLAY_CURRENCIES = ["BTC", "ETH", "LTC"]
# Number of historic timepoints to plot (days).
NUMBER_OF_TIMEPOINTS = 150
# Colour cycle to use for plotting currencies.
BREWER12PAIRED = [
"#a6cee3",
"#1f78b4",
"#b2df8a",
"#33a02c",
"#fb9a99",
"#e31a1c",
"#fdbf6f",
"#ff7f00",
"#cab2d6",
"#6a3d9a",
"#ffff99",
"#b15928",
]

305
pyqt6/demos/crypto/main.py Normal file
View File

@@ -0,0 +1,305 @@
import sys
from itertools import cycle
import constants
import numpy as np
# import requests_cache
from PyQt6.QtCore import (
Qt,
QThreadPool,
)
from PyQt6.QtGui import (
QBrush,
QColor,
QStandardItem,
QStandardItemModel,
)
from PyQt6.QtWidgets import (
QApplication,
QComboBox,
QHBoxLayout,
QMainWindow,
QMessageBox,
QProgressBar,
QTableView,
QToolBar,
QWidget,
)
from workers import UpdateWorker
color_cycle = cycle(constants.BREWER12PAIRED)
# requests_cache.install_cache('cache')
# Must be imported after PyQt6.
import pyqtgraph as pg
pg.setConfigOption("background", "w")
pg.setConfigOption("foreground", "k")
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
layout = QHBoxLayout()
self.ax = pg.PlotWidget()
self.ax.showGrid(True, True)
self.line = pg.InfiniteLine(
pos=-20,
pen=pg.mkPen("k", width=3),
movable=False, # We have our own code to handle dragless moving.
)
self.ax.addItem(self.line)
self.ax.setLabel("left", text="Rate")
self.p1 = self.ax.getPlotItem()
self.p1.scene().sigMouseMoved.connect(self.mouse_move_handler)
# Add the right-hand axis for the market activity.
self.p2 = pg.ViewBox()
self.p2.enableAutoRange(axis=pg.ViewBox.XYAxes, enable=True)
self.p1.showAxis("right")
self.p1.scene().addItem(self.p2)
self.p2.setXLink(self.p1)
self.ax2 = self.p1.getAxis("right")
self.ax2.linkToView(self.p2)
self.ax2.setGrid(False)
self.ax2.setLabel(text="Volume")
self._market_activity = pg.PlotCurveItem(
np.arange(constants.NUMBER_OF_TIMEPOINTS),
np.arange(constants.NUMBER_OF_TIMEPOINTS),
pen=pg.mkPen("k", style=Qt.PenStyle.DashLine, width=1),
)
self.p2.addItem(self._market_activity)
# Automatically rescale our twinned Y axis.
self.p1.vb.sigResized.connect(self.update_plot_scale)
self.base_currency = constants.DEFAULT_BASE_CURRENCY
# Store a reference to lines on the plot, and items in our
# data viewer we can update rather than redraw.
self._data_lines = dict()
self._data_items = dict()
self._data_colors = dict()
self._data_visible = constants.DEFAULT_DISPLAY_CURRENCIES
self.listView = QTableView()
self.model = QStandardItemModel()
self.model.setHorizontalHeaderLabels(["Currency", "Rate"])
self.model.itemChanged.connect(self.check_check_state)
self.listView.setModel(self.model)
self.threadpool = QThreadPool()
self.worker = False
layout.addWidget(self.ax)
layout.addWidget(self.listView)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
self.listView.setFixedSize(226, 400)
self.setFixedSize(650, 400)
toolbar = QToolBar("Main")
self.addToolBar(toolbar)
self.currencyList = QComboBox()
toolbar.addWidget(self.currencyList)
self.update_currency_list(constants.AVAILABLE_BASE_CURRENCIES)
self.currencyList.setCurrentText(self.base_currency)
self.currencyList.currentTextChanged.connect(self.change_base_currency)
self.progress = QProgressBar()
self.progress.setRange(0, 100)
toolbar.addWidget(self.progress)
self.refresh_historic_rates()
self.setWindowTitle("Goodforbitcoin")
self.show()
def update_currency_list(self, currencies):
for currency in currencies:
self.currencyList.addItem(currency)
self.currencyList.model().sort(0)
def check_check_state(self, i):
if not i.isCheckable(): # Skip data columns.
return
currency = i.text()
checked = i.checkState() == Qt.CheckState.Checked
if currency in self._data_visible:
if not checked:
self._data_visible.remove(currency)
self.redraw()
else:
if checked:
self._data_visible.append(currency)
self.redraw()
def get_currency_color(self, currency):
if currency not in self._data_colors:
self._data_colors[currency] = next(color_cycle)
return self._data_colors[currency]
def get_or_create_data_row(self, currency):
if currency not in self._data_items:
self._data_items[currency] = self.add_data_row(currency)
return self._data_items[currency]
def add_data_row(self, currency):
citem = QStandardItem()
citem.setText(currency)
citem.setForeground(QBrush(QColor(self.get_currency_color(currency))))
citem.setColumnCount(2)
citem.setCheckable(True)
if currency in constants.DEFAULT_DISPLAY_CURRENCIES:
citem.setCheckState(Qt.CheckState.Checked)
vitem = QStandardItem()
vitem.setTextAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
self.model.setColumnCount(2)
self.model.appendRow([citem, vitem])
self.model.sort(0)
return citem, vitem
def mouse_move_handler(self, pos):
pos = self.ax.getViewBox().mapSceneToView(pos)
self.line.setPos(pos.x())
self.update_data_viewer(int(pos.x()))
def update_data_viewer(self, i):
if i not in range(constants.NUMBER_OF_TIMEPOINTS):
return
for currency, data in self.data.items():
self.update_data_row(currency, data[i])
def update_data_row(self, currency, data):
citem, vitem = self.get_or_create_data_row(currency)
vitem.setText("%.4f" % data["close"])
def change_base_currency(self, currency):
self.base_currency = currency
self.refresh_historic_rates()
def refresh_historic_rates(self):
if self.worker:
# If we have a current worker, send a kill signal
self.worker.signals.cancel.emit()
# Prefill our data store with None ('no data')
self.data = {}
self.volume = []
self.worker = UpdateWorker(self.base_currency)
# Handle callbacks with data and trigger refresh.
self.worker.signals.data.connect(self.result_data_callback)
self.worker.signals.finished.connect(self.refresh_finished)
self.worker.signals.progress.connect(self.progress_callback)
self.worker.signals.error.connect(self.notify_error)
self.threadpool.start(self.worker)
def result_data_callback(self, rates, volume):
self.data = rates
self.volume = volume
self.redraw()
self.update_data_viewer(constants.NUMBER_OF_TIMEPOINTS - 1)
def progress_callback(self, progress):
self.progress.setValue(progress)
def refresh_finished(self):
self.worker = False
self.redraw()
def notify_error(self, error):
e, tb = error
msg = QMessageBox()
msg.setIcon(QMessageBox.Icon.Warning)
msg.setText(e.__class__.__name__)
msg.setInformativeText(str(e))
msg.setDetailedText(tb)
msg.exec()
def update_plot_scale(self):
self.p2.setGeometry(self.p1.vb.sceneBoundingRect())
def redraw(self):
y_min, y_max = sys.maxsize, 0
x = np.arange(constants.NUMBER_OF_TIMEPOINTS)
# Pre-process data into lists of x, y values.
for currency, data in self.data.items():
if data:
_, close, high, low = zip(
*[(v["time"], v["close"], v["high"], v["low"]) for v in data]
)
if currency in self._data_visible:
# This line should be visible, if it's not drawn draw it.
if currency not in self._data_lines:
self._data_lines[currency] = {}
self._data_lines[currency]["high"] = self.ax.plot(
x,
high, # Unpack a list of tuples into two lists, passed as individual args.
pen=pg.mkPen(
self.get_currency_color(currency),
width=2,
style=Qt.PenStyle.DotLine,
),
)
self._data_lines[currency]["low"] = self.ax.plot(
x,
low, # Unpack a list of tuples into two lists, passed as individual args.
pen=pg.mkPen(
self.get_currency_color(currency),
width=2,
style=Qt.PenStyle.DotLine,
),
)
self._data_lines[currency]["close"] = self.ax.plot(
x,
close, # Unpack a list of tuples into two lists, passed as individual args.
pen=pg.mkPen(
self.get_currency_color(currency),
width=3,
),
)
else:
self._data_lines[currency]["high"].setData(x, high)
self._data_lines[currency]["low"].setData(x, low)
self._data_lines[currency]["close"].setData(x, close)
y_min, y_max = min(y_min, *low), max(y_max, *high)
else:
# This line should not be visible, if it is delete it.
if currency in self._data_lines:
self._data_lines[currency]["high"].clear()
self._data_lines[currency]["low"].clear()
self._data_lines[currency]["close"].clear()
self.ax.setLimits(yMin=y_min * 0.9, yMax=y_max * 1.1, xMin=min(x), xMax=max(x))
self._market_activity.setData(x, self.volume)
self.p2.setYRange(0, max(self.volume))
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
app.exec()

View File

@@ -0,0 +1,4 @@
PyQt5>=5.6
requests>=2.0.0
requests-cache>=0.4.13
pyqtgraph>=0.10

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,91 @@
import traceback
import constants
import requests
# import requests_cache
from PyQt6.QtCore import (
QObject,
QRunnable,
pyqtSignal,
pyqtSlot,
)
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
finished = pyqtSignal()
error = pyqtSignal(tuple)
progress = pyqtSignal(int)
data = pyqtSignal(dict, list)
cancel = pyqtSignal()
class UpdateWorker(QRunnable):
"""
Worker thread for updating currency.
"""
signals = WorkerSignals()
def __init__(self, base_currency):
super().__init__()
self.is_interrupted = False
self.base_currency = base_currency
self.signals.cancel.connect(self.cancel)
@pyqtSlot()
def run(self):
auth_header = {"Apikey": constants.CRYPTOCOMPARE_API_KEY}
try:
rates = {}
for n, crypto in enumerate(constants.AVAILABLE_CRYPTO_CURRENCIES, 1):
url = "https://min-api.cryptocompare.com/data/histoday?fsym={fsym}&tsym={tsym}&limit={limit}"
r = requests.get(
url.format(
**{
"fsym": crypto,
"tsym": self.base_currency,
"limit": constants.NUMBER_OF_TIMEPOINTS - 1,
"extraParams": "www.pythonguis.com",
"format": "json",
}
),
headers=auth_header,
)
r.raise_for_status()
rates[crypto] = r.json().get("Data")
self.signals.progress.emit(int(100 * n / len(constants.AVAILABLE_CRYPTO_CURRENCIES)))
if self.is_interrupted:
# Stop without emitting finish signals.
return
url = "https://min-api.cryptocompare.com/data/exchange/histoday?tsym={tsym}&limit={limit}"
r = requests.get(
url.format(
**{
"tsym": self.base_currency,
"limit": constants.NUMBER_OF_TIMEPOINTS - 1,
"extraParams": "www.pythonguis.com",
"format": "json",
}
),
headers=auth_header,
)
r.raise_for_status()
volume = [d["volume"] for d in r.json().get("Data")]
except Exception as e:
self.signals.error.emit((e, traceback.format_exc()))
return
self.signals.data.emit(rates, volume)
self.signals.finished.emit()
def cancel(self):
self.is_interrupted = True

View File

@@ -0,0 +1,35 @@
# Doughnut — An exchange rate tracker for people nuts about dough, in PyQt.
This is a simple currency exchange rate tracker implemented in PyQt, using the [fixer.io](http://fixer.io) API
for data. The default setup shows currency data for the preceding 180 days.
![Doughnut](screenshot-currency1.jpg)
Data is loaded progressively, with increasing resolution. Currency rates for a given date are shown in the right
hand panel and updated to follow the position of the mouse.
![Doughnut](screenshot-currency2.jpg)
> If you want to learn more about build GUI applications with Python,
take a look at my [PyQt5 tutorials](https://www.pythonguis.com)
which covers everything you need to know to start building your own applications with PyQt5.
## Code notes
### Data handling
The interface presents a tracking plot (using PyQtGraph) of rates over the past 180 days. Since we don't want to
spam a free service, requests to the API are rate-limited to 1-per-second, giving a full-data-load time of 180s (3 min).
To avoid waiting each time, we use `requests_cache` which uses a local sqlite database to store the result of recent
requests. The requests for data use a progressive 'search' approach: where there is a gap in the data, the middle
point is filled first, and it prefers to load the most recent timepoints first. This means the whole plot gradually
increases in resolution over time, rather than working backwards only.
### Conversions
By default the app retrieves EUR rates and shows conversions to this base currency. If you change base currency
it will retrieve all data again for that new currency. This is daft, since if we have rates vs. EUR we can calculate
any other currency->currency conversion via EUR (with a small loss of accuracy).

View File

@@ -0,0 +1,31 @@
# Base currency is used to retrieve rates from fixer.io.
# If we change currency we re-request, though it would
# be possible to calculate any rates *through* the base.
DEFAULT_BASE_CURRENCY = "EUR"
DEFAULT_DISPLAY_CURRENCIES = [
"CAD",
"CYP",
"AUD",
"USD",
"EUR",
"GBP",
"NZD",
"SGD",
]
HISTORIC_DAYS_N = 180
# Colour sets.
BREWER12PAIRED = [
"#a6cee3",
"#1f78b4",
"#b2df8a",
"#33a02c",
"#fb9a99",
"#e31a1c",
"#fdbf6f",
"#ff7f00",
"#cab2d6",
"#6a3d9a",
"#ffff99",
"#b15928",
]

View File

@@ -0,0 +1,353 @@
import sys
import time
import traceback
from collections import defaultdict
from datetime import date, datetime, timedelta
from itertools import cycle
import constants
import requests
# import requests_cache
from PyQt6.QtCore import (
QObject,
QRunnable,
Qt,
QThreadPool,
pyqtSignal,
pyqtSlot,
)
from PyQt6.QtGui import (
QBrush,
QColor,
QStandardItem,
QStandardItemModel,
)
from PyQt6.QtWidgets import (
QApplication,
QComboBox,
QHBoxLayout,
QMainWindow,
QProgressBar,
QTableView,
QToolBar,
QWidget,
)
color_cycle = cycle(constants.BREWER12PAIRED)
# requests_cache.install_cache('cache')
# PyQtGraph must be imported after Qt.
import pyqtgraph as pg
pg.setConfigOption("background", "w")
pg.setConfigOption("foreground", "k")
# Build progressive request order, for filling up data
# Uses an depth-first search pattern, filling more recent data
# to a higher resolution more quickly with a
DATE_REQUEST_OFFSETS = [0]
current = [(0, constants.HISTORIC_DAYS_N)]
while current:
a, b = current.pop(0)
n = (a + b) // 2
DATE_REQUEST_OFFSETS.append(n)
if abs(a - n) > 1:
current.insert(0, (a, n))
if abs(b - n) > 1:
current.append((b, n))
class WorkerSignals(QObject):
"""
Defines the signals available from a running worker thread.
"""
finished = pyqtSignal()
error = pyqtSignal(tuple)
progress = pyqtSignal(int)
data = pyqtSignal(int, dict)
cancel = pyqtSignal()
class UpdateWorker(QRunnable):
"""
Worker thread for updating currency.
"""
signals = WorkerSignals()
is_interrupted = False
def __init__(self, base_currency):
super().__init__()
self.base_currency = base_currency
self.signals.cancel.connect(self.cancel)
@pyqtSlot()
def run(self):
try:
today = date.today()
total_requests = len(DATE_REQUEST_OFFSETS)
for n, offset in enumerate(DATE_REQUEST_OFFSETS, 1):
when = today - timedelta(days=offset)
url = "http://api.fixer.io/{}".format(when.isoformat())
r = requests.get(url, params={"base": self.base_currency})
r.raise_for_status()
data = r.json()
rates = data["rates"]
rates[self.base_currency] = 1.0
self.signals.data.emit(offset, rates)
self.signals.progress.emit(int(100 * n / total_requests))
if not r.from_cache:
time.sleep(1) # Don't be rude.
if self.is_interrupted:
break
except Exception as e:
print(e)
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
return
self.signals.finished.emit()
def cancel(self):
self.is_interrupted = True
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
layout = QHBoxLayout()
self.ax = pg.PlotWidget()
self.ax.showGrid(True, True)
self.line = pg.InfiniteLine(
pos=-20,
pen=pg.mkPen("k", width=3),
movable=False, # We have our own code to handle dragless moving.
)
self.ax.addItem(self.line)
self.ax.setLimits(xMin=-constants.HISTORIC_DAYS_N + 1, xMax=0)
self.ax.getPlotItem().scene().sigMouseMoved.connect(self.mouse_move_handler)
self.base_currency = constants.DEFAULT_BASE_CURRENCY
# Store a reference to lines on the plot, and items in our
# data viewer we can update rather than redraw.
self._data_lines = dict()
self._data_items = dict()
self._data_colors = dict()
self._data_visible = constants.DEFAULT_DISPLAY_CURRENCIES
self._last_updated = None
self.listView = QTableView()
self.model = QStandardItemModel()
self.model.setHorizontalHeaderLabels(["Currency", "Rate"])
self.model.itemChanged.connect(self.check_check_state)
self.listView.setModel(self.model)
self.threadpool = QThreadPool()
self.worker = False
layout.addWidget(self.ax)
layout.addWidget(self.listView)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
self.listView.setFixedSize(226, 400)
self.setFixedSize(650, 400)
toolbar = QToolBar("Main")
self.addToolBar(toolbar)
self.currencyList = QComboBox()
toolbar.addWidget(self.currencyList)
self.update_currency_list(constants.DEFAULT_DISPLAY_CURRENCIES)
self.currencyList.setCurrentText(self.base_currency)
self.currencyList.currentTextChanged.connect(self.change_base_currency)
self.progress = QProgressBar()
self.progress.setRange(0, 100)
toolbar.addWidget(self.progress)
self.refresh_historic_rates()
self.setWindowTitle("Doughnut")
self.show()
def update_currency_list(self, currencies):
for currency in currencies:
if self.currencyList.findText(currency) == -1:
self.currencyList.addItem(currency)
self.currencyList.model().sort(0)
def check_check_state(self, i):
if not i.isCheckable(): # Skip data columns.
return
currency = i.text()
checked = i.checkState() == Qt.CheckState.Checked
if currency in self._data_visible:
if not checked:
self._data_visible.remove(currency)
self.redraw()
else:
if checked:
self._data_visible.append(currency)
self.redraw()
def get_currency_color(self, currency):
if currency not in self._data_colors:
self._data_colors[currency] = next(color_cycle)
return self._data_colors[currency]
def add_data_row(self, currency):
citem = QStandardItem()
citem.setText(currency)
citem.setForeground(QBrush(QColor(self.get_currency_color(currency))))
citem.setColumnCount(2)
citem.setCheckable(True)
if currency in constants.DEFAULT_DISPLAY_CURRENCIES:
citem.setCheckState(Qt.CheckState.Checked)
vitem = QStandardItem()
vitem.setTextAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
self.model.setColumnCount(2)
self.model.appendRow([citem, vitem])
self.model.sort(0)
return citem, vitem
def get_or_create_data_row(self, currency):
if currency not in self._data_items:
self._data_items[currency] = self.add_data_row(currency)
return self._data_items[currency]
def mouse_move_handler(self, pos):
pos = self.ax.getViewBox().mapSceneToView(pos)
self.line.setPos(pos.x())
self.update_data_viewer(int(pos.x()))
def update_data_row(self, currency, value):
citem, vitem = self.get_or_create_data_row(currency)
vitem.setText("%.4f" % value)
def update_data_viewer(self, d):
try:
data = self.data[d]
except IndexError: # Skip update if out of bounds.
return
if not data: # Skip update if we have no data.
return
for k, v in data.items():
self.update_data_row(k, v)
def change_base_currency(self, currency):
self.base_currency = currency
self.refresh_historic_rates()
def refresh_historic_rates(self):
if self.worker:
# If we have a current worker, send a kill signal
self.worker.signals.cancel.emit()
# Prefill our data store with None ('no data')
self.data = [None] * constants.HISTORIC_DAYS_N
self.worker = UpdateWorker(self.base_currency)
# Handle callbacks with data and trigger refresh.
self.worker.signals.data.connect(self.result_data_callback)
self.worker.signals.finished.connect(self.refresh_finished)
self.worker.signals.progress.connect(self.progress_callback)
self.threadpool.start(self.worker)
def result_data_callback(self, n, rates):
self.data[n] = rates
# Refresh plot if we haven't for >1 second.
if (
self._last_updated is None
or self._last_updated < datetime.now() - timedelta(seconds=1)
):
self.redraw()
self._last_updated = datetime.now()
def progress_callback(self, progress):
self.progress.setValue(progress)
def refresh_finished(self):
self.worker = False
self.redraw()
# Ensure all currencies we know about are in the dropdown list now.
self.update_currency_list(self._data_items.keys())
def redraw(self):
"""
Process data from store and prefer to draw.
:return:
"""
today = date.today()
plotd = defaultdict(list)
x_ticks = []
tick_step_size = constants.HISTORIC_DAYS_N / 6
# Pre-process data into lists of x, y values
for n, data in enumerate(self.data):
if data:
for currency, v in data.items():
plotd[currency].append((-n, v))
when = today - timedelta(days=n)
if (n - tick_step_size // 2) % tick_step_size == 0:
x_ticks.append((-n, when.strftime("%d-%m")))
# Update the plot
keys = sorted(plotd.keys())
y_min, y_max = sys.maxsize, 0
for currency in keys:
x, y = zip(*plotd[currency])
if currency in self._data_visible:
y_min = min(y_min, *y)
y_max = max(y_max, *y)
else:
x, y = [], []
if currency in self._data_lines:
self._data_lines[currency].setData(x, y)
else:
self._data_lines[currency] = self.ax.plot(
x,
y, # Unpack a list of tuples into two lists, passed as individual args.
pen=pg.mkPen(self.get_currency_color(currency), width=2),
)
self.ax.setLimits(yMin=y_min * 0.9, yMax=y_max * 1.1)
self.ax.getAxis("bottom").setTicks([x_ticks, []])
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
app.exec()

View File

@@ -0,0 +1,4 @@
PyQt5>=5.6
requests>=2.0.0
requests_cache>=0.4.13
pyqtgraph>=0.10

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'mainwindow.ui'
#
# Created by: PyQt6 UI code generator 5.10
#
# WARNING! All changes made in this file will be lost!
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(484, 371)
self.centralWidget = QtWidgets.QWidget(MainWindow)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.centralWidget.sizePolicy().hasHeightForWidth())
self.centralWidget.setSizePolicy(sizePolicy)
self.centralWidget.setObjectName("centralWidget")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralWidget)
self.horizontalLayout.setContentsMargins(11, 11, 11, 11)
self.horizontalLayout.setSpacing(6)
self.horizontalLayout.setObjectName("horizontalLayout")
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setSpacing(6)
self.verticalLayout.setObjectName("verticalLayout")
self.playlistView = QtWidgets.QListView(self.centralWidget)
self.playlistView.setAcceptDrops(True)
self.playlistView.setProperty("showDropIndicator", True)
self.playlistView.setDragDropMode(QtWidgets.QAbstractItemView.DropOnly)
self.playlistView.setAlternatingRowColors(True)
self.playlistView.setUniformItemSizes(True)
self.playlistView.setObjectName("playlistView")
self.verticalLayout.addWidget(self.playlistView)
self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
self.horizontalLayout_4.setSpacing(6)
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
self.currentTimeLabel = QtWidgets.QLabel(self.centralWidget)
self.currentTimeLabel.setMinimumSize(QtCore.QSize(80, 0))
self.currentTimeLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignTrailing | QtCore.Qt.AlignmentFlag.AlignVCenter)
self.currentTimeLabel.setObjectName("currentTimeLabel")
self.horizontalLayout_4.addWidget(self.currentTimeLabel)
self.timeSlider = QtWidgets.QSlider(self.centralWidget)
self.timeSlider.setOrientation(QtCore.Qt.Orientation.Horizontal)
self.timeSlider.setObjectName("timeSlider")
self.horizontalLayout_4.addWidget(self.timeSlider)
self.totalTimeLabel = QtWidgets.QLabel(self.centralWidget)
self.totalTimeLabel.setMinimumSize(QtCore.QSize(80, 0))
self.totalTimeLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading | QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter)
self.totalTimeLabel.setObjectName("totalTimeLabel")
self.horizontalLayout_4.addWidget(self.totalTimeLabel)
self.verticalLayout.addLayout(self.horizontalLayout_4)
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
self.horizontalLayout_5.setSpacing(6)
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
self.previousButton = QtWidgets.QPushButton(self.centralWidget)
self.previousButton.setText("")
icon = QtGui.QIcon()
icon.addPixmap(
QtGui.QPixmap("images/control-skip-180.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.previousButton.setIcon(icon)
self.previousButton.setObjectName("previousButton")
self.horizontalLayout_5.addWidget(self.previousButton)
self.playButton = QtWidgets.QPushButton(self.centralWidget)
self.playButton.setText("")
icon1 = QtGui.QIcon()
icon1.addPixmap(
QtGui.QPixmap("images/control.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.playButton.setIcon(icon1)
self.playButton.setObjectName("playButton")
self.horizontalLayout_5.addWidget(self.playButton)
self.pauseButton = QtWidgets.QPushButton(self.centralWidget)
self.pauseButton.setText("")
icon2 = QtGui.QIcon()
icon2.addPixmap(
QtGui.QPixmap("images/control-pause.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.pauseButton.setIcon(icon2)
self.pauseButton.setObjectName("pauseButton")
self.horizontalLayout_5.addWidget(self.pauseButton)
self.stopButton = QtWidgets.QPushButton(self.centralWidget)
self.stopButton.setText("")
icon3 = QtGui.QIcon()
icon3.addPixmap(
QtGui.QPixmap("images/control-stop-square.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.stopButton.setIcon(icon3)
self.stopButton.setObjectName("stopButton")
self.horizontalLayout_5.addWidget(self.stopButton)
self.nextButton = QtWidgets.QPushButton(self.centralWidget)
self.nextButton.setText("")
icon4 = QtGui.QIcon()
icon4.addPixmap(
QtGui.QPixmap("images/control-skip.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.nextButton.setIcon(icon4)
self.nextButton.setObjectName("nextButton")
self.horizontalLayout_5.addWidget(self.nextButton)
self.viewButton = QtWidgets.QPushButton(self.centralWidget)
self.viewButton.setText("")
icon5 = QtGui.QIcon()
icon5.addPixmap(
QtGui.QPixmap("images/application-image.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.viewButton.setIcon(icon5)
self.viewButton.setCheckable(True)
self.viewButton.setObjectName("viewButton")
self.horizontalLayout_5.addWidget(self.viewButton)
spacerItem = QtWidgets.QSpacerItem(
40,
20,
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Minimum,
)
self.horizontalLayout_5.addItem(spacerItem)
self.label = QtWidgets.QLabel(self.centralWidget)
self.label.setText("")
self.label.setPixmap(QtGui.QPixmap("images/speaker-volume.png"))
self.label.setObjectName("label")
self.horizontalLayout_5.addWidget(self.label)
self.volumeSlider = QtWidgets.QSlider(self.centralWidget)
self.volumeSlider.setMaximum(100)
self.volumeSlider.setProperty("value", 100)
self.volumeSlider.setOrientation(QtCore.Qt.Orientation.Horizontal)
self.volumeSlider.setObjectName("volumeSlider")
self.horizontalLayout_5.addWidget(self.volumeSlider)
self.verticalLayout.addLayout(self.horizontalLayout_5)
self.horizontalLayout.addLayout(self.verticalLayout)
MainWindow.setCentralWidget(self.centralWidget)
self.menuBar = QtWidgets.QMenuBar(MainWindow)
self.menuBar.setGeometry(QtCore.QRect(0, 0, 484, 22))
self.menuBar.setObjectName("menuBar")
self.menuFIle = QtWidgets.QMenu(self.menuBar)
self.menuFIle.setObjectName("menuFIle")
MainWindow.setMenuBar(self.menuBar)
self.statusBar = QtWidgets.QStatusBar(MainWindow)
self.statusBar.setObjectName("statusBar")
MainWindow.setStatusBar(self.statusBar)
self.open_file_action = QtWidgets.QAction(MainWindow)
self.open_file_action.setObjectName("open_file_action")
self.menuFIle.addAction(self.open_file_action)
self.menuBar.addAction(self.menuFIle.menuAction())
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Failamp"))
self.currentTimeLabel.setText(_translate("MainWindow", "0:00"))
self.totalTimeLabel.setText(_translate("MainWindow", "0:00"))
self.menuFIle.setTitle(_translate("MainWindow", "FIle"))
self.open_file_action.setText(_translate("MainWindow", "Open file..."))

View File

@@ -0,0 +1,20 @@
# Failamp — Simple mediaplayer build in PyQt
Simple app to listen to and watch videos and audio files,
with built in playlist. Uses QtMultimedia and QtMultimediaWidgets
to handle playback and manage the playlist.
The main interface offers a playlist window in which you can drag-drop
media files to be played. Standard media controls are provided, along
with a timeline scrub widget and a volume control.
![Mediaplayer](screenshot-mediaplayer1.jpg)
For video playback you can pop out an external video viewer window
which floats on top.
![Mediaplayer](screenshot-mediaplayer2.jpg)
> If you want to learn more about build GUI applications with Python,
take a look at my [PyQt5 tutorials](https://www.pythonguis.com)
which covers everything you need to know to start building your own applications with PyQt5.

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

View File

@@ -0,0 +1,170 @@
import os
import sys
from MainWindow import Ui_MainWindow
from models import PlaylistModel
from PyQt6.QtCore import QSize, Qt, QUrl, pyqtSignal
from PyQt6.QtGui import QColor, QPalette
from PyQt6.QtMultimedia import (
QMediaContent,
QMediaPlayer,
QMediaPlaylist,
)
from PyQt6.QtMultimediaWidgets import QVideoWidget
from PyQt6.QtWidgets import QApplication, QFileDialog, QMainWindow
from utils import hhmmss
class ViewerWindow(QMainWindow):
state = pyqtSignal(bool)
def closeEvent(self, e):
# Emit the window state, to update the viewer toggle button.
self.state.emit(False)
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.player = QMediaPlayer()
self.player.error.connect(self.erroralert)
self.player.play()
# Setup the playlist.
self.playlist = QMediaPlaylist()
self.player.setPlaylist(self.playlist)
# Add viewer for video playback, separate floating window.
self.viewer = ViewerWindow(self)
self.viewer.setWindowFlags(
self.viewer.windowFlags() | Qt.WindowType.WindowStaysOnTopHint
)
self.viewer.setMinimumSize(QSize(480, 360))
videoWidget = QVideoWidget()
self.viewer.setCentralWidget(videoWidget)
self.player.setVideoOutput(videoWidget)
# Connect control buttons/slides for media player.
self.playButton.pressed.connect(self.player.play)
self.pauseButton.pressed.connect(self.player.pause)
self.stopButton.pressed.connect(self.player.stop)
self.volumeSlider.valueChanged.connect(self.player.setVolume)
self.viewButton.toggled.connect(self.toggle_viewer)
self.viewer.state.connect(self.viewButton.setChecked)
self.previousButton.pressed.connect(self.playlist.previous)
self.nextButton.pressed.connect(self.playlist.next)
self.model = PlaylistModel(self.playlist)
self.playlistView.setModel(self.model)
self.playlist.currentIndexChanged.connect(self.playlist_position_changed)
selection_model = self.playlistView.selectionModel()
selection_model.selectionChanged.connect(self.playlist_selection_changed)
self.player.durationChanged.connect(self.update_duration)
self.player.positionChanged.connect(self.update_position)
self.timeSlider.valueChanged.connect(self.player.setPosition)
self.open_file_action.triggered.connect(self.open_file)
self.setAcceptDrops(True)
self.show()
def dragEnterEvent(self, e):
if e.mimeData().hasUrls():
e.acceptProposedAction()
def dropEvent(self, e):
for url in e.mimeData().urls():
self.playlist.addMedia(QMediaContent(url))
self.model.layoutChanged.emit()
# If not playing, seeking to first of newly added + play.
if self.player.state() != QMediaPlayer.PlaybackState.PlayingState:
i = self.playlist.mediaCount() - len(e.mimeData().urls())
self.playlist.setCurrentIndex(i)
self.player.play()
def open_file(self):
path, _ = QFileDialog.getOpenFileName(
self,
"Open file",
"",
"mp3 Audio (*.mp3);;mp4 Video (*.mp4);;Movie files (*.mov);;All files (*.*)",
)
if path:
self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile(path)))
self.model.layoutChanged.emit()
def update_duration(self, duration):
self.timeSlider.setMaximum(duration)
if duration >= 0:
self.totalTimeLabel.setText(hhmmss(duration))
def update_position(self, position):
if position >= 0:
self.currentTimeLabel.setText(hhmmss(position))
# Disable the events to prevent updating triggering a setPosition event (can cause stuttering).
self.timeSlider.blockSignals(True)
self.timeSlider.setValue(position)
self.timeSlider.blockSignals(False)
def playlist_selection_changed(self, ix):
# We receive a QItemSelection from selectionChanged.
i = ix.indexes()[0].row()
self.playlist.setCurrentIndex(i)
def playlist_position_changed(self, i):
if i > -1:
ix = self.model.index(i)
self.playlistView.setCurrentIndex(ix)
def toggle_viewer(self, state):
if state:
self.viewer.show()
else:
self.viewer.hide()
def erroralert(self, *args):
print(args)
if __name__ == "__main__":
os.environ["QT_MULTIMEDIA_PREFERRED_PLUGINS"] = "windowsmediafoundation"
app = QApplication(sys.argv)
app.setApplicationName("Failamp")
app.setStyle("Fusion")
# Fusion dark palette from https://gist.github.com/QuantumCD/6245215.
palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor(53, 53, 53))
palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.Base, QColor(25, 25, 25))
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(53, 53, 53))
palette.setColor(QPalette.ColorRole.ToolTipBase, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.ToolTipText, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.Button, QColor(53, 53, 53))
palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)
palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218))
palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.black)
app.setPalette(palette)
app.setStyleSheet(
"QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }"
)
window = MainWindow()
app.exec()

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>484</width>
<height>371</height>
</rect>
</property>
<property name="windowTitle">
<string>Failamp</string>
</property>
<widget class="QWidget" name="centralWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusBar"/>
<action name="open_file_action">
<property name="text">
<string>Open file...</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,15 @@
from PyQt6.QtCore import QAbstractListModel, Qt
class PlaylistModel(QAbstractListModel):
def __init__(self, playlist):
super().__init__()
self.playlist = playlist
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
media = self.playlist.media(index.row())
return media.canonicalUrl().fileName()
def rowCount(self, index):
return self.playlist.mediaCount()

View File

@@ -0,0 +1 @@
PyQt5>=5.6

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1,8 @@
def hhmmss(ms):
# s = 1000
# m = 60000
# h = 360000
s = round(ms / 1000)
m, s = divmod(s, 60)
h, m = divmod(m, 60)
return ("%d:%02d:%02d" % (h, m, s)) if h else ("%d:%02d" % (m, s))

View File

@@ -0,0 +1,39 @@
# Moonsweeper — A minesweeper clone, on a moon with aliens, in PyQt.
Explore the mysterious moon of Q'tee without getting too close to the alien natives!
Moonsweeper is a single-player puzzle video game. The objective of the game is to
explore the area around your landed space rocket, without coming too close to the
deadly B'ug aliens. Your trusty tricounter will tell you the number of B'ugs in the
vicinity.
![Moonsweeper](screenshot-minesweeper1.jpg)
This a simple single-player exploration game modelled on _Minesweeper_
where you must reveal all the tiles without hitting hidden mines.
This implementation uses custom `QWidget` objects for the tiles, which
individually hold their state as mines, status and the
adjacent count of mines. In this version, the mines are replaced with
alien bugs (B'ug) but they could just as easily be anything else.
![Moonsweeper](screenshot-minesweeper2.jpg)
> If you want to learn more about build GUI applications with Python,
take a look at my [PyQt5 tutorials](https://www.pythonguis.com)
which covers everything you need to know to start building your own applications with PyQt5.
## Code notes
### Cheating the first turn
In many *Minesweeper* variants the initial turn is considered a free
go — if you hit a mine on the first click, it is moved somewhere else.
Here we cheat a little bit by taking the first go for the player, ensuring that
it is on a non-mine spot. This allows us not to worry about the bad first move
which would require us to recalculate the adjacencies.
We can explain this away as the "initial exploration around the rocket"
and make it sound completely sensible.
## Other licenses
Icons used in the application are by [Yusuke Kamiyaman](http://p.yusukekamiyamane.com/).

View File

@@ -0,0 +1,39 @@
from enum import IntEnum
from PyQt6.QtGui import (
QColor,
QImage,
)
IMG_BOMB = QImage("./images/bug.png")
IMG_FLAG = QImage("./images/flag.png")
IMG_START = QImage("./images/rocket.png")
IMG_CLOCK = QImage("./images/clock-select.png")
NUM_COLORS = {
1: QColor("#f44336"),
2: QColor("#9C27B0"),
3: QColor("#3F51B5"),
4: QColor("#03A9F4"),
5: QColor("#00BCD4"),
6: QColor("#4CAF50"),
7: QColor("#E91E63"),
8: QColor("#FF9800"),
}
LEVELS = [(8, 10), (16, 40), (24, 99)]
class Status(IntEnum):
READY = 0
PLAYING = 1
FAILED = 2
SUCCESS = 3
STATUS_ICONS = {
Status.READY: "./images/plus.png",
Status.PLAYING: "./images/smiley.png",
Status.FAILED: "./images/cross.png",
Status.SUCCESS: "./images/smiley-lol.png",
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 B

Some files were not shown because too many files have changed in this diff Show More