-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add initial wxpython window support #133
Conversation
Disclaimer: I'm not a wxPython expert, I just searched for some code and documentation that may be of use to you. Here's what I found: timerThe timer in the Qt backend is a feature that allows users to limit the FPS independently from the Qt window's redraw rate. I don't think this is technically required to support wxPython but I can see the value of the feature: if you leave the FPS uncapped, your CPU will constantly be pushing 100% utilization to render as many frames as it possibly can. :) Looks like there is a similar construct in wxPython that you could use: https://docs.wxpython.org/wx.Timer.html get_display_idThis is used only on Linux, and currently we (try to) support X11 and Wayland as window managers. Wgpu requires us to pass a display ID in order to make it work. Maybe you can use the Display class: https://docs.wxpython.org/wx.Display.html Also, I found this in the wxPython Phoenix changelog for version 2.6.0.1: Maybe you can open an issue and see if the wxPython maintainers can point you in the right direction? exampleWould you mind also adding a small |
Some extra notes: You can omit the timer in the first attempt, and add fps limiting later, or in a second PR. It's not actually necessary to implement
|
@Korijn, thanks for the notes.
Yes, this is why I was wondering if it would be necessary to add the wx.Timer or not. :)
I did take a look into that, but I wasn't sure if that would be the correct id. The QT function seemed to be getting the display id, so the terminology, etc kinda made me wonder if it was the right one in wxpython.
I did do this, but I suspect your probably correct about the id.
I think I could do that. Just follow https://github.com/pygfx/wgpu-py#developers to build from source, correct?
o.k.
Yes, I seen that too. ;)
Oh, do you mean it needs to close the whole application window or just the widget? Since I've implemented a widget (strangely called Thanks! |
Good question. The close mechanism may need some thought / documentation. I think the |
So, then hiding them like this should be o.k? |
Yes, this is fine! I think that in order to merge your PR all that is needed is for you to remove the overloaded get_display_id method. A timer and an example can be added in subsequent PRs. Nice job! |
Thanks! Will do. |
Should be fixed. Let me know if anything else is needed for now. |
I just tried running it with wxPython 4.1.1, and run into the following:
Here's the """
Import the viz from triangle.py and run it in a wxPython window.
"""
import wx
from wgpu.gui.wxpython import WxWgpuCanvas
import wgpu.backends.rs # noqa: F401, Select Rust backend
# Import the (async) function that we must call to run the visualization
from triangle import main
app = wx.App()
frm = WxWgpuCanvas()
frm.Show()
main(frm)
app.MainLoop() Any idea what might be wrong? I looked at the docs for I'd like to see this in a working state/know how to use it at least once before we merge. :) |
Hi @Korijn,
Yes, you're right. It should be Maybe we should change the wxpython.py class WxWgpuCanvas(WgpuCanvasBase, wx.Window):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def get_window_id(self):
return int(self.GetId())
def get_pixel_ratio(self):
return self.GetDPIScaleFactor()
def get_logical_size(self):
lsize = self.Size[0], self.Size[1]
return float(lsize[0]), float(lsize[1])
def get_physical_size(self):
lsize = self.Size[0], self.Size[1]
lsize = float(lsize[0]), float(lsize[1])
ratio = self.GetDPIScaleFactor()
return round(lsize[0] * ratio), round(lsize[1] * ratio)
def set_logical_size(self, width, height):
if width < 0 or height < 0:
raise ValueError("Window width and height must not be negative")
self.SetSize(width, height)
def _request_draw(self):
pass
def close(self):
self.Hide()
def is_closed(self):
return not self.IsShown() Also, I think the demo script is going to need a """
Import the viz from triangle.py and run it in a wxPython window.
"""
import wx
from wgpu.gui.wxpython import WxWgpuCanvas
import wgpu.backends.rs # noqa: F401, Select Rust backend
# Import the (async) function that we must call to run the visualization
from triangle import main
class AppFrame(wx.Frame):
def __init__(self, parent, id=wx.ID_ANY, title="WxWgpuCanvas Demo"):
wx.Frame.__init__(self, parent, id, title, pos, size, style, name)
canvas= WxWgpuCanvas(self)
self.Bind(wx.EVT_CLOSE, self.OnDestroy)
def OnDestroy(self, event):
self.Destroy()
app = wx.App()
frm = AppFrame()
frm.Show()
main(frm)
app.MainLoop() Thanks! |
After making that change we get further:
@almarklein Any ideas? |
Not sure if it's the problem, but I just realized it should use args and kwargs, like: """
Import the viz from triangle.py and run it in a wxPython window.
"""
import wx
from wgpu.gui.wxpython import WxWgpuCanvas
import wgpu.backends.rs # noqa: F401, Select Rust backend
# Import the (async) function that we must call to run the visualization
from triangle import main
class AppFrame(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
canvas= WxWgpuCanvas(self)
self.Bind(wx.EVT_CLOSE, self.OnDestroy)
def OnDestroy(self, event):
self.Destroy()
app = wx.App()
frm = AppFrame()
frm.Show()
main(frm)
app.MainLoop() The window itself should now be working correctly, at least. |
I think some users will want to use it as a widget, but making it easy to do a quick example is also nice. Maybe we should include both?
Mmm, that smells like the surface id not being matched or something. |
If we still need to implement the |
I've looked into this a bit and I don't see any custom draw event code here (in this PR). For the Qt canvas widget we had to disable the standard draw event, so that wgpu-py could draw instead. Is there a similar thing in wxPython? I noticed they have a GLCanvas subclass of Window for example? |
@Korijn, thanks for looking into this. Yes, that is probably what needs to be done. I wasn't sure about the drawing part, but wxpython does have ways to draw in a similar fashion, etc. However, I can't really tell what is needed from the qt.py as I've never used qt before and not sure I understand what is being done with the drawing in the qt example. Is the qt I am familiar with drawing in wxpython so maybe if you could tell me what is needed (tell me what is going on as far as the drawing in the qt demo), I could try to get this implemented. The main thing I don't understand is how the wgpu can draw to the qt window via If so, it may be something like: class WxWgpuCanvas(WgpuCanvasBase, wx.Window):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)
def on_paint(self, event):
self._draw_frame_and_present()
self.Refresh()
self.Update() but, as I said, I'm not sure what exactly is supposed to happen in order for wgpu to draw, etc. Thanks. |
Well, if you look at WgpuSubWidget in the Qt module you'll see that it does four things:
Together this ensures Qt leaves the drawing entirely up to wgpu, if I understand correctly. |
You can also have a look at the wx backend for Vispy: https://github.com/vispy/vispy/blob/master/vispy/app/backends/_wx.py#L288-L297 |
Ah, okay. :) Thanks, I will look into this. |
Hmmm....I am not sure. Is the problem the lack of a timer to control the frame-rate?
triangle_wxpython.py class AppFrame(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.canvas = WxWgpuCanvas(self)
self.Bind(wx.EVT_CLOSE, self.OnDestroy)
def OnDestroy(self, event):
self.Destroy()
app = wx.App()
frm = AppFrame()
frm.Show()
main(frm.canvas) # this was needed to access the canvas instead of the wx.frame
app.MainLoop() wxpython.py class WxWgpuCanvas(wx.Window, WgpuCanvasBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)
def on_paint(self, event):
dc = wx.PaintDC(self) # needed for wx
self._draw_frame_and_present()
del dc
event.Skip()
def get_window_id(self):
return int(self.GetId())
def get_pixel_ratio(self):
return 1 #self.GetDPIScaleFactor()
def get_logical_size(self):
lsize = self.Size[0], self.Size[1]
return float(lsize[0]), float(lsize[1])
def get_physical_size(self):
lsize = self.Size[0], self.Size[1]
lsize = float(lsize[0]), float(lsize[1])
ratio = 1 #self.GetDPIScaleFactor()
return round(lsize[0] * ratio), round(lsize[1] * ratio)
def set_logical_size(self, width, height):
if width < 0 or height < 0:
raise ValueError("Window width and height must not be negative")
self.SetSize(width, height)
def _request_draw(self):
pass
# dc = wx.PaintDC(self) # needed for wx
# self._draw_frame_and_present()
# del dc
def close(self):
self.Hide()
def is_closed(self):
return not self.IsShown() Note: I just set the dpi factor to 1 because it seemed to not be able to access the method, strangely. Whether I used |
It somewhat looks like it cannot get the server because it broke earlier due to running out of memory. Can you see whether there are any draws before the error occurs? Perhaps there is a memory leak somehow. I remember such leaks occurring when resizing a window a lot (not sure if this was an error in pygfx/wgpu-py/wgpu-native), so it looked like framebuffers not being cleaned up. I don't think the timer thing would help prevent this error. |
Sorry for my tardy reply. :) So, I re-installed everything according to the README (into a virtualenv) as I suspect I had installed things improperly the first time and ran the class AppFrame(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.canvas = WxWgpuCanvas(self)
self.Bind(wx.EVT_CLOSE, self.OnDestroy)
def OnDestroy(self, event):
self.Destroy()
app = wx.App()
frm = AppFrame(parent=None, title="wgpu triangle with wxPython")
frm.Show()
main(frm.canvas)
app.MainLoop()
class WxWgpuCanvas(wx.Window, WgpuCanvasBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)
def on_paint(self, event):
#print("paint")
dc = wx.PaintDC(self) # needed for wx
self._draw_frame_and_present()
del dc
#event.Skip()
def get_window_id(self):
return int(self.GetId())
def get_pixel_ratio(self):
return 1 #self.GetDPIScaleFactor()
def get_logical_size(self):
lsize = self.Size[0], self.Size[1]
return float(lsize[0]), float(lsize[1])
def get_physical_size(self):
lsize = self.Size[0], self.Size[1]
lsize = float(lsize[0]), float(lsize[1])
ratio = 1 #self.GetDPIScaleFactor()
return round(lsize[0] * ratio), round(lsize[1] * ratio)
def set_logical_size(self, width, height):
if width < 0 or height < 0:
raise ValueError("Window width and height must not be negative")
self.SetSize(width, height)
def _request_draw(self):
pass
def close(self):
self.Hide()
def is_closed(self):
return not self.IsShown() The widow comes up with a white square in the corner, then it crashes with this error (I guess this is progress!):
It seems to be similar to the one @Korijn had. Is the EDIT: it doesn't seem that the |
It's an environment variable that can be set to let Rust (wgpu-native is written in Rust) produce more info. But that maybe only when you're using the debug version of the lib.
I think that it lost the surface because its 'Unable to query surface capabilities', which might imply that the window id is not valid, perhaps? |
I gave it a try, and I managed to get it working by replacing With that, it works on Windows. Not sure if any casting is required on Mac and Linux. |
I confirmed that it works on Windows as well. I will update the PR then, @almarklein? |
Yes, please! |
So, this all seems to be working -which is great. However, it seems that when I change the dpi to
|
Did you also update it in |
Can you add |
Nope. Same error each time. For me, the
Sure. Haven't gotten there yet. :) |
This is the complete source I have currently: """
Support for rendering in a wxPython window. Provides a widget that
can be used as a standalone window or in a larger GUI.
"""
import sys
import time
import ctypes
import importlib
from .base import WgpuCanvasBase
import wx
try:
# fix blurry text on windows
ctypes.windll.shcore.SetProcessDpiAwareness(True)
except Exception:
pass # fail on non-windows
class WxWgpuCanvas(wx.Window, WgpuCanvasBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)
def on_paint(self, event):
dc = wx.PaintDC(self) # needed for wx
self._draw_frame_and_present()
del dc
event.Skip()
def get_window_id(self):
return int(self.GetHandle())
def get_pixel_ratio(self):
return self.GetDPIScaleFactor()
def get_logical_size(self):
lsize = self.Size[0], self.Size[1]
return float(lsize[0]), float(lsize[1])
def get_physical_size(self):
lsize = self.Size[0], self.Size[1]
lsize = float(lsize[0]), float(lsize[1])
ratio = self.GetDPIScaleFactor()
return round(lsize[0] * ratio), round(lsize[1] * ratio)
def set_logical_size(self, width, height):
if width < 0 or height < 0:
raise ValueError("Window width and height must not be negative")
self.SetSize(width, height)
def _request_draw(self):
pass
def close(self):
self.Hide()
def is_closed(self):
return not self.IsShown() """
Import the viz from triangle.py and run it in a wxPython window.
"""
import wx
from wgpu.gui.wxpython import WxWgpuCanvas
import wgpu.backends.rs # noqa: F401, Select Rust backend
# Import the (async) function that we must call to run the visualization
from examples.triangle import main
class AppFrame(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.canvas = WxWgpuCanvas(self)
self.Bind(wx.EVT_CLOSE, self.OnDestroy)
def OnDestroy(self, event):
self.Destroy()
app = wx.App()
frm = AppFrame(parent=None, title="wgpu triangle with wxPython")
frm.Show()
main(frm.canvas)
app.MainLoop() |
Is it possible that wx's self.size property is already multiplied by the scale factor? There is also
There's also
Maybe they give different results? |
Maybe.
Also, it should be noted that because we're accessing the canvas ( Overall, this is probably ready to merge, correct? -since this is "add initial wxpython window support". (I mean, after I commit the changes, of course) |
I think so! 🎉 Great work! |
Nice! Thanks @Correct-Syntax ! |
Uh-oh, I guess I now realize I probably committed to the repository instead of my fork (between switching computers, I lost track!). Sorry. Thanks for taking care of that @Korijn! |
Obviously this is not fully implemented, but It's a start.
I had some questions about the usage of the qt timer and how that should be translated into wxpython. I am also having a hard time figuring out how get a relevant value for
get_display_id
. Not sure if other methods are needed as well.