Okay, so I want to display a series of windows within windows and have the whole lot scrollable. I've been hunting through the wxWidgets documentation and a load of examples from various sources on t'internet. Most of those seem to imply that a wx.ScrolledWindow should work if I just pass it a nested group of sizers(?):
The most automatic and newest way is to simply let sizers determine the scrolling area.This is now the default when you set an interior sizer into a wxScrolledWindow with wxWindow::SetSizer. The scrolling area will be set to the size requested by the sizer and the scrollbars will be assigned for each orientation according to the need for them and the scrolling increment set by wxScrolledWindow::SetScrollRate.
...but all the example's I've seen seem to use the older methods listed as ways to achieve scrolling. I've got something basic working, but as soon as you start scrolling you lose the child windows:
import wx
class MyCustomWindow(wx.Window):
def __init__(self, parent):
wx.Window.__init__(self, parent)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.SetSize((50,50))
def OnPaint(self, event):
dc = wx.BufferedPaintDC(self)
dc.SetPen(wx.Pen('blue', 2))
dc.SetBrush(wx.Brush('blue'))
(width, height)=self.GetSizeTuple()
dc.DrawRoundedRectangle(0, 0,width, height, 8)
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1)
self.Bind(wx.EVT_SIZE, self.OnSize)
self.scrolling_window = wx.ScrolledWindow( self )
self.scrolling_window.SetScrollRate(1,1)
self.scrolling_window.EnableScrolling(True,True)
self.sizer_container = wx.BoxSizer( wx.VERTICAL )
self.sizer = wx.BoxSizer( wx.HORIZONTAL )
self.sizer_container.Add(self.sizer,1,wx.CENTER,wx.EXPAND)
self.child_windows = []
for i in range(0,50):
wind = MyCustomWindow(self.scrolling_window)
self.sizer.Add(wind, 0, wx.CENTER|wx.ALL, 5)
self.child_windows.append(wind)
self.scrolling_window.SetSizer(self.sizer_container)
def OnSize(self, event):
self.scrolling_window.SetSize(self.GetClientSize())
if __name__=='__main__':
app = wx.PySimpleApp()
f = TestFrame()
f.Show()
app.MainLoop()
Oops.. turns out I was creating my child windows badly:
wind = MyCustomWindow(self)
should be:
wind = MyCustomWindow(self.scrolling_window)
..which meant the child windows were waiting for the top-level window (the frame) to be re-drawn instead of listening to the scroll window. Changing that makes it all work wonderfully :)
Related
I so far failed to create what is colloquially called an "info icon" with wxPython. An icon with some sort of 'i' image that shows a large tooltip on hover.
I can add a wx.StaticBitmap for the image but it ignores all SetToolTipString or SetToolTip(wx.ToolTip()) calls. OR I can add a large tool tip to a wx.StaticText as shown below.
Ignore that the icon doesn't have the correct size yet.
Needless to say that eventually the tooltip needs a background color that is different from the panel background color (not the focus here). I can't use wx.adv.RichToolTip because I'm on wxPython 3.0.2.0 osx-cocoa.
What is a good way to solve this?
If you create a button with an ID of wx.ID_HELP then you'll get the stock help button for the platform, if it has one. Then you can do whatever you want with it like any button. Assign a tooltip, do something in the EVT_BUTTON event, etc. See the StockButtons sample in the demo. If the stock image or label doesn't meet your needs then you can probably just use a wx.BitmapButton to show the image you want and still have the standard tooltip support.
Something else you may want to look into is the ContextHelp sample in the demo. It shows how to use a wx.ContextHelpButton which, when clicked, puts the application into context-help mode. A popup tip window will then be shown for whatever widget is clicked on next. Not quite what you are asking for, but it might be a good fit.
wxArtProvider may be able to help http://docs.wxwidgets.org/trunk/classwx_art_provider.html
import wx
class Test(wx.Frame):
def __init__(self,parent,msg,title):
wx.Frame.__init__(self, None)
self.panel = wx.Panel(self, size=(300,400))
mainSizer = wx.BoxSizer(wx.HORIZONTAL)
staticIcon = wx.BitmapButton(self.panel, bitmap=wx.ArtProvider.GetBitmap(wx.ART_WARNING), size=(32,32))
mainSizer.Add(staticIcon, flag=wx.ALL, border=10)
ttip = "xxxxxxxxxxxxxxx\n"
ttip += "xxxxxxxxxxxxxxxxxxxxxxxxxx\n"
ttip += "xxxxxxxxxxxxxxxxxxxxxxxxxxx\n"
ttip += "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
staticIcon.SetToolTipString(ttip)
buttonText = wx.StaticText(self.panel, -1, msg, wx.DefaultPosition, wx.DefaultSize, 0)
mainSizer.Add(buttonText, flag=wx.ALL, border=10)
staticIcon.Bind(wx.EVT_BUTTON, self.OnButton)
self.SetSizer(mainSizer)
self.Show()
def OnButton(self, evt):
print "The button was pressed - display some help"
if __name__ == '__main__':
app = wx.App()
Test(None, "Dummy Exercise", "Test 123")
app.MainLoop()
If all you want to do is show a tooltip when the image is moused over, then you need to bind your instance of the wx.StaticBitmap to EVT_MOTION:
import wx
class MyPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
bmp = wx.ArtProvider.GetBitmap(wx.ART_WARNING)
self.image = wx.StaticBitmap(self, bitmap=bmp)
self.image.Bind(wx.EVT_MOTION, self.on_mouse_over)
def on_mouse_over(self, event):
self.image.SetToolTipString('BLAH BLAH BLAH')
class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title='Icon Mouser')
panel = MyPanel(self)
self.Show()
if __name__ == '__main__':
app = wx.App(False)
frame = MyFrame()
app.MainLoop()
When I run this code, I get something like this:
In wxPython, I'm having trouble centering a sub-panel that I've created to have a fixed aspect ratio.
To control the aspect ratio, the sub-panel needs to capture the Size event and then do an explicit SetSize. So, I've done that and it works well. Unfortunately, when I embed this sub-panel into another panel (using a sizer), the wx.ALIGN_CENTER_HORIZONTAL flag doesn't work.
Apparently, the sizer tells my sub-panel that it has the whole width to fit within. When my window doesn't use the whole width, then the sizer doesn't adjust. Here is a simplified version of my code that shows the problem:
import wx
ROWS = 6
COLS = 25
class TestPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, -1)
label = wx.StaticText(self, -1, 'Label')
grid = TestGrid(self)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(label, 0, flag=wx.ALIGN_CENTER_HORIZONTAL)
sizer.Add(grid, 1, flag=wx.ALIGN_CENTER_HORIZONTAL|wx.EXPAND)
self.SetSizer(sizer)
class TestGrid(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, -1)
self.SetBackgroundColour(wx.BLUE)
self.Bind(wx.EVT_SIZE, self.OnSize)
def OnSize(self, event):
w, h = self.GetSizeTuple()
delta = min(w // COLS, h // ROWS)
self.SetSize((COLS*delta, ROWS*delta))
self.Refresh()
if __name__ == '__main__':
app = wx.App()
frame = wx.Frame(None, -1, "Test", size=(600, 200))
panel = TestPanel(frame)
frame.Show(True)
app.MainLoop()
This is a screen capture:
As VZ explained in his answer, your wxEVT_SIZE handler isn't a very good idea. Remove it. When using sizers, you have to work within the sizer infrastructure to achieve what you want.
You want your TestGrid to fill as much space as possible within its parent, while maintaining a certain aspect ratio; that's exactly what wx.SHAPED is for (combined with a proportion greater than 0, which you already have). So, you should add grid to the sizer like this:
sizer.Add(grid, 1, flag=wx.ALIGN_CENTER_HORIZONTAL|wx.SHAPED)
The only other thing you need to do is tell the sizer what your desired aspect ratio is. The easiest way is to set an initial size for your TestGrid (before it's added to the sizer); for example, set the size of its wxPanel base:
wx.Panel.__init__(self, parent, -1, size=(COLS*7, ROWS*7))
That's all there is to it.
(I'm just guessing the Python syntax, so ignore any obvious errors :-) )
You can't, or at the very least shouldn't, resize a window from its own wxEVT_SIZE handler. I'm not sure what exactly are you trying to achieve, but if the goal is to encapsulate everything in TestGrid class, then you should let it have whichever size it has and create a nested window in it that you would resize to the appropriate size in your OnSize.
I uses a WrapSizer in order to have an automatic layout (as thumbnail gallery) like this (see screenshot on the left) :
I would like that if there are two many elements, a (vertical only)-ScrollBar is added on the panel (see right screenshot). How to add such a vertical scrollbar to a panel using a WrapSizer?
I tried by mixing WrapSizer and ScrolledPanel, but I cannot get the desired layout.
class MyPanel(scrolled.ScrolledPanel):
def __init__(self, parent):
scrolled.ScrolledPanel.__init__(self, parent)
self.SetBackgroundColour('#f8f8f8')
sizer = wx.WrapSizer()
self.SetupScrolling()
# add some widgets btn1, btn2, etc. in the WrapSizer
sizer.Add(btn1, 0, wx.ALL, 10)
sizer.Add(btn2, 0, wx.ALL, 10)
Solution:
reset the width of the scroll panel virtual size to the displayable size.
import wx
import wx.lib.scrolledpanel as scrolled
class MyPanel(scrolled.ScrolledPanel):
def __init__(self, parent):
scrolled.ScrolledPanel.__init__(self, parent, style=wx.VSCROLL)
self.SetBackgroundColour('#f8f8f8')
self.sizer = wx.WrapSizer()
self.SetupScrolling(scroll_x = False)
self.parent = parent
self.addButton(self.sizer , 10)
self.SetSizer(self.sizer )
self.Bind(wx.EVT_SIZE, self.onSize)
def onSize(self, evt):
size = self.GetSize()
vsize = self.GetVirtualSize()
self.SetVirtualSize((size[0], vsize[1]))
evt.Skip()
def addButton(self, sizer, num):
for i in range(1, num):
btn =wx.Button( self, wx.ID_ANY, "btn"+str(i), wx.DefaultPosition, wx.DefaultSize, 0 )
sizer.Add(btn, 0, wx.ALL, 10)
if __name__=='__main__':
app = wx.App(redirect=False)
frame = wx.Frame(None)
MyPanel(frame)
frame.Show()
app.MainLoop()
It looks like you just forgot to include
self.SetSizer(sizer)
Since the WrapSizer takes the whole frame, I think that will work. Also, instead of SetupScrolling, you can use
self.SetScrollRate(horiz, vert)
to specify the increment (in pixels, i think) of the scroll, and that should work.
I can't test it here right now though, and WrapSizers are a little weird - they sometimes have trouble figuring out their proper size. You may need to wrap it in a BoxSizer going the other direction.
I have a set of images available. If I click on one of those images is there a way to determine which of the images has been clicked on in wxPython?
You will almost certainly have to calculate it for yourself. The most straight-forward method would be to use a mouse event like wx.EVT_LEFT_DOWN and grab the mouse's coordinates in the event handler. Then use that information to tell you where on your wxPython window you clicked. Each of your image widgets or DCs or whatever you're using can report it's size and position, so if the mouse coordinates are in X image's boundaries, you know it's been clicked on. You might also be able to use the HitTest() method, depending on what you're using to show the images.
EDIT: Here is how you would do it if you were using a wx.StaticBitmap, which actually lets you attach an wx.EVT_LEFT_DOWN to it:
import wx
class PhotoCtrl(wx.Frame):
def __init__(self):
size = (400,800)
wx.Frame.__init__(self, None, title='Photo Control', size=size)
self.panel = wx.Panel(self)
img = wx.EmptyImage(240,240)
self.imageCtrl = wx.StaticBitmap(self.panel, wx.ID_ANY,
wx.BitmapFromImage(img),
name="emptyImage")
imageCtrl2 = wx.StaticBitmap(self.panel, wx.ID_ANY,
wx.BitmapFromImage(img),
name="anotherEmptyImage")
self.imageCtrl.Bind(wx.EVT_LEFT_DOWN, self.onClick)
imageCtrl2.Bind(wx.EVT_LEFT_DOWN, self.onClick)
mainSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer.Add(self.imageCtrl, 0, wx.ALL, 5)
mainSizer.Add(imageCtrl2, 0, wx.ALL, 5)
self.panel.SetSizer(mainSizer)
self.Show()
#----------------------------------------------------------------------
def onClick(self, event):
""""""
print event.GetPosition()
imgCtrl = event.GetEventObject()
print imgCtrl.GetName()
if __name__ == '__main__':
app = wx.App(False)
frame = PhotoCtrl()
app.MainLoop()
you dont tell us anything about how you are displaying your images? are you blitting them right on the dc? are you creating panels for them? etc... properly setting up your project is important. basically you give us zero information to help you with.
Keeping all that in mind, something like this would work fine (this is called a self contained code example, you should always provide one with your questions, to make it easier for people to help you)
import wx
a = wx.App(redirect=False)
f= wx.Frame(None,-1,"Some Frame",size = (200,200))
sz = wx.BoxSizer(wx.HORIZONTAL)
def OnClick(evt):
print "Clicked:",evt.GetId()-10023
for i,img in enumerate(["img1","img2","img3"]):
id = 10023+i
p = wx.Panel(f,-1)
sz.Add(p)
sz1 = wx.BoxSizer()
p.Bind(wx.EVT_LEFT_UP,OnClick)
bmp = wx.Image(img).ConvertToBitmap()
b = wx.StaticBitmap(p,-1,bmp)
sz1.Add(b)
p.SetSizer(sz1)
f.SetSizer(sz)
f.Layout()
f.Fit()
f.Show()
a.MainLoop()
Keep in mind I didnt test it... but theoretically it should work...
wx.SpinCtrl is limited to spinning across integers, and not floats. Therefore, I am building a wx.TextCtrl + wx.SpinButton combo class which enables me to spin across floats. I am able to size and layout both of them programmatically so that the combo looks exactly the same as an ordinary wx.SpinCtrl.
I am subclassing this combo from the wx.TextCtrl because I want its parent panel to catch wx.EVT_TEXT events. I would appreciate if you can improve on this argument of mine.
The wx.EVT_SPIN_UP and wx.EVT_SPIN_DOWN events from the wx.SpinButton are both internal implementations and the parent frame doesn't care about these events.
Now, I just hit a brick wall. My combo class doesn't work well with sizers. After .Add()ing the combo class to a wx.GridBagSizer, only the wx.TextCtrl is laid out within the wx.GridBagSizer. The wx.SpinButton is left on its own by itself. The wx.EVT_SPIN* bindings work very well, though.
My problem is the layout. How should I write the class if I want the wx.GridBagSizer to treat it as one widget?
Here is my combo class code:
class SpinnerSuper(wx.TextCtrl):
def __init__(self, parent, max):
wx.TextCtrl.__init__(self, parent=parent, size=(48, -1))
spin = wx.SpinButton(parent=parent, style=wx.SP_VERTICAL, size=(-1, 21))
self.OnInit()
self.layout(spin)
self.internalBindings(spin)
self.SizerFlag = wx.ALIGN_CENTER
self.min = 0
self.max = max
def OnInit(self):
self.SetValue(u"0.000")
def layout(self, spin):
pos = self.GetPosition()
size = self.GetSize()
RightEdge = pos[0] + size[0]
TopEdge = pos[1] - (spin.GetSize()[1]/2 - size[1]/2)
spin.SetPosition((RightEdge, TopEdge))
def internalBindings(self, spin):
spin.Bind(wx.EVT_SPIN_UP, self.handlerSpinUp(self), spin)
spin.Bind(wx.EVT_SPIN_DOWN, self.handlerSpinDown(self), spin)
def handlerSpinUp(CallerObject, *args):
def handler(CallerObject, *data):
text = data[0]
prev = text.GetValue()
next = float(prev) + 0.008
text.SetValue("{0:0.3f}".format(next))
return lambda event: handler(CallerObject, *args)
def handlerSpinDown(CallerObject, *args):
def handler(CallerObject, *data):
text = data[0]
prev = text.GetValue()
next = float(prev) - 0.008
text.SetValue("{0:0.3f}".format(next))
return lambda event: handler(CallerObject, *args)
You need to override DoGetBestSize() if you want your control to be
correctly managed by sizers. Have a look at CreatingCustomControls.
You could also have a look at FloatSpin that ships with wxPython
(in wx.lib.agw) from version 2.8.9.2 upwards.
In response to your comments:
Implementing DoGetBestSize() does not require drawing bitmaps directly. You just need to find a way, how you can determine the best size of your new widget. Typically you'd just use
the sizes of the two widgets it is composed of (text + spinner) as basis.
To let sizers treat two widgets as one, you can place them in another sizer.
The recommended way to implement a custom widget with wxPython is to derive your new widget from wx.PyControl, add a sizer to it and add the two widgets you want to combine to that sizer.
As mentionned in Kit's comments, FloatSpin is now the way to go.
It has been integrated in recent versions.
Here is a simple example of usage:
import wx
from wx.lib.agw.floatspin import FloatSpin
class Example_FloatSpin(wx.Frame):
def __init__(self, parent, title):
super(Example_FloatSpin, self).__init__(parent, title=title, size=(480, 250))
panel = wx.Panel(self)
vbox = wx.BoxSizer(wx.VERTICAL)
spin = FloatSpin(panel, value=0.0, min_val=0.0, max_val=8.0, increment=0.5, digits=2, size=(100,-1))
vbox.Add(spin, proportion=0, flag=wx.CENTER, border=15)
panel.SetSizer(vbox)
self.Centre()
self.Show()
if __name__ == '__main__':
app = wx.App()
Example_FloatSpin(None, title='Check FloatSpin')
app.MainLoop()