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

1
.gitignore vendored
View File

@ -71,3 +71,4 @@ dist
# Misc
*.autosave
.vscode

View File

@ -1,62 +1,92 @@
# 15 Minute Apps
# Python GUIs Examples
A collection of 15 small — *minute* — desktop applications written in Python
using the PyQt framework. These apps are intended as examples from
which you can poke, hack and prod your way to writing your own tools.
**This repository contains 100s of GUI examples written in Python**. From complete working applications to reusable widgets snippets, these examples can
be _freely_ re-used, re-mixed and tweaked to build your own Python GUI applications.
> Many of these apps have more detailed write-ups on my PyQt5/PySide2 site at [LearnPyQt.com](https://www.learnpyqt.com/apps/).
If you're new to creating GUI apps check out the introductory [pyqt5 tutorial](https://www.learnpyqt.com/courses/start/).
Examples are available for
[PyQt6](https://github.com/pythonguis/15-minute-apps/tree/master/pyqt6),
[PySide6](https://github.com/pythonguis/15-minute-apps/tree/master/pyside6),
[PySide2](https://github.com/pythonguis/15-minute-apps/tree/master/pyside2)
and [PyQt5](https://github.com/pythonguis/15-minute-apps/tree/master/pyqt5)
## The apps
> Many of these examples have more detailed write-ups on the [Python GUIs website](https://www.pythonguis.com/). If you're new to creating GUI apps check out the introductory [PyQt6 tutorial](https://www.pythonguis.com/pyqt6-tutorial/) or [PySide6 tutorial](https://www.pythonguis.com/pyside6-tutorial/).
## The demo apps
The apps showcase various parts of the Qt framework, including advanced widgets,
multimedia, graphics views and decorationless windows. However, the most
generally interesting/feature complete applications are Minesweeper, Solitaire
and Paint.
1. [Web Browser (untabbed)](browser/) - "MooseAche"
1. [Web Browser (tabbed)](browser_tabbed/) - "Mozzarella Ashbadger"
1. **[Minesweeper](minesweeper/) - "Moonsweeper"**
1. [Notepad](notepad/) - "No2Pads"
1. [Calculator](calculator/) - "Calculon" (QtDesigner)
1. [Word Processor](wordprocessor/) - "Megasolid Idiom"
1. [Webcam/Snapshot](camera/) - "NSAViewer"
1. [Media Player](mediaplayer/) - "Failamp"
1. [Post-it Notes](notes/) - "Brown Note" (QtDesigner)
1. **[Paint](paint/) - "Piecasso" (QtDesigner)**
1. [Unzip](unzip/) - "7Pez" (QtDesigner)
1. [Translator](translate/) - "Translataarrr" (QtDesigner)
1. [Weather](weather/) - "Raindar" (QtDesigner)
1. [Currency converter](currency/) - "Doughnut" (PyQtGraph)
1. **[Solitaire](solitaire/) - "Ronery" (QGraphicsScene)**
1. Web Browser (untabbed) - "MooseAche"
1. Web Browser (tabbed) - "Mozzarella Ashbadger"
1. Minesweeper - "Moonsweeper"
1. Notepad - "No2Pads"
1. Calculator - "Calculon" (QtDesigner)
1. Word Processor - "Megasolid Idiom"
1. Webcam/Snapshot - "NSAViewer"
1. Media Player - "Failamp"
1. Post-it Notes - "Brown Note" (QtDesigner)
1. Paint - "Piecasso" (QtDesigner)
1. Unzip - "7Pez" (QtDesigner)
1. Translator - "Translataarrr" (QtDesigner)
1. Weather - "Raindar" (QtDesigner)
1. Currency converter - "Doughnut" (PyQtGraph)
1. Solitaire - "Ronery" (QGraphicsScene)
## The widgets
![Graphical Equalizer](https://i.imgur.com/0F2ZgqE.gif)
**Graphical Equalizer** Visualize audio frequency changes with configurable styles and decay.
![Power Meter](https://i.imgur.com/0dpZIMV.gif)
**Power Bar** Rotary control with amplitude display.
![Palette](https://cdn.learnpyqt.com/media/images/Screenshot_2019-06-15_at_15.18.14.max-500x500.png)
**Palette** Select colors from a configurable linear or grid palette.
![Gradient Editor](https://cdn.learnpyqt.com/media/images/Screenshot_2019-06-15_at_18.32.52.max-500x500.png)
**Linear Gradient Editor** Design custom linear gradients with multiple stops and colors.
**Color Button** Simple button that displays and selects colors.
**Paint** Draw pictures with a custom bitmap canvas, with color and pen control.
**Password Edit** A password line editor with toggleable visibility action.
![Toggle Widget](https://i.imgur.com/rHrkkG3.gif)
Replace checkboxes with this handy toggle widget, with custom colors and optional animations.
## The tutorials
As well as the complete apps & re-usable widgets we've got selection of code snippets taken from tutorials on the [Python GUIs](https://www.pythonguis.com) website. These guide you through building GUIs with PyQt, PySide and other libraries.
## Getting started
To use each app you first need to install the requirements. In most cases
the only requirements are PyQt5, and occasionally requests. To install
app-specific requirements change to the folder of the app and run:
the only requirements are the GUI library and occasionally requests. To install
example specific requirements change to the folder of the example and run:
pip3 install -r requirements.txt
Once the requirements are installed, you can run the app using Python 3.
python3 <filename>.py
Once the requirements are installed, you can run the app. Every example in this repo comes with a main Python file called `main.py` to keep things simple.
python3 main.py
The application window should appear.
## Want to build your own apps?
> If you think these apps are neat and want to learn more about
PyQt in general, take a look at my [PyQt5 tutorial](https://www.learnpyqt.com)
which covers everything you need to know to start building your own applications with PyQt.
take a look at my [PyQt6 tutorial](https://www.pythonguis.com/pyqt6-tutorial)
which covers everything you need to know to start building your own applications with Python.
You can also find write-ups about these "minute apps" [on the same site](http://www.learnpyqt.com/apps).
You can also find more write-ups and tips [on the same site](http://www.pythonguis.com/).
## License
All code is **licensed under an MIT license**. This allows you to re-use the code freely,
remixed in both commercial and non-commercial projects. The only requirement is to
include the same license when distributing.
All code is **licensed under an MIT license**. This allows you to re-use the code freely, remixed in both commercial and non-commercial projects. The only requirement is to include the same license when distributing.
## Other licenses

View File

@ -1,47 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['crypto.py'],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='Goodforbitcoin',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
icon='resources/icon.ico'
)
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='Goodforbitcoin'
)
app = BUNDLE(coll,
name='Goodforbitcoin.app',
icon='resources/icon.icns',
bundle_identifier='com.learnpyqt.Goodforbitcoin',
info_plist={
'NSHighResolutionCapable': 'True'
},
)

View File

@ -1,227 +0,0 @@
<?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="QListView" name="playlistView">
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="showDropIndicator" stdset="0">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DropOnly</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::CopyAction</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="uniformItemSizes">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="currentTimeLabel">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>0:00</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="timeSlider">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="totalTimeLabel">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>0:00</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QPushButton" name="previousButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>images/control-skip-180.png</normaloff>images/control-skip-180.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="playButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>images/control.png</normaloff>images/control.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pauseButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>images/control-pause.png</normaloff>images/control-pause.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stopButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>images/control-stop-square.png</normaloff>images/control-stop-square.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="nextButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>images/control-skip.png</normaloff>images/control-skip.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="viewButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>images/application-image.png</normaloff>images/application-image.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>images/speaker-volume.png</pixmap>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="volumeSlider">
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</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="open_file_action"/>
</widget>
<addaction name="menuFIle"/>
</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

@ -1,10 +0,0 @@
# No2Pads — A Notepad clone in PyQt
A very simple notepad clone using the QTextEdit widget to handle more or less
everything. Supports file loading, saving and printing.
![No2Pads](screenshot-notepad.jpg)
> If you think this app is neat and want to learn more about
PyQt in general, take a look at my [free PyQt tutorials](https://www.learnpyqt.com)
which cover everything you need to know to start building your own applications with PyQt.

File diff suppressed because it is too large Load Diff

11
pyqt5-pyqt6.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
# Black the SOURCE files to limit subsequent reformatting.
black -l 128 $1
# PyQt6 (built from PyQt5) --------------------------------------------------
find $1 -type f -name "*.py" -print0 | xargs -0 sed -i 's/PyQt5/PyQt6/g'
find $1 -type f -name "*.py" -print0 | xargs -0 sed -i 's/exec_/exec/g'
find $1 -type f -name "*.py" -print0 | xargs -0 sed -i 's/qt-5/qt-6/g'
black -l 128 $1

12
pyqt5-pyside2.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
# Black the SOURCE files to limit subsequent reformatting (highlights possible issues).
black -l 128 $1
# PySide2 (built from PyQt5) --------------------------------------------------
find $1 -type f -name "*.py" -print0 | xargs -0 sed -i 's/PyQt5/PySide2/g'
find $1 -type f -name "*.py" -print0 | xargs -0 sed -i 's/pyqtSignal/Signal/g'
find $1 -type f -name "*.py" -print0 | xargs -0 sed -i 's/pyqtProperty/Property/g'
find $1 -type f -name "*.py" -print0 | xargs -0 sed -i 's/pyqtSlot/Slot/g'
black -l 128 $1

View File

@ -1,14 +1,14 @@
# 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
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 think this app is neat and want to learn more about
PyQt in general, take a look at my [free PyQt tutorials](https://www.learnpyqt.com)
which cover everything you need to know to start building your own applications with PyQt.
> 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

View File

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 615 B

View File

Before

Width:  |  Height:  |  Size: 589 B

After

Width:  |  Height:  |  Size: 589 B

View File

Before

Width:  |  Height:  |  Size: 829 B

After

Width:  |  Height:  |  Size: 829 B

View File

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 729 B

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 621 B

After

Width:  |  Height:  |  Size: 621 B

View File

Before

Width:  |  Height:  |  Size: 677 B

After

Width:  |  Height:  |  Size: 677 B

View File

Before

Width:  |  Height:  |  Size: 752 B

After

Width:  |  Height:  |  Size: 752 B

View File

Before

Width:  |  Height:  |  Size: 902 B

After

Width:  |  Height:  |  Size: 902 B

View File

Before

Width:  |  Height:  |  Size: 694 B

After

Width:  |  Height:  |  Size: 694 B

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 715 B

View File

Before

Width:  |  Height:  |  Size: 766 B

After

Width:  |  Height:  |  Size: 766 B

View File

Before

Width:  |  Height:  |  Size: 489 B

After

Width:  |  Height:  |  Size: 489 B

View File

@ -1,19 +1,31 @@
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtPrintSupport import *
from PyQt5.QtWidgets import QShortcut, QLabel, QHBoxLayout
import os
import sys
from PyQt5.QtCore import QSize, Qt, QUrl
from PyQt5.QtGui import QIcon, QKeySequence, QPixmap
from PyQt5.QtPrintSupport import QPrintPreviewDialog
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWidgets import (
QAction,
QApplication,
QDialog,
QDialogButtonBox,
QFileDialog,
QLabel,
QLineEdit,
QMainWindow,
QShortcut,
QStatusBar,
QToolBar,
QVBoxLayout,
)
class AboutDialog(QDialog):
def __init__(self, *args, **kwargs):
super(AboutDialog, self).__init__(*args, **kwargs)
def __init__(self):
super().__init__()
QBtn = QDialogButtonBox.Ok # No cancel
QBtn = QDialogButtonBox.StandardButton.Ok # No cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
@ -28,14 +40,14 @@ class AboutDialog(QDialog):
layout.addWidget(title)
logo = QLabel()
logo.setPixmap(QPixmap(os.path.join('images', 'ma-icon-128.png')))
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.AlignHCenter)
layout.itemAt(i).setAlignment(Qt.AlignmentFlag.AlignHCenter)
layout.addWidget(self.buttonBox)
@ -43,8 +55,8 @@ class AboutDialog(QDialog):
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
def __init__(self):
super().__init__()
self.browser = QWebEngineView()
self.browser.setUrl(QUrl("http://google.com"))
@ -59,26 +71,34 @@ class MainWindow(QMainWindow):
navtb = QToolBar("Navigation")
navtb.setIconSize(QSize(16, 16))
self.addToolBar(navtb)
self.shortcut_open = QShortcut(QKeySequence('F5'), self)
self.shortcut_open.triggered.connect(self.browser.reload)
back_btn = QAction(QIcon(os.path.join('images', 'arrow-180.png')), "Back", self)
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 = 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 = 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 = 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)
@ -86,14 +106,18 @@ class MainWindow(QMainWindow):
navtb.addSeparator()
self.httpsicon = QLabel() # Yes, really!
self.httpsicon.setPixmap(QPixmap(os.path.join('images', 'lock-nossl.png')))
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 = 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)
@ -103,74 +127,99 @@ class MainWindow(QMainWindow):
file_menu = self.menuBar().addMenu("&File")
open_file_action = QAction(QIcon(os.path.join('images', 'disk--arrow.png')), "Open file...", self)
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 = 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 = 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 = 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 = 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')))
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.udemy.com/522076"))
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 (*.*)")
filename, _ = QFileDialog.getOpenFileName(
self,
"Open file",
"",
"Hypertext Markup Language (*.htm *.html);;" "All files (*.*)",
)
if filename:
with open(filename, 'r') as f:
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:
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 (*.*)")
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_)
@ -187,24 +236,24 @@ class MainWindow(QMainWindow):
self.browser.setUrl(q)
def update_urlbar(self, q):
if q.scheme() == 'https':
if q.scheme() == "https":
# Secure padlock icon
self.httpsicon.setPixmap(QPixmap(os.path.join('images', 'lock-ssl.png')))
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.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-nossl.png")))
self.urlbar.setText(q.toString())
self.urlbar.setCursorPosition(0)
app = QApplication(sys.argv)
app.setApplicationName("MooseAche")
app.setOrganizationName("MooseAche")
app.setOrganizationDomain("MooseAche.org")
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setApplicationName("MooseAche")
app.setOrganizationName("MooseAche")
app.setOrganizationDomain("MooseAche.org")
window = MainWindow()
window = MainWindow()
app.exec_()
app.exec_()

View File

@ -0,0 +1,277 @@
import os
import sys
from PyQt5.QtCore import QSize, Qt, QUrl
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtPrintSupport import QPrintPreviewDialog
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineView
from PyQt5.QtWidgets import (
QAction,
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 PyQt5.QtCore import QSize, Qt, QUrl
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtPrintSupport import QPrintPreviewDialog
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineView
from PyQt5.QtWidgets import (
QAction,
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

@ -1,3 +1,2 @@
PyQt5>=5.6
PyQtWebEngine
sip

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -1,15 +1,15 @@
# 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
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 think this app is neat and want to learn more about
PyQt in general, take a look at my [free PyQt tutorials](https://www.learnpyqt.com)
which cover everything you need to know to start building your own applications with PyQt.
> 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

View File

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 615 B

View File

Before

Width:  |  Height:  |  Size: 589 B

After

Width:  |  Height:  |  Size: 589 B

View File

Before

Width:  |  Height:  |  Size: 829 B

After

Width:  |  Height:  |  Size: 829 B

View File

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 729 B

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 621 B

After

Width:  |  Height:  |  Size: 621 B

View File

Before

Width:  |  Height:  |  Size: 677 B

After

Width:  |  Height:  |  Size: 677 B

View File

Before

Width:  |  Height:  |  Size: 752 B

After

Width:  |  Height:  |  Size: 752 B

View File

Before

Width:  |  Height:  |  Size: 902 B

After

Width:  |  Height:  |  Size: 902 B

View File

Before

Width:  |  Height:  |  Size: 694 B

After

Width:  |  Height:  |  Size: 694 B

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 715 B

View File

Before

Width:  |  Height:  |  Size: 766 B

After

Width:  |  Height:  |  Size: 766 B

View File

Before

Width:  |  Height:  |  Size: 489 B

After

Width:  |  Height:  |  Size: 489 B

View File

@ -1,19 +1,32 @@
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtPrintSupport import *
from PyQt5.QtWidgets import QShortcut, QLabel, QHBoxLayout
import os
import sys
from PyQt5.QtCore import QSize, Qt, QUrl
from PyQt5.QtGui import QIcon, QKeySequence, QPixmap
from PyQt5.QtPrintSupport import QPrintPreviewDialog
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWidgets import (
QAction,
QApplication,
QDialog,
QDialogButtonBox,
QFileDialog,
QLabel,
QLineEdit,
QMainWindow,
QShortcut,
QStatusBar,
QTabWidget,
QToolBar,
QVBoxLayout,
)
class AboutDialog(QDialog):
def __init__(self, *args, **kwargs):
super(AboutDialog, self).__init__(*args, **kwargs)
def __init__(self):
super().__init__()
QBtn = QDialogButtonBox.Ok # No cancel
QBtn = QDialogButtonBox.StandardButton.Ok # No cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
@ -28,14 +41,14 @@ class AboutDialog(QDialog):
layout.addWidget(title)
logo = QLabel()
logo.setPixmap(QPixmap(os.path.join('images', 'ma-icon-128.png')))
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.AlignHCenter)
layout.itemAt(i).setAlignment(Qt.AlignmentFlag.AlignHCenter)
layout.addWidget(self.buttonBox)
@ -43,8 +56,8 @@ class AboutDialog(QDialog):
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
def __init__(self):
super().__init__()
self.tabs = QTabWidget()
self.tabs.setDocumentMode(True)
@ -61,28 +74,36 @@ class MainWindow(QMainWindow):
navtb = QToolBar("Navigation")
navtb.setIconSize(QSize(16, 16))
self.addToolBar(navtb)
'''Shortcuts'''
self.shortcut_open = QShortcut(QKeySequence('F5'), self)
self.shortcut_open.triggered.connect(lambda: self.tabs.currentWidget().reload())
back_btn = QAction(QIcon(os.path.join('images', 'arrow-180.png')), "Back", self)
"""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 = 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 = 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 = 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)
@ -90,14 +111,18 @@ class MainWindow(QMainWindow):
navtb.addSeparator()
self.httpsicon = QLabel() # Yes, really!
self.httpsicon.setPixmap(QPixmap(os.path.join('images', 'lock-nossl.png')))
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 = 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)
@ -107,50 +132,73 @@ class MainWindow(QMainWindow):
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 = 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 = 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 = 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 = 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 = 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 = 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.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')))
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('')
qurl = QUrl("")
browser = QWebEngineView()
browser.setUrl(qurl)
@ -160,14 +208,24 @@ class MainWindow(QMainWindow):
# 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.setTabText(i, browser.page().title()))
browser.titleChanged.connect(lambda _, i=i, browser=browser:
self.setTabToolTip(i, browser.page().title()))
browser.loadFinished.connect(lambda _, i=i, browser=browser:
self.tabs.setTabText(i, browser.page().title()))
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
@ -193,33 +251,39 @@ class MainWindow(QMainWindow):
self.setWindowTitle("%s - Mozarella Ashbadger" % title)
def navigate_mozarella(self):
self.tabs.currentWidget().setUrl(QUrl("https://www.udemy.com/522076"))
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 (*.*)")
filename, _ = QFileDialog.getOpenFileName(
self,
"Open file",
"",
"Hypertext Markup Language (*.htm *.html);;" "All files (*.*)",
)
if filename:
with open(filename, 'r') as f:
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 (*.*)")
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'))
with open(filename, "w") as f:
f.write(html.encode("utf8"))
def print_page(self):
dlg = QPrintPreviewDialog()
@ -237,28 +301,28 @@ class MainWindow(QMainWindow):
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':
if q.scheme() == "https":
# Secure padlock icon
self.httpsicon.setPixmap(QPixmap(os.path.join('images', 'lock-ssl.png')))
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.httpsicon.setPixmap(QPixmap(os.path.join("images", "lock-nossl.png")))
self.urlbar.setText(q.toString())
self.urlbar.setCursorPosition(0)
app = QApplication(sys.argv)
app.setApplicationName("Mozarella Ashbadger")
app.setOrganizationName("Mozarella")
app.setOrganizationDomain("mozarella.org")
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setApplicationName("Mozarella Ashbadger")
app.setOrganizationName("Mozarella")
app.setOrganizationDomain("mozarella.org")
window = MainWindow()
window = MainWindow()
app.exec_()
app.exec_()

View File

@ -1,3 +1,2 @@
PyQt5>=5.6
PyQtWebEngine
sip

View File

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

@ -1,318 +1,288 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'mainwindow.ui'
#
# Created by: PyQt5 UI code generator 5.10
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(484, 433)
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.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralWidget)
self.verticalLayout_2.setContentsMargins(11, 11, 11, 11)
self.verticalLayout_2.setSpacing(6)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setSpacing(6)
self.verticalLayout.setObjectName("verticalLayout")
self.lcdNumber = QtWidgets.QLCDNumber(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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)
self.verticalLayout_2.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.actionExit = QtWidgets.QAction(MainWindow)
self.actionExit.setObjectName("actionExit")
self.actionReset = QtWidgets.QAction(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"))
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'mainwindow.ui'
#
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(484, 433)
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.verticalLayout = QtWidgets.QVBoxLayout(self.centralWidget)
self.verticalLayout.setContentsMargins(11, 11, 11, 11)
self.verticalLayout.setSpacing(6)
self.verticalLayout.setObjectName("verticalLayout")
self.lcdNumber = QtWidgets.QLCDNumber(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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.actionExit = QtWidgets.QAction(MainWindow)
self.actionExit.setObjectName("actionExit")
self.actionReset = QtWidgets.QAction(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

@ -1,11 +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
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
PyQt in general, [take a look at my ebook & online course
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 PyQt.
which covers everything you need to know to start building your own applications with PyQt5.

View File

@ -1,10 +1,8 @@
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import operator
import sys
from MainWindow import Ui_MainWindow
from PyQt5.QtWidgets import QApplication, QMainWindow
# Calculator state.
READY = 0
@ -12,13 +10,13 @@ INPUT = 1
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
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))
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))
@ -96,7 +94,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
try:
self.stack = [self.current_op(*self.stack)]
except Exception:
self.lcdNumber.display('Err')
self.lcdNumber.display("Err")
self.stack = [0]
else:
self.current_op = None
@ -104,8 +102,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.display()
if __name__ == '__main__':
app = QApplication([])
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setApplicationName("Calculon")
window = MainWindow()

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

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -7,9 +7,9 @@ them.
![Camera](screenshot-camera.jpg)
> If you think this app is neat and want to learn more about
PyQt in general, take a look at my [free PyQt tutorials](https://www.learnpyqt.com)
which cover everything you need to know to start building your own applications with PyQt.
> 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

View File

Before

Width:  |  Height:  |  Size: 504 B

After

Width:  |  Height:  |  Size: 504 B

View File

Before

Width:  |  Height:  |  Size: 571 B

After

Width:  |  Height:  |  Size: 571 B

View File

@ -1,28 +1,34 @@
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtPrintSupport import *
from PyQt5.QtMultimedia import *
from PyQt5.QtMultimediaWidgets import *
import os
import sys
import time
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtMultimedia import QCamera, QCameraImageCapture, QCameraInfo
from PyQt5.QtMultimediaWidgets import QCameraViewfinder
from PyQt5.QtWidgets import (
QAction,
QApplication,
QComboBox,
QErrorMessage,
QFileDialog,
QMainWindow,
QStatusBar,
QToolBar,
)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
def __init__(self):
super().__init__()
self.available_cameras = QCameraInfo.availableCameras()
if not self.available_cameras:
pass #quit
pass # quit
self.status = QStatusBar()
self.setStatusBar(self.status)
self.save_path = ""
self.viewfinder = QCameraViewfinder()
@ -37,24 +43,30 @@ class MainWindow(QMainWindow):
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 = 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 = 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_selector.currentIndexChanged.connect(self.select_camera)
camera_toolbar.addWidget(camera_selector)
self.setWindowTitle("NSAViewer")
self.show()
@ -74,11 +86,12 @@ class MainWindow(QMainWindow):
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.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):
@ -95,10 +108,9 @@ class MainWindow(QMainWindow):
err.showMessage(s)
if __name__ == '__main__':
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setApplicationName("NSAViewer")
window = MainWindow()
app.exec_()
app.exec_()

View File

@ -1,2 +1 @@
PyQt5>=5.6
sip

View File

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,13 +1,20 @@
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import os
import sys
app = QApplication([])
from PyQt5.QtGui import QIcon
from PyQt5.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"))
icon = QIcon(os.path.join("images", "color.png"))
clipboard = QApplication.clipboard()
dialog = QColorDialog()
@ -17,20 +24,23 @@ 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()
))
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()
))
clipboard.setText(
"hsv(%d, %d, %d)" % (color.hue(), color.saturation(), color.value())
)
# Create the tray
tray = QSystemTrayIcon()
@ -47,13 +57,16 @@ 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_()
app.exec_()

View File

@ -10,19 +10,19 @@ hand panel and updated to follow the position of the mouse.
![Doughnut](screenshot-currency2.jpg)
> If you think this app is neat and want to learn more about
PyQt in general, take a look at my [free PyQt tutorials](https://www.learnpyqt.com)
which cover everything you need to know to start building your own applications with PyQt.
> 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
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
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.

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",
]

View File

@ -1,124 +1,46 @@
from datetime import datetime, timedelta, date
from itertools import cycle
import os
import sys
import traceback
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from itertools import cycle
import constants
import numpy as np
# import requests_cache
from PyQt5.QtCore import (
Qt,
QThreadPool,
)
from PyQt5.QtGui import (
QBrush,
QColor,
QStandardItem,
QStandardItemModel,
)
from PyQt5.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 PyQt5.
import pyqtgraph as pg
import requests
import requests_cache
# CryptoCompare.com API Key
CRYPTOCOMPARE_API_KEY = ''
# Define a requests http cache to minimise API requests.
requests_cache.install_cache(os.path.expanduser('~/.goodforbitcoin'))
# 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 = cycle(['#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00',
'#cab2d6', '#6a3d9a', '#ffff99', '#b15928' ])
# Base PyQtGraph configuration.
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
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(UpdateWorker, self).__init__()
self.is_interrupted = False
self.base_currency = base_currency
self.signals.cancel.connect(self.cancel)
@pyqtSlot()
def run(self):
auth_header = {
'Apikey': CRYPTOCOMPARE_API_KEY
}
try:
rates = {}
for n, crypto in enumerate(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': NUMBER_OF_TIMEPOINTS-1,
'extraParams': 'www.learnpyqt.com',
'format': 'json',
}),
headers=auth_header,
)
r.raise_for_status()
rates[crypto] = r.json().get('Data')
self.signals.progress.emit(int(100 * n / len(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': NUMBER_OF_TIMEPOINTS-1,
'extraParams': 'www.learnpyqt.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
pg.setConfigOption("background", "w")
pg.setConfigOption("foreground", "k")
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
def __init__(self):
super().__init__()
layout = QHBoxLayout()
@ -127,43 +49,44 @@ class MainWindow(QMainWindow):
self.line = pg.InfiniteLine(
pos=-20,
pen=pg.mkPen('k', width=3),
movable=False # We have our own code to handle dragless moving.
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.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.showAxis("right")
self.p1.scene().addItem(self.p2)
self.p2.setXLink(self.p1)
self.ax2 = self.p1.getAxis('right')
self.ax2 = self.p1.getAxis("right")
self.ax2.linkToView(self.p2)
self.ax2.setGrid(False)
self.ax2.setLabel(text='Volume')
self.ax2.setLabel(text="Volume")
self._market_activity = pg.PlotCurveItem(
np.arange(NUMBER_OF_TIMEPOINTS), np.arange(NUMBER_OF_TIMEPOINTS),
pen=pg.mkPen('k', style=Qt.DashLine, width=1)
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 = DEFAULT_BASE_CURRENCY
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 = DEFAULT_DISPLAY_CURRENCIES
self._data_visible = constants.DEFAULT_DISPLAY_CURRENCIES
self.listView = QTableView()
self.model = QStandardItemModel()
@ -189,7 +112,7 @@ class MainWindow(QMainWindow):
self.currencyList = QComboBox()
toolbar.addWidget(self.currencyList)
self.update_currency_list(AVAILABLE_BASE_CURRENCIES)
self.update_currency_list(constants.AVAILABLE_BASE_CURRENCIES)
self.currencyList.setCurrentText(self.base_currency)
self.currencyList.currentTextChanged.connect(self.change_base_currency)
@ -212,7 +135,7 @@ class MainWindow(QMainWindow):
return
currency = i.text()
checked = i.checkState() == Qt.Checked
checked = i.checkState() == Qt.CheckState.Checked
if currency in self._data_visible:
if not checked:
@ -225,7 +148,7 @@ class MainWindow(QMainWindow):
def get_currency_color(self, currency):
if currency not in self._data_colors:
self._data_colors[currency] = next(BREWER12PAIRED)
self._data_colors[currency] = next(color_cycle)
return self._data_colors[currency]
@ -237,17 +160,17 @@ class MainWindow(QMainWindow):
def add_data_row(self, currency):
citem = QStandardItem()
citem.setText(currency)
citem.setForeground(QBrush(QColor(
self.get_currency_color(currency)
)))
citem.setForeground(QBrush(QColor(self.get_currency_color(currency))))
citem.setColumnCount(2)
citem.setCheckable(True)
if currency in DEFAULT_DISPLAY_CURRENCIES:
citem.setCheckState(Qt.Checked)
if currency in constants.DEFAULT_DISPLAY_CURRENCIES:
citem.setCheckState(Qt.CheckState.Checked)
vitem = QStandardItem()
vitem.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
vitem.setTextAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
self.model.setColumnCount(2)
self.model.appendRow([citem, vitem])
self.model.sort(0)
@ -259,7 +182,7 @@ class MainWindow(QMainWindow):
self.update_data_viewer(int(pos.x()))
def update_data_viewer(self, i):
if i not in range(NUMBER_OF_TIMEPOINTS):
if i not in range(constants.NUMBER_OF_TIMEPOINTS):
return
for currency, data in self.data.items():
@ -267,7 +190,7 @@ class MainWindow(QMainWindow):
def update_data_row(self, currency, data):
citem, vitem = self.get_or_create_data_row(currency)
vitem.setText("%.4f" % data['close'])
vitem.setText("%.4f" % data["close"])
def change_base_currency(self, currency):
self.base_currency = currency
@ -294,7 +217,7 @@ class MainWindow(QMainWindow):
self.data = rates
self.volume = volume
self.redraw()
self.update_data_viewer(NUMBER_OF_TIMEPOINTS-1)
self.update_data_viewer(constants.NUMBER_OF_TIMEPOINTS - 1)
def progress_callback(self, progress):
self.progress.setValue(progress)
@ -306,7 +229,7 @@ class MainWindow(QMainWindow):
def notify_error(self, error):
e, tb = error
msg = QMessageBox()
msg.setIcon(QMessageBox.Warning)
msg.setIcon(QMessageBox.Icon.Warning)
msg.setText(e.__class__.__name__)
msg.setInformativeText(str(e))
msg.setDetailedText(tb)
@ -317,45 +240,58 @@ class MainWindow(QMainWindow):
def redraw(self):
y_min, y_max = sys.maxsize, 0
x = np.arange(NUMBER_OF_TIMEPOINTS)
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
])
_, 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.DotLine)
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.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.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.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)
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)
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._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))
@ -363,8 +299,7 @@ class MainWindow(QMainWindow):
self.p2.setYRange(0, max(self.volume))
if __name__ == '__main__':
app = QApplication([])
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
app.exec_()
app.exec_()

View File

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

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,91 @@
import traceback
import constants
import requests
# import requests_cache
from PyQt5.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

@ -10,19 +10,19 @@ hand panel and updated to follow the position of the mouse.
![Doughnut](screenshot-currency2.jpg)
> If you think this app is neat and want to learn more about
PyQt in general, take a look at my [free PyQt tutorials](https://www.learnpyqt.com)
which cover everything you need to know to start building your own applications with PyQt.
> 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
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
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.

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

@ -1,42 +1,54 @@
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import pyqtgraph as pg
import requests
import requests_cache
from collections import defaultdict
from datetime import datetime, timedelta, date
from itertools import cycle
import sys
import time
import traceback
from collections import defaultdict
from datetime import date, datetime, timedelta
from itertools import cycle
requests_cache.install_cache('http_cache')
import constants
import requests
# 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
# import requests_cache
from PyQt5.QtCore import (
QObject,
QRunnable,
Qt,
QThreadPool,
pyqtSignal,
pyqtSlot,
)
from PyQt5.QtGui import (
QBrush,
QColor,
QStandardItem,
QStandardItemModel,
)
from PyQt5.QtWidgets import (
QApplication,
QComboBox,
QHBoxLayout,
QMainWindow,
QProgressBar,
QTableView,
QToolBar,
QWidget,
)
# Colour sets.
BREWER12PAIRED = cycle(['#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00',
'#cab2d6', '#6a3d9a', '#ffff99', '#b15928' ])
color_cycle = cycle(constants.BREWER12PAIRED)
# requests_cache.install_cache("cache")
# Base PyQtGraph configuration
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
# 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, HISTORIC_DAYS_N)]
current = [(0, constants.HISTORIC_DAYS_N)]
while current:
a, b = current.pop(0)
n = (a + b) // 2
@ -49,11 +61,11 @@ while current:
current.append((b, n))
class WorkerSignals(QObject):
'''
"""
Defines the signals available from a running worker thread.
'''
"""
finished = pyqtSignal()
error = pyqtSignal(tuple)
progress = pyqtSignal(int)
@ -62,14 +74,15 @@ class WorkerSignals(QObject):
class UpdateWorker(QRunnable):
'''
"""
Worker thread for updating currency.
'''
"""
signals = WorkerSignals()
is_interrupted = False
def __init__(self, base_currency):
super(UpdateWorker, self).__init__()
super().__init__()
self.base_currency = base_currency
self.signals.cancel.connect(self.cancel)
@ -81,14 +94,14 @@ class UpdateWorker(QRunnable):
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})
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 = data["rates"]
rates[self.base_currency] = 1.0
self.signals.data.emit(offset ,rates)
self.signals.data.emit(offset, rates)
self.signals.progress.emit(int(100 * n / total_requests))
if not r.from_cache:
@ -97,7 +110,6 @@ class UpdateWorker(QRunnable):
if self.is_interrupted:
break
except Exception as e:
print(e)
exctype, value = sys.exc_info()[:2]
@ -110,11 +122,9 @@ class UpdateWorker(QRunnable):
self.is_interrupted = True
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
def __init__(self):
super().__init__()
layout = QHBoxLayout()
@ -123,22 +133,22 @@ class MainWindow(QMainWindow):
self.line = pg.InfiniteLine(
pos=-20,
pen=pg.mkPen('k', width=3),
movable=False # We have our own code to handle dragless moving.
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=-HISTORIC_DAYS_N + 1, xMax=0)
self.ax.setLimits(xMin=-constants.HISTORIC_DAYS_N + 1, xMax=0)
self.ax.getPlotItem().scene().sigMouseMoved.connect(self.mouse_move_handler)
self.base_currency = DEFAULT_BASE_CURRENCY
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 = DEFAULT_DISPLAY_CURRENCIES
self._data_visible = constants.DEFAULT_DISPLAY_CURRENCIES
self._last_updated = None
@ -166,7 +176,7 @@ class MainWindow(QMainWindow):
self.currencyList = QComboBox()
toolbar.addWidget(self.currencyList)
self.update_currency_list(DEFAULT_DISPLAY_CURRENCIES)
self.update_currency_list(constants.DEFAULT_DISPLAY_CURRENCIES)
self.currencyList.setCurrentText(self.base_currency)
self.currencyList.currentTextChanged.connect(self.change_base_currency)
@ -190,7 +200,7 @@ class MainWindow(QMainWindow):
return
currency = i.text()
checked = i.checkState() == Qt.Checked
checked = i.checkState() == Qt.CheckState.Checked
if currency in self._data_visible:
if not checked:
@ -203,24 +213,24 @@ class MainWindow(QMainWindow):
def get_currency_color(self, currency):
if currency not in self._data_colors:
self._data_colors[currency] = next(BREWER12PAIRED)
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.setForeground(QBrush(QColor(self.get_currency_color(currency))))
citem.setColumnCount(2)
citem.setCheckable(True)
if currency in DEFAULT_DISPLAY_CURRENCIES:
citem.setCheckState(Qt.Checked)
if currency in constants.DEFAULT_DISPLAY_CURRENCIES:
citem.setCheckState(Qt.CheckState.Checked)
vitem = QStandardItem()
vitem.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
vitem.setTextAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
self.model.setColumnCount(2)
self.model.appendRow([citem, vitem])
self.model.sort(0)
@ -262,7 +272,7 @@ class MainWindow(QMainWindow):
self.worker.signals.cancel.emit()
# Prefill our data store with None ('no data')
self.data = [None] * HISTORIC_DAYS_N
self.data = [None] * constants.HISTORIC_DAYS_N
self.worker = UpdateWorker(self.base_currency)
# Handle callbacks with data and trigger refresh.
@ -275,9 +285,10 @@ class MainWindow(QMainWindow):
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)
):
if (
self._last_updated is None
or self._last_updated < datetime.now() - timedelta(seconds=1)
):
self.redraw()
self._last_updated = datetime.now()
@ -299,7 +310,7 @@ class MainWindow(QMainWindow):
plotd = defaultdict(list)
x_ticks = []
tick_step_size = HISTORIC_DAYS_N / 6
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:
@ -307,8 +318,8 @@ class MainWindow(QMainWindow):
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')))
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())
@ -327,20 +338,16 @@ class MainWindow(QMainWindow):
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
)
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,[]])
self.ax.getAxis("bottom").setTicks([x_ticks, []])
if __name__ == '__main__':
app = QApplication([])
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
app.exec_()
app.exec_()

View File

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

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -8,6 +8,7 @@
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
@ -39,16 +40,16 @@ class Ui_MainWindow(object):
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
self.currentTimeLabel = QtWidgets.QLabel(self.centralWidget)
self.currentTimeLabel.setMinimumSize(QtCore.QSize(80, 0))
self.currentTimeLabel.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
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.Horizontal)
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.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
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)
@ -58,47 +59,76 @@ class Ui_MainWindow(object):
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)
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)
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)
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)
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)
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)
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)
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("")
@ -108,7 +138,7 @@ class Ui_MainWindow(object):
self.volumeSlider = QtWidgets.QSlider(self.centralWidget)
self.volumeSlider.setMaximum(100)
self.volumeSlider.setProperty("value", 100)
self.volumeSlider.setOrientation(QtCore.Qt.Horizontal)
self.volumeSlider.setOrientation(QtCore.Qt.Orientation.Horizontal)
self.volumeSlider.setObjectName("volumeSlider")
self.horizontalLayout_5.addWidget(self.volumeSlider)
self.verticalLayout.addLayout(self.horizontalLayout_5)
@ -138,4 +168,3 @@ class Ui_MainWindow(object):
self.totalTimeLabel.setText(_translate("MainWindow", "0:00"))
self.menuFIle.setTitle(_translate("MainWindow", "FIle"))
self.open_file_action.setText(_translate("MainWindow", "Open file..."))

View File

@ -1,6 +1,6 @@
# Failamp — Simple mediaplayer build in PyQt
Simple app to listen to and watch videos and audio files,
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.
@ -15,6 +15,6 @@ which floats on top.
![Mediaplayer](screenshot-mediaplayer2.jpg)
> If you think this app is neat and want to learn more about
PyQt in general, take a look at my [free PyQt tutorials](https://www.learnpyqt.com)
which cover everything you need to know to start building your own applications with PyQt.
> 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.

View File

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 544 B

View File

Before

Width:  |  Height:  |  Size: 427 B

After

Width:  |  Height:  |  Size: 427 B

View File

Before

Width:  |  Height:  |  Size: 577 B

After

Width:  |  Height:  |  Size: 577 B

View File

Before

Width:  |  Height:  |  Size: 572 B

After

Width:  |  Height:  |  Size: 572 B

View File

Before

Width:  |  Height:  |  Size: 411 B

After

Width:  |  Height:  |  Size: 411 B

View File

Before

Width:  |  Height:  |  Size: 470 B

After

Width:  |  Height:  |  Size: 470 B

View File

Before

Width:  |  Height:  |  Size: 566 B

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 PyQt5.QtCore import QSize, Qt, QUrl, pyqtSignal
from PyQt5.QtGui import QColor, QPalette
from PyQt5.QtMultimedia import (
QMediaContent,
QMediaPlayer,
QMediaPlaylist,
)
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5.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.State.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 PyQt5.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

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 82 KiB

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

@ -18,9 +18,9 @@ alien bugs (B'ug) but they could just as easily be anything else.
![Moonsweeper](screenshot-minesweeper2.jpg)
> If you think this app is neat and want to learn more about
PyQt in general, take a look at my [free PyQt tutorials](https://www.learnpyqt.com)
which cover everything you need to know to start building your own applications with PyQt.
> 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

View File

@ -0,0 +1,39 @@
from enum import IntEnum
from PyQt5.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",
}

View File

Before

Width:  |  Height:  |  Size: 752 B

After

Width:  |  Height:  |  Size: 752 B

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