Looking for a DirectGUI color picker

Hello! I’m looking for a DirectGUI color picker that I could use in my Panda3D application. I’ve been looking everywhere and I could only find wx/tk color pickers. Somebody once posted one here but megaupload was shut down a few years ago.

Does anybody have one? Thank you in advance!

I’ve been looking for this too, in case anyone has a Panda solution for it.

This code does it using tkinter, but I hide the tk window, so is my current way to do it:

from Tkinter import *
import tkColorChooser

root = Tk()
root.iconify()

colorTuple = tkColorChooser.askcolor()

# contains RBG tuple and hex-value as rrggbb
print colorTuple     # for red = ((255, 0, 0), '#ff0000')
print colorTuple[1]  # #ff0000

Not sure how useful this will be, but if all you’re looking for is a simple way to quickly get a color from a gradient palette image by clicking on one of its pixels, then consider the following code:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenImage import OnscreenImage


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)
        
        texture_filename = 'my_color_palette.png'
        self.palette_img = PNMImage(Filename.fromOsSpecific(texture_filename))
        width = self.palette_img.getReadXSize()
        height = self.palette_img.getReadYSize()
        self.palette_size = (width, height)
        palette = OnscreenImage(image=texture_filename)
        palette.reparentTo(self.pixel2d)
        # note that the origin of an OnscreenImage is at its center
        palette.setPos(280, 0, -130)
        # the following line scales the onscreen palette image to its actual size,
        # noting that the default size of an OnscreenImage is 2x2 panda units
        palette.setScale(width * .5, 1., height * .5)
        self.palette = palette
        offset_x = palette.getX() - width / 2
        offset_y = -palette.getZ() - height / 2
        self.palette_offset = (offset_x, offset_y)

        self.accept("mouse1", self.pickColor)
        

    def pickColor(self):
    
        pointer = self.win.getPointer(0)
        x, y = pointer.getX(), pointer.getY()
        offset_x, offset_y = self.palette_offset
        x -= offset_x
        y -= offset_y
        width, height = self.palette_size

        if 0 <= x < width and 0 <= y < height:
            print "Picked color:", self.palette_img.getXel(int(x), int(y))
        else:
            print "Clicked outside palette"
        
        
app = MyApp()
app.run()

Hopefully this helped a bit.

Hello! Thanks for the help.

However, I need the color picker to be on aspect2d not pixel2d (and possibly generalized with DirectGUI). I can’t figure it out properly. Could you please help further?

Thanks!

The following code allows you to get the color of the texture pixel that you clicked on, using aspect2d instead of pixel2d (which I previously used because it’s easy to display the gradient palette texture at its actual size then):

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenImage import OnscreenImage


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)
        
        texture_filename = 'my_color_palette.png'
        self.palette_img = PNMImage(Filename.fromOsSpecific(texture_filename))
        width = self.palette_img.getReadXSize()
        height = self.palette_img.getReadYSize()
        self.palette_size = (width, height)
        palette = OnscreenImage(image=texture_filename)
        palette.setPos(-.2, 0., .25)
        scale = .4
        palette.setScale(scale, 1., scale * height / width)
        self.palette = palette

        self.accept("mouse1", self.pickColor)
        

    def pickColor(self):
        
        x, y = self.mouseWatcherNode.getMouse()
        win_w, win_h = self.win.getSize()
        width, height = self.palette_size

        if win_w < win_h:
            y *= 1. * win_h / win_w
        else:
            x *= 1. * win_w / win_h

        x -= self.palette.getX()
        y -= self.palette.getY()
        sx = self.palette.getSx()
        sy = self.palette.getSz()
        x = (.5 + x / (2. * sx)) * width
        y = (.5 + y / -(2. * sy)) * height

        if 0 <= x < width and 0 <= y < height:
            print "Picked color:", self.palette_img.getXel(int(x), int(y))
        else:
            print "Clicked outside palette"
        
        
app = MyApp()
app.run()

Not sure what you mean by that exactly - do you want a DirectFrame instead of an OnscreenImage? Can’t immediately see the benefit unless you want borders or such, but the above code should be a good starting point anyhow.

By that I mean I want to have multiple color pickers on the same screen, each independent. It would be amazing if I could just do something like:

colorPicker = DirectColorPicker(self, relief=None, pos=…)

Alright, I made a DirectColorPicker class that wraps several DirectGui objects into a rather basic - but fully functional - color picker object. You can create one by clicking any of the 3 “Color me” buttons (only if there isn’t one already for that button). You can drag it around using the left mouse button on an empty space on the color picker’s frame. Click the color spectrum to select the color at the clicked pixel. Use the slider to change the color’s brightness. Click the OK button to apply the selected color to the corresponding “Color me” button.

Here’s the code:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *



class ColorSpectrum(object):

    def __init__(self, app, click_handler, **kwargs):

        self._app = app
        self._frame = DirectFrame(**kwargs)
        self._marker = DirectFrame(parent=self._frame,
                                    frameColor=(0., 0., 0., 1.),
                                    frameSize=(-.06, .06, -.06, .06),
                                    pos=(-.89, 0., -.89))
        self._marker_center = DirectFrame(parent=self._marker,
                                          frameColor=(.5, .5, .5, 1.),
                                          frameSize=(-.02, .02, -.02, .02))
        
        texture_filename = self._frame['image']
        self._palette_img = img = PNMImage(Filename.fromOsSpecific(texture_filename))
        width = img.getReadXSize()
        height = img.getReadYSize()
        self._palette_size = (width, height)
        self._frame['state'] = DGG.NORMAL
        self._frame.bind(DGG.B1PRESS, command=click_handler)
        

    def getColorUnderMouse(self, update_marker=False):
        
        if not self._app.mouseWatcherNode.hasMouse():
            return
        
        x, y = self._app.mouseWatcherNode.getMouse()
        win_w, win_h = self._app.win.getSize()
        width, height = self._palette_size

        if win_w < win_h:
            y *= 1. * win_h / win_w
        else:
            x *= 1. * win_w / win_h

        screen = self._app.aspect2d
        x -= self._frame.getX(screen)
        y -= self._frame.getZ(screen)
        img_scale = self._frame['image_scale']
        sx = self._frame.getSx(screen) * img_scale[0]
        sy = self._frame.getSz(screen) * img_scale[2]
        marker_x = max(-.89, min(.89, x / sx))
        marker_y = max(-.89, min(.89, y / sy))
        x = (.5 + x / (2. * sx)) * width
        y = (.5 + y / -(2. * sy)) * height

        if 0 <= x < width and 0 <= y < height:

            r, g, b = color = self._palette_img.getXel(int(x), int(y))
            
            if update_marker:
                self._marker_center['frameColor'] = (r, g, b, 1.)
                self._marker.setPos(marker_x, 0., marker_y)

            return color


    def setMarkerCoord(self, coord):

        x, y = coord
        marker_x = max(-.89, min(.89, x))
        marker_y = max(-.89, min(.89, y))
        self._marker.setPos(marker_x, 0., marker_y)


    def getMarkerCoord(self):

        marker_x, y, marker_y = self._marker.getPos()

        return (marker_x, marker_y)


    def setMarkerColor(self, color):

        r, g, b = color
        self._marker_center['frameColor'] = (r, g, b, 1.)



class ColorSwatch(object):

    def __init__(self, parent, color=None):

        self._frame = DirectFrame(parent=parent,
                                  relief=DGG.SUNKEN,
                                  borderWidth=(.05, .05),
                                  frameColor=(.3, .3, .3, 1.),
                                  frameSize=(-.4, .4, -.3, .3),
                                  scale=(.5, 1., .5))
        self._swatch = DirectFrame(parent=self._frame,
                                    frameColor=color if color else (.5, .5, .5, 1.),
                                    frameSize=(-.35, .35, -.25, .25))
        self._color = color


    def setColor(self, color=None):

        self._swatch['frameColor'] = color if color else (.5, .5, .5, 1.)
        self._color = color


    def getColor(self):

        return self._color


    def setPos(self, *pos):

        self._frame.setPos(*pos)



class ColorData(object):

    def __init__(self, color=None, brightness=None, spectrum_coord=None):

        self._color = color if color else Vec3(.5, .5, .5)
        self._brightness = .5 if brightness is None else brightness
        self._coord = spectrum_coord if spectrum_coord else (-1., -1.)


    def copy(self):

        color_data = ColorData(self._color, self._brightness, self._coord)

        return color_data


    def setColor(self, color):

        self._color = color


    def getColor(self):

        return self._color


    def setBrightness(self, brightness):

        self._brightness = brightness


    def getBrightness(self):

        return self._brightness


    def getColorShade(self):
            
        if self._brightness < .5:
            color = self._color
        else:
            color = Vec3(1., 1., 1.) - self._color

        color = color * 2. * (self._brightness - .5)
        color += self._color
        r, g, b = color

        return VBase4(r, g, b, 1.)


    def setCoord(self, coord):

        self._coord = coord


    def getCoord(self):

        return self._coord



class DirectColorPicker(object):

    def __init__(self, app, picker_command, on_destroy, selected_color=None, **kwargs):

        if 'frameSize' not in kwargs:
            kwargs['frameSize'] = (-.8, .8, -1., 1.)

        self._app = app
        self._on_destroy = on_destroy
        self._frame = DirectFrame(**kwargs)
        self._frame['state'] = DGG.NORMAL
        self._frame.bind(DGG.B1PRESS, command=self.__startDrag)
        self._frame.bind(DGG.B1RELEASE, command=self.__stopDrag)
        
        spectrum_filename = 'color_spectrum.png'
        self._spectrum = ColorSpectrum(app,
                                        self.__handleColorSelection,
                                        parent=self._frame,
                                        relief=DGG.SUNKEN,
                                        borderWidth=(.05, .05),
                                        image=spectrum_filename,
                                        image_scale=(.95, 1., .95),
                                        frameColor=(.3, .3, .3, 1.),
                                        frameSize=(-1., 1., -1., 1.),
                                        pos=(-.15, 0., .3),
                                        scale=(.5, 1., .5))
        
        self._selected_color = selected_color if selected_color else ColorData()
        self._spectrum.setMarkerCoord(self._selected_color.getCoord())
        self._spectrum.setMarkerColor(self._selected_color.getColor())
        
        def updateBrightness():
        
            self._selected_color.setBrightness(self._slider['value'])
            self._swatch_selected.setColor(self._selected_color.getColorShade())
        
        brightness_filename = 'brightness.png'
        brightness = self._selected_color.getBrightness()
        self._slider = DirectSlider(parent=self._frame,
                                    orientation=DGG.VERTICAL,
                                    relief=DGG.SUNKEN,
                                    borderWidth=(.05, .05),
                                    image=brightness_filename,
                                    image_scale=(.05, 1., .95),
                                    frameColor=(.3, .3, .3, 1.),
                                    frameSize=(-.4, .4, -1., 1.),
                                    thumb_frameSize=(-.2, .2, -.1, .1),
                                    command=updateBrightness,
                                    value=brightness,
                                    pos=(.55, 0., .3),
                                    scale=(.5, 1., .5))
                                    
        self._swatch_selected = ColorSwatch(self._frame)
        self._swatch_selected.setPos(-.45, 0., -.4)
        self._swatch_current = ColorSwatch(self._frame)
        self._swatch_current.setPos(.15, 0., -.4)
        
        self._task = app.taskMgr.add(self.__showColorUnderMouse, 'show_color')
        self._drag_start = Point2() # used when dragging the color picker frame
        self._drag_offset = Vec2() # used when dragging the color picker frame
        
        def onOK():
        
            self.destroy()
            picker_command(self._selected_color)
        
        self._btn_ok = DirectButton(parent=self._frame,
                                    borderWidth=(.05, .05),
                                    frameColor=(.35, .35, .35, 1.),
                                    frameSize=(-.3, .3, -.15, .15),
                                    command=onOK,
                                    text='OK',
                                    text_pos=(0., -.03),
                                    text_scale=(.12, .12),
                                    pos=(-.35, 0., -.75))
        self._btn_cancel = DirectButton(parent=self._frame,
                                        borderWidth=(.05, .05),
                                        frameColor=(.35, .35, .35, 1.),
                                        frameSize=(-.3, .3, -.15, .15),
                                        command=self.destroy,
                                        text='Cancel',
                                        text_pos=(0., -.03),
                                        text_scale=(.12, .12),
                                        pos=(.35, 0., -.75))


    def destroy(self):

        self._task.remove()
        del self._task
        self._frame.destroy()
        del self._frame
        self._on_destroy()


    def __handleColorSelection(self, *args):

        color = self._spectrum.getColorUnderMouse(update_marker=True)

        if color:
            coord = self._spectrum.getMarkerCoord()
            self._selected_color.setCoord(coord)
            self._selected_color.setColor(color)
            self._swatch_selected.setColor(self._selected_color.getColorShade())


    def __showColorUnderMouse(self, task):

        color = self._spectrum.getColorUnderMouse()

        if color:
            r, g, b = color
            self._swatch_current.setColor(VBase4(r, g, b, 1.))

        return task.cont


    def __drag(self, task):
        
        if not self._app.mouseWatcherNode.hasMouse():
            return task.cont
        
        x, y = self._app.mouseWatcherNode.getMouse()
        win_w, win_h = self._app.win.getSize()

        if win_w < win_h:
            y *= 1. * win_h / win_w
        else:
            x *= 1. * win_w / win_h
        
        pos = Point2(x, y)
        x, z = pos + self._drag_offset
        self._frame.setPos(x, 0., z)
        self._drag_start = Point2(pos)

        return task.cont


    def __startDrag(self, *args):
        
        x, y = self._app.mouseWatcherNode.getMouse()
        win_w, win_h = self._app.win.getSize()

        if win_w < win_h:
            y *= 1. * win_h / win_w
        else:
            x *= 1. * win_w / win_h
        
        self._drag_start = Point2(x, y)
        x, y, z = self._frame.getPos()
        self._drag_offset = Point2(x, z) - self._drag_start
        self._app.taskMgr.add(self.__drag, 'drag_color_picker')


    def __stopDrag(self, *args):
        
        self._app.taskMgr.remove('drag_color_picker')



class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        self.btns = []
        self.btn_colors = []
        self.col_pickers = {}

        for i in range(3):
            getCommand = lambda btn_id: lambda: self.createColorPicker(btn_id)
            btn = DirectButton(borderWidth=(.05, .05),
                                frameColor=(.5, .5, .5, 1.),
                                frameSize=(-.4, .4, -.15, .15),
                                command=getCommand(i),
                                text='Color me',
                                text_pos=(0., -.03),
                                text_scale=(.12, .12),
                                pos=(.85 * i - .85, 0., -.65))
            self.btns.append(btn)
            color_data = ColorData(Vec3(.5, .5, .5))
            self.btn_colors.append(color_data)
            self.col_pickers[i] = None
    
    
    def setColor(self, btn_id, color_data):
    
        self.btns[btn_id]['frameColor'] = color_data.getColorShade()
        self.btn_colors[btn_id] = color_data.copy()
    
    
    def removeColorPicker(self, btn_id):
    
        self.col_pickers[btn_id] = None


    def createColorPicker(self, btn_id):
        
        if not self.col_pickers[btn_id]:
            color_data = self.btn_colors[btn_id]
            col_picker = DirectColorPicker(self,
                                            lambda color: self.setColor(btn_id, color),
                                            lambda: self.removeColorPicker(btn_id),
                                            color_data.copy(),
                                            relief=DGG.RAISED,
                                            borderWidth=(.05, .05),
                                            frameColor=(.5, .5, .5, 1.),
                                            pos=(.85 * btn_id - .85, 0., .35),
                                            scale=(.5, 1., .5))
            self.col_pickers[btn_id] = col_picker



app = MyApp()
app.run()

And these are the images I used:


Hope it will be useful.

Thanks! Great bit of code. Will certainly get some use

Thank you very much!

Great work as always, but it seems to have some issues when the window is resized. Any thoughts on that?

Thanks.

Hi! :slight_smile:

What issues are you having exactly? Mousing over the colour spectrum seems to display the correct colour in the swatch, even after resizing the Panda window in various ways.

If you mean that the buttons and colour picker controls are being scaled and clipped by the window border in a weird way, then there’s probably not much I can do about that, as this is just how aspect2d (to which DirectGUI widgets are parented) behaves, I believe.

Well, that’s the problem in my case, when the window is resized in various ways, mousing over the colour spectrum does not display the correct colour in the swatch…:thinking:

When you drag a colour picker widget around, does it follow the mouse cursor precisely, or is there a deviation visible then as well?

It would be interesting to see if things work better when you replace code like this:

win_w, win_h = self._app.win.getSize()

with something like:

win_props = self._app.win.getProperties()
win_w = win_props.getXSize()
win_h = win_props.getYSize()

At the moment, that’s really all I can suggest, I’m afraid.

There is a visible deviation as well.

I did but sadly, the problem still persists for me, when I set the window to full screen or resize it any other way the movement of the mouse cursor doesn’t result in the correct colour being selected and displayed on the swatch.

It’s okay if there seems to be no way around this; I’ll just have to use fixed square grids for my end in sight. Thanks as always for the help though!

Are you encountering this issue when running my code snippet itself, or after integrating it into your own project? In the latter case, maybe you are overriding the ShowBase handler for the window-event, such that the aspect ratio isn’t being respected at all?

Another thing you could try is to use ShowBase.win.getPointer(0) instead of ShowBase.mouseWatcherNode;
replace instances of this code:

        x, y = self._app.mouseWatcherNode.getMouse()

with this:

        mouse_pointer = self._app.win.getPointer(0)
        x = mouse_pointer.getX()
        y = -mouse_pointer.getY()
        x = (x * 2. / win_w) - 1.
        y = (y * 2. / win_h) + 1.

This still needs to take aspect ratio into account, so it probably won’t help much either. getPointer is also more suited to be used together with pixel2d instead of aspect2d, so here’s an alternative version of the code snippet that reparents the colour picker widgets to pixel2d:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *


class ColorSpectrum(object):

    def __init__(self, app, click_handler, **kwargs):

        self._app = app
        self._frame = DirectFrame(**kwargs)
        self._marker = DirectFrame(parent=self._frame,
                                    frameColor=(0., 0., 0., 1.),
                                    frameSize=(-.06, .06, -.06, .06),
                                    pos=(-.89, 0., -.89))
        self._marker_center = DirectFrame(parent=self._marker,
                                          frameColor=(.5, .5, .5, 1.),
                                          frameSize=(-.02, .02, -.02, .02))
        
        texture_filename = self._frame['image']
        self._palette_img = img = PNMImage(Filename.fromOsSpecific(texture_filename))
        width = img.getReadXSize()
        height = img.getReadYSize()
        self._palette_size = (width, height)
        self._frame['state'] = DGG.NORMAL
        self._frame.bind(DGG.B1PRESS, command=click_handler)

    def getColorUnderMouse(self, update_marker=False):
        
        if not self._app.mouseWatcherNode.hasMouse():
            return
        
        mouse_pointer = self._app.win.getPointer(0)
        x = mouse_pointer.getX()
        y = -mouse_pointer.getY()
        width, height = self._palette_size
        screen = self._app.pixel2d
        x -= self._frame.getX(screen)
        y -= self._frame.getZ(screen)
        img_scale = self._frame['image_scale']
        sx = self._frame.getSx(screen) * img_scale[0]
        sy = self._frame.getSz(screen) * img_scale[2]
        marker_x = max(-.89, min(.89, x / sx))
        marker_y = max(-.89, min(.89, y / sy))
        x = (.5 + x / (2. * sx)) * width
        y = (.5 + y / -(2. * sy)) * height

        if 0 <= x < width and 0 <= y < height:

            r, g, b = color = self._palette_img.getXel(int(x), int(y))
            
            if update_marker:
                self._marker_center['frameColor'] = (r, g, b, 1.)
                self._marker.setPos(marker_x, 0., marker_y)

            return color

    def setMarkerCoord(self, coord):

        x, y = coord
        marker_x = max(-.89, min(.89, x))
        marker_y = max(-.89, min(.89, y))
        self._marker.setPos(marker_x, 0., marker_y)

    def getMarkerCoord(self):

        marker_x, y, marker_y = self._marker.getPos()

        return (marker_x, marker_y)

    def setMarkerColor(self, color):

        r, g, b = color
        self._marker_center['frameColor'] = (r, g, b, 1.)


class ColorSwatch(object):

    def __init__(self, parent, color=None):

        self._frame = DirectFrame(parent=parent,
                                  relief=DGG.SUNKEN,
                                  borderWidth=(.05, .05),
                                  frameColor=(.3, .3, .3, 1.),
                                  frameSize=(-.4, .4, -.3, .3),
                                  scale=(.5, 1., .5))
        self._swatch = DirectFrame(parent=self._frame,
                                    frameColor=color if color else (.5, .5, .5, 1.),
                                    frameSize=(-.35, .35, -.25, .25))
        self._color = color

    def setColor(self, color=None):

        self._swatch['frameColor'] = color if color else (.5, .5, .5, 1.)
        self._color = color

    def getColor(self):

        return self._color

    def setPos(self, *pos):

        self._frame.setPos(*pos)


class ColorData(object):

    def __init__(self, color=None, brightness=None, spectrum_coord=None):

        self._color = color if color else Vec3(.5, .5, .5)
        self._brightness = .5 if brightness is None else brightness
        self._coord = spectrum_coord if spectrum_coord else (-1., -1.)

    def copy(self):

        color_data = ColorData(self._color, self._brightness, self._coord)

        return color_data

    def setColor(self, color):

        self._color = color

    def getColor(self):

        return self._color

    def setBrightness(self, brightness):

        self._brightness = brightness

    def getBrightness(self):

        return self._brightness

    def getColorShade(self):
            
        if self._brightness < .5:
            color = self._color
        else:
            color = Vec3(1., 1., 1.) - self._color

        color = color * 2. * (self._brightness - .5)
        color += self._color
        r, g, b = color

        return VBase4(r, g, b, 1.)

    def setCoord(self, coord):

        self._coord = coord

    def getCoord(self):

        return self._coord


class DirectColorPicker(object):

    def __init__(self, app, picker_command, on_destroy, selected_color=None, **kwargs):

        if 'frameSize' not in kwargs:
            kwargs['frameSize'] = (-.8, .8, -1., 1.)

        self._app = app
        self._on_destroy = on_destroy
        self._frame = DirectFrame(**kwargs)
        self._frame['state'] = DGG.NORMAL
        self._frame.bind(DGG.B1PRESS, command=self.__startDrag)
        self._frame.bind(DGG.B1RELEASE, command=self.__stopDrag)
        
        spectrum_filename = 'color_spectrum.png'
        self._spectrum = ColorSpectrum(app,
                                        self.__handleColorSelection,
                                        parent=self._frame,
                                        relief=DGG.SUNKEN,
                                        borderWidth=(.05, .05),
                                        image=spectrum_filename,
                                        image_scale=(.95, 1., .95),
                                        frameColor=(.3, .3, .3, 1.),
                                        frameSize=(-1., 1., -1., 1.),
                                        pos=(-.15, 0., .3),
                                        scale=(.5, 1., .5))
        
        self._selected_color = selected_color if selected_color else ColorData()
        self._spectrum.setMarkerCoord(self._selected_color.getCoord())
        self._spectrum.setMarkerColor(self._selected_color.getColor())
        
        def updateBrightness():
        
            self._selected_color.setBrightness(self._slider['value'])
            self._swatch_selected.setColor(self._selected_color.getColorShade())
        
        brightness_filename = 'brightness.png'
        brightness = self._selected_color.getBrightness()
        self._slider = DirectSlider(parent=self._frame,
                                    orientation=DGG.VERTICAL,
                                    relief=DGG.SUNKEN,
                                    borderWidth=(.05, .05),
                                    image=brightness_filename,
                                    image_scale=(.05, 1., .95),
                                    frameColor=(.3, .3, .3, 1.),
                                    frameSize=(-.4, .4, -1., 1.),
                                    thumb_frameSize=(-.2, .2, -.1, .1),
                                    command=updateBrightness,
                                    value=brightness,
                                    pos=(.55, 0., .3),
                                    scale=(.5, 1., .5))
                                    
        self._swatch_selected = ColorSwatch(self._frame)
        self._swatch_selected.setPos(-.45, 0., -.4)
        self._swatch_current = ColorSwatch(self._frame)
        self._swatch_current.setPos(.15, 0., -.4)
        
        self._task = app.taskMgr.add(self.__showColorUnderMouse, 'show_color')
        self._drag_start = Point2() # used when dragging the color picker frame
        self._drag_offset = Vec2() # used when dragging the color picker frame
        
        def onOK():
        
            self.destroy()
            picker_command(self._selected_color)
        
        self._btn_ok = DirectButton(parent=self._frame,
                                    borderWidth=(.05, .05),
                                    frameColor=(.35, .35, .35, 1.),
                                    frameSize=(-.3, .3, -.15, .15),
                                    command=onOK,
                                    text='OK',
                                    text_pos=(0., -.03),
                                    text_scale=(.12, .12),
                                    pos=(-.35, 0., -.75))
        self._btn_cancel = DirectButton(parent=self._frame,
                                        borderWidth=(.05, .05),
                                        frameColor=(.35, .35, .35, 1.),
                                        frameSize=(-.3, .3, -.15, .15),
                                        command=self.destroy,
                                        text='Cancel',
                                        text_pos=(0., -.03),
                                        text_scale=(.12, .12),
                                        pos=(.35, 0., -.75))

    def destroy(self):

        self._task.remove()
        del self._task
        self._frame.destroy()
        del self._frame
        self._on_destroy()

    def __handleColorSelection(self, *args):

        color = self._spectrum.getColorUnderMouse(update_marker=True)

        if color:
            coord = self._spectrum.getMarkerCoord()
            self._selected_color.setCoord(coord)
            self._selected_color.setColor(color)
            self._swatch_selected.setColor(self._selected_color.getColorShade())

    def __showColorUnderMouse(self, task):

        color = self._spectrum.getColorUnderMouse()

        if color:
            r, g, b = color
            self._swatch_current.setColor(VBase4(r, g, b, 1.))

        return task.cont

    def __drag(self, task):

        if not self._app.mouseWatcherNode.hasMouse():
            return task.cont

        mouse_pointer = self._app.win.getPointer(0)
        x = mouse_pointer.getX()
        y = -mouse_pointer.getY()
        pos = Point2(x, y)
        x, z = pos + self._drag_offset
        self._frame.setPos(x, 0., z)
        self._drag_start = Point2(pos)

        return task.cont

    def __startDrag(self, *args):

        mouse_pointer = self._app.win.getPointer(0)
        x = mouse_pointer.getX()
        y = -mouse_pointer.getY()

        self._drag_start = Point2(x, y)
        x, y, z = self._frame.getPos()
        self._drag_offset = Point2(x, z) - self._drag_start
        self._app.taskMgr.add(self.__drag, 'drag_color_picker')

    def __stopDrag(self, *args):
        
        self._app.taskMgr.remove('drag_color_picker')


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        self.btns = []
        self.btn_colors = []
        self.col_pickers = {}

        for i in range(3):
            getCommand = lambda btn_id: lambda: self.createColorPicker(btn_id)
            btn = DirectButton(borderWidth=(.05, .05),
                                frameColor=(.5, .5, .5, 1.),
                                frameSize=(-.4, .4, -.15, .15),
                                command=getCommand(i),
                                text='Color me',
                                text_pos=(0., -.03),
                                text_scale=(.12, .12),
                                pos=(.85 * i - .85, 0., -.65))
            self.btns.append(btn)
            color_data = ColorData(Vec3(.5, .5, .5))
            self.btn_colors.append(color_data)
            self.col_pickers[i] = None

    def setColor(self, btn_id, color_data):

        self.btns[btn_id]['frameColor'] = color_data.getColorShade()
        self.btn_colors[btn_id] = color_data.copy()

    def removeColorPicker(self, btn_id):

        self.col_pickers[btn_id] = None

    def createColorPicker(self, btn_id):

        if not self.col_pickers[btn_id]:
            color_data = self.btn_colors[btn_id]
            col_picker = DirectColorPicker(self,
                                            lambda color: self.setColor(btn_id, color),
                                            lambda: self.removeColorPicker(btn_id),
                                            color_data.copy(),
                                            parent=self.pixel2d,
                                            relief=DGG.RAISED,
                                            borderWidth=(.05, .05),
                                            frameColor=(.5, .5, .5, 1.),
                                            pos=(250 * btn_id + 150, 0., -200),
                                            scale=(150, 1., 150))
            self.col_pickers[btn_id] = col_picker


app = MyApp()
app.run()

If you don’t mind that the size and position of the colour pickers remains the same regardless of the window size, then this might fix your issue. Fingers crossed!

The issue appears when I copy your code, line for line with zero changes on both my computers. Though I wonder if it has to do with the panda version I am using?

I doubt that’s the case. Though things look either too thin or too wide when I do change the window size here and there; perhaps I should take a look into that later.

Nope, I don’t really mind, that did the trick and now the code runs without any issues, everything else in the screen gets jumbled up but the colour picker retains its size and position.
Thanks as always!:smile: