More accurate positioning and alignment of bitmaps in a button

I can create a Button with an image in it, no problem. However, there doesn’t seem to be a way to affect the positioning of that image very much; I can put the image left/right/above/below any text, but that’s it. There is a SetBitmapMargins, but this is only available on Windows (and it wouldn’t help much because the margin size would depend on the text length). In particular, I’d like to somehow justify or separate image and text so that a bunch of buttons with images have the images and text line up.

That is: currently buttons with text and images do this:


(note that the images don’t line up, and there’s no gap between the images and the text)

and I would like them to do something more like this:


I can’t work out how to do this. I don’t think I’m allowed to put a BoxSizer in a Button and then manage things that way, as far as I can tell? So given that… how can I have buttons with images and which line up vertically?

One possible way to do this would be to use a modified version of the GenBitmapTextButton class.

The GenBitmapTextButton class can display a bitmap and text. Its DrawLabel() method produces a similar layout to the standard wx.Button with the bitmap and text centred in the button. However, it’s possible to create a derived class and override its DrawLabel() method to position the bitmap and text where you want them.

In the following example I just set the x-coord of the bitmap to 20+dx and the x-coord of the text to 80+dx. In this simple layout, it doesn’t matter if the bitmaps are different sizes, the text will still line up.

import wx
import wx.lib.buttons as buttons

class CustomGenBitmapTextButton(buttons.GenBitmapTextButton):

    def DrawLabel(self, dc, width, height, dx=0, dy=0):
        bmp = self.bmpLabel
        if bmp is not None:     # if the bitmap is used
            if self.bmpDisabled and not self.IsEnabled():
                bmp = self.bmpDisabled
            if self.bmpFocus and self.hasFocus:
                bmp = self.bmpFocus
            if self.bmpSelected and not self.up:
                bmp = self.bmpSelected
            bw,bh = bmp.GetWidth(), bmp.GetHeight()
            if not self.up:
                dx = dy = self.labelDelta
            hasMask = bmp.GetMask() is not None
            bw = bh = 0     # no bitmap -> size is zero
            hasMask = False

        if self.IsEnabled():

        label = self.GetLabel()
        tw, th = dc.GetTextExtent(label)        # size of text
        if not self.up:
            dx = dy = self.labelDelta

        # pos_x = (width-bw-tw)//2+dx      # adjust for bitmap and text to centre
        pos_x = 20+dx      # Fix bitmap x-coord
        if bmp is not None:
            dc.DrawBitmap(bmp, pos_x, (height-bh)//2+dy, hasMask) # draw bitmap if available
            #pos_x = pos_x + 2   # extra spacing from bitmap

        # dc.DrawText(label, pos_x + dx+bw, (height-th)//2+dy)      # draw the text
        pos_x = 80+dx      # Fix text x-coord
        dc.DrawText(label, pos_x, (height-th)//2+dy)      # draw the text

class TestFrame(wx.Frame):
    def __init__(self):

        self.SetSize(300, 300)

        sizer = wx.BoxSizer(wx.VERTICAL)

        b1 = CustomGenBitmapTextButton(self, -1, None, "Go Home", size = (200, 45))
        bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_HOME, wx.ART_TOOLBAR, (32, 32))
        mask = wx.Mask(bmp, wx.BLUE)

        b2 = CustomGenBitmapTextButton(self, -1, None, "Print Document", size = (200, 45))
        bmp = wx.ArtProvider.GetBitmap(wx.ART_PRINT, wx.ART_TOOLBAR, (24, 24))
        mask = wx.Mask(bmp, wx.BLUE)


app = wx.App(False)
frame = TestFrame()

Screenshot at 2023-02-06 20-59-50