Let's consider this little snippet:
import sys
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5.QtWidgets import QMenu
from PyQt5.QtGui import QKeySequence
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCursor
def create_action(parent, text, slot=None,
shortcut=None, shortcuts=None, shortcut_context=None,
icon=None, tooltip=None,
checkable=False, checked=False):
action = QtWidgets.QAction(text, parent)
if icon is not None:
action.setIcon(QIcon(':/%s.png' % icon))
if shortcut is not None:
action.setShortcut(shortcut)
if shortcuts is not None:
action.setShortcuts(shortcuts)
if shortcut_context is not None:
action.setShortcutContext(shortcut_context)
if tooltip is not None:
action.setToolTip(tooltip)
action.setStatusTip(tooltip)
if checkable:
action.setCheckable(True)
if checked:
action.setChecked(True)
if slot is not None:
action.triggered.connect(slot)
return action
class Settings():
WIDTH = 20
HEIGHT = 15
NUM_BLOCKS_X = 10
NUM_BLOCKS_Y = 14
class QS(QtWidgets.QGraphicsScene):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
width = Settings.NUM_BLOCKS_X * Settings.WIDTH
height = Settings.NUM_BLOCKS_Y * Settings.HEIGHT
self.setSceneRect(0, 0, width, height)
self.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)
class QV(QtWidgets.QGraphicsView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.view_menu = QMenu(self)
self.create_actions()
def create_actions(self):
act = create_action(self.view_menu, "Zoom in",
slot=self.on_zoom_in,
shortcut=QKeySequence("+"), shortcut_context=Qt.WidgetShortcut)
self.view_menu.addAction(act)
act = create_action(self.view_menu, "Zoom out",
slot=self.on_zoom_out,
shortcut=QKeySequence("-"), shortcut_context=Qt.WidgetShortcut)
self.view_menu.addAction(act)
self.addActions(self.view_menu.actions())
def on_zoom_in(self):
if not self.scene():
return
self.scale(1.5, 1.5)
def on_zoom_out(self):
if not self.scene():
return
self.scale(1.0 / 1.5, 1.0 / 1.5)
def drawBackground(self, painter, rect):
gr = rect.toRect()
start_x = gr.left() + Settings.WIDTH - (gr.left() % Settings.WIDTH)
start_y = gr.top() + Settings.HEIGHT - (gr.top() % Settings.HEIGHT)
painter.save()
painter.setPen(QtGui.QColor(60, 70, 80).lighter(90))
painter.setOpacity(0.7)
for x in range(start_x, gr.right(), Settings.WIDTH):
painter.drawLine(x, gr.top(), x, gr.bottom())
for y in range(start_y, gr.bottom(), Settings.HEIGHT):
painter.drawLine(gr.left(), y, gr.right(), y)
painter.restore()
super().drawBackground(painter, rect)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
a = QS()
b = QV()
b.setScene(a)
# b.resize(800,600)
b.show()
sys.exit(app.exec_())
If we run it we can see the number block of grids is ok, as specified it's 8x10:
Now, let's say i set NUM_BLOCKS_X=3 and NUM_BLOCKS_Y=2, the output will be this one:
That's wrong! I definitely don't want that, I'd like the QGraphicsView to be shrinked properly to the grid's settings I've specified.
1st Question: How can i achieve that?
Another thing I'd like to know is, let's consider the posted snippet where the grid is 10x8 and then let's resize the QGraphicsWidget to 800x600, the output will be:
But I'd like to know how i can draw only the QGraphicsScene region. Right now I'm using rect in drawBackground.
So my 2nd question is: How can I draw the grid only inside QGraphicsScene's region?
One of the main problems appears when I zoom out, in that case I'd like to see only the size of the QGraphicsScene, because I'll be adding items only on that region, let's call it "drawable" region. As you can see, right now it's drawing grid lines on "non-drawable" region and that's really confusing
I would draw the grid in the Scene like this:
class QS(QtWidgets.QGraphicsScene):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
width = Settings.NUM_BLOCKS_X * Settings.WIDTH
height = Settings.NUM_BLOCKS_Y * Settings.HEIGHT
self.setSceneRect(0, 0, width, height)
self.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)
for x in range(0,Settings.NUM_BLOCKS_X+1):
xc = x * Settings.WIDTH
self.addLine(xc,0,xc,height)
for y in range(0,Settings.NUM_BLOCKS_Y+1):
yc = y * Settings.HEIGHT
self.addLine(0,yc,width,yc)
EDIT:
Additional visibility/opacity functionality:
class QS(QtWidgets.QGraphicsScene):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.lines = []
self.draw_grid()
self.set_opacity(0.3)
#self.set_visible(False)
#self.delete_grid()
def draw_grid(self):
width = Settings.NUM_BLOCKS_X * Settings.WIDTH
height = Settings.NUM_BLOCKS_Y * Settings.HEIGHT
self.setSceneRect(0, 0, width, height)
self.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)
pen = QPen(QColor(255,0,100), 1, Qt.SolidLine)
for x in range(0,Settings.NUM_BLOCKS_X+1):
xc = x * Settings.WIDTH
self.lines.append(self.addLine(xc,0,xc,height,pen))
for y in range(0,Settings.NUM_BLOCKS_Y+1):
yc = y * Settings.HEIGHT
self.lines.append(self.addLine(0,yc,width,yc,pen))
def set_visible(self,visible=True):
for line in self.lines:
line.setVisible(visible)
def delete_grid(self):
for line in self.lines:
self.removeItem(line)
del self.lines[:]
def set_opacity(self,opacity):
for line in self.lines:
line.setOpacity(opacity)
You need to add import: from PyQt5.QtGui import QPen, QColor
EDIT2:
Painting a rectangle in the scene:
def draw_insert_at_marker(self):
w = Settings.WIDTH * 3
h = Settings.HEIGHT
r = QRectF(7 * Settings.WIDTH, 7 * Settings.HEIGHT, w, h)
gradient = QLinearGradient(r.topLeft(), r.bottomRight())
gradient.setColorAt(1, QColor(255, 255, 255, 0))
gradient.setColorAt(0, QColor(255, 255, 255, 127))
rect = self.addRect(r, Qt.white, gradient)
You can obtain an automatically resizing grid from the library:
grid = QtGui.QGridLayout ()
http://zetcode.com/gui/pyqt4/layoutmanagement/
You can set the border color. It actually serves as an alignment tool for widgets, but maybe it's possible to adapt it to your needs.
Related
I am trying to create a custom checkbox. I am using paintEvent function to create my special checkbox.
It's design:
The result on Qt:
First of all, rounded should be added and the junction of the lines should be a smoother transition.
I need a more professional solution. Which is pretty looking.
Thanks!
Code:
import sys, os, time
from PySide6 import QtCore, QtWidgets, QtGui
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *
class ECheckBoxData(object):
Radius = 10
AnimationTime = 600 # ms
FontSize, FontSpacing = 16, 0
Color = {
"CORNER": QColor(239, 239, 239),
"BASE_BACKGROUND": QColor(255, 125, 51),
"BASE_FOREGROUND": QColor(255, 152, 91),
"BASE_HOVER_BACKGROUND" :QColor(255, 152, 91),
"BASE_HOVER_FOREGROUND": QColor(247, 247, 250),
}
TextElide = Qt.ElideMiddle
CheckWidth, CheckHeight = 128, 128
class ECheckBox(QCheckBox):
CheckBoxData = ECheckBoxData()
def __init__(self, CheckBoxData=ECheckBoxData()):
super(ECheckBox, self).__init__(None)
self.CheckBoxData = CheckBoxData
self.myfont = QFont("Times New Roman", 16, weight=QFont.Bold)
self.myfont.setWordSpacing(self.CheckBoxData.FontSpacing)
self.myfont.setStyleHint(QFont.Monospace)
self.myfontMetrics = QFontMetrics(self.myfont)
# font.setStyleHint(QFont.Times, QFont.PreferAntialias)
self.setFont(self.myfont)
self.setFixedHeight(self.CheckBoxData.CheckHeight+2)
self.setMinimumWidth(self.CheckBoxData.CheckWidth+8)
def paintEvent(self, event: QPaintEvent):
pt = QPainter(self)
pt.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing )
border = QPainterPath()
pt.setBrush(self.CheckBoxData.Color["BASE_BACKGROUND"])
pt.setPen(QPen(self.CheckBoxData.Color["CORNER"],5))
border.addRoundedRect(QRectF(2,2,self.CheckBoxData.CheckWidth-2, self.CheckBoxData.CheckHeight-2),self.CheckBoxData.Radius, self.CheckBoxData.Radius)
pt.drawPath(border)
pt.setClipPath(border)
pt.setPen(QPen(Qt.white,self.CheckBoxData.CheckWidth/10))
pt.setBrush(Qt.white)
path2 = QPainterPath()
arrow_width, arrow_height = self.width()/4, self.height()/ (66/8)
center_width, center_height = int(self.width()/2), int(self.height()/2)
#path2.moveTo((self.width() - arrow_width * 2) / 2, (center_height + 2))
#path2.lineTo(QPoint((self.width() - arrow_width) / 2 + 2, (center_height) + arrow_height + 1))
#path2.lineTo(QPoint((self.width()-arrow_width), (center_height)-arrow_height))
path2.addPolygon(QPolygonF([
QPoint((self.width()-arrow_width*2)/2, (center_height+2)), QPoint((self.width()-arrow_width)/2+2, (center_height)+arrow_height+1)
]))
path2.addPolygon(QPolygonF([QPoint((self.width()-arrow_width)/2+2, (center_height)+arrow_height+1), QPoint((self.width()-arrow_width-12), (center_height)-arrow_height)]))
pt.drawPath(path2)
if __name__ == "__main__":
app = QApplication(sys.argv)
wind = QMainWindow()
wind.setStyleSheet("QMainWindow{background-color:rgb(247,247,250)}")
wind.resize(221, 150)
wid = QWidget()
lay = QHBoxLayout(wid)
lay.setAlignment(Qt.AlignCenter)
Data = ECheckBoxData()
e = ECheckBox(Data)
e.setChecked(True)
lay.addWidget(e)
wind.setCentralWidget(wid)
wind.show()
sys.exit(app.exec())
In order to create a smooth and curved outline, you need to properly set the QPen cap and join style.
Using a polygon to draw the outline is obviously not a valid solution, as that outline will be drawn with the pen, but what you need is a path that will be painted with a thick pen and the preferred cap and join styles.
Also, in order to be able to draw a good icon at different sizes, you should not rely on fixed sizes (even if properly computed), but use the current size as a reference instead.
def paintEvent(self, event: QPaintEvent):
pt = QPainter(self)
pt.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
size = min(self.width(), self.height())
border = max(1, size / 32)
rect = QRectF(0, 0, size - border, size - border)
# move the square to the *exact* center using a QRectF based on the
# current widget; note: it is very important that you get the center
# using a QRectF, because the center of QRect is always in integer
# values, and will almost always be imprecise at small sizes
rect.moveCenter(QRectF(self.rect()).center())
borderPath = QPainterPath()
# with RelativeSize we can specify the radius as 30% of half the size
borderPath.addRoundedRect(rect, 30, 30, Qt.RelativeSize)
pt.setBrush(self.CheckBoxData.Color["BASE_BACKGROUND"])
pt.setPen(QPen(self.CheckBoxData.Color["CORNER"], border * 2.5))
pt.drawPath(borderPath)
pt.setPen(QPen(Qt.white, size * .125,
cap=Qt.RoundCap, join=Qt.RoundJoin))
arrow_path = QPainterPath()
arrow_path.moveTo(size * .25, size * .5)
arrow_path.lineTo(size * .40, size * .65)
arrow_path.lineTo(size * .7, size * .325)
pt.drawPath(arrow_path.translated(rect.topLeft()))
I have this snippet:
import sys
from PyQt5.QtWidgets import QLabel, QMainWindow, QApplication
from PyQt5.QtGui import QPixmap, QImage, QResizeEvent
import numpy as np
class VLabel(QLabel):
def __init__(self):
super(VLabel, self).__init__()
self.pixmap_width: int = 1
self.pixmapHeight: int = 1
def setPixmap(self, pm: QPixmap) -> None:
self.pixmap_width = pm.width()
self.pixmapHeight = pm.height()
self.updateMargins()
super(VLabel, self).setPixmap(pm)
def resizeEvent(self, a0: QResizeEvent) -> None:
self.updateMargins()
super(VLabel, self).resizeEvent(a0)
def updateMargins(self):
if self.pixmap() is None:
return
pixmapWidth = self.pixmap().width()
pixmapHeight = self.pixmap().height()
if pixmapWidth <= 0 or pixmapHeight <= 0:
return
w, h = self.width(), self.height()
if w <= 0 or h <= 0:
return
if w * pixmapHeight > h * pixmapWidth:
m = int((w - (pixmapWidth * h / pixmapHeight)) / 2)
self.setContentsMargins(m, 0, m, 0)
else:
m = int((h - (pixmapHeight * w / pixmapWidth)) / 2)
self.setContentsMargins(0, m, 0, m)
class ControlWindow(QMainWindow):
def __init__(self):
super(ControlWindow, self).__init__()
#frame = np.random.randint(0,255,(1080,1920,3), np.uint8)
frame = np.random.randint(0,255,(400,800,3), np.uint8)
img = QImage(frame, frame.shape[1], frame.shape[0],QImage.Format_RGB888)
pix = QPixmap.fromImage(img)
vl = VLabel()
vl.setPixmap(pix)
vl.setScaledContents(True)
self.setCentralWidget(vl)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = ControlWindow()
window.show()
window.raise_()
sys.exit(app.exec_())
Which shows an image in a QLabel. It works, and I can resize window bigger - and VLabel will scale automatically and fill up available space while keeping aspectration. Thats AWESOME!
HOWEVER...when i try to resize window to be smaller than initial image...i cannot :(
For some reason the minimum size is the initial image size which is very unfortunate. How can i also enable resize of smaller windows so that when i drag-resize to something smaller than initial image size, then image will shrink accordingly (while still maintaining aspect ratio) ?
I am a new to pyqt and need help with rotating the label. I am confused and cannot understand how to rotate the whole widget on a specific angle. Not the content of the widget, but the widget itself. I am searching for the solution but cannot find anything.
A QWidget does not support rotation, but a workaround is to insert the widget into a QGraphicsProxyWidget and add it to a QGraphicsScene, and then rotate the QGraphicsProxyWidget that visually generates the same widget rotation effect.
from PyQt5 import QtCore, QtGui, QtWidgets
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
label = QtWidgets.QLabel("Stack Overflow", alignment=QtCore.Qt.AlignCenter)
graphicsview = QtWidgets.QGraphicsView()
scene = QtWidgets.QGraphicsScene(graphicsview)
graphicsview.setScene(scene)
proxy = QtWidgets.QGraphicsProxyWidget()
proxy.setWidget(label)
proxy.setTransformOriginPoint(proxy.boundingRect().center())
scene.addItem(proxy)
slider = QtWidgets.QSlider(minimum=0, maximum=359, orientation=QtCore.Qt.Horizontal)
slider.valueChanged.connect(proxy.setRotation)
label_text = QtWidgets.QLabel(
"{}°".format(slider.value()), alignment=QtCore.Qt.AlignCenter
)
slider.valueChanged.connect(
lambda value: label_text.setText("{}°".format(slider.value()))
)
slider.setValue(45)
w = QtWidgets.QWidget()
lay = QtWidgets.QVBoxLayout(w)
lay.addWidget(graphicsview)
lay.addWidget(slider)
lay.addWidget(label_text)
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
As #eyllanesc correctly explains, there's no "widget rotation" support in Qt (as in most standard frameworks).
There are a couple of tricks on your hand, though.
"Simple" label (not using a QLabel)
That's the "simple" solution. Since you're talking about a "label", that can be implemented using some math.
The biggest advantage in this approach is that the size hint is "simple", meaning that it's only based on the text contents (as in QFontMetrics.boundingRect()), and whenever the main font, text or alignment is changed, the size hint reflects them.
While it supports multi-line labels, the biggest problem about this approach comes in place if you need to use rich text, though; a QTextDocument can be used instead of a standard string, but that would require a more complex implementation for size hint computing.
from math import radians, sin, cos
from random import randrange
from PyQt5 import QtCore, QtGui, QtWidgets
class AngledLabel(QtWidgets.QWidget):
_alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop
def __init__(self, text='', angle=0, parent=None):
super(AngledLabel, self).__init__(parent)
self._text = text
self._angle = angle % 360
# keep radians of the current angle *and* its opposite; we're using
# rectangles to get the overall area of the text, and since they use
# right angles, that opposite is angle + 90
self._radians = radians(-angle)
self._radiansOpposite = radians(-angle + 90)
def alignment(self):
return self._alignment
def setAlignment(self, alignment):
# text alignment might affect the text size!
if alignment == self._alignment:
return
self._alignment = alignment
self.setMinimumSize(self.sizeHint())
def angle(self):
return self._angle
def setAngle(self, angle):
# the angle clearly affects the overall size
angle %= 360
if angle == self._angle:
return
self._angle = angle
# update the radians to improve optimization of sizeHint and paintEvent
self._radians = radians(-angle)
self._radiansOpposite = radians(-angle + 90)
self.setMinimumSize(self.sizeHint())
def text(self):
return self._text
def setText(self, text):
if text == self._text:
return
self._text = text
self.setMinimumSize(self.sizeHint())
def sizeHint(self):
# get the bounding rectangle of the text
rect = self.fontMetrics().boundingRect(QtCore.QRect(), self._alignment, self._text)
# use trigonometry to get the actual size of the rotated rectangle
sinWidth = abs(sin(self._radians) * rect.width())
cosWidth = abs(cos(self._radians) * rect.width())
sinHeight = abs(sin(self._radiansOpposite) * rect.height())
cosHeight = abs(cos(self._radiansOpposite) * rect.height())
return QtCore.QSize(cosWidth + cosHeight, sinWidth + sinHeight)
def minimumSizeHint(self):
return self.sizeHint()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
textRect = self.fontMetrics().boundingRect(
QtCore.QRect(), self._alignment, self._text)
width = textRect.width()
height = textRect.height()
# we have to translate the painting rectangle, and that depends on which
# "angle sector" the current angle is
if self._angle <= 90:
deltaX = 0
deltaY = sin(self._radians) * width
elif 90 < self._angle <= 180:
deltaX = cos(self._radians) * width
deltaY = sin(self._radians) * width + sin(self._radiansOpposite) * height
elif 180 < self._angle <= 270:
deltaX = cos(self._radians) * width + cos(self._radiansOpposite) * height
deltaY = sin(self._radiansOpposite) * height
else:
deltaX = cos(self._radiansOpposite) * height
deltaY = 0
qp.translate(.5 - deltaX, .5 - deltaY)
qp.rotate(-self._angle)
qp.drawText(self.rect(), self._alignment, self._text)
class TestWindow(QtWidgets.QWidget):
def __init__(self):
super(TestWindow, self).__init__()
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.randomizeButton = QtWidgets.QPushButton('Randomize!')
layout.addWidget(self.randomizeButton, 0, 0, 1, 3)
self.randomizeButton.clicked.connect(self.randomize)
layout.addWidget(QtWidgets.QLabel('Standard label'), 1, 0)
text = 'Some text'
layout.addWidget(QtWidgets.QLabel(text), 1, 2)
self.labels = []
for row, angle in enumerate([randrange(360) for _ in range(8)], 2):
angleLabel = QtWidgets.QLabel(u'{}°'.format(angle))
angleLabel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
layout.addWidget(angleLabel, row, 0)
label = AngledLabel(text, angle)
layout.addWidget(label, row, 2)
self.labels.append((angleLabel, label))
separator = QtWidgets.QFrame()
separator.setFrameShape(separator.VLine|separator.Sunken)
layout.addWidget(separator, 1, 1, layout.rowCount() - 1, 1)
def randomize(self):
for angleLabel, label in self.labels:
angle = randrange(360)
angleLabel.setText(str(angle))
label.setAngle(angle)
self.adjustSize()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = TestWindow()
w.show()
sys.exit(app.exec_())
QGraphicsView implementation
I would also like to expand the solution proposed by eyllanesc, as it is more modular and allows to use "any" widget; unfortunately, while his answer works as expected, I'm afraid that it's an answer that is just valid "for the sake of the argument".
From the graphical point of view, the obvious issues are the QGraphicsView visual hints (borders and background). But, since we're talking about widgets that might have to be inserted in a graphical interface, the size (and its hint[s]) require some care.
The main advantage of this approach is that almost any type of widget can be added to the interface, but due to the nature of per-widget size policy and QGraphicsView implementations, if the content of the "rotated" widget changes, perfect drawing will always be something hard to achieve.
from random import randrange
from PyQt5 import QtCore, QtGui, QtWidgets
class AngledObject(QtWidgets.QGraphicsView):
_angle = 0
def __init__(self, angle=0, parent=None):
super(AngledObject, self).__init__(parent)
# to prevent the graphics view to draw its borders or background, set the
# FrameShape property to 0 and a transparent background
self.setFrameShape(0)
self.setStyleSheet('background: transparent')
self.setScene(QtWidgets.QGraphicsScene())
# ignore scroll bars!
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
def angle(self):
return self._angle
def setAngle(self, angle):
angle %= 360
if angle == self._angle:
return
self._angle = angle
self._proxy.setTransform(QtGui.QTransform().rotate(-angle))
self.adjustSize()
def resizeEvent(self, event):
super(AngledObject, self).resizeEvent(event)
# ensure that the scene is fully visible after resizing
QtCore.QTimer.singleShot(0, lambda: self.centerOn(self.sceneRect().center()))
def sizeHint(self):
return self.scene().itemsBoundingRect().size().toSize()
def minimumSizeHint(self):
return self.sizeHint()
class AngledLabel(AngledObject):
def __init__(self, text='', angle=0, parent=None):
super(AngledLabel, self).__init__(angle, parent)
self._label = QtWidgets.QLabel(text)
self._proxy = self.scene().addWidget(self._label)
self._label.setStyleSheet('background: transparent')
self.setAngle(angle)
self.alignment = self._label.alignment
def setAlignment(self, alignment):
# text alignment might affect the text size!
if alignment == self._label.alignment():
return
self._label.setAlignment(alignment)
self.setMinimumSize(self.sizeHint())
def text(self):
return self._label.text()
def setText(self, text):
if text == self._label.text():
return
self._label.setText(text)
self.setMinimumSize(self.sizeHint())
class AngledButton(AngledObject):
def __init__(self, text='', angle=0, parent=None):
super(AngledButton, self).__init__(angle, parent)
self._button = QtWidgets.QPushButton(text)
self._proxy = self.scene().addWidget(self._button)
self.setAngle(angle)
class TestWindow(QtWidgets.QWidget):
def __init__(self):
super(TestWindow, self).__init__()
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.randomizeButton = QtWidgets.QPushButton('Randomize!')
layout.addWidget(self.randomizeButton, 0, 0, 1, 3)
self.randomizeButton.clicked.connect(self.randomize)
layout.addWidget(QtWidgets.QLabel('Standard label'), 1, 0)
text = 'Some text'
layout.addWidget(QtWidgets.QLabel(text), 1, 2)
self.labels = []
for row, angle in enumerate([randrange(360) for _ in range(4)], 2):
angleLabel = QtWidgets.QLabel(u'{}°'.format(angle))
angleLabel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
layout.addWidget(angleLabel, row, 0)
label = AngledLabel(text, angle)
layout.addWidget(label, row, 2)
self.labels.append((angleLabel, label))
for row, angle in enumerate([randrange(360) for _ in range(4)], row + 1):
angleLabel = QtWidgets.QLabel(u'{}°'.format(angle))
angleLabel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
layout.addWidget(angleLabel, row, 0)
label = AngledButton('Button!', angle)
layout.addWidget(label, row, 2)
self.labels.append((angleLabel, label))
separator = QtWidgets.QFrame()
separator.setFrameShape(separator.VLine|separator.Sunken)
layout.addWidget(separator, 1, 1, layout.rowCount() - 1, 1)
def randomize(self):
for angleLabel, label in self.labels:
angle = randrange(360)
angleLabel.setText(str(angle))
label.setAngle(angle)
self.adjustSize()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = TestWindow()
w.show()
sys.exit(app.exec_())
As you can see, the "randomize" functions have very different results. While the second approach allows using more complex widgets, the first one better reacts to contents changes.
:)
does someone know how to scale overlays in python?
I want this overlay here:
to this:
python code:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Crosshair(QtWidgets.QWidget):
def __init__(self, parent=None, windowSize=24, penWidth=2, opacity=40):
QtWidgets.QWidget.__init__(self, parent)
self.ws = windowSize
self.resize(windowSize+1, windowSize+1)
self.pen = QtGui.QPen(QtGui.QColor(0,255,0,255))
self.pen.setWidth(penWidth)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.WindowTransparentForInput)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
self.move(QtWidgets.QApplication.desktop().screen().rect().center() - self.rect().center() + QtCore.QPoint(1,1))
self.opacity = opacity
def paintEvent(self, event):
ws = self.ws
d = 241
painter = QtGui.QPainter(self)
painter.setOpacity(self.opacity)
painter.setPen(self.pen)
# painter.drawLine( x1,y1, x2,y2 )
painter.drawLine(ws/2, 0, ws/2, ws/2 - ws/d) # Top
painter.drawLine(ws/2, ws/2 + ws/d, ws/2, ws) # Bottom
painter.drawLine(0, ws/2, ws/2 - ws/d, ws/2) # Left
painter.drawLine(ws/2 + ws/d, ws/2, ws, ws/2) # Right
# Drawing X on center (How to scale this here?)
painter.drawLine(100,100,120.5,120.5) # Top Left
painter.drawLine(200, 50, 120.5, 120.5) # Top Right
painter.drawLine(0, 200, 120.5, 120.5) # Bottom Left
painter.drawLine(200, 200, 120.5, 120.5) # Bottom Right
app = QtWidgets.QApplication(sys.argv)
widget = Crosshair(windowSize=241, penWidth=2.5, opacity=0.5)
widget.show()
sys.exit(app.exec_())
I implementing custom chart. But I stucked with mouse hitting detection with QPainterPath.
I tried with graphicsitem's shape(), boundingRect(). but that only checks rough shape of boundary.
I want to check mouse hit system with exact position on QPainterPath path instance. But seems to no api like that functionality.
My app's QGraphicsScene is set with same coordinate with QGraphicsView in view's resizeEvent().
scene: MyScene = self.scene()
scene.setSceneRect(self.rect().x(), self.rect().y(),
self.rect().width(), self.rect().height())
At the same time, my plot QGraphicsItem scales by QTransform.
plot: QGraphicsItem = scene.plot
trans = QTransform()
data = plot.df['data']
data = data - data.min()
data_max = data.max()
data_min = data.min()
trans.scale(self.width() / len(data),
self.height() / (data_max - data_min))
plot.trans = trans
plot.setTransform(trans)
And in the MyScene, add rect item mouse_rec. So, I check mouse_rec and plot item's path with mouse_rec.collidesWithPath(path)
It just works only with original path.
Here is all code. Just copy and paste, you could run it.
Red plot is original path and yellow plot is scaled path. Mouse hit check is only works with red plot...
import numpy
import pandas
from PyQt5 import QtGui
from PyQt5.QtCore import Qt, QRectF, QRect
from PyQt5.QtGui import QRadialGradient, QGradient, QPen, QPainterPath, QTransform, QPainter, QColor
from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView, QGraphicsSceneMouseEvent, QGraphicsItem, \
QStyleOptionGraphicsItem, QWidget, QGraphicsRectItem
class MyItem(QGraphicsItem):
def __init__(self, df, parent=None):
QGraphicsItem.__init__(self, parent)
self.num = 1
self.df = df
self.path = QPainterPath()
self.trans = QTransform()
self.cached = False
self.printed = False
self.setZValue(0)
def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionGraphicsItem', widget: QWidget = ...):
data = self.df['data']
data = data - data.min()
data_max = data.max()
data_min = data.min()
if not self.cached:
for i in range(data.size - 1):
self.path.moveTo(i, data[i])
self.path.lineTo(i+1, data[i+1])
self.cached = True
pen = QPen(Qt.white)
pen.setCosmetic(True)
painter.setPen(pen)
painter.drawRect(0, 0, data.size, data_max - data_min)
pen.setColor(Qt.yellow)
painter.setPen(pen)
painter.drawPath(self.path)
if not self.printed:
rec_item = self.scene().addPath(self.path, QPen(Qt.red))
rec_item.setZValue(-10)
self.printed = True
def boundingRect(self):
data = self.df['data']
data_max = data.max()
data_min = data.min()
return QRectF(0, 0, data.size, data_max - data_min)
class MyScene(QGraphicsScene):
def __init__(self, data, parent=None):
QGraphicsScene.__init__(self, parent)
self.data = data
self.mouse_rect = QGraphicsRectItem()
self.plot: MyItem(data) = None
self.bounding_rect = QGraphicsRectItem()
self.setBackgroundBrush(QColor('#14161f'))
self.addItem(self.bounding_rect)
self.printed = False
def mouseMoveEvent(self, event: 'QGraphicsSceneMouseEvent'):
print()
print("rec rect : ", self.mouse_rect.rect())
print("Scene rect : ", self.sceneRect())
print("ItemBounding rect : ", self.itemsBoundingRect())
print("transform : ", self.plot.transform().m11(), ", ", self.plot.transform().m22())
item = self.itemAt(event.scenePos(), self.plot.transform())
if item and isinstance(item, MyItem):
print()
print('collides path : ', self.mouse_rect.collidesWithPath(item.path))
print('collides item : ', self.mouse_rect.collidesWithItem(item))
super().mouseMoveEvent(event)
def print_bound(self, rect):
self.bounding_rect.setPen(QPen(Qt.green))
self.bounding_rect.setRect(rect.x() + 5, rect.y() + 5,
rect.width() - 10, rect.height() - 10)
class MyView(QGraphicsView):
def __init__(self, data, parent=None):
QGraphicsView.__init__(self, parent)
self.data = data
self.setMouseTracking(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
def wheelEvent(self, event: QtGui.QWheelEvent):
print("pixel / Data : {}".format(self.width() / len(self.data)))
def resizeEvent(self, event: QtGui.QResizeEvent):
scene: MyScene = self.scene()
scene.setSceneRect(self.rect().x(), self.rect().y(),
self.rect().width(), self.rect().height())
scene.print_bound(self.rect())
plot: QGraphicsItem = scene.plot
trans = QTransform()
data = plot.df['data']
data = data - data.min()
data_max = data.max()
data_min = data.min()
trans.scale(self.width() / len(data),
self.height() / (data_max - data_min))
plot.trans = trans
plot.setTransform(trans)
def mouseMoveEvent(self, event: QtGui.QMouseEvent):
mouse_rect: QGraphicsRectItem = self.scene().mouse_rect
mouse_rect.setRect(event.pos().x() - 2, event.pos().y() - 2, 4, 4)
super().mouseMoveEvent(event)
if __name__ == '__main__':
df = pandas.DataFrame({'data': numpy.random.randint(0, 20, 50)})
app = QApplication([])
scene = MyScene(df)
view = MyView(df)
view.setScene(scene)
rec = QGraphicsRectItem(-2, -2, 4, 4)
rec.setPen(Qt.white)
scene.mouse_rect = rec
scene.addItem(rec)
plot = MyItem(df)
scene.addItem(plot)
scene.plot = plot
view.show()
app.exec_()
Any idea checking mouse point with path ?? I first tried custom math function calculating [point <-> line] distance, but that need much time and making lagging app..
I will make not only line plot but also bar, area, points, candlestick plot.. Is there any idea to solve this problem?
You have to convert the position of the path with respect to the item that is scaled to the position relative to the scene using mapToScene():
if item and isinstance(item, MyItem):
print('collides path : ', self.mouse_rect.collidesWithPath(item.mapToScene(item.path)))
print('collides item : ', self.mouse_rect.collidesWithItem(item))