Search code examples
pythonwxpythonwxwidgets

wx.StaticText updates slower with whitespace?


I'm writing a simple roguelike in Python 3.10 as a way to learn the language as well as principles of proper software architecture. I'm using wxPython310 (4.1.2a2).

The map is an octagonal grid of cells that player may traverse. My map is a wx.StaticText that updates once every user input. I set it to DoubleBuffered, as per advice I found here. It works really nice.

Recently I started implementing field of vision mechanics that makes tiles (grid cells) outside of player's nearby surroundings return 'fog of war' character and only display their actual character representation once a player approaches (the player is denoted as '@' character):

The player is @ character.

I noticed that if I keep holding a move arrow, the wx.StaticText doesn't update until I release it. Disabling DoubleBuffered obviously corrects that behaviour, but makes the screen very blinky (as in, it flickers very rapidly). What's more interesting, I noticed that the wx.StaticText updates correctly and quickly if a 'fog of war' character is some actual character, rather than just whitespace (in the following example, an underscore):

enter image description here

It's as if wxStaticText updated slower when there's plenty of whitespace, but quicker if there's a proper character.

My reproducible example code:

import wx


class MyFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super().__init__(None, *args, **kwargs)
        self.Title = 'Stackoverflow example'
        self.SetSize(width=1480, height=600)

        self.panel = MainPanel(self)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel,1, wx.EXPAND)
        self.SetSizer(sizer)
        self.Center()
        self.Layout()
        self.Show()


class MainPanel(wx.Panel):
    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)

        self.mymap = MyMap()
        self.x = -1
        self.moveRight = True

        self.SetDoubleBuffered(True)

        font = wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.DEFAULT)
        font.SetFaceName('Courier New')
        self.ST = wx.StaticText(self, label=self.Display(), style=wx.ALIGN_LEFT)
        self.ST.SetFont(font)

        self.Bind(wx.EVT_KEY_DOWN, self.EvaluateCommand)

    def EvaluateCommand(self,e):
        key = e.GetKeyCode()
        if key == 78: # n for newbie!
            self.MoveRectangleOnAMap()
            self.ST.SetLabel(self.Display())
        elif key == wx.WXK_ESCAPE:
            quit()

    def MoveRectangleOnAMap(self):
        all_tiles = self.mymap.FindRectangleOfTiles((0, 0), (self.mymap.width, self.mymap.height))
        for tile in all_tiles:
            tile.SetDisplayToDormant()

        if self.moveRight:
            self.x += 1
            if self.x == self.mymap.width - 15:
                self.moveRight = False
        else:
            self.x -= 1
            if self.x == 0:
                self.moveRight = True

        rectangle = self.mymap.FindRectangleOfTiles((self.x, 10), (self.x + 15, 25))
        for tile in rectangle:
            tile.SetDisplayToActive()

    def Display(self):
        width = self.mymap.width
        height = self.mymap.height
        text = ''

        for y in range(0,height):
            line = ''
            for x in range(0,width):
                line += self.mymap.FindTileFromCoords((x,y)).display
            text += line + '\n'

        return text


class Tile():
    def __init__(self,coords):
        self.coords = coords
        self.SetDisplayToDormant()

    def SetDisplayToActive(self):
        self.display = ' '

    def SetDisplayToDormant(self):
        self.display = '#'


class MyMap():
    def __init__(self):
        self.tiles = dict()
        self.width = 200
        self.height = 35

        for x in range(0,self.width):
            self.tiles[str(x)] = dict()
            for y in range(0,self.height):
                self.tiles[str(x)][str(y)]= Tile((x,y))

    def FindTileFromCoords(self,coords):
        coord_x, coord_y = coords
        return self.tiles[str(coord_x)][str(coord_y)]

    def FindRectangleOfTiles(self,coords_up_left, coords_down_right):
        coords_up_left_x, coords_up_left_y = coords_up_left
        coords_down_right_x, coords_down_right_y = coords_down_right
        tiles = []

        for x in range(coords_up_left_x,coords_down_right_x):
            for y in range(coords_up_left_y,coords_down_right_y):
                tiles.append(self.FindTileFromCoords((x,y)))

        return tiles


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

If I execute this code and press and hold 'n' on keyboard (n for newbie!), the rectangle moves in nice and steady fashion. If you switch around the characters in Tile class...:

class Tile():
    def __init__(self,coords):
        self.coords = coords
        self.SetDisplayToDormant()

    def SetDisplayToActive(self):
        self.display = '#'

    def SetDisplayToDormant(self):
        self.display = ' '

... and hold 'n' again, the StaticText label won't update until you release the key.

Could someone explain to me why that is? And is there some smart way to circumvent that problem?


Solution

  • 1. Switch whitespace to no-break whitespace.

    As VZ. mentioned, wxStaticText aren't really meant to be used to update big blobs of text. However, he also mentioned that the word wrapping in wxStaticText is a probable culprit for this stuttery behaviour. This got me thinking, if that's word wrapping that's causing it, how may I circumvent it? Well, it turns out that switching a standard whitespace character (U+0020) to a no-break space (U+00A0) indeed makes the widget updates much more smooth! In my reproducible code example, it's especially trivial, since Python 3+ makes all strings utf-8 encoded:

    class Tile():
        def __init__(self,coords):
            self.coords = coords
            self.SetDisplayToDormant()
    
        def SetDisplayToActive(self):
            self.display = '#'
    
        def SetDisplayToDormant(self):
            self.display = '\u00a0'
    

    2. Use GenStaticText instead.

    It turns out using wx.lib.stattext.GenStaticText instead of wxStaticText also seems to correct the issue and is probably also way more sensible.