I have a QSlider that I want to move to the position of the mouse cursor when the user presses the left mouse button. I've been hunting around and couldn't find anything that was recent and solved my problem.
This is the slider I have. I want to be able to click to have the slider jump to the position where the mouse clicks. I can drag the slider, but I want to be able to click. I tested out clicking on the slider in the Dolphin file manager. It incremented rather than jumping to the exact position of the mouse.
Looking at the Qt5 documentation
QSlider has very few of its own functions [...]
This would indicate that there is no built-in way to do this. Is there no way to get where the mouse clicked and move the slider to that point?
The solution is to make a calculation of the position and set it in the mousePressEvent, the calculation is not easy as an arithmetic calculation since it depends on the style of each OS and the stylesheet so we must use QStyle as shown below:
from PyQt5 import QtCore, QtWidgets
class Slider(QtWidgets.QSlider):
def mousePressEvent(self, event):
super(Slider, self).mousePressEvent(event)
if event.button() == QtCore.Qt.LeftButton:
val = self.pixelPosToRangeValue(event.pos())
self.setValue(val)
def pixelPosToRangeValue(self, pos):
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
gr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderGroove, self)
sr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt, QtWidgets.QStyle.SC_SliderHandle, self)
if self.orientation() == QtCore.Qt.Horizontal:
sliderLength = sr.width()
sliderMin = gr.x()
sliderMax = gr.right() - sliderLength + 1
else:
sliderLength = sr.height()
sliderMin = gr.y()
sliderMax = gr.bottom() - sliderLength + 1;
pr = pos - sr.center() + sr.topLeft()
p = pr.x() if self.orientation() == QtCore.Qt.Horizontal else pr.y()
return QtWidgets.QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), p - sliderMin,
sliderMax - sliderMin, opt.upsideDown)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QWidget()
flay = QtWidgets.QFormLayout(w)
w1 = QtWidgets.QSlider(QtCore.Qt.Horizontal)
w2 = Slider(QtCore.Qt.Horizontal)
flay.addRow("default: ", w1)
flay.addRow("modified: ", w2)
w.show()
sys.exit(app.exec_())
QSlider doesn't have such feature so the only way to implepent this is write custom widget and override mouse click in it:
class Slider(QSlider):
def mousePressEvent(self, e):
if e.button() == Qt.LeftButton:
e.accept()
x = e.pos().x()
value = (self.maximum() - self.minimum()) * x / self.width() + self.minimum()
self.setValue(value)
else:
return super().mousePressEvent(self, e)
Note that this code will work for horizontal slider only.
I believe I have a much less involved solution:
from PyQt5.QtWidgets import QSlider
class ClickSlider(QSlider):
"""A slider with a signal that emits its position when it is pressed. Created to get around the slider only updating when the handle is dragged, but not when a new position is clicked"""
sliderPressedWithValue = QSignal(int)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sliderPressed.connect(self.on_slider_pressed)
def on_slider_pressed(self):
"""emits a more descriptive signal when pressed (with slider value during the press event)"""
self.sliderPressedWithValue.emit(self.value())
And then just make sure to connect to whatever you're updating like this:
# example if you're updating a QMediaPlayer object
from PyQt5.QtMultimedia import QMediaPlayer
player = QMediaPlayer()
slider = ClickSlider()
slider.sliderPressedWithValue.connect(player.setPosition) # updates on click
slider.sliderMoved.connect(player.setPosition) # updates on drag
Try this code.
class MySliderStyle : public QProxyStyle
{
public:
virtual int styleHint(StyleHint hint, const QStyleOption * option = 0, const QWidget * widget = 0, QStyleHintReturn * returnData = 0) const{
if (hint == QStyle::SH_Slider_AbsoluteSetButtons)
{
return Qt::LeftButton;
}
else
{
return QProxyStyle::styleHint(hint, option, widget, returnData);
}
}
};
ui->mySlider->setStyle(new MySliderStyle);
Related
I want to display a loading screen every time a user presses a button (a process that takes a few seconds runs).
I want something like this
QSplashScreen does not help me because that is only used before opening the application and a QDialog is not useful for me because I want that by dragging the window the application will move along with the message Loading...
What do I have to use?
The only (safe) way to achieve this is to add a child widget without adding it to any layout manager.
The only things you have to care about is that the widget is always raised as soon as it's shown, and that the geometry is always updated to the parent widget (or, better, the top level window).
This is a slightly more advanced example, but it has the benefit that you can just subclass any widget adding the LoadingWidget class to the base classes in order to implement a loading mechanism.
from random import randrange
from PyQt5 import QtCore, QtGui, QtWidgets
class Loader(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
self.gradient = QtGui.QConicalGradient(.5, .5, 0)
self.gradient.setCoordinateMode(self.gradient.ObjectBoundingMode)
self.gradient.setColorAt(.25, QtCore.Qt.transparent)
self.gradient.setColorAt(.75, QtCore.Qt.transparent)
self.animation = QtCore.QVariantAnimation(
startValue=0., endValue=1.,
duration=1000, loopCount=-1,
valueChanged=self.updateGradient
)
self.stopTimer = QtCore.QTimer(singleShot=True, timeout=self.stop)
self.focusWidget = None
self.hide()
parent.installEventFilter(self)
def start(self, timeout=None):
self.show()
self.raise_()
self.focusWidget = QtWidgets.QApplication.focusWidget()
self.setFocus()
if timeout:
self.stopTimer.start(timeout)
else:
self.stopTimer.setInterval(0)
def stop(self):
self.hide()
self.stopTimer.stop()
if self.focusWidget:
self.focusWidget.setFocus()
self.focusWidget = None
def updateGradient(self, value):
self.gradient.setAngle(-value * 360)
self.update()
def eventFilter(self, source, event):
# ensure that we always cover the whole parent area
if event.type() == QtCore.QEvent.Resize:
self.setGeometry(source.rect())
return super().eventFilter(source, event)
def showEvent(self, event):
self.setGeometry(self.parent().rect())
self.animation.start()
def hideEvent(self, event):
# stop the animation when hidden, just for performance
self.animation.stop()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setRenderHints(qp.Antialiasing)
color = self.palette().window().color()
color.setAlpha(max(color.alpha() * .5, 128))
qp.fillRect(self.rect(), color)
text = 'Loading...'
interval = self.stopTimer.interval()
if interval:
remaining = int(max(0, interval - self.stopTimer.remainingTime()) / interval * 100)
textWidth = self.fontMetrics().width(text + ' 000%')
text += ' {}%'.format(remaining)
else:
textWidth = self.fontMetrics().width(text)
textHeight = self.fontMetrics().height()
# ensure that there's enough space for the text
if textWidth > self.width() or textHeight * 3 > self.height():
drawText = False
size = max(0, min(self.width(), self.height()) - textHeight * 2)
else:
size = size = min(self.height() / 3, max(textWidth, textHeight))
drawText = True
circleRect = QtCore.QRect(0, 0, size, size)
circleRect.moveCenter(self.rect().center())
if drawText:
# text is going to be drawn, move the circle rect higher
circleRect.moveTop(circleRect.top() - textHeight)
middle = circleRect.center().x()
qp.drawText(
middle - textWidth / 2, circleRect.bottom() + textHeight,
textWidth, textHeight,
QtCore.Qt.AlignCenter, text)
self.gradient.setColorAt(.5, self.palette().windowText().color())
qp.setPen(QtGui.QPen(self.gradient, textHeight))
qp.drawEllipse(circleRect)
class LoadingExtension(object):
# a base class to extend any QWidget subclass's top level window with a loader
def startLoading(self, timeout=0):
window = self.window()
if not hasattr(window, '_loader'):
window._loader = Loader(window)
window._loader.start(timeout)
# this is just for testing purposes
if not timeout:
QtCore.QTimer.singleShot(randrange(1000, 5000), window._loader.stop)
def loadingFinished(self):
if hasattr(self.window(), '_loader'):
self.window()._loader.stop()
class Test(QtWidgets.QWidget, LoadingExtension):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
# just a test widget
textEdit = QtWidgets.QTextEdit()
layout.addWidget(textEdit, 0, 0, 1, 2)
textEdit.setMinimumHeight(20)
layout.addWidget(QtWidgets.QLabel('Timeout:'))
self.timeoutSpin = QtWidgets.QSpinBox(maximum=5000, singleStep=250, specialValueText='Random')
layout.addWidget(self.timeoutSpin, 1, 1)
self.timeoutSpin.setValue(2000)
btn = QtWidgets.QPushButton('Start loading...')
layout.addWidget(btn, 2, 0, 1, 2)
btn.clicked.connect(lambda: self.startLoading(self.timeoutSpin.value()))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(app.exec_())
Please check Qt::WindowFlags. The Qt::SplashScreen flag will give you splash screen experience without usage QSplashScreen (you can use it with all widget as show) or, better, use QDialog with this flag.
For moving, probably fine solution is not available but you can just use parent moveEvent to emmit signal. For example:
Main window:
moveEvent -> signal moved
Dialog:
signal move -> re-center window.
Its look as not hard.
By the way, I think block all GUI during application run is not the best solution. You you think use QProgressBar?
You can use this slot: void QWidget::raise().
Raises this widget to the top of the parent widget's stack.
After this call the widget will be visually in front of any overlapping sibling widgets.
Can somebody please provide an explanation, or better yet a short example, of how to use the RubberBandDrag enum value in QGraphicsView? PyQt5 would be great, but I can translate from the C++ version if that is preferred for whomever can provide a helpful exmaple.
NoDrag and ScrollHandDrag are relatively easy to understand (NoDrag makes the mouse a pointer and you can capture clicks at certain locations, ScrollHandDrag makes the mouse a hand and you can implement click and drag to scroll around), but I'm unclear on what RubberBandDrag can be used for.
Before somebody says "go read the docs", here is the information provided
https://doc.qt.io/qt-5/qgraphicsview.html
enum QGraphicsView::DragMode
QGraphicsView::RubberBandDrag
A rubber band will appear. Dragging the mouse will set the rubber band
geometry, and all items covered by the rubber band are selected. This
mode is disabled for non-interactive views.
This is clear but I'm not sure how I could actually use RubberBandDrag. Is there a way to use this to drag points of a QPolygon around after initial placement? That would be really useful.
The QGraphicsView::RubberBandDrag flag only serves to activate the internal QRubberBand:
And the QRubberBand in general only aims to visualize a selected area and in the case of QGraphicsView select the items below that area if they are selectable(enable QGraphicsItem::ItemIsSelectable flag).
According to your last question: Is there a way to use this to drag points of a QPolygon around after initial placement? That would be really useful, it seems to me that you have an XY problem since it seems that the use of drag in the terminology makes you think that it serves to drag elements, because no, that drag refers to the way of creating the rubber band.
In a few moments I will show how to implement the drag of the vertices to modify the QPolygon.
The following shows how to modify the position of the vertices by dragging the mouse:
import math
from PyQt5 import QtCore, QtGui, QtWidgets
class GripItem(QtWidgets.QGraphicsPathItem):
circle = QtGui.QPainterPath()
circle.addEllipse(QtCore.QRectF(-10, -10, 20, 20))
square = QtGui.QPainterPath()
square.addRect(QtCore.QRectF(-15, -15, 30, 30))
def __init__(self, annotation_item, index):
super(GripItem, self).__init__()
self.m_annotation_item = annotation_item
self.m_index = index
self.setPath(GripItem.circle)
self.setBrush(QtGui.QColor("green"))
self.setPen(QtGui.QPen(QtGui.QColor("green"), 2))
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
self.setAcceptHoverEvents(True)
self.setZValue(11)
self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
def hoverEnterEvent(self, event):
self.setPath(GripItem.square)
self.setBrush(QtGui.QColor("red"))
super(GripItem, self).hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self.setPath(GripItem.circle)
self.setBrush(QtGui.QColor("green"))
super(GripItem, self).hoverLeaveEvent(event)
def mouseReleaseEvent(self, event):
self.setSelected(False)
super(GripItem, self).mouseReleaseEvent(event)
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isEnabled():
self.m_annotation_item.movePoint(self.m_index, value)
return super(GripItem, self).itemChange(change, value)
class PolygonAnnotation(QtWidgets.QGraphicsPolygonItem):
def __init__(self, parent=None):
super(PolygonAnnotation, self).__init__(parent)
self.m_points = []
self.setZValue(10)
self.setPen(QtGui.QPen(QtGui.QColor("green"), 2))
self.setAcceptHoverEvents(True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
self.m_items = []
def addPoint(self, p):
self.m_points.append(p)
self.setPolygon(QtGui.QPolygonF(self.m_points))
item = GripItem(self, len(self.m_points) - 1)
self.scene().addItem(item)
self.m_items.append(item)
item.setPos(p)
def movePoint(self, i, p):
if 0 <= i < len(self.m_points):
self.m_points[i] = self.mapFromScene(p)
self.setPolygon(QtGui.QPolygonF(self.m_points))
def move_item(self, index, pos):
if 0 <= index < len(self.m_items):
item = self.m_items[index]
item.setEnabled(False)
item.setPos(pos)
item.setEnabled(True)
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
for i, point in enumerate(self.m_points):
self.move_item(i, self.mapToScene(point))
return super(PolygonAnnotation, self).itemChange(change, value)
def hoverEnterEvent(self, event):
self.setBrush(QtGui.QColor(255, 0, 0, 100))
super(PolygonAnnotation, self).hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
super(PolygonAnnotation, self).hoverLeaveEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
scene = QtWidgets.QGraphicsScene()
w = QtWidgets.QGraphicsView(scene)
polygon_item = PolygonAnnotation()
scene.addItem(polygon_item)
r = 100
sides = 10
for i in range(sides):
angle = 2 * math.pi * i / sides
x = r * math.cos(angle)
y = r * math.sin(angle)
p = QtCore.QPointF(x, y) + QtCore.QPointF(200, 200)
polygon_item.addPoint(p)
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
I'm working on an opensource markdown supported minimal note taking application for Windows/Linux. I'm trying to remove the title bar and add my own buttons. I want something like, a title bar with only two custom buttons as shown in the figure
Currently I have this:
I've tried modifying the window flags:
With not window flags, the window is both re-sizable and movable. But no custom buttons.
Using self.setWindowFlags(QtCore.Qt.FramelessWindowHint), the window has no borders, but cant move or resize the window
Using self.setWindowFlags(QtCore.Qt.CustomizeWindowHint), the window is resizable but cannot move and also cant get rid of the white part at the top of the window.
Any help appreciated. You can find the project on GitHub here.
Thanks..
This is my python code:
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets, uic
import sys
import os
import markdown2 # https://github.com/trentm/python-markdown2
from PyQt5.QtCore import QRect
from PyQt5.QtGui import QFont
simpleUiForm = uic.loadUiType("Simple.ui")[0]
class SimpleWindow(QtWidgets.QMainWindow, simpleUiForm):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
self.markdown = markdown2.Markdown()
self.css = open(os.path.join("css", "default.css")).read()
self.editNote.setPlainText("")
#self.noteView = QtWebEngineWidgets.QWebEngineView(self)
self.installEventFilter(self)
self.displayNote.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
#self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.WindowActivate:
print("widget window has gained focus")
self.editNote.show()
self.displayNote.hide()
elif event.type() == QtCore.QEvent.WindowDeactivate:
print("widget window has lost focus")
note = self.editNote.toPlainText()
htmlNote = self.getStyledPage(note)
# print(note)
self.editNote.hide()
self.displayNote.show()
# print(htmlNote)
self.displayNote.setHtml(htmlNote)
elif event.type() == QtCore.QEvent.FocusIn:
print("widget has gained keyboard focus")
elif event.type() == QtCore.QEvent.FocusOut:
print("widget has lost keyboard focus")
return False
The UI file is created in the following hierarchy
Here are the steps you just gotta follow:
Have your MainWindow, be it a QMainWindow, or QWidget, or whatever [widget] you want to inherit.
Set its flag, self.setWindowFlags(Qt.FramelessWindowHint)
Implement your own moving around.
Implement your own buttons (close, max, min)
Implement your own resize.
Here is a small example with move around, and buttons implemented. You should still have to implement the resize using the same logic.
import sys
from PyQt5.QtCore import QPoint
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.layout = QVBoxLayout()
self.layout.addWidget(MyBar(self))
self.setLayout(self.layout)
self.layout.setContentsMargins(0,0,0,0)
self.layout.addStretch(-1)
self.setMinimumSize(800,400)
self.setWindowFlags(Qt.FramelessWindowHint)
self.pressing = False
class MyBar(QWidget):
def __init__(self, parent):
super(MyBar, self).__init__()
self.parent = parent
print(self.parent.width())
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.title = QLabel("My Own Bar")
btn_size = 35
self.btn_close = QPushButton("x")
self.btn_close.clicked.connect(self.btn_close_clicked)
self.btn_close.setFixedSize(btn_size,btn_size)
self.btn_close.setStyleSheet("background-color: red;")
self.btn_min = QPushButton("-")
self.btn_min.clicked.connect(self.btn_min_clicked)
self.btn_min.setFixedSize(btn_size, btn_size)
self.btn_min.setStyleSheet("background-color: gray;")
self.btn_max = QPushButton("+")
self.btn_max.clicked.connect(self.btn_max_clicked)
self.btn_max.setFixedSize(btn_size, btn_size)
self.btn_max.setStyleSheet("background-color: gray;")
self.title.setFixedHeight(35)
self.title.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.title)
self.layout.addWidget(self.btn_min)
self.layout.addWidget(self.btn_max)
self.layout.addWidget(self.btn_close)
self.title.setStyleSheet("""
background-color: black;
color: white;
""")
self.setLayout(self.layout)
self.start = QPoint(0, 0)
self.pressing = False
def resizeEvent(self, QResizeEvent):
super(MyBar, self).resizeEvent(QResizeEvent)
self.title.setFixedWidth(self.parent.width())
def mousePressEvent(self, event):
self.start = self.mapToGlobal(event.pos())
self.pressing = True
def mouseMoveEvent(self, event):
if self.pressing:
self.end = self.mapToGlobal(event.pos())
self.movement = self.end-self.start
self.parent.setGeometry(self.mapToGlobal(self.movement).x(),
self.mapToGlobal(self.movement).y(),
self.parent.width(),
self.parent.height())
self.start = self.end
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
def btn_close_clicked(self):
self.parent.close()
def btn_max_clicked(self):
self.parent.showMaximized()
def btn_min_clicked(self):
self.parent.showMinimized()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
Here are some tips:
Option 1:
Have a QGridLayout with widget in each corner and side(e.g. left, top-left, menubar, top-right, right, bottom-right, bottom and bottom left)
With the approach (1) you would know when you are clicking in each border, you just got to define each one size and add each one on their place.
When you click on each one treat them in their respective ways, for example, if you click in the left one and drag to the left, you gotta resize it larger and at the same time move it to the left so it will appear to be stopped at the right place and grow width.
Apply this reasoning to each edge, each one behaving in the way it has to.
Option 2:
Instead of having a QGridLayout you can detect in which place you are clicking by the click pos.
Verify if the x of the click is smaller than the x of the moving pos to know if it's moving left or right and where it's being clicked.
The calculation is made in the same way of the Option1
Option 3:
Probably there are other ways, but those are the ones I just thought of. For example using the CustomizeWindowHint you said you are able to resize, so you just would have to implement what I gave you as example. BEAUTIFUL!
Tips:
Be careful with the localPos(inside own widget), globalPos(related to your screen). For example: If you click in the very left of your left widget its 'x' will be zero, if you click in the very left of the middle(content)it will be also zero, although if you mapToGlobal you will having different values according to the pos of the screen.
Pay attention when resizing, or moving, when you have to add width or subtract, or just move, or both, I'd recommend you to draw on a paper and figure out how the logic of resizing works before implementing it out of blue.
GOOD LUCK :D
While the accepted answer can be considered valid, it has some issues.
using setGeometry() is not appropriate (and the reason for using it was wrong) since it doesn't consider possible frame margins set by the style;
the position computation is unnecessarily complex;
resizing the title bar to the total width is wrong, since it doesn't consider the buttons and can also cause recursion problems in certain situations (like not setting the minimum size of the main window); also, if the title is too big, it makes impossible to resize the main window;
buttons should not accept focus;
setting a layout creates a restraint for the "main widget" or layout, so the title should not be added, but the contents margins of the widget should be used instead;
I revised the code to provide a better base for the main window, simplify the moving code, and add other features like the Qt windowTitle() property support, standard QStyle icons for buttons (instead of text), and proper maximize/normal button icons. Note that the title label is not added to the layout.
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.titleBar = MyBar(self)
self.setContentsMargins(0, self.titleBar.height(), 0, 0)
self.resize(640, self.titleBar.height() + 480)
def changeEvent(self, event):
if event.type() == event.WindowStateChange:
self.titleBar.windowStateChanged(self.windowState())
def resizeEvent(self, event):
self.titleBar.resize(self.width(), self.titleBar.height())
class MyBar(QWidget):
clickPos = None
def __init__(self, parent):
super(MyBar, self).__init__(parent)
self.setAutoFillBackground(True)
self.setBackgroundRole(QPalette.Shadow)
# alternatively:
# palette = self.palette()
# palette.setColor(palette.Window, Qt.black)
# palette.setColor(palette.WindowText, Qt.white)
# self.setPalette(palette)
layout = QHBoxLayout(self)
layout.setContentsMargins(1, 1, 1, 1)
layout.addStretch()
self.title = QLabel("My Own Bar", self, alignment=Qt.AlignCenter)
# if setPalette() was used above, this is not required
self.title.setForegroundRole(QPalette.Light)
style = self.style()
ref_size = self.fontMetrics().height()
ref_size += style.pixelMetric(style.PM_ButtonMargin) * 2
self.setMaximumHeight(ref_size + 2)
btn_size = QSize(ref_size, ref_size)
for target in ('min', 'normal', 'max', 'close'):
btn = QToolButton(self, focusPolicy=Qt.NoFocus)
layout.addWidget(btn)
btn.setFixedSize(btn_size)
iconType = getattr(style,
'SP_TitleBar{}Button'.format(target.capitalize()))
btn.setIcon(style.standardIcon(iconType))
if target == 'close':
colorNormal = 'red'
colorHover = 'orangered'
else:
colorNormal = 'palette(mid)'
colorHover = 'palette(light)'
btn.setStyleSheet('''
QToolButton {{
background-color: {};
}}
QToolButton:hover {{
background-color: {}
}}
'''.format(colorNormal, colorHover))
signal = getattr(self, target + 'Clicked')
btn.clicked.connect(signal)
setattr(self, target + 'Button', btn)
self.normalButton.hide()
self.updateTitle(parent.windowTitle())
parent.windowTitleChanged.connect(self.updateTitle)
def updateTitle(self, title=None):
if title is None:
title = self.window().windowTitle()
width = self.title.width()
width -= self.style().pixelMetric(QStyle.PM_LayoutHorizontalSpacing) * 2
self.title.setText(self.fontMetrics().elidedText(
title, Qt.ElideRight, width))
def windowStateChanged(self, state):
self.normalButton.setVisible(state == Qt.WindowMaximized)
self.maxButton.setVisible(state != Qt.WindowMaximized)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.clickPos = event.windowPos().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
self.window().move(event.globalPos() - self.clickPos)
def mouseReleaseEvent(self, QMouseEvent):
self.clickPos = None
def closeClicked(self):
self.window().close()
def maxClicked(self):
self.window().showMaximized()
def normalClicked(self):
self.window().showNormal()
def minClicked(self):
self.window().showMinimized()
def resizeEvent(self, event):
self.title.resize(self.minButton.x(), self.height())
self.updateTitle()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
layout = QVBoxLayout(mw)
widget = QTextEdit()
layout.addWidget(widget)
mw.show()
mw.setWindowTitle('My custom window with a very, very long title')
sys.exit(app.exec_())
This is for the people who are going to implement custom title bar in PyQt6 or PySide6
The below changes should be done in the answer given by #musicamante
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
# self.clickPos = event.windowPos().toPoint()
self.clickPos = event.scenePosition().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
# self.window().move(event.globalPos() - self.clickPos)
self.window().move(event.globalPosition().toPoint() - self.clickPos)
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
# sys.exit(app.exec_())
sys.exit(app.exec())
References:
QMouseEvent.globalPosition(),
QMouseEvent.scenePosition()
This method of moving Windows with Custom Widget doesn't work with WAYLAND. If anybody has a solution for that please post it here for future reference
Working functions for WAYLAND and PyQT6/PySide6 :
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._move()
return super().mousePressEvent(event)
def _move(self):
window = self.window().windowHandle()
window.startSystemMove()
Please check.
I am currently making a program where a user selects an image qpushbutton. I already have superseded mouseMoveEvent, mousePressEvent, and mouseReleaseEvent in the button class to get a movable button. The buttons are currently moving independently, but I would like the buttons to move so that the horizontal distance between them stays the same.
So currently in pseudo code I have:
import stuff
import mvbutton as mv
class MasterClass(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
#more setup stuff, layout, etc
self.addbutton(image,name,size)
def addbutton(#args):
self.button=mv.dragbutton(#args)
#some more setup
#now rename so that each button has its own name
if name== "name1":
self.name1=self.button
else:
self.name2=self.button
self.button=""
#more code to set up
I supersede the mouse motion/press/release functions in the dragbutton class. I cannot, therefore reference the new self.name# there. So the self.move(pos) in my dragbutton class cannot get the self.name# because it is a different self. Any ideas on how I could get this to work? Thanks.
Done something very rough after trying to understand your requirement.
Hope this helps.
EDIT
tried to add more accuracy in moving. Won't do real time moving cause it has problems with lag and update. I guess the moving won't be jittery any more.
from PyQt4 import QtGui
import sys
class MultiButton(QtGui.QWidget):
def __init__(self, *args, **kwargs):
QtGui.QWidget.__init__(self, *args, **kwargs)
self._b1 = QtGui.QPushButton("B1")
self._b2 = QtGui.QPushButton("B2")
self._arrangeWidgets()
self.setStyleSheet("background-color: rgb(0, 0, 0);\n"+\
"color: rgb(255, 255, 255);\n"+\
"border:1px solid #7F462C ;\n")
self._moveStart = False
self._startX = 0
self._startY = 0
def _arrangeWidgets(self):
layout = QtGui.QHBoxLayout()
layout.addWidget(self._b1)
#horizontal spacing remains constant now
layout.addSpacing(90)
layout.addWidget(self._b2)
self.setLayout(layout)
def mousePressEvent(self,event):
self._moveStart = True
self._startX = event.pos().x() - self.pos().x()
self._startY = event.pos().y() - self.pos().y()
return QtGui.QWidget.mousePressEvent(self, event)
def mouseReleaseEvent(self, event):
if self._moveStart:
self.setGeometry(event.pos().x() - self._startX,event.pos().y() - self._startY,self.width(),self.height())
self._moveStart = False
self._startX = 0
self._startY = 0
return QtGui.QWidget.mouseReleaseEvent(self, event)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
wd = QtGui.QMainWindow()
wd.resize(500,500)
mb = MultiButton()
mb.setFixedSize(200,50)
wd.setCentralWidget(mb)
wd.show()
sys.exit(app.exec_())
here the MultiButton widget moves the two buttons keeping the horizontal space between the two always constant.
How can I make it so when a user clicks on, the up or down arrow of a QSpinBox, the value will increase as the cursor is dragging up and the value will decrease if dragging down. I fond this function very useful for users to be able to just click and drag their cursor than to constantly click the errors. Here is reference source code for a spinner made in C# which works the way i would like it to in python. http://www.paulneale.com/tutorials/dotNet/numericUpDown/numericUpDown.htm
import sys
from PySide import QtGui, QtCore
class Wrap_Spinner( QtGui.QSpinBox ):
def __init__( self, minVal=0, maxVal=100, default=0):
super( Wrap_Spinner, self ).__init__()
self.drag_origin = None
self.setRange( minVal, maxVal )
self.setValue( default)
def get_is_dragging( self ):
# are we the widget that is also the active mouseGrabber?
return self.mouseGrabber( ) == self
### Dragging Handling Methods ################################################
def do_drag_start( self ):
# Record position
# Grab mouse
self.drag_origin = QtGui.QCursor( ).pos( )
self.grabMouse( )
def do_drag_update( self ):
# Transpose the motion into values as a delta off of the recorded click position
curPos = QtGui.QCursor( ).pos( )
offsetVal = self.drag_origin.y( ) - curPos.y( )
self.setValue( offsetVal )
print offsetVal
def do_drag_end( self ):
self.releaseMouse( )
# Restore position
# Reset drag origin value
self.drag_origin = None
### Mouse Override Methods ################################################
def mousePressEvent( self, event ):
if QtCore.Qt.LeftButton:
print 'start drag'
self.do_drag_start( )
elif self.get_is_dragging( ) and QtCore.Qt.RightButton:
# Cancel the drag
self.do_drag_end( )
else:
super( Wrap_Spinner, self ).mouseReleaseEvent( event )
def mouseMoveEvent( self, event ):
if self.get_is_dragging( ):
self.do_drag_update( )
else:
super( Wrap_Spinner, self ).mouseReleaseEvent( event )
def mouseReleaseEvent( self, event ):
if self.get_is_dragging( ) and QtCore.Qt.LeftButton:
print 'finish drag'
self.do_drag_end( )
else:
super( Wrap_Spinner, self ).mouseReleaseEvent( event )
class Example(QtGui.QWidget ):
def __init__( self):
super( Example, self ).__init__( )
self.initUI( )
def initUI( self ):
self.spinFrameCountA = Wrap_Spinner( 2, 50, 40)
self.spinB = Wrap_Spinner( 0, 100, 10)
self.positionLabel = QtGui.QLabel( 'POS:' )
grid = QtGui.QGridLayout( )
grid.setSpacing( 0 )
grid.addWidget( self.spinFrameCountA, 0, 0, 1, 1 )
grid.addWidget( self.spinB, 1, 0, 1, 1 )
grid.addWidget( self.positionLabel, 2, 0, 1, 1 )
self.setLayout( grid )
self.setGeometry( 800, 400, 200, 150 )
self.setWindowTitle( 'Max Style Spinner' )
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
self.show( )
def main( ):
app = QtGui.QApplication( sys.argv )
ex = Example( )
sys.exit( app.exec_( ) )
if __name__ == '__main__':
main()
I'm a big fan of your plugins so I'm happy I can answer this one for you! I assume you are coding a Max plug-in in pyside, because that's exactly what I was doing when I ran into the same problem (I like the Max default "scrubby" spinners too).
The solution is actually pretty simple, you just have to do it manually. I subclassed the QSpinBox and captured the mouse event, using it to calculate the y position relative to when you first start clicking on the widget. Here's the code, this is pyside2 because as of 3DS Max and Maya 2018 that's what Autodesk is using:
from PySide2 import QtWidgets, QtGui, QtCore
import MaxPlus
class SampleUI(QtWidgets.QDialog):
def __init__(self, parent=MaxPlus.GetQMaxMainWindow()):
super(SampleUI, self).__init__(parent)
self.setWindowTitle("Max-style spinner")
self.initUI()
MaxPlus.CUI.DisableAccelerators()
def initUI(self):
mainLayout = QtWidgets.QHBoxLayout()
lbl1 = QtWidgets.QLabel("Test Spinner:")
self.spinner = SuperSpinner(self)
#self.spinner = QtWidgets.QSpinBox() -- here's the old version
self.spinner.setMaximum(99999)
mainLayout.addWidget(lbl1)
mainLayout.addWidget(self.spinner)
self.setLayout(mainLayout)
def closeEvent(self, e):
MaxPlus.CUI.EnableAccelerators()
class SuperSpinner(QtWidgets.QSpinBox):
def __init__(self, parent):
super(SuperSpinner, self).__init__(parent)
self.mouseStartPosY = 0
self.startValue = 0
def mousePressEvent(self, e):
super(SuperSpinner, self).mousePressEvent(e)
self.mouseStartPosY = e.pos().y()
self.startValue = self.value()
def mouseMoveEvent(self, e):
self.setCursor(QtCore.Qt.SizeVerCursor)
multiplier = .5
valueOffset = int((self.mouseStartPosY - e.pos().y()) * multiplier)
print valueOffset
self.setValue(self.startValue + valueOffset)
def mouseReleaseEvent(self, e):
super(SuperSpinner, self).mouseReleaseEvent(e)
self.unsetCursor()
if __name__ == "__main__":
try:
ui.close()
except:
pass
ui = SampleUI()
ui.show()
I ran into the same issue and unfortunately the solutions I found only work when you click and drag from the arrows or the spinbox's border. But most users would want to drag from the actual text field, so doing this wasn't intuitive.
Instead you can subclass a QLineEdit to get the proper behavior. When you click it, it'll save its current value so that when the user drags it gets the mouse position's delta and applies that back onto the spinbox.
Here's a full example I'm using myself. Sorry though, it's in Maya's attribute style instead of Max's, so you click and drag the middle-mouse button to set the value. With some tweaking you can easily get it to work exactly like Max's:
from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtWidgets
class CustomSpinBox(QtWidgets.QLineEdit):
"""
Tries to mimic behavior from Maya's internal slider that's found in the channel box.
"""
IntSpinBox = 0
DoubleSpinBox = 1
def __init__(self, spinbox_type, value=0, parent=None):
super(CustomSpinBox, self).__init__(parent)
self.setToolTip(
"Hold and drag middle mouse button to adjust the value\n"
"(Hold CTRL or SHIFT change rate)")
if spinbox_type == CustomSpinBox.IntSpinBox:
self.setValidator(QtGui.QIntValidator(parent=self))
else:
self.setValidator(QtGui.QDoubleValidator(parent=self))
self.spinbox_type = spinbox_type
self.min = None
self.max = None
self.steps = 1
self.value_at_press = None
self.pos_at_press = None
self.setValue(value)
def wheelEvent(self, event):
super(CustomSpinBox, self).wheelEvent(event)
steps_mult = self.getStepsMultiplier(event)
if event.delta() > 0:
self.setValue(self.value() + self.steps * steps_mult)
else:
self.setValue(self.value() - self.steps * steps_mult)
def mousePressEvent(self, event):
if event.buttons() == QtCore.Qt.MiddleButton:
self.value_at_press = self.value()
self.pos_at_press = event.pos()
self.setCursor(QtGui.QCursor(QtCore.Qt.SizeHorCursor))
else:
super(CustomSpinBox, self).mousePressEvent(event)
self.selectAll()
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton:
self.value_at_press = None
self.pos_at_press = None
self.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
return
super(CustomSpinBox, self).mouseReleaseEvent(event)
def mouseMoveEvent(self, event):
if event.buttons() != QtCore.Qt.MiddleButton:
return
if self.pos_at_press is None:
return
steps_mult = self.getStepsMultiplier(event)
delta = event.pos().x() - self.pos_at_press.x()
delta /= 6 # Make movement less sensitive.
delta *= self.steps * steps_mult
value = self.value_at_press + delta
self.setValue(value)
super(CustomSpinBox, self).mouseMoveEvent(event)
def getStepsMultiplier(self, event):
steps_mult = 1
if event.modifiers() == QtCore.Qt.CTRL:
steps_mult = 10
elif event.modifiers() == QtCore.Qt.SHIFT:
steps_mult = 0.1
return steps_mult
def setMinimum(self, value):
self.min = value
def setMaximum(self, value):
self.max = value
def setSteps(self, steps):
if self.spinbox_type == CustomSpinBox.IntSpinBox:
self.steps = max(steps, 1)
else:
self.steps = steps
def value(self):
if self.spinbox_type == CustomSpinBox.IntSpinBox:
return int(self.text())
else:
return float(self.text())
def setValue(self, value):
if self.min is not None:
value = max(value, self.min)
if self.max is not None:
value = min(value, self.max)
if self.spinbox_type == CustomSpinBox.IntSpinBox:
self.setText(str(int(value)))
else:
self.setText(str(float(value)))
class MyTool(QtWidgets.QWidget):
"""
Example of how to use the spinbox.
"""
def __init__(self, parent=None):
super(MyTool, self).__init__(parent)
self.setWindowTitle("Custom spinboxes")
self.resize(300, 150)
self.int_spinbox = CustomSpinBox(CustomSpinBox.IntSpinBox, parent=self)
self.int_spinbox.setMinimum(-50)
self.int_spinbox.setMaximum(100)
self.float_spinbox = CustomSpinBox(CustomSpinBox.DoubleSpinBox, parent=self)
self.float_spinbox.setSteps(0.1)
self.main_layout = QtWidgets.QVBoxLayout()
self.main_layout.addWidget(self.int_spinbox)
self.main_layout.addWidget(self.float_spinbox)
self.setLayout(self.main_layout)
# Run the tool.
global tool_instance
tool_instance = MyTool()
tool_instance.show()
I tried to make the functions match Qt's native spinBox. I didn't need it in my case, but it would be easy to add a signal when the value changes on release. It would also be easy to take it to the next level like Houdini's sliders so that the steps rate can change depending on where the mouse is vertically. Bah, maybe for a rainy day though :).
Here's what this features right now:
Can do both integer or double spinboxes
Click then drag middle-mouse button to set the value
While dragging, hold ctrl to increase the rate or hold shift to slow the rate
You can still type in the value like normal
You can also change the value by scrolling the mouse wheel (holding ctrl and shift changes rate)
This is old, but is still a top hit on Google.
I found a few possibilities online, but none were ideal. My solution was top create a new type of label that 'scrubs' a QSpinBox or QDoubleSpinBox when dragged. Here you go:
////////////////////////////////////////////////////////////////////////////////
// Label for a QSpinBox or QDoubleSpinBox (or derivatives) that scrubs the spinbox value on click-drag
//
// Notes:
// - Cursor is hidden and cursor position remains fixed during the drag
// - Holding 'Ctrl' reduces the speed of the scrub
// - Scrub multipliers are currently hardcoded - may want to make that a parameter in the future
template <typename SpinBoxT, typename ValueT>
class SpinBoxLabel : public QLabel
{
public:
SpinBoxLabel(const QString& labelText, SpinBoxT& buddy)
: QLabel(labelText)
, Buddy(&buddy)
{
setBuddy(&buddy);
}
protected:
virtual void mouseMoveEvent(QMouseEvent* event) override {
if (!(event->buttons() & Qt::LeftButton))
return QLabel::mouseMoveEvent(event);
if (!IsDragging) {
StartDragPos = QCursor::pos();
Value = double(Buddy->value());
IsDragging = true;
QApplication::setOverrideCursor(Qt::BlankCursor);
}
else {
int dragDist = QCursor::pos().x() - StartDragPos.x();
if (dragDist == 0)
return;
double dragMultiplier = .25 * Buddy->singleStep();
if (!(event->modifiers() & Qt::ControlModifier))
dragMultiplier *= 10.0;
Value += dragMultiplier * dragDist;
Buddy->setValue(ValueT(Value));
QCursor::setPos(StartDragPos);
}
}
virtual void mouseReleaseEvent(QMouseEvent* event) override {
if (!IsDragging || event->button() != Qt::LeftButton)
return QLabel::mouseReleaseEvent(event);
IsDragging = false;
QApplication::restoreOverrideCursor();
}
private:
SpinBoxT* Buddy;
bool IsDragging = false;
QPoint StartDragPos;
double Value = 0.0;
};
typedef SpinBoxLabel<QDoubleSpinBox, double> DoubleSpinBoxLabel;
typedef SpinBoxLabel<QSpinBox, int> IntSpinBoxLabel;
The speed of the spinbox increment can be changed with QAbstractSpinBox.setAccelerated:
self.spinFrameCountA.setAccelerated(True)
With this enabled, the spinbox value will change more quickly the longer the mouse button is held down.