Related
I have a PyQt application where I have drawn points using QPainter over a QGraphicsScene and made a drag n drop sort of a thing.
Now, there is one issue which I'm facing and that is I'm unable to drag those points at the extreme corner and edges of QGraphicsScene. It always seems as if some amount of padding or space is left.
How do I get round this problem?
Code:
from collections import deque
from datetime import datetime
import sys
from threading import Thread
import time
import numpy as np
import cv2
from PyQt4 import QtCore, QtGui
class CameraWidget(QtGui.QGraphicsView):
"""Independent camera feed
Uses threading to grab IP camera frames in the background
#param width - Width of the video frame
#param height - Height of the video frame
#param stream_link - IP/RTSP/Webcam link
#param aspect_ratio - Whether to maintain frame aspect ratio or force into fraame
"""
def __init__(self, width, height, stream_link=0, aspect_ratio=False, parent=None, deque_size=1):
super(CameraWidget, self).__init__(parent)
# Initialize deque used to store frames read from the stream
self.deque = deque(maxlen=deque_size)
self.screen_width = width
self.screen_height = height
self.maintain_aspect_ratio = aspect_ratio
self.camera_stream_link = stream_link
# Flag to check if camera is valid/working
self.online = False
self.capture = None
self.setScene(QtGui.QGraphicsScene(self))
self._pixmap_item = self.scene().addPixmap(QtGui.QPixmap())
canvas = Canvas()
lay = QtGui.QVBoxLayout()
lay.addWidget(canvas)
self.setLayout(lay)
self.load_network_stream()
# Start background frame grabbing
self.get_frame_thread = Thread(target=self.get_frame, args=())
self.get_frame_thread.daemon = True
self.get_frame_thread.start()
# Periodically set video frame to display
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.set_frame)
self.timer.start(0.5)
print("Started camera: {}".format(self.camera_stream_link))
def load_network_stream(self):
"""Verifies stream link and open new stream if valid"""
def load_network_stream_thread():
if self.verify_network_stream(self.camera_stream_link):
self.capture = cv2.VideoCapture(self.camera_stream_link)
self.online = True
self.load_stream_thread = Thread(target=load_network_stream_thread, args=())
self.load_stream_thread.daemon = True
self.load_stream_thread.start()
def verify_network_stream(self, link):
"""Attempts to receive a frame from given link"""
cap = cv2.VideoCapture(link)
if not cap.isOpened():
return False
cap.release()
return True
def get_frame(self):
"""Reads frame, resizes, and converts image to pixmap"""
while True:
try:
if self.capture.isOpened() and self.online:
# Read next frame from stream and insert into deque
status, frame = self.capture.read()
if status:
self.deque.append(frame)
else:
self.capture.release()
self.online = False
else:
# Attempt to reconnect
print("attempting to reconnect", self.camera_stream_link)
self.load_network_stream()
self.spin(2)
self.spin(0.001)
except AttributeError:
pass
def spin(self, seconds):
"""Pause for set amount of seconds, replaces time.sleep so program doesnt stall"""
time_end = time.time() + seconds
while time.time() < time_end:
QtGui.QApplication.processEvents()
def set_frame(self):
"""Sets pixmap image to video frame"""
if not self.online:
self.spin(1)
return
if self.deque and self.online:
# Grab latest frame
frame = self.deque[-1]
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = frame.shape
bytesPerLine = ch * w
# Convert to pixmap and set to video frame
image = QtGui.QImage(frame, w, h, bytesPerLine, QtGui.QImage.Format_RGB888)
pixmap = QtGui.QPixmap.fromImage(image.copy())
self._pixmap_item.setPixmap(pixmap)
self.fix_size()
def resizeEvent(self, event):
self.fix_size()
super().resizeEvent(event)
def fix_size(self):
self.fitInView(
self._pixmap_item,
QtCore.Qt.KeepAspectRatio
if self.maintain_aspect_ratio
else QtCore.Qt.IgnoreAspectRatio,
)
class Window(QtGui.QWidget):
def __init__(self, cam=None, parent=None):
super(Window, self).__init__(parent)
self.showMaximized()
self.screen_width = self.width()
self.screen_height = self.height()
# Create camera widget
print("Creating Camera Widget...")
self.camera = CameraWidget(self.screen_width, self.screen_height, cam)
lay = QtGui.QVBoxLayout(self)
lay.setContentsMargins(0, 0, 0, 0)
lay.setSpacing(0)
lay.addWidget(self.camera)
class Canvas(QtGui.QWidget):
DELTA = 200 #for the minimum distance
def __init__(self, parent=None):
super(Canvas, self).__init__(parent)
self.draggin_idx = -1
self.points = np.array([[x[0],x[1]] for x in [[100,200], [200,200], [100,400], [200,400]]], dtype=np.float)
self.id = None
self.points_dict = {}
for i, x in enumerate(self.points):
point=(int(x[0]),int(x[1]))
self.points_dict[i] = point
def paintEvent(self, e):
qp = QtGui.QPainter()
qp.begin(self)
self.drawPoints(qp)
self.drawLines(qp)
qp.end()
def drawPoints(self, qp):
pen = QtGui.QPen()
pen.setWidth(10)
pen.setColor(QtGui.QColor('red'))
qp.setPen(pen)
for x,y in self.points:
qp.drawPoint(x,y)
def drawLines(self, qp):
qp.setPen(QtCore.Qt.red)
qp.drawLine(self.points_dict[0][0], self.points_dict[0][1], self.points_dict[1][0], self.points_dict[1][1])
qp.drawLine(self.points_dict[1][0], self.points_dict[1][1], self.points_dict[3][0], self.points_dict[3][1])
qp.drawLine(self.points_dict[3][0], self.points_dict[3][1], self.points_dict[2][0], self.points_dict[2][1])
qp.drawLine(self.points_dict[2][0], self.points_dict[2][1], self.points_dict[0][0], self.points_dict[0][1])
def _get_point(self, evt):
pos = evt.pos()
if pos.x() < 0:
pos.setX(0)
elif pos.x() > self.width():
pos.setX(self.width())
if pos.y() < 0:
pos.setY(0)
elif pos.y() > self.height():
pos.setY(self.height())
return np.array([pos.x(), pos.y()])
#get the click coordinates
def mousePressEvent(self, evt):
if evt.button() == QtCore.Qt.LeftButton and self.draggin_idx == -1:
point = self._get_point(evt)
int_point = (int(point[0]), int(point[1]))
min_dist = ((int_point[0]-self.points_dict[0][0])**2 + (int_point[1]-self.points_dict[0][1])**2)**0.5
for i, x in enumerate(list(self.points_dict.values())):
distance = ((int_point[0]-x[0])**2 + (int_point[1]-x[1])**2)**0.5
if min_dist >= distance:
min_dist = distance
self.id = i
#dist will hold the square distance from the click to the points
dist = self.points - point
dist = dist[:,0]**2 + dist[:,1]**2
dist[dist>self.DELTA] = np.inf #obviate the distances above DELTA
if dist.min() < np.inf:
self.draggin_idx = dist.argmin()
def mouseMoveEvent(self, evt):
if self.draggin_idx != -1:
point = self._get_point(evt)
self.points[self.draggin_idx] = point
self.update()
def mouseReleaseEvent(self, evt):
if evt.button() == QtCore.Qt.LeftButton and self.draggin_idx != -1:
point = self._get_point(evt)
int_point = (int(point[0]), int(point[1]))
self.points_dict[self.id] = int_point
self.points[self.draggin_idx] = point
self.draggin_idx = -1
self.update()
camera = 0
if __name__ == "__main__":
app = QtGui.QApplication([])
win = Window(camera)
sys.exit(app.exec_())
Edit:
I've one more requirement.
The mousePressEvent and mouseReleaseEvent in my Canvas class gives me coordinates w.r.t. my monitor resolution, instead I want it w.r.t. QGraphicsView. Say e.g. my screen_resolution is 1920x1080 and the size of my QGraphicsView is 640x480 then I should get points in accordance with 640x480.
The simplest solution would be to add lay.setContentsMargins(0, 0, 0, 0) for the layout of the graphics view:
class CameraWidget(QtGui.QGraphicsView):
def __init__(self, width, height, stream_link=0, aspect_ratio=False, parent=None, deque_size=1):
# ...
canvas = Canvas()
lay = QtGui.QVBoxLayout()
lay.addWidget(canvas)
self.setLayout(lay)
lay.setContentsMargins(0, 0, 0, 0)
# ...
But consider that doing all this is not suggested.
First of all, you don't need a layout for a single widget, as you could just create the widget with the view as a parent and then resize it in the resizeEvent:
# ...
self.canvas = Canvas(self)
def resizeEvent(self, event):
self.fix_size()
super().resizeEvent(event)
self.canvas.resize(self.size())
Widgets like QGraphicsView should not have a layout set, it's unsupported and may lead to unwanted behavior or even bugs under certain conditions.
In any case, it doesn't make a lot of sense to add a widget on top of a QGraphicsView if that widget is used for painting and mouse interaction: QGraphicsView already provides better implementation for that by using QGraphicsRectItem or QGraphicsLineItem.
And, even if it weren't the case, custom drawing over a graphics view should be done in its drawForeground() implementation.
I am making a data acquisition program that communicates with a measurement device. The status of the device needs to be checked periodically (e.g., every 0.1 sec) to see if acquisition is done. Furthermore, the program must have the 'abort' method because acquisition sometime takes longer than few minutes. Thus I need to use multi-threading.
I attached the flow-chart and the example code. But I have no idea how I call the main-thread to execute a method from the sub-thread.
python 3.7.2
wxpython 4.0.6
Flow Chart
import wx
import time
from threading import Thread
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="Test Frame")
panel = wx.Panel(self)
self.Btn1 = wx.Button(panel, label="Start Measurement")
self.Btn1.Bind(wx.EVT_BUTTON, self.OnStart)
self.Btn2 = wx.Button(panel, label="Abort Measurement")
self.Btn2.Bind(wx.EVT_BUTTON, self.OnAbort)
self.Btn2.Enable(False)
self.DoneFlag = False
self.SubThread = Thread(target=self.Check, daemon=True)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.Btn1, 0, wx.EXPAND)
sizer.Add(self.Btn2, 0, wx.EXPAND)
panel.SetSizer(sizer)
def OnStart(self, event):
# self.N is the number of data points
self.N = 0
# self.N_max is the number of data points that is going to be acquired
self.N_max = int(input("How many data points do yo want? (greater than 1) : "))
self.DoneFlag = False
self.Btn1.Enable(False)
self.Btn2.Enable(True)
self.Start()
def OnAbort(self, event):
self.DoneFlag = True
def Start(self):
self.SubThread.start()
def Done(self):
if self.DoneFlag is True:
self.Finish()
elif self.DoneFlag is False:
self.Start()
def Finish(self):
print("Measurement done (N = {})\n".format(self.N))
self.Btn1.Enable(True)
self.Btn2.Enable(False)
def Check(self):
# In the actual program, this method communicates with a data acquisition device to check its status
# For example,
# "RunningStatus" is True when the device is still running (acquisition has not been done yet),
# is False when the device is in idle state (acquisition has done)
#
# [Structure of the actual program]
# while True:
# RunningStatus = GetStatusFromDevice()
# if RunningStatus is False or self.DoneFlag is True:
# break
# else:
# time.sleep(0.1)
# In below code, it just waits 3 seconds then assumes the acqusition is done
t = time.time()
time.sleep(1)
for i in range(3):
if self.DoneFlag is True:
break
print("{} sec left".format(int(5-time.time()+t)))
time.sleep(1)
# Proceed to the next steps after the acquisition is done.
if self.DoneFlag is False:
self.N += 1
print("Data acquired (N = {})\n".format(self.N))
if self.N == self.N_max:
self.DoneFlag = True
self.Done() # This method should be excuted in the main thread
if __name__ == "__main__":
app = wx.App()
frame = TestFrame()
frame.Show()
app.MainLoop()
When using a GUI it is not recommended to call GUI functions from another thread, see:
https://docs.wxwidgets.org/trunk/overview_thread.html
One of your options, is to use events to keep track of what is going on.
One function creates and dispatches an event when something happens or to denote progress for example, whilst another function listens for and reacts to a specific event.
So, just like pubsub but native.
Here, I use one event to post information about progress and another for results but with different targets.
It certainly will not be an exact fit for your scenario but should give enough information to craft a solution of your own.
import time
import wx
from threading import Thread
import wx.lib.newevent
progress_event, EVT_PROGRESS_EVENT = wx.lib.newevent.NewEvent()
results_event, EVT_RESULTS_EVENT = wx.lib.newevent.NewEvent()
class ThreadFrame(wx.Frame):
def __init__(self, title, parent=None):
wx.Frame.__init__(self, parent=parent, title=title)
panel = wx.Panel(self)
self.parent = parent
self.btn = wx.Button(panel,label='Stop Measurements', size=(200,30), pos=(10,10))
self.btn.Bind(wx.EVT_BUTTON, self.OnExit)
self.progress = wx.Gauge(panel,size=(240,10), pos=(10,50), range=30)
#Bind to the progress event issued by the thread
self.Bind(EVT_PROGRESS_EVENT, self.OnProgress)
#Bind to Exit on frame close
self.Bind(wx.EVT_CLOSE, self.OnExit)
self.Show()
self.mythread = TestThread(self)
def OnProgress(self, event):
self.progress.SetValue(event.count)
#or for indeterminate progress
#self.progress.Pulse()
if event.result != 0:
evt = results_event(result=event.result)
#Send back result to main frame
try:
wx.PostEvent(self.parent, evt)
except:
pass
def OnExit(self, event):
if self.mythread.isAlive():
self.mythread.terminate() # Shutdown the thread
self.mythread.join() # Wait for it to finish
self.Destroy()
class TestThread(Thread):
def __init__(self,parent_target):
Thread.__init__(self)
self.parent = parent_target
self.stopthread = False
self.start() # start the thread
def run(self):
curr_loop = 0
while self.stopthread == False:
curr_loop += 1
# Send a result every 3 seconds for test purposes
if curr_loop < 30:
time.sleep(0.1)
evt = progress_event(count=curr_loop,result=0)
#Send back current count for the progress bar
try:
wx.PostEvent(self.parent, evt)
except: # The parent frame has probably been destroyed
self.terminate()
else:
curr_loop = 0
evt = progress_event(count=curr_loop,result=time.time())
#Send back current count for the progress bar
try:
wx.PostEvent(self.parent, evt)
except: # The parent frame has probably been destroyed
self.terminate()
def terminate(self):
evt = progress_event(count=0,result="Measurements Ended")
try:
wx.PostEvent(self.parent, evt)
except:
pass
self.stopthread = True
class MyPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.text_count = 0
self.thread_count = 0
self.parent=parent
btn = wx.Button(self, wx.ID_ANY, label='Start Measurements', size=(200,30), pos=(10,10))
btn.Bind(wx.EVT_BUTTON, self.Thread_Frame)
btn2 = wx.Button(self, wx.ID_ANY, label='Is the GUI still active?', size=(200,30), pos=(10,50))
btn2.Bind(wx.EVT_BUTTON, self.AddText)
self.txt = wx.TextCtrl(self, wx.ID_ANY, style= wx.TE_MULTILINE, pos=(10,90),size=(400,100))
#Bind to the result event issued by the thread
self.Bind(EVT_RESULTS_EVENT, self.OnResult)
def Thread_Frame(self, event):
self.thread_count += 1
frame = ThreadFrame(title='Measurement Task '+str(self.thread_count), parent=self)
def AddText(self,event):
self.text_count += 1
txt = "Gui is still active " + str(self.text_count)+"\n"
self.txt.write(txt)
def OnResult(self,event):
txt = "Result received " + str(event.result)+"\n"
self.txt.write(txt)
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title='Main Frame', size=(600,400))
panel = MyPanel(self)
self.Show()
if __name__ == '__main__':
app = wx.App(False)
frame = MainFrame()
app.MainLoop()
I'm very new to wxPython and working on a desktop screenshot utility. In short, user selects a rectangular region on their screen from and produce an image from it as most screenshot tools already do. Its all working except for the fact that I cannot get the main frame, and all it contains, to hide or close, so the screenshot image produced contains the overlay mask and dashed selection box.
FYI, screenshots are triggered by pressing the ENTER key. I have tried a vast number of ways to achieve this without luck. Hiding the frame, or destroying the panel first etc. makes no difference. However, if I break things up to two steps with button to proceed it all works fine so I am betting it's something small. I tried Refresh on the frame and panel as well. For what it's worth, my desktop environment if Linux KDE (latest ArchLinux rolling release).
Here is my code in full.
#!/usr/bin/env python2
import wx, time
#--------------------------------------------------------------------------------------------------------#
class SelectableFrame(wx.Frame):
c1 = None
c2 = None
def __init__(self, parent=None, id=-1, title=""):
wx.Frame.__init__(self, parent, id, title, pos=(0, 0), size=wx.DisplaySize(), style=wx.FRAME_NO_TASKBAR | wx.NO_BORDER | wx.STAY_ON_TOP)
self.panel = wx.Panel(self, size=self.GetSize())
self.panel.Bind(wx.EVT_MOTION, self.OnMouseMove)
self.panel.Bind(wx.EVT_LEFT_DOWN, self.OnMouseSelect)
self.panel.Bind(wx.EVT_RIGHT_DOWN, self.OnReset)
self.panel.Bind(wx.EVT_LEFT_UP, self.OnMouseUp)
self.panel.Bind(wx.EVT_PAINT, self.OnPaint)
self.panel.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
self.Bind(wx.EVT_CLOSE, self.OnClose)
self.SetCursor(wx.StockCursor(wx.CURSOR_CROSS))
self.panel.SetBackgroundColour(wx.Colour(0, 0, 0))
self.panel.SetBackgroundStyle(wx.BG_STYLE_COLOUR)
self.SetTransparent(150)
def OnClose(self, event):
self.Destroy()
def OnMouseMove(self, event):
if event.Dragging() and event.LeftIsDown():
self.c2 = event.GetPosition()
self.Refresh()
def OnMouseSelect(self, event):
self.SetCursor(wx.StockCursor(wx.CURSOR_CROSS))
self.c1 = event.GetPosition()
def OnMouseUp(self, event):
self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW))
def OnKeyDown(self, event):
key = event.GetKeyCode()
if key == wx.WXK_ESCAPE:
self.Close()
elif (key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER) and self.RegionSelected():
self.TakeScreenshot()
event.Skip()
def OnReset(self, event=None):
wx.PaintDC(self.panel).Clear()
self.SetCursor(wx.StockCursor(wx.CURSOR_CROSS))
def OnPaint(self, event):
if not self.RegionSelected():
return
dc = wx.PaintDC(self.panel)
dc.SetPen(wx.Pen("white", 1, wx.LONG_DASH))
dc.SetBrush(wx.Brush(wx.Colour(100, 100, 100), wx.SOLID))
dc.DrawRectangle(self.c1.x, self.c1.y, self.c2.x - self.c1.x, self.c2.y - self.c1.y)
def RegionSelected(self):
if self.c1 is None or self.c2 is None:
return False
else:
return True
def TakeScreenshot(self):
if not self.RegionSelected():
return
wx.PaintDC(self.panel).Clear()
self.Hide()
self.Refresh()
dcScreen = wx.ScreenDC()
bmp = wx.EmptyBitmap(self.c2.x - self.c1.x, self.c2.y - self.c1.y)
memDC = wx.MemoryDC()
memDC.SelectObject(bmp)
memDC.Blit(0, 0, self.c2.x - self.c1.x, self.c2.y - self.c1.y, dcScreen, self.c1.x, self.c1.y)
memDC.SelectObject(wx.NullBitmap)
img = bmp.ConvertToImage()
filename = "/tmp/ocr-screenshot-" + time.strftime('%Y%m%d-%H%M%S') + ".png"
img.SaveFile(filename, wx.BITMAP_TYPE_PNG)
self.Close()
#--------------------------------------------------------------------------------------------------------
class SelectableApp(wx.App):
def OnInit(self):
self.frame = SelectableFrame()
self.frame.Show(True)
self.SetTopWindow(self.frame)
return True
#--------------------------------------------------------------------------------------------------------#
app = SelectableApp(False)
app.MainLoop()
I am trying to write a non-webbased client for a chat service on a web site, it connects to it fine via socket and can communicate with it and everything. I am writing a GUI for it (I tried writing it in tkinter but I hit some walls I really wantd to get passed, so I switched to wxPython)
What I'm having a problem with:
This application uses an extended Notebook widget called NotebookCtrl. However the same problem appears with regular Notebook. First it creates a page in which things are logged to, which is successful, and then it connects, and it's supposed to add pages with each chatroom it joins on the service. However, when it adds a tab after it's mainloop has been started (I am communicating with the GUI and the sockets via queues and threading), the tab comes up completely blank. I have been stuck on this for hours and hours and got absolutely nowhere
The example that came with the NotebookCtrl download adds and deletes pages by itself perfectly fine. I am on the edge of completely giving up on this project. Here is what the code looks like (note this is a very small portion of the application, but this covers the wxPython stuff)
class Chatroom(Panel):
''' Frame for the notebook widget to tabulate a chatroom'''
def __init__(self, ns, parent):
Panel.__init__(self, parent, -1)
self.msgs, self.typed, self.pcbuff = [], [], {}
self.members, self._topic, self._title, self.pc = None, None, None, None
self.name, self.tabsign, = ns, 0
self.hSizer1 = wx.BoxSizer(wx.HORIZONTAL)
self.vSizer = wx.BoxSizer(wx.VERTICAL)
self.Grid = wx.GridBagSizer(5, 2)
self.Title = TextCtrl(self, size=(-1, 50), style=wx.TE_MULTILINE | wx.TE_READONLY)
self.Topic = TextCtrl(self, size=(-1, 50), style=wx.TE_MULTILINE | wx.TE_READONLY)
self.Privclasses = TreeCtrl(self, size=(150, -1))
self.Buffer = wx.html.HtmlWindow(self)
self.Buffer.SetStandardFonts(8)
self.templbl = StaticText(self, -1, 'This is where the formatting buttons will go!')
# note to remember: self.templbl.SetLabel('string') sets the label
self.Typer = TextCtrl(self, size=(-1, 50), style=wx.TE_MULTILINE)
self.Grid.Add(self.Title, (0,0), (1,2), wx.EXPAND, 2)
self.Grid.Add(self.Topic, (1,0), (1,1), wx.EXPAND, 2)
self.Grid.Add(self.Privclasses, (1,1), (2,1), wx.EXPAND, 2)
self.Grid.Add(self.Buffer, (2,0), (1,1), wx.EXPAND, 2)
self.Grid.Add(self.templbl, (3,0), (1,1), wx.EXPAND | wx.ALIGN_LEFT, 2)
self.Grid.Add(self.Typer, (4,0), (1,1), wx.EXPAND, 2)
self.Grid.AddGrowableCol(0)
self.Grid.AddGrowableRow(2)
self.SetSizerAndFit(self.Grid)
self.Show(True)
self.Typer.Bind(EVT_CHAR, self.Typer_OnKeyDown)
def Typer_OnKeyDown(self, event):
keycode = event.GetKeyCode()
if event.ShiftDown():
if keycode == WXK_RETURN:
pass
elif keycode == WXK_BACK:
pass
elif keycode == WXK_UP:
pass
elif keycode == WXK_DOWN:
pass
else:
if keycode == WXK_RETURN:
pass
event.Skip()
def Write(self, msg, K):
self.msgs.append(msg)
if len(self.msgs) > 300:
self.msgs = self.msgs[50:]
self.Buffer.SetPage('<br>'.join(self.msgs))
class Application(App):
def __init__(self, K):
self.Queue = Queue.Queue()
self.current = ''
self.chatorder = []
self.Window = App(0)
self.frame = MainFrame(None, 0, "Komodo Dragon")
self.Pages = NC.NotebookCtrl(self.frame, 9000)
self.Channels = {}
self.AddChatroom('~Komodo', K)
self.frame.Show(True)
self.Window.SetTopWindow(self.frame)
self.Timer = _Timer(0.050, self.OnTimer)
self.Timer.start()
self.Pages.Bind(NC.EVT_NOTEBOOKCTRL_PAGE_CHANGED, self.onPageChanged)
self.Pages.Bind(NC.EVT_NOTEBOOKCTRL_PAGE_CHANGING, self.onPageChanging)
self.Pages.Bind(EVT_PAINT, self.onPaint)
self.Pages.Bind(EVT_SIZE, self.onSize)
def onPaint(self, event):
event.Skip()
def onSize(self, event):
event.Skip()
def Run(self):
self.Window.MainLoop()
def onPageChanged(self, event):
event.Skip()
def onPageChanging(self, event):
event.Skip()
# Timer and Queue functions
def OnTimer(self):
self.CheckQueue()
self.Timer = _Timer(0.050, self.OnTimer)
self.Timer.start()
def CheckQueue(self): # the Application needs to use a queue to do things in order to prevent
try: # overlaps from happening, such as runtime errors and widgets changing
while not self.Queue.empty(): # suddenly. The most common error seems to be size
func = self.Queue.get_nowait() # changes during iterations. Everything from
func() # packet processing to adding widgets needs to wait in line this way
except Queue.Empty:
pass
def AddQueue(self, func):
self.Queue.put(func)
# Channel controls
def AddChatroom(self, ns, K):
if ns in self.Channels: return
#self.typedindex = 0
c = K.format_ns(ns)
self.chatorder.append(ns)
self.current = ns
self.Channels[ns] = Chatroom(ns, self.Pages)
self.Pages.AddPage(self.Channels[ns], ns, True)
def DeleteChatroom(self, ns, bot): # Delete a channel, it's properties, and buttons
ind = self.chatorder.index(ns)
del self.chatorder[ind]
for each in self.chatorder[ind:]:
x = self.channels[each].tab.grid_info()
if x['column'] == '0': r, c = int(x['row'])-1, 9
else: r, c = int(x['row']), int(x['column'])-1
self.channels[each].tab.grid_configure(row=r, column=c)
x = self.channels[each].tab.grid_info()
self.channels[ns].Tab.destroy()
self.channels[ns].tab.destroy()
self.channels[self.chatorder[-1]].tab.select()
self.switchtab(bot, self.chatorder[-1])
x = self.tabids_reverse[ns]
del self.tabids_reverse[ns], self.tabids[x], self.channels[ns]
The Chatroom class covers what each tab should have in it. the first tab that is added in class Application's init function is perfectly fine, and even prints messages it receives from the chat service when it attempts to connect. Once the service sends a join packet to me, it calls the same exact function used to add that tab, AddChatroom(), and should create the exact same thing, only printing messages specifically for that chatroom. It creates the tab, but the Chatroom page is completely empty and grey. I am very sad :C
Thanks in advance if you can help me.
EDIT
I have written a working example of this script you can run yourself (if you have wxPython). It uses regular Notebook instead of NotebookCtrl so you don't have to download that widget as well, but the bug happens for both. The example sets up the window, and sets up the main debug tab and then a chatroom tab before entering mainloop. After mainloop, any chatrooms added afterwords are completely blank
from wx import *
import Queue, time
from threading import Timer as _Timer
from threading import Thread
# import System._NotebookCtrl.NotebookCtrl as NC ## Using regular notebook for this example
class MainFrame(Frame):
def __init__(self, parent, ID, title):
ID_FILE_LOGIN = 100
ID_FILE_RESTART = 101
ID_FILE_EXIT = 102
ID_HELP_ABOUT = 200
Frame.__init__(self, parent, ID, title,
DefaultPosition, Size(1000, 600))
self.CreateStatusBar()
self.SetStatusText("This is the statusbar")
menu_File = Menu()
menu_Help = Menu()
menu_Edit = Menu()
menu_Config = Menu()
menu_File.Append(ID_FILE_LOGIN, "&Login Info",
"Enter your deviantArt Login information")
menu_File.AppendSeparator()
menu_File.Append(ID_FILE_RESTART, "&Restart",
"Restart the program")
menu_File.Append(ID_FILE_EXIT, "E&xit",
"Terminate the program")
menu_Help.Append(ID_HELP_ABOUT, "&About",
"More information about this program")
menuBar = MenuBar()
menuBar.Append(menu_File, "&File");
menuBar.Append(menu_Edit, "&Edit");
menuBar.Append(menu_Config, "&Config");
menuBar.Append(menu_Help, "&Help");
self.SetMenuBar(menuBar)
EVT_MENU(self, ID_FILE_LOGIN, self.OnLogin)
EVT_MENU(self, ID_FILE_RESTART, self.OnRestart)
EVT_MENU(self, ID_FILE_EXIT, self.OnQuit)
EVT_MENU(self, ID_HELP_ABOUT, self.OnAbout)
def OnAbout(self, event):
dlg = MessageDialog(self, "Hi! I am Komodo Dragon! I am an application\n"
"that communicates with deviantArt's Messaging Network (dAmn)",
"Komodo", OK | ICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()
def OnQuit(self, event):
self.Close(True)
def OnRestart(self, event):
pass
def OnLogin(self, event):
dlg = MessageDialog(self, "Enter your Login information here:\n"
"Work in progress, LOL",
"Login", OK | ICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()
class Chatroom(Panel):
''' Frame for the notebook widget to tabulate a chatroom'''
def __init__(self, parent, ns):
Panel.__init__(self, parent, -1)
self.msgs, self.typed, self.pcbuff = [], [], {}
self.members, self._topic, self._title, self.pc = None, None, None, None
self.name, self.tabsign, = ns, 0
self.hSizer1 = wx.BoxSizer(wx.HORIZONTAL)
self.vSizer = wx.BoxSizer(wx.VERTICAL)
self.Grid = wx.GridBagSizer(5, 2)
self.Title = TextCtrl(self, size=(-1, 50), style=wx.TE_MULTILINE | wx.TE_READONLY)
self.Topic = TextCtrl(self, size=(-1, 50), style=wx.TE_MULTILINE | wx.TE_READONLY)
self.Privclasses = TreeCtrl(self, size=(150, -1))
self.Buffer = wx.html.HtmlWindow(self)
self.Buffer.SetStandardFonts(8)
self.templbl = StaticText(self, -1, 'This is where the formatting buttons will go!')
self.Typer = TextCtrl(self, size=(-1, 50), style=wx.TE_MULTILINE)
self.Grid.Add(self.Title, (0,0), (1,2), wx.EXPAND, 2)
self.Grid.Add(self.Topic, (1,0), (1,1), wx.EXPAND, 2)
self.Grid.Add(self.Privclasses, (1,1), (2,1), wx.EXPAND, 2)
self.Grid.Add(self.Buffer, (2,0), (1,1), wx.EXPAND, 2)
self.Grid.Add(self.templbl, (3,0), (1,1), wx.EXPAND | wx.ALIGN_LEFT, 2)
self.Grid.Add(self.Typer, (4,0), (1,1), wx.EXPAND, 2)
self.Grid.AddGrowableCol(0)
self.Grid.AddGrowableRow(2)
self.SetSizerAndFit(self.Grid)
self.Show(True)
self.Typer.Bind(EVT_CHAR, self.Typer_OnKeyDown)
def Typer_OnKeyDown(self, event):
keycode = event.GetKeyCode()
if event.ShiftDown():
if keycode == WXK_RETURN:
pass
elif keycode == WXK_BACK:
pass
elif keycode == WXK_UP:
pass
elif keycode == WXK_DOWN:
pass
else:
if keycode == WXK_RETURN:
pass
event.Skip()
def Write(self, msg):
self.msgs.append(msg)
if len(self.msgs) > 300:
self.msgs = self.msgs[50:]
self.Buffer.SetPage('<br>'.join(self.msgs))
class Application(App):
def __init__(self, K):
self.Queue = Queue.Queue()
self.current = ''
self.chatorder = []
self.Window = App(0)
self.frame = MainFrame(None, 0, "Komodo Dragon")
self.Pages = Notebook(self.frame, 9000)
self.Channels = {}
self.AddChatroom('~Komodo', K)
self.frame.Show(True)
self.Window.SetTopWindow(self.frame)
self.Timer = _Timer(0.050, self.OnTimer)
self.Pages.Bind(EVT_NOTEBOOK_PAGE_CHANGED, self.onPageChanged)
self.Pages.Bind(EVT_NOTEBOOK_PAGE_CHANGING, self.onPageChanging)
self.Pages.Bind(EVT_PAINT, self.onPaint)
self.Pages.Bind(EVT_SIZE, self.onSize)
def onPaint(self, event):
event.Skip()
def onSize(self, event):
event.Skip()
def onPageChanged(self, event):
event.Skip()
def onPageChanging(self, event):
event.Skip()
def Run(self):
self.Window.MainLoop()
# Timer and Queue functions
def OnTimer(self):
self.CheckQueue()
self.Timer = _Timer(0.050, self.OnTimer)
self.Timer.start()
def CheckQueue(self): # the Application needs to use a queue to do things in order to prevent
try: # overlaps from happening, such as runtime errors and widgets changing
while not self.Queue.empty(): # suddenly. The most common error seems to be size
func = self.Queue.get_nowait() # changes during iterations. Everything from
if func is not None:
func() # packet processing to adding widgets needs to wait in line this way
except Queue.Empty:
pass
def AddQueue(self, func):
self.Queue.put(func)
# Channel controls
def AddChatroom(self, ns, K):
if ns in self.Channels: return
self.chatorder.append(ns)
self.current = ns
self.Channels[ns] = Chatroom(self.Pages, ns)
self.Pages.AddPage(self.Channels[ns], ns, True)
class _Thread(Thread):
def __init__(self, K):
Thread.__init__(self)
self.K = K
def run(self):
self.K.Mainloop()
class K:
def __init__(self):
self.App = Application(self)
self.App.AddQueue(self.App.Channels['~Komodo'].Write('>> Welcome!') )
self.App.AddQueue(self.App.Channels['~Komodo'].Write('>> Entering mainloop...') )
self.App.AddChatroom('#TestChatroom1', self)
self.roomcount = 1
self.timer = time.time() + 3
self.thread = _Thread(self)
self.thread.start()
self.App.Timer.start()
self.App.Run()
def Mainloop(self):
while True:
if time.time() > self.timer:
self.App.AddQueue(
lambda: self.App.Channels['~Komodo'].Write('>> Testing') )
if self.roomcount < 5:
self.roomcount += 1
self.App.AddQueue(
lambda: self.App.AddChatroom('#TestChatroom{0}'.format(self.roomcount), self) )
self.timer = time.time() + 3
if __name__ == '__main__':
test = K()
Here is your problem:
lambda: self.App.AddChatroom('#TestChatroom{0}'.format(self.roomcount), self) )
Fixed by using wx.CallAfter (tested on win xp sp3):
lambda: wx.CallAfter(self.App.AddChatroom, '#TestChatroom{0}'.format(self.roomcount), self)
You were probably tying up the GUI by calling wx objects from thread code. See this wxPython wiki article.
It doesn't look like you're creating your Chatroom with its parent as the Notebook. What is "K" in Application.__init__? (you didn't post a fully runnable sample)
When adding or deleting tabs, you probably need to call Layout() right after you're done. One easy way to find out is to grab an edge of the frame and resize it slightly. If you see widgets magically appear, then it's because Layout() was called and your page re-drawn.
I have been working on this project for some time now - it was originally supposed to be a test to see if, using wxPython, I could build a button 'from scratch.' From scratch means: that i would have full control over all the aspects of the button (i.e. controlling the BMP's that are displayed... what the event handlers did... etc.)
I have run into several problems (as this is my first major python project.) Now, when the all the code is working for the life of me I can't get an image to display.
Basic code - not working
dc = wx.BufferedPaintDC(self)
dc.SetFont(self.GetFont())
dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
dc.Clear()
dc.DrawBitmap(wx.Bitmap("/home/wallter/Desktop/Mouseover.bmp"), 100, 100)
self.Refresh()
self.Update()
Full Main.py
import wx
from Custom_Button import Custom_Button
from wxPython.wx import *
ID_ABOUT = 101
ID_EXIT = 102
class MyFrame(wx.Frame):
def __init__(self, parent, ID, title):
wxFrame.__init__(self, parent, ID, title,
wxDefaultPosition, wxSize(400, 400))
self.CreateStatusBar()
self.SetStatusText("Program testing custom button overlays")
menu = wxMenu()
menu.Append(ID_ABOUT, "&About", "More information about this program")
menu.AppendSeparator()
menu.Append(ID_EXIT, "E&xit", "Terminate the program")
menuBar = wxMenuBar()
menuBar.Append(menu, "&File");
self.SetMenuBar(menuBar)
# The call for the 'Experiential button'
self.Button1 = Custom_Button(parent, -1,
wx.Point(100, 100),
wx.Bitmap("/home/wallter/Desktop/Mouseover.bmp"),
wx.Bitmap("/home/wallter/Desktop/Normal.bmp"),
wx.Bitmap("/home/wallter/Desktop/Click.bmp"))
# The following three lines of code are in place to try to get the
# Button1 to display (trying to trigger the Paint event (the _onPaint.)
# Because that is where the 'draw' functions are.
self.Button1.Show(true)
self.Refresh()
self.Update()
# Because the Above three lines of code did not work, I added the
# following four lines to trigger the 'draw' functions to test if the
# '_onPaint' method actually worked.
# These lines do not work.
dc = wx.BufferedPaintDC(self)
dc.SetFont(self.GetFont())
dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
dc.DrawBitmap(wx.Bitmap("/home/wallter/Desktop/Mouseover.bmp"), 100, 100)
EVT_MENU(self, ID_ABOUT, self.OnAbout)
EVT_MENU(self, ID_EXIT, self.TimeToQuit)
def OnAbout(self, event):
dlg = wxMessageDialog(self, "Testing the functions of custom "
"buttons using pyDev and wxPython",
"About", wxOK | wxICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()
def TimeToQuit(self, event):
self.Close(true)
class MyApp(wx.App):
def OnInit(self):
frame = MyFrame(NULL, -1, "wxPython | Buttons")
frame.Show(true)
self.SetTopWindow(frame)
return true
app = MyApp(0)
app.MainLoop()
Full CustomButton.py
import wx
from wxPython.wx import *
class Custom_Button(wx.PyControl):
def __init__(self, parent, id, Pos, Over_BMP, Norm_BMP, Push_BMP, **kwargs):
wx.PyControl.__init__(self,parent, id, **kwargs)
self.Bind(wx.EVT_LEFT_DOWN, self._onMouseDown)
self.Bind(wx.EVT_LEFT_UP, self._onMouseUp)
self.Bind(wx.EVT_LEAVE_WINDOW, self._onMouseLeave)
self.Bind(wx.EVT_ENTER_WINDOW, self._onMouseEnter)
self.Bind(wx.EVT_ERASE_BACKGROUND,self._onEraseBackground)
self.Bind(wx.EVT_PAINT,self._onPaint)
self.pos = Pos
self.Over_bmp = Over_BMP
self.Norm_bmp = Norm_BMP
self.Push_bmp = Push_BMP
self._mouseIn = False
self._mouseDown = False
def _onMouseEnter(self, event):
self._mouseIn = True
def _onMouseLeave(self, event):
self._mouseIn = False
def _onMouseDown(self, event):
self._mouseDown = True
def _onMouseUp(self, event):
self._mouseDown = False
self.sendButtonEvent()
def sendButtonEvent(self):
event = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, self.GetId())
event.SetInt(0)
event.SetEventObject(self)
self.GetEventHandler().ProcessEvent(event)
def _onEraseBackground(self,event):
# reduce flicker
pass
def Iz(self):
dc = wx.BufferedPaintDC(self)
dc.DrawBitmap(self.Norm_bmp, 100, 100)
def _onPaint(self, event):
# The printing functions, they should work... but don't.
dc = wx.BufferedPaintDC(self)
dc.SetFont(self.GetFont())
dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
dc.Clear()
dc.DrawBitmap(self.Norm_bmp)
# This never printed... I don't know if that means if the EVT
# is triggering or what.
print '_onPaint'
# draw whatever you want to draw
# draw glossy bitmaps e.g. dc.DrawBitmap
if self._mouseIn: # If the Mouse is over the button
dc.DrawBitmap(self.Over_bmp, self.pos)
else: # Since the mouse isn't over it Print the normal one
# This is adding on the above code to draw the bmp
# in an attempt to get the bmp to display; to no avail.
dc.DrawBitmap(self.Norm_bmp, self.pos)
if self._mouseDown: # If the Mouse clicks the button
dc.DrawBitmap(self.Push_bmp, self.pos)
This code won't work? I get no BMP displayed why? How do i get one? I've gotten the staticBitmap(...) to display one, but it won't move, resize, or anything for that matter... - it's only in the top left corner of the frame?
Note: the frame is 400pxl X 400pxl - and the "/home/wallter/Desktop/Mouseover.bmp"
Are your sure you code is working without exceptions because when I run it i get many errors, read the points below and you should have a button which at least draws correctly
When O run it it gives error because Custom_Button is passed NULL parent instead pass frame e.g. Custom_Button(self, ...)
Your drawBitmap call is also wrong, it throws exception, instead of dc.DrawBitmap(self.Norm_bmp) it should be dc.DrawBitmap(self.Norm_bmp, 0, 0)
dc.DrawBitmap(self.Over_bmp, self.pos) also throws error as pos should be x,y not a tuple so instead do dc.DrawBitmap(self.Over_bmp, *self.pos)
and lastly you do not need to do "from wxPython.wx import *" instead just do "from wx import *" and instead of wxXXX class names use wx.XXX, instead of true use True etc
here is my working code
from wx import *
ID_ABOUT = 101
ID_EXIT = 102
class Custom_Button(wx.PyControl):
def __init__(self, parent, id, Pos, Over_BMP, Norm_BMP, Push_BMP, **kwargs):
wx.PyControl.__init__(self,parent, id, **kwargs)
self.Bind(wx.EVT_LEFT_DOWN, self._onMouseDown)
self.Bind(wx.EVT_LEFT_UP, self._onMouseUp)
self.Bind(wx.EVT_LEAVE_WINDOW, self._onMouseLeave)
self.Bind(wx.EVT_ENTER_WINDOW, self._onMouseEnter)
self.Bind(wx.EVT_ERASE_BACKGROUND,self._onEraseBackground)
self.Bind(wx.EVT_PAINT,self._onPaint)
self.pos = Pos
self.Over_bmp = Over_BMP
self.Norm_bmp = Norm_BMP
self.Push_bmp = Push_BMP
self._mouseIn = False
self._mouseDown = False
def _onMouseEnter(self, event):
self._mouseIn = True
def _onMouseLeave(self, event):
self._mouseIn = False
def _onMouseDown(self, event):
self._mouseDown = True
def _onMouseUp(self, event):
self._mouseDown = False
self.sendButtonEvent()
def sendButtonEvent(self):
event = wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, self.GetId())
event.SetInt(0)
event.SetEventObject(self)
self.GetEventHandler().ProcessEvent(event)
def _onEraseBackground(self,event):
# reduce flicker
pass
def Iz(self):
dc = wx.BufferedPaintDC(self)
dc.DrawBitmap(self.Norm_bmp, 100, 100)
def _onPaint(self, event):
# The printing functions, they should work... but don't.
dc = wx.BufferedPaintDC(self)
dc.SetFont(self.GetFont())
dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
dc.Clear()
dc.DrawBitmap(self.Norm_bmp, 0, 0)
# This never printed... I don't know if that means if the EVT
# is triggering or what.
print '_onPaint'
# draw whatever you want to draw
# draw glossy bitmaps e.g. dc.DrawBitmap
if self._mouseIn: # If the Mouse is over the button
dc.DrawBitmap(self.Over_bmp, *self.pos)
else: # Since the mouse isn't over it Print the normal one
# This is adding on the above code to draw the bmp
# in an attempt to get the bmp to display; to no avail.
dc.DrawBitmap(self.Norm_bmp, *self.pos)
if self._mouseDown: # If the Mouse clicks the button
dc.DrawBitmap(self.Push_bmp, *self.pos)
class MyFrame(wx.Frame):
def __init__(self, parent, ID, title):
wx.Frame.__init__(self, parent, ID, title,
wx.DefaultPosition, wx.Size(400, 400))
self.CreateStatusBar()
self.SetStatusText("Program testing custom button overlays")
menu = wx.Menu()
menu.Append(ID_ABOUT, "&About", "More information about this program")
menu.AppendSeparator()
menu.Append(ID_EXIT, "E&xit", "Terminate the program")
menuBar = wx.MenuBar()
menuBar.Append(menu, "&File");
self.SetMenuBar(menuBar)
# The call for the 'Experiential button'
s = r"D:\virtual_pc\mockup\mockupscreens\embed_images\toolbar\options.png"
self.Button1 = Custom_Button(self, -1,
wx.Point(100, 100),
wx.Bitmap(s),
wx.Bitmap(s),
wx.Bitmap(s))
self.Button1.Show(True)
EVT_MENU(self, ID_ABOUT, self.OnAbout)
EVT_MENU(self, ID_EXIT, self.TimeToQuit)
def OnAbout(self, event):
dlg = wxMessageDialog(self, "Testing the functions of custom "
"buttons using pyDev and wxPython",
"About", wxOK | wxICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()
def TimeToQuit(self, event):
self.Close(true)
class MyApp(wx.App):
def OnInit(self):
frame = MyFrame(None, -1, "wxPython | Buttons")
frame.Show(True)
self.SetTopWindow(frame)
return True
app = MyApp(0)
app.MainLoop()