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()))
Related
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.
Basically I am trying to create a radial menu like this
I was using QPainter and here is a attempt from my side. But I can't figure out how to add a click event on the pixmaps. Is there any lib available for this ?
Images Link
from PySide2 import QtWidgets, QtGui, QtCore
import sys
import os
RESPATH = "/{your_folder}/radialMenu/res"
class RadialMenu(QtWidgets.QGraphicsRectItem):
addButton = 1
disableButton = 2
clearButton = 3
exportButton = 4
infoButton = 5
runButton = 6
scriptsButton = 7
def __init__(self, parent=None):
super(RadialMenu, self).__init__(parent)
def paint(self, painter, option, widget=None):
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setBrush(QtGui.QBrush(QtGui.QColor(71, 71, 71, 0)))
tempPen = QtGui.QPen(QtGui.QColor(178, 141, 58), 20.0, QtCore.Qt.CustomDashLine)
tempPen.setDashPattern([4, 4, 4, 4])
painter.setPen(tempPen)
painter.drawEllipse(0, 0, 150, 150)
topX = 0
topY = 0
pixmap1 = QtGui.QPixmap(os.path.join(RESPATH, "add.png"))
painter.drawPixmap(topX + 50, topY - 8, pixmap1)
pixmap2 = QtGui.QPixmap(os.path.join(RESPATH, "disable.png"))
painter.drawPixmap(topX + 90, topY - 5, pixmap2)
pixmap3 = QtGui.QPixmap(os.path.join(RESPATH, "clear.png"))
painter.drawPixmap(topX - 10, topY + 70, pixmap3)
pixmap4 = QtGui.QPixmap(os.path.join(RESPATH, "export.png"))
pixmap4 = pixmap4.transformed(QtGui.QTransform().rotate(15))
painter.drawPixmap(topX - 2, topY + 100, pixmap4)
pixmap5 = QtGui.QPixmap(os.path.join(RESPATH, "info.png"))
painter.drawPixmap(topX + 20, topY + 125, pixmap5)
pixmap6 = QtGui.QPixmap(os.path.join(RESPATH, "run.png"))
painter.drawPixmap(topX + 113, topY + 125, pixmap6)
pixmap6 = QtGui.QPixmap(os.path.join(RESPATH, "scripts.png"))
painter.drawPixmap(topX + 137, topY + 85, pixmap6)
class RadialTest(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.scene=QtWidgets.QGraphicsScene(self)
buttonItem = RadialMenu()
self.scene.addItem(buttonItem)
buttonItem.setPos(100,100)
buttonItem.setZValue(1000)
self.scene.update()
self.view = QtWidgets.QGraphicsView(self.scene, self)
self.scene.setSceneRect(0, 0, 300, 300)
self.setGeometry(50, 50, 305, 305)
self.show()
if __name__ == "__main__":
app=QtWidgets.QApplication(sys.argv)
firstScene = RadialTest()
sys.exit(app.exec_())
This code will give this result
For this kind of objects, keeping a hierarchical object structure is always suggested. Also, when dealing with object that possibly have "fixed" sizes (like images, but not only), fixed positioning can be tricky expecially with newer system that support different DPI screen values.
With this approach I'm not using mapped images at all (but button icons can be still set), instead I chose to use a purely geometrical concept, using pixel "radius" values and angles for each button.
from PyQt5 import QtWidgets, QtGui, QtCore
from math import sqrt
class RadialMenu(QtWidgets.QGraphicsObject):
buttonClicked = QtCore.pyqtSignal(int)
def __init__(self, parent=None):
super().__init__(parent)
self.setAcceptHoverEvents(True)
self.buttons = {}
def addButton(self, id, innerRadius, size, startAngle, angleSize, pen=None,
brush=None, icon=None):
# if a button already exists with the same id, remove it
if id in self.buttons:
oldItem = self.buttons.pop(id)
if self.scene():
self.scene().removeItem(oldItem)
oldItem.setParent(None)
# compute the extents of the inner and outer "circles"
startRect = QtCore.QRectF(
-innerRadius, -innerRadius, innerRadius * 2, innerRadius * 2)
outerRadius = innerRadius + size
endRect = QtCore.QRectF(
-outerRadius, -outerRadius, outerRadius * 2, outerRadius * 2)
# create the circle section path
path = QtGui.QPainterPath()
# move to the start angle, using the outer circle
path.moveTo(QtCore.QLineF.fromPolar(outerRadius, startAngle).p2())
# draw the arc to the end of the angle size
path.arcTo(endRect, startAngle, angleSize)
# draw a line that connects to the inner circle
path.lineTo(QtCore.QLineF.fromPolar(innerRadius, startAngle + angleSize).p2())
# draw the inner circle arc back to the start angle
path.arcTo(startRect, startAngle + angleSize, -angleSize)
# close the path back to the starting position; theoretically unnecessary,
# but better safe than sorry
path.closeSubpath()
# create a child item for the "arc"
item = QtWidgets.QGraphicsPathItem(path, self)
item.setPen(pen if pen else (QtGui.QPen(QtCore.Qt.transparent)))
item.setBrush(brush if brush else QtGui.QColor(180, 140, 70))
self.buttons[id] = item
if icon is not None:
# the maximum available size is at 45 degrees, use the Pythagorean
# theorem to compute it and create a new pixmap based on the icon
iconSize = int(sqrt(size ** 2 / 2))
pixmap = icon.pixmap(iconSize)
# create the child icon (pixmap) item
iconItem = QtWidgets.QGraphicsPixmapItem(pixmap, self)
# push it above the "arc" item
iconItem.setZValue(item.zValue() + 1)
# find the mid of the angle and put the icon there
midAngle = startAngle + angleSize / 2
iconPos = QtCore.QLineF.fromPolar(innerRadius + size * .5, midAngle).p2()
iconItem.setPos(iconPos)
# use the center of the pixmap as the offset for centering
iconItem.setOffset(-pixmap.rect().center())
def itemAtPos(self, pos):
for button in self.buttons.values():
if button.shape().contains(pos):
return button
def checkHover(self, pos):
hoverButton = self.itemAtPos(pos)
for button in self.buttons.values():
# set a visible border only for the hovered item
button.setPen(QtCore.Qt.red if button == hoverButton else QtCore.Qt.transparent)
def hoverEnterEvent(self, event):
self.checkHover(event.pos())
def hoverMoveEvent(self, event):
self.checkHover(event.pos())
def hoverLeaveEvent(self, event):
for button in self.buttons.values():
button.setPen(QtCore.Qt.transparent)
def mousePressEvent(self, event):
clickButton = self.itemAtPos(event.pos())
if clickButton:
for id, btn in self.buttons.items():
if btn == clickButton:
self.buttonClicked.emit(id)
def boundingRect(self):
return self.childrenBoundingRect()
def paint(self, qp, option, widget):
# required for QGraphicsObject subclasses
pass
ButtonData = [
(50, 40, QtWidgets.QStyle.SP_MessageBoxInformation),
(90, 40, QtWidgets.QStyle.SP_MessageBoxQuestion),
(180, 20, QtWidgets.QStyle.SP_FileDialogBack),
(200, 20, QtWidgets.QStyle.SP_DialogOkButton),
(220, 20, QtWidgets.QStyle.SP_DialogOpenButton),
(290, 30, QtWidgets.QStyle.SP_ArrowDown),
(320, 30, QtWidgets.QStyle.SP_ArrowUp),
]
class RadialTest(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.scene = QtWidgets.QGraphicsScene(self)
buttonItem = RadialMenu()
self.scene.addItem(buttonItem)
buttonItem.buttonClicked.connect(self.buttonClicked)
for index, (startAngle, extent, icon) in enumerate(ButtonData):
icon = self.style().standardIcon(icon, None, self)
buttonItem.addButton(index, 64, 20, startAngle, extent, icon=icon)
buttonItem.setPos(150, 150)
buttonItem.setZValue(1000)
self.view = QtWidgets.QGraphicsView(self.scene, self)
self.view.setRenderHints(QtGui.QPainter.Antialiasing)
self.scene.setSceneRect(0, 0, 300, 300)
self.setGeometry(50, 50, 305, 305)
self.show()
def buttonClicked(self, id):
print('Button id {} has been clicked'.format(id))
And this is the result:
Using PySide2 or PyQt5, I want to make a table widget with header labels that are on a 45 degree angle, like in the image here.
I don't see anything like this in QtCreator (Designer) for the QTable widget. I can rotate a label using something like this:
class MyLabel(QtGui.QWidget):
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setPen(QtCore.Qt.black)
painter.translate(20, 100)
painter.rotate(-45)
painter.drawText(0, 0, "hellos")
painter.end()
But, there are several niggles. Ideally this would be a QLineEdit widget, I would need the widgets to 'play nice' so as not to overlap anything else, and I would like them to fill in above the table from the header. I'm looking for suggestions.
This is a very interesting topic, as Qt doesn't provide such a feature, but it can be implemented.
The following example is far from perfect, I'll list its main pros/cons.
Pros
it works ;-)
changing horizontal header labels automatically updates the header height
supports horizontal scrolling "over" the last item position (if the table view is smaller than its contents, the horizontal scrollbar allows to see the full header text)
it works :-D
Cons
sections are fixed
sections are not movable
QAbstractItemView.ScrollPerPixel is mandatory for the horizontal scroll mode in this implementation. Qt's ScrollPerItem mode is a bit complex, and has some issues if it's not overrided with huge care. This doesn't mean that it's not possible to use that mode, but it requires a lot of efforts, possibly by carefully reading and understanding the source code of both QTableView and QAbstractItemView. Long story short: ScrollPerItem works until you reach the maximum value of the horizontal scrollbar; at that point, the view will try to resize and adapt its viewport and scrollbar value/range, and the last header labels will be "cut out".
if all horizontal columns are visible (meaning that the items wouldn't require horizontal scrolling), the last horizontal headers are not completely shown, since the horizontal scroll bar is not required.
I think that it should be possible to support all header features (custom/stretchable section size, movable sections, item scroll, etc.), but it would require a very deep reimplementation process of both QTableView and QHeaderView methods.
Anyhow, that's the result I've got so far, which supports scrolling, painting, and basic mouse interaction (section highlight on click).
Example screenshot:
Scrolled (near the right edge) screenshot:
Table sized slightly after the right edge of the last horizontal column:
Example code
import sys
from math import sqrt, sin, acos, hypot, degrees, radians
from PyQt5 import QtCore, QtGui, QtWidgets
class AngledHeader(QtWidgets.QHeaderView):
borderPen = QtGui.QColor(0, 190, 255)
labelBrush = QtGui.QColor(255, 212, 0)
def __init__(self, parent=None):
QtWidgets.QHeaderView.__init__(self, QtCore.Qt.Horizontal, parent)
self.setSectionResizeMode(self.Fixed)
self.setDefaultSectionSize(sqrt((self.fontMetrics().height() + 4)** 2 *2))
self.setSectionsClickable(True)
self.setDefaultSectionSize(int(sqrt((self.fontMetrics().height() + 4)** 2 *2)))
self.setMaximumHeight(100)
# compute the ellipsis size according to the angle; remember that:
# 1. if the angle is not 45 degrees, you'll need to compute this value
# using trigonometric functions according to the angle;
# 2. we assume ellipsis is done with three period characters, so we can
# "half" its size as (usually) they're painted on the bottom line and
# they are large enough, allowing us to show as much as text is possible
self.fontEllipsisSize = int(hypot(*[self.fontMetrics().height()] * 2) * .5)
self.setSectionsClickable(True)
def sizeHint(self):
# compute the minimum height using the maximum header label "hypotenuse"'s
hint = QtWidgets.QHeaderView.sizeHint(self)
count = self.count()
if not count:
return hint
fm = self.fontMetrics()
width = minSize = self.defaultSectionSize()
# set the minimum width to ("hypotenuse" * sectionCount) + minimumHeight
# at least, ensuring minimal horizontal scroll bar interaction
hint.setWidth(width * count + self.minimumHeight())
maxDiag = maxWidth = maxHeight = 1
for s in range(count):
if self.isSectionHidden(s):
continue
# compute the diagonal of the text's bounding rect,
# shift its angle by 45° to get the minimum required
# height
rect = fm.boundingRect(
str(self.model().headerData(s, QtCore.Qt.Horizontal)) + ' ')
# avoid math domain errors for empty header labels
diag = max(1, hypot(rect.width(), rect.height()))
if diag > maxDiag:
maxDiag = diag
maxWidth = max(1, rect.width())
maxHeight = max(1, rect.height())
# get the angle of the largest boundingRect using the "Law of cosines":
# https://en.wikipedia.org/wiki/Law_of_cosines
angle = degrees(acos(
(maxDiag ** 2 + maxWidth ** 2 - maxHeight ** 2) /
(2. * maxDiag * maxWidth)
))
# compute the minimum required height using the angle found above
minSize = max(minSize, sin(radians(angle + 45)) * maxDiag)
hint.setHeight(min(self.maximumHeight(), minSize))
return hint
def mousePressEvent(self, event):
width = self.defaultSectionSize()
start = self.sectionViewportPosition(0)
rect = QtCore.QRect(0, 0, width, -self.height())
transform = QtGui.QTransform().translate(0, self.height()).shear(-1, 0)
for s in range(self.count()):
if self.isSectionHidden(s):
continue
if transform.mapToPolygon(
rect.translated(s * width + start, 0)).containsPoint(
event.pos(), QtCore.Qt.WindingFill):
self.sectionPressed.emit(s)
return
def paintEvent(self, event):
qp = QtGui.QPainter(self.viewport())
qp.setRenderHints(qp.Antialiasing)
width = self.defaultSectionSize()
delta = self.height()
# add offset if the view is horizontally scrolled
qp.translate(self.sectionViewportPosition(0) - .5, -.5)
fmDelta = (self.fontMetrics().height() - self.fontMetrics().descent()) * .5
# create a reference rectangle (note that the negative height)
rect = QtCore.QRectF(0, 0, width, -delta)
diagonal = hypot(delta, delta)
for s in range(self.count()):
if self.isSectionHidden(s):
continue
qp.save()
qp.save()
qp.setPen(self.borderPen)
# apply a "shear" transform making the rectangle a parallelogram;
# since the transformation is applied top to bottom
# we translate vertically to the bottom of the view
# and draw the "negative height" rectangle
qp.setTransform(qp.transform().translate(s * width, delta).shear(-1, 0))
qp.drawRect(rect)
qp.setPen(QtCore.Qt.NoPen)
qp.setBrush(self.labelBrush)
qp.drawRect(rect.adjusted(2, -2, -2, 2))
qp.restore()
qp.translate(s * width + width, delta)
qp.rotate(-45)
label = str(self.model().headerData(s, QtCore.Qt.Horizontal))
elidedLabel = self.fontMetrics().elidedText(
label, QtCore.Qt.ElideRight, diagonal - self.fontEllipsisSize)
qp.drawText(0, -fmDelta, elidedLabel)
qp.restore()
class AngledTable(QtWidgets.QTableView):
def __init__(self, *args, **kwargs):
QtWidgets.QTableView.__init__(self, *args, **kwargs)
self.setHorizontalHeader(AngledHeader(self))
self.verticalScrollBarSpacer = QtWidgets.QWidget()
self.addScrollBarWidget(self.verticalScrollBarSpacer, QtCore.Qt.AlignTop)
self.fixLock = False
def setModel(self, model):
if self.model():
self.model().headerDataChanged.disconnect(self.fixViewport)
QtWidgets.QTableView.setModel(self, model)
model.headerDataChanged.connect(self.fixViewport)
def fixViewport(self):
if self.fixLock:
return
self.fixLock = True
# delay the viewport/scrollbar states since the view has to process its
# new header data first
QtCore.QTimer.singleShot(0, self.delayedFixViewport)
def delayedFixViewport(self):
# add a right margin through the horizontal scrollbar range
QtWidgets.QApplication.processEvents()
header = self.horizontalHeader()
if not header.isVisible():
self.verticalScrollBarSpacer.setFixedHeight(0)
self.updateGeometries()
return
self.verticalScrollBarSpacer.setFixedHeight(header.sizeHint().height())
bar = self.horizontalScrollBar()
bar.blockSignals(True)
step = bar.singleStep() * (header.height() / header.defaultSectionSize())
bar.setMaximum(bar.maximum() + step)
bar.blockSignals(False)
self.fixLock = False
def resizeEvent(self, event):
# ensure that the viewport and scrollbars are updated whenever
# the table size change
QtWidgets.QTableView.resizeEvent(self, event)
self.fixViewport()
class TestWidget(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
l = QtWidgets.QGridLayout()
self.setLayout(l)
self.table = AngledTable()
l.addWidget(self.table)
model = QtGui.QStandardItemModel(4, 5)
self.table.setModel(model)
self.table.setHorizontalScrollMode(self.table.ScrollPerPixel)
model.setVerticalHeaderLabels(['Location {}'.format(l + 1) for l in range(8)])
columns = ['Column {}'.format(c + 1) for c in range(8)]
columns[3] += ' very, very, very, very, very, very, long'
model.setHorizontalHeaderLabels(columns)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = TestWidget()
w.show()
sys.exit(app.exec_())
Please note that I edited the painting and click detection code using QTransforms instead QPolygons: while it's a bit more complex to understand its mechanics, it's faster than creating a polygon and computing its points each time a column header has to be drawn.
Also, I've added support for maximum header height (in case any header label get too long), and a "spacer" widget that shifts the vertical scrollbar to the actual "beginning" of the table contents.
musicamante posted such an excellent answer that I've used it as the basis to add a few more (stolen) bits. In this code, when a user double clicks an angled header they are greeted with a popup where they can rename the header. Because of the wonderful code that music provided, it redraws everything automatically.
import sys
from math import sqrt, sin, acos, hypot, degrees, radians
from PySide2 import QtCore, QtGui, QtWidgets
class AngledHeader(QtWidgets.QHeaderView):
borderPen = QtGui.QColor(0, 190, 255)
labelBrush = QtGui.QColor(255, 212, 0)
def __init__(self, parent=None):
QtWidgets.QHeaderView.__init__(self, QtCore.Qt.Horizontal, parent)
self.setSectionResizeMode(self.Fixed)
self.setDefaultSectionSize(sqrt((self.fontMetrics().height() + 4)** 2 *2))
self.setSectionsClickable(True)
def sizeHint(self):
# compute the minimum height using the maximum header
# label "hypotenuse"'s
fm = self.fontMetrics()
width = minSize = self.defaultSectionSize()
count = self.count()
for s in range(count):
if self.isSectionHidden(s):
continue
# compute the diagonal of the text's bounding rect,
# shift its angle by 45° to get the minimum required
# height
rect = fm.boundingRect(str(self.model().headerData(s, QtCore.Qt.Horizontal)) + ' ')
diag = hypot(rect.width(), rect.height())
# get the angle of the boundingRect using the
# "Law of cosines":
# https://en.wikipedia.org/wiki/Law_of_cosines
angle = degrees(acos((diag ** 2 + rect.width() ** 2 - rect.height() ** 2) / (2. * diag * rect.width())))
# compute the minimum required height using the
# angle found above
minSize = max(minSize, sin(radians(angle + 45)) * diag)
hint = QtCore.QSize(width * count + 2000, minSize)
return hint
def mousePressEvent(self, event):
width = self.defaultSectionSize()
first = self.sectionViewportPosition(0)
rect = QtCore.QRect(0, 0, width, -self.height())
transform = QtGui.QTransform().translate(0, self.height()).shear(-1, 0)
for s in range(self.count()):
if self.isSectionHidden(s):
continue
if transform.mapToPolygon(rect.translated(s * width + first,
0)).containsPoint(event.pos(), QtCore.Qt.WindingFill):
self.sectionPressed.emit(s)
self.last = ("Click", s) #log initial click and define the column index
return
def mouseReleaseEvent(self, event):
if self.last[0] == "Double Click":#if this was a double click then we have work to do
index = self.last[1]
oldHeader = str(self.model().headerData(index, QtCore.Qt.Horizontal))
newHeader, ok = QtWidgets.QInputDialog.getText(self,
'Change header label for column %d' % index,
'Header:',
QtWidgets.QLineEdit.Normal,
oldHeader)
if ok:
self.model().horizontalHeaderItem(index).setText(newHeader)
self.update()
def mouseDoubleClickEvent(self, event):
self.last = ("Double Click", self.last[1])
#log that it's a double click and pass on the index
def paintEvent(self, event):
qp = QtGui.QPainter(self.viewport())
qp.setRenderHints(qp.Antialiasing)
width = self.defaultSectionSize()
delta = self.height()
# add offset if the view is horizontally scrolled
qp.translate(self.sectionViewportPosition(0) - .5, -.5)
fmDelta = (self.fontMetrics().height() - self.fontMetrics().descent()) * .5
# create a reference rectangle (note that the negative height)
rect = QtCore.QRectF(0, 0, width, -delta)
for s in range(self.count()):
if self.isSectionHidden(s):
continue
qp.save()
qp.save()
qp.setPen(self.borderPen)
# apply a "shear" transform making the rectangle a parallelogram;
# since the transformation is applied top to bottom
# we translate vertically to the bottom of the view
# and draw the "negative height" rectangle
qp.setTransform(qp.transform().translate(s * width, delta).shear(-1, 0))
qp.drawRect(rect)
qp.setPen(QtCore.Qt.NoPen)
qp.setBrush(self.labelBrush)
qp.drawRect(rect.adjusted(2, -2, -2, 2))
qp.restore()
qp.translate(s * width + width, delta)
qp.rotate(-45)
qp.drawText(0, -fmDelta, str(self.model().headerData(s, QtCore.Qt.Horizontal)))
qp.restore()
class AngledTable(QtWidgets.QTableView):
def __init__(self, *args, **kwargs):
QtWidgets.QTableView.__init__(self, *args, **kwargs)
self.setHorizontalHeader(AngledHeader(self))
self.fixLock = False
def setModel(self, model):
if self.model():
self.model().headerDataChanged.disconnect(self.fixViewport)
QtWidgets.QTableView.setModel(self, model)
model.headerDataChanged.connect(self.fixViewport)
def fixViewport(self):
if self.fixLock:
return
self.fixLock = True
# delay the viewport/scrollbar states since the view has to process its
# new header data first
QtCore.QTimer.singleShot(0, self.delayedFixViewport)
def delayedFixViewport(self):
# add a right margin through the horizontal scrollbar range
QtWidgets.QApplication.processEvents()
header = self.horizontalHeader()
bar = self.horizontalScrollBar()
bar.blockSignals(True)
step = bar.singleStep() * (header.height() / header.defaultSectionSize())
bar.setMaximum(bar.maximum() + step)
bar.blockSignals(False)
self.fixLock = False
def resizeEvent(self, event):
# ensure that the viewport and scrollbars are updated whenever
# the table size change
QtWidgets.QTableView.resizeEvent(self, event)
self.fixViewport()
class TestWidget(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
l = QtWidgets.QGridLayout()
self.setLayout(l)
self.table = AngledTable()
l.addWidget(self.table)
model = QtGui.QStandardItemModel(4, 5)
self.table.setModel(model)
self.table.setHorizontalScrollMode(self.table.ScrollPerPixel)
self.table.headerlist = ['Column{}'.format(c + 1) for c in range(8)]
model.setVerticalHeaderLabels(['Location 1', 'Location 2', 'Location 3', 'Location 4'])
model.setHorizontalHeaderLabels(self.table.headerlist)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = TestWidget()
w.show()
sys.exit(app.exec_())
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.
With the following simple example (which works well with either PySide or PyQt4 in my computer):
import sys
import random
import numpy
from PySide import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(800, 500)
self.view = QtGui.QGraphicsView()
self.scene = QtGui.QGraphicsScene()
self.view.setScene(self.scene)
self.setWindowTitle('Example')
# Layout
layout = QtGui.QGridLayout()
layout.addWidget(self.view, 0, 0)
self.setLayout(layout)
# Styles
self.pen = QtGui.QPen(QtCore.Qt.black, 0, QtCore.Qt.SolidLine)
self.brush = QtGui.QBrush(QtGui.QColor(255, 255, 255, 255))
def addLine(self, x0, y0, x1, y1):
line = QtCore.QLineF(x0, -y0, x1, -y1)
pen = QtGui.QPen(QtGui.QColor(250, 0, 0, 255), 0, QtCore.Qt.SolidLine)
pen.setStyle(QtCore.Qt.CustomDashLine)
pen.setDashPattern([1, 4, 5, 4])
l = self.scene.addLine(line, pen)
def addRect(self, left, top, width, height):
rect = QtCore.QRectF(left, -top, width, abs(height))
r = self.scene.addRect(rect, self.pen, self.brush)
def fit(self):
self.view.fitInView(self.scene.sceneRect())
def resizeEvent(self, event = None):
self.fit()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
window.addRect(0, 1, 1, 1)
window.addLine(-1, -1, 2, 2)
window.addLine(0, 1, 1, 0)
window.fit()
sys.exit(app.exec_())
I am able to paint a two red lines that have constant width; which means that they do not change no matters the size (coordinates) of the square and lines and no matters if I re-size the window:
This is because I am using a QtGui.Qpen with width 0. However, if I use another width > 0, then the observed line width will change on window re-size (or will change if the square and lines have other dimensions too):
Is it possible to change (increase) the line width so that the observed lines are thicker than those obtained when the width is set to 0, while maintaining the same "observed" width on window resize or when the dimensions of the square/lines vary?
EDIT
Using setCosmetic(True), as suggested by o11c, has an unexpected behavior (at least I would not expect that to happen); it adds margins to the image (increases the size of scene.sceneRect()). These margins seem to be proportional to the width of the QPen when isCosmetic() == True:
A new question has been created related to this issue. See question 26231374 for more details:
According to the Qt C++ documentation, use the setCosmetic function on QPen. I assume the python wrapper exposes this.