Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[run]
omit = tests/*
121 changes: 119 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,119 @@
# pyqt-loading-button
A QPushButton with built-in loading animations for PyQt and PySide
# PyQt Loading Button

[![PyPI](https://img.shields.io/badge/pypi-v1.0.0-blue)](https://pypi.org/project/pyqt-loading-button)
[![Python](https://img.shields.io/badge/python-3.7+-blue)](https://github.com/marcohenning/pyqt-loading-button)
[![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/marcohenning/pyqt-loading-button/blob/master/LICENSE)
[![Coverage](https://img.shields.io/badge/coverage-99%25-neon)](https://github.com/marcohenning/pyqt-loading-button)
[![Build](https://img.shields.io/badge/build-passing-neon)](https://github.com/marcohenning/pyqt-loading-button)

A QPushButton with built-in loading animations for PyQt and PySide.

![Main](https://github.com/user-attachments/assets/e4142cd2-9618-498e-a4c1-a2000239b0c9)

## About

The widget functions exactly like PyQt's regular `QPushButton` with the only exception being the way methods are connected to the `clicked` event. Normally you would connect a method to the `clicked` event by using the `connect()` method. On this button you use the `setAction()` method instead, passing a callable object as its parameter the same way you would do with the `connect()` method. The method will then get executed in a `QThread`, allowing the button to display a loading animation.

## Installation

```
pip install pyqt-loading-button
```

## Example

```python
import time
from PyQt6.QtGui import QColor
from PyQt6.QtWidgets import QMainWindow
from pyqt_loading_button import LoadingButton, AnimationType


class Window(QMainWindow):

def __init__(self):
super().__init__(parent=None)

# LoadingButton
self.button_1 = LoadingButton(self)
self.button_1.setText('Click me!')
self.button_1.setAnimationType(AnimationType.Circle)
self.button_1.setAnimationSpeed(2000)
self.button_1.setAnimationColor(QColor(0, 0, 0))
self.button_1.setAnimationWidth(15)
self.button_1.setAnimationStrokeWidth(3)
self.button_1.setAction(self.do_something)

def do_something(self):
time.sleep(5) # Simulate long task
```

## Documentation

* **Setting the button text:**
```python
loading_button.setText('Click me!')
```

* **Setting the action connected to the clicked event:**
```python
def do_something():
time.sleep(5) # Simulate long task

loading_button.setAction(do_something)
```

* **Setting the animation type:**
```python
loading_button.setAnimationType(AnimationType.Circle) # Circular animation
loading_button.setAnimationType(AnimationType.Dots) # Dotted animation
```

* **Setting the animation speed:**
```python
# 2000 means each loop of the animation takes 2000 ms to complete
loading_button.setAnimationSpeed(2000)
```

* **Setting the animation width:**
```python
loading_button.setAnimationWidth(15) # Total width of the animation is 15 px
```

* **Setting the animation stroke width:**
```python
loading_button.setAnimationStrokeWidth(3) # Stroke width of the brush is 3 px
```

* **Setting the animation color:**
```python
loading_button.setAnimationColor(QColor(0, 0, 0))
```

* **Checking whether the action is currently being executed:**
```python
loading_button.isRunning()
```

**<br>All methods:**

| Method | Description |
|---------------------------------------------------------|------------------------------------------------------------------------------------------|
| `text(self)` | Get the current button text |
| `setText(self, text: str)` | Set the button text |
| `setAction(self, action: callable)` | Set the action connected to the clicked event |
| `isRunning(self)` | Get whether the action is currently being executed |
| `getAnimationType(self)` | Get the current animation type |
| `setAnimationType(self, animation_type: AnimationType)` | Set the animation type |
| `getAnimationSpeed(self)` | Get the current animation speed (time it takes the animation to complete one loop in ms) |
| `setAnimationSpeed(self, speed: int)` | Set the animation speed (time it takes the animation to complete one loop in ms) |
| `getAnimationWidth(self)` | Get the current width of the animation |
| `setAnimationWidth(self, width: int)` | Set the width of the animation |
| `getAnimationStrokeWidth(self)` | Get the current width of the brush stroke |
| `setAnimationStrokeWidth(self, width: int)` | Set the width of the brush stroke |
| `getAnimationColor(self)` | Get the current animation color |
| `setAnimationColor(self, color: QColor)` | Set the animation color |

## License

This software is licensed under the [MIT license](https://github.com/marcohenning/pyqt-loading-button/blob/master/LICENSE).
60 changes: 60 additions & 0 deletions examples/pyqt6/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import sys
import time
from PyQt6.QtGui import QColor
from PyQt6.QtWidgets import QMainWindow, QApplication
from src.pyqt_loading_button import LoadingButton, AnimationType


class Window(QMainWindow):

def __init__(self):
super().__init__(parent=None)

# Window settings
self.setWindowTitle('Example')
self.setFixedSize(300, 165)

# Button with circular animation
self.button_1 = LoadingButton(self)
self.button_1.setGeometry(94, 42, 110, 30)
self.button_1.setText('Click me!')
self.button_1.setAnimationType(AnimationType.Circle) # Animation type
self.button_1.setAnimationSpeed(2000) # Time it takes until the animation is completed once (in ms)
self.button_1.setAnimationColor(QColor(255, 255, 255)) # Animation color
self.button_1.setAnimationWidth(15) # Width of the entire animation
self.button_1.setAnimationStrokeWidth(3) # Width of the circle's brush stroke
self.button_1.setAction(self.do_something) # Connect the button to a method
self.button_1.setStyleSheet(
'color: white;'
'background: #23395d;'
'border: none;'
'border-radius: 5px;'
)

# Button with dotted animation
self.button_2 = LoadingButton(self)
self.button_2.setGeometry(94, 82, 110, 30)
self.button_2.setText('Click me!')
self.button_2.setAnimationType(AnimationType.Dots) # Animation type
self.button_2.setAnimationSpeed(800) # Time it takes until the animation is completed once (in ms)
self.button_2.setAnimationColor(QColor(255, 255, 255)) # Animation color
self.button_2.setAnimationWidth(20) # Width of the entire animation
self.button_2.setAnimationStrokeWidth(4) # Width of each dot
self.button_2.setAction(self.do_something) # Connect the button to a method
self.button_2.setStyleSheet(
'color: white;'
'background: #23395d;'
'border: none;'
'border-radius: 5px;'
)

def do_something(self):
time.sleep(5) # Simulate long task


# Run the example
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.show()
app.exec()
60 changes: 60 additions & 0 deletions examples/pyside6/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import sys
import time
from PySide6.QtGui import QColor
from PySide6.QtWidgets import QMainWindow, QApplication
from src.pyqt_loading_button import LoadingButton, AnimationType


class Window(QMainWindow):

def __init__(self):
super().__init__(parent=None)

# Window settings
self.setWindowTitle('Example')
self.setFixedSize(300, 165)

# Button with circular animation
self.button_1 = LoadingButton(self)
self.button_1.setGeometry(94, 42, 110, 30)
self.button_1.setText('Click me!')
self.button_1.setAnimationType(AnimationType.Circle) # Animation type
self.button_1.setAnimationSpeed(2000) # Time it takes until the animation is completed once (in ms)
self.button_1.setAnimationColor(QColor(255, 255, 255)) # Animation color
self.button_1.setAnimationWidth(15) # Width of the entire animation
self.button_1.setAnimationStrokeWidth(3) # Width of the circle's brush stroke
self.button_1.setAction(self.do_something) # Connect the button to a method
self.button_1.setStyleSheet(
'color: white;'
'background: #23395d;'
'border: none;'
'border-radius: 5px;'
)

# Button with dotted animation
self.button_2 = LoadingButton(self)
self.button_2.setGeometry(94, 82, 110, 30)
self.button_2.setText('Click me!')
self.button_2.setAnimationType(AnimationType.Dots) # Animation type
self.button_2.setAnimationSpeed(800) # Time it takes until the animation is completed once (in ms)
self.button_2.setAnimationColor(QColor(255, 255, 255)) # Animation color
self.button_2.setAnimationWidth(20) # Width of the entire animation
self.button_2.setAnimationStrokeWidth(4) # Width of each dot
self.button_2.setAction(self.do_something) # Connect the button to a method
self.button_2.setStyleSheet(
'color: white;'
'background: #23395d;'
'border: none;'
'border-radius: 5px;'
)

def do_something(self):
time.sleep(5) # Simulate long task


# Run the example
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.show()
app.exec()
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
qt_api=pyqt6
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
QtPy>=2.4.1
28 changes: 28 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from setuptools import setup, find_namespace_packages


with open('README.md', 'r') as fh:
readme = "\n" + fh.read()

setup(
name='pyqt-loading-button',
version='1.0.0',
author='Marco Henning',
license='MIT',
packages=find_namespace_packages(where="src"),
package_dir={"": "src"},
install_requires=[
'QtPy>=2.4.1'
],
python_requires='>=3.7',
description='A QPushButton with built-in loading animations for PyQt and PySide',
long_description=readme,
long_description_content_type='text/markdown',
url='https://github.com/marcohenning/pyqt-loading-button',
keywords=['python', 'pyqt', 'qt', 'button', 'animation'],
classifiers=[
'Programming Language :: Python :: 3',
'Operating System :: OS Independent',
'License :: OSI Approved :: MIT License'
]
)
2 changes: 1 addition & 1 deletion src/pyqt_loading_button/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .loading_button import LoadingButton
from .loading_button import LoadingButton, AnimationType
6 changes: 6 additions & 0 deletions src/pyqt_loading_button/animation_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from enum import Enum


class AnimationType(Enum):
Circle = 1
Dots = 2
Loading