In tkinter it is possible to create a 'Toplevel' window, and allow it to be transparent using the following code:
self._window = tkinter.Toplevel(self._tk_master)
self._window.wait_visibility()
self._window.wm_attributes('-alpha', 0.4)
self._canvas = tkinter.Canvas(self._window, width=self._width, height=self._height)
self._canvas.pack()
Now my question is how to create a non transparent rectangle with black background on the new transparent window?
I tried just creating a rectangle and set the 'fill' to black, but the rectangle seems to inherit the window transparency:
self._background_rectangle = self._canvas.create_rectangle(
0,
0,
self._width,
self._height,
fill="black")
I tried to create an image with 'RGBA' and set the transparency, but it seems to have no effect on the UI displayed:
alpha = 0.99
fill = tk_master.winfo_rgb(fill) + (int(alpha * 255),)
image_object = PIL.Image.new("RGBA", (width, height), fill)
image_object = PIL.ImageTk.PhotoImage(image_object)
self._canvas.create_image(x, y, image=image_object, anchor=tkinter.NW)
I also took an image, that contains only black background, and tried to add it, and still the image does not show!
image_object = PIL.Image.open(image_with_only_black_background)
image_object.resize((width, height), PIL.Image.ANTIALIAS)
image_object = PIL.ImageTk.PhotoImage(image_object)
self._canvas.create_image(x, y, image=image_object, anchor=tkinter.NW)
Does someone know how to solve this? Or what am I doing wrong?
Thanks!
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)
I have created a simple Tkinter application with a canvas widget like this:
from tkinter import *
root = Tk()
root.geometry("500x500-500+500")
canvas = Canvas(root, width = 400, height = 400, bg = "white")
canvas.pack()
canvas.create_line(0, 0, 200, 100, width = 20, fill = "black")
root.mainloop()
My question is, how can I get the color of the canvas in a specific position? Say for instance I clicked somewhere on the line, how can I get back the color "black" from that?
In other words, if I wanted a function like this,
def getColor(cnvs, event = None):
x = event.x
y = event.y
# somehow gets the color of cnvs at position (x, y) and stores it as color
return color
how would I go about doing that?
You can take a screen shot of the canvas using Pillow.ImageGrab module and get the required pixel color from the snapshot image:
from PIL import ImageGrab
def get_color(cnvs, event):
x, y = cnvs.winfo_rootx()+event.x, cnvs.winfo_rooty()+event.y
# x, y = cnvs.winfo_pointerx(), cnvs.winfo_pointery()
image = ImageGrab.grab((x, y, x+1, y+1)) # 1 pixel image
return image.getpixel((0, 0))
Note that the color returned is in (R, G, B) format.
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_())
I have the following code and I'm trying to change the text color of DC. I have searched the internet and found that SetTextForeground should be used for this, but somehow I'm unable to make it work.
import wx
class GUI():
def __init__(self):
self.InitUI()
def InitUI(self):
self.window = wx.Frame(None, wx.ID_ANY, "Example Title")
textList = ['text1', 'text2']
for i in range(len(textList)):
bmp = wx.Image('images/step_background.png').Rescale(160, 40).ConvertToBitmap()
bmp = self.drawTextOverBitmap(bmp, textList[i])
control = wx.StaticBitmap(self.window, -1, bmp, (0, 30*i+20), size=(160,30))
self.window.Show()
def drawTextOverBitmap(self, bitmap, text='', color=(0, 0, 0)):
dc = wx.MemoryDC(bitmap)
dc.SetTextForeground(color)
w,h = dc.GetSize()
tw, th = dc.GetTextExtent(text)
dc.DrawText(text, (w - tw) / 2, (h - th) / 2) #display text in center
return bitmap
if __name__ == '__main__':
app = wx.App()
gui = GUI()
app.MainLoop()
Do you have any ideas what am I doing wrong? I would be grateful for any idea.
Thank you
You are right, transparency is the issue here. Tried your code on a non-transparent image and it works fine displaying the color you set.
Quoting from the official docs:
In general wxDC methods don't support alpha transparency and the alpha
component of wxColour is simply ignored and you need to use
wxGraphicsContext for full transparency support.
So try creating a graphics context like:
dc = wx.MemoryDC(bmp)
gc = wx.GraphicsContext.Create(dc)
font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
gc.SetFont(font, "Red")
w,h = dc.GetSize()
tw, th = dc.GetTextExtent(textList[i])
gc.DrawText(textList[i], (w - tw) / 2, (h - th) / 2)