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:
1
pyqt5/widgets/colorbutton/__init__.py
Normal file
1
pyqt5/widgets/colorbutton/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .colorbutton import ColorButton
|
||||
58
pyqt5/widgets/colorbutton/colorbutton.py
Normal file
58
pyqt5/widgets/colorbutton/colorbutton.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import pyqtSignal as Signal
|
||||
from PyQt5.QtGui import QColor
|
||||
from PyQt5.QtWidgets import QColorDialog, QPushButton
|
||||
|
||||
|
||||
class ColorButton(QPushButton):
|
||||
"""
|
||||
Custom Qt Widget to show a chosen color.
|
||||
|
||||
Left-clicking the button shows the color-chooser, while
|
||||
right-clicking resets the color to the default color (None by default).
|
||||
"""
|
||||
|
||||
colorChanged = Signal(object)
|
||||
|
||||
def __init__(self, *args, color=None, **kwargs):
|
||||
super(ColorButton, self).__init__(*args, **kwargs)
|
||||
self.setObjectName("ColorButton")
|
||||
self._color = None
|
||||
self._default = color
|
||||
self.pressed.connect(self.onColorPicker)
|
||||
|
||||
# Set the initial/default state.
|
||||
self.setColor(self._default)
|
||||
|
||||
def setColor(self, color):
|
||||
if color != self._color:
|
||||
self._color = color
|
||||
self.colorChanged.emit(color)
|
||||
|
||||
if self._color:
|
||||
self.setStyleSheet(f"#ColorButton {{background-color: {self._color};}}")
|
||||
else:
|
||||
self.setStyleSheet("")
|
||||
|
||||
def color(self):
|
||||
return self._color
|
||||
|
||||
def onColorPicker(self):
|
||||
"""
|
||||
Show color-picker dialog to select color.
|
||||
|
||||
Qt will use the native dialog by default.
|
||||
|
||||
"""
|
||||
dlg = QColorDialog(self)
|
||||
if self._color:
|
||||
dlg.setCurrentColor(QColor(self._color))
|
||||
|
||||
if dlg.exec_():
|
||||
self.setColor(dlg.currentColor().name())
|
||||
|
||||
def mousePressEvent(self, e):
|
||||
if e.button() == Qt.MouseButton.RightButton:
|
||||
self.setColor(self._default)
|
||||
|
||||
return super().mousePressEvent(e)
|
||||
23
pyqt5/widgets/colorbutton/main.py
Normal file
23
pyqt5/widgets/colorbutton/main.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import sys
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow
|
||||
|
||||
from colorbutton import ColorButton
|
||||
|
||||
|
||||
class Window(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
palette = ColorButton(color="red")
|
||||
palette.colorChanged.connect(self.show_selected_color)
|
||||
self.setCentralWidget(palette)
|
||||
|
||||
def show_selected_color(self, c):
|
||||
print("Selected: {}".format(c))
|
||||
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = Window()
|
||||
w.show()
|
||||
app.exec_()
|
||||
1
pyqt5/widgets/equalizer_bar/__init__.py
Normal file
1
pyqt5/widgets/equalizer_bar/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .equalizer_bar import EqualizerBar
|
||||
142
pyqt5/widgets/equalizer_bar/equalizer_bar.py
Normal file
142
pyqt5/widgets/equalizer_bar/equalizer_bar.py
Normal file
@@ -0,0 +1,142 @@
|
||||
from PyQt5.QtCore import QRect, QRectF, QSize, Qt, QTimer
|
||||
from PyQt5.QtGui import QBrush, QColor, QPainter
|
||||
from PyQt5.QtWidgets import QSizePolicy, QWidget
|
||||
|
||||
|
||||
class EqualizerBar(QWidget):
|
||||
def __init__(self, bars, steps):
|
||||
super().__init__()
|
||||
|
||||
self.setSizePolicy(
|
||||
QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding
|
||||
)
|
||||
|
||||
if isinstance(steps, list):
|
||||
# list of colours.
|
||||
self.n_steps = len(steps)
|
||||
self.steps = steps
|
||||
|
||||
elif isinstance(steps, int):
|
||||
# int number of bars, defaults to red.
|
||||
self.n_steps = steps
|
||||
self.steps = ["red"] * steps
|
||||
|
||||
else:
|
||||
raise TypeError("steps must be a list or int")
|
||||
|
||||
# Bar appearance.
|
||||
self.n_bars = bars
|
||||
self._x_solid_percent = 0.8
|
||||
self._y_solid_percent = 0.8
|
||||
self._background_color = QColor("black")
|
||||
self._padding = 25 # n-pixel gap around edge.
|
||||
|
||||
# Bar behaviour
|
||||
self._timer = None
|
||||
self.setDecayFrequencyMs(100)
|
||||
self._decay = 10
|
||||
|
||||
# Ranges
|
||||
self._vmin = 0
|
||||
self._vmax = 100
|
||||
|
||||
# Current values are stored in a list.
|
||||
self._values = [0.0] * bars
|
||||
|
||||
def paintEvent(self, e):
|
||||
painter = QPainter(self)
|
||||
|
||||
brush = QBrush()
|
||||
brush.setColor(self._background_color)
|
||||
brush.setStyle(Qt.BrushStyle.SolidPattern)
|
||||
rect = QRect(0, 0, painter.device().width(), painter.device().height())
|
||||
painter.fillRect(rect, brush)
|
||||
|
||||
# Define our canvas.
|
||||
d_height = painter.device().height() - (self._padding * 2)
|
||||
d_width = painter.device().width() - (self._padding * 2)
|
||||
|
||||
# Draw the bars.
|
||||
step_y = d_height / self.n_steps
|
||||
bar_height = step_y * self._y_solid_percent
|
||||
bar_height_space = step_y * (1 - self._x_solid_percent) / 2
|
||||
|
||||
step_x = d_width / self.n_bars
|
||||
bar_width = step_x * self._x_solid_percent
|
||||
bar_width_space = step_x * (1 - self._y_solid_percent) / 2
|
||||
|
||||
for b in range(self.n_bars):
|
||||
# Calculate the y-stop position for this bar, from the value in range.
|
||||
pc = (self._values[b] - self._vmin) / (self._vmax - self._vmin)
|
||||
n_steps_to_draw = int(pc * self.n_steps)
|
||||
|
||||
for n in range(n_steps_to_draw):
|
||||
brush.setColor(QColor(self.steps[n]))
|
||||
rect = QRectF(
|
||||
self._padding + (step_x * b) + bar_width_space,
|
||||
self._padding + d_height - ((1 + n) * step_y) + bar_height_space,
|
||||
bar_width,
|
||||
bar_height,
|
||||
)
|
||||
painter.fillRect(rect, brush)
|
||||
|
||||
painter.end()
|
||||
|
||||
def sizeHint(self):
|
||||
return QSize(20, 120)
|
||||
|
||||
def _trigger_refresh(self):
|
||||
self.update()
|
||||
|
||||
def setDecay(self, f):
|
||||
self._decay = float(f)
|
||||
|
||||
def setDecayFrequencyMs(self, ms):
|
||||
if self._timer:
|
||||
self._timer.stop()
|
||||
|
||||
if ms:
|
||||
self._timer = QTimer()
|
||||
self._timer.setInterval(ms)
|
||||
self._timer.timeout.connect(self._decay_beat)
|
||||
self._timer.start()
|
||||
|
||||
def _decay_beat(self):
|
||||
self._values = [max(0, v - self._decay) for v in self._values]
|
||||
self.update() # Redraw new position.
|
||||
|
||||
def setValues(self, v):
|
||||
self._values = v
|
||||
self.update()
|
||||
|
||||
def values(self):
|
||||
return self._values
|
||||
|
||||
def setRange(self, vmin, vmax):
|
||||
assert float(vmin) < float(vmax)
|
||||
self._vmin, self._vmax = float(vmin), float(vmax)
|
||||
|
||||
def setColor(self, color):
|
||||
self.steps = [color] * self._bar.n_steps
|
||||
self.update()
|
||||
|
||||
def setColors(self, colors):
|
||||
self.n_steps = len(colors)
|
||||
self.steps = colors
|
||||
self.update()
|
||||
|
||||
def setBarPadding(self, i):
|
||||
self._padding = int(i)
|
||||
self.update()
|
||||
|
||||
def setBarSolidXPercent(self, f):
|
||||
self._x_solid_percent = float(f)
|
||||
self.update()
|
||||
|
||||
def setBarSolidYPercent(self, f):
|
||||
self._y_solid_percent = float(f)
|
||||
self.update()
|
||||
|
||||
def setBackgroundColor(self, color):
|
||||
self._background_color = QColor(color)
|
||||
self.update()
|
||||
51
pyqt5/widgets/equalizer_bar/main.py
Normal file
51
pyqt5/widgets/equalizer_bar/main.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import random
|
||||
import sys
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow
|
||||
|
||||
from equalizer_bar import EqualizerBar
|
||||
|
||||
|
||||
class Window(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.equalizer = EqualizerBar(
|
||||
5,
|
||||
[
|
||||
"#0C0786",
|
||||
"#40039C",
|
||||
"#6A00A7",
|
||||
"#8F0DA3",
|
||||
"#B02A8F",
|
||||
"#CA4678",
|
||||
"#E06461",
|
||||
"#F1824C",
|
||||
"#FCA635",
|
||||
"#FCCC25",
|
||||
"#EFF821",
|
||||
],
|
||||
)
|
||||
self.equalizer.setBarSolidYPercent(0.4)
|
||||
# self.equalizer.setBarSolidXPercent(0.4)
|
||||
self.setCentralWidget(self.equalizer)
|
||||
|
||||
self._timer = QTimer()
|
||||
self._timer.setInterval(100)
|
||||
self._timer.timeout.connect(self.update_values)
|
||||
self._timer.start()
|
||||
|
||||
def update_values(self):
|
||||
self.equalizer.setValues(
|
||||
[
|
||||
min(100, v + random.randint(0, 50) if random.randint(0, 5) > 2 else v)
|
||||
for v in self.equalizer.values()
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = Window()
|
||||
w.show()
|
||||
app.exec_()
|
||||
1
pyqt5/widgets/gradient/__init__.py
Normal file
1
pyqt5/widgets/gradient/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .gradient import Gradient
|
||||
190
pyqt5/widgets/gradient/gradient.py
Normal file
190
pyqt5/widgets/gradient/gradient.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from PyQt5.QtCore import QRect, QRectF, QSize, Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QColor, QLinearGradient, QPainter, QPen
|
||||
from PyQt5.QtWidgets import QColorDialog, QSizePolicy, QWidget
|
||||
|
||||
|
||||
class Gradient(QWidget):
|
||||
gradientChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, gradient=None):
|
||||
super().__init__()
|
||||
|
||||
self.setSizePolicy(
|
||||
QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding
|
||||
)
|
||||
|
||||
if gradient:
|
||||
self._gradient = gradient
|
||||
|
||||
else:
|
||||
self._gradient = [
|
||||
(0.0, "#000000"),
|
||||
(1.0, "#ffffff"),
|
||||
]
|
||||
|
||||
# Stop point handle sizes.
|
||||
self._handle_w = 10
|
||||
self._handle_h = 10
|
||||
|
||||
self._drag_position = None
|
||||
|
||||
def paintEvent(self, e):
|
||||
painter = QPainter(self)
|
||||
width = painter.device().width()
|
||||
height = painter.device().height()
|
||||
|
||||
# Draw the linear horizontal gradient.
|
||||
gradient = QLinearGradient(0, 0, width, 0)
|
||||
for stop, color in self._gradient:
|
||||
gradient.setColorAt(stop, QColor(color))
|
||||
|
||||
rect = QRect(0, 0, width, height)
|
||||
painter.fillRect(rect, gradient)
|
||||
|
||||
pen = QPen()
|
||||
|
||||
y = painter.device().height() / 2
|
||||
|
||||
# Draw the stop handles.
|
||||
for stop, _ in self._gradient:
|
||||
pen.setColor(QColor("white"))
|
||||
painter.setPen(pen)
|
||||
|
||||
painter.drawLine(
|
||||
int(stop * width),
|
||||
int(y - self._handle_h),
|
||||
int(stop * width),
|
||||
int(y + self._handle_h),
|
||||
)
|
||||
|
||||
pen.setColor(QColor("red"))
|
||||
painter.setPen(pen)
|
||||
|
||||
rect = QRectF(
|
||||
stop * width - self._handle_w / 2,
|
||||
y - self._handle_h / 2,
|
||||
self._handle_w,
|
||||
self._handle_h,
|
||||
)
|
||||
painter.drawRect(rect)
|
||||
|
||||
painter.end()
|
||||
|
||||
def sizeHint(self):
|
||||
return QSize(200, 50)
|
||||
|
||||
def _sort_gradient(self):
|
||||
self._gradient = sorted(self._gradient, key=lambda g: g[0])
|
||||
|
||||
def _constrain_gradient(self):
|
||||
self._gradient = [
|
||||
# Ensure values within valid range.
|
||||
(max(0.0, min(1.0, stop)), color)
|
||||
for stop, color in self._gradient
|
||||
]
|
||||
|
||||
def setGradient(self, gradient):
|
||||
assert all([0.0 <= stop <= 1.0 for stop, _ in gradient])
|
||||
self._gradient = gradient
|
||||
self._constrain_gradient()
|
||||
self._sort_gradient()
|
||||
self.gradientChanged.emit()
|
||||
|
||||
def gradient(self):
|
||||
return self._gradient
|
||||
|
||||
@property
|
||||
def _end_stops(self):
|
||||
return [0, len(self._gradient) - 1]
|
||||
|
||||
def addStop(self, stop, color=None):
|
||||
# Stop is a value 0...1, find the point to insert this stop
|
||||
# in the list.
|
||||
assert 0.0 <= stop <= 1.0
|
||||
|
||||
for n, g in enumerate(self._gradient):
|
||||
if g[0] > stop:
|
||||
# Insert before this entry, with specified or next color.
|
||||
self._gradient.insert(n, (stop, color or g[1]))
|
||||
break
|
||||
self._constrain_gradient()
|
||||
self.gradientChanged.emit()
|
||||
self.update()
|
||||
|
||||
def removeStopAtPosition(self, n):
|
||||
if n not in self._end_stops:
|
||||
del self._gradient[n]
|
||||
self.gradientChanged.emit()
|
||||
self.update()
|
||||
|
||||
def setColorAtPosition(self, n, color):
|
||||
if n < len(self._gradient):
|
||||
stop, _ = self._gradient[n]
|
||||
self._gradient[n] = stop, color
|
||||
self.gradientChanged.emit()
|
||||
self.update()
|
||||
|
||||
def chooseColorAtPosition(self, n, current_color=None):
|
||||
dlg = QColorDialog(self)
|
||||
if current_color:
|
||||
dlg.setCurrentColor(QColor(current_color))
|
||||
|
||||
if dlg.exec_():
|
||||
self.setColorAtPosition(n, dlg.currentColor().name())
|
||||
|
||||
def _find_stop_handle_for_event(self, e, to_exclude=None):
|
||||
width = self.width()
|
||||
height = self.height()
|
||||
midpoint = height / 2
|
||||
|
||||
# Are we inside a stop point? First check y.
|
||||
if e.y() >= midpoint - self._handle_h and e.y() <= midpoint + self._handle_h:
|
||||
for n, (stop, color) in enumerate(self._gradient):
|
||||
if to_exclude and n in to_exclude:
|
||||
# Allow us to skip the extreme ends of the gradient.
|
||||
continue
|
||||
if (
|
||||
e.x() >= stop * width - self._handle_w
|
||||
and e.x() <= stop * width + self._handle_w
|
||||
):
|
||||
return n
|
||||
|
||||
def mousePressEvent(self, e):
|
||||
# We're in this stop point.
|
||||
if e.button() == Qt.MouseButton.RightButton:
|
||||
n = self._find_stop_handle_for_event(e)
|
||||
if n is not None:
|
||||
_, color = self._gradient[n]
|
||||
self.chooseColorAtPosition(n, color)
|
||||
|
||||
elif e.button() == Qt.MouseButton.LeftButton:
|
||||
n = self._find_stop_handle_for_event(e, to_exclude=self._end_stops)
|
||||
if n is not None:
|
||||
# Activate drag mode.
|
||||
self._drag_position = n
|
||||
|
||||
def mouseReleaseEvent(self, e):
|
||||
self._drag_position = None
|
||||
self._sort_gradient()
|
||||
|
||||
def mouseMoveEvent(self, e):
|
||||
# If drag active, move the stop.
|
||||
if self._drag_position:
|
||||
stop = e.x() / self.width()
|
||||
_, color = self._gradient[self._drag_position]
|
||||
self._gradient[self._drag_position] = stop, color
|
||||
self._constrain_gradient()
|
||||
self.update()
|
||||
|
||||
def mouseDoubleClickEvent(self, e):
|
||||
# Calculate the position of the click relative 0..1 to the width.
|
||||
n = self._find_stop_handle_for_event(e)
|
||||
if n:
|
||||
self._sort_gradient() # Ensure ordered.
|
||||
# Delete existing, if not at the ends.
|
||||
if n > 0 and n < len(self._gradient) - 1:
|
||||
self.removeStopAtPosition(n)
|
||||
|
||||
else:
|
||||
stop = e.x() / self.width()
|
||||
self.addStop(stop)
|
||||
20
pyqt5/widgets/gradient/main.py
Normal file
20
pyqt5/widgets/gradient/main.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import sys
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow
|
||||
|
||||
from gradient import Gradient
|
||||
|
||||
|
||||
class Window(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
gradient = Gradient()
|
||||
gradient.setGradient([(0, "black"), (1, "green"), (0.5, "red")])
|
||||
self.setCentralWidget(gradient)
|
||||
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = Window()
|
||||
w.show()
|
||||
app.exec_()
|
||||
1
pyqt5/widgets/paint/__init__.py
Normal file
1
pyqt5/widgets/paint/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .paint import Paint
|
||||
21
pyqt5/widgets/paint/main.py
Normal file
21
pyqt5/widgets/paint/main.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import sys
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow
|
||||
|
||||
from paint import PaintWidget
|
||||
|
||||
|
||||
class Window(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
paint = PaintWidget(300, 300)
|
||||
paint.setPenWidth(5)
|
||||
paint.setPenColor("#EB5160")
|
||||
self.setCentralWidget(paint)
|
||||
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = Window()
|
||||
w.show()
|
||||
app.exec_()
|
||||
95
pyqt5/widgets/paint/paint.py
Normal file
95
pyqt5/widgets/paint/paint.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from PyQt5.QtCore import QPoint, Qt
|
||||
from PyQt5.QtGui import QBrush, QColor, QPainter, QPen, QPixmap
|
||||
from PyQt5.QtWidgets import QLabel
|
||||
|
||||
|
||||
class PaintWidget(QLabel):
|
||||
def __init__(self, width, height, background="white", *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
pixmap = QPixmap(width, height)
|
||||
self.setPixmap(pixmap)
|
||||
|
||||
# Fill the canvas with the initial color.
|
||||
painter = QPainter(self.pixmap())
|
||||
brush = QBrush()
|
||||
brush.setColor(QColor(background))
|
||||
brush.setStyle(Qt.BrushStyle.SolidPattern)
|
||||
painter.fillRect(0, 0, pixmap.width(), pixmap.height(), brush)
|
||||
painter.end()
|
||||
|
||||
self.last_x, self.last_y = None, None
|
||||
self._pen_color = QColor("#000000")
|
||||
self._pen_width = 4
|
||||
|
||||
def setPenColor(self, c):
|
||||
self._pen_color = QColor(c)
|
||||
|
||||
def setPenWidth(self, w):
|
||||
self._pen_width = int(w)
|
||||
|
||||
def mouseMoveEvent(self, e):
|
||||
if self.last_x is None: # First event.
|
||||
self.last_x = e.x()
|
||||
self.last_y = e.y()
|
||||
return # Ignore the first time.
|
||||
|
||||
painter = QPainter(self.pixmap())
|
||||
p = painter.pen()
|
||||
p.setWidth(self._pen_width)
|
||||
p.setColor(self._pen_color)
|
||||
painter.setPen(p)
|
||||
painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
|
||||
painter.end()
|
||||
self.update()
|
||||
|
||||
# Update the origin for next time.
|
||||
self.last_x = e.x()
|
||||
self.last_y = e.y()
|
||||
|
||||
def mousePressEvent(self, e):
|
||||
if e.button() == Qt.MouseButton.RightButton:
|
||||
self._flood_fill_from_event(e)
|
||||
|
||||
def mouseReleaseEvent(self, e):
|
||||
self.last_x = None
|
||||
self.last_y = None
|
||||
|
||||
def _flood_fill_from_event(self, e):
|
||||
image = self.pixmap().toImage()
|
||||
w, h = image.width(), image.height()
|
||||
x, y = e.x(), e.y()
|
||||
|
||||
# Get our target color from origin.
|
||||
target_color = image.pixel(x, y)
|
||||
|
||||
have_seen = set()
|
||||
queue = [(x, y)]
|
||||
|
||||
def get_cardinal_points(have_seen, center_pos):
|
||||
points = []
|
||||
cx, cy = center_pos
|
||||
for x, y in [(1, 0), (0, 1), (-1, 0), (0, -1)]:
|
||||
xx, yy = cx + x, cy + y
|
||||
if (
|
||||
xx >= 0
|
||||
and xx < w
|
||||
and yy >= 0
|
||||
and yy < h
|
||||
and (xx, yy) not in have_seen
|
||||
):
|
||||
points.append((xx, yy))
|
||||
have_seen.add((xx, yy))
|
||||
|
||||
return points
|
||||
|
||||
# Now perform the search and fill.
|
||||
painter = QPainter(self.pixmap())
|
||||
painter.setPen(QPen(self._pen_color))
|
||||
|
||||
while queue:
|
||||
x, y = queue.pop()
|
||||
if image.pixel(x, y) == target_color:
|
||||
painter.drawPoint(QPoint(x, y))
|
||||
queue.extend(get_cardinal_points(have_seen, (x, y)))
|
||||
|
||||
self.update()
|
||||
1
pyqt5/widgets/palette/__init__.py
Normal file
1
pyqt5/widgets/palette/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .palette import PaletteGrid, PaletteHorizontal, PaletteVertical
|
||||
25
pyqt5/widgets/palette/main.py
Normal file
25
pyqt5/widgets/palette/main.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import sys
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow
|
||||
|
||||
from palette import PaletteGrid, PaletteHorizontal, PaletteVertical
|
||||
|
||||
|
||||
class Window(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# PaletteGrid or PaletteHorizontal, or PaletteVertical
|
||||
palette = PaletteGrid("17undertones")
|
||||
palette.selected.connect(self.show_selected_color)
|
||||
self.setCentralWidget(palette)
|
||||
self.show()
|
||||
|
||||
def show_selected_color(self, c):
|
||||
print("Selected: {}".format(c))
|
||||
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = Window()
|
||||
w.show()
|
||||
app.exec_()
|
||||
123
pyqt5/widgets/palette/palette.py
Normal file
123
pyqt5/widgets/palette/palette.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from PyQt5.QtCore import QSize, pyqtSignal
|
||||
from PyQt5.QtWidgets import QGridLayout, QHBoxLayout, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
PALETTES = {
|
||||
# bokeh paired 12
|
||||
"paired12": [
|
||||
"#000000",
|
||||
"#a6cee3",
|
||||
"#1f78b4",
|
||||
"#b2df8a",
|
||||
"#33a02c",
|
||||
"#fb9a99",
|
||||
"#e31a1c",
|
||||
"#fdbf6f",
|
||||
"#ff7f00",
|
||||
"#cab2d6",
|
||||
"#6a3d9a",
|
||||
"#ffff99",
|
||||
"#b15928",
|
||||
"#ffffff",
|
||||
],
|
||||
# d3 category 10
|
||||
"category10": [
|
||||
"#000000",
|
||||
"#1f77b4",
|
||||
"#ff7f0e",
|
||||
"#2ca02c",
|
||||
"#d62728",
|
||||
"#9467bd",
|
||||
"#8c564b",
|
||||
"#e377c2",
|
||||
"#7f7f7f",
|
||||
"#bcbd22",
|
||||
"#17becf",
|
||||
"#ffffff",
|
||||
],
|
||||
# 17 undertones https://lospec.com/palette-list/17undertones
|
||||
"17undertones": [
|
||||
"#000000",
|
||||
"#141923",
|
||||
"#414168",
|
||||
"#3a7fa7",
|
||||
"#35e3e3",
|
||||
"#8fd970",
|
||||
"#5ebb49",
|
||||
"#458352",
|
||||
"#dcd37b",
|
||||
"#fffee5",
|
||||
"#ffd035",
|
||||
"#cc9245",
|
||||
"#a15c3e",
|
||||
"#a42f3b",
|
||||
"#f45b7a",
|
||||
"#c24998",
|
||||
"#81588d",
|
||||
"#bcb0c2",
|
||||
"#ffffff",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class _PaletteButton(QPushButton):
|
||||
def __init__(self, color):
|
||||
super().__init__()
|
||||
self.setFixedSize(QSize(24, 24))
|
||||
self.color = color
|
||||
self.setStyleSheet("background-color: %s;" % color)
|
||||
|
||||
|
||||
class _PaletteBase(QWidget):
|
||||
selected = pyqtSignal(object)
|
||||
|
||||
def _emit_color(self, color):
|
||||
self.selected.emit(color)
|
||||
|
||||
|
||||
class _PaletteLinearBase(_PaletteBase):
|
||||
def __init__(self, colors, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isinstance(colors, str):
|
||||
if colors in PALETTES:
|
||||
colors = PALETTES[colors]
|
||||
|
||||
palette = self.layoutvh()
|
||||
|
||||
for c in colors:
|
||||
b = _PaletteButton(c)
|
||||
b.pressed.connect(lambda c=c: self._emit_color(c))
|
||||
palette.addWidget(b)
|
||||
|
||||
self.setLayout(palette)
|
||||
|
||||
|
||||
class PaletteHorizontal(_PaletteLinearBase):
|
||||
layoutvh = QHBoxLayout
|
||||
|
||||
|
||||
class PaletteVertical(_PaletteLinearBase):
|
||||
layoutvh = QVBoxLayout
|
||||
|
||||
|
||||
class PaletteGrid(_PaletteBase):
|
||||
def __init__(self, colors, n_columns=5, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if isinstance(colors, str):
|
||||
if colors in PALETTES:
|
||||
colors = PALETTES[colors]
|
||||
|
||||
palette = QGridLayout()
|
||||
row, col = 0, 0
|
||||
|
||||
for c in colors:
|
||||
b = _PaletteButton(c)
|
||||
b.pressed.connect(lambda c=c: self._emit_color(c))
|
||||
palette.addWidget(b, row, col)
|
||||
col += 1
|
||||
if col == n_columns:
|
||||
col = 0
|
||||
row += 1
|
||||
|
||||
self.setLayout(palette)
|
||||
1
pyqt5/widgets/passwordedit/__init__.py
Normal file
1
pyqt5/widgets/passwordedit/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .password import PasswordEdit
|
||||
46
pyqt5/widgets/passwordedit/eye.svg
Normal file
46
pyqt5/widgets/passwordedit/eye.svg
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 469.333 469.333" style="enable-background:new 0 0 469.333 469.333;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M234.667,170.667c-35.307,0-64,28.693-64,64s28.693,64,64,64s64-28.693,64-64S269.973,170.667,234.667,170.667z"/>
|
||||
<path d="M234.667,74.667C128,74.667,36.907,141.013,0,234.667c36.907,93.653,128,160,234.667,160
|
||||
c106.773,0,197.76-66.347,234.667-160C432.427,141.013,341.44,74.667,234.667,74.667z M234.667,341.333
|
||||
c-58.88,0-106.667-47.787-106.667-106.667S175.787,128,234.667,128s106.667,47.787,106.667,106.667
|
||||
S293.547,341.333,234.667,341.333z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
50
pyqt5/widgets/passwordedit/hidden.svg
Normal file
50
pyqt5/widgets/passwordedit/hidden.svg
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 469.44 469.44" style="enable-background:new 0 0 469.44 469.44;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M231.147,160.373l67.2,67.2l0.32-3.52c0-35.307-28.693-64-64-64L231.147,160.373z"/>
|
||||
<path d="M234.667,117.387c58.88,0,106.667,47.787,106.667,106.667c0,13.76-2.773,26.88-7.573,38.933l62.4,62.4
|
||||
c32.213-26.88,57.6-61.653,73.28-101.333c-37.013-93.653-128-160-234.773-160c-29.867,0-58.453,5.333-85.013,14.933l46.08,45.973
|
||||
C207.787,120.267,220.907,117.387,234.667,117.387z"/>
|
||||
<path d="M21.333,59.253l48.64,48.64l9.707,9.707C44.48,145.12,16.64,181.707,0,224.053c36.907,93.653,128,160,234.667,160
|
||||
c33.067,0,64.64-6.4,93.547-18.027l9.067,9.067l62.187,62.293l27.2-27.093L48.533,32.053L21.333,59.253z M139.307,177.12
|
||||
l32.96,32.96c-0.96,4.587-1.6,9.173-1.6,13.973c0,35.307,28.693,64,64,64c4.8,0,9.387-0.64,13.867-1.6l32.96,32.96
|
||||
c-14.187,7.04-29.973,11.307-46.827,11.307C175.787,330.72,128,282.933,128,224.053C128,207.2,132.267,191.413,139.307,177.12z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
18
pyqt5/widgets/passwordedit/main.py
Normal file
18
pyqt5/widgets/passwordedit/main.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import sys
|
||||
|
||||
from password import PasswordEdit
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow
|
||||
|
||||
|
||||
class Window(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
password = PasswordEdit()
|
||||
self.setCentralWidget(password)
|
||||
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = Window()
|
||||
w.show()
|
||||
app.exec_()
|
||||
58
pyqt5/widgets/passwordedit/password.py
Normal file
58
pyqt5/widgets/passwordedit/password.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import os
|
||||
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QLineEdit
|
||||
|
||||
folder = os.path.dirname(__file__)
|
||||
|
||||
|
||||
class PasswordEdit(QLineEdit):
|
||||
"""
|
||||
Password LineEdit with icons to show/hide password entries.
|
||||
Based on this example https://kushaldas.in/posts/creating-password-input-widget-in-pyqt.html by Kushal Das.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
show_visibility=True,
|
||||
visible_icon=None,
|
||||
hidden_icon=None,
|
||||
icons_from_theme=False,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if icons_from_theme:
|
||||
self.visibleIcon = QIcon.fromTheme("view-visible")
|
||||
self.hiddenIcon = QIcon.fromTheme("view-hidden")
|
||||
else:
|
||||
if visible_icon:
|
||||
self.visibleIcon = visible_icon
|
||||
else:
|
||||
self.visibleIcon = QIcon(os.path.join(folder, "eye.svg"))
|
||||
if hidden_icon:
|
||||
self.hiddenIcon = hidden_icon
|
||||
else:
|
||||
self.hiddenIcon = QIcon(os.path.join(folder, "hidden.svg"))
|
||||
|
||||
self.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
|
||||
if show_visibility:
|
||||
# Add the password hide/shown toggle at the end of the edit box.
|
||||
self.togglepasswordAction = self.addAction(
|
||||
self.visibleIcon, QLineEdit.ActionPosition.TrailingPosition
|
||||
)
|
||||
self.togglepasswordAction.triggered.connect(self.on_toggle_password_Action)
|
||||
|
||||
self.password_shown = False
|
||||
|
||||
def on_toggle_password_Action(self):
|
||||
if not self.password_shown:
|
||||
self.setEchoMode(QLineEdit.EchoMode.Normal)
|
||||
self.password_shown = True
|
||||
self.togglepasswordAction.setIcon(self.hiddenIcon)
|
||||
else:
|
||||
self.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
self.password_shown = False
|
||||
self.togglepasswordAction.setIcon(self.visibleIcon)
|
||||
1
pyqt5/widgets/power_bar/__init__.py
Normal file
1
pyqt5/widgets/power_bar/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .power_bar import PowerBar
|
||||
26
pyqt5/widgets/power_bar/main.py
Normal file
26
pyqt5/widgets/power_bar/main.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import sys
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from power_bar import PowerBar
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
volume = PowerBar(
|
||||
[
|
||||
"#053061",
|
||||
"#2166ac",
|
||||
"#4393c3",
|
||||
"#92c5de",
|
||||
"#d1e5f0",
|
||||
"#f7f7f7",
|
||||
"#fddbc7",
|
||||
"#f4a582",
|
||||
"#d6604d",
|
||||
"#b2182b",
|
||||
"#67001f",
|
||||
]
|
||||
)
|
||||
volume.setBarSolidPercent(0.8)
|
||||
volume.setBarPadding(5)
|
||||
volume.show()
|
||||
app.exec_()
|
||||
158
pyqt5/widgets/power_bar/power_bar.py
Normal file
158
pyqt5/widgets/power_bar/power_bar.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from PyQt5.QtCore import QRect, QRectF, QSize, Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QBrush, QColor, QPainter
|
||||
from PyQt5.QtWidgets import QDial, QSizePolicy, QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class _Bar(QWidget):
|
||||
clickedValue = pyqtSignal(int)
|
||||
|
||||
def __init__(self, steps):
|
||||
super().__init__()
|
||||
|
||||
self.setSizePolicy(
|
||||
QSizePolicy.Policy.MinimumExpanding,
|
||||
QSizePolicy.Policy.MinimumExpanding,
|
||||
)
|
||||
|
||||
if isinstance(steps, list):
|
||||
# list of colours.
|
||||
self.n_steps = len(steps)
|
||||
self.steps = steps
|
||||
|
||||
elif isinstance(steps, int):
|
||||
# int number of bars, defaults to red.
|
||||
self.n_steps = steps
|
||||
self.steps = ["red"] * steps
|
||||
|
||||
else:
|
||||
raise TypeError("steps must be a list or int")
|
||||
|
||||
self._bar_solid_percent = 0.8
|
||||
self._background_color = QColor("black")
|
||||
self._padding = 4.0 # n-pixel gap around edge.
|
||||
|
||||
def paintEvent(self, e):
|
||||
painter = QPainter(self)
|
||||
|
||||
brush = QBrush()
|
||||
brush.setColor(self._background_color)
|
||||
brush.setStyle(Qt.BrushStyle.SolidPattern)
|
||||
rect = QRect(0, 0, painter.device().width(), painter.device().height())
|
||||
painter.fillRect(rect, brush)
|
||||
|
||||
# Get current state.
|
||||
parent = self.parent()
|
||||
vmin, vmax = parent.minimum(), parent.maximum()
|
||||
value = parent.value()
|
||||
|
||||
# Define our canvas.
|
||||
d_height = painter.device().height() - (self._padding * 2)
|
||||
d_width = painter.device().width() - (self._padding * 2)
|
||||
|
||||
# Draw the bars.
|
||||
step_size = d_height / self.n_steps
|
||||
bar_height = step_size * self._bar_solid_percent
|
||||
bar_spacer = step_size * (1 - self._bar_solid_percent) / 2
|
||||
|
||||
# Calculate the y-stop position, from the value in range.
|
||||
pc = (value - vmin) / (vmax - vmin)
|
||||
n_steps_to_draw = int(pc * self.n_steps)
|
||||
|
||||
for n in range(n_steps_to_draw):
|
||||
brush.setColor(QColor(self.steps[n]))
|
||||
rect = QRectF(
|
||||
self._padding,
|
||||
self._padding + d_height - ((1 + n) * step_size) + bar_spacer,
|
||||
d_width,
|
||||
bar_height,
|
||||
)
|
||||
painter.fillRect(rect, brush)
|
||||
|
||||
painter.end()
|
||||
|
||||
def sizeHint(self):
|
||||
return QSize(40, 120)
|
||||
|
||||
def _trigger_refresh(self):
|
||||
self.update()
|
||||
|
||||
def _calculate_clicked_value(self, e):
|
||||
parent = self.parent()
|
||||
vmin, vmax = parent.minimum(), parent.maximum()
|
||||
d_height = self.size().height() + (self._padding * 2)
|
||||
step_size = d_height / self.n_steps
|
||||
click_y = e.y() - self._padding - step_size / 2
|
||||
|
||||
pc = (d_height - click_y) / d_height
|
||||
value = vmin + pc * (vmax - vmin)
|
||||
self.clickedValue.emit(value)
|
||||
|
||||
def mouseMoveEvent(self, e):
|
||||
self._calculate_clicked_value(e)
|
||||
|
||||
def mousePressEvent(self, e):
|
||||
self._calculate_clicked_value(e)
|
||||
|
||||
|
||||
class PowerBar(QWidget):
|
||||
"""
|
||||
Custom Qt Widget to show a power bar and dial.
|
||||
Demonstrating compound and custom-drawn widget.
|
||||
|
||||
Left-clicking the button shows the color-chooser, while
|
||||
right-clicking resets the color to None (no-color).
|
||||
"""
|
||||
|
||||
def __init__(self, steps=5):
|
||||
super().__init__()
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self._bar = _Bar(steps)
|
||||
layout.addWidget(self._bar)
|
||||
|
||||
# Create the QDial widget and set up defaults.
|
||||
# - we provide accessors on this class to override.
|
||||
self._dial = QDial()
|
||||
self._dial.setNotchesVisible(True)
|
||||
self._dial.setWrapping(False)
|
||||
self._dial.valueChanged.connect(self._bar._trigger_refresh)
|
||||
|
||||
# Take feedback from click events on the meter.
|
||||
self._bar.clickedValue.connect(self._dial.setValue)
|
||||
|
||||
layout.addWidget(self._dial)
|
||||
self.setLayout(layout)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self.__dict__:
|
||||
return self[name]
|
||||
|
||||
try:
|
||||
return getattr(self._dial, name)
|
||||
except AttributeError:
|
||||
raise AttributeError(
|
||||
"'{}' object has no attribute '{}'".format(
|
||||
self.__class__.__name__, name
|
||||
)
|
||||
)
|
||||
|
||||
def setColor(self, color):
|
||||
self._bar.steps = [color] * self._bar.n_steps
|
||||
self._bar.update()
|
||||
|
||||
def setColors(self, colors):
|
||||
self._bar.n_steps = len(colors)
|
||||
self._bar.steps = colors
|
||||
self._bar.update()
|
||||
|
||||
def setBarPadding(self, i):
|
||||
self._bar._padding = int(i)
|
||||
self._bar.update()
|
||||
|
||||
def setBarSolidPercent(self, f):
|
||||
self._bar._bar_solid_percent = float(f)
|
||||
self._bar.update()
|
||||
|
||||
def setBackgroundColor(self, color):
|
||||
self._bar._background_color = QColor(color)
|
||||
self._bar.update()
|
||||
1
pyqt5/widgets/rangeslider/__init__.py
Normal file
1
pyqt5/widgets/rangeslider/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .rangeslider import RangeSlider
|
||||
12
pyqt5/widgets/rangeslider/main.py
Normal file
12
pyqt5/widgets/rangeslider/main.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import sys
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from range_slider import RangeSlider
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
slider = RangeSlider()
|
||||
slider.valueChanged.connect(print)
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
||||
179
pyqt5/widgets/rangeslider/range_slider.py
Normal file
179
pyqt5/widgets/rangeslider/range_slider.py
Normal file
@@ -0,0 +1,179 @@
|
||||
from PyQt5.QtCore import QRect, QSize, Qt, pyqtSignal
|
||||
from PyQt5.QtGui import QBrush, QMouseEvent, QPainter, QPaintEvent, QPalette
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication,
|
||||
QSizePolicy,
|
||||
QSlider,
|
||||
QStyle,
|
||||
QStyleOptionSlider,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
|
||||
class RangeSlider(QWidget):
|
||||
valueChanged = pyqtSignal(int, int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.first_position = 1
|
||||
self.second_position = 8
|
||||
|
||||
self.opt = QStyleOptionSlider()
|
||||
self.opt.minimum = 0
|
||||
self.opt.maximum = 10
|
||||
|
||||
self.setTickPosition(QSlider.TickPosition.TicksAbove)
|
||||
self.setTickInterval(1)
|
||||
|
||||
self.setSizePolicy(
|
||||
QSizePolicy(
|
||||
QSizePolicy.Policy.Expanding,
|
||||
QSizePolicy.Policy.Fixed,
|
||||
QSizePolicy.ControlType.Slider,
|
||||
)
|
||||
)
|
||||
|
||||
def setRangeLimit(self, minimum: int, maximum: int):
|
||||
self.opt.minimum = minimum
|
||||
self.opt.maximum = maximum
|
||||
|
||||
def setRange(self, start: int, end: int):
|
||||
self.first_position = start
|
||||
self.second_position = end
|
||||
|
||||
def getRange(self):
|
||||
return (self.first_position, self.second_position)
|
||||
|
||||
def setTickPosition(self, position: QSlider.TickPosition):
|
||||
self.opt.tickPosition = position
|
||||
|
||||
def setTickInterval(self, ti: int):
|
||||
self.opt.tickInterval = ti
|
||||
|
||||
def paintEvent(self, event: QPaintEvent):
|
||||
painter = QPainter(self)
|
||||
|
||||
# Draw rule
|
||||
self.opt.initFrom(self)
|
||||
self.opt.rect = self.rect()
|
||||
self.opt.sliderPosition = 0
|
||||
self.opt.subControls = (
|
||||
QStyle.SubControl.SC_SliderGroove | QStyle.SubControl.SC_SliderTickmarks
|
||||
)
|
||||
|
||||
# Draw GROOVE
|
||||
self.style().drawComplexControl(
|
||||
QStyle.ComplexControl.CC_Slider, self.opt, painter
|
||||
)
|
||||
|
||||
# Draw INTERVAL
|
||||
|
||||
color = self.palette().color(QPalette.ColorRole.Highlight)
|
||||
color.setAlpha(160)
|
||||
painter.setBrush(QBrush(color))
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
|
||||
self.opt.sliderPosition = self.first_position
|
||||
x_left_handle = (
|
||||
self.style()
|
||||
.subControlRect(
|
||||
QStyle.ComplexControl.CC_Slider,
|
||||
self.opt,
|
||||
QStyle.SubControl.SC_SliderHandle,
|
||||
)
|
||||
.right()
|
||||
)
|
||||
|
||||
self.opt.sliderPosition = self.second_position
|
||||
x_right_handle = (
|
||||
self.style()
|
||||
.subControlRect(
|
||||
QStyle.ComplexControl.CC_Slider,
|
||||
self.opt,
|
||||
QStyle.SubControl.SC_SliderHandle,
|
||||
)
|
||||
.left()
|
||||
)
|
||||
|
||||
groove_rect = self.style().subControlRect(
|
||||
QStyle.ComplexControl.CC_Slider, self.opt, QStyle.SubControl.SC_SliderGroove
|
||||
)
|
||||
|
||||
selection = QRect(
|
||||
x_left_handle,
|
||||
groove_rect.y(),
|
||||
x_right_handle - x_left_handle,
|
||||
groove_rect.height(),
|
||||
).adjusted(-1, 1, 1, -1)
|
||||
|
||||
painter.drawRect(selection)
|
||||
|
||||
# Draw first handle
|
||||
|
||||
self.opt.subControls = QStyle.SubControl.SC_SliderHandle
|
||||
self.opt.sliderPosition = self.first_position
|
||||
self.style().drawComplexControl(
|
||||
QStyle.ComplexControl.CC_Slider, self.opt, painter
|
||||
)
|
||||
|
||||
# Draw second handle
|
||||
self.opt.sliderPosition = self.second_position
|
||||
self.style().drawComplexControl(
|
||||
QStyle.ComplexControl.CC_Slider, self.opt, painter
|
||||
)
|
||||
|
||||
def mousePressEvent(self, event: QMouseEvent):
|
||||
self.opt.sliderPosition = self.first_position
|
||||
self._first_sc = self.style().hitTestComplexControl(
|
||||
QStyle.ComplexControl.CC_Slider, self.opt, event.pos(), self
|
||||
)
|
||||
|
||||
self.opt.sliderPosition = self.second_position
|
||||
self._second_sc = self.style().hitTestComplexControl(
|
||||
QStyle.ComplexControl.CC_Slider, self.opt, event.pos(), self
|
||||
)
|
||||
|
||||
def mouseMoveEvent(self, event: QMouseEvent):
|
||||
distance = self.opt.maximum - self.opt.minimum
|
||||
|
||||
pos = self.style().sliderValueFromPosition(
|
||||
0, distance, event.pos().x(), self.rect().width()
|
||||
)
|
||||
|
||||
if self._first_sc == QStyle.SubControl.SC_SliderHandle:
|
||||
if pos <= self.second_position:
|
||||
self.first_position = pos
|
||||
self.valueChanged.emit(self.first_position, self.second_position)
|
||||
self.update()
|
||||
return
|
||||
|
||||
if self._second_sc == QStyle.SubControl.SC_SliderHandle:
|
||||
if pos >= self.first_position:
|
||||
self.second_position = pos
|
||||
self.valueChanged.emit(self.first_position, self.second_position)
|
||||
self.update()
|
||||
|
||||
def sizeHint(self):
|
||||
"""override"""
|
||||
SliderLength = 84
|
||||
TickSpace = 5
|
||||
|
||||
w = SliderLength
|
||||
h = self.style().pixelMetric(
|
||||
QStyle.PixelMetric.PM_SliderThickness, self.opt, self
|
||||
)
|
||||
|
||||
if (
|
||||
self.opt.tickPosition & QSlider.TickPosition.TicksAbove
|
||||
or self.opt.tickPosition & QSlider.TickPosition.TicksBelow
|
||||
):
|
||||
h += TickSpace
|
||||
|
||||
return (
|
||||
self.style()
|
||||
.sizeFromContents(
|
||||
QStyle.ContentsType.CT_Slider, self.opt, QSize(w, h), self
|
||||
)
|
||||
.expandedTo(QApplication.globalStrut())
|
||||
)
|
||||
1
pyqt5/widgets/toggle/__init__.py
Normal file
1
pyqt5/widgets/toggle/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .toggle import Toggle, AnimatedToggle
|
||||
27
pyqt5/widgets/toggle/main.py
Normal file
27
pyqt5/widgets/toggle/main.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
|
||||
|
||||
from toggle import AnimatedToggle, Toggle
|
||||
|
||||
|
||||
class Window(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
toggle_1 = Toggle()
|
||||
toggle_2 = AnimatedToggle(
|
||||
checked_color="#FFB000", pulse_checked_color="#44FFB000"
|
||||
)
|
||||
|
||||
container = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(toggle_1)
|
||||
layout.addWidget(toggle_2)
|
||||
container.setLayout(layout)
|
||||
|
||||
self.setCentralWidget(container)
|
||||
|
||||
|
||||
app = QApplication([])
|
||||
w = Window()
|
||||
w.show()
|
||||
app.exec_()
|
||||
196
pyqt5/widgets/toggle/toggle.py
Normal file
196
pyqt5/widgets/toggle/toggle.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from PyQt5.QtCore import (
|
||||
QEasingCurve,
|
||||
QPoint,
|
||||
QPointF,
|
||||
QPropertyAnimation,
|
||||
QRectF,
|
||||
QSequentialAnimationGroup,
|
||||
QSize,
|
||||
Qt,
|
||||
pyqtProperty,
|
||||
pyqtSlot,
|
||||
)
|
||||
from PyQt5.QtGui import QBrush, QColor, QPainter, QPaintEvent, QPen
|
||||
from PyQt5.QtWidgets import QCheckBox
|
||||
|
||||
|
||||
class Toggle(QCheckBox):
|
||||
_transparent_pen = QPen(Qt.GlobalColor.transparent)
|
||||
_light_grey_pen = QPen(Qt.GlobalColor.lightGray)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
bar_color=Qt.GlobalColor.gray,
|
||||
checked_color="#00B0FF",
|
||||
handle_color=Qt.GlobalColor.white,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
# Save our properties on the object via self, so we can access them later
|
||||
# in the paintEvent.
|
||||
self._bar_brush = QBrush(bar_color)
|
||||
self._bar_checked_brush = QBrush(QColor(checked_color).lighter())
|
||||
|
||||
self._handle_brush = QBrush(handle_color)
|
||||
self._handle_checked_brush = QBrush(QColor(checked_color))
|
||||
|
||||
# Setup the rest of the widget.
|
||||
|
||||
self.setContentsMargins(8, 0, 8, 0)
|
||||
self._handle_position = 0
|
||||
|
||||
self.stateChanged.connect(self.handle_state_change)
|
||||
|
||||
def sizeHint(self):
|
||||
return QSize(58, 45)
|
||||
|
||||
def hitButton(self, pos: QPoint):
|
||||
return self.contentsRect().contains(pos)
|
||||
|
||||
def paintEvent(self, e: QPaintEvent):
|
||||
contRect = self.contentsRect()
|
||||
handleRadius = round(0.24 * contRect.height())
|
||||
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
p.setPen(self._transparent_pen)
|
||||
barRect = QRectF(
|
||||
0, 0, contRect.width() - handleRadius, 0.40 * contRect.height()
|
||||
)
|
||||
barRect.moveCenter(contRect.center())
|
||||
rounding = barRect.height() / 2
|
||||
|
||||
# the handle will move along this line
|
||||
trailLength = contRect.width() - 2 * handleRadius
|
||||
xPos = contRect.x() + handleRadius + trailLength * self._handle_position
|
||||
|
||||
if self.isChecked():
|
||||
p.setBrush(self._bar_checked_brush)
|
||||
p.drawRoundedRect(barRect, rounding, rounding)
|
||||
p.setBrush(self._handle_checked_brush)
|
||||
|
||||
else:
|
||||
p.setBrush(self._bar_brush)
|
||||
p.drawRoundedRect(barRect, rounding, rounding)
|
||||
p.setPen(self._light_grey_pen)
|
||||
p.setBrush(self._handle_brush)
|
||||
|
||||
p.drawEllipse(QPointF(xPos, barRect.center().y()), handleRadius, handleRadius)
|
||||
|
||||
p.end()
|
||||
|
||||
@pyqtSlot(int)
|
||||
def handle_state_change(self, value):
|
||||
self._handle_position = 1 if value else 0
|
||||
|
||||
@pyqtProperty(float)
|
||||
def handle_position(self):
|
||||
return self._handle_position
|
||||
|
||||
@handle_position.setter
|
||||
def handle_position(self, pos):
|
||||
"""change the property
|
||||
we need to trigger QWidget.update() method, either by:
|
||||
1- calling it here [ what we're doing ].
|
||||
2- connecting the QPropertyAnimation.valueChanged() signal to it.
|
||||
"""
|
||||
self._handle_position = pos
|
||||
self.update()
|
||||
|
||||
@pyqtProperty(float)
|
||||
def pulse_radius(self):
|
||||
return self._pulse_radius
|
||||
|
||||
@pulse_radius.setter
|
||||
def pulse_radius(self, pos):
|
||||
self._pulse_radius = pos
|
||||
self.update()
|
||||
|
||||
|
||||
class AnimatedToggle(Toggle):
|
||||
_transparent_pen = QPen(Qt.GlobalColor.transparent)
|
||||
_light_grey_pen = QPen(Qt.GlobalColor.lightGray)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
pulse_unchecked_color="#44999999",
|
||||
pulse_checked_color="#4400B0EE",
|
||||
**kwargs,
|
||||
):
|
||||
self._pulse_radius = 0
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.animation = QPropertyAnimation(self, b"handle_position", self)
|
||||
self.animation.setEasingCurve(QEasingCurve.Type.InOutCubic)
|
||||
self.animation.setDuration(200) # time in ms
|
||||
|
||||
self.pulse_anim = QPropertyAnimation(self, b"pulse_radius", self)
|
||||
self.pulse_anim.setDuration(350) # time in ms
|
||||
self.pulse_anim.setStartValue(10)
|
||||
self.pulse_anim.setEndValue(20)
|
||||
|
||||
self.animations_group = QSequentialAnimationGroup()
|
||||
self.animations_group.addAnimation(self.animation)
|
||||
self.animations_group.addAnimation(self.pulse_anim)
|
||||
|
||||
self._pulse_unchecked_animation = QBrush(QColor(pulse_unchecked_color))
|
||||
self._pulse_checked_animation = QBrush(QColor(pulse_checked_color))
|
||||
|
||||
@pyqtSlot(int)
|
||||
def handle_state_change(self, value):
|
||||
self.animations_group.stop()
|
||||
if value:
|
||||
self.animation.setEndValue(1)
|
||||
else:
|
||||
self.animation.setEndValue(0)
|
||||
self.animations_group.start()
|
||||
|
||||
def paintEvent(self, e: QPaintEvent):
|
||||
contRect = self.contentsRect()
|
||||
handleRadius = round(0.24 * contRect.height())
|
||||
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
p.setPen(self._transparent_pen)
|
||||
barRect = QRectF(
|
||||
0, 0, contRect.width() - handleRadius, 0.40 * contRect.height()
|
||||
)
|
||||
barRect.moveCenter(contRect.center())
|
||||
rounding = barRect.height() / 2
|
||||
|
||||
# the handle will move along this line
|
||||
trailLength = contRect.width() - 2 * handleRadius
|
||||
|
||||
xPos = contRect.x() + handleRadius + trailLength * self._handle_position
|
||||
|
||||
if self.pulse_anim.state() == QPropertyAnimation.State.Running:
|
||||
p.setBrush(
|
||||
self._pulse_checked_animation
|
||||
if self.isChecked()
|
||||
else self._pulse_unchecked_animation
|
||||
)
|
||||
p.drawEllipse(
|
||||
QPointF(xPos, barRect.center().y()),
|
||||
self._pulse_radius,
|
||||
self._pulse_radius,
|
||||
)
|
||||
|
||||
if self.isChecked():
|
||||
p.setBrush(self._bar_checked_brush)
|
||||
p.drawRoundedRect(barRect, rounding, rounding)
|
||||
p.setBrush(self._handle_checked_brush)
|
||||
|
||||
else:
|
||||
p.setBrush(self._bar_brush)
|
||||
p.drawRoundedRect(barRect, rounding, rounding)
|
||||
p.setPen(self._light_grey_pen)
|
||||
p.setBrush(self._handle_brush)
|
||||
|
||||
p.drawEllipse(QPointF(xPos, barRect.center().y()), handleRadius, handleRadius)
|
||||
|
||||
p.end()
|
||||
Reference in New Issue
Block a user