Buttons on top of GLCanvas

I am trying to get wxPython buttons and other objects to display on top of a GLCanvas. I have managed to achieve what I was trying to do but I don’t know if how I am doing it is exploiting a bug that could get fixed or if it is a know feature. I just wanted to enquire to see what other users thought or if there is an official way of doing what I am trying to do.

In summary I want a GLCanvas with wx buttons and other things on top of the GLCanvas.
Currently how I am doing this is creating all the wx objects and then creating the GLCanvas. The wx objects are put in their locations using sizers but the canvas is not put inside a sizer. The canvas is still there and intractable with the buttons on top and intractable.

It feels like this is exploiting a bug with object draw order but it does work (at least on my computer).

The code is part of a larger program but the specific code in question is here https://github.com/Amulet-Team/Amulet-Map-Editor/blob/cbab7f74e7d026777673900404c6b14bab4ae699/amulet_map_editor/plugins/programs/edit/edit.py#L177

Have you tried making the panels holding the buttons to be children of the GLCanvas? That would be “more proper” but I don’t know how the GL context and canvas might interact with child widgets.

Thanks. That seems to have worked. I wasn’t aware the GLCanvas could have things put inside it.
The docs only show the methods it implements and not the ones from its parent classes. Same with PyCharm.

Following up to this. We have had this working on windows for a while but I can’t get the same thing working on a mac. I think the issue exists in linux as well.

The issue seems to be that on mac the canvas is drawn over all the buttons but on windows it is drawn under them as I would expect.

I have written up a simplified example to reproduce this that has a glcanvas with just a solid colour which is randomly picked each draw. There should be a button in the top left but on mac it is not visible but is still interactable. It works as expected on windows.

Is there some way to solve this my end or is it a wxpython/wxwidgets bug?

#!/usr/bin/env python

import wx
from wx import glcanvas
from OpenGL.GL import *
import random


class MyCanvas(glcanvas.GLCanvas):
    def __init__(self, parent):
        glcanvas.GLCanvas.__init__(self, parent, -1)
        self.context = glcanvas.GLContext(self)

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

        sizer = wx.BoxSizer()
        self.SetSizer(sizer)
        self._button = wx.Button(self, label="hello")
        self._button.Bind(wx.EVT_BUTTON, lambda evt: print("hello"))
        sizer.Add(self._button)

    def OnEraseBackground(self, event):
        pass  # Do nothing, to avoid flashing on MSW.

    def OnSize(self, event):
        wx.CallAfter(self.DoSetViewport)
        event.Skip()

    def DoSetViewport(self):
        size = self.size = self.GetClientSize() * self.GetContentScaleFactor()
        self.SetCurrent(self.context)
        glViewport(0, 0, size.width, size.height)

    def OnPaint(self, event):
        dc = wx.PaintDC(self)
        self.SetCurrent(self.context)
        self.OnDraw()

    def OnDraw(self):
        glClearColor(random.randrange(256)/255, random.randrange(256)/255, random.randrange(256)/255, 1.0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        self.SwapBuffers()
        # self._button.Refresh()  # these don't have any effect
        # self._button.Update()


def main():
    app = wx.App()
    frame = wx.Frame(None)
    sizer = wx.BoxSizer()
    frame.SetSizer(sizer)
    canvas = MyCanvas(frame)
    sizer.Add(canvas, 1, wx.EXPAND)
    frame.Show()
    app.MainLoop()
    

if __name__ == '__main__':
    main()

Doing some googling shows that this is an issue with other toolkits as well. I expect that it is simply a matter of OpenGL using a native UI layer or viewport that is above the one used for placing the widgets upon. OSX does things like this in a few other situations too. I seem to recall that there is a GL flag that can be used to control this, but I don’t remember details.

Another option turned up by google is to use some GL based widgets in your GLCanvas window instead of native wx widgets. (IOW, widgets that are drawn by Open GL itself.) This one seems to have Python bindings, but it appears that the PyPI package with the same name is something else, so you’ll probably need to build it yourself.

It does seem like it is due to the OpenGL canvas drawing over the top of the UI.

I did some more digging but couldn’t not find a way to make the canvas display behind the UI but I did find a bit of a workaround.
This post mentions that mixing opengl and wx is not advised but mentioned a workaround to make it work. https://forums.wxwidgets.org/viewtopic.php?t=45552
The penultimate question reads:

Q. Can I mix OGL and wx controls?
A. Normally, not. The OS and the GPU will fight for the window.
But you can render to a not-shown buffer (GL_BACK, or better a FBO ) without SwapBuffers, then read the picture by glReadPixels, and set it as a background of the window.

I have implemented this in wxpython and it mostly works on the mac but I am having issues with it not updating.
Each frame instead of calling canvas.SwapBuffers() I am calling canvas.Refresh() which triggers the paint event. The paint function looks like this. It gets the image from the renderer and sets it as the background image.

def _on_paint(self, evt):
	if sys.platform != "win32":
		dc = wx.PaintDC(self)
		size = self.GetClientSize()
		width = size.width * self.GetContentScaleFactor()
		height = size.height * self.GetContentScaleFactor()
		img = glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE)
		btmap = wx.Image(width, height, img).Scale(size.width, size.height).Mirror(False).ConvertToBitmap()
		dc.DrawBitmap(btmap, 0, 0)

All of this works as expected. The rendered image is displayed behind the UI and the canvas can be interacted with.
The issue is that the image only changes when the window is moved or resized or programs are switched between. Interestingly after doing this it will draw the next hadful of frames correctly and then go back into its frozen state. The same also applies to the UI itself.

A few more things to mention. If a dialog is opened and moved around the screen the section around it will update correctly. If instead of calling Refresh you call Hide and then Show it animates correctly but as expected you can’t interact with the canvas for more than a frame.

TLDR: I have mostly gotten what I want working. The rendered image is behind the UI but Refresh is not updating the screen. It requires a forced redraw by tabbing between programs to get the display to update.

This may be a shot in the dark, but the next thing I would try is calling Refresh from a timer.

Yes if Refresh() is called after SwapBuffers() then wx widgets are displayed on top of GLCanvas