Here's some sample code:
import wx
class MainPanel(wx.Panel):
p1 = None
p2 = None
def __init__(self, parent):
wx.Panel.__init__(self, parent=parent)
self.frame = parent
#self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
#self.bg = wx.Bitmap("2.jpg")
self.Bind(wx.EVT_MOTION, self.OnMouseMove)
self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.SetCursor(wx.StockCursor(wx.CURSOR_CROSS))
def OnMouseMove(self, event):
if event.Dragging() and event.LeftIsDown():
self.p2 = event.GetPosition()
self.Refresh()
def OnMouseDown(self, event):
self.p1 = event.GetPosition()
def OnPaint(self, event):
if self.p1 is None or self.p2 is None: return
dc = wx.PaintDC(self)
dc.SetPen(wx.Pen('red', 3, wx.LONG_DASH))
dc.SetBrush(wx.Brush(wx.Color(0, 0, 0), wx.SOLID))
dc.DrawRectangle(self.p1.x, self.p1.y, self.p2.x - self.p1.x, self.p2.y - self.p1.y)
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, size=(600,450))
panel = MainPanel(self)
self.Center()
class Main(wx.App):
def __init__(self, redirect=False, filename=None):
wx.App.__init__(self, redirect, filename)
dlg = MainFrame()
dlg.Show()
if __name__ == "__main__":
app = Main()
app.MainLoop()
The two offending lines are:
#self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
#self.bg = wx.Bitmap("2.jpg")
If you run the code like that, you're able to draw a rectangle on the panel. If you start a new rectangle, the old one disappears, because the panel is refreshing.
However, if you uncomment those two lines, when you draw a rectangle, it stays there. You can have infinite number of rectangles. If I use dc.Clear(), the background disappears and reloading the background makes the application slow and it flickers.
How do I make the panel fully refresh while using a custom background?
By the way, the background image doesn't load with this code, but the behavior is the same.
EDIT: I found a workaround to the flickering caused by using dc.Clear() and reloading the background. Setting double buffering on the panel to true solves the flickering:
self.SetDoubleBuffered(True)
I will use that, but I'll leave the question open in case someone knows the answer to the initial problem.
When using the wx.BG_STYLE_CUSTOM background style it assumes that you will paint (or clear) the entire window in the EVT_PAINT event handler. The easiest way to do that without flicker is to only push pixels to the screen one time per paint event.
Using SetDoubleBuffered does that at the system level, although that has been known to have side-effects in some cases. Another easy way is to use a wx.BufferedPaintDC instead of wx.PaintDC. It will create a bitmap that is the target of all the drawing operations and then at the end the contents of the bitmap will be flushed to the screen.
Related
I am writing a custom button control in wxPython 4.1.1 on Python 3.9.12 using MacOS 12.2. The custom button uses a bitmap background with text written on it. The bitmap is transparent so parts of underlying controls can be seen through it. This worked in 4.0.7-Pre2, but no longer. I have reduced the problem to the use of MemoryDC to create a a buffered copy of the background image.When BufferedPaintDC or MemoryDC are used, the alpha channel disappears and the transparent region is black. However, if I don't use a buffered DC and draw directly on the control, everything still works. Why is this?
The minimum code to reproduce looks like:
import wx
class MyButton(wx.Control):
def __init__(self, parent, id, text, **kwargs):
wx.Control.__init__(self,parent, id, **kwargs)
self.Bind(wx.EVT_PAINT,self._onPaint)
self.myBG = self.MakeBG()
def MakeBG(self):
newBG = wx.Bitmap(50,50)
mdc = wx.MemoryDC()
mdc.SelectObject(newBG)
mdc.SetBackground(wx.Brush(wx.Colour(0,0,255, wx.ALPHA_TRANSPARENT)))
mdc.Clear()
mdc.DrawText("HELLO", 0,0)
return newBG
def _onPaint(self, event):
dc = wx.PaintDC(self)
dc.DrawBitmap(self.myBG, 0,0, True)
class MyFrame(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(200,100))
button = MyButton(self, -1 , "TEST BUTTON")
self.BackgroundColour = wx.Colour(255,0,0)
self.Show(True)
self.Refresh()
if __name__ == "__main__":
app = wx.App(False) # Create a new app, don't redirect stdout/stderr to a window.
frame = MyFrame(None, "Hello World") # A Frame is a top-level window.
app.MainLoop()
However, if I change the paint event to not use the bitmap and draw directly with the PaintDC, it works.
def _onPaint(self, event):
dc = wx.PaintDC(self)
dc.SetBackground(wx.Brush(wx.Colour(0,0,255, wx.ALPHA_TRANSPARENT)))
dc.Clear()
dc.DrawText("HELLO", 0,0)
I think the problem is with retaining the alpha channel, but I think something deeper is wrong because a BufferedPaintDC also has the same problem.
I created a small GUI that allows me to draw a number. That number is supposed to be classified with a CNN. The CNN is not connected to this GUI yet. Will do that later on. I am still very new to PyQt5 and used some code that i found online for the drawing with QImage. It works, but at the moment i can draw all over the GUI. Is it possible to place that in a widget? So that I can only draw inside a specific frame and not all over the GUI?
So basically how can i get the self.image = QImage(...) iside a widget or something that i created earlier on my GUI? Is that possible somehow or would you even suggest to solve it in totaly different way?
import sys
from PyQt5 import QtWidgets
from PyQt5.QtGui import QIcon, QImage, QPainter, QPen, QBrush
from PyQt5.QtCore import Qt, QPoint
from UI.mainwindow_2 import Ui_MainWindow
import matplotlib.pyplot as plt
import numpy as np
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.image = QImage(self.size(), QImage.Format_RGB32)
self.image.fill(Qt.white)
self.drawing = False
self.brushSize = 28
self.brushColor = Qt.black
self.lastPoint = QPoint()
self.ui.Clear.clicked.connect(self.clear)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.drawing = True
self.lastPoint = event.pos()
def mouseMoveEvent(self, event):
if(event.buttons() & Qt.LeftButton) & self.drawing:
painter = QPainter(self.image)
painter.setPen(QPen(self.brushColor, self.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.drawLine(self.lastPoint, event.pos())
self.lastPoint = event.pos()
self.update()
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
self.drawing = False
def paintEvent(self, event):
canvasPainter = QPainter(self)
canvasPainter.drawImage(self.rect(),self.image, self.image.rect())
def clear(self):
self.image.fill(Qt.white)
self.update()
def main():
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
First of all, the following aspects must be considered:
paint events are received by any (visible) QWidget subclass
painting is always constricted to the geometry of the widget
painting always happens from the bottom to the top, so whenever a widget has some children, whatever has been painted on that parent widget will be potentially covered by those children
So, the first thing to do is to implement the paintEvent only on the actual widget for which you want to paint. Since you're using Designer, this makes things a bit more complex, as there is no way to subclass a widget that already exists in an ui.
Luckily, Qt has a concept called "promoted widgets": it allows to "expand" a certain widget by specifying the custom subclass that will be actually used when the ui will be finally generated in the program.
choose which widget in your ui will be used for painting; I suggest you to start from a basic QWidget, I'll explain more about this later;
right click on it, and select "Promote to..." from the context menu;
in the "Promoted class name" type the exact class name you are going to use (let's say "Canvas");
in the "Header file" field, type the file name that will contain that class, as it would appear in an import statement (meaning that it should not have the py extension!); assuming you want to do everything in a single script and your script is named "mycanvas.py", type "mycanvas" in that field;
ensure that the "Base class name" combo is set to the exact class type of the widget you've chosen (QWidget, in this case, which is usually automatically selected)
click "Add" and then "Promote";
save the ui and generate the file with pyuic;
Now, the implementation of your mycanvas.py file is simple:
class Canvas(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.image = QImage(self.size(), QImage.Format_RGB32)
self.image.fill(Qt.white)
self.drawing = False
self.brushSize = 28
self.brushColor = Qt.black
self.lastPoint = QPoint()
# ... all the painting related methods, as you did in your code
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.ui.Clear.clicked.connect(self.ui.canvas.clear)
def main():
# ...
Two considerations:
a QFrame usually has some borders, and since painting can always happen within the whole widget size, you might end up painting on the borders too. If you want a QFrame and paint inside that widget, then add a QWidget as a child to that frame and ensure that a layout is set for the frame;
setting the QImage size within the init is not a good approach, as the widget might change its size or it could be initialized with a size smaller than it will eventually have; you either set a fixed size in the __init__ before creating the QImage, or you keep track of the points/lines in an internal container and continuously draw the contents in the paintEvent;
Database Loading Panel Screen StaticText is Blurred Out when Shown
Hello,
I have an odd issue with a loading screen. The problem is this: When I start with a main screen, then when you click on the 'next' button,
it goes to the database loading screen, when it is done, it prints on the screen that the Database Loading is complete.
The problem is that the loading screen text is blurred out for some reason. Also, something I discovered is that if you pop up a message dialog
box, the loading text is shown...
In this example, I am not actually loading into an actual database. Instead, I am just sleeping for seven seconds.
If you are confused as to what I am saying, just run the example, and you will see that the loading screen is all messed up...
Here is the code:
import wx
import time
class TextPanel(wx.Panel):
def __init__(self, parent, label):
self.__myParent = parent
wx.Panel.__init__(self, self.__myParent, size = (800, 800))
staticText = wx.StaticText(self, label = label)
class MainPanel(wx.Panel):
def __init__(self, parent):
self.__myParent = parent
wx.Panel.__init__(self, self.__myParent, size = (800, 800))
nextButton = wx.Button(self, label = 'Next')
nextButton.Bind(wx.EVT_BUTTON, self.__onNext)
def __onNext(self, event):
self.__myParent.onNextScreen()
class MainFrame(wx.Frame):
def __init__(self):
# Base contsructor.
wx.Frame.__init__(self, None, id = -1, title = 'Testing...', size = (800, 800))
self.__myMainPanel = MainPanel(self)
self.__myMainPanel.Show()
self.__myDatabase = TextPanel(self, 'Loading Data...')
self.__myDatabase.Hide()
self.__myFinalPanel = TextPanel(self, 'Database Loading Complete!')
self.__myFinalPanel.Hide()
def onNextScreen(self):
self.__myMainPanel.Hide()
self.__myDatabase.Show()
self.doDatabaseLoad()
self.__myDatabase.Hide()
self.__myFinalPanel.Show()
def doDatabaseLoad(self):
time.sleep(7) # before, this method would load data into a database...
if __name__ == '__main__':
app = wx.App()
frame = MainFrame()
frame.Show(True)
app.MainLoop()
print 'Exiting...'
Thanks all for your help,
I found an easy solution to this issue.
I first found out that wx.Panel.Update() gets called to repaint the screen when ever the event handler method returns. So, the loading panel seems to look like it needed to be repainted, as the static text box is all grayed out. I then just called self.__myDatabase.Update() after self.__myDatabase.Show().
Here is the new version of onNextScreen(self):
def onNextScreen(self):
self.__myMainPanel.Hide()
self.__myDatabase.Show()
self.__myDatabase.Update() # Fixes the bug...
self.doDatabaseLoad()
self.__myDatabase.Hide()
self.__myFinalPanel.Show()
Also, another way to fix it is to call SetLabel on the static text, but the above solution is better. It just seems that Update was not being called automatically, because it was in the middle of an event...
I was working with OpenCV gui functions for a while, and the possibilities are a little restricting for python users. Today I started with Pyqt and come across the following conclusion: qt is really confusing.
Now the question concerning mouse events:
In OpenCV I just do the following:
import cv2
cv2.namedWindow('Window',1)
def CallBackFunc(event,x,y,flags,param):
global xc,yc,evt,flg
xc,yc,evt,flg=x,y,event,flags
cv2.setMouseCallback('Window', CallBackFunc)
This opens a seperate thread, which constantly refreshes the global variables xc,yc,evt,flg, and I can access them anywhere, at anytime I want. If I want to stop the refreshing, I just do a cv2.setMouseCallback('Window',nothing), whereby nothing is
def nothing():
pass
It may not be the most beautiful way of dealing with mouse events, but I am fine with it. How can I achieve such freedom with PyQt?
EDIT:
For example, the following script is displaying a white circle, and constantly drawing a text into it.
import sys
from PySide import QtGui
import numpy as np
import cv2
class QCustomLabel (QtGui.QLabel):
def __init__ (self, parent = None):
super(QCustomLabel, self).__init__(parent)
self.setMouseTracking(True)
def mouseMoveEvent (self, eventQMouseEvent):
self.x,self.y=eventQMouseEvent.x(),eventQMouseEvent.y()
cvImg=np.zeros((900,900),dtype=np.uint8)
cv2.circle(cvImg,(449,449),100,255,-1)
cv2.putText(cvImg,"x at {}, y at {}".format(self.x,self.y),(375,455), cv2.FONT_HERSHEY_SIMPLEX,0.5,(0,0,0),1,cv2.LINE_AA)
height, width= cvImg.shape
bytearr=cvImg.data
qImg = QtGui.QImage(bytearr, width, height, QtGui.QImage.Format_Indexed8)
self.setPixmap(QtGui.QPixmap.fromImage(qImg))
def mousePressEvent (self, eventQMouseEvent):
self.evt=eventQMouseEvent.button()
class QCustomWidget (QtGui.QWidget):
def __init__ (self, parent = None):
super(QCustomWidget, self).__init__(parent)
self.setWindowOpacity(1)
# Init QLabel
self.positionQLabel = QCustomLabel(self)
# Init QLayout
layoutQHBoxLayout = QtGui.QHBoxLayout()
layoutQHBoxLayout.addWidget(self.positionQLabel)
self.setLayout(layoutQHBoxLayout)
self.show()
if QtGui.QApplication.instance() is not None:
myQApplication=QtGui.QApplication.instance()
else:
myQApplication = QtGui.QApplication(sys.argv)
myQTestWidget = QCustomWidget()
myQTestWidget.show()
myQApplication.exec_()
The problem here is, that this is all executed inside the QCustomLabel Class, and inside the MouseMoveEvent function. But I want a seperate function, lets call it drawCircle, outside of that class, which has access to the mouse position and events. With opencv this would be no problem at all. And it would take only a fraction of the writing effort, which is needed for a pyqt implementation.
I think the right question is: Why dont I like pyqt yet?
You can use an event-filter to avoid having to subclass the QLabel:
class QCustomWidget (QtGui.QWidget):
def __init__ (self, parent = None):
super(QCustomWidget, self).__init__(parent)
self.setWindowOpacity(1)
# Init QLabel
self.positionQLabel = QtGui.QLabel(self)
self.positionQLabel.setMouseTracking(True)
self.positionQLabel.installEventFilter(self)
# Init QLayout
layoutQHBoxLayout = QtGui.QHBoxLayout()
layoutQHBoxLayout.addWidget(self.positionQLabel)
self.setLayout(layoutQHBoxLayout)
self.show()
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.MouseMove:
self.drawCircle(event.x(), event.y())
return super(QCustomWidget, self).eventFilter(source, event)
def drawCircle(self, x, y):
# whatever
I am new to python. I am trying to write a motion detection app. Currently, I am trying to get the webcam video to display on the screen. Current code right now has no flicker at first, but after any resizing, the flicker will come back. Any clue? Also, why doesn't it work without self.Refresh() in the timer event, isn't paint event always happening unless the frame is minimized? Thanks in advance.
import wx
import cv
class LiveFrame(wx.Frame):
fps = 30
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, title="Live Camera Feed")
self.SetDoubleBuffered(True)
self.capture = None
self.bmp = None
#self.displayPanel = wx.Panel(self,-1)
#set up camaera init
self.capture = cv.CaptureFromCAM(0)
frame = cv.QueryFrame(self.capture)
if frame:
cv.CvtColor(frame,frame,cv.CV_BGR2RGB)
self.bmp = wx.BitmapFromBuffer(frame.width,frame.height,frame.tostring())
self.SetSize((frame.width,frame.height))
self.displayPanel = wx.Panel(self,-1)
self.fpstimer = wx.Timer(self)
self.fpstimer.Start(1000/self.fps)
self.Bind(wx.EVT_TIMER, self.onNextFrame, self.fpstimer)
self.Bind(wx.EVT_PAINT, self.onPaint)
self.Show(True)
def updateVideo(self):
frame = cv.QueryFrame(self.capture)
if frame:
cv.CvtColor(frame,frame,cv.CV_BGR2RGB)
self.bmp.CopyFromBuffer(frame.tostring())
self.Refresh()
def onNextFrame(self,evt):
self.updateVideo()
#self.Refresh()
evt.Skip()
def onPaint(self,evt):
#if self.bmp:
wx.BufferedPaintDC(self.displayPanel, self.bmp)
evt.Skip()
if __name__=="__main__":
app = wx.App()
app.RestoreStdio()
LiveFrame(None)
app.MainLoop()
I have found the solution to this problem. The flickering came from the panel clearing its background. I had to create a panel instance and have its EVT_ERASE_BACKGROUND bypass. Another thing is that I had to put the webcam routine inside that panel and have BufferPaintedDC drawing on the panel itself. For some reason, flicker still persist if wx.BufferedPaintedDC is drawing from the frame to self.displaypanel .
When you're drawing, you just have to call Refresh. It's a requirement. I don't remember why. To get rid of flicker, you'll probably want to read up on DoubleBuffering: http://wiki.wxpython.org/DoubleBufferedDrawing
Or maybe you could use the mplayer control. There's an example here: http://www.blog.pythonlibrary.org/2010/07/24/wxpython-creating-a-simple-media-player/