#

這一個章節將完成iPlayer的最後兩個功能,分別是「自動儲存播放清單」及「自動載入播放清單」,這兩個功能指的是每當有新的MP3加入播放清單時,iPlayer就會自動的把播放清單的內容儲存一份在硬碟裡面,當然的等到下次再重新啟動iPlayer時,我們就可以把之前的播放清單內容由硬碟裡載回來。如此,就不用每次啟動iPlayer時還要重加播放清單的內容了。

26.1 儲存播放清單的位置

其實播放清單儲存的位置,是一個可以好好考慮的問題。我們先假設這份播放清單是一件很重要的設定檔,如果隨便找到地方儲存,也許哪天重灌電腦時就很有可能會忘了備份他。另外,對於一些有系統潔癖的人來說,隨意的在磁碟裡留下一些檔案,也是非常不可以接受的事。那到底要儲存在哪裡才是適當的呢? 如果玩過Linux或Mac的朋友,那有很大的機會你會遵守「儲存在家目錄」的規則,事實上這可能是最好的方法。但對於在Windows上開發程式的朋友來說「家目錄」可能就覺得有點陌生了,不過不要緊家目錄的觀念很簡單,它可以解釋成每一位登入電腦的使用者所私人擁有的資料夾。

在Tcl裡家目錄的位置可以由全域變數$::env(HOME)來取得,::env陣列會在啟動Tcl程式時自動被建立,它裡面儲存了一些系統資訊,例如:環境變數、作業系統的版本、登入系統的帳號....等。有興趣的朋友可以去看看的Tcl文件,裡面有更詳細的說明,目前我們只要知道$::env(HOME)裡面儲存了家目錄的位置就可以了。下面是一個簡單的例子,它會印出家目錄的位置。

puts $::env(HOME)

正常的情況下,在Linux的平台你應該會看到「/home/你登入的帳號」像這樣子的輸出。在Mac的平台會輸出「/Users/你登入的帳號」。在Windows下則會輸出「C:/Documents and Settings/你登入的帳號」。 配合上面的觀念,接下來我們就規劃把播放清單儲存在家目錄下的playlist.txt檔案裡好了。

26.2 自動儲存播放清單

要完成這項功能我們要寫一個新的程序叫做pl_save,這個程序要做的功能很簡單,就是把目前播放清單中的項目逐一取出,然後儲存在指定的檔案裡。它實際的程式碼如下:

# 儲存播放清單的程序
proc pl_save {} {
 variable Priv
 variable pathTbl
 
 # 組合儲存播放清單的檔案路徑
 set fpath [file join $::env(HOME) playlist.txt]
 # 取出播放清單中所有的項目
 set allItems [$Priv(pl) children {}]
 # 開啟儲存播放清單的檔案
 set fd [open $fpath w]
 # 把播放清單中的項目逐一存入檔案
 foreach item $allItems {
  puts $fd $pathTbl($item)
 }
 # 關閉檔案
 close $fd
}

在這個程序裡,我們把播放清單裡的每一個MP3都以一行一個項目的方式儲存在playlist.txt裡面。pl_save完成後,我們還要在每次MP3加入播放清單時執行它一次,也就是說我們要修改pl_add程序,修改的方法是在pl_add的尾巴加上一行pl_save。

# 把MP3加入清單方塊
proc pl_add {} {
 variable Priv
 variable pathTbl
 
 # 跳出選擇資料夾的對話方塊
 set dir [tk_chooseDirectory -title "加入MP3"]
 
 # 判斷是否有選擇資料夾
 if {[file exists $dir]} {
   # 掃描選中資料夾裡的MP3
  foreach mp3 [glob -nocomplain -directory $dir -types {f} *.mp3] {
   # 把掃中的MP3加入清單方塊
   set item [$Priv(pl) insert {} end -text  [file rootname [file tail $mp3]]]
   # 記下item與完整路徑的關係
   set pathTbl($item) $mp3
  }
 } 
 #=================注意這邊!!
 # 儲存播放清單 
 pl_save
 #=================
}

Ok!!自動儲存的功能完成了,現在你可以試試看把一些MP3加入播放清單然後關掉iPlayer,再去家目錄下找看看應該會有playlist.txt這個檔案,裡面會有我們儲存的內容。

26.3 自動載入播放清單

剛剛我們寫的pl_save程序可以把播放清單裡的MP3逐一儲存到檔案裡。接下來要實作的pl_load程序剛好是反其道而行,這次我們要把剛剛儲存的檔案內容,以一次一行的方法讀回播放清單,它的程式碼如下:

#自動載入播放清單
proc pl_load {} {
 variable Priv
 variable pathTbl

 # 組合儲存播放清單的檔案路徑
 set fpath [file join $::env(HOME) playlist.txt]
 # 如果儲存播放清單的檔案不存在就離開程序
 if {![file exists $fpath]} {return 0}
 # 開啟儲存播放清單的檔案
 set fd [open $fpath r]
 # 一次讀取一行,直到檔案讀取完畢
 while {![eof $fd]} {
  # 讀一行到mp3變數
  gets $fd mp3
  # 如果mp3變數儲存的檔案不存在,就執行下一圈
  if {![file exists $mp3]} {continue}
  # 把MP3加入清單方塊
  set item [$Priv(pl) insert {} end -text  [file rootname [file tail $mp3]]]
  # 記下item與完整路徑的關係
  set pathTbl($item) $mp3
 }
 # 關閉檔案
 close $fd
}

為了讓這個程序可以正常的運作,我們還要在iPlayer啟動時執行一次這個程序,方法是在init程序執行它:

# init 程序用來處理所有程式初始化的工作
proc init {} {
 variable snd
 variable currentFile
 variable Priv
 
 # 載入snack 套件
 package require snack
 # 建立播放物件
 set snd [::snack::sound]
 
 # 執行圖形介面初始化
 gui_init

 #======================注意這邊!!
 # 載入之前儲存的播放清單
 pl_load
 #======================

 #這個變數用來記錄播放狀態
  set Priv(State) "STOP"
 
  # 把清單方塊綁定一個「滑鼠點兩下的事件」,當事件發生時就執行play程序
  bind $Priv(pl) <Double-Button-1> {::iPlayer::play}
 
}

大功告成!! 現在iPlayer會自動載入播放清單內容了,請大家自己試試吧!!

26.5 結論

我們好不容易完成了一個MP3播放程式,雖然它的功能不是很完善,也還有很多可以改進的地方,不過以訓練寫程式來說,這次的iPlayer實作,應該是有達到某種程度的效果了。另外,老實說iPlayer的外觀真的是醜了點,不過我想改改外觀對你來說並不會是太大的困難才對,有興趣的朋友可以試著對iPlayer修修改改,把它改成更符合你個人使用的程式。希望大家玩的愉快~

4 個意見

匿名 | 2013年10月6日 下午3:00

Hi Dai 大大,

感謝你這些豐富的教學資源,的確很容易讓人感受到Tcl/Tk的撰寫效率,根據你這幾篇的實作過程,有幾個問題想請教一下

1. 第四篇的 Priv(pl) selection set [lindex $allItems 0] 應該前面少了一個$,實測會出現錯誤,(download的source code裡面是正確的)
但同時衍生了一個問題:什麼時候要用$,什麼時候不用?
像是 set chk [::ttk::checkbutton $fme.chkLoop -text "循環 " -onvalue 1 -offvalue 0 -variable ::iPlayer::Priv(loop)] 裡面的::iPlayer::Priv(loop)
,因為它控制的是變數本身而不是值,所以不用嗎?

2. 呼叫volume那邊的proc很有趣,我一直在看是誰傳遞了vol給這個函數,因為呼叫它的是 -command {::iPlayer::volume}],但是後面沒有接參數,
看起來是-value [::snack::audio play_gain]把值帶到command裡面,但是這樣的用法讓然覺得很奇怪,畢竟-command{ }裡面可以有多個動作或函數,
在我沒指定參數的時候,程式怎麼知道要把value當參數丟進去command裡的哪個或哪些函數當作參數?

3. 我在手打的過程中常常會漏掉namespace最前面的::,但是從來沒發生問題過,有"::"這個跟沒有差在哪邊?

4. pl_clear也那邊可以改動一下,不然按clear了,關閉後重新打開程式,playList還是存在

dai | 2013年10月7日 上午9:59

Hi,

1. 確實少了一個$,已修正了謝謝!! (也許你是第一個真的動手寫並測試這個例子的朋友,呵~)
2. $號+變數名稱是取值(代換)的意義,而checkbutton 的 -variable 選項,其用法是要將核取的結果,連結(即時設定)到指定的變數上所以只需要指定變數的名稱。
3. -command {::iPlayer::volume}] 的值並不是由 -value [::snack::audio play_gain] 帶進去的,事實上是由::ttk::scale帶進去的,::ttk::scale被使用者拉動時,會執行-command選項內的命令,並把目前::ttk::scale的目前值當成參數帶入-command選項內指定的命令。
4.namespace最前面加上「::」是表示由頂程的namespace開始,若沒有「::」則表示由目前的namespace開始,差別會在程式執行多層namespace時顯示出來。
5.pl_clear是該在最後再加上pl_save較好,這....就留給有興趣的朋友自行改進了~

匿名 | 2013年10月8日 晚上10:10

Hi Dai 大大,

感謝你的解答,不好意思又要繼續多問幾個問題~

1. 關於變數的應用,從範例四看到下面的部分,一開始只覺得明明Priv(loop)就在上面,但為甚麼下面的-variable ::iPlayer::Priv(loop)要寫全路徑的變數名稱,
結果發現如果不這樣寫,指血Priv(loop),跑的時候沒錯誤訊息,但是變數會綁不上去!

set Priv(loop) 0
set chk [::ttk::checkbutton $fme.chkLoop -text "循環" -onvalue 1 -offvalue 0 -variable ::iPlayer::Priv(loop) ]

2. 關於-command {::iPlayer::volumne},測試了一下,的確是scale自己invoke時把值傳進去,而且拉動的時候,只允許接一個function,裡面寫多個不行
3. 上網查了一下,英文底子有點差,原本想像C++一樣有using namespace xxx;的功能直用"namespace inport ::ttk"引進當前namespace,藉此來縮短coding上的重複性,但是好像TK不允許這麼做,不知道大大有沒有其他建議的方式?

dai | 2013年10月10日 下午1:19

1. -variable 設定的變數名稱,會在頂層的namespace被設定,所以若寫 -variable Priv(loop) 相當於 -variable ::Priv(loop)

2. namespace import 的語法如下:

namespace import ?-force? ?pattern pattern ...?

* Imports commands into a namespace, or queries the set of imported commands in a namespace.

留下您的意見

Theme Design by devolux.org. Converted by Wordpress To Blogger for WP Blogger Themes. Sponsored by iBlogtoBlog
This template is brought to you by : allblogtools.com | Blogger Templates