Search code examples
wxpythonword-wraptextctrl

WXPython, can't make word wrap work for TextCtrl with very long word


I'm trying to create a full screen window in which statictextctrl with quite long label are layed out in a table, with vertical scrollbar when there are a lot of them. What I need to do is wrap the label accordingly to the width that is available to each statictextctrl (based on the number of textctrl per row and the hpad). Unfortunately, when there is a very long word, I can't make it work, the word is not wraped, even if I set explicitely the textctrl style to wx.TE_BESTWRAP (which is default anyway, but I thought it was worth trying). Any idea how to achieve this?

import wx
import wx.lib.scrolledpanel as scrolled

class MyPanel(scrolled.ScrolledPanel):

    def __init__(self, parent):
        scrolled.ScrolledPanel.__init__(self, parent, -1)
        
        
        ## Configuring the panel
        self.SetAutoLayout(1)
        self.SetupScrolling()
        self.SetScrollRate(1,40)
        
    # needs to be called after main window's laytou, so its size is actually
    # known and can be used to compute textctrl's width
    def Build(self):
        labelPerRow=7
        hgap = 40
        vgap = 20
        label_width=int(self.GetClientSize()[0]/labelPerRow)-hgap
        print("label width", label_width)
        
        grid_sizer = wx.FlexGridSizer(labelPerRow,  vgap, hgap)
        
        self.SetSizer(grid_sizer)
        i=0
        for label in range(200)   :
            label = "very long title withaverybigwordthatdoesntfitonasinglelinesoitsquitehardtomanagewordwrap"+str(i)
            title = wx.StaticText(self,
                                  label=label,
                                  style=wx.ALIGN_CENTRE_HORIZONTAL |
                                        wx.TE_BESTWRAP)
            
            title.Wrap(int(label_width))
            grid_sizer.AddMany([(title)])
            i = i+1
            
        self.Layout()

class MyFrame(wx.Frame):

    def __init__(self):

        wx.Frame.__init__(self, None, title="Test",  style=wx.NO_BORDER)
        self.Maximize(True)
        frameSizer = wx.BoxSizer()
        p = MyPanel(self)
        frameSizer.Add(p, 1, wx.EXPAND)
        self.SetSizer(frameSizer)
        self.Layout()
        p.Build()


app = wx.App(0)
frame = MyFrame()
frame.Show()
app.MainLoop()

Solution

  • Here is what I came up with. I hope it will be usefull to others. Basically, it computes every combinations of splitting either with '\n' or ' ', keeps the label for which the width fits and with minimum height. If no wrapping fits, it uses the label with the smallest width, and removes the proper characters at the end, replacing them with an ellipsis, for the label to fit. It could be improved, for example with a max number of rows or more options for text alignment, but it proved effective so far. Note that computing all the combinations may be time-consuming if the unwrapped label is long (the number of combinations being the exponential of the number of spaces).

    
    class WrappedStaticText(wx.StaticText):
        """
        A StaticText with advanced wrapping management. Every possible wrapping
        of the label is computed (can be CPU-consuming if label has many spaces) and
        the best one is selected, i.e. the one that respects the wrapping rule and
        uses the smallest number of rows. If no wrapping is possible, an ellipsed
        label is used, based on the least wide and high combination.
        """
        def __init__(self, parent, label, width, font, center=False):
            """
            Constructor.
            
            * `parent`: wx parent,
            * `label`: text to be displayed wrapped
            * `width`: width on which `label` is wrapped
            * `font`: Font used to compute xrapping and display the wrapped label
            * `center`: Whether the wrapped label is centered
            """
            super().__init__(parent)
            
            self.center = center
            self.unwrappedLabel = label
            self.wrappedWidth = width
            self.maxRows = max_rows
            
            self.SetFont(font)
            self.SetLabel(label)
            self.Bind(wx.EVT_PAINT, self.OnPaint) 
    
        def SetLabel(self, label):
            """
            Changes the text's label. Recompute the wrapped label
            
            * `label`: new label to wrap and display
            """
            dc = wx.ScreenDC()
            dc.SetFont(self.GetFont())
            
            label_split = label.split()
            
            ## For wrapping label
            best_height=sys.maxsize
            best_height=sys.maxsize
            best_label = None
            
            ## For ellipsis if wrapping is not possible because of too long word
            best_unwrapped_height=sys.maxsize
            best_unwrapped_width=sys.maxsize
            best_unwrapped_label = None
            labels = self.get_wrappings(label_split[0], label_split[1:])
            
            ## Search for the best label
            for l in labels:
                w,h = dc.GetMultiLineTextExtent(l)
                
                ## Update best wrapping
                if w <= self.wrappedWidth:
                    if h < best_height:
                        best_label  = l
                        best_height = h
                ## Update best ellipsis in case of wrapping fail
                elif w == best_unwrapped_width:
                     if h < best_unwrapped_height:
                            best_unwrapped_label  = l
                            best_unwrapped_height = h
                elif w < best_unwrapped_width:
                            best_unwrapped_label  = l
                            bes_unwrappedt_widtht = w
                            best_unwrapped_height = h
            
            ## In case of wrapping fail, search for the best ellipsed label
            if best_label == None:
                while best_unwrapped_width > self.wrappedWidth:
                    best_unwrapped_label = best_unwrapped_label[:-1]
                    best_unwrapped_width = \
                        dc.GetMultiLineTextExtent(best_unwrapped_label+"…")[0]
                best_label = best_unwrapped_label+"…"
            super().SetLabel(best_label)
            print(best_label)
    
        def SetFont(self, font):
            """
            Changes the text's font. Recompute the wrapped label
            
            * `font`: new font to wrap and display the text's label with
            """
            super().SetFont(font)
            self.SetLabel(self.unwrappedLabel)
    
        def get_wrappings(self, text, words):
            """
            Recursively compute all combinations of text splitting using either a
            space or a new line. May be time-consuming if label has too many spaces.
            
            * `text`: beginning of the full label, already wrapped
            * `words`: list of remaining words to be added to `text`
            
            * yields: every combinations of `text` and remaining words, concatenated
              either with `'\n'` ot `' '`
            """
    
            if len(words) == 0:
                ## If there is no space at all in the unwrapped label
                yield text
            elif len(words) == 1:
                ## if it's the last word of the text, just yield the two versions
                yield text+" "  + words[0]
                yield text+"\n" + words[0]
            else:
                ## try the two spliting combinations, and recursively compute the
                #  wrapping for the remaining text
                yield from self.get_wrappings(text+" "  + words[0], words[1:])
                yield from self.get_wrappings(text+"\n" + words[0], words[1:])
    
        
        def OnPaint(self, event):
            """
            Paint callback. Overrides the paint event callback so that centered text
            painting renders the right way with wrapped text.
            """
            dc = wx.PaintDC(self) 
    
            curr_size   = self.GetSize()
            curr_width  = curr_size[0]
            curr_height = curr_size[1]
            
            label_h = 0
            for line in self.GetLabel().split("\n"):
                text_w, text_h = dc.GetTextExtent(line)
                if self.center:
                    w = curr_width/2 - text_w/2
                else:
                    w = 0
                dc.DrawText(line, w, label_h)
                label_h+=text_h