How do I stop paintEvent from painting children widgets? - python

I'm trying to add rounded corners to a QDialog. I'm defining my own paintEvent method to create rounded corners. It's working, but it's adding rounded borders to everything. Even the cursor is getting a border. Is there any way to disable this behavior?
Example code:
from PySide import QtCore, QtGui
class RenameDialog(QtGui.QDialog):
def __init__(self, parent=None, **kwargs):
super(RenameDialog, self).__init__(
parent=parent, f=QtCore.Qt.CustomizeWindowHint)
self.fieldA = QtGui.QLineEdit(self)
self.fieldB = QtGui.QLineEdit(self)
self.setLayout(QtGui.QHBoxLayout())
self.layout().addWidget(self.fieldA)
self.layout().addWidget(self.fieldB)
# Set background transparent. Only items drawn in paintEvent
# will be visible.
palette = QtGui.QPalette()
palette.setColor(QtGui.QPalette.Base, QtCore.Qt.transparent)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
self.setPalette(palette)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
fillColor = QtGui.QColor(75, 75, 75, 255)
lineColor = QtCore.Qt.gray
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtGui.QPen(QtGui.QBrush(lineColor), 2.0))
painter.setBrush(QtGui.QBrush(fillColor))
painter.drawRoundedRect(event.rect(), 15, 15)
I'm trying to do this with a paintEvent because:
QDialog stylesheets cannot use border-radius. Curved borders do show up, but corners are still visible.
QDialogs.setMask() works, but there is no way (that I know of) to anti-alias the mask.
Here is what that looks like:

Paint events are sent to a window/widget with the precise rectangle that needs updating not the whole bounding rectangle of the widget. When you call event.rect() it returns the rectangle that needs updating (As far as I know)
Try changing this line
painter.drawRoundedRect(event.rect(), 15, 15)
To this
painter.drawRoundedRect(self.rect(), 15, 15)
EDIT:
You also need to add this line anywhere in the constructor
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
Hope this helps.

I've found a work-around for now. You can hide the extra borders by using QPainter.eraseRect on the children and having the correct stylesheet set. I've also found that painting over the offending area with QPainter.fillRect works too.
def paintEvent(self, event):
painter = QtGui.QPainter(self)
fillColor = QtGui.QColor(75, 75, 75, 255)
lineColor = QtCore.Qt.gray
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtGui.QPen(QtGui.QBrush(lineColor), 2.0))
painter.setBrush(QtGui.QBrush(fillColor))
painter.drawRoundedRect(event.rect(), 15, 15)
# Sketchy fix:
painter.eraseRect(self.childrenRect())
# OR
painter.fillRect(self.childrenRect(), QtGui.QBrush(fillColor))
This doesn't answer my original question though. I'd like to avoid this behavior rather than masking it. So I'm not going to mark this as the answer.

Related

Making Highly Customized, Hoverable, Overlappable, Widgets

I want to up my game in UI design using PyQt5. I feel like the resources for UI customization in PyQt5 are not easy to find. It is possible to try and make personalized widget, but the overall method seems non-standardized.
I need to build an arrow widget that is hoverable, overlappable with other widgets and highly customized. As I read in this tutorial and some other posts, it possible to do exactly what you need using paintEvent. Thus that is what I tried, but overall, I feel like the method is quite messy, and I'd like some guidelines on building complex Customized, general widget. Here's what I have:
Customized Shape: I built my code based on this
Hoverable property: I read everywhere that modifying the projects styleSheet is usually the way to go, especially if you want to make your Widget general and adapt to colors, the problem is that I wasn't able to find how to use properly self.palette to fetch the current colors of the QApplication styleSheet. I feel like i's have to maybe use enterEvent and leaveEvent, but I tried to redraw the whole widget with a painter in those functions and it said
QPainter::begin: Painter already active
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::setRenderHint: Painter must be active to set rendering hints
Overlappable Property: I found a previous post which seemed to have found a solution: create a second widget that is children of the main widget, in order to be able to move the children around. I tried that but it seems that it doesn't want to move, no matter the position I give the widget.
Here is my code:
import sys
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QGraphicsDropShadowEffect, QApplication, QFrame, QPushButton
from PyQt5.QtCore import Qt, QPoint, QLine
from PyQt5.QtGui import QPainter, QPen, QColor, QPalette
class MainWidget(QWidget):
def __init__(self):
super(MainWidget, self).__init__()
self.resize(500, 500)
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.myPush = QPushButton()
self.layout.addWidget(self.myPush)
self.arrow = ArrowWidget(self)
position = QPoint(-40, 0)
self.layout.addWidget(self.arrow)
self.arrow.move(position)
class ArrowWidget(QWidget):
def __init__(self, parent=None):
super(ArrowWidget, self).__init__(parent)
self.setWindowFlag(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.w = 200
self.h = 200
self.blurRadius = 20
self.xO = 0
self.yO = 20
self.resize(self.w, self.h)
self.layout = QHBoxLayout()
# myFrame = QFrame()
# self.layout.addWidget(myFrame)
self.setLayout(self.layout)
self.setStyleSheet("QWidget:hover{border-color: rgb(255,0,0);background-color: rgb(255,50,0);}")
shadow = QGraphicsDropShadowEffect(blurRadius=self.blurRadius, xOffset=self.xO, yOffset=self.yO)
self.setGraphicsEffect(shadow)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.begin(self)
# painter.setBrush(self.palette().window())
# painter.setPen(QPen(QPalette, 5))
ok = self.frameGeometry().width()/2-self.blurRadius/2-self.xO/2
oky = self.frameGeometry().height()/2-self.blurRadius/2-self.yO/2
painter.drawEllipse(QPoint(self.frameGeometry().width()/2-self.blurRadius/2-self.xO/2, self.frameGeometry().height()/2-self.blurRadius/2-self.yO/2), self.w/2-self.blurRadius/2-self.yO/2-self.xO/2, self.h/2-self.blurRadius/2-self.yO/2-self.xO/2)
painter.drawLines(QLine(ok-25, oky-50, ok+25, oky), QLine(ok+25, oky, ok-25, oky+50))
painter.end()
if __name__ == '__main__':
app = QApplication(sys.argv)
testWidget = MainWidget()
testWidget.show()
sys.exit(app.exec_())
If someone could help me make this work and explain along the way to help us better understand the structure of customized widgets and explain a better method that isn't messy like this one, I believe it would be a plus to the beginners like me using PyQt5 as a main Framework for UI making.
There is no "standard" method for custom widgets, but usually paintEvent overriding is required.
There are different issues in your example, I'll try and address to them.
Overlapping
If you want a widget to be "overlappable", it must not be added to a layout. Adding a widget to a layout will mean that it will have its "slot" within the layout, which in turn will try to compute its sizes (based on the widgets it contains); also, normally a layout has only one widget per "layout slot", making it almost impossible to make widget overlap; the QGridLayout is a special case which allows (by code only, not using Designer) to add more widget to the same slot(s), or make some overlap others. Finally, once a widget is part of a layout, it cannot be freely moved nor resized (unless you set a fixedSize).
The only real solution to this is to create the widget with a parent. This will make it possible to use move() and resize(), but only within the boundaries of the parent.
Hovering
While it's true that most widgets can use the :hover selector in the stylesheet, it only works for standard widgets, which do most of their painting by themself (through QStyle functions). About this, while it's possible to do some custom painting with stylesheets, it's generally used for very specific cases, and even in this case there is no easy way to access to the stylesheet properties.
In your case, there's no need to use stylesheets, but just override enterEvent and leaveEvent, set there any color you need for painting and then call self.update() at the end.
Painting
The reason you're getting those warnings is because you are calling begin after declaring the QPainter with the paint device as an argument: once it's created it automatically calls begin with the device argument. Also, it usually is not required to call end(), as it is automatically called when the QPainter is destroyed, which happens when the paintEvent returns since it's a local variable.
Example
I created a small example based on your question. It creates a window with a button and a label within a QGridLayout, and also uses a QFrame set under them (since it's been added first), showing the "overlapping" layout I wrote about before. Then there's your arrow widget, created with the main window as parent, and that can be moved around by clicking on it and dragging it.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ArrowWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# since the widget will not be added to a layout, ensure
# that it has a fixed size (otherwise it'll use QWidget default size)
self.setFixedSize(200, 200)
self.blurRadius = 20
self.xO = 0
self.yO = 20
shadow = QtWidgets.QGraphicsDropShadowEffect(blurRadius=self.blurRadius, xOffset=self.xO, yOffset=self.yO)
self.setGraphicsEffect(shadow)
# create pen and brush colors for painting
self.currentPen = self.normalPen = QtGui.QPen(QtCore.Qt.black)
self.hoverPen = QtGui.QPen(QtCore.Qt.darkGray)
self.currentBrush = self.normalBrush = QtGui.QColor(QtCore.Qt.transparent)
self.hoverBrush = QtGui.QColor(128, 192, 192, 128)
def mousePressEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
self.mousePos = event.pos()
def mouseMoveEvent(self, event):
# move the widget based on its position and "delta" of the coordinates
# where it was clicked. Be careful to use button*s* and not button
# within mouseMoveEvent
if event.buttons() == QtCore.Qt.LeftButton:
self.move(self.pos() + event.pos() - self.mousePos)
def enterEvent(self, event):
self.currentPen = self.hoverPen
self.currentBrush = self.hoverBrush
self.update()
def leaveEvent(self, event):
self.currentPen = self.normalPen
self.currentBrush = self.normalBrush
self.update()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setRenderHints(qp.Antialiasing)
# painting is not based on "pixels", to get accurate results
# translation of .5 is required, expecially when using 1 pixel lines
qp.translate(.5, .5)
# painting rectangle is always 1px smaller than the actual size
rect = self.rect().adjusted(0, 0, -1, -1)
qp.setPen(self.currentPen)
qp.setBrush(self.currentBrush)
# draw an ellipse smaller than the widget
qp.drawEllipse(rect.adjusted(25, 25, -25, -25))
# draw arrow lines based on the center; since a QRect center is a QPoint
# we can add or subtract another QPoint to get the new positions for
# top-left, right and bottom left corners
qp.drawLine(rect.center() + QtCore.QPoint(-25, -50), rect.center() + QtCore.QPoint(25, 0))
qp.drawLine(rect.center() + QtCore.QPoint(25, 0), rect.center() + QtCore.QPoint(-25, 50))
class MainWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.button = QtWidgets.QPushButton('button')
layout.addWidget(self.button, 0, 0)
self.label = QtWidgets.QLabel('label')
self.label.setAlignment(QtCore.Qt.AlignCenter)
layout.addWidget(self.label, 0, 1)
# create a frame that uses as much space as possible
self.frame = QtWidgets.QFrame()
self.frame.setFrameShape(self.frame.StyledPanel|self.frame.Raised)
self.frame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# add it to the layout, ensuring it spans all rows and columns
layout.addWidget(self.frame, 0, 0, layout.rowCount(), layout.columnCount())
# "lower" the frame to the bottom of the widget's stack, otherwise
# it will be "over" the other widgets, preventing them to receive
# mouse events
self.frame.lower()
self.resize(640, 480)
# finally, create your widget with a parent, *without* adding to a layout
self.arrowWidget = ArrowWidget(self)
# now you can place it wherever you want
self.arrowWidget.move(220, 140)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
testWidget = MainWidget()
testWidget.show()
sys.exit(app.exec_())

PyQt - Animations without redrawing everything?

So I'm trying to learn animations in PyQt and throughout all the examples I can find online, they all seem to use the self.update() or self.repaint() methods to increment the animations. This means basically the code has to erase then redraw the entire widget for every frame, even though much of what I intend to animate is static.
For example the code below, this generates a circular progress pie. The important bit is the paint() method under ProgressMeter (first class): for every frame in the animation this example paints the background, the actual progress pie, and the percentage indicator.
If I change the code to something like:
if self.angle > 120:
# do not draw background
then after 120 frames, the background does not get drawn anymore.
This seems terribly inefficient because (logically) the background should only be drawn once, no?
What would you recommend for animations like these?
Addendum: I have lurked a lot on this site to steal examples and code, but haven't posted for a long time. Please let me know about proper etiquette etc if I am not following it properly.
import sys
from PyQt4 import QtGui, QtCore
class ProgressMeter(QtGui.QGraphicsItem):
def __init__(self, parent):
super(ProgressMeter, self).__init__()
self.parent = parent
self.angle = 0
self.per = 0
def boundingRect(self):
return QtCore.QRectF(0, 0, self.parent.width(),
self.parent.height())
def increment(self):
self.angle += 1
self.per = int(self.angle / 3.6)
if self.angle > 360:
return False
else:
return True
def paint(self, painter, option, widget):
self.drawBackground(painter, widget)
self.drawMeter(painter, widget)
self.drawText(painter)
def drawBackground(self, painter, widget):
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtCore.Qt.NoPen)
p1 = QtCore.QPointF(80, 80)
g = QtGui.QRadialGradient(p1 * 0.2, 80 * 1.1)
g.setColorAt(0.0, widget.palette().light().color())
g.setColorAt(1.0, widget.palette().dark().color())
painter.setBrush(g)
painter.drawEllipse(0, 0, 80, 80)
p2 = QtCore.QPointF(40, 40)
g = QtGui.QRadialGradient(p2, 70 * 1.3)
g.setColorAt(0.0, widget.palette().midlight().color())
g.setColorAt(1.0, widget.palette().dark().color())
painter.setBrush(g)
painter.drawEllipse(7.5, 7.5, 65, 65)
def drawMeter(self, painter, widget):
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(widget.palette().highlight().color())
painter.drawPie(7.5, 7.5, 65, 65, 0, -self.angle * 16)
def drawText(self, painter):
text = "%d%%" % self.per
font = painter.font()
font.setPixelSize(11)
painter.setFont(font)
brush = QtGui.QBrush(QtGui.QColor("#000000"))
pen = QtGui.QPen(brush, 1)
painter.setPen(pen)
# size = painter.fontMetrics().size(QtCore.Qt.TextSingleLine, text)
painter.drawText(0, 0, 80, 80,
QtCore.Qt.AlignCenter, text)
class MyView(QtGui.QGraphicsView):
def __init__(self):
super(MyView, self).__init__()
self.initView()
self.setupScene()
self.setupAnimation()
self.setGeometry(300, 150, 250, 250)
def initView(self):
self.setWindowTitle("Progress meter")
self.setRenderHint(QtGui.QPainter.Antialiasing)
policy = QtCore.Qt.ScrollBarAlwaysOff
self.setVerticalScrollBarPolicy(policy)
self.setHorizontalScrollBarPolicy(policy)
self.setBackgroundBrush(self.palette().window())
self.pm = ProgressMeter(self)
self.pm.setPos(55, 55)
def setupScene(self):
self.scene = QtGui.QGraphicsScene(self)
self.scene.setSceneRect(0, 0, 250, 250)
self.scene.addItem(self.pm)
self.setScene(self.scene)
def setupAnimation(self):
self.timer = QtCore.QTimeLine()
self.timer.setLoopCount(0)
self.timer.setFrameRange(0, 100)
self.animation = QtGui.QGraphicsItemAnimation()
self.animation.setItem(self.pm)
self.animation.setTimeLine(self.timer)
self.timer.frameChanged[int].connect(self.doStep)
self.timer.start()
def doStep(self, i):
if not self.pm.increment():
self.timer.stop()
self.pm.update()
app = QtGui.QApplication([])
view = MyView()
view.show()
sys.exit(app.exec_())
The Qt's documentation about QWidget repaint slot says:
Repaints the widget directly by calling paintEvent() immediately, unless updates are disabled or the widget is hidden.
We suggest only using repaint() if you need an immediate repaint, for example during animation. In almost all circumstances update() is better, as it permits Qt to optimize for speed and minimize flicker.
Warning: If you call repaint() in a function which may itself be called from paintEvent(), you may get infinite recursion. The update() function never causes recursion.
That should give you an answer about when to use or not repaint or update slots.
About making animations I'd suggest you also to take a look to the Qt4's animation framework or Qt5's animation framework, which is a really powerful way to animate widgets on Qt.

QGraphicsPathItem different colors

Is there a way to get 2 different fill colors for a single QGraphicsPathItem object?
For example:
# Try to get a white text onto a grey rectangle
itemPath = QtGui.QPainterPath()
itemPath.setFillRule(QtCore.Qt.WindingFill)
self.setBrush( QtGui.QColor(100, 100, 100) )
itemPath.addRect(-10, -60, 150, 70)
itemFont = QtGui.QFont()
itemFont.setPointSize(50)
self.setBrush( QtGui.QColor(255, 255, 255) )
itemPath.addText(0, 0, itemFont, txt)
Right now it's just using the last brush color for the rectangle and text. I'd like to color them differently though, but still as the same QGraphicsPathItem. Or better yet, a way to give the text a background so it's easier to select.
Update
Here's an example. I want a white text with a red background, but I only get the last used brush color.
from PySide import QtGui, QtCore
class TextItem(QtGui.QGraphicsPathItem):
def __init__(self):
super(TextItem, self).__init__()
itemPath = QtGui.QPainterPath()
itemPath.setFillRule(QtCore.Qt.WindingFill)
# Create rectangle with red color
self.setBrush( QtGui.QColor(255, 0, 0) )
itemPath.addRect(-10, -50, 130, 60)
self.setPath(itemPath)
# Create text with white color
itemFont = QtGui.QFont()
itemFont.setPointSize(40)
self.setBrush( QtGui.QColor(255, 255, 255) )
itemPath.addText(0, 0, itemFont, 'Test!')
self.setPath(itemPath)
self.moveBy(100, 100)
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(500, 500)
self.view = QtGui.QGraphicsView(self)
self.view.setScene( QtGui.QGraphicsScene(self) )
self.view.setSceneRect( 0, 0, 500, 500 )
self.view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
mainLayout = QtGui.QVBoxLayout()
mainLayout.addWidget(self.view)
self.setLayout(mainLayout)
newItem = TextItem()
self.view.scene().addItem( newItem )
def run(self):
self.show()
win = Window()
win.show()
The problem is that your first self.setPath and self.setBrush calls are just over-ridden by the second ones. These properties of the QGraphicsPathItem are not used until you get to the paint event. So the first settings do nothing and the values left in these properties after the initialisation are the ones used to paint the item.
It's not too difficult to do what you want but you have quite a lot of options, and you need to choose based on other aspects of your design.
The simplest option would be to just create a QGraphicsRectItem and then give it a QGraphicsSimpleTextItem child. This is particularly simple in that you wouldn't even need to create a subclass of your own: simply create the instances, set their Paths and Brushes and use them directly. I strongly recommend this approach if it does what you neeed.
rectItem = QtGui.QGraphicsRectItem(0, 0, 130, 60)
rectItem.setBrush(QtGui.QColor(255, 0, 0))
textItem = QtGui.QGraphicsSimpleTextItem("Test!",rectItem)
itemFont = QtGui.QFont()
itemFont.setPointSize(40)
textItem.setBrush( QtGui.QColor(200, 200, 200) )
textItem.setFont(itemFont)
textItem.setPos(10,0)
self.view.scene().addItem( rectItem )
rectItem.moveBy(100,100)
Another simple option would be just create the two items and then add them into a QGraphicsItemGroup. This makes sense if you might want to separate the items or work with them separately.
rectItem = QtGui.QGraphicsRectItem(0, 0, 130, 60)
rectItem.setBrush(QtGui.QColor(255, 0, 0))
self.view.scene().addItem( rectItem )
textItem = QtGui.QGraphicsSimpleTextItem("Test!")
itemFont = QtGui.QFont()
itemFont.setPointSize(40)
textItem.setBrush( QtGui.QColor(200, 200, 200) )
textItem.setFont(itemFont)
textItem.setPos(10,0)
self.view.scene().addItem( textItem )
group = QtGui.QGraphicsItemGroup()
group.addToGroup(rectItem)
group.addToGroup(textItem)
group.moveBy(100,100)
self.view.scene().addItem( group )
If you need to create a class of your own for other reasons then you have quite a few options. The most general will be to subclass QGraphicsItem. You can create a MyTextItem with, for example, two Paths and two Brushes, and use them both when you paint the Item. You will have to implement boundingRect and paint but in this case both will be quite easy. This is probably a good way to go if you foresee adding lots of additional functionality to the item.
Other options would be to subclass QRectItem and add a text path to it. Again you will need to re-implement paint, but that might be all that was needed. Or you could subclass QSimpleTextItem and add a rect to it.

wxPython DrawRectangle Not Filled with Black Background

I am using python 2.7 and the most up to date version of wxpython for it (3.0).
I am trying to draw a rectangle that is not filled. When I run the code below, it draws a red rectangle with the proper thickness, but the inside is white and not my background color (black).
def __init__(self, parent):
wx.Frame.__init__(self, parent, title="Maze")
self.SetBackgroundColour('black')
self.Bind(wx.EVT_PAINT, self.OnPaint)
def OnPaint(self, event=None):
self.dc = wx.PaintDC(self)
self.dc.Clear()
self.dc.SetPen(wx.Pen(wx.RED, 2))
self.dc.DrawRectangle(50, 50, 50, 50)
I don't really understand why I am having this problem and any help would be greatly appreciated. I apologize if this is a trivial answer, as I am new to wxpython.
DrawRectangle is drawed with the current pen and filled with the current brush. So you must also call self.dc.SetBrush.

How to draw non-transparent text on an icon (not bitmap) using wxpython?

Short question:
I know how to draw text on a wx.Bitmap, but how can I draw text on a wx.Icon in wxpython so that it does not appear transparent?
Long question:
I have a wxpython based GUI application, that has a taskbar icon, which I set using mytaskbaricon.SetIcon("myicon.ico").
Now I would like to dynamically put some text on the icon, so I tried to use the wx .DrawText method as explained here.This works fine if I test this for bitmaps (which I use in menu items).
However, the taskbar requires an wxIcon instead of a wxBitmap. So I figured I'll convert the icon to a bitmap, draw the text, and then convert it back to an icon. This works, except that the text is not shown transparent. Why ? And how can I make the text NOT transparent ?
My code is as roughly follows:
import wx
class MyTaskBarIcon(wx.TaskBarIcon):
...
icon = wx.Icon("myicon.ico", wx.BITMAP_TYPE_ICO)
bmp = wx.Bitmap("myicon.ico", wx.BITMAP_TYPE_ICO)
memDC = wx.MemoryDC()
memDC.SetTextForeground(wx.RED)
memDC.SelectObject(bmp)
memDC.DrawText("A", 0, 0)
icon.CopyFromBitmap(bmp)
self.SetIcon(icon, APP_NAME_WITH_VERSION)
...
So, no errors raised and myicon.ico is shown, but the letter A is transparant (instead of red). If I use a .bmp file to start with (myicon.bmp) the text appears in the correct color (but the borders are jagged). I've played around with masks, foreground and background colors, but that didn't help.
(I am using Windows 7, Python 2.6, wxpython 2.8)
Edit: I've shortened my explanation, and made the code more self-contained
Short answer: It seems to me that there is a bug in this particular piece of wx code. I am going to report it and see what comes out of it.
Long answer: You can hack your way around. Setup a color, which is not used in the image. Then draw using that color and when done, fix alpha values and color of those pixels to match your expectation:
import wx
from wx import ImageFromStream, BitmapFromImage, EmptyIcon
import cStringIO, zlib
# ================================ ICON ======================================
def getData():
return zlib.decompress(
'x\xda\x01\x97\x03h\xfc\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\
\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\
\x08\x08\x08|\x08d\x88\x00\x00\x03NIDAT8\x8dm\xd2ML\x9bu\x00\xc7\xf1\xef\xf3\
<\xed\xda><\xa3#\xcb\x8a\x0cp\xac8\x15\x87\x89/ \x11\xd1d&:5&#n\xc9\\\xa2\
\xc6\xc3b\xe2\xd1y0Y2\xa3q^\xcc\xb8\x9a\xb9\xf9rQc\xc6\x0es\xa4\xd1\x91\xe98\
\xc8\x96\xb98H\xc3\x8b\xc0\xc6\x00\x91\xd2\xb2\xa7}\xda\xe7\xa5\xcf\xd3\xf6\
\xf9{0\xa2\x07\xbf\xf7_\xf29\xfc$\x00\xf1>\xb2\xd9\xc7\tI0$\xc0\xd5d\x06\xa5\
\x17q\xf9O\xa5\x0b$$\x85KB\xa2\xec\xcb\xbc\x1e}\x81\xdf\x01$q\x9a`>\xce\xc9`\
\xc7\x91#\xa1\xce\xa3;\xed\xdbg\xb3s\x19c\xe1\x9cz\xbe*A\x0f\x80\x80\xf4A\
\xeb\xb0\xfcPG\xa2;\x10\x8aI\xe5\xd9\x93\x8bB\xe6`l\x88U)\xf3-\xc7\xc3\xbb_{\
;r\xef\xe1Vci\xa4\xb0\xbc:\x17\xb8\xdczQ\xd3B5"A\x1f\x00\xa7"\xe39\x16\xfb\
\xd6_\xb1wu\x1f#\xa9\x15-k\xe6\xd4j\xa2D\xbf\xec\x95\x91\xe5PGX_\x18),.\xcei\
W\xdb\xbf\xd3:\xb7{49\x0e\xeem\x1dkAG+Z\xb4l\xdf\xc6o-\xc3\xea\x9fK\xbf\x84\
\xe5\xa6\xfe&\xa1>\xa8\xad)\xec\x96n}\xc6`E\xa8g7\x95d\xdbD\xf2\x82\xda\xae\
\x06\x08\xd95\x1e\xeej\xa2\xa1^F \xa1\x1b5\xae\xcf\xe5\xa8D\x14\xea\xf4\xf3\
\xdco\x9es\xb7\x9933\xe1Z\xe9U\t\xe0\xd8\xe7\x17?\t4\xecz7\x99\xd0hp\x05\x87\
\xf6u\x927\x0c6-\x87\xf6\xd6\x16\x00\xaa\x02\xbeN\xdd\xc2\xd7\x04\x99\xec:9K\
\xf9\xf8\xd37\x07\x8e\xcb\x00\x99\xca=\xbd\xbe\x00\xbf\xe4\xb1wO\x0c\xbb*\
\x08\x06\x83\x8c\xfd\xf8\x03E\xc3\xa0\xe2\xba\\\x1a\xfb\x99\xee=q\x8c\xac\
\x83#7RtC\x03\x00\x01\x80r\xd9\xea\xa9z2\x86\xeb\x13\x8bEpk\x82:U\xe5\x8f\
\x95\x15\xc6~\x1a\'=5\xc9\xb3\xcf\xef\xa7q\x87Jn\xd3A4\x04)\x97\xad\x1e\x00\
\x19\xc0\xb3-,\xbb\x82\xe3\xf9\xb85\xa8\xf8\x905J\xd4i\x1a\xe9\xa9I^:0\xc4#\
\xbd}\xb8U\xa8x>\x96]\xc1\xb3-\xb6\x04^\xd9N\x17K\x91gv\xc6\x03,el\xeek\x8b\
\x82\x1c\xe6\xd1\xc7\xfby\xa0g/j\xb4\x1e\xd3\x85\xd5\x8cE0"\x91+\xd9xe;\xfd\
\xaf\xc0\xb1\xae\x14\r\x03\xbd\xecr\xf5\xe6\x06\xc1\x10\xd4\x85\x83<5\xf8$\
\xf1\xc6zB\x80\x16\x86_of\xf1\xf0(\x1a\x06\x9ec]\xd9\x12\xb8\xb63\xea:\xe6\
\xa1\xd9\x9a\xd2-\xb7U\xf9bD\xf0\\o\x82\xaeD\x1d\x08X\xc9Z\x8c^\xcbP4\xd6\
\x99\xdf\xb00\xf3k3\x08e\x14#\xfa\xe7\xeb}GO\xbd\xf5Xr\xc7\xf0BAS[\xe3\x1a\
\xb1P\x08\xc5\x97\xa9\xf9\x82\x8aT\xc5\xf0\\\xaa\xd5*\xaa\xb8k\xa7\xefl\xbes\
\xfd\xcc\xb1\xd3[\x02\x80\xe17\x9e\x98\x8fF\xa3jv3_;12\xaf\xccJ*\xb2\x12\x06\
\xc0\xaf\x95iV+\xbc\xf7rR\xc8rcD\xa2kv\xe0\xcc\xdf;\x19 \x95J5\x17\n\x85\xef\
\xc3\xe10f\xa9`\x98\xf9;\x1f\xda\xb9\xe9qk\xe3\x86nm\xdc\xd0\xed\xdc\xf4\xf8\
\xf2\xf2\xfc\x07\x85B\xdel\x8e\xc7%]\xd7/\xa7R\xa9\xe4\x96\xc04M\xc7q\x9c\
\xb5\x89\x89\x89N!\xc4\xd3S\xdf|4\xcd\xfftw\xff\x97_]\xd3\xf5I\xc0\xf2}\xdf\
\x02\xf8\x0b\xc1.\x9e\xd8Y.\x85\x85\x00\x00\x00\x00IEND\xaeB`\x822\x86\xba\
\xb3' )
def getBitmap():
return BitmapFromImage(getImage())
def getImage():
stream = cStringIO.StringIO(getData())
return ImageFromStream(stream)
def getIcon():
icon = EmptyIcon()
icon.CopyFromBitmap(getBitmap())
return icon
# ============================================================================
class MainWindow(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.number = 0
self.Bind(wx.EVT_CLOSE, self.OnClose)
self.panel = wx.Panel(self)
self.button = wx.Button(self.panel, label="Test")
self.button.Bind(wx.EVT_BUTTON, self.OnButton)
self.tbicon = wx.TaskBarIcon()
self.tbicon.SetIcon(getIcon(), "Test")
self.sizer = wx.BoxSizer()
self.sizer.Add(self.button)
self.panel.SetSizerAndFit(self.sizer)
self.Show()
# --------------------------------------------------------------------------
def OnClose(self, e):
self.tbicon.Destroy()
self.Destroy()
wx.Exit()
# --------------------------------------------------------------------------
def OnButton(self, e):
# HERE WE GO!
self.number += 1
bitmap = getBitmap()
# Find unused color
image = bitmap.ConvertToImage()
my_solid_color = wx.Color(*image.FindFirstUnusedColour(0, 0, 0)[1:])
# Use the unused *unique* color to draw
dc = wx.MemoryDC()
dc.SetTextForeground(my_solid_color)
dc.SelectObject(bitmap)
dc.DrawText(str(self.number), 0, 0)
dc.SelectObject(wx.NullBitmap)
# Convert the bitmap to Image again
# and fix the alpha of pixels with that color
image = bitmap.ConvertToImage()
for x in range(image.GetWidth()):
for y in range(image.GetHeight()):
p = wx.Colour(image.GetRed(x, y),
image.GetGreen(x, y),
image.GetBlue(x, y))
if p == my_solid_color:
image.SetAlpha(x, y, 255) # Clear the alpha
image.SetRGB(x, y, 0, 0, 0) # Set the color that we want
# Convert back to Bitmap and save to Icon
bitmap = image.ConvertToBitmap()
icon = wx.IconFromBitmap(bitmap)
self.tbicon.SetIcon(icon, "Test")
app = wx.App(False)
win = MainWindow(None)
app.MainLoop()
Note: A had to add some icon. You can ignore that part of the code.
Just a guess, but perhaps create your initial icon as an "EmptyIcon", then copy the bmp to it.
import wx
class MyTaskBarIcon(wx.TaskBarIcon):
...
icon = wx.EmptyIcon()
bmp = wx.Bitmap("myicon.ico", wx.BITMAP_TYPE_ICO)
bmp = WriteTextOnBitmap("A", bmp, color=wx.RED) #this function is as in the link above
icon.CopyFromBitmap(bmp)
self.SetIcon(icon, APP_NAME_WITH_VERSION)
...

Categories

Resources