wxPython for image and buttons (resizable)

2019-02-11 09:26发布

问题:

I want to put such an image in a wx.Panel :

  • The animals should be "buttons" so that if I click on them, their image changes, and if I reclick, the image returns to normal (thus the animals can be considered as simple BitmapToggleButtons, as suggested by another question here on SO)

  • This panel should be resized/rescaled (all all the children images / togglebuttons too!) keeping the aspect ratio, if the parent wx.Panel is resized to something smaller for example (like would do the standard Windows Photo Viewer : http://res1.windows.microsoft.com/resbox/en/windows%207/main/7eaf462a-86dd-42d2-a789-7413f5472dae_63.jpg)

I am still a bit lost on : how to implement such a clickable (with toggle buttons) and rescalable Canvas?

Edit : I started with something fruitful here Rescale image when parent is resized in wxPython, but now I'm totally stuck about how to continue (detect clicks, update buttons with direct DC Painting ?), that's why the bounty.

回答1:

You will have to implement your own hit testing, i.e. be able to determine where each animal is -- this is the difficult part and there is really nothing in wxWidgets to help you with this. The rest is relatively simple, you might even be able to use the existing wxMouseEventsManager to avoid writing the boilerplate code yourself (but if you can't, you can at least look at its implementation, which is done entirely in wxWidgets itself, to see what you need to do).



回答2:

Depending on how much of this is already written, you may want to take a look at FloatCanvas (it's in the wxPython library).

If you have most of the code-base done, you can use a hit test, which is rather simple. Just make a dictionary with the [x][y] coordinates as keys which has the the BitmapTogglebutton as its value.

Here's some code that does something similar (it's been a while since I've used wxPython so it may not be 100%):

def onLeftDown( event ):
    x,y = event.GetX(), event.GetY()
    hitmap_x = hitmap.get(x,None)
    if hitmap_x:
        btn = hitmap_x.get(y, None)
    ...stuff with btn like toggles


回答3:

I made some code for practice recently. It may somehow match with your requirement. Code is ugly and mess because I'm a newcomer to python.

Support:

  1. image on the backgroud draggable
  2. image animating with double-click
  3. background and image on it resizeable

Notice:

  1. you need have pygame to run the code
  2. you can load real image by replacing PyGamePseudoImage()
  3. image coordinate adjustment is not smooth enough while zoom in/out

Code:

import wx
import pygame

BLACK = (  0,   0,   0)
WHITE = (255, 255, 255)
BLUE =  (  0,   0, 255)
GREEN = (  0, 255,   0)
RED =   (255,   0,   0)

pygame.font.init()
try:
    regular_font_file = os.path.join(os.path.dirname(__file__), "Vera.ttf")
    bold_font_file = os.path.join(os.path.dirname(__file__), "VeraBd.ttf")

    # Check for cx_Freeze
    #
    if "frozen" in sys.__dict__.keys() and sys.frozen:

        regular_font_file = os.path.join(sys.path[1], "Vera.ttf")
        bold_font_file = os.path.join(sys.path[1], "VeraBd.ttf")

    BIG_FONT = pygame.font.Font(regular_font_file, 30)
    SMALL_FONT = pygame.font.Font(regular_font_file, 12)
    BOLD_FONT = pygame.font.Font(bold_font_file, 12)

except:
    # TODO: log used font: pygame.font.get_default_font()
    #print("Could not load {0}".format(os.path.join(os.path.dirname(__file__), "Vera.ttf")))
    BIG_FONT = pygame.font.Font(None, 40)
    SMALL_FONT = BOLD_FONT = pygame.font.Font(None, 20)


class PyGamePseudoImage():
    def __init__(self, size, color):
        self.screen = pygame.Surface(size, 0, 32)
        self.screen.fill(color)

    def getImage(self):
        return self.screen

class __MouseMixin:

    def onLeftUp(self, event):
        pass

    def onLeftDown(self, event):
        pass

    def onLeftDClick(self, event):
        pass

    def onRightUp(self, event):
        pass

    def onRightDown(self, event):
        pass

    def onDragging(self, event):
        pass

    def onMouseEnter(self, event):
        pass

    def OnMouseHandler(self, event):
        event.Skip()

        if event.LeftUp():
            self.onLeftUp(event)
        elif event.LeftDown():
            self.onLeftDown(event)
        elif event.LeftDClick():
            self.onLeftDClick(event)
        elif event.RightUp():
            self.onRightUp(event)
        elif event.RightDown():
            self.onRightDown(event)
        elif event.Dragging() and event.LeftIsDown():
            self.onDragging(event)

        pass


class DragSprite(__MouseMixin, pygame.sprite.Sprite):
    SPRITE_BUTTON, SPRITE_TRANSPORTER = range(2)

    def __init__(self, parent=None):
        pygame.sprite.Sprite.__init__(self)
        self.is_select = 0
        self.lastPos = 0
        self.lastUpdate = 0
        self.parent = parent

    def setLastPos(self, pos):
        self.lastPos = pos

    def move(self, pos):
        dx = pos[0] - self.lastPos[0]
        dy = pos[1] - self.lastPos[1]
        self.lastPos = pos
        center = (self.rect.center[0] + dx, self.rect.center[1] + dy)
        self.rect.center = center
        return

    def isSelected(self):
        return self.is_select

    def setSelect(self, is_select):
        self.is_select = is_select
        return

    def update(self, current_time):
        return

def drawBoader(image, rect):
    W,H = (rect.width, rect.height)
    yellow = (255, 255, 0)
    pygame.draw.rect(image, yellow, (0,0,W-2,H-2), 2)

class ButtonSprite(DragSprite):
    def __init__(self, parent=None, initPos=(0,0), width=50, height=50, dicts=None):
        DragSprite.__init__(self, parent)
        self.type = DragSprite.SPRITE_BUTTON
        self.resourceCfgDict = dicts
        self.imageResource = {}
        self.status = 0
        self.index = 0

        self.parent = parent
        self.initPos = (initPos[0], initPos[1])
        self.width = width
        self.height = height
        self.rectOnLoad = pygame.Rect(initPos, (width, height))
        self.rect = self.rectOnLoad.copy()

        self.operationOn = None
        self.operationOff = None

        self.operationDic = {"on": self.getOperationOnItem, "off": self.getOperationOffItem}
        self.guiCfg = None

        for dic in dicts:
            self.loadImgResource(dic)

        self.setCurrentResource("off")

    def getOperationOnItem(self):
        return self.operationOn

    def getOperationOffItem(self):
        return self.operationOff

    def loadImgResource(self, dict):
        """
            load image with pygame lib
        """
        key = dict[0]
        file_name = dict[1]

        #image_file = pygame.image.load(file_name) #use this to load real image
        image_file = PyGamePseudoImage((500,500), file_name).getImage()
        imagedata = pygame.image.tostring(image_file, "RGBA")
        imagesize = image_file.get_size()
        imageSurface = pygame.image.fromstring(imagedata, imagesize , "RGBA")

        self.imageResource[key] = (file_name, imageSurface)

    def resizeResource(self, src, size):
        return pygame.transform.smoothscale(src, size)

    def setCurrentResource(self, status):
        self.currentStatus = status
        self.imageOnLoad = self.resizeResource(self.imageResource[status][1], (self.width, self.height))
        self.image = pygame.transform.scale(self.imageOnLoad, (self.rect.width, self.rect.height))

    def switchResource(self, index):
        self.setCurrentResource(index)

    def onZoomUpdate(self, zoomRatio):
        parentRect = pygame.Rect(self.parent.GetRect())
        dx = self.rectOnLoad.centerx - parentRect.centerx
        dy = self.rectOnLoad.centery - parentRect.centery

        self.rect.centerx = parentRect.centerx + dx*zoomRatio
        self.rect.centery = parentRect.centery + dy*zoomRatio

        self.rect.height = self.imageOnLoad.get_rect().height * zoomRatio
        self.rect.width = self.imageOnLoad.get_rect().width * zoomRatio

        self.image = pygame.transform.scale(self.imageOnLoad, (self.rect.width, self.rect.height))

    def update(self, current_time, ratio):
        if self.isSelected():
            drawBoader(self.image, self.image.get_rect())
        else:
            pass
            #self.image = self.imageOnLoad.copy()

    def onRightUp(self, event):
        print "onRightUp"
        event.Skip(False)
        pass

    def onLeftDClick(self, event):
        if self.currentStatus == "on":
            self.setCurrentResource("off")
        elif self.currentStatus == "off":
            self.setCurrentResource("on")

        return

    def move(self, pos):
        DragSprite.move(self, pos)

        parentRect = pygame.Rect(self.parent.GetRect())
        centerDx = self.rect.centerx - parentRect.centerx
        centerDy = self.rect.centery - parentRect.centery

        self.rectOnLoad.centerx = parentRect.centerx + centerDx/self.parent.zoomRatio
        self.rectOnLoad.centery = parentRect.centery + centerDy/self.parent.zoomRatio


class MyHmiPanel(wx.Panel):
    def __init__(self, parent, ID):
        wx.Window.__init__(self, parent, ID)
        self.parent = parent
        self.hwnd = self.GetHandle()
        self.size = self.GetSizeTuple()
        self.size_dirty = True
        self.rootSpriteGroup = pygame.sprite.LayeredUpdates()

        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_TIMER, self.Update, self.timer)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.fps = 60.0
        self.timespacing = 1000.0 / self.fps
        self.timer.Start(self.timespacing, False)
        self.previous_time = 0
        self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)

        self.selectedSprite = None

        self.zoomRatio = 1
        self.background = None
        self.bgRect = None
        self.backgroundOnUpdate = None
        self.bgRetOnUpdate = None

        self.loadBackground()
        self.addTestSprite()

    def loadBackground(self):
        #self.background = pygame.image.load(image_file) #use this to load real image
        self.background = PyGamePseudoImage((500,500), (255, 0, 0)).getImage()
        self.bgRect = self.background.get_rect()
        self.backgroundOnUpdate = self.background.copy()
        self.bgRetOnUpdate = self.bgRect.copy()

    def resizeUpdateBackground(self):
        self.bgRect.center = self.screen.get_rect().center
        self.bgRetOnUpdate = self.bgRect.copy()

    def zoomUpdateBackground(self, zoomRatio):
        self.bgRetOnUpdate.width = self.bgRect.width * zoomRatio
        self.bgRetOnUpdate.height = self.bgRect.height * zoomRatio
        self.bgRetOnUpdate.width = self.bgRect.width * zoomRatio
        self.bgRetOnUpdate.center = self.screen.get_rect().center
        self.backgroundOnUpdate = pygame.transform.scale(self.background, (self.bgRetOnUpdate.width, self.bgRetOnUpdate.height))

    def drawBackground(self, screen):
        screen.blit(self.backgroundOnUpdate, self.bgRetOnUpdate)

    def addTestSprite(self):
        #self.rootSpriteGroup.add(ButtonSprite(self, initPos=(100, 100), width=100, height=100, dicts= [('on', btn_red_on), ('off', btn_red_off)]))
        #self.rootSpriteGroup.add(ButtonSprite(self, initPos=(200, 200), width=100, height=100, dicts= [('on', btn_red_on), ('off', btn_red_off)]))
        self.rootSpriteGroup.add(ButtonSprite(self, initPos=(100, 100), width=100, height=100, dicts= [('on', GREEN), ('off', BLUE)]))
        self.rootSpriteGroup.add(ButtonSprite(self, initPos=(200, 200), width=100, height=100, dicts= [('on', GREEN), ('off', BLUE)]))

    def Update(self, event):
        self.Redraw()
        return

    def Redraw(self):
        if  self.size[0] == 0  or  self.size[1] == 0:
            return

        if self.size_dirty:
            self.screen = pygame.Surface(self.size, 0, 32)
            self.resizeUpdateBackground()
            self.size_dirty = False

        self.screen.fill((0,0,0))
        self.drawBackground(self.screen)

        w, h = self.screen.get_size()
        current_time = pygame.time.get_ticks()

        self.previous_time = current_time
        self.rootSpriteGroup.update(current_time, self.zoomRatio)
        self.rootSpriteGroup.draw(self.screen)

        s = pygame.image.tostring(self.screen, 'RGB')  # Convert the surface to an RGB string
        #img = wx.ImageFromData(self.size[0], self.size[1], s)  # Load this string into a wx image
        img = wx.ImageFromData(w, h, s)  # Load this string into a wx image

        #if img.IsOk() is not True:
           # return
        bmp = wx.BitmapFromImage(img)  # Get the image in bitmap form
        dc = wx.ClientDC(self)  # Device context for drawing the bitmap
        dc = wx.BufferedDC( dc)
        dc.DrawBitmap(bmp, 0, 0, 1)  # Blit the bitmap image to the display


    def checkCollide(self, event):
        x , y = (event.GetX(),event.GetY())

        mousePoint = pygame.sprite.Sprite()
        mousePoint.rect = pygame.Rect(x, y, 1, 1)
        copoint = pygame.sprite.spritecollide(mousePoint, self.rootSpriteGroup, None)

        if copoint:
            copoint = copoint[-1]

        return copoint

    def removeSelectedSprite(self):
        if self.selectedSprite:
            self.selectedSprite.setSelect(0)
            self.selectedSprite = None

    def setNewSelectedSprite(self, sprite):
        self.removeSelectedSprite()
        sprite.setSelect(1)
        self.selectedSprite = sprite

    def onSelectSprite(self, event, onMouseObj):
        if onMouseObj:
            if self.selectedSprite:
                if onMouseObj != self.selectedSprite:
                    self.setNewSelectedSprite(onMouseObj)
            else:
                self.setNewSelectedSprite(onMouseObj)

            self.selectedSprite.setLastPos((event.GetX(),event.GetY()))
        else:
            self.removeSelectedSprite()

    def OnMouse(self, event):
        onMouseObj = self.checkCollide(event)
        event.Skip()

        if onMouseObj:
            onMouseObj.OnMouseHandler(event)

        if not event.GetSkipped():
            print "event dropped "
            return

        if event.LeftDown():
            self.onSelectSprite(event, onMouseObj)
        elif event.LeftUp():
            pass
        elif event.RightUp():
            self.onSelectSprite(event, onMouseObj)
        elif event.RightDown():
            self.onSelectSprite(event, onMouseObj)
        elif event.Dragging() and event.LeftIsDown():
            if self.selectedSprite:
                self.selectedSprite.move((event.GetX(),event.GetY()))

    def OnPaint(self, event):
        self.Redraw()
        event.Skip()  # Make sure the parent frame gets told to redraw as well

    def OnSize(self, event):
        self.size = self.GetSizeTuple()
        self.size_dirty = True

    def Kill(self, event):
        self.Unbind(event=wx.EVT_PAINT, handler=self.OnPaint)
        self.Unbind(event=wx.EVT_TIMER, handler=self.Update, source=self.timer)

    def onZoomIn(self):
        self.zoomRatio += 0.2
        self.onZoomUpdate()

    def onZoomReset(self):
        self.zoomRatio = 1
        self.onZoomUpdate()

    def onZoomOut(self):
        if self.zoomRatio > 0.2:
            self.zoomRatio -= 0.2
        self.onZoomUpdate()

    def onZoomUpdate(self):
        self.zoomUpdateBackground(self.zoomRatio)
        for s in self.rootSpriteGroup.sprites():
            s.onZoomUpdate(self.zoomRatio)


class TestFrame ( wx.Frame ):
    def __init__( self, parent, fSize ):
        wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = fSize, style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL )

        self.SetSizeHintsSz( wx.DefaultSize, wx.DefaultSize )

        fgSizer1 = wx.FlexGridSizer( 2, 1, 0, 0 )
        fgSizer1.AddGrowableCol( 0 )
        fgSizer1.AddGrowableRow( 0 )
        fgSizer1.SetFlexibleDirection( wx.VERTICAL )
        fgSizer1.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_ALL )


        self.panelMain = MyHmiPanel(self, -1)

        fgSizer1.Add( self.panelMain, 1, wx.EXPAND |wx.ALL, 5 )

        self.m_panel4 = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
        bSizer3 = wx.BoxSizer( wx.HORIZONTAL )

        self.bZoomIn = wx.Button( self.m_panel4, wx.ID_ANY, u"Zoom In", wx.DefaultPosition, wx.DefaultSize, 0 )
        bSizer3.Add( self.bZoomIn, 0, wx.ALL, 5 )

        self.bReset = wx.Button( self.m_panel4, wx.ID_ANY, u"Reset", wx.DefaultPosition, wx.DefaultSize, 0 )
        bSizer3.Add( self.bReset, 0, wx.ALL, 5 )

        self.bZoomOut = wx.Button( self.m_panel4, wx.ID_ANY, u"Zoom Out", wx.DefaultPosition, wx.DefaultSize, 0 )
        bSizer3.Add( self.bZoomOut, 0, wx.ALL, 5 )

        self.m_panel4.SetSizer( bSizer3 )
        self.m_panel4.Layout()
        bSizer3.Fit( self.m_panel4 )
        fgSizer1.Add( self.m_panel4, 1, wx.EXPAND |wx.ALL, 5 )

        self.SetSizer( fgSizer1 )
        self.Layout()
        self.Centre( wx.BOTH )

        self.bZoomIn.Bind( wx.EVT_BUTTON, self.onZoomIn )
        self.bReset.Bind( wx.EVT_BUTTON, self.onZoomReset )
        self.bZoomOut.Bind( wx.EVT_BUTTON, self.onZoomOut )

    def __del__( self ):
        pass

    def onZoomIn( self, event ):
        self.panelMain.onZoomIn()
        event.Skip()

    def onZoomReset( self, event ):
        self.panelMain.onZoomReset()
        event.Skip()

    def onZoomOut( self, event ):
        self.panelMain.onZoomOut()
        event.Skip()


if __name__=='__main__':
        app = wx.App(redirect=False)
        frame = TestFrame(None, (800, 600))
        frame.SetPosition((100, 100))
        frame.Show()
        app.MainLoop()


回答4:

I solved the problem with :

import wx
from floatcanvas import FloatCanvas

class MyPanel(wx.Panel):
    def __init__(self, parent):
        super(MyPanel, self).__init__(parent)
        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(self.sizer)

        # add a canvas
        self.Canvas = FloatCanvas.FloatCanvas(self, BackgroundColor = "LIGHT GREY")
        self.Canvas.Bind(wx.EVT_SIZE, self.OnSize)
        self.sizer.Add(self.Canvas, -1, flag=wx.EXPAND)

        # add a toggle button
        image_dis = wx.Image('file_disabled.png')
        image_ena = wx.Image('file_enabled.png')
        img_dis = self.Canvas.AddScaledBitmap(image_dis, (x,-y), Height=image_dis.GetHeight(), Position = 'tl')
        img_ena = self.Canvas.AddScaledBitmap(image_ena, (x,-y), Height=image_ena.GetHeight(), Position = 'tl')
        img_dis.other = img_ena
        img_ena.other = img_dis
        img_ena.Visible = False

        # bind the toggle button event 
        img_dis.Bind(FloatCanvas.EVT_FC_LEFT_UP, self.OnToggle)
        img_ena.Bind(FloatCanvas.EVT_FC_LEFT_UP, self.OnToggle)

    def OnToggle(self, button):
        button.other.Visible = True
        button.Visible = False
        self.Canvas.Draw(True)

    def OnSize(self, event):
        event.Skip()
        wx.CallLater(1, self.Canvas.ZoomToBB)  


回答5:

I can't answer the scaling issue, but an old trick I recall for doing the arbitrary image target hit-checking (no buttons required) goes like this:

1) Create a blank invisible image the same size as the visible one.

2) As you draw targets on the main image, draw an identically shaped "shadow" to the invisible with all the same value pixel (but a unique value for every target). A "handle", if you will.

3) When you get a mouse click on the main image, use the coordinates to get the same pixel from your invisible shadow image. The value will be the handle for the target.

Simple once you hear it, isn't it?