第 3 章 打地鼠

2018-02-24 15:51 更新

作者介紹

Ellen Spertus

本書(shū)的共同作者之一,美國(guó)加州奧克蘭市米爾斯大學(xué)的計(jì)算機(jī)科學(xué)教授,同時(shí)也是谷歌公司的資深科學(xué)家。她先后在MIT獲得了計(jì)算機(jī)科學(xué)與工程學(xué)士學(xué)位、電子工程與計(jì)算機(jī)科學(xué)碩士及博士學(xué)位,并利用暑假的空閑時(shí)間為微軟公司工作。她曾撰文探討技術(shù)及社會(huì)問(wèn)題,而且經(jīng)常將兩者相結(jié)合。1993年紐約時(shí)報(bào)曾以《改變計(jì)算機(jī)領(lǐng)域面貌的女性》為題介紹Spertus,并在后續(xù)的文章中稱(chēng)其為“最性感的活著的極客”。2009年Spertus加入谷歌的App Inventor for Android團(tuán)隊(duì),并參與撰寫(xiě)了本書(shū)的部分章節(jié)。

本章將創(chuàng)建一個(gè)“打地鼠”的游戲,游戲靈感來(lái)自一款經(jīng)典的街機(jī)游戲Whac-A-Mole,其中的小動(dòng)物會(huì)突然從洞中冒出,玩家則用木槌擊打它們,擊中得分?!按虻厥蟆钡膭?chuàng)作者是一名App Inventor團(tuán)隊(duì)的成員,與其說(shuō)她是為了測(cè)試sprite組件的功能(她做到了),不如說(shuō)是她自己喜歡玩游戲。

{%}

{%}

圖 3-1 打地鼠游戲的用戶(hù)界面

當(dāng)Ellen Spertus加入Google公司的App Inventor團(tuán)隊(duì)時(shí),她希望App Inventor也可以用于游戲的開(kāi)發(fā),因此她自告奮勇地承擔(dān)起sprites的實(shí)現(xiàn)任務(wù)。sprite原本用來(lái)表示神話中的角色,如仙女、妖精等,到20世紀(jì)70年代開(kāi)始出現(xiàn)在計(jì)算機(jī)界,用來(lái)代表那些能夠在電腦屏幕上移動(dòng)的圖像(在電子游戲中)。Ellen第一次使用sprite是在20世紀(jì)80年代早期,她曾經(jīng)參加電腦訓(xùn)練營(yíng)并使用TI 99/4 編程。她在sprites以及“打地鼠”游戲上所做的努力,受到了雙重懷舊情緒的驅(qū)使——計(jì)算機(jī)以及游戲——她童年時(shí)代的最?lèi)?ài)。

可以查看Android版“打地鼠”游戲的視頻教程?!敬私坛逃蒞olber教授基于上一個(gè)版本的App Inventor錄制的,但同樣可以有助于理解開(kāi)發(fā)過(guò)程?!?/p>

學(xué)習(xí)目標(biāo)

如圖3-1所示的“打地鼠”應(yīng)用將實(shí)現(xiàn)以下功能:

  • 一只地鼠隨機(jī)出現(xiàn)在屏幕上,每秒鐘移動(dòng)一次;

  • 如果手指觸碰到地鼠,則讓設(shè)備震動(dòng),顯示的命中數(shù)加1,地鼠隨機(jī)移動(dòng)到一個(gè)新位置;

  • 如果手指直接觸摸到屏幕但沒(méi)點(diǎn)擊中地鼠,則顯示失敗數(shù)加1;

  • 點(diǎn)擊“重新開(kāi)始”按鈕,游戲重新開(kāi)始,命中和失敗的計(jì)數(shù)歸零。

學(xué)習(xí)內(nèi)容

本章內(nèi)容覆蓋了以下的組件及概念:

  • ImageSprite組件:具有觸感的可移動(dòng)圖像;

  • Canvas組件:容納ImageSprite的平臺(tái);

  • Clock組件:用來(lái)計(jì)時(shí),讓sprite隨即移動(dòng);

  • Sound組件:擊中地鼠時(shí)產(chǎn)生震動(dòng);

  • Button組件:開(kāi)始新游戲;

  • Procedures:用來(lái)實(shí)現(xiàn)一系列的指令,可以重復(fù)調(diào)用,如移動(dòng)地鼠;

  • 產(chǎn)生隨機(jī)數(shù);

  • 使用加法塊(+)及減法塊(-)。

準(zhǔn)備開(kāi)始

登陸App Inventor網(wǎng)站,開(kāi)始新項(xiàng)目“MoleMash ”,將屏幕標(biāo)題(title)設(shè)為“打地鼠”,并連接到測(cè)試設(shè)備。

下載地鼠圖片mole.png。下載方法:控制鍵+單擊(Mac)或單擊右鍵(Windows)并選擇“圖片另存為”或類(lèi)似選項(xiàng)。下載成功后,在設(shè)計(jì)器組件列表下方的Media部分,單擊“Upload file…”,找到剛下載的文件mole.png并上傳到App Inventor中。

設(shè)計(jì)組件

創(chuàng)建“打地鼠”游戲需要以下組件:

  • Canvas組件:用來(lái)限定游戲中地鼠的活動(dòng)區(qū)域;

  • ImageSprite組件:用來(lái)顯示地鼠圖片,隨機(jī)移動(dòng),并具有觸感;

  • Sound組件:當(dāng)?shù)厥蟊挥|摸到時(shí),發(fā)出震動(dòng);

  • Label組件:用來(lái)顯示“命中: ”、“失敗: ”以及命中、失敗的次數(shù);

  • HorizontalArrangements組件:用來(lái)放置Label組件,使組件的布局合理;

  • Button組件:用來(lái)將命中及失敗次數(shù)歸零(重新開(kāi)始游戲);

  • Clock組件:使地鼠每秒鐘隨機(jī)移動(dòng)一次。

表3-1顯示了應(yīng)用中用到的全部組件。

表3-1 “打地鼠”應(yīng)用中的全部組件列表

組件類(lèi)型 組件種類(lèi) 命名 作用
Canvas Drawing and Animation Canvas1 ImageSprite的容
ImageSprite Drawing and Animation Mole 用戶(hù)點(diǎn)擊的目標(biāo)
Button User Interface ResetButton 重新設(shè)置得分
Clock User Interface Clock1 控制地鼠的移動(dòng)頻率
Sound Media Sound1 當(dāng)?shù)厥蟊粨糁袝r(shí)震動(dòng)
Label User Interface HitsLabel 顯示文字“擊中: ”
Label User Interface HitsCountLabel 顯示擊中次數(shù)
HorizontalArrangement Layout HorizontalArrangement1 放置HitsLabel及HitsCountLabel
Label User Interface MissesLabel 顯示文字“失?。?”
Label User Interface MissesCountLabel 顯示失敗次數(shù)
HorizontalArrangement Layout HorizontalArrangement2 放置MissesLabel及MissesCountLabel

設(shè)置活動(dòng)組件

本節(jié)將設(shè)置游戲中所需的活動(dòng)組件,下節(jié)再來(lái)設(shè)置顯示分?jǐn)?shù)的組件。

1. 找到Palette->Drawing and Animation->Canvas組件,拖入預(yù)覽窗口,采用其默認(rèn)名稱(chēng)Canvas1,設(shè)置Width屬性為“Fill parent”,即與屏幕等寬,設(shè)置Height屬性為300像素;

2. 找到Palette->Drawing and Animation->ImageSprite,將ImageSprite組件拖入到Canvas1中的任何位置,在組件列表底部單擊rename,改名為“Mole”,設(shè)置其Picture屬性為之前上傳的mole.png;

3. 找到Palette->User Interface->Button,拖動(dòng)Button組件放在Canvas1下面,改名為“ResetButton”,并設(shè)置其Text屬性為“重新開(kāi)始”;

4. 找到Palette->User Interface->Clock,拖入Clock組件,它將落在預(yù)覽窗口下方的“非可是組件”區(qū)域;

5. 找到Palette->Media->Sound,拖入Sound組件,它也將落在“非可視組件”區(qū)域。

現(xiàn)在組件設(shè)計(jì)器看起來(lái)應(yīng)該如圖3-2(地鼠的位置有可能不同)。

圖 3-2 組件設(shè)計(jì)器視圖中的所有“活動(dòng)”組件

布置Label組件

現(xiàn)在設(shè)置顯示用戶(hù)得分的組件,即,顯示命中與失敗次數(shù)的組件。

1. 找到Palette->Layout->HorizontalArrangement,拖動(dòng)組件放在“重新啟動(dòng)”按鈕的下方,保留HorizontalArrangement1的默認(rèn)名稱(chēng);

2. 從Palette->User Interface中拖動(dòng)兩個(gè)Label組件到HorizontalArrangement1中;

  • 將左側(cè)Label改名為HitsLabel,設(shè)置其Text屬性為“命中: ”(確保冒號(hào)后有一個(gè)空格);

  • 將右側(cè)Label改名為HitsCountLabel,設(shè)置其Text屬性為“0”;

3. 拖入第二個(gè)HorizontalArrangement,將其放在HorizontalArrangement1下面;

4. 將兩個(gè)Label拖放在HorizontalArrangement2中;

  • 左側(cè)Label改名為MissesLabel,設(shè)置其Text屬性為“失?。?”(確保冒號(hào)后有一個(gè)空格);

  • 右側(cè)Label改名為MissesCountLabel,設(shè)置其Text屬性為“0”。

你的屏幕看起來(lái)如圖3-3。

圖 3-3 組件設(shè)計(jì)器視圖中“打地鼠”應(yīng)用的所有組件

為組件添加行為

組件已經(jīng)創(chuàng)建完成,下面切換到塊編輯器來(lái)實(shí)現(xiàn)程序的行為。設(shè)置的目標(biāo):①讓地鼠每秒鐘在Canvas1上隨機(jī)移動(dòng)一次;②用戶(hù)拍打這只隨機(jī)移動(dòng)的地鼠,應(yīng)用顯示用戶(hù)命中或失敗的次數(shù)(注:建議用手指而不是木槌拍打?。?;按下“重新啟動(dòng)”按鈕命中及失敗次數(shù)歸零。

移動(dòng)地鼠

在迄今為止完成的應(yīng)用中,曾經(jīng)調(diào)用過(guò)內(nèi)置過(guò)程 ,如HelloPurr中的Sound1.Vibrate(震動(dòng))。假如App Inventor中有一個(gè)內(nèi)置過(guò)程,可以將ImageSprite移動(dòng)到屏幕上的某個(gè)隨機(jī)位置,那豈不是很好?可惜沒(méi)有,不過(guò)我們可以自己來(lái)創(chuàng)建過(guò)程!就像內(nèi)置過(guò)程一樣,自己創(chuàng)建的過(guò)程會(huì)顯示在Procedures抽屜中,需要時(shí)可以隨時(shí)調(diào)用它。

具體來(lái)說(shuō),創(chuàng)建一個(gè)名為MoveMole的過(guò)程,讓地鼠在屏幕上移動(dòng)到某個(gè)隨機(jī)位置。游戲開(kāi)始時(shí)調(diào)用一次MoveMole過(guò)程,當(dāng)用戶(hù)成功地點(diǎn)擊到地鼠后,每秒鐘執(zhí)行一次該過(guò)程。

創(chuàng)建MoveMole過(guò)程

要理解地鼠如何移動(dòng),需要了解Android的圖形定位機(jī)制。Canvas(以及Screen)可以看作是由x(水平)坐標(biāo)和y(垂直)坐標(biāo)織成的網(wǎng)格,其左上角的(x,y)坐標(biāo)為(0,0)。 x坐標(biāo)向右為增大, y坐標(biāo)向下為增大,如圖3-4所示。一個(gè)ImageSprite的x、y屬性表示它左上角的位置,因此當(dāng)?shù)厥笪挥谄聊蛔笊辖菚r(shí),他的x和y值都是0。

{%}

圖 3-4 屏幕上Mole的位置——坐標(biāo)、高度和寬度信息,x坐標(biāo)及寬度以藍(lán)色表示,y坐標(biāo)和高度以橙色表示

為了將地鼠的移動(dòng)限制在屏幕之內(nèi),要確定x和y的最大值,這要用到地鼠Mole和畫(huà)布Canvas1的Width(寬度)及Height(高度)屬性。(地鼠的Width和Height屬性值與上傳的圖片的大小相同,而在創(chuàng)建Canvas1時(shí),你設(shè)置的高度是300像素,寬度為“Fill parent”,即等于它的“父”容器——屏幕的寬度。)如果地鼠圖片的寬度是36像素,畫(huà)布寬度是200像素,那么Mole的x坐標(biāo)最低可以為0(靠近屏幕左側(cè)邊緣),而最大為164(200 - 36,或Canvas1.Width - Mole.Width),這樣才能保證Mole不超出屏幕的右側(cè)邊緣。同樣,Mole頂部的y坐標(biāo)范圍可從0到Canvas1.Height - Mole.Height。

圖3-5顯示了創(chuàng)建的MoveMole過(guò)程,圖中標(biāo)有詳細(xì)注釋?zhuān)梢杂羞x擇地添加到過(guò)程中)。

為了隨機(jī)地放置Mole,x坐標(biāo)要在0到Canvas1.Width - Mole.Width的范圍內(nèi)選擇,同樣,y坐標(biāo)要在0到Canvas1.Height - Mole.Height的范圍內(nèi)。使用Math抽屜里的內(nèi)置過(guò)程random integer生成一個(gè)隨機(jī)整數(shù),將“from”參數(shù)從改默的1改為0,同樣修改“to”參數(shù),如圖3-5所示。

{%}

圖 3-5A MoveMole過(guò)程,用于將Mole放在一個(gè)隨機(jī)的位置上

按如下步驟創(chuàng)建過(guò)程:

1. 找到Procedures:?jiǎn)螕魤K編輯器中的Procedures抽屜;

2. 得到to procedure:在Procedures抽屜中點(diǎn)擊to procedure塊(不帶result的to procedure);

3. 設(shè)置過(guò)程名稱(chēng):?jiǎn)螕魤K中的文字“procedure”并輸入“MoveMole”;

4. 移動(dòng)Mole:?jiǎn)螕鬗ole抽屜,將call Mole.MoveTo塊拖到procedure塊中“do”的右側(cè);注意:我們還需要提供x和y的坐標(biāo);

5. 設(shè)定Mole的x坐標(biāo):如前所述,x坐標(biāo)范圍在0與Canvas1.Width - Mole.Width之間:

  • 點(diǎn)擊Math抽屜;

  • 拖出random integer from塊,將左側(cè)插頭(突起)插入call Mole.MoveTo塊的“x”插槽;

  • 點(diǎn)選from之后的數(shù)字1并輸入0;

  • 丟棄數(shù)字100:點(diǎn)擊該塊,再按鍵盤(pán)上的Del或Delete鍵,或直接拖入垃圾箱;

  • 點(diǎn)擊Math抽屜,將一個(gè)減法塊(-)拖入to插槽;

  • 點(diǎn)擊Canvas1抽屜,向下滾動(dòng)直到看見(jiàn)Canvas1.BackgroundColor ,將其拖入到減法塊“-”的左側(cè),然后從BackgroundColor所在的下拉菜單中選擇Width選項(xiàng);

  • 同樣,點(diǎn)擊Mole抽屜并拖入Mole.Enabled塊,然后從Enabled塊所在的下拉菜單中選擇Width選項(xiàng),并將它插入到“-”右側(cè)的插槽中;

6. 按類(lèi)似步驟設(shè)定y坐標(biāo),應(yīng)該是一個(gè)從0到Canvas1.Height - Mole.Height的隨機(jī)整數(shù);

7. 對(duì)圖3-5A(行內(nèi)輸入)或3-5B(外展輸入)檢查操作結(jié)果。

8. random integer from to塊的“external inputs”(外展輸入)方式:右鍵點(diǎn)擊random塊,選擇列表第三項(xiàng)external inputs;如果想恢復(fù)行內(nèi)輸入,右鍵點(diǎn)擊random塊,選擇inline inputs。

{%}

圖 3-5B MoveMole過(guò)程,用于將Mole放在一個(gè)隨機(jī)的位置上

在應(yīng)用啟動(dòng)時(shí)調(diào)用MoveMole過(guò)程

已經(jīng)完成了MoveMole過(guò)程,現(xiàn)在該調(diào)用它了。對(duì)于程序員來(lái)說(shuō),最熟悉的事情就是在應(yīng)用啟動(dòng)的同時(shí)執(zhí)行某些指令,塊Screen1.Initialize就是專(zhuān)為這個(gè)目的而設(shè)計(jì)的:

1. 點(diǎn)擊Screen1抽屜,并拖出Screen1.Initialize塊;

2. 單擊Procedures抽屜,你會(huì)看到一個(gè)call MoveMole塊(這很有趣:你自己創(chuàng)建了一個(gè)新塊,不是嗎?!)。把它拖入Screen1.Initialize,如圖3-6所示。

{%}

圖 3-6 在應(yīng)用啟動(dòng)時(shí)調(diào)用MoveMole過(guò)程

每秒鐘調(diào)用一次MoveMole過(guò)程

要讓地鼠每一秒移動(dòng)一次,需要用到Clock組件。設(shè)置Clock1的TimerInterval屬性為其默認(rèn)值1000(毫秒),即1秒,我們稱(chēng)每秒一次的計(jì)時(shí)為計(jì)時(shí)器的心跳。這意味著,在Clock1.Timer塊中,無(wú)論設(shè)定什么動(dòng)作,它都會(huì)隨著計(jì)時(shí)器的心跳,每秒鐘執(zhí)行一次。以下是具體設(shè)置:

1. 單擊Clock1抽屜,并拖出Clock1.Timer;

2. 單擊Procedures抽屜,將call MoveMole塊拖到Clock1.Timer塊中,如圖3-7所示。

{%}

圖 3-7 計(jì)時(shí)器開(kāi)始計(jì)時(shí)后,每次心跳(每秒)都會(huì)調(diào)用一次MoveMole過(guò)程

如果你覺(jué)得心跳得太快或太慢,可以在組件設(shè)計(jì)器中改變Clock1的TimerInterval屬性,來(lái)增加或減小地鼠的移動(dòng)頻率。

記錄成績(jī)

剛才我們創(chuàng)建了兩個(gè)Label:初始值為0的HitsCountsLabel和MissesCountsLabel,希望以此來(lái)記錄用戶(hù)的成績(jī):當(dāng)用戶(hù)命中Mole一次,或失敗一次(直接拍打到屏幕)時(shí),對(duì)應(yīng)Label中的數(shù)字增加,為此要用到Canvas1.Touched塊,它表示Canvas被觸摸到,并記錄了觸摸點(diǎn)的x和y坐標(biāo)(我們不必關(guān)心),以及是否碰到了sprite(這是我們關(guān)心的)。圖3-8顯示了即將創(chuàng)建的代碼。

{%}

圖 3-8 觸碰到Canvas1時(shí),讓命中(HitsCountLabel)或失?。∕issesCountLabel)次數(shù)遞增

圖3-8可以理解為:當(dāng)觸碰到canvas時(shí),檢查sprite是否也被碰到。應(yīng)用中只有一個(gè)sprite,即Mole,如果碰到Mole,則HitsCountLabel.Text中的數(shù)字+1,否則,MissesCountLabel.Text中的數(shù)字+1(如果沒(méi)碰到sprite,則touchedSprite的值為false )。

下面介紹如何創(chuàng)建這些塊:

1. 點(diǎn)擊Canvas1抽屜,并拖出Canvas1.Touched;

2. 單擊Control抽屜,拖出Ifelse塊(先拖入if塊,然后為其添加else塊:點(diǎn)擊if左邊的藍(lán)色方塊,在彈出框中將else塊拖入if塊),并放入Canvas1.Touched塊中;

3. 從Variables抽屜中拖出get塊,放入ifelse的if插槽內(nèi),選擇下拉菜單中的touchedSprite選項(xiàng);或者將鼠標(biāo)懸停在when Canvas.Touched塊的參數(shù)touchedSprite上,從中獲取get touchedSprite塊;

4. 按照我們的設(shè)想,如果if檢測(cè)成功(即Mole被觸摸到),則HitsCountLabel.Text遞增:

  • 從HitsCountLabel抽屜里拖出set HitsCountLabel.Text to塊并放入“then”的右邊;

  • 點(diǎn)擊Math抽屜,拖出一個(gè)加號(hào)(+),將其放在“to”插槽中;

  • 點(diǎn)擊HitsCountLabel抽屜,拖動(dòng)HitsCountLabel.Text塊到“+”的左邊;

  • 點(diǎn)擊Math抽屜,并拖動(dòng)一個(gè)“0”塊到“+”的右邊,將0改為1 ;

5. 在ifelse塊的“else”部分,對(duì)MissesCountLabel塊重復(fù)步驟4。

 測(cè)試:測(cè)試你的新代碼:在設(shè)備上觸摸Canvas,命中或錯(cuò)過(guò)地鼠,看看分?jǐn)?shù)有什么變化。

過(guò)程抽象

計(jì)算機(jī)科學(xué)的重要手段之一,就是命名然后調(diào)用一組指令(如MoveMole),這種能力被稱(chēng)為過(guò)程抽象。之所以叫做“抽象”,是因?yàn)檫^(guò)程的調(diào)用者(在實(shí)際項(xiàng)目中,很有可能不是過(guò)程的開(kāi)發(fā)者)只需要知道過(guò)程的功能(如移動(dòng)地鼠),而不需要知道過(guò)程的實(shí)現(xiàn)方法(生成兩個(gè)隨機(jī)整數(shù))。如果沒(méi)有過(guò)程抽象,不可能實(shí)現(xiàn)那些大型程序,因?yàn)樗鼈兊拇a量太大,對(duì)個(gè)人來(lái)說(shuō)是力所不及的,這一點(diǎn)與現(xiàn)實(shí)世界中的勞動(dòng)分工相類(lèi)似。例如,不同的工程師設(shè)計(jì)出汽車(chē)的不同部件,沒(méi)有人了解所有的細(xì)節(jié),而司機(jī)只需要了解接口(例如,踩下制動(dòng)踏板把車(chē)停下來(lái)),而無(wú)需了解如何實(shí)現(xiàn)這些接口。

與復(fù)制和粘貼代碼相比,過(guò)程抽象的優(yōu)勢(shì)在于:

  • 由于過(guò)程的代碼獨(dú)立于其它部分的程序,因此更易于對(duì)過(guò)程的測(cè)試;

  • 如果代碼中有錯(cuò)誤,只需要對(duì)局部進(jìn)行修改;

  • 如果需要改變過(guò)程的實(shí)現(xiàn) (或功能),如確保地鼠不連續(xù)出現(xiàn)在同一個(gè)位置,只需要修改一處的代碼;

  • 可以將過(guò)程匯集到一個(gè)程序庫(kù)中,以便在不同的程序中使用。(遺憾的是App Inventor暫時(shí)不支持這項(xiàng)功能。)

  • 將大塊代碼拆分成代碼片段,有助于對(duì)應(yīng)用做深入剖析,并加以實(shí)現(xiàn)(“分而治之”)。

  • 給過(guò)程一個(gè)有意義的命名,將有助于提高代碼的可讀性,更易被別人(或一個(gè)月后的自己)讀懂;

在后面的章節(jié)中,還將學(xué)到過(guò)程更加強(qiáng)大的功能:添加參數(shù),提供返回值,以及調(diào)用過(guò)程本身。有關(guān)內(nèi)容請(qǐng)參見(jiàn)第21章。

重置分?jǐn)?shù)

朋友看到你玩MoleMash,他可能也想試試身手,所以最好能讓成績(jī)歸零。根據(jù)前面學(xué)過(guò)的內(nèi)容,不經(jīng)提示你也有能力把它做出來(lái)。閱讀之前動(dòng)腦筋試試看。

我們要在ResetButton.Click塊中設(shè)置HitsCountLabel.Text和MissesCountLabel.Text的值為0。如圖3-9所示。

{%}

圖 3-9 按下Reset按鈕讓命中次數(shù)(HitsCountLabel)和失敗次數(shù)(MissesCountLabel)歸零

此處提供一個(gè)技巧,來(lái)快速建立ResetButton.Click的事件處理程序:在工作區(qū)直接輸入0并回車(chē),將生成數(shù)字塊0,等同于從Math抽屜中拖出。(這種輸入方式對(duì)其他塊也同樣有效。)

 測(cè)試:開(kāi)始游戲,嘗試多次命中及錯(cuò)過(guò)地鼠,然后按下“重新啟動(dòng)”按鈕。

添加觸摸地鼠行為

我們希望在觸摸到地鼠時(shí),設(shè)備能夠振動(dòng),這要用到Sound1.Vibrate塊。如圖3-10所示。

{%}

圖 3-10 碰到地鼠時(shí)讓設(shè)備短暫振動(dòng)(100毫秒)

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)