I'm totally stuck with 2 problems:
1) i'm using QSlider to set some values (they're float ~0.5, so i'm using *1000). SingleStep and PageStep work fine for keyboard input and mouse wheel, ticks're all set... But when i'm using mouse to drag the slider - it ignores all those ticks, steps, etc, and i want it to move only from one tick to another.
self.ui.x_coord.setMaximum(l_d*1000)
self.ui.x_coord.setSingleStep(l_d/N*1000)
self.ui.x_coord.setTickInterval(l_d/N*1000)
self.ui.x_coord.setTickPosition(QtGui.QSlider.TicksBothSides)
self.ui.x_coord.setPageStep(l_d/N * 10000)
is something missing here in my code (maybe smth like setMouseStep)?
2) That QSlider's connected to a function
self.graph = BPlot(self.ui)
self.ui.x_coord.valueChanged.connect(self.setCoordLabelValue)
....
def setCoordLabelValue(self):
x = self.ui.x_coord.value()/1000
y = self.ui.y_coord.value()/1000
self.graph.setCoordText(x,y)
....
class BPlot(QtGui.QGraphicsView):
def __init__(self, ui, parent=None):
super(BPlot, self).__init__(parent)
self.scene = QtGui.QGraphicsScene()
self.ui = ui
self.coordText = QtGui.QGraphicsTextItem(None, self.scene)
self.coordText.setPlainText("123")
self.x_offset = 40
self.y_offset = 20
self.currentPoint = QtGui.QGraphicsRectItem(None, self.scene)
self.cph = 4
self.cpw = 4
def resizeEvent(self, event):
size = event.size()
def showEvent(self, event):
aw = self.viewport().width()
ah = self.viewport().height()
self.scene.setSceneRect(0,0,aw,ah)
self.setScene(self.scene)
self.axis_pen = QtGui.QPen(QtCore.Qt.DashDotLine)
self.scene.addLine(0, 3/4*ah, aw, 3/4*ah, self.axis_pen)
self.normal_pen = QtGui.QPen(QtCore.Qt.SolidLine)
self.scene.addLine(self.x_offset, 3/4*ah - self.y_offset, aw - self.x_offset, 3/4*ah - self.y_offset)
self.currentPoint.setRect(self.x_offset - self.cpw/2, 3/4*ah - self.y_offset - self.cph/2, self.cpw, self.cph)
def setCoordText(self, x, y):
self.coordText.setPlainText(str(x) + ":" + str(y))
the problem is that setCoordText func doesn't redraw coordText. If i'll use print(coordText.toPlainText()) - i'll get the right output, but on the screen there'll be still "123" from __init__
I already tried adding self.scene.update() to the end of setCoordText, with no luck.
Ugh... Solved. Logic, where're u, my dear friend???
def setCoordLabelValue(self):
x = self.ui.x_coord.value()/1000
y = self.ui.y_coord.value()/1000
self.graph.setCoordText(x,y)
self.graph.invalidateScene()
.......
def paintEvent(self, event):
painter = QtGui.QPainter(self.viewport())
# set color and width of line drawing pen
painter.setPen(QtGui.QPen(QtCore.Qt.black, 2))
# drawLine(x1, y1, x2, y2) from point (x1,y1) to (x2,y2)
# draw the baseline
painter.drawText(10,20,str(x_coord))
# set up color and width of the bars
width = 20
painter.setPen(QtGui.QPen(QtCore.Qt.red, width))
delta = width + 5
x = 30
painter.end()
self.viewport().update()
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.
I'm using PYQT5 frame work for developing an application that will help to
annotate video, the video is being displayed in the QGraphicsScene Widget.
self.w = 742
self.h = 529
# Below code is responsible for displaying the scene & rectangle
self.scene = QtWidgets.QGraphicsScene(self.graphicsView_CameraFeed)
self.scene.drawBackground = self.drawBackground
self.graphicsView_CameraFeed.setScene(self.scene)
self.scene.setSceneRect(0, 0, self.w, self.h)
The video is being drawn on the widget using the Qpainter class which is being drawn on the scene continuously at the origin, the following was the output.
Using the above configurations when I was drawing the rectangles at [0,0] position the output was something which is not expected as rectangles has to be drawn on the top left since co-ordinates [0,0] are given.
But when I changed the sceneRectangle's Origin to the following
self.scene.setSceneRect(70,0,self.w,self.h)
I got the expected result i.e.
I don't know how it worked out, but due to changing of this value there are some other issues in my application.
Here is the sample code I'm using, please don't look for syntactical errors, I've added a part of my working code only.
def setupUi(self):
self.gridLayout_2 = QtWidgets.QGridLayout(self.tab)
self.gridLayout_2.setObjectName('gridLayout_2')
self.graphicsView_CameraFeed = QtWidgets.QGraphicsView(self.tab)
sizePolicy = \
QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.graphicsView_CameraFeed.sizePolicy().hasHeightForWidth())
self.graphicsView_CameraFeed.setSizePolicy(sizePolicy)
self.graphicsView_CameraFeed.setStyleSheet('background-color: rgb(203, 235, 255);'
)
self.graphicsView_CameraFeed.setObjectName('graphicsView_CameraFeed'
)
self.gridLayout_2.addWidget(self.graphicsView_CameraFeed, 1, 0,
1, 11)
def initUi(self):
# Below code is responsible for displaying the scene & rectangle
self.scene = \
QtWidgets.QGraphicsScene(self.graphicsView_CameraFeed)
self.scene.drawBackground = self.drawBackground
self.graphicsView_CameraFeed.setScene(self.scene)
self.scene.setSceneRect(0, 0, self.w, self.h)
def drawBackground(self, painter, rect):
height = self.graphicsView_CameraFeed.size().height()
width = self.graphicsView_CameraFeed.size().width()
self.imageScaled = self.image.scaled(width, height,
QtCore.Qt.KeepAspectRatioByExpanding)
painter.drawImage(rect.x(), rect.y(), self.imageScaled)
self.graphicsView_CameraFeed.fitInView(self.scene.sceneRect(),
QtCore.Qt.KeepAspectRatio)
def buttonClick(self):
x = 0
y = 0
w = 80
h = 200
box = QGraphicsRectItem(x, y, w, h)
self.scene.addItem(box)
I'm going to assume that you are adding your rect from the buttonClick function, I don't know how you are calling that function but I'm going to assume you didn't connect it to the QGraphicsView. So theres not always a overlap between the QGraphicsView and the QGraphicsScene, So you will need to map the view coordinates to scene coordinates:
def buttonClick(self):
x = 0
y = 0
w = 80
h = 200
scene_point = self.graphicsView_CameraFeed.mapToScene(x, y)
box = QGraphicsRectItem(scene_point.x(), scene_point.y(), w, h)
self.scene.addItem(box)
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_())
Hello I got a problem with using the Tkinter package on python. I want to create a window containing two major widgets of which one is a canvas that will later show a grid with cells. When initializing the canvas I use "create_rectangle" to fill the canvas with the desired objects. Also when clicking on a cell (for testing reasons) the canvas should change its color in the area of the rectangle. However when initialy displaying the window at first no objects can be seen (the expected result would be only white colored rectangles) and only when a Click on the canvas is performed the area changes its color as desired.
While looking through the internet I tried several variations of the order of the pack()- and create_rectangle()-methods. This is the code:
from tkinter import *
from tkinter.ttk import *
import cells
GRID_WIDTH = 15
GRID_HEIGHT = 15
class Ui(Frame):
""" Class to represent the ui of conways game of life"""
def __init__(self, parent, grid):
self.parent = parent
self.grid = grid
Frame.__init__(self, parent)
self.__setup()
def __setup(self):
""" Method to setup the components of the ui """
self.parent.title("Conway's game of life")
self.pack()
#Setup a frame to hold control components
frame_cntrl = Frame(self)
frame_cntrl.pack(side = RIGHT, anchor="n")
self.__setup_cntrl_components(frame_cntrl)
#Setup a frame to hold the Grid
self.canvas = Canvas(self)
self.canvas.pack(side = LEFT)
self.__draw_grid()
self.canvas.bind("<Button-1>", self.__canvas_clicked)
def __setup_cntrl_components(self, parent):
""" Method to setup the control elements of the ui"""
#Setup a label for the generation
self.lb_generation = Label(parent, text="dummy")
self.lb_generation.pack(side = TOP)
#Setup a button for iteration
self.bt_iteration = Button(parent, text="Iterate")
self.bt_iteration.pack(side = TOP)
def __draw_cell(self, x, y):
""" Draws a cell on the canvas"""
width, height = self.canvas.winfo_width(), self.canvas.winfo_height()
color = "black" if self.grid.cell_alive(x, y) else "white"
x0 = width * x / self.grid.width + 1
x1 = width * (x + 1) / self.grid.width
y0 = height * y / self.grid.height + 1
y1 = height * (y + 1) / self.grid.height
self.canvas.create_rectangle(x0, y0, x1, y1, width=0, fill=color)
def __draw_grid(self):
""" Method to setup the grid itself"""
width, height = self.canvas.winfo_width(), self.canvas.winfo_height()
for i in range(0, self.grid.width):
for j in range(0, self.grid.height):
self.__draw_cell(i, j)
def __canvas_clicked(self, event):
""" Method for when the cell was clicked """
x, y, width, height = event.x, event.y, self.canvas.winfo_width(), self.canvas.winfo_height()
cell_x = int(x / width * self.grid.width)
cell_y = int(y / height * self.grid.height)
self.grid.switch_cell(cell_x, cell_y)
self.__draw_cell(cell_x, cell_y)
if __name__ == "__main__":
Ui(Tk(), cells.Grid(GRID_WIDTH, GRID_HEIGHT)).mainloop()
Problem 1:
Your main problem is that, before the canvas is actually displayed, canvas.winfo_width() and canvas.winfo_height() do not return the canvas width and height, but the value 1.
I suggest you create the canvas as follows:
# Define canvas and save dimensions
self.canvas_width = 300
self.canvas_height = 200
self.canvas = Canvas(self, width = self.canvas_width,
height = self.canvas_height)
Then, in your code, replace each instance of:
width, height = self.canvas.winfo_width(), self.canvas.winfo_height()
with
width, height = self.canvas_width, self.canvas_height
Problem 2:
When creating each cell I don't think you need to add 1 to x0 and y0. Instead, it should be:
x0 = width * x / self.grid.width
x1 = width * (x + 1) / self.grid.width
y0 = height * y / self.grid.height
y1 = height * (y + 1) / self.grid.height
Is it possible to change the color of a frame background or any other widget to transparent light blue or any other transparent color?
Yes there is a way. Unfortunately it only works for the entire window (The window and all of the child widgets).
Here is a little demo I wrote up a while ago which does what you want, among other things.
Transparent Window Demo :
import Tkinter as Tk, re
class TransparentWin (Tk.Tk) :
''' Transparent Tk Window Class '''
def __init__ (self) :
Tk.Tk.__init__(self)
self.Drag = Drag(self)
''' Sets focus to the window. '''
self.focus_force()
''' Removes the native window boarder. '''
self.overrideredirect(True)
''' Disables resizing of the widget. '''
self.resizable(False, False)
''' Places window above all other windows in the window stack. '''
self.wm_attributes("-topmost", True)
''' This changes the alpha value (How transparent the window should
be). It ranges from 0.0 (completely transparent) to 1.0
(completely opaque). '''
self.attributes("-alpha", 0.7)
''' The windows overall position on the screen '''
self.wm_geometry('+' + str(439) + '+' + str(172))
''' Changes the window's color. '''
bg = '#3e4134'
self.config(bg=bg)
self.Frame = Tk.Frame(self, bg=bg)
self.Frame.pack()
''' Exits the application when the window is right clicked. '''
self.Frame.bind('<Button-3>', self.exit)
''' Changes the window's size indirectly. '''
self.Frame.configure(width=162, height=100)
def exit (self, event) :
self.destroy()
def position (self) :
_filter = re.compile(r"(\d+)?x?(\d+)?([+-])(\d+)([+-])(\d+)")
pos = self.winfo_geometry()
filtered = _filter.search(pos)
self.X = int(filtered.group(4))
self.Y = int(filtered.group(6))
return self.X, self.Y
class Drag:
''' Makes a window dragable. '''
def __init__ (self, par, dissable=None, releasecmd=None) :
self.Par = par
self.Dissable = dissable
self.ReleaseCMD = releasecmd
self.Par.bind('<Button-1>', self.relative_position)
self.Par.bind('<ButtonRelease-1>', self.drag_unbind)
def relative_position (self, event) :
cx, cy = self.Par.winfo_pointerxy()
x, y = self.Par.position()
self.OriX = x
self.OriY = y
self.RelX = cx - x
self.RelY = cy - y
self.Par.bind('<Motion>', self.drag_wid)
def drag_wid (self, event) :
cx, cy = self.Par.winfo_pointerxy()
d = self.Dissable
if d == 'x' :
x = self.OriX
y = cy - self.RelY
elif d == 'y' :
x = cx - self.RelX
y = self.OriY
else:
x = cx - self.RelX
y = cy - self.RelY
if x < 0 :
x = 0
if y < 0 :
y = 0
self.Par.wm_geometry('+' + str(x) + '+' + str(y))
def drag_unbind (self, event) :
self.Par.unbind('<Motion>')
if self.ReleaseCMD != None :
self.ReleaseCMD()
def dissable (self) :
self.Par.unbind('<Button-1>')
self.Par.unbind('<ButtonRelease-1>')
self.Par.unbind('<Motion>')
def __run__ () :
TransparentWin().mainloop()
if __name__ == '__main__' :
__run__()
For what it's worth, here's an arguably more readable version of the code in #rectangletangle's answer that has been re-formatted to more closely adhere to PEP 8 - Style Guide for Python Code recommendations.
Except for a trivial modification to the way the Tkinter module is imported so it will work in both Python 2 and 3, all other executable code is identical to his.
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
import re
class TransparentWin(tk.Tk):
""" Transparent Tkinter Window Class. """
def __init__(self):
tk.Tk.__init__(self)
self.Drag = Drag(self)
# Sets focus to the window.
self.focus_force()
# Removes the native window boarder.
self.overrideredirect(True)
# Disables resizing of the widget.
self.resizable(False, False)
# Places window above all other windows in the window stack.
self.wm_attributes("-topmost", True)
# This changes the alpha value (How transparent the window should be).
# It ranges from 0.0 (completely transparent) to 1.0 (completely opaque).
self.attributes("-alpha", 0.7)
# The windows overall position on the screen
self.wm_geometry('+' + str(439) + '+' + str(172))
# Changes the window's color.
bg = '#3e4134'
self.config(bg=bg)
self.Frame = tk.Frame(self, bg=bg)
self.Frame.pack()
# Exits the application when the window is right clicked.
self.Frame.bind('<Button-3>', self.exit)
# Changes the window's size indirectly.
self.Frame.configure(width=162, height=100)
def exit(self, event):
self.destroy()
def position(self):
_filter = re.compile(r"(\d+)?x?(\d+)?([+-])(\d+)([+-])(\d+)")
pos = self.winfo_geometry()
filtered = _filter.search(pos)
self.X = int(filtered.group(4))
self.Y = int(filtered.group(6))
return self.X, self.Y
class Drag:
""" Makes a window draggable. """
def __init__(self, par, dissable=None, releasecmd=None):
self.Par = par
self.Dissable = dissable
self.ReleaseCMD = releasecmd
self.Par.bind('<Button-1>', self.relative_position)
self.Par.bind('<ButtonRelease-1>', self.drag_unbind)
def relative_position(self, event):
cx, cy = self.Par.winfo_pointerxy()
x, y = self.Par.position()
self.OriX = x
self.OriY = y
self.RelX = cx - x
self.RelY = cy - y
self.Par.bind('<Motion>', self.drag_wid)
def drag_wid(self, event):
cx, cy = self.Par.winfo_pointerxy()
d = self.Dissable
if d == 'x':
x = self.OriX
y = cy - self.RelY
elif d == 'y':
x = cx - self.RelX
y = self.OriY
else:
x = cx - self.RelX
y = cy - self.RelY
if x < 0:
x = 0
if y < 0:
y = 0
self.Par.wm_geometry('+' + str(x) + '+' + str(y))
def drag_unbind(self, event):
self.Par.unbind('<Motion>')
if self.ReleaseCMD != None:
self.ReleaseCMD()
def dissable(self):
self.Par.unbind('<Button-1>')
self.Par.unbind('<ButtonRelease-1>')
self.Par.unbind('<Motion>')
def __run__():
TransparentWin().mainloop()
if __name__ == '__main__':
__run__()