Python/wxWidgets: Vertically aligning text in a wrapped StaticText - python

I've seen a lot of posts about this around the Internet but none of the advice works.
Here is my test code:
import wx
class DisplayText(wx.Dialog):
def __init__(self, parent, text="", displayMode=0):
# Initialize dialog
wx.Dialog.__init__(self, parent, size=(480,320), style=( wx.DIALOG_EX_METAL | wx.STAY_ON_TOP ) )
# Freeze UI so user won't see stuff flashing
self.Freeze()
# (For Mac) Setup a panel
self.panel = wx.Panel(self,size=(480,320))
self.panel.SetBackgroundColour(wx.Colour(0,0,128))
# Setup sizer for vertical centering
self.sizer = wx.BoxSizer(wx.HORIZONTAL)
# Create text field
self.txtField = wx.StaticText(self.panel,size=(448,-1),style=wx.ALIGN_CENTRE_HORIZONTAL)
self.txtField.SetLabel(text)
self.txtField.Wrap(448)
self.txtField.SetLabel(self.txtField.GetLabel())
self.txtField.SetFont(wx.Font(36, wx.DEFAULT, wx.BOLD, 0))
self.txtField.SetForegroundColour(wx.Colour(255,255,255))
# Add the static text to the sizer
self.sizer.Add(self.txtField,1, 0,15)
self.panel.SetSizer(self.sizer)
# Ensure layouts are applied
self.sizer.Layout()
# Thaw UI
self.Thaw()
# Center form
self.Center()
app = wx.App(False)
c = DisplayText(None, text="Now is the time for all good men to come to the aid of their country.")
c.Show()
import wx.lib.inspection
wx.lib.inspection.InspectionTool().Show()
app.MainLoop()
Based on my understanding of StaticText, the problem seems to be that when I call the Wrap() function, the control wraps the text but does not change the control's size. This means the sizer, unaware of the actual vertical size of the wrapped text, cannot properly reposition the static text.
One post I found suggested using wx.EXPAND, but this does not solve the problem. Allowing the sizer to expand the StaticText simply makes the StaticText fill the entire frame, and since StaticText doesn't vertically center in and of itself, the text will end up at the top of the form, not the center.
Another post suggested experimenting with wx.ALIGN_CENTER_VERTICAL in the sizer, but this didn't help either, because the same problem remains - the sizer can only work on the size of the control, and the control's size is NOT changing after the text is wrapped and re-rendered.
Various attempts have resulted in either the text wrapped but at the top of the form, only the first line being shown in the center of the form, only the first line being shown at the top of the form, and the text not being wrapped and thus flowing off the right hand edge of the form. But no combination of anything I've read has worked to vertically center the wrapped text on the form.
What I therefore would have to be able to do is somehow figure out the size of the text vertically after the wrapping has taken place. This can't be assumed because on different platforms, or even on the same platform, or even based on the text itself, the font's vertical pixel size may differ. So this needs to somehow be calculated.
I'm thinking if I can somehow figure out the actual size of the rendered text inside the StaticText control, I could then explicitly set the control's size and then let the sizer do its work.
This seems like it should be a relatively simple thing to accomplish...
Advice would be greatly appreciated!

The key was the static text style; you needed to enable the multi-line style for the wrap to work: wx.TE_MULTILINE.
I also set the layout on the panel itself, not the sizer but that might not be related to the problem.
On my machine, Windows 7 64bit, I needed to change the SetFont call; you might need to change it back.
import math
from threading import Thread
import time
import wx
e = lambda x: complex(math.cos(x), 0) + complex(0, math.sin(x))
r = lambda x: ((e(0*math.pi/3.0)*e(x)).real + 1)/2.0
g = lambda x: ((e(2*math.pi/3.0)*e(x)).real + 1)/2.0
b = lambda x: ((e(4*math.pi/3.0)*e(x)).real + 1)/2.0
colo = lambda rad: map(lambda x: int(128 * x(rad)), [r, g, b])
class DisplayText(wx.Dialog):
def __init__(self, parent, text="", displayMode=0):
# Initialize dialog
wx.Dialog.__init__(self, parent, size=(480, 320), style=( wx.DIALOG_EX_METAL | wx.STAY_ON_TOP ) )
# Freeze UI so user won't see stuff flashing
self.Freeze()
# (For Mac) Setup a panel
self.panel = wx.Panel(self, size=(480, 320))
self.panel.SetBackgroundColour(wx.Colour(0, 0, 128))
self.panel.SetBackgroundColour(wx.Colour(*colo(0)))
# Setup sizer for vertical centering
self.sizer = wx.BoxSizer(wx.HORIZONTAL)
self.panel.SetSizer(self.sizer)
# Create text field
self.txtField = wx.StaticText(self.panel, size=(448, -1), style=(wx.ALIGN_CENTRE_HORIZONTAL | wx.TE_MULTILINE ))
self.txtField.SetFont(wx.Font(36, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD))
self.txtField.SetForegroundColour(wx.Colour(255, 255, 255))
self.txtField.SetLabel(text)
self.txtField.Wrap(448)
self.sizer.Add(self.txtField, 1, 0, 15)
# Add the static text to the sizer
# Ensure layouts are applied
self.panel.Layout()
self.panel.Refresh()
# Thaw UI
self.Thaw()
# Center form
self.Center()
self.angle = 0
self.angle_inc = (2*math.pi)/25.0
def change_colour(self):
t = Thread(target=self.epilepsy_mode)
t.start()
def epilepsy_mode(self):
while True:
time.sleep(0.02)
self.panel.SetBackgroundColour(wx.Colour(*colo(self.angle)))
self.panel.Refresh()
self.angle = (self.angle + self.angle_inc) % (2*math.pi)
app = wx.App(False)
c = DisplayText(None, text="Now is the time for all good men to come to the aid of their country.")
c.Show()
#import wx.lib.inspection
#wx.lib.inspection.InspectionTool().Show()
c.change_colour()
app.MainLoop()

Related

wxPython SetMinSize seems to set size explicitly, not allows window to grow when needed

Considering the code below. I create a StaticText on which I set a Minimum size. However, the StaticText always gets this size as the actual size. Even though the text does not fit. Am I doing something wrong here ?
I want to use this functionality to create a key-value display with proper alignment of the values and only expanding the key part if needed:
a key - the value
another key - another value
a big key which does not fit - this value
the key - the value
import wx
class TestPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
sizer = wx.BoxSizer()
static_text = wx.StaticText(
parent=self, label="a long label exceeding min size."
)
static_text.SetMinSize(wx.Size(50, -1))
sizer.Add(static_text)
self.SetSizer(sizer)
if __name__ == "__main__":
app = wx.App()
frm = wx.Frame(None, title="Test MinSize")
pnl = TestPanel(frm)
frm.Show()
app.MainLoop()
This may not be want to you want to hear but here goes.
The sizer performs a dance between the programmers desire to display data and the variable nature of that data. Additionally it has to cope with that pesky external thing we refer to as the user, who has a habit of resizing windows.
SetMinSize is an instruction to the sizer, a hint if you like, on what it should attempt to do automatically. Most controls will also set the minimal size to the size given in the control’s constructor, as a best guess.
These instructions can be overridden or adjusted with the proportion and flag values of the sizer entry for that control.
Always keep in mind that other controls within the same sizer will be affected, which can have undesirable results on presentation.
If we give the sizer the wx.EXPAND flag for that control, it will show the whole widget whilst keeping the MinSize. In this case, expanding it vertically.
If we give the sizer a proportion of 1 it will stretch it as much as it can in the direction of the sizer, relative to other controls sharing that sizer.
To appreciate what goes on, it's best to play with code like this, altering the MinSize, proportion and flags, testing each change, until the voodoo of sizers becomes slightly less obscure.
Note: In addition, also test re-sizing the window, to see what happens in each scenario.
Additional testing notes:
Test sizers with more than 1 control or widget
Assign colours to highlight what isn't always obvious.
import wx
class TestPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
sizer = wx.BoxSizer(wx.HORIZONTAL)
static_text = wx.StaticText(
parent=self, label="a long label exceeding min size.")
static_text2 = wx.StaticText(self, label="another label")
static_text.SetBackgroundColour('green')
static_text.SetMinSize(wx.Size(50, -1))
sizer.Add(static_text, proportion=1, flag=wx.ALL, border=0)
sizer.Add(static_text2, 0, wx.EXPAND, 0)
self.SetSizer(sizer)
if __name__ == "__main__":
app = wx.App()
frm = wx.Frame(None, title="Test MinSize", size=(300,100))
pnl = TestPanel(frm)
frm.Show()
app.MainLoop()
Examples for the above code:

Fixing layout for this ListCtrl

I understand there are lots of settings for setting layouts. Here is a ListCtrl of fixed dimensions inside a horizontal sizer.
This absolute size either displays leftover space or is too small depending on number of columns.
What are the layout commands to draw a ListCtrl?
How do I make the ListCtrl more responsive, so that it:
shows data from many columns at a glance without needing a resize
doesnt become bad on different GUIs and resizes
Code listing for the panel that hosts the list:
class CalcPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.lc = wx.ListCtrl(self, -1, size=(200,200), style=wx.LC_REPORT)
self.lc.InsertColumn(0, 'State')
self.lc.InsertColumn(1, 'Capital')
#self.lc.SetColumnWidth(0, 140)
#self.lc.SetColumnWidth(1, 153)
#self.list_ctrl.Show()
self.sizer = wx.BoxSizer(wx.HORIZONTAL)
btn = wx.Button(self, label="Add Line")
self.sizer.Add(btn)
self.sizer.Add(self.lc, 1, wx.LEFT | wx.TOP | wx.GROW)
self.SetSizer(self.sizer)
self.Fit()
def InitUI(self):
pass
Have you looked at wx.lib.mixins.listctrl.ListCtrlAutoWidthMixin?
if you inherit from wx.lib.mixins.listctrl.ListCtrlAutoWidthMixin as well as wx.ListCtrl, and call the classes __init__ in your __init__ method the last column will auto resize with resize events
You can also change which column auto resizes with setResizeColumn
I submitted an answer that was COMPLETELY wrong because I did not read the question anywhere near well enough. To make up for this mistake, I did some research and I think I have an answer that works.
Checking the wx.ListCtrl API, I've found a method SetColumnWidth(self, col, width) which I think does exactly what you want.
You said that your wx.ListCtrl is a fixed width? In that case...
lcWidth = 200
lcHeight = 200
self.lc = wx.ListCtrl(self, -1, size=(lcWidth,lcHeight), style=wx.LC_REPORT)
self.lc.InsertColumn(0, 'State')
self.lc.InsertColumn(1, 'Capital')
stateWidth = self.lc.GetColumnWidth(0)
self.lc.SetColumnWidth(1, (lcWidth - stateWdith))
Incidently, if you wish your wx.ListCtrl to remain a fixed size, you should remove the wx.GROW flag from your self.sizer.Add() call. wx.GROW will resize the item so that it fills all the available space, not what you want.
self.sizer.Add(self.lc, 1, wx.LEFT | wx.TOP)
If your wx.ListCtrl is NOT a fixed width, you could declare a custom wx.ListCtrl that handles this resizes accordingly
class ResizingColListCtrl (wx.ListCtrl):
def __init__(self, *args, **kwargs):
wx.ListCtrl.__init__(self, *args, **kwargs)
self.sizeColumns()
self.Bind(EVT_SIZE, self.sizeColumns)
def sizeColumns(event=None)
width = self.GetSize()[0]
numCols = self.GetColumnCount()
for i in range(numCols):
self.SetColumnWidth(i, width/numCols) #set your column width to whatever proportions you want
if event:
event.Skip() #since this is a wx.EVT_SIZE, this line is important
So far as I can tell, wxPython does not have this functionality natively.

Force a panel to be square in wx (python)

Does anybody know how i would go about forcing a panel to be square.
The case where this occurs is have a panel in which I have a horizontal BoxSizer with 2 slots, In the left slot i have a panel that I am going to be drawing to though wx.PaintDC, on the right I am going to have a a list control or some other widget.
What i am trying to achieve is to have the window realizable and to have the left panel always stay square, and to have the right hand content fill the rest of the space.
One of the solutions is to use EVT_SIZE to respond to window resizing and update panel size in the event function. Simple example code:
import wx
from wx.lib.mixins.inspection import InspectionMixin
class MyApp(wx.App, InspectionMixin):
def OnInit(self):
self.Init() # initialize the inspection tool
frame = wx.Frame(None)
sizer = wx.BoxSizer(wx.HORIZONTAL)
frame.SetSizer(sizer)
self.__squarePanel = wx.Panel(frame)
sizer.Add(self.__squarePanel, 0, wx.ALL | wx.EXPAND, 5)
frame.Bind(wx.EVT_SIZE, self.OnSize)
frame.Show()
self.SetTopWindow(frame)
return True
def OnSize(self, evt):
frame = evt.GetEventObject()
frameW, frameH = frame.GetSize()
targetSide = min(frameW, frameH)
self.__squarePanel.SetSize((targetSide, targetSide))
app = MyApp()
app.MainLoop()
You can bind to wx.EVT_SIZE to resize the panel when the window is resized. Partial code (untested, but someting like this):
self.panel = wx.Panel(self, -1, size=(200, 200))
self.Bind(wx.EVT_SIZE, self.resize_panel)
def resize_panel():
w, h = self.sizer.GetSize()
w = h
panel.SetSize(w, h)

When its containing panel or frame becomes too small, wxListBox stops using its horizontal scrollbar and expands to take up too much space

SSCCE is at the bottom
My layout has two wx.ListBoxes, side-by side in a wx.FlexGridSizer:
My real layout is more complex, thus the FGS, but this small example still exhibits the problem.
As you can see above, I have successfully used style = wx.LB_HSCROLL to make each listbox use a horizontal scroll bar when one of its elements would make it too large to fit in the wx.Frame.
However, as I resize the window smaller and smaller, eventually some critical point is reached, the first listbox decides it doesn't want to use its scrollbar anymore, and instead expands to its full size, pushing the second box to the right:
The point at which the list goes crazy depends on how long the string is. If I put a long enough string in the first box, then the above process is reversed: the layout starts off wrong and I have to resize the window up to the critical point, where all of a sudden the listbox starts using its scrollbar, gets a lot smaller, and the window becomes split down the middle as it should be.
I'm not sure if this is a bug in wxWidgets/wxPython or if I'm doing something wrong, but it's frustrating either way. Here is the simplest code I can come up with that shows the problem:
import wx
class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent = None, size = (640, 480))
self.list1 = wx.ListBox(self, style = wx.LB_HSCROLL)
self.list2 = wx.ListBox(self, style = wx.LB_HSCROLL)
self.list1.Append('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
self.list2.Append('bbbbbbbbbbb')
self.fgs = wx.FlexGridSizer(1, 2)
self.fgs.AddMany([(self.list1, 1, wx.EXPAND), (self.list2, 1, wx.EXPAND)])
self.fgs.AddGrowableRow(0, 1)
self.fgs.AddGrowableCol(0, 1)
self.fgs.AddGrowableCol(1, 1)
self.Bind(wx.EVT_SIZE, self.OnSize)
self.Sizer = fgs
self.Layout()
self.Show()
def Exit(self, event):
self.Close(True)
app = wx.App(False)
frame = MyFrame()
app.MainLoop()
EDIT: Here is my implementation of ravenspoint's code in python (code above was changed slightly to support this):
def OnSize(self, event):
if not self.list1 or not self.list2;
return
clientRect = self.GetClientRect()
min = wx.Size(clientRect.width / 2, clientRect.height)
self.list1.MinSize = min
self.list2.MinSize = min
You may have to look after handling the resize event yourself. In C++ the handler would look something like this:
void MyFrame::OnSize(wxSizeEvent& )
{
if( ! ( list1 && list2 ) )
return;
wxRect frame_client = GetClientRect();
wxSize min(frame_client.width/2,frame_client.height );
list1->SetMinSize(min);
list2->SetMinSize(min);
fgs->Layout();
}
Since I ran into this problem as well, I'll post this for future reference. At least with wxPython, you must specify a min size or it will use the best size (at least for list boxes) and the sizer could possibly shove widgets outside of the current window frame. Thus, the added code to get the above code working as desired is:
self.list1.SetMinSize((10,10));
self.list2.SetMinSize((10,10));
The total working answer is:
import wx
class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent = None, size = (640, 480))
self.list1 = wx.ListBox(self, style = wx.LB_HSCROLL)
self.list2 = wx.ListBox(self, style = wx.LB_HSCROLL)
self.list1.Append('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
self.list2.Append('bbbbbbbbbbb')
self.fgs = wx.FlexGridSizer(1, 2)
self.fgs.AddMany([(self.list1, 1, wx.EXPAND), (self.list2, 1, wx.EXPAND)])
self.fgs.AddGrowableRow(0, 1)
self.fgs.AddGrowableCol(0, 1)
self.fgs.AddGrowableCol(1, 1)
# Don't forget these two lines to allow for correct
# expansion/contraction of the sizer!
self.list1.SetMinSize((10,10));
self.list2.SetMinSize((10,10));
self.Sizer = self.fgs
self.Layout()
self.Show()
if __name__ == "__main__":
app = wx.App(False)
frame = MyFrame()
app.MainLoop()

Issue with wxPython mulltiline text box being too large for wx.Frame - can I auto "shrink" to the correct size of the text box?

Not sure if I am asking this correctly (e.g. using widget/object in the correct context). This code is meant just as an experiment (learning about classes and wxPython right now) - so it might not make sense as functional code.
Problem: I have a window/frame that I am allowing the user to specify width/height, and there is a multiline text box that is nested inside the window/frame. Originally, I was setting the dimensions of the multiline text box to the same size of the parent frame, but the scroll bar was being hidden because the scroll bar of the text box is larger than the frame/text box size. So... I decided to just manually adjust the size of the text box to make sure the size of the text box accounted for the scroll bar (see code below, specifically lines "sizexTxt = sizex - 17" and "sizeyTxt = sizey - 44").
Question: Is there some sort of way for adjusting the text box other than manually adjusting pixel by pixel? Is there a suggestion someone can offer that would be considered pythonic?
import wx
class myFrame(wx.Frame):
def __init__(self, parent, title, sizexy, sizexyTxt):
wx.Frame.__init__(self, parent, title=title, size=sizexy)
self.Show(True)
wx.TextCtrl(self, style=wx.TE_MULTILINE, size=(sizexyTxt))
pass
sizex = input("X= ")
sizey = input("Y= ")
sizexy=(sizex, sizey)
sizexTxt = sizex - 17
sizeyTxt = sizey - 44
sizexyTxt = (sizexTxt, sizeyTxt)
myApp = wx.App(False)
frameNow = myFrame(None,"This is my title", sizexy, sizexyTxt)
myApp.MainLoop()
Jake pointed you in a right direction, but there is one more thing. You should be aware that first object which goes into the frame is wrapped into the basic sizer. The problem is that as far as I know you have no control of this sizer. So you have to add your own sizer, which you can use for frame fitting. You need the control over the sizer, because you would like the frame to change its size according to sizer.
import wx
class myFrame(wx.Frame):
def __init__(self, size, *arg, **kwarg):
wx.Frame.__init__(self, *arg, **kwarg)
self.text = wx.TextCtrl(self, style=wx.TE_MULTILINE, size=size)
self.sizer = wx.BoxSizer()
self.sizer.Add(self.text, proportion=1, flag=wx.EXPAND)
self.SetSizerAndFit(self.sizer)
self.Show()
myApp = wx.App(False)
size = []
for a in ["X", "Y"]:
d = wx.NumberEntryDialog(None, "Text size", "%s: " % a, "Window size entry", 100, 50, 200)
if d.ShowModal() == wx.ID_OK:
size.append(d.GetValue())
frameNow = myFrame(size, None)
myApp.MainLoop()
You'll want to use a sizer (or sizers) and then specify wx.EXPAND when adding to make it fit the sizer horizontally and wx.ALL to fit vertically.
Check out the sizer documentation here: http://wiki.wxpython.org/Getting%20Started#Sizers
Actually you might want to read the whole Working with Windows section, very useful.

Categories