连点成图:享受创建图形的乐趣
定位
本文适合于想要使用 wxpython 进行 python gui 编程的筒鞋。 读完本文后,应当能了解 wxpython 编写基本 GUI 的知识,以及熟悉策略设计模式的使用。
缘起
发现这个游戏还挺有意思的, 不如自己来开发个? 主要设计思路如下:
1. 使用 wxpython 做基本的 GUI , 可以生成 n * n 的初始点阵图 , n = (2, 25) ;
2. 使用 Python 实现策略模式; 编写了一个可扩展的小框架, 只要编写 xxxStrategyOfLinkpoints , 并添加到 strategyManager.strategiesForlinkPoints 即可; 实现委托模式, 绘图策略相关的接口均由 LinkPointStrategy 来提供, 由 StrategyManager 来管理; StrategyManager 使用单例模式来实现;
3. 点阵图的算法, 程序员自然是使用程序生成, 既有趣也很锻炼算法思维连带激发创造力哦。已经对点阵图的点联接实现了抽象, 只需要当做 (0,0) - (n-1,n-1) 的二维矩阵来考虑。 算法需要生成一个列表, 列表中的每个元素是一个点对元组 ((x1,y1), (x2, y2)), 每个点是一个元组,包含该点的逻辑坐标。 不需要涉及点的实际坐标。
4. 实现拖拽的创作模式, 在鼠标按下时获取位置, 检测是否有效点范围并记录起始点, 在鼠标释放时记下结束点并取出起始点进行绘图。
不足之处是, 在鼠标释放之前, 所绘制的直线是不可见的, 这一点用户体验不佳。暂时没有找到方法在鼠标移动过程中实时绘线后再删除。
5. 回退机制: 只能回到过去和现在, 无法回到过去的未来。比如从 A 到 X, 然后回退到 R, 接着前进到 S', 那么之前的 S 到 X 的部分都将丢失。
鉴于数据量比较少, 采用简单的方案。 保持一份工作状态的列表以及一个指向某个工作状态的指针。 回退或前进只是移动指针。如果回退后push, 那么需要将回退前的回退点之后的部分截掉。
6. 保存和从文件恢复功能。 采用可读的文本形式, 便于分析和调试。
改进点:
1. 从欣赏模式到创作模式或保存文件的过程中会有“未响应”的问题, 用户体验不佳;
2. 创建常用简单图案的函数, 更方便地使用算法组合出更复杂更美妙的图案;
3. 关卡设置。
你也来试试吧! 如果有兴趣做成移动游戏, 那就更棒了!
源程序:
linkpointsUI.py
# -*- coding: utf8 -*- # ------------------------------------------------------------------------------- # Name: linkpointsUI.py # Purpose: a game which links points to a gragh and enjoy # # Author: qin.shuq # # Created: 12/06/2014 # Copyright: (c) qin.shuq 2014 # Licence: ONLY BE USED FOR STUDY #------------------------------------------------------------------------------- import wx import time import math import os import threading import copy from LinkPointStrategy import * class LinkPointsFrame(wx.Frame): ''' generate dotSize * dotSize dotted graph and app ui ''' def __init__(self, parent, title, dotSize=18, uiSize=(810,560)): wx.Frame.__init__(self, parent, title=title, size=uiSize) self.mainPanel = None # 主面板,用于绘制点阵图 self.dc = None # 用于绘制图形的对象 self.dotSize = dotSize # 点阵图大小设定,形成 dotSize * dotSize 点阵图 self.displayDemoTimer = None # 欣赏模式下自动显示已有创作的定时器 self.validPointsRange = set() # 拖拽模式时有效点的范围 set([px, py]) self.isCreateMode = False # 是否创作模式 self.origin = 10 # 原点的实际坐标 self.pointRadius = 3 # 点使用实心圆圈表示,增大点击范围 self.mousePostion = MousePositionEachPressAndRelease() # 拖拽时记录下鼠标所在位置 self.currWork = [] # 记录创作模式的当前工作以便于保存 self.history = WorkHistory() # 记录当前工作状态的历史,便于回退及前进 panelSize = self.GetClientSize() topBottomMargin = 20 leftRightMargin = 30 uiWidth = panelSize[0] - leftRightMargin panelHeight = panelSize[1] - topBottomMargin self.intervalBetweenPoints = (panelHeight-self.origin*2) / (self.dotSize-1) self.validPointsRange = self.obtainRealCoordsOfDottedPoints() self.mainPanelSize = (panelHeight, panelHeight) self.ctrlPanelSize = (uiWidth - self.mainPanelSize[0], panelHeight) self.initUI() self.Centre() def initUI(self): ### UI Design follows top-down thinking and down-top building bigPanel = wx.Panel(self, name="WhileWindow") font = wx.Font(12, wx.ROMAN, wx.NORMAL, wx.NORMAL) hboxLayout = wx.BoxSizer(wx.HORIZONTAL) self.mainpanel = wx.Panel(bigPanel, name="mainPanel", size=self.mainPanelSize) self.mainpanel.SetBackgroundColour('#fffff0') self.mainpanel.Bind(wx.EVT_KEY_DOWN, self.onKeyDown) self.mainpanel.Bind(wx.EVT_LEFT_DOWN, self.mouseLeftPressHandler) #self.mainpanel.Bind(wx.EVT_MOTION, self.mouseMoveHandler) self.mainpanel.Bind(wx.EVT_LEFT_UP, self.mouseLeftReleaseHandler) ctrlPanel = wx.Panel(bigPanel, name="ctrlPanel", size=self.ctrlPanelSize) hboxLayout.Add(self.mainpanel, 0, wx.EXPAND|wx.ALL, 10) hboxLayout.Add(ctrlPanel, 0, wx.EXPAND|wx.ALL, 10) bigPanel.SetSizer(hboxLayout) topPanel = wx.Panel(ctrlPanel, name="topPanel") tipInfo ="How to Play: \n\nJust link points to build a graph, \nSo Easy And Enjoy Yourself !\n\n" keyInfo = "Press ESC to quit. \nPress z to back.\nPress x to forward.\n" staticText = wx.StaticText(topPanel, label=decodeUTF8(tipInfo+keyInfo)) staticText.SetFont(font) btnBoxSizer = wx.GridSizer(8,2, 10, 5) buttonSize = (100, 30) enterCreateModeBtn = wx.Button(ctrlPanel, name="createMode", label=decodeUTF8("创作模式"), size=buttonSize) enterDemoModeBtn = wx.Button(ctrlPanel, name="demoMode", label=decodeUTF8("欣赏模式"), size=buttonSize) saveBtn = wx.Button(ctrlPanel, name="SaveWork", label=decodeUTF8("保存工作"), size=buttonSize) restoreBtn = wx.Button(ctrlPanel, name="restore", label=decodeUTF8("恢复已存工作"), size=buttonSize) self.Bind(wx.EVT_BUTTON, self.enterCreateMode, enterCreateModeBtn) self.Bind(wx.EVT_BUTTON, self.enterDemoMode, enterDemoModeBtn) self.Bind(wx.EVT_BUTTON, self.saveWork, saveBtn) self.Bind(wx.EVT_BUTTON, self.restoreWork, restoreBtn) btnBoxSizer.Add(enterCreateModeBtn, 0, wx.ALL) btnBoxSizer.Add(enterDemoModeBtn, 0, wx.ALL) btnBoxSizer.Add(saveBtn,0, wx.ALL) btnBoxSizer.Add(restoreBtn,0, wx.ALL) vboxLayout = wx.BoxSizer(wx.VERTICAL) vboxLayout.Add(topPanel, 1, wx.EXPAND|wx.ALL, 5) vboxLayout.Add(btnBoxSizer, 1, wx.EXPAND|wx.ALL, 5) ctrlPanel.SetSizer(vboxLayout) self.Show(True) # show demo self.displayDemoTimer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.displayDemoGraph, self.displayDemoTimer) self.createDemoForUsage() self.displayDemoInTimer() def enterCreateMode(self, event): self.mainpanel.SetFocus() # 使键盘事件获得响应 self.isCreateMode = True if self.displayDemoTimer: self.displayDemoTimer.Stop() self.createNewDottedGraph() self.history.clear() self.currWork = [] def enterDemoMode(self, event): self.mainpanel.SetFocus() self.isCreateMode = False self.displayDemoTimer.Start(100, oneShot=True) def createNewDottedGraph(self): ''' 清空屏幕, 重新绘制点阵图 ''' if self.dc: self.dc.Clear() self.dc = wx.ClientDC(self.mainpanel) self.dc.SetPen(wx.Pen('GREEN')) self.dc.SetBrush(wx.Brush('GREEN')) for xcoord in range(self.origin, self.mainPanelSize[0] + self.intervalBetweenPoints, self.intervalBetweenPoints): for ycoord in range(self.origin, self.mainPanelSize[1] + self.intervalBetweenPoints, self.intervalBetweenPoints): self.dc.DrawPoint(xcoord, ycoord) self.dc.DrawCircle(xcoord,ycoord, self.pointRadius) def createDemoForUsage(self): ''' 展示创建图案的接口用法 ''' self.createNewDottedGraph() linkpointsStrategy = LinkPointStrategy(self.dotSize) allLines = linkpointsStrategy.obtainAllLinesByLinkPoints() self.drawGraph(allLines) ### demo for registering user-defined strategy def myStrategy(allPoints, size): return [(point, (point[0]+1, point[1]+1)) for point in allPoints if (point[0] == point[1] and point[0]<size-1)] LinkPointStrategy.registerStrategy("my", myStrategy) LinkPointStrategy.setStrategy("my") self.createNewDottedGraph() self.drawGraph(linkpointsStrategy.obtainAllLinesByLinkPoints()) def displayDemoGraph(self, event): linkpointsStrategy = LinkPointStrategy(self.dotSize) allStrategies = linkpointsStrategy.getAllStrategies() for strategyName in allStrategies: self.createNewDottedGraph() linkpointsStrategy.setStrategy(strategyName) self.drawGraph(linkpointsStrategy.obtainAllLinesByLinkPoints()) time.sleep(2) def displayDemoInTimer(self): ''' 欣赏模式下使用定时器自动展示已创建的图案 ''' self.displayDemoTimer.Start(100, oneShot=True) def drawGraphForRealCoords(self, allLines): ''' 根据已生成的所有线的设置绘制图案 一条线是一个元组: ((x1,y1), (x2, y2)) xi, yi 是实际坐标 ''' for line in allLines: self.dc.DrawLine(line[0][0], line[0][1], line[1][0], line[1][1]) def drawGraph(self, allLines): ''' 根据已生成的所有线的设置绘制图案 一条线是一个元组: ((x1,y1), (x2, y2)) xi, yi 是逻辑坐标 ''' #print '***************************************' for line in allLines: #print line[0][0], ' ', line[0][1], ' ', line[1][0], ' ', line[1][1] x1 = self.obtainRealCoords(line[0][0]) y1 = self.obtainRealCoords(line[0][1]) x2 = self.obtainRealCoords(line[1][0]) y2 = self.obtainRealCoords(line[1][1]) self.dc.DrawLine(x1, y1, x2, y2) def mouseLeftPressHandler(self, event): ''' 拖拽时鼠标按下时的动作 ''' if self.isCreateMode: pos = event.GetPosition() nearestPoint = self.nearestPoint(pos) if nearestPoint: self.mousePostion.pushPressPos(nearestPoint[0], nearestPoint[1]) else: showMsgDialog('请将鼠标放于点的位置进行拖拽!', '提示') def mouseMoveHandler(self, event): ''' 拖拽时鼠标移动的动作 ''' pass # if event.Dragging() and event.LeftIsDown(): # pressPos = self.mousePostion.getPressPos() # lastPos = self.mousePostion.getLastMovePos() # moveNowPos = event.GetPosition() # self.mousePostion.pushMovePos(moveNowPos[0], moveNowPos[1]) # #self.dc.DrawLine(pressPos[0], pressPos[1], moveNowPos[0], moveNowPos[1]) # event.Skip() def mouseLeftReleaseHandler(self, event): ''' 拖拽时鼠标释放时的动作 ''' if self.isCreateMode: nearestStart = self.mousePostion.getPressPos() releasePos = event.GetPosition() nearestEnd = self.nearestPoint(releasePos) if nearestEnd: self.dc.DrawLine(nearestStart[0], nearestStart[1], nearestEnd[0], nearestEnd[1]) self.currWork.append((nearestStart, nearestEnd)) self.history.push(copy.copy(self.currWork)) else: showMsgDialog('请将鼠标放于点的位置进行拖拽!', '提示') def onKeyDown(self, event): #self.history.show() kc=event.GetKeyCode() if kc == wx.WXK_ESCAPE: ret = wx.MessageBox(decodeUTF8("确定要退出程序吗?"), decodeUTF8("询问"), wx.YES_NO|wx.NO_DEFAULT,self) if ret == wx.YES: self.Close() if kc == 90: # press z lastWork = self.history.back() if lastWork is None: showMsgDialog('已经位于最开始的地方,无法回退!', '提示') return self.createNewDottedGraph() self.drawGraphForRealCoords(lastWork) self.currWork = copy.copy(lastWork) elif kc == 88: # press x nextWork = self.history.forward() if nextWork is None: showMsgDialog('已经位于最后的状态,无法向前!', '提示') return self.createNewDottedGraph() self.drawGraphForRealCoords(nextWork) self.currWork = copy.copy(nextWork) #self.history.show() def obtainRealCoordsOfDottedPoints(self): ''' 获取点阵图中所有点的实际坐标 ''' validPointsRange = set() for localXCoord in range(self.dotSize): for localYCoord in range(self.dotSize): validPointsRange.add((self.obtainRealCoords(localXCoord), self.obtainRealCoords(localYCoord))) return validPointsRange def nearestPoint(self, point): ''' 鼠标按下或释放时判断鼠标位置是否处于有效点的范围,并获取最近的有效点用于连线 如果鼠标位置未处于有效点的位置,则返回 None ''' if point in self.validPointsRange: return point tolerance = self.intervalBetweenPoints/4 ### 允许用户点击离有效点范围很近的地方 for validPoint in self.validPointsRange: if self.distance(point, validPoint) <= self.pointRadius + tolerance: return validPoint return None def distance(self, point1, point2): return math.hypot(point1[0]-point2[0], point1[1]-point2[1]) def obtainRealCoords(self, localCoord): ''' 将逻辑坐标 (x,y) 转换为 实际坐标 (real_x, real_y). eg. 假设原点坐标是 (15,15), 点间隔为 (30, 30), 则 (1,1) -> (45,45) 这样在绘图时就可以专注于以逻辑坐标来思考,摒弃实际坐标的细节干扰 ''' return self.origin+localCoord*self.intervalBetweenPoints def saveWork(self, event): self.mainpanel.SetFocus() file_wildcard = "files(*.lp)|*.lp|All files(*.*)|*.*" dlg = wx.FileDialog(self, "Save as ...", os.getcwd(), "default.lp", style = wx.SAVE | wx.OVERWRITE_PROMPT, wildcard = file_wildcard) f_work = None if dlg.ShowModal() != wx.ID_OK: dlg.Destroy() return filename = dlg.GetPath() if not os.path.splitext(filename)[1]: #如果没有文件名后缀 filename = filename + '.lp' f_work = open(filename, 'w') dlg.Destroy() f_work.write("LINK POINTS FILE.\n") for (startPoint , endPoint) in self.currWork: f_work.write(str(startPoint[0]) + ' ' + str(startPoint[1]) + ' ' + str(endPoint[0]) + ' ' + str(endPoint[1]) + '\n') f_work.close() showMsgDialog('工作保存成功!^_^', '提示') def restoreWork(self, event): self.mainpanel.SetFocus() file_wildcard = "files(*.lp)|*.lp|All files(*.*)|*.*" dlg = wx.FileDialog(self, "Open file...", os.getcwd(), style = wx.OPEN, wildcard = file_wildcard) if dlg.ShowModal() != wx.ID_OK: dlg.Destroy() return filename = dlg.GetPath() f_work = open(filename) lines = f_work.readlines() f_work.close() dlg.Destroy() self.history.clear() if lines[0].strip() != 'LINK POINTS FILE.': showMsgDialog('文件类型无效,请打开后缀为.lp的文件!', '提示') else: self.createNewDottedGraph() self.currWork = [] for line in lines[1:]: pointCoords = line.strip().split(' ') if len(pointCoords) != 4: showMsgDialog('文件内容已损坏!', '提示') return startPointX = pointCoords[0] startPointY = pointCoords[1] endPointX = pointCoords[2] endPointY = pointCoords[3] try: self.dc.DrawLine(int(startPointX), int(startPointY), int(endPointX), int(endPointY)) self.currWork.append( ((int(startPointX), int(startPointY)), (int(endPointX), int(endPointY))) ) except: showMsgDialog('文件内容已损坏!', '提示') return self.history.push(self.currWork) showMsgDialog('成功恢复工作,自动进入创作模式!^_^ ', '提示') self.isCreateMode = True class MousePositionEachPressAndRelease(object): ''' mousePosition: [(xpress, ypress), (xlastMove, ylastMove)] ''' def __init__(self): self.mousePosition = [] def pushPressPos(self, xcoord, ycoord): self.mousePosition.insert(0, (xcoord, ycoord)) def pushMovePos(self, xcoord, ycoord): self.mousePosition.insert(1, (xcoord, ycoord)) def getPressPos(self): return self.mousePosition[0] def getLastMovePos(self): return self.mousePosition[1] class WorkHistory(object): ''' 保存工作快照列表,实现回退功能 ''' def __init__(self): self.worksnapshots = [[]] self.currPoint = 0 def push(self, currWork): ### 如果在回退操作之后立即 push , 则回退之前从回退点之后的动作都将清空 self.currPoint+=1 self.worksnapshots = self.worksnapshots[0: self.currPoint] self.worksnapshots.append(currWork) def back(self): if self.currPoint <= 0: return None else: self.currPoint-=1 return self.worksnapshots[self.currPoint] def forward(self): if self.currPoint >= len(self.worksnapshots)-1: return None else: self.currPoint+=1 return self.worksnapshots[self.currPoint] def clear(self): self.worksnapshots = [[]] self.currPoint = 0 def show(self): print "curr point: ", self.currPoint for snapshot in self.worksnapshots: print snapshot # utils def decodeUTF8(msg): return msg.decode('utf8') def showMsgDialog(msg, title): dialog = wx.MessageDialog(None, decodeUTF8(msg), decodeUTF8(title), wx.YES_DEFAULT) dialog.ShowModal() dialog.Destroy() def main(): app = wx.App(False) frame = LinkPointsFrame(None, decodeUTF8('连点成图: 享受创建图形的乐趣')) app.MainLoop() if __name__ == '__main__': main()
LinkPointStrategy.py
# -*- coding: utf8 -*- # ------------------------------------------------------------------------------- # Name: LinkPointStrategy.py # Purpose: varieties of algorithms for linking points # # Author: qin.shuq # # Created: 11/29/2014 # Copyright: (c) qin.shuq 2014 # Licence: ONLY BE USED FOR STUDY # ------------------------------------------------------------------------------- def linesForDiamond(centerPoint, radius): ''' centerPoint: (localXCoord, localYCoord) radius: the distance from points in diamond to centerpoint ''' centerx = centerPoint[0] centery = centerPoint[1] leftPoint = (centerx-1, centery) rightPoint = (centerx+1, centery) topPoint = (centerx, centery-1) bottomPoint = (centerx, centery+1) return [(leftPoint, topPoint), (topPoint, rightPoint), (rightPoint, bottomPoint), (bottomPoint, leftPoint)] def repeatedDiamondStrategy(allPoints, size): allLines = [] radius = 2 for point in allPoints: if not isOutOfBound(point, radius, size): allLines.extend(linesForDiamond(point, radius)) return allLines def isOutOfBound(centerPoint, radius, dotSize): if centerPoint[0] <= radius-1 or centerPoint[0] + radius >= dotSize: return True if centerPoint[1] <= radius-1 or centerPoint[1] + radius >= dotSize: return True return False def simpleLoopStrategyOfLinkpoints(allPoints, size): pairs = [] for i in range(size): if i*2 <= size-1: pairs.append((i, size-1-i)) allLines = [] for pair in pairs: allLines.append( ((pair[0], pair[0]), (pair[0], pair[1])) ) allLines.append( ((pair[0], pair[0]), (pair[1], pair[0])) ) allLines.append( ((pair[0], pair[1]), (pair[1], pair[1])) ) allLines.append( ((pair[1], pair[0]), (pair[1], pair[1])) ) return allLines def loopStrategyOfLinkpoints(allPoints, size): pairs = [] for i in range(size): if i*2 <= size-1: pairs.append((i, size-1-i)) allLines = [] for pair in pairs: begin = (pair[0], pair[0]) end = (pair[1], pair[1]) for localXCoord in range(pair[0], pair[1], 1): allLines.append(((pair[0], localXCoord), (pair[0], localXCoord+1))) allLines.append(((pair[1], localXCoord), (pair[1], localXCoord+1))) for localYCoord in range(pair[0], pair[1], 1): allLines.append(((localYCoord, pair[0]), (localYCoord+1, pair[0]))) allLines.append(((localYCoord, pair[1]), (localYCoord+1, pair[1]))) return allLines def defaultStrategyOfLinkpoints(allPoints, size): return [( point, (point[0]+1, point[1]+1) ) for point in allPoints if not isRightOrButtomBoundPoint(point, size)] def isRightOrButtomBoundPoint(point, size): localXCoord = point[0] localYCoord = point[1] return localXCoord == size-1 or localYCoord == size-1 def singleton(cls): ''' Implements Singleton pattern in python. From http://blog.csdn.net/ghostfromheaven/article/details/7671853 ''' instances = {} def _singleton(*args, **kw): if cls not in instances: instances[cls] = cls(*args, **kw) return instances[cls] return _singleton @singleton class StrategyManager(object): def __init__(self): self.strategiesForlinkPoints = { 'default': defaultStrategyOfLinkpoints, 'loop': loopStrategyOfLinkpoints, 'simpleLoop': simpleLoopStrategyOfLinkpoints, 'diamond': repeatedDiamondStrategy } self.DEFAULT_STRATEGY = self.strategiesForlinkPoints['default'] self.CURR_STRATEGY = self.DEFAULT_STRATEGY def getStrategy(self, strategyName): strategyForLinkPoints = self.strategiesForlinkPoints.get(strategyName) if strategyForLinkPoints is None: raise Exception('No stragegy named "%s". You can write one. ' % strategyName) return strategyForLinkPoints def registerStrategy(self, strategyName, strategyForLinkPoints): oldStrategy = self.strategiesForlinkPoints.get(strategyName) if oldStrategy: self.strategiesForlinkPoints['old_' + strategyName] = oldStrategy self.strategiesForlinkPoints[strategyName] = strategyForLinkPoints def setCurrStrategy(self, strategyName): self.CURR_STRATEGY = self.getStrategy(strategyName) def getCurrStratety(self): return self.CURR_STRATEGY def getAllStrategies(self): return self.strategiesForlinkPoints.keys() class LinkPointStrategy(object): ''' just think in a dotted graph of (0,0) - (dotSize-1, dotSize-1) with interval of points = 1 (0,0), (0,1), ... , (0, dotSize-1) (1,0), (1,1), ... , (1, dotSize-1) ... , ... , ... , ... (dotSize-1,0), (dotSize-1, 1), ..., (dotSize-1, dotSize-1) and output a set of [((x1,y1), (x2,y2)), ..., ((xm,ym), (xn,yn))] ''' strategyManager = StrategyManager() def __init__(self, dotSize): self.dotSize = dotSize self.allPoints = [] for localXCoord in range(dotSize): for localYCoord in range(dotSize): self.allPoints.append((localXCoord, localYCoord)) @classmethod def setStrategy(cls, strategyName): cls.strategyManager.setCurrStrategy(strategyName) @classmethod def getStrategy(cls, strategyName): return cls.strategyManager.getStrategy(strategyName) @classmethod def registerStrategy(cls, strategyName, strategyFunc): cls.strategyManager.registerStrategy(strategyName, strategyFunc) @classmethod def getAllStrategies(cls): return cls.strategyManager.getAllStrategies() def obtainAllLinesByLinkPoints(self): ''' generate all lines between points according to given strategy which is a algorithm of linking points line: a tuple of (x1, y1, x2, y2) note: (x1, y1, x2, y2) are local coordinates which will be converted into real coordinates upon drawing ''' currStrategy = LinkPointStrategy.strategyManager.getCurrStratety() return currStrategy(self.allPoints, self.dotSize) if __name__ == '__main__': strategyManager = StrategyManager() anoStrategyManager = StrategyManager() assert id(strategyManager) == id(anoStrategyManager)