wxPython draw filled rectangle

Is there another way to draw a wxPython filled rectangle that isn’t binded to an event (evt_paint)? I just want to draw a rectangle on a panel directly (no event binding).

Thank you…

The issue is that the panel wouldn’t know how to redraw the rectangle if it was covered and then exposed.

One simple solution would be to use a wxStaticBitmap that has been initialised with a wxBitmap of the required dimensions and colour.

Example:

import wx

def getFilledRectBitmap(width, height, colour):
    r, g, b, a = colour.Get(includeAlpha=True)
    bitmap = wx.Bitmap.FromRGBA(width, height, r, g, b, a)
    return bitmap


class MyFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Filled Rectangle on Panel", size=(300, 300))
        panel = wx.Panel(self)
        x = 60
        y = 70
        width = 30
        height = 20
        colour = wx.Colour(80, 180, 40)
        bitmap = getFilledRectBitmap(width, height, colour)
        self.rect_bitmap = wx.StaticBitmap(panel, wx.ID_ANY, bitmap, (x, y), (width, height))


if __name__ == "__main__":
    app = wx.App()
    frame = MyFrame().Show()
    app.MainLoop()

This works on Python 3.8.5 + wxPython 4.1.1 gtk3 (phoenix) wxWidgets 3.1.5 + Linux Mint 20.1

The issue is that the panel wouldn’t know how to redraw the rectangle if it was covered and then exposed.
I see. Let me think about that.

What I’m trying to do is paint a small rectangle onto a panel that is on the right side of my styledTextCtrl. This panel looks very much like a vertical scroll bar but it’s not. I’m using it to paint a small rectangle approximately where a found word is located within the whole text. So if I’m searching for the word ‘import’ and it finds it about 50% of the way into the file, the rectangle would be painted about 50% of the way down the panel. I have all of this working fine, I just need to paint it now.

I’m lost understanding WHEN the Bind event paints the rectangle? I feel like I don’t have control over WHEN that is painted? The panel is created when the styledTextCtrl is created. So the panel sits there, but I might not be searching for words. When I do search, THEN I want to paint the rectangles for the found words. So when do I attach the bind event?

The rectangles look something like this:

UPDATE: I got it working using wxStaticBitmap – thank you very much for the help.

An alternative approach would be to create a custom class from wx.Control

import wx
import wx.stc as stc

class IndicatorBar(wx.Control):
    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
                 size=wx.DefaultSize, style=wx.NO_BORDER,
                 validator=wx.DefaultValidator, name="indicator_bar"):
        wx.Control.__init__(self, parent, id, pos, size, style, validator, name)

        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
        self.Bind(wx.EVT_PAINT,            self.OnPaint)

        self.mark_pen = wx.Pen(wx.GREEN)
        self.mark_brush = wx.Brush(wx.GREEN)
        self.marks = []


    def Draw(self, dc):
        # Clear the control using its background colour
        bg_brush = wx.Brush(wx.LIGHT_GREY, wx.BRUSHSTYLE_SOLID)
        dc.SetBackground(bg_brush)
        dc.Clear()

        dc.SetPen(self.mark_pen)
        dc.SetBrush(self.mark_brush)

        x = 5
        mark_width = 10
        control_height = self.GetSize()[1]

        for y1, y2 in self.marks:
            mark_y = int(y1 * control_height)
            mark_height = int((y2 - y1) * control_height)
            dc.DrawRectangle(x, mark_y, mark_width, mark_height)


    def setMarks(self, marks):
        self.marks = marks
        self.Refresh()
        self.Update()


    def OnEraseBackground(self, event):
        """ Handle a wx.EVT_ERASE_BACKGROUND event. """

        # This is intentionally empty, because we are using the combination
        # of wx.BufferedPaintDC + an empty OnEraseBackground event to
        # reduce flicker
        pass


    def OnPaint(self, _event):
        """ Handle a wx.EVT_PAINT event. """

        # Use wx.BufferedPaintDC to reduce flicker
        dc = wx.BufferedPaintDC(self)
        self.Draw(dc)



class MyFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Indicator Bar Test", size=(300, 300))

        self.stc = stc.StyledTextCtrl(self, wx.ID_ANY)
        self.ind_bar = IndicatorBar(self, wx.ID_ANY, size=(30, 50))

        top_sizer = wx.BoxSizer(wx.HORIZONTAL)
        top_sizer.Add(self.stc, 1, wx.EXPAND, 0)
        top_sizer.Add(self.ind_bar, 0, wx.EXPAND, 0)

        button = wx.Button(self, wx.ID_ANY, "Search")

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(top_sizer, 1, wx.EXPAND, 0)
        sizer.Add(button, 0, wx.TOP, 4)

        self.Bind(wx.EVT_BUTTON, self.OnSearch, button)

        self.SetSizer(sizer)
        self.Layout()

    def OnSearch(self, event):
        # Sequence of y-coordinates as proportions of the control's height
        marks = ((.2, .24), (.30, .31))
        self.ind_bar.setMarks(marks)


if __name__ == "__main__":
    app = wx.App()
    frame = MyFrame().Show()
    app.MainLoop()

In this crude example the IndicatorBar class contains the instance attribute self.marks which is initially an empty list. The Draw() method will draw the marks on the IndicatorBar. When the list is empty, no marks are drawn.

When you click the Search button, a sequence of floats will be passed to the IndicatorBar’s setMarks() method. That method assigns the sequence to self.marks and triggers the Draw() method to be called, which then scales and draws the rectangles.

Using this approach, the Bind() calls are made when the custom control is created and you can update the position of the marks by calling the setMarks() method when necessary.

Sorry, it’s a bit rough (for example it doesn’t allow for the horizontal scroll bar), but it may give you some ideas.

Wow – thank you so much. I’ll look it over.