|
| 1 | +# python imports |
| 2 | +import math |
| 3 | +import ctypes |
| 4 | +# pip imports |
| 5 | +import wx |
| 6 | +from wx import glcanvas |
| 7 | +import skia |
| 8 | +import moderngl |
| 9 | + |
| 10 | +GL_RGBA8 = 0x8058 |
| 11 | + |
| 12 | + |
| 13 | +"""Enable high-res displays.""" |
| 14 | +try: |
| 15 | + ctypes.windll.shcore.SetProcessDpiAwareness(1) |
| 16 | + ctypes.windll.shcore.SetProcessDpiAwareness(2) |
| 17 | +except Exception: |
| 18 | + pass # fail on non-windows |
| 19 | + |
| 20 | + |
| 21 | +class SkiaWxGPUCanvas(glcanvas.GLCanvas): |
| 22 | + """A skia based GlCanvas""" |
| 23 | + |
| 24 | + def __init__(self, parent, size): |
| 25 | + glcanvas.GLCanvas.__init__(self, parent, -1, size=size) |
| 26 | + self.glctx = glcanvas.GLContext(self) # ✅ Correct GLContext |
| 27 | + self.size = size |
| 28 | + self.ctx = None |
| 29 | + self.canvas = None |
| 30 | + self.surface = None |
| 31 | + self.is_dragging = False |
| 32 | + self.last_mouse_pos = (0.0, 0.0) |
| 33 | + self.offset_x = 0.0 |
| 34 | + self.offset_y = 0.0 |
| 35 | + self.zoom = 1.0 |
| 36 | + |
| 37 | + self.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_left_down) |
| 38 | + self.Bind(wx.EVT_LEFT_UP, self.on_mouse_left_up) |
| 39 | + self.Bind(wx.EVT_MOTION, self.on_mouse_move) |
| 40 | + self.Bind(wx.EVT_MOUSEWHEEL, self.on_mouse_wheel) |
| 41 | + self.Bind(wx.EVT_PAINT, self.on_paint) |
| 42 | + self.Bind(wx.EVT_SIZE, self.on_size) |
| 43 | + # Do nothing, to avoid flashing on MSW. |
| 44 | + self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) |
| 45 | + |
| 46 | + def init_gl(self): |
| 47 | + """Initialize Skia GPU context and surface.""" |
| 48 | + if self.ctx is None: |
| 49 | + self.ctx = moderngl.create_context() |
| 50 | + context = skia.GrDirectContext.MakeGL() |
| 51 | + backend_render_target = skia.GrBackendRenderTarget( |
| 52 | + self.size[0], self.size[1], 0, 0, skia.GrGLFramebufferInfo( |
| 53 | + 0, GL_RGBA8) |
| 54 | + ) |
| 55 | + self.surface = skia.Surface.MakeFromBackendRenderTarget( |
| 56 | + context, backend_render_target, skia.kBottomLeft_GrSurfaceOrigin, |
| 57 | + skia.kRGBA_8888_ColorType, skia.ColorSpace.MakeSRGB() |
| 58 | + ) |
| 59 | + self.canvas = self.surface.getCanvas() |
| 60 | + |
| 61 | + def on_paint(self, event): |
| 62 | + """Handle drawing.""" |
| 63 | + |
| 64 | + self.SetCurrent(self.glctx) |
| 65 | + |
| 66 | + if self.canvas is None: |
| 67 | + self.init_gl() |
| 68 | + |
| 69 | + # This is your actual skia based drawing function |
| 70 | + self.on_draw() |
| 71 | + |
| 72 | + self.SwapBuffers() |
| 73 | + |
| 74 | + def on_mouse_left_down(self, event): |
| 75 | + self.is_dragging = True |
| 76 | + self.last_mouse_pos = (event.GetX(), event.GetY()) |
| 77 | + event.Skip() |
| 78 | + |
| 79 | + def on_mouse_left_up(self, event): |
| 80 | + self.is_dragging = False |
| 81 | + event.Skip() |
| 82 | + |
| 83 | + def on_mouse_move(self, event): |
| 84 | + if self.is_dragging: |
| 85 | + xpos, ypos = event.GetX(), event.GetY() |
| 86 | + lx, ly = self.last_mouse_pos |
| 87 | + dx, dy = xpos - lx, ypos - ly |
| 88 | + self.offset_x += dx / self.zoom |
| 89 | + self.offset_y += dy / self.zoom |
| 90 | + self.last_mouse_pos = (xpos, ypos) |
| 91 | + self.Refresh() |
| 92 | + event.Skip() |
| 93 | + |
| 94 | + def on_mouse_wheel(self, event): |
| 95 | + """Implement zoom to point""" |
| 96 | + x, y = event.GetX(), event.GetY() |
| 97 | + rotation = event.GetWheelRotation() |
| 98 | + |
| 99 | + if rotation > 1: |
| 100 | + zoom_factor = 1.1 |
| 101 | + elif rotation < -1: |
| 102 | + zoom_factor = 0.9 |
| 103 | + else: |
| 104 | + return |
| 105 | + |
| 106 | + old_zoom = self.zoom |
| 107 | + new_zoom = self.zoom * zoom_factor |
| 108 | + |
| 109 | + # Convert screen coords to centered canvas coords |
| 110 | + cx, cy = self.size[0] / 2, self.size[1] / 2 |
| 111 | + dx = x - cx |
| 112 | + dy = y - cy |
| 113 | + |
| 114 | + # World coords under mouse before zoom |
| 115 | + world_x = (dx / old_zoom) - self.offset_x |
| 116 | + world_y = (dy / old_zoom) - self.offset_y |
| 117 | + |
| 118 | + # Apply zoom |
| 119 | + self.zoom = new_zoom |
| 120 | + |
| 121 | + # Adjust offset so world point stays under cursor |
| 122 | + self.offset_x = (dx / self.zoom) - world_x |
| 123 | + self.offset_y = (dy / self.zoom) - world_y |
| 124 | + |
| 125 | + self.Refresh() |
| 126 | + |
| 127 | + def on_draw(self): |
| 128 | + """Draw on Skia canvas.""" |
| 129 | + w, h = self.GetSize() |
| 130 | + if w == 0 or h == 0: |
| 131 | + return |
| 132 | + self.ctx.viewport = (0, 0, self.size.width, self.size.height) |
| 133 | + |
| 134 | + self.canvas.clear(skia.ColorWHITE) |
| 135 | + self.canvas.save() |
| 136 | + |
| 137 | + # Set up the translation and zoom (pan/zoom) |
| 138 | + self.canvas.translate(w / 2, h / 2) |
| 139 | + self.canvas.scale(self.zoom, self.zoom) |
| 140 | + self.canvas.translate(self.offset_x, self.offset_y) |
| 141 | + |
| 142 | + # Create a solid paint (Blue color, but explicitly defining RGBA) |
| 143 | + paint = skia.Paint(AntiAlias=True, Color=skia.Color(255, 0, 0),) |
| 144 | + |
| 145 | + # Draw a series of circles in a spiral pattern |
| 146 | + for i in range(150): |
| 147 | + angle = i * math.pi * 0.1 |
| 148 | + x = math.cos(angle) * i * 3 |
| 149 | + y = math.sin(angle) * i * 3 |
| 150 | + # Draw solid circles |
| 151 | + self.canvas.drawCircle(x, y, 4 + (i % 4), paint) |
| 152 | + |
| 153 | + self.canvas.restore() |
| 154 | + self.surface.flushAndSubmit() |
| 155 | + |
| 156 | + def on_size(self, event): |
| 157 | + """Handle resizing of the canvas.""" |
| 158 | + wx.CallAfter(self.set_viewport) |
| 159 | + event.Skip() |
| 160 | + |
| 161 | + def set_viewport(self): |
| 162 | + # Actual drawing area (without borders) is GetClientSize |
| 163 | + size = self.GetClientSize() |
| 164 | + # On HiDPI screens, this could be 1.25, 1.5, 2.0, etc. |
| 165 | + scale = self.GetContentScaleFactor() |
| 166 | + width = int(size.width * scale) |
| 167 | + height = int(size.height * scale) |
| 168 | + self.size = wx.Size(width, height) |
| 169 | + if self.ctx: |
| 170 | + self.SetCurrent(self.glctx) |
| 171 | + self.ctx.viewport = (0, 0, width, height) |
| 172 | + self.init_gl() |
| 173 | + |
| 174 | + |
| 175 | +class MainFrame(wx.Frame): |
| 176 | + def __init__(self): |
| 177 | + super().__init__(None, title="Skia WxPython GPU Canvas", size=(800, 600)) |
| 178 | + panel = wx.Panel(self) |
| 179 | + sizer = wx.BoxSizer(wx.VERTICAL) |
| 180 | + self.canvas = SkiaWxGPUCanvas(panel, (800, 600)) |
| 181 | + sizer.Add(self.canvas, 1, wx.EXPAND) |
| 182 | + panel.SetSizer(sizer) |
| 183 | + self.Show() |
| 184 | + |
| 185 | + |
| 186 | +if __name__ == "__main__": |
| 187 | + app = wx.App(False) |
| 188 | + frame = MainFrame() |
| 189 | + frame.Show() |
| 190 | + app.MainLoop() |
0 commit comments