├── .gitignore
├── LICENSE
├── README.md
├── qtwidgets
├── __init__.py
├── colorbutton
│ ├── __init__.py
│ ├── colorbutton.py
│ └── demo.py
├── equalizer_bar
│ ├── __init__.py
│ ├── demo.py
│ └── equalizer_bar.py
├── gradient
│ ├── __init__.py
│ ├── demo.py
│ └── gradient.py
├── paint
│ ├── __init__.py
│ ├── demo.py
│ └── paint.py
├── palette
│ ├── __init__.py
│ ├── demo.py
│ └── palette.py
├── passwordedit
│ ├── __init__.py
│ ├── demo.py
│ ├── eye.svg
│ ├── hidden.svg
│ └── password.py
├── power_bar
│ ├── __init__.py
│ ├── demo.py
│ └── power_bar.py
├── rangeslider
│ ├── __init__.py
│ ├── demo_pyqt5.py
│ ├── demo_pyside2.py
│ └── rangeslider.py
└── toggle
│ ├── __init__.py
│ ├── demo.py
│ └── toggle.py
├── requirements.txt
├── setup.cfg
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io
2 |
3 | ### OSX ###
4 | .DS_Store
5 | .AppleDouble
6 | .LSOverride
7 |
8 | # Icon must end with two \r
9 | Icon
10 |
11 |
12 | # Thumbnails
13 | ._*
14 |
15 | # Files that might appear on external disk
16 | .Spotlight-V100
17 | .Trashes
18 |
19 | # Directories potentially created on remote AFP share
20 | .AppleDB
21 | .AppleDesktop
22 | Network Trash Folder
23 | Temporary Items
24 | .apdisk
25 |
26 |
27 | ### Python ###
28 | # Byte-compiled / optimized / DLL files
29 | __pycache__/
30 | *.py[cod]
31 |
32 | # C extensions
33 | *.so
34 |
35 | # Distribution / packaging
36 | .Python
37 | env/
38 | build/
39 | develop-eggs/
40 | dist/
41 | downloads/
42 | eggs/
43 | lib/
44 | lib64/
45 | parts/
46 | sdist/
47 | var/
48 | *.egg-info/
49 | .installed.cfg
50 | *.egg
51 |
52 | # PyInstaller
53 | # Usually these files are written by a python script from a template
54 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
55 | *.manifest
56 | *.spec
57 |
58 | # Installer logs
59 | pip-log.txt
60 | pip-delete-this-directory.txt
61 |
62 | # Unit test / coverage reports
63 | htmlcov/
64 | .tox/
65 | .coverage
66 | .cache
67 | nosetests.xml
68 | coverage.xml
69 |
70 | # Translations
71 | *.mo
72 | *.pot
73 |
74 | # Sphinx documentation
75 | docs/_build/
76 |
77 | # PyBuilder
78 | target/
79 |
80 |
81 | ### Django ###
82 | *.log
83 | *.pot
84 | *.pyc
85 | __pycache__/
86 | local_settings.py
87 |
88 | .env
89 | db.sqlite3
90 |
91 | static/**
92 |
93 | media/**
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Martin Fitzpatrick
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Custom Qt5 Python Widgets
2 |
3 | Qt5 comes with a huge number of widgets built-in, from simple text boxes to digital displays, vector graphics canvas and a full-blown web browser. While you can build perfectly functional applications with the built-in widgets, sometimes your applications will need a *more*.
4 |
5 | This repo contains a library of [custom Python Qt5 widgets](https://www.learnpyqt.com/widgets/) which are free to use in your own applications. Widgets are compatible with both PyQt5 and PySide2 (Qt for Python). Currently the repository includes -
6 |
7 | | Widgets | Library |
8 | | :---: | :---: |
9 | | 
**Graphical Equalizer**
Visualize audio frequency changes with configurable styles and decay
`from qtwidgets import EqualizerBar`
[Documentation](https://www.learnpyqt.com/widgets/equalizerbar/) | 
**Power Bar**
Rotary control with amplitude display
`from qtwidgets import PowerBar`
[Documentation](https://www.learnpyqt.com/courses/custom-widgets/creating-your-own-custom-widgets/) |
10 | | 
**Palette**
Select colours from a configurable linear or grid palette.
`from qtwidgets import PaletteHorizontal`
`from qtwidgets import PaletteGrid`
[Documentation](https://www.learnpyqt.com/widgets/palette/) | 
**Linear Gradient Editor**
Design custom linear gradients with multiple stops and colours.
`from qtwidgets import Gradient`
[Documentation](https://www.learnpyqt.com/widgets/gradient/)|
11 | | **Color Button**
Simple button that displays and selects colours.
`from qtwidgets import ColorButton` | **Paint**
Draw pictures with a custom bitmap canvas, with colour and pen control.
`from qtwidgets import Paint` |
12 | | **Password Edit**
A password line editor with toggleable visibility action.
`from qtwidgets import PasswordEdit` |
Replace checkboxes with this handy toggle widget, with custom colors and optional animations
`from qtwidgets import Toggle`
`from qtwidgets import AnimatedToggle`
[Documentation](https://www.learnpyqt.com/widgets/toggle/)|
13 |
14 | For a more detailed introduction to each widget and a walkthrough of their APIs
15 | see [the custom widget library on LearnPyQt](https://www.learnpyqt.com/widgets/).
16 |
17 | More custom widgets will follow, *if you have ideas just let me know!*
18 |
19 | **Licensed MIT/BSDv2** feel free to use in your own projects.
20 |
--------------------------------------------------------------------------------
/qtwidgets/__init__.py:
--------------------------------------------------------------------------------
1 | from .colorbutton import ColorButton
2 | # from color_duo
3 | # from equalizer
4 | from .equalizer_bar import EqualizerBar
5 | # from filebrowser
6 | from .gradient import Gradient
7 | from .paint import Paint
8 | from .passwordedit import PasswordEdit
9 | from .power_bar import PowerBar
10 | from .palette import PaletteGrid, PaletteHorizontal, PaletteVertical
11 | from .rangeslider import RangeSlider
12 | # from scrubber
13 | # from stopwatch
14 | from .toggle import Toggle, AnimatedToggle
--------------------------------------------------------------------------------
/qtwidgets/colorbutton/__init__.py:
--------------------------------------------------------------------------------
1 | from .colorbutton import ColorButton
--------------------------------------------------------------------------------
/qtwidgets/colorbutton/colorbutton.py:
--------------------------------------------------------------------------------
1 | import sys
2 | if 'PyQt5' in sys.modules:
3 | from PyQt5 import QtCore, QtGui, QtWidgets
4 | from PyQt5.QtCore import Qt, pyqtSignal as Signal
5 |
6 | else:
7 | from PySide2 import QtCore, QtGui, QtWidgets
8 | from PySide2.QtCore import Qt, Signal
9 |
10 |
11 |
12 | class ColorButton(QtWidgets.QPushButton):
13 | '''
14 | Custom Qt Widget to show a chosen color.
15 |
16 | Left-clicking the button shows the color-chooser, while
17 | right-clicking resets the color to the default color (None by default).
18 | '''
19 |
20 | colorChanged = Signal(object)
21 |
22 | def __init__(self, *args, color=None, **kwargs):
23 | super(ColorButton, self).__init__(*args, **kwargs)
24 |
25 | self._color = None
26 | self._default = color
27 | self.pressed.connect(self.onColorPicker)
28 |
29 | # Set the initial/default state.
30 | self.setColor(self._default)
31 |
32 | def setColor(self, color):
33 | if color != self._color:
34 | self._color = color
35 | self.colorChanged.emit(color)
36 |
37 | if self._color:
38 | self.setStyleSheet("background-color: %s;" % self._color)
39 | else:
40 | self.setStyleSheet("")
41 |
42 | def color(self):
43 | return self._color
44 |
45 | def onColorPicker(self):
46 | '''
47 | Show color-picker dialog to select color.
48 |
49 | Qt will use the native dialog by default.
50 |
51 | '''
52 | dlg = QtWidgets.QColorDialog(self)
53 | if self._color:
54 | dlg.setCurrentColor(QtGui.QColor(self._color))
55 |
56 | if dlg.exec_():
57 | self.setColor(dlg.currentColor().name())
58 |
59 | def mousePressEvent(self, e):
60 | if e.button() == Qt.RightButton:
61 | self.setColor(self._default)
62 |
63 | return super(ColorButton, self).mousePressEvent(e)
64 |
--------------------------------------------------------------------------------
/qtwidgets/colorbutton/demo.py:
--------------------------------------------------------------------------------
1 | from qtpy import QtWidgets
2 | from qtwidgets import ColorButton
3 |
4 |
5 | class Window(QtWidgets.QMainWindow):
6 |
7 | def __init__(self):
8 | super().__init__()
9 |
10 | palette = ColorButton(color='red')
11 | palette.colorChanged.connect(self.show_selected_color)
12 | self.setCentralWidget(palette)
13 |
14 | def show_selected_color(self, c):
15 | print("Selected: {}".format(c))
16 |
17 |
18 | app = QtWidgets.QApplication([])
19 | w = Window()
20 | w.show()
21 | app.exec_()
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/qtwidgets/equalizer_bar/__init__.py:
--------------------------------------------------------------------------------
1 | from .equalizer_bar import EqualizerBar
--------------------------------------------------------------------------------
/qtwidgets/equalizer_bar/demo.py:
--------------------------------------------------------------------------------
1 | from qtpy import QtCore, QtWidgets
2 | from qtwidgets import EqualizerBar
3 |
4 | import random
5 |
6 |
7 | class Window(QtWidgets.QMainWindow):
8 |
9 | def __init__(self):
10 | super().__init__()
11 |
12 | self.equalizer = EqualizerBar(5, ['#0C0786', '#40039C', '#6A00A7', '#8F0DA3', '#B02A8F', '#CA4678', '#E06461',
13 | '#F1824C', '#FCA635', '#FCCC25', '#EFF821'])
14 | self.equalizer.setBarSolidYPercent(0.4)
15 | #self.equalizer.setBarSolidXPercent(0.4)
16 | self.setCentralWidget(self.equalizer)
17 |
18 | self._timer = QtCore.QTimer()
19 | self._timer.setInterval(100)
20 | self._timer.timeout.connect(self.update_values)
21 | self._timer.start()
22 |
23 | def update_values(self):
24 | self.equalizer.setValues([
25 | min(100, v+random.randint(0, 50) if random.randint(0, 5) > 2 else v)
26 | for v in self.equalizer.values()
27 | ])
28 |
29 |
30 | app = QtWidgets.QApplication([])
31 | w = Window()
32 | w.show()
33 | app.exec_()
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/qtwidgets/equalizer_bar/equalizer_bar.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from qtpy import QtCore, QtGui, QtWidgets
4 | from qtpy.QtCore import Qt
5 | from qtpy.QtCore import Signal
6 |
7 |
8 | class EqualizerBar(QtWidgets.QWidget):
9 |
10 | def __init__(self, bars, steps, *args, **kwargs):
11 | super().__init__(*args, **kwargs)
12 |
13 | self.setSizePolicy(
14 | QtWidgets.QSizePolicy.MinimumExpanding,
15 | QtWidgets.QSizePolicy.MinimumExpanding
16 | )
17 |
18 | if isinstance(steps, list):
19 | # list of colours.
20 | self.n_steps = len(steps)
21 | self.steps = steps
22 |
23 | elif isinstance(steps, int):
24 | # int number of bars, defaults to red.
25 | self.n_steps = steps
26 | self.steps = ['red'] * steps
27 |
28 | else:
29 | raise TypeError('steps must be a list or int')
30 |
31 | # Bar appearance.
32 | self.n_bars = bars
33 | self._x_solid_percent = 0.8
34 | self._y_solid_percent = 0.8
35 | self._background_color = QtGui.QColor('black')
36 | self._padding = 25 # n-pixel gap around edge.
37 |
38 | # Bar behaviour
39 | self._timer = None
40 | self.setDecayFrequencyMs(100)
41 | self._decay = 10
42 |
43 | # Ranges
44 | self._vmin = 0
45 | self._vmax = 100
46 |
47 | # Current values are stored in a list.
48 | self._values = [0.0] * bars
49 |
50 |
51 | def paintEvent(self, e):
52 | painter = QtGui.QPainter(self)
53 |
54 | brush = QtGui.QBrush()
55 | brush.setColor(self._background_color)
56 | brush.setStyle(Qt.SolidPattern)
57 | rect = QtCore.QRect(0, 0, painter.device().width(), painter.device().height())
58 | painter.fillRect(rect, brush)
59 |
60 | # Define our canvas.
61 | d_height = painter.device().height() - (self._padding * 2)
62 | d_width = painter.device().width() - (self._padding * 2)
63 |
64 | # Draw the bars.
65 | step_y = d_height / self.n_steps
66 | bar_height = step_y * self._y_solid_percent
67 | bar_height_space = step_y * (1 - self._x_solid_percent) / 2
68 |
69 | step_x = d_width / self.n_bars
70 | bar_width = step_x * self._x_solid_percent
71 | bar_width_space = step_x * (1 - self._y_solid_percent) / 2
72 |
73 | for b in range(self.n_bars):
74 |
75 | # Calculate the y-stop position for this bar, from the value in range.
76 | pc = (self._values[b] - self._vmin) / (self._vmax - self._vmin)
77 | n_steps_to_draw = int(pc * self.n_steps)
78 |
79 | for n in range(n_steps_to_draw):
80 | brush.setColor(QtGui.QColor(self.steps[n]))
81 | rect = QtCore.QRect(
82 | self._padding + (step_x * b) + bar_width_space,
83 | self._padding + d_height - ((1 + n) * step_y) + bar_height_space,
84 | bar_width,
85 | bar_height
86 | )
87 | painter.fillRect(rect, brush)
88 |
89 | painter.end()
90 |
91 | def sizeHint(self):
92 | return QtCore.QSize(20, 120)
93 |
94 | def _trigger_refresh(self):
95 | self.update()
96 |
97 | def setDecay(self, f):
98 | self._decay = float(f)
99 |
100 | def setDecayFrequencyMs(self, ms):
101 | if self._timer:
102 | self._timer.stop()
103 |
104 | if ms:
105 | self._timer = QtCore.QTimer()
106 | self._timer.setInterval(ms)
107 | self._timer.timeout.connect(self._decay_beat)
108 | self._timer.start()
109 |
110 | def _decay_beat(self):
111 | self._values = [
112 | max(0, v - self._decay)
113 | for v in self._values
114 | ]
115 | self.update() # Redraw new position.
116 |
117 | def setValues(self, v):
118 | self._values = v
119 | self.update()
120 |
121 | def values(self):
122 | return self._values
123 |
124 | def setRange(self, vmin, vmax):
125 | assert float(vmin) < float(vmax)
126 | self._vmin, self._vmax = float(vmin), float(vmax)
127 |
128 | def setColor(self, color):
129 | self.steps = [color] * self._bar.n_steps
130 | self.update()
131 |
132 | def setColors(self, colors):
133 | self.n_steps = len(colors)
134 | self.steps = colors
135 | self.update()
136 |
137 | def setBarPadding(self, i):
138 | self._padding = int(i)
139 | self.update()
140 |
141 | def setBarSolidXPercent(self, f):
142 | self._x_solid_percent = float(f)
143 | self.update()
144 |
145 | def setBarSolidYPercent(self, f):
146 | self._y_solid_percent = float(f)
147 | self.update()
148 |
149 | def setBackgroundColor(self, color):
150 | self._background_color = QtGui.QColor(color)
151 | self.update()
152 |
153 |
--------------------------------------------------------------------------------
/qtwidgets/gradient/__init__.py:
--------------------------------------------------------------------------------
1 | from .gradient import Gradient
--------------------------------------------------------------------------------
/qtwidgets/gradient/demo.py:
--------------------------------------------------------------------------------
1 | from qtpy import QtCore, QtGui, QtWidgets
2 | from gradient import Gradient
3 |
4 |
5 | class Window(QtWidgets.QMainWindow):
6 |
7 | def __init__(self):
8 | super().__init__()
9 |
10 | gradient = Gradient()
11 | gradient.setGradient([(0, 'black'), (1, 'green'), (0.5, 'red')])
12 | self.setCentralWidget(gradient)
13 |
14 |
15 | app = QtWidgets.QApplication([])
16 | w = Window()
17 | w.show()
18 | app.exec_()
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/qtwidgets/gradient/gradient.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from qtpy import QtCore, QtGui, QtWidgets
4 | from qtpy.QtCore import Qt
5 | from qtpy.QtCore import Signal
6 |
7 |
8 | class Gradient(QtWidgets.QWidget):
9 |
10 | gradientChanged = Signal()
11 |
12 | def __init__(self, gradient=None, *args, **kwargs):
13 | super().__init__(*args, **kwargs)
14 |
15 | self.setSizePolicy(
16 | QtWidgets.QSizePolicy.MinimumExpanding,
17 | QtWidgets.QSizePolicy.MinimumExpanding
18 | )
19 |
20 | if gradient:
21 | self._gradient = gradient
22 |
23 | else:
24 | self._gradient = [
25 | (0.0, '#000000'),
26 | (1.0, '#ffffff'),
27 | ]
28 |
29 | # Stop point handle sizes.
30 | self._handle_w = 10
31 | self._handle_h = 10
32 |
33 | self._drag_position = None
34 |
35 | def paintEvent(self, e):
36 | painter = QtGui.QPainter(self)
37 | width = painter.device().width()
38 | height = painter.device().height()
39 |
40 | # Draw the linear horizontal gradient.
41 | gradient = QtGui.QLinearGradient(0, 0, width, 0)
42 | for stop, color in self._gradient:
43 | gradient.setColorAt(stop, QtGui.QColor(color))
44 |
45 | rect = QtCore.QRect(0, 0, width, height)
46 | painter.fillRect(rect, gradient)
47 |
48 | pen = QtGui.QPen()
49 |
50 | y = painter.device().height() / 2
51 |
52 |
53 | # Draw the stop handles.
54 | for stop, _ in self._gradient:
55 | pen.setColor(QtGui.QColor('white'))
56 | painter.setPen(pen)
57 |
58 | painter.drawLine(stop * width, y - self._handle_h, stop * width, y + self._handle_h)
59 |
60 | pen.setColor(QtGui.QColor('red'))
61 | painter.setPen(pen)
62 |
63 | rect = QtCore.QRect(
64 | stop * width - self._handle_w/2,
65 | y - self._handle_h/2,
66 | self._handle_w,
67 | self._handle_h
68 | )
69 | painter.drawRect(rect)
70 |
71 | painter.end()
72 |
73 | def sizeHint(self):
74 | return QtCore.QSize(200, 50)
75 |
76 | def _sort_gradient(self):
77 | self._gradient = sorted(self._gradient, key=lambda g:g[0])
78 |
79 | def _constrain_gradient(self):
80 | self._gradient = [
81 | # Ensure values within valid range.
82 | (max(0.0, min(1.0, stop)), color)
83 | for stop, color in self._gradient
84 | ]
85 |
86 | def setGradient(self, gradient):
87 | assert all([0.0 <= stop <= 1.0 for stop, _ in gradient])
88 | self._gradient = gradient
89 | self._constrain_gradient()
90 | self._sort_gradient()
91 | self.gradientChanged.emit()
92 |
93 | def gradient(self):
94 | return self._gradient
95 |
96 | @property
97 | def _end_stops(self):
98 | return [0, len(self._gradient)-1]
99 |
100 | def addStop(self, stop, color=None):
101 | # Stop is a value 0...1, find the point to insert this stop
102 | # in the list.
103 | assert 0.0 <= stop <= 1.0
104 |
105 | for n, g in enumerate(self._gradient):
106 | if g[0] > stop:
107 | # Insert before this entry, with specified or next color.
108 | self._gradient.insert(n, (stop, color or g[1]))
109 | break
110 | self._constrain_gradient()
111 | self.gradientChanged.emit()
112 | self.update()
113 |
114 | def removeStopAtPosition(self, n):
115 | if n not in self._end_stops:
116 | del self._gradient[n]
117 | self.gradientChanged.emit()
118 | self.update()
119 |
120 | def setColorAtPosition(self, n, color):
121 | if n < len(self._gradient):
122 | stop, _ = self._gradient[n]
123 | self._gradient[n] = stop, color
124 | self.gradientChanged.emit()
125 | self.update()
126 |
127 | def chooseColorAtPosition(self, n, current_color=None):
128 | dlg = QtWidgets.QColorDialog(self)
129 | if current_color:
130 | dlg.setCurrentColor(QtGui.QColor(current_color))
131 |
132 | if dlg.exec_():
133 | self.setColorAtPosition(n, dlg.currentColor().name())
134 |
135 | def _find_stop_handle_for_event(self, e, to_exclude=None):
136 | width = self.width()
137 | height = self.height()
138 | midpoint = height / 2
139 |
140 | # Are we inside a stop point? First check y.
141 | if (
142 | e.y() >= midpoint - self._handle_h and
143 | e.y() <= midpoint + self._handle_h
144 | ):
145 |
146 | for n, (stop, color) in enumerate(self._gradient):
147 | if to_exclude and n in to_exclude:
148 | # Allow us to skip the extreme ends of the gradient.
149 | continue
150 | if (
151 | e.x() >= stop * width - self._handle_w and
152 | e.x() <= stop * width + self._handle_w
153 | ):
154 | return n
155 |
156 | def mousePressEvent(self, e):
157 | # We're in this stop point.
158 | if e.button() == Qt.RightButton:
159 | n = self._find_stop_handle_for_event(e)
160 | if n is not None:
161 | _, color = self._gradient[n]
162 | self.chooseColorAtPosition(n, color)
163 |
164 | elif e.button() == Qt.LeftButton:
165 | n = self._find_stop_handle_for_event(e, to_exclude=self._end_stops)
166 | if n is not None:
167 | # Activate drag mode.
168 | self._drag_position = n
169 |
170 |
171 | def mouseReleaseEvent(self, e):
172 | self._drag_position = None
173 | self._sort_gradient()
174 |
175 | def mouseMoveEvent(self, e):
176 | # If drag active, move the stop.
177 | if self._drag_position:
178 | stop = e.x() / self.width()
179 | _, color = self._gradient[self._drag_position]
180 | self._gradient[self._drag_position] = stop, color
181 | self._constrain_gradient()
182 | self.update()
183 |
184 | def mouseDoubleClickEvent(self, e):
185 | # Calculate the position of the click relative 0..1 to the width.
186 | n = self._find_stop_handle_for_event(e)
187 | if n:
188 | self._sort_gradient() # Ensure ordered.
189 | # Delete existing, if not at the ends.
190 | if n > 0 and n < len(self._gradient) - 1:
191 | self.removeStopAtPosition(n)
192 |
193 | else:
194 | stop = e.x() / self.width()
195 | self.addStop(stop)
196 |
197 |
198 |
199 |
200 |
201 |
--------------------------------------------------------------------------------
/qtwidgets/paint/__init__.py:
--------------------------------------------------------------------------------
1 | from .paint import Paint
--------------------------------------------------------------------------------
/qtwidgets/paint/demo.py:
--------------------------------------------------------------------------------
1 | from qtpy import QtCore, QtGui, QtWidgets
2 | from qtwidgets import Paint
3 |
4 |
5 | class Window(QtWidgets.QMainWindow):
6 |
7 | def __init__(self):
8 | super().__init__()
9 |
10 | paint = Paint(300, 300)
11 | paint.setPenWidth(5)
12 | paint.setPenColor('#EB5160')
13 | self.setCentralWidget(paint)
14 |
15 |
16 | app = QtWidgets.QApplication([])
17 | w = Window()
18 | w.show()
19 | app.exec_()
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/qtwidgets/paint/paint.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from qtpy import QtCore, QtGui, QtWidgets
3 | from qtpy.QtCore import Qt
4 |
5 |
6 | class Paint(QtWidgets.QLabel):
7 |
8 | def __init__(self, width, height, background='white', *args, **kwargs):
9 | super().__init__(*args, **kwargs)
10 | pixmap = QtGui.QPixmap(width, height)
11 | self.setPixmap(pixmap)
12 |
13 | # Fill the canvas with the initial color.
14 | painter = QtGui.QPainter(self.pixmap())
15 | brush = QtGui.QBrush()
16 | brush.setColor(QtGui.QColor(background))
17 | brush.setStyle(Qt.SolidPattern)
18 | painter.fillRect(0, 0, pixmap.width(), pixmap.height(), brush)
19 | painter.end()
20 |
21 | self.last_x, self.last_y = None, None
22 | self._pen_color = QtGui.QColor('#000000')
23 | self._pen_width = 4
24 |
25 | def setPenColor(self, c):
26 | self._pen_color = QtGui.QColor(c)
27 |
28 | def setPenWidth(self, w):
29 | self._pen_width = int(w)
30 |
31 | def mouseMoveEvent(self, e):
32 | if self.last_x is None: # First event.
33 | self.last_x = e.x()
34 | self.last_y = e.y()
35 | return # Ignore the first time.
36 |
37 | painter = QtGui.QPainter(self.pixmap())
38 | p = painter.pen()
39 | p.setWidth(self._pen_width)
40 | p.setColor(self._pen_color)
41 | painter.setPen(p)
42 | painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
43 | painter.end()
44 | self.update()
45 |
46 | # Update the origin for next time.
47 | self.last_x = e.x()
48 | self.last_y = e.y()
49 |
50 | def mousePressEvent(self, e):
51 | if e.button() == Qt.RightButton:
52 | self._flood_fill_from_event(e)
53 |
54 | def mouseReleaseEvent(self, e):
55 | self.last_x = None
56 | self.last_y = None
57 |
58 | def _flood_fill_from_event(self, e):
59 |
60 | image = self.pixmap().toImage()
61 | w, h = image.width(), image.height()
62 | x, y = e.x(), e.y()
63 |
64 | # Get our target color from origin.
65 | target_color = image.pixel(x, y)
66 |
67 | have_seen = set()
68 | queue = [(x, y)]
69 |
70 | def get_cardinal_points(have_seen, center_pos):
71 | points = []
72 | cx, cy = center_pos
73 | for x, y in [(1, 0), (0, 1), (-1, 0), (0, -1)]:
74 | xx, yy = cx + x, cy + y
75 | if (xx >= 0 and xx < w and
76 | yy >= 0 and yy < h and
77 | (xx, yy) not in have_seen):
78 | points.append((xx, yy))
79 | have_seen.add((xx, yy))
80 |
81 | return points
82 |
83 | # Now perform the search and fill.
84 | p = QtGui.QPainter(self.pixmap())
85 | p.setPen(QtGui.QPen(self._pen_color))
86 |
87 | while queue:
88 | x, y = queue.pop()
89 | if image.pixel(x, y) == target_color:
90 | p.drawPoint(QtCore.QPoint(x, y))
91 | queue.extend(get_cardinal_points(have_seen, (x, y)))
92 |
93 | self.update()
94 |
--------------------------------------------------------------------------------
/qtwidgets/palette/__init__.py:
--------------------------------------------------------------------------------
1 | from .palette import PaletteGrid, PaletteHorizontal, PaletteVertical
--------------------------------------------------------------------------------
/qtwidgets/palette/demo.py:
--------------------------------------------------------------------------------
1 | from qtpy import QtCore, QtGui, QtWidgets
2 | from qtwidgets import PaletteGrid, PaletteHorizontal, PaletteVertical
3 |
4 |
5 | class Window(QtWidgets.QMainWindow):
6 |
7 | def __init__(self):
8 | super().__init__()
9 |
10 | palette = PaletteGrid('17undertones') # or PaletteHorizontal, or PaletteVertical
11 | palette.selected.connect(self.show_selected_color)
12 | self.setCentralWidget(palette)
13 |
14 | def show_selected_color(self, c):
15 | print("Selected: {}".format(c))
16 |
17 |
18 | app = QtWidgets.QApplication([])
19 | w = Window()
20 | w.show()
21 | app.exec_()
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/qtwidgets/palette/palette.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from qtpy import QtCore, QtWidgets
3 | from qtpy.QtCore import Signal
4 |
5 |
6 | PALETTES = {
7 | # bokeh paired 12
8 | 'paired12':['#000000', '#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00', '#cab2d6', '#6a3d9a', '#ffff99', '#b15928', '#ffffff'],
9 | # d3 category 10
10 | 'category10':['#000000', '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf', '#ffffff'],
11 | # 17 undertones https://lospec.com/palette-list/17undertones
12 | '17undertones': ['#000000', '#141923', '#414168', '#3a7fa7', '#35e3e3', '#8fd970', '#5ebb49', '#458352','#dcd37b', '#fffee5', '#ffd035', '#cc9245', '#a15c3e', '#a42f3b', '#f45b7a', '#c24998', '#81588d', '#bcb0c2', '#ffffff']
13 | }
14 |
15 |
16 | class _PaletteButton(QtWidgets.QPushButton):
17 | def __init__(self, color):
18 | super().__init__()
19 | self.setFixedSize(QtCore.QSize(24, 24))
20 | self.color = color
21 | self.setStyleSheet("background-color: %s;" % color)
22 |
23 | class _PaletteBase(QtWidgets.QWidget):
24 |
25 | selected = Signal(object)
26 |
27 | def _emit_color(self, color):
28 | self.selected.emit(color)
29 |
30 |
31 | class _PaletteLinearBase(_PaletteBase):
32 | def __init__(self, colors, *args, **kwargs):
33 | super().__init__(*args, **kwargs)
34 |
35 | if isinstance(colors, str):
36 | if colors in PALETTES:
37 | colors = PALETTES[colors]
38 |
39 | palette = self.layoutvh()
40 |
41 | for c in colors:
42 | b = _PaletteButton(c)
43 | b.pressed.connect(
44 | lambda c=c: self._emit_color(c)
45 | )
46 | palette.addWidget(b)
47 |
48 | self.setLayout(palette)
49 |
50 |
51 | class PaletteHorizontal(_PaletteLinearBase):
52 | layoutvh = QtWidgets.QHBoxLayout
53 |
54 |
55 | class PaletteVertical(_PaletteLinearBase):
56 | layoutvh = QtWidgets.QVBoxLayout
57 |
58 |
59 | class PaletteGrid(_PaletteBase):
60 |
61 | def __init__(self, colors, n_columns=5, *args, **kwargs):
62 | super().__init__(*args, **kwargs)
63 |
64 | if isinstance(colors, str):
65 | if colors in PALETTES:
66 | colors = PALETTES[colors]
67 |
68 | palette = QtWidgets.QGridLayout()
69 | row, col = 0, 0
70 |
71 | for c in colors:
72 | b = _PaletteButton(c)
73 | b.pressed.connect(
74 | lambda c=c: self._emit_color(c)
75 | )
76 | palette.addWidget(b, row, col)
77 | col += 1
78 | if col == n_columns:
79 | col = 0
80 | row += 1
81 |
82 | self.setLayout(palette)
83 |
--------------------------------------------------------------------------------
/qtwidgets/passwordedit/__init__.py:
--------------------------------------------------------------------------------
1 | from .password import PasswordEdit
--------------------------------------------------------------------------------
/qtwidgets/passwordedit/demo.py:
--------------------------------------------------------------------------------
1 | from qtpy import QtCore, QtGui, QtWidgets
2 | from qtwidgets import PasswordEdit
3 |
4 |
5 | class Window(QtWidgets.QMainWindow):
6 |
7 | def __init__(self):
8 | super().__init__()
9 |
10 | password = PasswordEdit()
11 | self.setCentralWidget(password)
12 |
13 |
14 | app = QtWidgets.QApplication([])
15 | w = Window()
16 | w.show()
17 | app.exec_()
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/qtwidgets/passwordedit/eye.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/qtwidgets/passwordedit/hidden.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
51 |
--------------------------------------------------------------------------------
/qtwidgets/passwordedit/password.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from qtpy import QtCore, QtGui, QtWidgets
5 | from qtpy.QtCore import Qt, Signal
6 |
7 | folder = os.path.dirname(__file__)
8 |
9 |
10 | class PasswordEdit(QtWidgets.QLineEdit):
11 | """
12 | Password LineEdit with icons to show/hide password entries.
13 | Based on this example https://kushaldas.in/posts/creating-password-input-widget-in-pyqt.html by Kushal Das.
14 | """
15 |
16 | def __init__(
17 | self,
18 | show_visibility=True,
19 | visible_icon=None,
20 | hidden_icon=None,
21 | icons_from_theme=False,
22 | *args,
23 | **kwargs
24 | ):
25 | super().__init__(*args, **kwargs)
26 |
27 | if icons_from_theme:
28 | self.visibleIcon = QtGui.QIcon.fromTheme("view-visible")
29 | self.hiddenIcon = QtGui.QIcon.fromTheme("view-hidden")
30 | else:
31 | if visible_icon:
32 | self.visibleIcon = visible_icon
33 | else:
34 | self.visibleIcon = QtGui.QIcon(os.path.join(folder, "eye.svg"))
35 | if hidden_icon:
36 | self.hiddenIcon = hidden_icon
37 | else:
38 | self.hiddenIcon = QtGui.QIcon(
39 | os.path.join(folder, "hidden.svg")
40 | )
41 |
42 | self.setEchoMode(QtWidgets.QLineEdit.Password)
43 |
44 | if show_visibility:
45 | # Add the password hide/shown toggle at the end of the edit box.
46 | self.togglepasswordAction = self.addAction(
47 | self.visibleIcon, QtWidgets.QLineEdit.TrailingPosition
48 | )
49 | self.togglepasswordAction.triggered.connect(
50 | self.on_toggle_password_Action
51 | )
52 |
53 | self.password_shown = False
54 |
55 | def on_toggle_password_Action(self):
56 | if not self.password_shown:
57 | self.setEchoMode(QtWidgets.QLineEdit.Normal)
58 | self.password_shown = True
59 | self.togglepasswordAction.setIcon(self.hiddenIcon)
60 | else:
61 | self.setEchoMode(QtWidgets.QLineEdit.Password)
62 | self.password_shown = False
63 | self.togglepasswordAction.setIcon(self.visibleIcon)
64 |
--------------------------------------------------------------------------------
/qtwidgets/power_bar/__init__.py:
--------------------------------------------------------------------------------
1 | from .power_bar import PowerBar
--------------------------------------------------------------------------------
/qtwidgets/power_bar/demo.py:
--------------------------------------------------------------------------------
1 | from qtpy import QtCore, QtGui, QtWidgets
2 | from qtwidgets import PowerBar
3 |
4 |
5 | app = QtWidgets.QApplication([])
6 | volume = PowerBar(["#053061", "#2166ac", "#4393c3", "#92c5de", "#d1e5f0", "#f7f7f7", "#fddbc7", "#f4a582", "#d6604d", "#b2182b", "#67001f"])
7 | volume.setBarSolidPercent(0.8)
8 | volume.setBarPadding(5)
9 | volume.show()
10 | app.exec_()
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/qtwidgets/power_bar/power_bar.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from qtpy import QtCore, QtGui, QtWidgets
4 | from qtpy.QtCore import Qt
5 | from qtpy.QtCore import Signal
6 |
7 |
8 | class _Bar(QtWidgets.QWidget):
9 |
10 | clickedValue = Signal(int)
11 |
12 | def __init__(self, steps, *args, **kwargs):
13 | super().__init__(*args, **kwargs)
14 |
15 | self.setSizePolicy(
16 | QtWidgets.QSizePolicy.MinimumExpanding,
17 | QtWidgets.QSizePolicy.MinimumExpanding
18 | )
19 |
20 | if isinstance(steps, list):
21 | # list of colours.
22 | self.n_steps = len(steps)
23 | self.steps = steps
24 |
25 | elif isinstance(steps, int):
26 | # int number of bars, defaults to red.
27 | self.n_steps = steps
28 | self.steps = ['red'] * steps
29 |
30 | else:
31 | raise TypeError('steps must be a list or int')
32 |
33 | self._bar_solid_percent = 0.8
34 | self._background_color = QtGui.QColor('black')
35 | self._padding = 4.0 # n-pixel gap around edge.
36 |
37 | def paintEvent(self, e):
38 | painter = QtGui.QPainter(self)
39 |
40 | brush = QtGui.QBrush()
41 | brush.setColor(self._background_color)
42 | brush.setStyle(Qt.SolidPattern)
43 | rect = QtCore.QRect(0, 0, painter.device().width(), painter.device().height())
44 | painter.fillRect(rect, brush)
45 |
46 | # Get current state.
47 | parent = self.parent()
48 | vmin, vmax = parent.minimum(), parent.maximum()
49 | value = parent.value()
50 |
51 | # Define our canvas.
52 | d_height = painter.device().height() - (self._padding * 2)
53 | d_width = painter.device().width() - (self._padding * 2)
54 |
55 | # Draw the bars.
56 | step_size = d_height / self.n_steps
57 | bar_height = step_size * self._bar_solid_percent
58 | bar_spacer = step_size * (1 - self._bar_solid_percent) / 2
59 |
60 | # Calculate the y-stop position, from the value in range.
61 | pc = (value - vmin) / (vmax - vmin)
62 | n_steps_to_draw = int(pc * self.n_steps)
63 |
64 | for n in range(n_steps_to_draw):
65 | brush.setColor(QtGui.QColor(self.steps[n]))
66 | rect = QtCore.QRect(
67 | self._padding,
68 | self._padding + d_height - ((1 + n) * step_size) + bar_spacer,
69 | d_width,
70 | bar_height
71 | )
72 | painter.fillRect(rect, brush)
73 |
74 | painter.end()
75 |
76 | def sizeHint(self):
77 | return QtCore.QSize(40, 120)
78 |
79 | def _trigger_refresh(self):
80 | self.update()
81 |
82 | def _calculate_clicked_value(self, e):
83 | parent = self.parent()
84 | vmin, vmax = parent.minimum(), parent.maximum()
85 | d_height = self.size().height() + (self._padding * 2)
86 | step_size = d_height / self.n_steps
87 | click_y = e.y() - self._padding - step_size / 2
88 |
89 | pc = (d_height - click_y) / d_height
90 | value = vmin + pc * (vmax - vmin)
91 | self.clickedValue.emit(value)
92 |
93 | def mouseMoveEvent(self, e):
94 | self._calculate_clicked_value(e)
95 |
96 | def mousePressEvent(self, e):
97 | self._calculate_clicked_value(e)
98 |
99 |
100 | class PowerBar(QtWidgets.QWidget):
101 | """
102 | Custom Qt Widget to show a power bar and dial.
103 | Demonstrating compound and custom-drawn widget.
104 |
105 | Left-clicking the button shows the color-chooser, while
106 | right-clicking resets the color to None (no-color).
107 | """
108 |
109 | def __init__(self, steps=5, *args, **kwargs):
110 | super().__init__(*args, **kwargs)
111 |
112 | layout = QtWidgets.QVBoxLayout()
113 | self._bar = _Bar(steps)
114 | layout.addWidget(self._bar)
115 |
116 | # Create the QDial widget and set up defaults.
117 | # - we provide accessors on this class to override.
118 | self._dial = QtWidgets.QDial()
119 | self._dial.setNotchesVisible(True)
120 | self._dial.setWrapping(False)
121 | self._dial.valueChanged.connect(self._bar._trigger_refresh)
122 |
123 | # Take feedback from click events on the meter.
124 | self._bar.clickedValue.connect(self._dial.setValue)
125 |
126 | layout.addWidget(self._dial)
127 | self.setLayout(layout)
128 |
129 | def __getattr__(self, name):
130 | if name in self.__dict__:
131 | return self[name]
132 |
133 | try:
134 | return getattr(self._dial, name)
135 | except AttributeError:
136 | raise AttributeError(
137 | "'{}' object has no attribute '{}'".format(self.__class__.__name__, name)
138 | )
139 |
140 | def setColor(self, color):
141 | self._bar.steps = [color] * self._bar.n_steps
142 | self._bar.update()
143 |
144 | def setColors(self, colors):
145 | self._bar.n_steps = len(colors)
146 | self._bar.steps = colors
147 | self._bar.update()
148 |
149 | def setBarPadding(self, i):
150 | self._bar._padding = int(i)
151 | self._bar.update()
152 |
153 | def setBarSolidPercent(self, f):
154 | self._bar._bar_solid_percent = float(f)
155 | self._bar.update()
156 |
157 | def setBackgroundColor(self, color):
158 | self._bar._background_color = QtGui.QColor(color)
159 | self._bar.update()
160 |
161 |
162 |
--------------------------------------------------------------------------------
/qtwidgets/rangeslider/__init__.py:
--------------------------------------------------------------------------------
1 | from .rangeslider import RangeSlider
2 |
--------------------------------------------------------------------------------
/qtwidgets/rangeslider/demo_pyqt5.py:
--------------------------------------------------------------------------------
1 | from PyQt5 import QtCore, QtGui, QtWidgets
2 | from qtwidgets import RangeSlider
3 |
4 | app = QtWidgets.QApplication([])
5 |
6 | slider = RangeSlider()
7 | slider.show()
8 |
9 | app.exec_()
10 |
--------------------------------------------------------------------------------
/qtwidgets/rangeslider/demo_pyside2.py:
--------------------------------------------------------------------------------
1 | from PySide2 import QtCore, QtGui, QtWidgets
2 | from qtwidgets import RangeSlider
3 |
4 |
5 | app = QtWidgets.QApplication([])
6 |
7 | slider = RangeSlider()
8 | slider.show()
9 |
10 | app.exec_()
11 |
--------------------------------------------------------------------------------
/qtwidgets/rangeslider/rangeslider.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | if "PyQt5" in sys.modules:
4 | from PyQt5 import QtCore, QtWidgets
5 | from PyQt5.QtCore import Qt, pyqtSignal as Signal
6 |
7 | else:
8 | from PySide2 import QtCore, QtWidgets, QtGui
9 | from PySide2.QtCore import Signal, Qt
10 |
11 |
12 | class RangeSlider(QtWidgets.QWidget):
13 | def __init__(self, parent=None):
14 | super().__init__(parent)
15 |
16 | self.first_position = 1
17 | self.second_position = 8
18 |
19 | self.opt = QtWidgets.QStyleOptionSlider()
20 | self.opt.minimum = 0
21 | self.opt.maximum = 10
22 |
23 | self.setTickPosition(QtWidgets.QSlider.TicksAbove)
24 | self.setTickInterval(1)
25 |
26 | self.setSizePolicy(
27 | QtWidgets.QSizePolicy(
28 | QtWidgets.QSizePolicy.Expanding,
29 | QtWidgets.QSizePolicy.Fixed,
30 | QtWidgets.QSizePolicy.Slider,
31 | )
32 | )
33 |
34 | def setRangeLimit(self, minimum: int, maximum: int):
35 | self.opt.minimum = minimum
36 | self.opt.maximum = maximum
37 |
38 | def setRange(self, start: int, end: int):
39 | self.first_position = start
40 | self.second_position = end
41 |
42 | def getRange(self):
43 | return (self.first_position, self.second_position)
44 |
45 | def setTickPosition(self, position: QtWidgets.QSlider.TickPosition):
46 | self.opt.tickPosition = position
47 |
48 | def setTickInterval(self, ti: int):
49 | self.opt.tickInterval = ti
50 |
51 | def paintEvent(self, event: QtGui.QPaintEvent):
52 |
53 | painter = QtGui.QPainter(self)
54 |
55 | # Draw rule
56 | self.opt.initFrom(self)
57 | self.opt.rect = self.rect()
58 | self.opt.sliderPosition = 0
59 | self.opt.subControls = (
60 | QtWidgets.QStyle.SC_SliderGroove | QtWidgets.QStyle.SC_SliderTickmarks
61 | )
62 |
63 | # Draw GROOVE
64 | self.style().drawComplexControl(QtWidgets.QStyle.CC_Slider, self.opt, painter)
65 |
66 | # Draw INTERVAL
67 |
68 | color = self.palette().color(QtGui.QPalette.Highlight)
69 | color.setAlpha(160)
70 | painter.setBrush(QtGui.QBrush(color))
71 | painter.setPen(Qt.NoPen)
72 |
73 | self.opt.sliderPosition = self.first_position
74 | x_left_handle = (
75 | self.style()
76 | .subControlRect(
77 | QtWidgets.QStyle.CC_Slider, self.opt, QtWidgets.QStyle.SC_SliderHandle
78 | )
79 | .right()
80 | )
81 |
82 | self.opt.sliderPosition = self.second_position
83 | x_right_handle = (
84 | self.style()
85 | .subControlRect(
86 | QtWidgets.QStyle.CC_Slider, self.opt, QtWidgets.QStyle.SC_SliderHandle
87 | )
88 | .left()
89 | )
90 |
91 | groove_rect = self.style().subControlRect(
92 | QtWidgets.QStyle.CC_Slider, self.opt, QtWidgets.QStyle.SC_SliderGroove
93 | )
94 |
95 | selection = QtCore.QRect(
96 | x_left_handle,
97 | groove_rect.y(),
98 | x_right_handle - x_left_handle,
99 | groove_rect.height(),
100 | ).adjusted(-1, 1, 1, -1)
101 |
102 | painter.drawRect(selection)
103 |
104 | # Draw first handle
105 |
106 | self.opt.subControls = QtWidgets.QStyle.SC_SliderHandle
107 | self.opt.sliderPosition = self.first_position
108 | self.style().drawComplexControl(QtWidgets.QStyle.CC_Slider, self.opt, painter)
109 |
110 | # Draw second handle
111 | self.opt.sliderPosition = self.second_position
112 | self.style().drawComplexControl(QtWidgets.QStyle.CC_Slider, self.opt, painter)
113 |
114 | def mousePressEvent(self, event: QtGui.QMouseEvent):
115 |
116 | self.opt.sliderPosition = self.first_position
117 | self._first_sc = self.style().hitTestComplexControl(
118 | QtWidgets.QStyle.CC_Slider, self.opt, event.pos(), self
119 | )
120 |
121 | self.opt.sliderPosition = self.second_position
122 | self._second_sc = self.style().hitTestComplexControl(
123 | QtWidgets.QStyle.CC_Slider, self.opt, event.pos(), self
124 | )
125 |
126 | def mouseMoveEvent(self, event: QtGui.QMouseEvent):
127 |
128 | distance = self.opt.maximum - self.opt.minimum
129 |
130 | pos = self.style().sliderValueFromPosition(
131 | 0, distance, event.pos().x(), self.rect().width()
132 | )
133 |
134 | if self._first_sc == QtWidgets.QStyle.SC_SliderHandle:
135 | if pos <= self.second_position:
136 | self.first_position = pos
137 | self.update()
138 | return
139 |
140 | if self._second_sc == QtWidgets.QStyle.SC_SliderHandle:
141 | if pos >= self.first_position:
142 | self.second_position = pos
143 | self.update()
144 |
145 | def sizeHint(self):
146 | """ override """
147 | SliderLength = 84
148 | TickSpace = 5
149 |
150 | w = SliderLength
151 | h = self.style().pixelMetric(
152 | QtWidgets.QStyle.PM_SliderThickness, self.opt, self
153 | )
154 |
155 | if (
156 | self.opt.tickPosition & QtWidgets.QSlider.TicksAbove
157 | or self.opt.tickPosition & QtWidgets.QSlider.TicksBelow
158 | ):
159 | h += TickSpace
160 |
161 | return (
162 | self.style()
163 | .sizeFromContents(
164 | QtWidgets.QStyle.CT_Slider, self.opt, QtCore.QSize(w, h), self
165 | )
166 | .expandedTo(QtWidgets.QApplication.globalStrut())
167 | )
168 |
--------------------------------------------------------------------------------
/qtwidgets/toggle/__init__.py:
--------------------------------------------------------------------------------
1 | from .toggle import Toggle, AnimatedToggle
2 |
--------------------------------------------------------------------------------
/qtwidgets/toggle/demo.py:
--------------------------------------------------------------------------------
1 | from qtpy import QtCore, QtGui, QtWidgets
2 | from toggle import Toggle, AnimatedToggle
3 |
4 |
5 | class Window(QtWidgets.QMainWindow):
6 |
7 | def __init__(self):
8 | super().__init__()
9 |
10 | toggle_1 = Toggle()
11 | toggle_2 = AnimatedToggle(
12 | checked_color="#FFB000",
13 | pulse_checked_color="#44FFB000"
14 | )
15 |
16 | container = QtWidgets.QWidget()
17 | layout = QtWidgets.QVBoxLayout()
18 | layout.addWidget(toggle_1)
19 | layout.addWidget(toggle_2)
20 | container.setLayout(layout)
21 |
22 | self.setCentralWidget(container)
23 |
24 |
25 | app = QtWidgets.QApplication([])
26 | w = Window()
27 | w.show()
28 | app.exec_()
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/qtwidgets/toggle/toggle.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from qtpy.QtCore import (
4 | Qt, QSize, QPoint, QPointF, QRectF,
5 | QEasingCurve, QPropertyAnimation, QSequentialAnimationGroup,
6 | Slot, Property)
7 |
8 | from qtpy.QtWidgets import QCheckBox
9 | from qtpy.QtGui import QColor, QBrush, QPaintEvent, QPen, QPainter
10 |
11 |
12 |
13 | class Toggle(QCheckBox):
14 |
15 | _transparent_pen = QPen(Qt.transparent)
16 | _light_grey_pen = QPen(Qt.lightGray)
17 |
18 | def __init__(self,
19 | parent=None,
20 | bar_color=Qt.gray,
21 | checked_color="#00B0FF",
22 | handle_color=Qt.white,
23 | ):
24 | super().__init__(parent)
25 |
26 | # Save our properties on the object via self, so we can access them later
27 | # in the paintEvent.
28 | self._bar_brush = QBrush(bar_color)
29 | self._bar_checked_brush = QBrush(QColor(checked_color).lighter())
30 |
31 | self._handle_brush = QBrush(handle_color)
32 | self._handle_checked_brush = QBrush(QColor(checked_color))
33 |
34 | # Setup the rest of the widget.
35 |
36 | self.setContentsMargins(8, 0, 8, 0)
37 | self._handle_position = 0
38 |
39 | self.stateChanged.connect(self.handle_state_change)
40 |
41 | def sizeHint(self):
42 | return QSize(58, 45)
43 |
44 | def hitButton(self, pos: QPoint):
45 | return self.contentsRect().contains(pos)
46 |
47 | def paintEvent(self, e: QPaintEvent):
48 |
49 | contRect = self.contentsRect()
50 | handleRadius = round(0.24 * contRect.height())
51 |
52 | p = QPainter(self)
53 | p.setRenderHint(QPainter.Antialiasing)
54 |
55 | p.setPen(self._transparent_pen)
56 | barRect = QRectF(
57 | 0, 0,
58 | contRect.width() - handleRadius, 0.40 * contRect.height()
59 | )
60 | barRect.moveCenter(contRect.center())
61 | rounding = barRect.height() / 2
62 |
63 | # the handle will move along this line
64 | trailLength = contRect.width() - 2 * handleRadius
65 | xPos = contRect.x() + handleRadius + trailLength * self._handle_position
66 |
67 | if self.isChecked():
68 | p.setBrush(self._bar_checked_brush)
69 | p.drawRoundedRect(barRect, rounding, rounding)
70 | p.setBrush(self._handle_checked_brush)
71 |
72 | else:
73 | p.setBrush(self._bar_brush)
74 | p.drawRoundedRect(barRect, rounding, rounding)
75 | p.setPen(self._light_grey_pen)
76 | p.setBrush(self._handle_brush)
77 |
78 | p.drawEllipse(
79 | QPointF(xPos, barRect.center().y()),
80 | handleRadius, handleRadius)
81 |
82 | p.end()
83 |
84 | @Slot(int)
85 | def handle_state_change(self, value):
86 | self._handle_position = 1 if value else 0
87 |
88 | @Property(float)
89 | def handle_position(self):
90 | return self._handle_position
91 |
92 | @handle_position.setter
93 | def handle_position(self, pos):
94 | """change the property
95 | we need to trigger QWidget.update() method, either by:
96 | 1- calling it here [ what we're doing ].
97 | 2- connecting the QPropertyAnimation.valueChanged() signal to it.
98 | """
99 | self._handle_position = pos
100 | self.update()
101 |
102 | @Property(float)
103 | def pulse_radius(self):
104 | return self._pulse_radius
105 |
106 | @pulse_radius.setter
107 | def pulse_radius(self, pos):
108 | self._pulse_radius = pos
109 | self.update()
110 |
111 |
112 |
113 | class AnimatedToggle(Toggle):
114 |
115 | _transparent_pen = QPen(Qt.transparent)
116 | _light_grey_pen = QPen(Qt.lightGray)
117 |
118 | def __init__(self, *args, pulse_unchecked_color="#44999999",
119 | pulse_checked_color="#4400B0EE", **kwargs):
120 |
121 | self._pulse_radius = 0
122 |
123 | super().__init__(*args, **kwargs)
124 |
125 | self.animation = QPropertyAnimation(self, b"handle_position", self)
126 | self.animation.setEasingCurve(QEasingCurve.InOutCubic)
127 | self.animation.setDuration(200) # time in ms
128 |
129 | self.pulse_anim = QPropertyAnimation(self, b"pulse_radius", self)
130 | self.pulse_anim.setDuration(350) # time in ms
131 | self.pulse_anim.setStartValue(10)
132 | self.pulse_anim.setEndValue(20)
133 |
134 | self.animations_group = QSequentialAnimationGroup()
135 | self.animations_group.addAnimation(self.animation)
136 | self.animations_group.addAnimation(self.pulse_anim)
137 |
138 | self._pulse_unchecked_animation = QBrush(QColor(pulse_unchecked_color))
139 | self._pulse_checked_animation = QBrush(QColor(pulse_checked_color))
140 |
141 |
142 |
143 | @Slot(int)
144 | def handle_state_change(self, value):
145 | self.animations_group.stop()
146 | if value:
147 | self.animation.setEndValue(1)
148 | else:
149 | self.animation.setEndValue(0)
150 | self.animations_group.start()
151 |
152 | def paintEvent(self, e: QPaintEvent):
153 |
154 | contRect = self.contentsRect()
155 | handleRadius = round(0.24 * contRect.height())
156 |
157 | p = QPainter(self)
158 | p.setRenderHint(QPainter.Antialiasing)
159 |
160 | p.setPen(self._transparent_pen)
161 | barRect = QRectF(
162 | 0, 0,
163 | contRect.width() - handleRadius, 0.40 * contRect.height()
164 | )
165 | barRect.moveCenter(contRect.center())
166 | rounding = barRect.height() / 2
167 |
168 | # the handle will move along this line
169 | trailLength = contRect.width() - 2 * handleRadius
170 |
171 | xPos = contRect.x() + handleRadius + trailLength * self._handle_position
172 |
173 | if self.pulse_anim.state() == QPropertyAnimation.Running:
174 | p.setBrush(
175 | self._pulse_checked_animation if
176 | self.isChecked() else self._pulse_unchecked_animation)
177 | p.drawEllipse(QPointF(xPos, barRect.center().y()),
178 | self._pulse_radius, self._pulse_radius)
179 |
180 | if self.isChecked():
181 | p.setBrush(self._bar_checked_brush)
182 | p.drawRoundedRect(barRect, rounding, rounding)
183 | p.setBrush(self._handle_checked_brush)
184 |
185 | else:
186 | p.setBrush(self._bar_brush)
187 | p.drawRoundedRect(barRect, rounding, rounding)
188 | p.setPen(self._light_grey_pen)
189 | p.setBrush(self._handle_brush)
190 |
191 | p.drawEllipse(
192 | QPointF(xPos, barRect.center().y()),
193 | handleRadius, handleRadius)
194 |
195 | p.end()
196 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | qtpy
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 |
4 |
5 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | version = '1.2'
4 |
5 | with open("README.md", "r") as fh:
6 | long_description = fh.read()
7 |
8 | setup(
9 | name='qtwidgets',
10 | version=version,
11 | author='Martin Fitzpatrick',
12 | author_email='martin.fitzpatrick@gmail.com',
13 | description='Custom widget library for PyQt6, PyQt5, PySide6 and PySide2 (Qt for Python). Free to use in your own applications.',
14 | long_description=long_description,
15 | long_description_content_type="text/markdown",
16 | url='http://github.com/learnpyqt/python-qtwidgets',
17 | license='MIT',
18 | packages=find_packages(),
19 | install_requires=[
20 | 'markdown',
21 | ],
22 | include_package_data=True,
23 | classifiers=[
24 | 'Development Status :: 2 - Pre-Alpha',
25 | 'Intended Audience :: Developers',
26 | 'Topic :: Desktop Environment',
27 | 'Topic :: Software Development :: Build Tools',
28 | 'Topic :: Software Development :: Widget Sets',
29 | 'Programming Language :: Python :: 2.7',
30 | 'Programming Language :: Python :: 3.4'
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------