從這次開始,我會由簡單到困難(其實也不會困難到哪裡去)講幾個例程,每一個例程都是我自己寫(或者修改,那樣的話我會提供原始出處)的,都具有一定的操作性和娛樂性。例程中匯儘量覆蓋到以前所講的pygame中方方面面,如果看到哪一步不明白,那就再回去複習複習,基本沒有人會看一遍什麼都記住什麼都掌握的,重複是學習之母,實踐是掌握一門技藝的最好手段!
這次就先從一個最簡單的程式開始,說實話有些太簡單我都不好意思拿出手了,不過從簡單的開始,容易建立自信培養興趣。興趣是學習之母嘛。我們這次做一個畫板,類似Windows裡自帶的畫板,還記不記得第一次接觸電腦用畫板時的驚歎?現在想起來其實那個真的非常簡陋,不過我們的比那個還要樸素,因為打算一篇講完,就不追加很多功能了,等你把這一次講解的都理解了,很容易可以自己給它增加新的機能。沒準,你就開發出一個非常牛X的畫圖工具擊敗了Photoshop,然後日進斗金名垂千古(眾:喂,別做夢了!)……
功能樣式
做之前總要有個數,我們的程式做出來會是個什麼樣子。所謂從頂到底或者從底到頂啥的,我們就不研究了,這個小程式隨你怎麼弄了,而且我們主要是來熟悉pygame,高階的軟體設計方法一概不談~
因為是抄襲畫圖板,也就是滑鼠按住了能在上面塗塗畫畫就是了,選區、放大鏡、滴管功能啥的就統統不要了。畫筆的話,基本的鉛筆畫筆總是要的,也可以考慮加一個刷子畫筆,這樣有一點變化;然後顏色應該是要的,否則太過單調了,不過調色盤啥的就暫時免了,提供幾個候選色就好了;然後橡皮……橡皮不就是白色的畫筆麼?免了免了!還有啥?似乎夠了。。。 OK,開始吧!
框架
pygame程式的框架都是差不多的,考慮到我們這個程式的實際作用,大概建立這樣的一個程式碼架子就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import pygame from pygame.locals import * class Brush(): def __init__(self): pass class Painter(): def __init__(self): self.screen = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Painter") self.clock = pygame.time.Clock() def run(self): self.screen.fill((255, 255, 255)) while True: # max fps limit self.clock.tick(30) for event in pygame.event.get(): if event.type == QUIT: return elif event.type == KEYDOWN: pass elif event.type == MOUSEBUTTONDOWN: pass elif event.type == MOUSEMOTION: pass elif event.type == MOUSEBUTTONUP: pass pygame.display.update() if __name__ == '__main__': app = Painter() app.run() |
這個非常簡單,準備好畫板類,畫筆類,暫時還都是空的,其實也就是做了一些pygame的初始化工作。如果這樣還不能讀懂的話,您需要把前面22篇從頭再看看,有幾句話不懂就看幾遍:)
這裡只有一點要注意一下,我們把幀率控制在了30,沒有人希望在畫畫的時候,CPU風扇狂轉的。而且只是畫板,沒有自動運動的物體,純粹的互動驅動,我們也不需要很高的重新整理率。
第一次的繪圖程式碼
按住滑鼠然後在上面移動就畫東西,我們很容易可以想到這個流程:
1 2 3 |
按下左鍵 → 繪製flag開 移動滑鼠 → flag開的時候,在移動座標上留下痕跡 放開左鍵 → 繪製flag關 |
立刻試一試吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class Brush(): def __init__(self, screen): self.screen = screen self.color = (0, 0, 0) self.size = 1 self.drawing = False def start_draw(self): self.drawing = True def end_draw(self): self.drawing = False def draw(self, pos): if self.drawing: pygame.draw.circle(self.screen, self.color, pos, self.size) class Painter(): def __init__(self): #*#*#*#*# self.brush = Brush(self.screen) def run(self): #*#*#*#*# elif event.type == KEYDOWN: # press esc to clear screen if event.key == K_ESCAPE: self.screen.fill((255, 255, 255)) elif event.type == MOUSEBUTTONDOWN: self.brush.start_draw() elif event.type == MOUSEMOTION: self.brush.draw(event.pos) elif event.type == MOUSEBUTTONUP: self.brush.end_draw() |
框架中有的程式碼我就不貼了,用#*#*#*#*#代替,最後會給出完整程式碼的。
這裡主要是給Brush類增加了一些功能,也就是上面我們提到的流程想對應的功能。留下痕跡,我們是使用了在座標上畫圓的方法,這也是最容易想到的方法。這樣的效果好不好呢?我們試一試:
哦,太糟糕了,再劣質的鉛筆也不會留下這樣斷斷續續的筆跡。上面是當我們滑鼠移動的快一些的時候,點之間的間距很大;下面是移動慢一些的時候,勉勉強強顯得比較連續。從這裡我們也可以看到pygame事件響應的頻度(這個距離和上面設定的最大幀率有關)。
怎麼辦?要修改幀率讓pygame平滑的反應麼?不,那樣做得不償失,換一個角度思考,如果有間隙,我們讓pygame把這個間隙連線起來不好麼?
第二次的繪圖程式碼
思路還是很簡單,當移動的時候,Brush在上一次和這一次的點之間連一條線就好了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Brush(): def __init__(self, screen): self.screen = screen self.color = (0, 0, 0) self.size = 1 self.drawing = False self.last_pos = None # <-- def start_draw(self, pos): self.drawing = True self.last_pos = pos # <-- def end_draw(self): self.drawing = False def draw(self, pos): if self.drawing: pygame.draw.line(self.screen, self.color, self.last_pos, pos, self.size * 2) self.last_pos = pos |
在__init__和start_draw中各加了一句,用來儲存上一個點的位置,然後draw也由剛剛的話圓變成畫線,效果如何?我們來試試。嗯,好多了,如果你動作能溫柔一些的話,線條已經很圓潤了,至少沒有斷斷續續的存在了。
滿足了麼?我希望你的回答是“NO”,為什麼,如果你劃線很快的話,你就能明顯看出稜角來,就好像左圖上半部分,還是能看出是由幾個線段組合的。只有永不滿足,我們才能不停進步。
不過對我們這個例程而言,差不多了,一般人在真正畫東西的時候,也不會動那麼快的:)
那麼這個就是我們最終的繪圖機制了麼?回頭看看我們的樣式,好用還需要加一個筆刷……所謂筆刷,不僅僅是很粗,而且是由很多細小的毛組成,畫出來的線是給人一種一縷一縷的感覺,用這個方法可以實現麼?好像非常非常的困難。。。孜孜不倦的我們再次進入了沉思……
這個時候,如果沒有頭緒,就得借鑑一下前輩的經驗了。看看人家是如何實現的?
把Photoshop的筆刷尺寸改大,你會發現它會畫成這樣
如果你的Photoshop不錯,應該知道它裡面複雜的筆刷設定,而Photoshop畫出來的筆畫,並不是真正一直線的,而是由無數細小的點組成的,這些點之間的間距是如此的密,以至於我們誤會它是一直線……所以說,我們還得回到第一種方法上,把它發揚光大一下~ 這沒有什麼不好意思的,放棄第二種方法並不意味著我們是多麼的愚蠢,而是說明我們從自己身上又學到了很多!
(公元前1800年)醫生:來,試試吃點兒這種草根,感謝偉大的部落守護神賜與我們神藥!
(公元900年)醫生:別再吃那種草根,簡直是野蠻不開化不尊重上帝,這是一篇祈禱詞,每天虔誠地向上帝祈禱一次,不久就會治癒你的疾病。
(公元1650年)醫生:祈禱?!封建迷信!!!來,只要喝下這種藥水,什麼病都能治好!
(公元1960年)醫生:什麼藥水?早就不用了!別喝那騙人的”萬靈藥”,還是這種藥片的療效快!
(公元1995年)醫生:哪個庸醫給你開的處方?那種藥片吃半瓶也抵不上這一粒,來來來,試試科技新成果—抗生素
(公元2003年)醫生:據最新科學研究,抗生素副作用太強,畢竟是人造的東西呀……來,試試吃點兒這種草根!早在公元前1800年,文獻就有記載了。
返璞歸真,大抵如此了。
第三次的繪圖程式碼
這次我們考慮的更多,希望在點與點之間充滿我們的筆畫,很自然的我們就需要一個迴圈來做這樣的事情。我們的筆畫有兩種,普通的實心和刷子,實心的話,用circle來畫也不失為一個好主意;刷子的話,我們可能需要一個刷子的圖案來填充了。
下面是我們新的Brush類:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
class Brush(): def __init__(self, screen): self.screen = screen self.color = (0, 0, 0) self.size = 1 self.drawing = False self.last_pos = None self.space = 1 # if style is True, normal solid brush # if style is False, png brush self.style = False # load brush style png self.brush = pygame.image.load("brush.png").convert_alpha() # set the current brush depends on size self.brush_now = self.brush.subsurface((0,0), (1, 1)) def start_draw(self, pos): self.drawing = True self.last_pos = pos def end_draw(self): self.drawing = False def set_brush_style(self, style): print "* set brush style to", style self.style = style def get_brush_style(self): return self.style def set_size(self, size): if size < 0.5: size = 0.5 elif size > 50: size = 50 print "* set brush size to", size self.size = size self.brush_now = self.brush.subsurface((0,0), (size*2, size*2)) def get_size(self): return self.size def draw(self, pos): if self.drawing: for p in self._get_points(pos): # draw eveypoint between them if self.style == False: pygame.draw.circle(self.screen, self.color, p, self.size) else: self.screen.blit(self.brush_now, p) self.last_pos = pos def _get_points(self, pos): """ Get all points between last_point ~ now_point. """ points = [ (self.last_pos[0], self.last_pos[1]) ] len_x = pos[0] - self.last_pos[0] len_y = pos[1] - self.last_pos[1] length = math.sqrt(len_x ** 2 + len_y ** 2) step_x = len_x / length step_y = len_y / length for i in xrange(int(length)): points.append( (points[-1][0] + step_x, points[-1][1] + step_y)) points = map(lambda x:(int(0.5+x[0]), int(0.5+x[1])), points) # return light-weight, uniq list return list(set(points)) |
我們增加了幾個方法,_get_points()返回上一個點到現在點之間所有的點(這話聽著真彆扭),draw根據這些點填充。
同時我們把get_size()、set_size()也加上了,用來設定當前筆刷的大小。
而變化最大的,則是set_style()和get_style(),我們現在載入一個PNG圖片作為筆刷的樣式,當style==True的時候,draw不再使用circle填充,而是使用這個PNG樣式,當然,這個樣式大小也是應該可調的,所有我們在set_size()中,會根據size大小實時的調整PNG筆刷。
當然,我們得在主迴圈中呼叫set方法,才能讓這些東西工作起來~ 過一會兒再講。再回顧下我們的樣式,還有什麼?顏色……我們馬上把顏色設定程式碼也加進去吧,太簡單了!我這裡就先偷偷懶了~
控制程式碼
到現在,我們已經完成了繪圖部分的所有功能了。現在已經可以在螢幕上自由發揮了,但是筆刷的顏色和大小好像不能改啊……我們有這樣的介面你卻不呼叫,浪費了。
…… 真抱歉,我原想一次就把塗鴉畫板講完了,沒想到筆刷就講了這麼多,再把GUI說一下就文章就太長了,不好消化。所以還是分成兩部分吧,下一次我們把GUI部分加上,就能完成對筆刷的控制了,我們的第一個實戰也就宣告成功了!