wxSuperToolTip changes position on SetMessage - python

I'm creating a wx.agw.SuperToolTip. I'm updating the message in the tip every few seconds, and if the tip is showing when the message updates the tip is redrawn in a different position.
The new position seems to be relative to the original position's relation to the top left corner of the screen, but that could just be coincidence.
Also, if I modify wx.lib.agw.supertooltip.ToolTipWindowBase.Invalidate() by commenting out the call to self.CalculateBestSize() the problem goes away. Of course then the window won't resize, so that's no solution.
I'm using wxPython 2.8.12.1.
Here's an app that demonstrates the problem:
class MyFrame(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, -1, title,
pos=(150, 150), size=(350, 225))
panel = wx.Panel(self)
btn = wx.Button(panel, -1, "Hover over this")
self._superTip = SuperToolTip("")
self._superTip.SetHeader("Heyo!")
self._superTip.SetTarget(btn)
self._superTip.EnableTip(True)
self._superTip.SetDrawHeaderLine(True)
self._superTip.SetDrawFooterLine(True)
self._superTip.SetStartDelay(1)
self._superTip.SetEndDelay(60)
currentFooterFont = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
currentFooterFont.SetPointSize(6)
currentFooterFont.SetWeight(wx.NORMAL)
self._superTip.SetFooterFont(currentFooterFont)
self._superTip.SetFooter('(Click to close)')
self._superTip.ApplyStyle("Blue Glass")
self._superTip.SetDropShadow(True)
self.ttTimer = wx.Timer(self)
self.ttText = 'What the?'
self.Bind(wx.EVT_TIMER, self.onTimer, self.ttTimer)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(btn, 0, wx.ALL, 10)
panel.SetSizer(sizer)
self.ttTimer.Start(2000)
panel.Layout()
def onTimer(self, evt):
self._superTip.SetMessage(self.ttText)
self.ttText += '?'
class MyApp(wx.App):
def OnInit(self):
frame = MyFrame(None, "STT error demo")
self.SetTopWindow(frame)
frame.Show(True)
return True
app = MyApp(redirect=True)
app.MainLoop()
Any thoughts on how I can update a visible tooltip without its location changing?
Thanks a lot.

Related

Make TextCtrl Scale with Window

I've just gotten into wxpython for Python 3.6 and I've hit a roadblock. I just can't work out how to make elements/widgets scale with the screen! I know you have to use sizers but that's about it, I'm still fairly new to programming so just reading the documentation didn't help. If someone could just sample some code that works I'd be very thankful as I could then read through it and work out what I was doing wrong. The code that draws out the GUI I want to scale with window size is below, the key idea is that the TextCtrl scales, other elements don't really need scaling.
def createGUI(self):
panel = wx.Panel(self)
menuBar = wx.MenuBar()
menuButton = wx.Menu()
newItem = wx.MenuItem(menuButton, wx.ID_NEW, 'New Note\tCtrl+N')
delItem = wx.MenuItem(menuButton, wx.ID_DELETE, 'Delete Note\tCtrl+Backspace')
saveItem = wx.MenuItem(menuButton, wx.ID_SAVE, 'Save\tCtrl+S')
exitItem = wx.MenuItem(menuButton, wx.ID_EXIT, 'Quit\tCtrl+Q')
menuButton.Append(newItem)
menuButton.Append(saveItem)
menuButton.Append(delItem)
menuButton.Append(exitItem)
menuBar.Append(menuButton, 'Menu')
self.SetMenuBar(menuBar)
self.Bind(wx.EVT_MENU, self.new, newItem)
self.Bind(wx.EVT_MENU, self.delete, delItem)
self.Bind(wx.EVT_MENU, self.save, saveItem)
self.Bind(wx.EVT_MENU, self.onExit, exitItem)
self.noteText = wx.TextCtrl(panel)
self.noteText.AppendText(self.notecontent)
self.Bind(wx.EVT_CLOSE, self.onExit)
self.SetTitle(f'Welcome {self.username}! You are working on {self.notepath}')
self.Centre()
self.Show(True)
To begin with it's best to equate sizers to something familiar and I usually think of storage boxes or a chest of drawers.
When we define widgets, they are all dumped into a container, the parent object, often the ubiquitous self or self.panel.
If we do not assign a size and pos to each item, it's just a jumbled mess, a pile of widgets.
The sizer, there are many types, are the virtual drawers in our chest of drawers, that herds this pile of widgets into order.
The widgets are assigned to the appropriate sizer, note sizers can go into other sizers, and eventually when everything has been assigned a place or drawer in our chest of drawers, the sizers do their magic, arranging and sizing all of widgets into a coherent screen for display or arranging the drawers contents and relative positions in the chest of drawers.
Below, I've used the simplest sizer a boxsizer.
One will arrange things vertically and the other horizontally.
The horizontal sizer is for the buttons and the vertical sizer, will be the main sizer, into which I place not only the TectCtrl but also the buttons, prearranged in their horizontal sizer.
For a better and more comprehensive description of sizers and their controls see: https://docs.wxpython.org/sizers_overview.html and for details on the actual sizers available see the detailed documenation on each.
A loose approximation of your code:
#!/usr/bin/python
import wx
class Example(wx.Frame):
def __init__(self, parent, title):
super(Example, self).__init__(parent, title=title,
size=(450, 350))
self.panel = wx.Panel(self, -1)
self.main_sizer = wx.BoxSizer(wx.VERTICAL)
self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.noteText = wx.TextCtrl(self.panel, -1, style=wx.TE_MULTILINE)
self.Button_close = wx.Button(self.panel, -1, label="Quit")
self.Button_1 = wx.Button(self.panel, -1, label="Btn1")
self.Button_2 = wx.Button(self.panel, -1, label="Btn2")
self.Bind(wx.EVT_CLOSE, self.onExit)
self.Button_close.Bind(wx.EVT_BUTTON, self.onExit)
self.Button_1.Bind(wx.EVT_BUTTON, self.onButton)
self.Button_2.Bind(wx.EVT_BUTTON, self.onButton)
# Place buttons within their own horizontal sizer
self.button_sizer.Add(self.Button_close,proportion=0, flag=wx.ALL, border=10)
self.button_sizer.Add(self.Button_1,proportion=0, flag=wx.ALL, border=10)
self.button_sizer.Add(self.Button_2,proportion=0, flag=wx.ALL, border=10)
# Add textctrl and the button sizer to the main sizer (vertical)
self.main_sizer.Add(self.noteText,proportion=1, flag=wx.EXPAND|wx.ALL, border=10)
self.main_sizer.Add(self.button_sizer, 0, 0, 0)
self.panel.SetSizer(self.main_sizer)
self.Show()
def onExit(self, event):
self.Destroy()
def onButton(self, event):
print("A button was pressed")
if __name__ == '__main__':
app = wx.App()
Example(None, title="Example")
app.MainLoop()
Edit:
Here's the same code without using sizers, although they are recommended.
#!/usr/bin/python
import wx
class Example(wx.Frame):
def __init__(self, parent, title):
super(Example, self).__init__(parent, title=title,
size=(450, 350))
self.panel = wx.Panel(self, -1)
self.noteText = wx.TextCtrl(self.panel, -1, pos=(10,10), size=(400,280), style=wx.TE_MULTILINE)
self.Button_close = wx.Button(self.panel, -1, label="Quit", pos=(10,290), size=(50,30))
self.Button_1 = wx.Button(self.panel, -1, label="Btn1", pos=(70,290), size=(50,30))
self.Button_2 = wx.Button(self.panel, -1, label="Btn2", pos=(130,290), size=(50,30))
self.Bind(wx.EVT_CLOSE, self.onExit)
self.Button_close.Bind(wx.EVT_BUTTON, self.onExit)
self.Button_1.Bind(wx.EVT_BUTTON, self.onButton)
self.Button_2.Bind(wx.EVT_BUTTON, self.onButton)
self.Show()
def onExit(self, event):
self.Destroy()
def onButton(self, event):
print("A button was pressed")
if __name__ == '__main__':
app = wx.App()
Example(None, title="Example")
app.MainLoop()

wxpython - Erase background erases non-background components

In wxpython, I want to have a window with a picture that changes based on use of toolbar buttons with text controls on top of the picture. When I click the toolbar buttons, I am posting an erase background event, then capturing the erase event, and redrawing the new background from there (base on this).
Mostly works well, except that the text controls cease to be drawn once I redraw the background. They're still there, just not drawn.
Here is a simplified code that demonstrates the problem. If you run this code and click the button to toggle drawing the background image or not, the text controls disappear.:
import wx
import wx.lib.inspection
class PanelWithDrawing(wx.Panel):
def __init__(self, parent):
super(PanelWithDrawing, self).__init__(parent, size=(100, 40))
self.showbmp = False
self.txt = wx.TextCtrl(self, pos=(10, 10))
def onErase(self, dc):
if self.showbmp:
# dc.DrawBitmap(wx.Bitmap('background.png', 0, 0)
dc.DrawRectangle(0, 0, 40, 40) # use a drawing instead so you don't have to find a png
class Toolbar(wx.ToolBar):
def __init__(self, parent):
super(Toolbar, self).__init__(parent, -1)
self.AddLabelTool(wx.ID_SAVE, "Record", wx.Bitmap("picture.png", wx.BITMAP_TYPE_ANY), wx.NullBitmap, wx.ITEM_NORMAL, "", "")
class Example(wx.Frame):
def __init__(self, parent, title):
super(Example, self).__init__(parent, title=title)
self.toolbar = Toolbar(self)
self.SetToolBar(self.toolbar)
self.toolbar.Realize()
self.panel = wx.Panel(self)
vbox = wx.BoxSizer(wx.VERTICAL)
self.panel1 = PanelWithDrawing(self.panel)
vbox.Add(self.panel1)
# self.panel2 = PanelWithText(self.panel)
# vbox.Add(self.panel2)
self.panel.SetSizer(vbox)
self.Centre()
self.Show()
self.toolbar.Bind(wx.EVT_TOOL, self.onButton)
self.panel1.Bind(wx.EVT_ERASE_BACKGROUND, self.onErase)
def onErase(self, evt):
try:
dc = evt.GetDC()
except:
dc = wx.ClientDC(self)
rect = self.GetUpdateRegion().GetBox()
dc.SetClippingRect(rect)
dc.Clear()
self.panel1.onErase(dc)
def onButton(self, evt):
self.panel1.showbmp = not self.panel1.showbmp
wx.PostEvent(self.panel1, wx.PyCommandEvent(wx.wxEVT_ERASE_BACKGROUND))
if __name__ == '__main__':
app = wx.App()
Example(None, title='Example')
wx.lib.inspection.InspectionTool().Show() # use this for debugging GUI design
app.MainLoop()
How do I tell wxpython to draw all the non-background stuff again? Alternatively, how do I not un-draw it in the first place?
After working on it for a few days, I got it! And the answer is trivially simple (as usual).
wx.PostEvent(self.panel1, wx.PyCommandEvent(wx.wxEVT_ERASE_BACKGROUND)) should be replaced with self.Refresh() to refresh the whole frame and not just force a specific (and apparently unsafe) redraw.

wxpython textctrl line and column

I am writing a text editor using wxpython. I want to display the line & column numbers based on the position of the text pointer(when changed using keyboard or mouse), in the statusbar.
I tried to do it using the below code:
import wx
class TextFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, 'Text Editor', size=(300, 250))
self.panel = wx.Panel(self, -1)
self.multiText = wx.TextCtrl(self.panel, -1,"",size=(200, 100), style=wx.TE_MULTILINE|wx.EXPAND)
sizer = wx.BoxSizer()
sizer.Add(self.multiText, proportion=1, flag=wx.CENTER|wx.EXPAND)
self.panel.SetSizer(sizer)
self.CreateStatusBar()
self.multiText.Bind(wx.EVT_KEY_DOWN, self.updateLineCol)
self.multiText.Bind(wx.EVT_LEFT_DOWN, self.updateLineCol)
def updateLineCol(self, event):
#lineNum = len(self.multiText.GetRange( 0, self.multiText.GetInsertionPoint() ).split("\n"))
l,c = self.multiText.PositionToXY(self.multiText.GetInsertionPoint())
self.StatusBar.SetStatusText(str(l)+","+str(c), number=0)
event.Skip()
app = wx.App(False)
frame = TextFrame()
frame.Show()
app.MainLoop()
I tried the below 2 ways to achieve that:
1.) lineNum = len(self.multiText.GetRange( 0, self.multiText.GetInsertionPoint() ).split("\n"))
2.) l,c = self.multiText.PositionToXY(self.multiText.GetInsertionPoint())
But, both, always give the line & column values of previous position of the pointer instead of current.
Is there a way to display the current line & column values?
I think you just need to bind to EVT_KEY_UP instead of EVT_KEY_DOWN. That way the event handler isn't called until after you have finished typing. I also updated the content written to the statusbar to make it a bit clearer which value was which:
import wx
class TextFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, 'Text Editor', size=(300, 250))
self.panel = wx.Panel(self, -1)
self.multiText = wx.TextCtrl(self.panel, -1,"",size=(200, 100), style=wx.TE_MULTILINE|wx.EXPAND)
sizer = wx.BoxSizer()
sizer.Add(self.multiText, proportion=1, flag=wx.CENTER|wx.EXPAND)
self.panel.SetSizer(sizer)
self.CreateStatusBar()
self.multiText.Bind(wx.EVT_KEY_UP, self.updateLineCol)
self.multiText.Bind(wx.EVT_LEFT_DOWN, self.updateLineCol)
def updateLineCol(self, event):
#lineNum = len(self.multiText.GetRange( 0, self.multiText.GetInsertionPoint() ).split("\n"))
l,c = self.multiText.PositionToXY(self.multiText.GetInsertionPoint())
stat = "col=%s, row=%s" % (l,c)
self.StatusBar.SetStatusText(stat, number=0)
event.Skip()
app = wx.App(False)
frame = TextFrame()
frame.Show()
app.MainLoop()

wxpython: centering text within a panel within a sizer

It seems to me that the following code should display text right in the centre of the window; that is, in the centre of the inner panel. It doesn't however, and I'm wondering why not. If you run the code, you'll see a white panel in the middle of the frame, 150px by 150px. I do not want this area to change in size at all, but when I go about adding some text (uncommenting the txt variable in the middle of the snippet)the panel invariably shrinks to fit the text. Even specifying the size of the StaticText to match the panel isn't a solution because the text doesn't then centre-align.
import wx
class MyFrame(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title)
self.rootPanel = wx.Panel(self)
innerPanel = wx.Panel(self.rootPanel,-1, size=(150,150), style=wx.ALIGN_CENTER)
innerPanel.SetBackgroundColour('WHITE')
hbox = wx.BoxSizer(wx.HORIZONTAL)
vbox = wx.BoxSizer(wx.VERTICAL)
# I want this line visible in the CENTRE of the inner panel
#txt = wx.StaticText(innerPanel, id=-1, label="TEXT HERE",style=wx.ALIGN_CENTER, name="")
hbox.Add(innerPanel, 0, wx.ALL|wx.ALIGN_CENTER)
vbox.Add(hbox, 1, wx.ALL|wx.ALIGN_CENTER, 5)
self.rootPanel.SetSizer(vbox)
vbox.Fit(self)
class MyApp(wx.App):
def OnInit(self):
frame = MyFrame(None, -1, 'wxBoxSizer.py')
frame.Show(True)
frame.Center()
return True
app = MyApp(0)
app.MainLoop()
You just need to add a couple spacers to make it work.
import wx
class MyFrame(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title)
self.rootPanel = wx.Panel(self)
innerPanel = wx.Panel(self.rootPanel,-1, size=(150,150), style=wx.ALIGN_CENTER)
innerPanel.SetBackgroundColour('WHITE')
hbox = wx.BoxSizer(wx.HORIZONTAL)
vbox = wx.BoxSizer(wx.VERTICAL)
innerBox = wx.BoxSizer(wx.VERTICAL)
# I want this line visible in the CENTRE of the inner panel
txt = wx.StaticText(innerPanel, id=-1, label="TEXT HERE",style=wx.ALIGN_CENTER, name="")
innerBox.AddSpacer((150,75))
innerBox.Add(txt, 0, wx.CENTER)
innerBox.AddSpacer((150,75))
innerPanel.SetSizer(innerBox)
hbox.Add(innerPanel, 0, wx.ALL|wx.ALIGN_CENTER)
vbox.Add(hbox, 1, wx.ALL|wx.ALIGN_CENTER, 5)
self.rootPanel.SetSizer(vbox)
vbox.Fit(self)
class MyApp(wx.App):
def OnInit(self):
frame = MyFrame(None, -1, 'wxBoxSizer.py')
frame.Show(True)
frame.Center()
return True
app = MyApp(0)
app.MainLoop()

wxPython - Redrawing Error when replacing wxFrame's Panel

I'm creating a small wxPython utility for the first time, and I'm stuck on a problem.
I would like to add components to an already created frame. To do this, I am destroying the frame's old panel, and creating a new panel with all new components.
1: Is there a better way of dynamically adding content to a panel?
2: Why, in the following example, do I get a a strange redraw error in which in the panel is drawn only in the top left hand corner, and when resized, the panel is drawn correctly?
(WinXP, Python 2.5, latest wxPython)
Thank you for the help!
import wx
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, 'TimeTablr')
#Variables
self.iCalFiles = ['Empty', 'Empty', 'Empty']
self.panel = wx.Panel(self, -1)
self.layoutElements()
def layoutElements(self):
self.panel.Destroy()
self.panel = wx.Panel(self, -1)
#Buttons
self.getFilesButton = wx.Button(self.panel, 1, 'Get Files')
self.calculateButton = wx.Button(self.panel, 2, 'Calculate')
self.quitButton = wx.Button(self.panel, 3, 'Quit Application')
#Binds
self.Bind(wx.EVT_BUTTON, self.Quit, id=3)
self.Bind(wx.EVT_BUTTON, self.getFiles, id=1)
#Layout Managers
vbox = wx.BoxSizer(wx.VERTICAL)
#Panel Contents
self.ctrlsToDescribe = []
self.fileNames = []
for iCalFile in self.iCalFiles:
self.ctrlsToDescribe.append(wx.TextCtrl(self.panel, -1))
self.fileNames.append(wx.StaticText(self.panel, -1, iCalFile))
#Add Components to Layout Managers
for i in range(0, len(self.ctrlsToDescribe)):
hboxtemp = wx.BoxSizer(wx.HORIZONTAL)
hboxtemp.AddStretchSpacer()
hboxtemp.Add(self.fileNames[i], 1, wx.EXPAND)
hboxtemp.AddStretchSpacer()
hboxtemp.Add(self.ctrlsToDescribe[i], 2, wx.EXPAND)
hboxtemp.AddStretchSpacer()
vbox.Add(hboxtemp)
finalHBox = wx.BoxSizer(wx.HORIZONTAL)
finalHBox.Add(self.getFilesButton)
finalHBox.Add(self.calculateButton)
finalHBox.Add(self.quitButton)
vbox.Add(finalHBox)
self.panel.SetSizer(vbox)
self.Show()
def Quit(self, event):
self.Destroy()
def getFiles(self, event):
self.iCalFiles = ['Example1','Example1','Example1','Example1','Example1','Example1']
self.layoutElements()
self.Update()
app = wx.App()
MainFrame()
app.MainLoop()
del app
1) I beleive the Sizer will let you insert elements into the existing ordering of them. That would probably be a bit faster.
2) I don't see the behavior you're describing on OSX, but at a guess, try calling self.Layout() before self.Show() in layoutElements?
I had a similar problem where the panel would be squished into the upper-right corner. I solved it by calling panel.Fit().
In your example, you should call self.panel.Fit() after self.panel.SetSizer(vbox)

Categories