def __init__(self, numReps, repDuration, imagesPerRep, sliceHeight=0, metadata='', savePath=''): ## Number of images to be collected per camera per rep. self.imagesPerRep = imagesPerRep ## List of cameras. Assume our cameras are all active cameras. self.cameras = [] for cam in depot.getHandlersOfType(depot.CAMERA): if cam.getIsEnabled(): self.cameras.append(cam) ## List of light sources. Assume our lights are all active lights. self.lights = [] for light in depot.getHandlersOfType(depot.LIGHT_TOGGLE): if light.getIsEnabled(): self.lights.append(light) super().__init__(numReps, repDuration, None, 0, 0, sliceHeight, self.cameras, self.lights, {}, metadata=metadata, savePath=savePath)
def __init__(self, parent): super().__init__(parent) self.Sizer = wx.BoxSizer(wx.VERTICAL) self.Sizer.Add(PanelLabel(self, label="Lights")) sz = wx.BoxSizer(wx.HORIZONTAL) self.Sizer.Add(sz) lightToggles = sorted(depot.getHandlersOfType(depot.LIGHT_TOGGLE), key=lambda l: l.wavelength) lightPowers = depot.getHandlersOfType(depot.LIGHT_POWER) lightFilters = list( filter(lambda f: f.lights, depot.getHandlersOfType(depot.LIGHT_FILTER))) self.panels = {} for light in lightToggles: power = next( filter(lambda p: p.groupName == light.groupName, lightPowers), None) filters = list( filter(lambda f: light.name in f.lights, lightFilters)) panel = LightPanel(self, light, power, filters) sz.Add(panel, flag=wx.EXPAND) self.panels[light] = panel sz.AddSpacer(4) self.Fit()
def __init__(self): ## Maps axis to the handlers for that axis, sorted by their range of # motion. self.axisToHandlers = depot.getSortedStageMovers() ## XXX: We have a single index for all axis, even though each ## axis may have a different number of stages. While we don't ## refactor this assumption, we just make copies of the movers ## with the most precise movement (issues #413 and #415) self.n_stages = max([len(s) for s in self.axisToHandlers.values()]) for axis, stages in self.axisToHandlers.items(): stages.extend([stages[-1]] * (self.n_stages - len(stages))) ## Indicates which stage handler is currently under control. self.curHandlerIndex = 0 ## Maps Site unique IDs to Site instances. self.idToSite = {} ## Maps handler names to events indicating if those handlers # have stopped moving. self.nameToStoppedEvent = {} events.subscribe("stage mover", self.onMotion) events.subscribe("stage stopped", self.onStop) ## Device-speficic primitives to draw on the macrostage. self.primitives = set() for h in depot.getHandlersOfType(depot.STAGE_POSITIONER): ps = h.getPrimitives() if ps: self.primitives.update(ps) self.primitives.discard(None)
def __init__(self, parent): wx.Frame.__init__(self, parent, title = "Camera views", style=wx.FRAME_NO_TASKBAR | wx.CAPTION) self.numCameras = len(depot.getHandlersOfType(depot.CAMERA)) self.panel = wx.Panel(self) # Make a 2xN grid of camera canvases, with menus above for selecting # which camera to use in that location. self.sizer = wx.FlexGridSizer(2, 5, 5) ## List of ViewPanels we contain. self.views = [] for i in range(self.numCameras): view = viewPanel.ViewPanel(self.panel) self.views.append(view) self.SetPosition((675, 280)) events.subscribe(events.CAMERA_ENABLE, self.onCameraEnableEvent) events.subscribe("image pixel info", self.onImagePixelInfo) cockpit.gui.keyboard.setKeyboardHandlers(self) self.Bind(wx.EVT_CLOSE, self.onClose) self.resetGrid() self.SetDropTarget(cockpit.gui.viewFileDropTarget.ViewFileDropTarget(self))
def __init__(self, parent, configKey): super().__init__(parent=parent) self.configKey = configKey + self._CONFIG_KEY_SUFFIX self.allLights = depot.getHandlersOfType(depot.LIGHT_TOGGLE) self.settings = self.loadSettings() sizer = wx.BoxSizer(wx.VERTICAL) rowSizer = wx.BoxSizer(wx.HORIZONTAL) text = wx.StaticText(self, -1, "Exposure bleach compensation (%):") rowSizer.Add(text, 0, wx.ALL, 5) ## Ordered list of bleach compensation percentages. self.bleachCompensations, subSizer = guiUtils.makeLightsControls( self, [str(l.name) for l in self.allLights], self.settings['bleachCompensations']) rowSizer.Add(subSizer) sizer.Add(rowSizer) # Now a row for the collection order. rowSizer = wx.BoxSizer(wx.HORIZONTAL) self.siCollectionOrder = guiUtils.addLabeledInput( self, rowSizer, label="Collection order", control=wx.Choice(self, choices=sorted(COLLECTION_ORDERS.keys())), helperString= "What order to change the angle, phase, and Z step of the experiment. E.g. for \"Angle, Phase, Z\" Angle will change most slowly and Z will change fastest." ) self.siCollectionOrder.SetSelection(self.settings['siCollectionOrder']) sizer.Add(rowSizer) self.SetSizerAndFit(sizer)
def onSelector(self, event): ## TODO: fix focus issue so that key bindings work immediately after camera enable. ## Currently, have to mouse-over the bitmap area, or click in another window. menu = wx.Menu() if self.curCamera is not None: item = menu.Append(-1, "Disable %s" % self.curCamera.descriptiveName) self.Bind(wx.EVT_MENU, lambda event: self.curCamera.toggleState(), item) menu.InsertSeparator(1) items = self.canvas.getMenuActions() for label, action in items: item = menu.Append(-1, label) self.Bind(wx.EVT_MENU, lambda event, action=action: action(), item) menu.InsertSeparator(len(items) + 2) for size in self.curCamera.getImageSizes(): item = menu.Append(-1, "Set image size to %s" % str(size)) self.Bind( wx.EVT_MENU, lambda event, size=size: self.curCamera.setImageSize(size), item) else: # Get all inactive cameras. cameras = depot.getHandlersOfType(depot.CAMERA) cameras.sort(key=lambda c: c.descriptiveName) for camera in cameras: if not camera.getIsEnabled(): item = menu.Append(-1, "Enable %s" % camera.descriptiveName) self.Bind(wx.EVT_MENU, lambda event, cam=camera: cam.toggleState(), item) cockpit.gui.guiUtils.placeMenuAtMouse(self, menu)
def save(self): # First, re-order our images into WTZ order (with the Z axis being # flat). numTimes = len(self.timesAndImages) numCams = len(self.timesAndImages[0][1]) allImages = numpy.zeros((numCams, numTimes, 1, self.maxImageDims[0], self.maxImageDims[1]), dtype = numpy.float32) exposureTimes = [] for timepoint, (exposureTime, images, rawImages) in enumerate(self.timesAndImages): for wavelength, image in enumerate(images): height, width = image.shape allImages[wavelength, timepoint, 0, :height, :width] = image exposureTimes.append(exposureTime) drawer = depot.getHandlersOfType(depot.DRAWER)[0] header = cockpit.util.datadoc.makeHeaderFor(allImages, wavelengths = [cam.wavelength for cam in self.cameras]) # Number of bytes allocated to the extended header: 4 per image, since # we use a 32-bit floating point for the exposure time. header.next = 4 * numCams * numTimes header.NumFloats = 1 handle = open(self.savePath, 'wb') handle.write(header._array.tostring()) exposureTimes = numpy.array(exposureTimes, dtype = numpy.float32) handle.write(exposureTimes) handle.write(allImages) handle.close()
def _getDefaultSettings(self): allLights = depot.getHandlersOfType(depot.LIGHT_TOGGLE) default = { 'bleachCompensations': ['' for l in self.allLights], 'siCollectionOrder': 0, } return default
def generateTitles(self): typeToHandlers = {} # Include light filters for our active lights, even though they aren't # a part of self.allHandlers. typeToHandlers[depot.LIGHT_FILTER] = [] filters = depot.getHandlersOfType(depot.LIGHT_FILTER) for light in self.lights: wavelength = light.getWavelength() for filterHandler in filters: if light in filterHandler.lights: typeToHandlers[depot.LIGHT_FILTER].append(filterHandler) for handler in self.allHandlers: # We don't care about stage positioners because we always include # the complete stage position anyway. if handler.deviceType != depot.STAGE_POSITIONER: if handler.deviceType not in typeToHandlers: typeToHandlers[handler.deviceType] = [] typeToHandlers[handler.deviceType].append(handler) titles = [ "Date & time: %s; pos: %s" % ( time.strftime('%Y/%m/%d %H:%M:%S'), str(['%.2f' % p for p in (cockpit.interfaces.stageMover.getPosition())]) ) ] # Append the metadata we were given to start. for i in range(0, len(self.metadata) + 80, 80): substring = self.metadata[i * 80 : (i + 1) * 80] if substring: titles.append(substring) for deviceType, handlers in typeToHandlers.items(): handlers = sorted(handlers, key = lambda a: a.name) entries = [] for handler in handlers: text = handler.getSavefileInfo() if handler in self.lightToExposureTime and self.lightToExposureTime[handler]: text += ': ' + ','.join(["%.3fms" % t for t in sorted(self.lightToExposureTime[handler]) ]) # Record the exposure duration(s) of the light source. # find associated power entries (if they have them) for hand in depot.getHandlersInGroup(handler.groupName): if hand.deviceType == 'light power': text += " %3.3f mW" % hand.lastPower if text: entries.append(text) if entries: entry = "[%s: %s]" % (deviceType, ';'.join(entries)) while len(entry) > 80: # Must split it across lines. # \todo For now doing this in an optimally-space-saving # method that will result in ugly titles since we split # lines in the middle of a word. titles.append(entry[:80]) entry = entry[80:] titles.append(entry) if len(titles) > 10: raise RuntimeError("Have too much miscellaneous information to fit into the \"titles\" section of the MRC file (max 10 lines). Lines are:\n%s" % "\n".join(titles)) return titles
def __init__(self): ## List of Handlers capable of taking images. self.imageHandlers = depot.getHandlersOfType(depot.IMAGER) ## Set of active cameras, so we can check their framerates. self.activeCameras = set() events.subscribe( events.CAMERA_ENABLE, lambda c, isOn: self.toggle(self.activeCameras, c, isOn)) ## Set of active light sources, so we can check their exposure times. self.activeLights = set() events.subscribe( events.LIGHT_SOURCE_ENABLE, lambda l, isOn: self.toggle(self.activeLights, l, isOn)) ## Time of last call to takeImage(), so we can avoid calling it # faster than the time it takes to actually collect another image. self.lastImageTime = time.time() ## Boolean to control activity of the video mode thread. self.shouldStopVideoMode = False ## Boolean that indicates if we're currently in video mode. self.amInVideoMode = False events.subscribe('user abort', self.stopVideo) # Update exposure times on certain events. events.subscribe('light exposure update', self.updateExposureTime) events.subscribe(events.LIGHT_SOURCE_ENABLE, lambda *args: self.updateExposureTime()) events.subscribe(events.CAMERA_ENABLE, lambda *args: self.updateExposureTime())
def goToSite(uniqueID, shouldBlock = False): site = mover.idToSite[uniqueID] objective = depot.getHandlersOfType(depot.OBJECTIVE)[0] objOffset = objective.getOffset() offsetPosition=list(site.position) for i in range(len(offsetPosition)): offsetPosition[i]=offsetPosition[i]+objOffset[i] goTo(offsetPosition, shouldBlock)
def disconnect(self): if self.connection is not None: server = depot.getHandlersOfType(depot.SERVER)[0] server.unregister(self.callback) try: self.connection.receiveClient(None) except Exception as e: print ("Couldn't disconnect from %s: %s" % (self.serviceName, e)) self.connection = None
def cleanUp(self): for i, shouldActivate in enumerate(self.activeLights): self.allLights[i].setEnabled(shouldActivate) if (self.shouldPowerDownWhenDone is not None and self.shouldPowerDownWhenDone.GetValue()): handlers = depot.getHandlersOfType(depot.POWER_CONTROL) for handler in handlers: handler.disable() events.publish(events.UPDATE_STATUS_LIGHT, 'device waiting', '')
def connect(self, callback, timeout = 5): self.callback = callback connection = Pyro4.Proxy( 'PYRO:%s@%s:%d' % (self.serviceName, self.ipAddress, self.port)) connection._pyroTimeout = timeout self.connection = connection server = depot.getHandlersOfType(depot.SERVER)[0] uri = server.register(self.callback, self.localIp) self.connection.receiveClient(uri)
def loadSettings(self): result = cockpit.util.userConfig.getValue( self.configKey, default=self._getDefaultSettings()) allLights = depot.getHandlersOfType(depot.LIGHT_TOGGLE) if len(result['bleachCompensations']) != len(self.allLights): # Number of light sources has changed; invalidate the config. result['bleachCompensations'] = ['' for light in self.allLights] return result
def onObjectiveChange(self, name, pixelSize, transform, offset, **kwargs): h = depot.getHandlersOfType(depot.OBJECTIVE)[0] self.crosshairBoxSize = 512 * pixelSize self.offset = offset self.objectiveSelectedText.SetLabel(name.center(15)) colour = tuple(map(lambda x: 255 * x, h.getColour())) self.objectiveSelectedText.SetBackgroundColour(colour) #force a redraw so that the crosshairs are properly sized self.Refresh()
def __init__(self, parent, size, id=-1, *args, **kwargs): super().__init__(parent, id, size=size, *args, **kwargs) ## WX context for drawing. self.context = wx.glcanvas.GLContext(self) ## Whether or not we have done some one-time-only logic. self.haveInitedGL = False ## Whether or not we should try to draw self.shouldDraw = True ## Font for drawing text try: self.font = ftgl.TextureFont(cockpit.gui.FONT_PATH) self.font.setFaceSize(18) except Exception as e: print("Failed to make font:", e) ## X values below this are off the canvas. We leave it up to children # to fill in proper values for these. self.minX = 0 ## X values above this are off the canvas self.maxX = 1000 ## Y values below this are off the canvas self.minY = 0 ## Y values above this are off the canvas self.maxY = 1000 ## (X, Y, Z) vector describing the stage position as of the last # time we drew ourselves. We need this to display motion deltas. self.prevStagePosition = numpy.zeros(3) ## As above, but for the current position. self.curStagePosition = numpy.zeros(3) ## Event used to indicate when drawing is done, so we can update # the above. self.drawEvent = threading.Event() ##objective offset info to get correct position and limits self.objective = depot.getHandlersOfType(depot.OBJECTIVE)[0] self.listObj = list(self.objective.nameToOffset.keys()) self.listOffsets = list(self.objective.nameToOffset.values()) self.offset = self.objective.getOffset() ## Boolean to just force a redraw. self.shouldForceRedraw = False ## Thread that ensures we don't spam redisplaying ourselves. self.redrawTimerThread = threading.Thread(target=self.refreshWaiter, name="macrostage-refresh") self.redrawTimerThread.start() self.Bind(wx.EVT_PAINT, self.onPaint) self.Bind(wx.EVT_SIZE, lambda event: event) self.Bind(wx.EVT_ERASE_BACKGROUND, lambda event: event) # Do nothing, to avoid flashing events.subscribe(events.STAGE_POSITION, self.onMotion) events.subscribe("stage step index", self.onStepIndexChange)
def disconnect(self): if not self._listening: # Nothing to do. return server = depot.getHandlersOfType(depot.SERVER)[0] server.unregister(self._callback) try: self._proxy.receiveClient(None) except Exception as e: print ("Couldn't disconnect listener from %s: %s" % (self._proxy, e)) self._listening = False
def connect(self, callback=None, timeout=5): server = depot.getHandlersOfType(depot.SERVER)[0] if self._listening: server.unregister(self._callback) if callback: self._callback = callback elif not self._callback: # No callback specified in either self._callback or this call. raise Exception('No callback set.') uri = server.register(self._callback, self._localIp) self._proxy.receiveClient(uri) self._listening = True
def __init__(self, numReps, repDuration, imagesPerRep, sliceHeight=0, metadata='', savePath=''): super().__init__(numReps, repDuration, None, 0, 0, sliceHeight, {}, metadata=metadata, savePath=savePath) # Number of images to be collected per camera per rep. self.imagesPerRep = imagesPerRep # We didn't pass a proper exposureSettings value when calling # the Experiment constructor so we must fix the cameras and # lights attributes, as well as related attributes, ourselves. ## List of cameras. Assume our cameras are all active cameras. self.cameras = [] for cam in depot.getHandlersOfType(depot.CAMERA): if cam.getIsEnabled(): self.cameras.append(cam) ## List of light sources. Assume our lights are all active lights. self.lights = [] for light in depot.getHandlersOfType(depot.LIGHT_TOGGLE): if light.getIsEnabled(): self.lights.append(light) self.allHandlers += self.cameras + self.lights self.lightToExposureTime = {l: set() for l in self.lights} self.cameraToIsReady = {c: True for c in self.cameras} self.cameraToImageCount = {c: 0 for c in self.cameras} self.cameraToIgnoredImageIndices = {c: set() for c in self.cameras}
def centerCanvas(self, event=None): curPosition = cockpit.interfaces.stageMover.getPosition()[:2] # Calculate the size of the box at the center of the crosshairs. # \todo Should we necessarily assume a 512x512 area here? objective = depot.getHandlersOfType(depot.OBJECTIVE)[0] #if we havent previously set crosshairBoxSize (maybe no camera active) if (self.crosshairBoxSize == 0): self.crosshairBoxSize = 512 * objective.getPixelSize() self.offset = objective.getOffset() scale = (150. / self.crosshairBoxSize) self.canvas.zoomTo(-curPosition[0] + self.offset[0], curPosition[1] - self.offset[1], scale)
def laserExpUpdate(self, source=None): # TODO: fix this to use the handler reference passed in source # i.e. we *do* know which light is update. #Dont know which light is updated so update them all. lightToggles = depot.getHandlersOfType(depot.LIGHT_TOGGLE) lightToggles = sorted(lightToggles, key=lambda l: l.wavelength) for light in lightToggles: textString = self.nameToText[light.groupName + 'exp'] label = '%5d ms' % (light.getExposureTime()) textString.SetLabel(label.rjust(10)) self.Refresh()
def __init__(self, parent): super().__init__(parent) self.Sizer = wx.BoxSizer(wx.VERTICAL) self.Sizer.Add(PanelLabel(self, label="Filters")) subpanel = wx.Panel(self, style=wx.BORDER_RAISED) self.Sizer.Add(subpanel, 1, wx.EXPAND) subpanel.Sizer = wx.WrapSizer(orient=wx.VERTICAL) filters = depot.getHandlersOfType(depot.LIGHT_FILTER) if not filters: self.Hide() return for i, f in enumerate(filters): subpanel.Sizer.Add(f.makeUI(subpanel), 0, wx.EXPAND | wx.RIGHT | wx.BOTTOM, 8)
def initGL(self): glClearColor(1, 1, 1, 0) # Non-zero objective offsets require expansion of area covered # by megatiles. objs = depot.getHandlersOfType(depot.OBJECTIVE) offsets = list( itertools.chain(*[ob.nameToOffset.values() for ob in objs])) minmax = lambda l: (min(l), max(l)) xOffLim = minmax([-offset[0] for offset in offsets]) yOffLim = minmax([offset[1] for offset in offsets]) (xMin, xMax), (yMin, yMax) = self.stageHardLimits # Bounds checks ensure we only increase the megatile area, not # decrease it. # # We need to increase upper limit further so we don't miss tiles at # the edges. We should just need to add megaTileMicronSize to each # axis to make sure we add tiles up to and including our bounds. This # works for the x-axis, but not y. For some reason, we need to add # *four* times this value to the y-axis. The megaTile's y-origin is at # its centre, whereas the x-origin is at an edge; as far as I can see, # but this should require only up to three times the tilesize added to # the upper limit, not four. # Four works, though. # Arrays run [0,0] .. [ncols, nrows]; GL runs (-1,-1) .. (1,1). Since # making adjustments to render [0,0] at (-1,1), we now add two megatiles # at each y limit, rather than 4 at one edge. tsize = glGetInteger(GL_MAX_TEXTURE_SIZE) # If we use the full texture size, it seems it's too large for manipul- # ation in a framebuffer: # * on Macs with Intel chipsets; # * on some mobile nVidia chipsets. # Check vendor with glGetString(GL_VENDOR) # GL_MAX_FRAMEBUFFER_WIDTH and _HEIGHT are not universally available, # so we just use a quarter of the max texture size or a reasonable # upper bound which has been found to work in tests on 2017-ish # Macbook Pro. tsize = min(tsize // 4, 16384) MegaTile.setPixelSize(tsize) xMin += min(0, xOffLim[0]) - MegaTile.micronSize xMax += max(0, xOffLim[1]) + MegaTile.micronSize yMin += min(0, yOffLim[0]) - 2 * MegaTile.micronSize yMax += max(0, yOffLim[1]) + 2 * MegaTile.micronSize for x in np.arange(xMin, xMax, MegaTile.micronSize): for y in np.arange(yMin, yMax, MegaTile.micronSize): self.megaTiles.append(MegaTile((-x, y))) self.haveInitedGL = True
def onSelector(self, event): ## TODO: fix focus issue so that key bindings work immediately after camera enable. ## Currently, have to mouse-over the bitmap area, or click in another window. menu = wx.Menu() if self.curCamera is not None: item = menu.Append(-1, "Disable %s" % self.curCamera.descriptiveName) self.Bind(wx.EVT_MENU, lambda event: self.curCamera.toggleState(), item) else: # Get all inactive cameras. cameras = depot.getHandlersOfType(depot.CAMERA) cameras.sort(key = lambda c: c.descriptiveName) for camera in cameras: if not camera.getIsEnabled(): item = menu.Append(-1, "Enable %s" % camera.descriptiveName) self.Bind(wx.EVT_MENU, lambda event, cam=camera: cam.toggleState(), item) cockpit.gui.guiUtils.placeMenuAtMouse(self, menu)
def onMouse(self, event): if event.LeftDClick(): if self.imagePos is None: self.imagePos = cockpit.interfaces.stageMover.getPosition() x, y = event.GetPosition() pixelSize = depot.getHandlersOfType( depot.OBJECTIVE)[0].getPixelSize() x0, y0 = self.canvas.glToIndices(0, 0) dy, dx = self.canvas.canvasToIndices(x, y) dx -= x0 dy -= y0 dx *= pixelSize dy *= pixelSize target = (self.imagePos[0] - dx, self.imagePos[1] + dy) cockpit.interfaces.stageMover.goToXY(target) else: event.Skip()
def changeObjective(self): # If we have only two objectives, then simply flip them h = depot.getHandlersOfType(depot.OBJECTIVE)[0] if (h.numObjectives == 2): for obj in h.sortedObjectives: if h.currentObj != obj: h.changeObjective(obj) break else: # More than 2 objectives so need to present a list. menu = wx.Menu() for i, objective in enumerate(h.sortedObjectives): menu.Append(i, objective) menu.Bind(wx.EVT_MENU, lambda evt, obj=objective: h.changeObjective(obj), id=i) cockpit.gui.guiUtils.placeMenuAtMouse(self.panel, menu)
def __init__(self, parent): super().__init__(parent) self.Sizer = wx.BoxSizer(wx.VERTICAL) label = PanelLabel(self, label="Objective") self.Sizer.Add(label) panel = wx.Panel(self, style=wx.RAISED_BORDER) self.Sizer.Add(panel, 1, wx.EXPAND) panel.Sizer = wx.BoxSizer(wx.VERTICAL) for o in depot.getHandlersOfType(depot.OBJECTIVE): ctrl = wx.Choice(panel) ctrl.Set(o.sortedObjectives) panel.Sizer.Add(ctrl) ctrl.Bind(wx.EVT_CHOICE, lambda evt: o.changeObjective(evt.GetString())) events.subscribe( "objective change", lambda *a, **kw: ctrl.SetSelection(ctrl.FindString(a[0])))
def OnStart(self, evt): self.saveSettings() curLoc = cockpit.interfaces.stageMover.getPosition() imageWidth = float(self.imageWidth.GetValue()) imageHeight = float(self.imageHeight.GetValue()) markerSize = float(self.markerSize.GetValue()) objective = depot.getHandlersOfType(depot.OBJECTIVE)[0] pixelSize = objective.getPixelSize() for xOffset in range(int(self.numColumns.GetValue())): xLoc = curLoc[0] - xOffset * pixelSize * imageWidth for yOffset in range(int(self.numRows.GetValue())): yLoc = curLoc[1] - yOffset * pixelSize * imageHeight target = numpy.array([xLoc, yLoc, curLoc[2]]) newSite = cockpit.interfaces.stageMover.Site(target, size=markerSize) cockpit.interfaces.stageMover.saveSite(newSite) self.Destroy()
def __init__(self, parent): super().__init__(parent) self.Sizer = wx.BoxSizer(wx.VERTICAL) sz = wx.BoxSizer(wx.HORIZONTAL) label = PanelLabel(self, label="Cameras") self.Sizer.Add(label) self.Sizer.Add(sz) cameras = sorted(depot.getHandlersOfType(depot.CAMERA), key=lambda c: c.name) self.panels = {} for cam in cameras: panel = CameraPanel(self, cam) sz.Add(panel, flag=wx.EXPAND) self.panels[cam] = panel sz.AddSpacer(4) self.Fit()