一個線上音樂軟體的故事(五、讓我們開始寫程式碼吧)
讓我們開始寫程式碼吧
現在有了明確的功能需求,幾乎克服了所有的技術障礙,那麼就可以開始動手編寫這個音樂播放軟體了。
一、組織專案結構
這個故事所講的線上音樂播放軟體並沒有很複雜的功能需求,介面數量很少,沒有資料庫操作。這樣的專案幾乎可以任意組織程式碼檔案,甚至可以沒有任何結構,把所有的程式碼都儲存在同一個目錄中,但我們依然希望能有一套便於組織和維護的專案結構。
我們把專案劃分為:專案配置、協議分析、音樂播放、其他獨立程式碼、使用者介面與控制這幾個程式碼包,他們各自負責各自的功能,相對獨立。因為專案比較簡單,就沒有將View層與控制層再進行區分,所以在使用者介面與控制包中可以看到很多應該在控制層的程式碼。
專案配置程式碼包和其他獨立程式碼包中的程式碼非常簡單,特別是前者,僅存放一些常量配置資料,比如:網路請求的超時時間、緩衝區大小、各種資原始檔的儲存路徑、搜尋音訊資源所用到的各種請求路徑等。其他獨立程式碼包中是一些功能完全獨立的函式,如:是否在py2exe編譯環境中執行、專案的主目錄位置和一些其他的函式。
在config和util兩個程式碼包目錄中可找到對應的原始碼檔案。
二、獲得音樂資料,分析協議
在音樂從哪裡來那一節中已經說明了通過什麼方式獲得音樂資料,但並沒有介紹如何實現他們。我在協議分析程式碼包中實現這些功能,這裡有個名叫TencentProtocol的類,所有與獲取音樂資訊有關的操作都在這裡。
騰訊QQ音樂的所有請求響應資料都是按JSON格式傳輸的,因此在這個類中有個獨立的classmethod用於發出請求獲取JSON文字。而資訊獲取的第一步是通過關鍵字搜尋,這是一個網路操作,存在一定的等待時間,為了不影響GUI介面的操作,同樣要用到多執行緒,所以TencentProtocol這個類從QThread繼承,在run()介面方法中啟動搜尋發出JSON請求,獲取JSON文字。接著我們就能從JSON物件中獲取我們所需的資訊。
def search_song(self):
"""
搜尋歌曲
"""
song_list = []
self.exception_list = []
try:
_url = self.qq_searcher(self.keyword, self.page_index,
self.page_size)
json_string = self.json_request(_url)
json_string = json_string[9:len(json_string) - 1]
song_list_json = json.loads(json_string)
for song in song_list_json[u'data'][u'song'][u'list']:
song_info = SongInfo()
song_info.album_id = int(song[u'albumid'])
song_info.album_mid = song[u'albummid']
song_info.album_name = self.decode_korean(song[u'albumname'])
song_info.id = int(song[u'songid'])
song_info.mid = song[u'songmid']
song_info.name = self.decode_korean(song[u'songname'])
song_info.interval = int(song[u'interval'])
song_info.length = seconds2time(int(song[u'interval']))
song_info.pub_time = int(song[u'pubtime'])
song_info.url = song[u'songurl'] if u'songurl' in song else ''
song_info.nt = int(song[u'nt'])
song_info.singer = []
song_info.singer_names = ''
singer_names = []
for sg in song[u'singer']:
name = self.decode_korean(sg[u'name'])
singer = {
'id': int(sg[u'id']),
'mid': sg[u'mid'],
'name': name
}
singer_names.append(name)
song_info.singer += [singer]
song_info.singer_names = u','.join(singer_names)
song_list += [song_info]
except BaseException as e:
e.message += u"搜尋音樂資訊錯誤。"
self.exception_list.append(e)
self.song_list = song_list
self.emit(SIGNAL('search_complete()'))
在獲取音樂資訊的過程中要注意,搜尋結果所返回的JSON文字中可能包含非中文的文字編碼,其中需要特殊處理的是韓文編碼。為了能在JSON中傳輸韓文文字,韓文會被編碼為ꨤ這種格式的Unicode碼,其中“&#”為字首“;”為字尾,一個這樣的編碼結構為一個韓文文字,所以我們需要一個函式來解碼並正確顯示韓文。其實無論哪種文字,只要是這樣的編碼結構,這個函式都能解碼。
@classmethod
def decode_korean(cls, string, prefix='&#', postfix=';'):
"""
韓文解碼函式
:param str string: 需要解碼的韓文 Unicode 資料
:param str prefix: 韓文 Unicode 編碼的開始符號
:param str postfix: 韓文 Unicode 編碼的結束符號
:return: 解碼後的 UTF-8 文字內容
"""
exp = prefix + r"(\d{5}?)" + postfix
code_list = re.findall(exp, string)
for code in code_list:
u_code = u'{0}'.format('\u' + hex(int(code))[2:6])
word = u_code.decode('unicode-escape')
string = string.replace(prefix + code + postfix, word)
string = string.decode('utf-8')
return string
在搜尋函式中還需要完成一項重要任務,就是通知別的執行緒搜尋已經完成,但是否存在錯誤需要其他執行緒自己檢查。這個通知任務也就是執行緒間通訊的主要目的,因為搜尋是在一個獨立的執行緒中執行的,當搜尋完成之後,主執行緒或其他啟動執行緒並不知道,我們需要發出一個訊號通知其他正在執行的執行緒,搜尋任務已經完成,可以繼續執行後續動作。
在搜尋函式的最後一行可以看到,通過呼叫QThread的emit()方法發出一個訊號,這個訊號的名稱是search_complete(),並且這個訊號不帶任何引數。如果這個訊號帶有引數,那麼需要在訊號名稱中指定引數的型別,比如search_complete(int),並且要在emit方法中提供這個引數值,寫成類似這樣的結構:
self.emit(SIGNAL('search_complete(int)'), 10)
實際範例可以在這個類的下載函式中找到。這樣其他執行緒可以通過槽連線到這個訊號,來捕捉這個訊號,一旦捕捉到這個訊號,槽中的函式就被呼叫執行,也就意味著後續動作開始了。在PyQt、PySide中GUI元件也是通過“訊號-槽”的方式來傳遞事件訊號的。
TencentProtocol類除了包含搜尋功能,還包含獲得音樂專輯封面圖片地址、獲取音樂源地址的功能,總的來說都是傳送請求,分析響應結果的過程,就不再逐一介紹了。關於執行緒內部的異常,建議不要嘗試丟擲,推薦的做法是把異常儲存下來,留給其他執行緒去捕捉處理。
搜尋功能的最終產出是一個儲存SongInfo物件的列表,在搜尋函式的倒數第二行可以看到。有了這些SongInfo物件,就能在後續的操作中載入音訊資料。但是這裡我們首先要分析一下SongInfo類,對於這個類我們有一定的要求。
三、儲存分析結果,在軟體中傳遞
搜尋結果中的音樂資訊是儲存在JSON物件中的,JSON物件實際上就是dict物件,通過key來獲取有用的音樂資訊。由於獲取的音樂資訊要在軟體的不同位置多次傳遞,為了防止輸入的key名稱錯誤,最好避免直接使用dict物件儲存資料。因此編寫一個叫做SongInfo的類,用於儲存音樂資訊。
實際上這個類非常簡單,只要一些基本屬性就可以了,但這個類最好能保留dict的所有特點,能動態增加資料項,又能有固定名稱的屬性,而且這些屬效能夠很方便增加,因為在開始動手編碼時並不能確定SongInfo究竟需要多少屬性。
這就需要用到Python的元類技術。學過資料庫管理的都知道,資料庫中有元資料的概念,元資料就是用來描述資料的資料,你可以理解元類就是描述類的類,基於這麼相似的兩個特性,元類設計思路也常常用在Python資料庫操作的ORM對映中。
元類能改變類的屬性型別、屬性數量、以及這些屬性的初始值。當你通過 __metaclass__ 屬性為類A設定了元類,那麼在載入A類時(注意不是類例項化物件的時間,要比這個更早)會首先執行元類的初始化動作,以明確A類應當具有哪些屬性,這些屬性的初始值是什麼。元類都是從type繼承下來的,通常會重寫type類的 __new__ 方法,來加工類A的屬性或方法,可以參考下面的程式碼:
class SongMetaclass(type):
"""
用元類的方式初始化音樂資訊類,自動增加對應的屬性
"""
@classmethod
def __new__(mcs, *more):
class_name = more[1] # type: str
super_classes = more[2] # type: tuple
attributes = more[3] # type: dict
mappings = dict()
for k, v in attributes.items():
if isinstance(v, SongInfoTag):
mappings[k] = v
attributes.pop(k)
attributes['__mappings__'] = mappings
return type.__new__(mcs, class_name, super_classes, attributes)
注意 __new__ 方法的引數 *more,這是一個元組型別的引數,其次序和含義是固定的,分別是:類名稱、父類元組、屬性字典,我們要實現的功能是將SongInfoTag型別的屬性放在mappings中,並把他們從屬性字典中刪除,因為我們要的並不是屬性,而是和字典一樣的資料項。
現在編寫SongInfo類的思路就清晰了,首先從dict繼承,設定__metaclass__屬性為 SongMetaclass,然後在 __init__() 方法中遍歷 mappings 列表,逐個建立資料項,並用None初始化這些資料項,最後重寫 __getattr__() 和 __setattr()__ 兩個方法,首先從字典中讀寫資料,如果沒有找到再嘗試直接操作物件的屬性資料。
對於我們需要的音樂資訊屬性,只要增加SongInfoTag型別的屬性即可,這些屬性在物件初始化過程中會被替換為None資料。這樣我們就不用像操作dict那樣使用key名稱讀寫資料,只要通過點操作符就能讀寫屬性資料,能在一定程度上避免出錯。
在protocol程式碼包中可以找到SongInfo.py和TencentProtocol.py兩個原始碼檔案,以上所講內容在這兩個程式碼檔案中都有對應實現。
四、載入和播放音樂
前面已經講了很多播放音樂時應該注意的問題,但是並沒有詳細實現,現在就要來完成這部分工作。
按照前面的分析,我們應該首先解決音樂載入的問題,因為音樂並不總是從網路載入,對於已經播放過的並且成功快取的音樂應該從本地載入,只有沒有播放過的或快取失敗的音樂才需要從網路載入。
因此在player程式碼包中有個稱為音樂裝載器的類AudioLoader專門負責載入音樂檔案,無論是從網路載入還是從本地載入,都是由它負責。但是要使用這個類,必須先有一個SongInfo物件,然後才能通過音樂裝載器載入對應的音樂檔案。前文已經介紹了通過關鍵字搜尋可以獲得一批SongInfo物件,其實還有其他的方式獲得SongInfo物件,後面會講到。
既然AudioLoader類要負責從檔案或網路載入音訊資料,顯然AudioLoader類也必須支援多執行緒操作,所以也是從QThread繼承。這個類的source_type屬性用於區分從網路載入資料,還是從檔案載入資料。由AudioLoader的run()方法要負責判斷,並呼叫不同的載入方法載入資料。
def run(self, *args, **kwargs):
if self.source_type == AUDIO_FROM_INTERNET:
self.cache_from_url()
elif self.source_type == AUDIO_FROM_LOCAL:
# 從本地快取讀取音訊檔案時要求 song_info 必須具有 file_path 鍵值
self.cache_from_local()
從本地檔案載入音訊資料的操作比較簡單,直接通過AudioSegment.from_file()方法就能完成載入動作。這裡以從網路裝載為例,說明載入和快取的過程:
def cache_from_url(self):
"""
從一個 URL 地址獲取音樂資料,並快取在臨時目錄中
實際裝載的過程是首先檢查快取目錄中是否存在有效的音樂副本和封面圖片副本
如果有,就直接從快取播放,否則從網路下載,並快取
:return: 返回快取的臨時檔案物件
"""
self.emit(SIGNAL('before_cache()'))
self.is_stop = False
self.exception_list = []
try:
if self.song_info.song_url is None:
tencent = TencentProtocol()
tencent.get_play_key(self.song_info)
tencent.get_song_address(self.song_info)
tencent.get_image_address(self.song_info)
if tencent.has_exception:
self.exception_list += tencent.exception_list
raise tencent.exception_list[0]
self.image_data = self.check_image_cache()
if not self.image_data:
"""
從網路讀取專輯封面,並寫入本地快取檔案
"""
self.image_data = \
requests.get(self.song_info.image_url).content
cache_image_path = QM_DEFAULT_CACHE_PATH + \
str(self.song_info.mid) + '.jpg'
if os.path.isfile(cache_image_path):
os.remove(cache_image_path)
with open(cache_image_path, 'wb') as cover_file:
cover_file.write(self.image_data)
cache_audio = self.check_audio_cache()
if isinstance(cache_audio, AudioSegment):
"""
從快取載入音訊
"""
self.audio_segment = cache_audio
self.emit(SIGNAL('caching()'))
else:
"""
從網路快取音訊,並寫入本地快取
"""
request = Request(self.song_info.song_url)
pipe = urlopen(url=request, timeout=QM_TIMEOUT)
cache_file = QM_DEFAULT_CACHE_PATH + \
str(self.song_info.filename)
if os.path.isfile(cache_file):
os.remove(cache_file)
with open(cache_file, 'wb') as audio_file:
while True:
data = pipe.read(QM_BUFFER_SIZE)
if self.is_stop or data is None or len(data) == 0:
audio_file.close()
break
audio_file.write(data)
sleep(0.01)
self.audio_segment = \
AudioSegment.from_file(audio_file.name)
self.emit(SIGNAL('caching()'))
audio_file.close()
except RuntimeError as e:
e.message += u"執行時錯誤。"
self.exception_list.append(e)
except BaseException as e:
e.message += u"獲取音樂資料錯誤。"
self.exception_list.append(e)
self.is_stop = True
self.emit(SIGNAL('after_cache()'))
這個方法先檢查SongInfo物件的資訊是否完整,如果不完整,則補全播放鍵、音樂地址、專輯封面圖片地址等資訊。然後檢查是否存在專輯封面圖片快取,有則從網路讀取,並快取。
音樂檔案也是這樣,先檢查快取然後從不同的地方載入資料。從本地快取載入只要一次就能載入所有資料,所以只會發出一次caching()訊號,也只會產生一個用於儲存音訊資料的AudioSegment物件。
從網路載入時要根據緩衝區的大小多次載入,再分別寫入快取檔案,這裡不再使用臨時檔案儲存快取資料,而是在引數配置中設定一個目錄位置,專門用於儲存快取資料,每首音樂都會建立一個快取檔案,同時還會快取與音樂對應的專輯封面圖片。快取過程中會多次讀取網路流資料再追加寫入快取檔案,所以會多次發出caching()訊號。也將會產生多個AudioSegment物件,且每次這個物件都會從快取音訊檔案中載入所有資料,以便送給播放器物件播放。載入結束之後裝載器會發出after_cache()訊號,通知其他執行緒載入工作已經完成。但是否存在異常要通過檢查exception_list才能知道。
播放音樂並不需要等到發出after_cache()再開始播放,只要收到caching()訊號,就能確認快取資料已經存在,就可以呼叫Player類的方法開始播放音樂。
在上面介紹播放進度的時候,我們已經看到了Player類的play()方法,這裡不再介紹播放函式,而是要介紹播放器類載入播放資料的過程。
Player類有兩個屬性重要,一個是audio_segment,另一個是start_position,這兩個屬性分別對應於播放資料物件和播放開始的時間,當你為播放器設定這兩個引數時會分別呼叫不同的設定方法,用於設定待播放的chunks。這裡我們要重點介紹的是setup_chunks_for_cache()函式。
def setup_chunks_for_cache(self):
"""
從檔案緩衝中載入要播放的 chunks
由於快取檔案在不停的變化,因此要記錄下累計從快取中載入了多少資料
下次快取訊息發出的時候,從已經載入的資料位置開始繼續載入
:return: 快取載入的狀態
"""
if not self.is_valid:
return False
start = self.loaded_length
length = self.duration - self.loaded_length
self.loaded_length += length
# 建立要播放的 chunk
play_chunk = self.audio_segment[start * 1000.0: \
(start+length) * 1000.0] - (60 - (60 * (self.volume / 100.0)))
self.chunks += make_chunks(play_chunk, self.chunk_duration * 1000)
return True
從這個函式可以看出,每次從快取檔案載入音訊資料,都會記錄下載入的總量,下次再通過快取檔案載入時,將從上次快取的結尾處開始讀取音訊資料。然後將資料加到self.chunks的結尾,由播放函式負責寫入音效卡資料流。
可以看到通過AudioLoader和Player兩個類的配合使用,就能完成對音訊的載入、快取、播放這幾個操作。
五、構建簡約時尚的GUI介面
還是根據那張圖片來構建軟體的GUI介面,從圖片上可以看出來,這個介面主要分為頂部綠色搜尋區、左邊面板選擇按鈕區、中部音樂列表面板區和底部深色的音樂播放控制區這幾個主要的區域。
每個面板都是從PySide的QFrame元件繼承而來,QFrame和QWidget、QApplication一樣都支援layout操作,但是與QWidget不一樣的是,QFrame可以直接設定背景色。
在QFrame上新增UI元件的方式很簡單,一般來說會先為QFrame設定layou元件。我們稱為佈局元件。PySide支援很多佈局方式,水平方向佈局元件(QHBoxLayout)、垂直方向佈局(QVBoxLayout)、表單佈局(QFormLayout)、網格佈局(QGridLayout)這些佈局元件能讓你在設計GUI介面的時後非常方便快捷。這裡我們不打算詳細介紹如何使用這些佈局元件,不過如果你是Java Swing的使用者,這些佈局對你來說就非常熟悉了。
回到我們的軟體,前面已經說過,我們把面板分解為幾個區域,現在我們就來說說每一個區域的佈局,頂部和底部一樣,都是使用的從左到有的水平方向的佈局,所以我們為QFrame設定的是QHBoxLayout,然後向layout新增按鈕、文字框元件即可。
但是仔細觀察頂部和底部的面板會發現,面板上的按鈕分為左邊區域和右邊區域。這裡有個小技巧,當你希望在水平佈局時一部分元件放在左邊,另一部分放在右邊,那麼在兩個區域的中間你可以想象為需要一個彈簧,把元件往面板的兩邊頂。這在PySide中很簡單,只要呼叫一次layout.addStretch(1)方法就可以了。
左邊的面板是按照垂直方向,從上到下進行排列布局,只要為QFrame設定QVBoxLayout,然後再為面板新增按鈕就能實現這樣的排列布局。
中部的列表區域與其他區域不一樣,不是在QFrame上直接新增元件,而是首先新增QStackedWidget元件,然後在這個元件中新增多個QFrame面板。QStackedWidget的特點是它可以擁有很多Widget元件,但是每次只顯示其中的一個,我們需要通過左邊按鈕面板上的按鈕來控制QStackedWidget應該顯示哪個面板。
通過PySide的layout佈局方式構建像上面圖片那樣的軟體介面並不是很麻煩,可以說很簡單。但軟體介面上還有很多Icon、圖片等輔助資源,這些資源必須事先準備好,並顯示在正確的位置上,否則一個只有文字的軟體介面是很枯燥無味的。
這裡需要說明PySide支援的圖示格式比較豐富,但是我們用的比較多的是PNG和SVG兩種,這兩種圖示都能很好地支援Alpha通道的透明部分,其中PNG是點陣圖,放大縮小PNG最好不要幅度太大,否則變形會比較嚴重。而SVG是XML格式描述的圖片,支援向量變化,縮放比例可以很大且不失真。SVG的另外一個好處是小,因為是用文字描述的,在影象結構不復雜的情況下(通常Icon圖示都不會太複雜),檔案會非常小。可以檢視專案資源目錄中icons目錄中的檔案,幾乎所有的Icon都是SVG格式的。
無論是使用PNG格式還是使用SVG格式,建立一個圖示的方法都是一樣的:
icon = QIcon(QM_ICON_PATH + 'qq_music_sm.png')
要在修改窗體圖示可以使用:
QMainWindow.setWindowIcon(icon)
要在按鈕上應用圖示可以使用:
QPushButton.setIcon(icon)
當我們通過layout完成佈局,併為視窗、按鈕都新增好圖示之後,我們得到的介面可能是這樣的:
因為我們還沒有應用樣式,所以很多與尺寸、顏色、滑鼠等有關的特效都無法表現出來。在“簡約時尚的介面”那一節已經知道如何設定元件的CSS樣式,現在就是它大顯身手的時候了!
通過前面的章節知道可以為每個元件單獨設定樣式,不過作為一個完整獨立的軟體,分別設定每個元件的樣式,還是太麻煩了,我希望能像HTML那樣,直接匯入一個CSS樣式表文件,應用在整個專案上,各個元件可以像HTML元素那樣設定class屬性,來設定各自元件的樣式檔案。
幸好PySide具有這樣的功能,而且實現比較簡單。只要從檔案讀入所有的CSS文字內容,通過下面的程式碼應用在主視窗上就可以了:
qss = self.load_qss(QM_QSS_PATH)
self.setStyleSheet(qss)
上面一行是從檔案讀入所有的CSS內容,下面一行是將讀取到的所有文字內容設定為主視窗的樣式表。
接著,我們就針對有需要用到樣式的元件單獨設定屬性,就像設定HTML元素的class一樣,來看看這樣的程式碼怎麼寫:
self.btn_playlist.setProperty('class', 'highlight_button')
只要這樣設定就可以了,但是需要注意,這裡的class並不是PySide指定的,你可以任意指定,只要和css檔案中的樣式名稱一致就行。比如上面的屬性設定,對應的樣式表宣告是這樣的:
LeftPanel QPushButton[class="highlight_button"],
LeftPanel QToolButton[class="highlight_button"] {
height: 32px;
width: 140px;
border: none;
padding-left: 10px;
padding-right: 10px;
border-radius: 5px;
text-align: left;
color:#555555;
}
可以看到屬性名稱宣告是class,屬性值是highlight_button。這段宣告的實際含義是:應用在所有LeftPanel例項物件的QPushButton和QToolButton物件上,並且這些按鈕物件要具有名為class值為highlight_button的屬性。在CSS檔案中,還能看到很多其他的樣式宣告,但都會有屬性名稱和屬性值的宣告。
還有一種宣告是狀態宣告,在對音樂表格樣式宣告的時候用到了,樣式宣告是這樣的:
SongTable::item::selected {
background-color: #DDEEDD;
}
這段宣告是表示對於所有SongTable的例項物件的元素,只要被選了,那麼他們都會應用這個樣式宣告。
當為所有的元件都設定了樣式屬性,並在應用程式啟動時讀入所有樣式文字,設定為應用程式的樣式,就能看到像第一張圖片那樣的介面。
六、讓各部分協同工作
上一小節介紹瞭如何把各個區域的面板構建出來,現在我們希望能讓各個區域能協同工作。先來看一看個部分應該負責的工作:
頂部面板:負責接受使用者輸入,點選搜尋按鈕後執行搜尋功能;顯示等待動畫。
左側面板:負責切換中部區域的表格。
中部面板:負責展示不同的面板,表格或別的內容,暫時並無其他內容。
底部面板:負責控制裝載器和播放器,實現音訊播放和控制。
搜尋面板:負責執行搜尋功能,並展示搜尋結果。
快取面板:負責從本地讀取歷史快取檔案並顯示,同時顯示最新快取記錄。
下載面板:負責從本地下載目錄讀取歷史下載檔案並顯示,同時顯示最新下載記錄。
這裡並不對所有的面板功能都作相信說明,只對:搜尋面板、底部面板、主程式介面進行說明。
1. 搜尋面板(SearchPanel)
首先需要說明的是搜尋面板,搜尋面板負責執行搜尋,顯示搜尋結果。在前面獲得音樂資料、分析協議小節中已經詳細說明了音樂資訊的搜尋過程,那些程式就是在這裡呼叫的。所有在這個面板中最重要的就是建立TencentProtocol類的物件,並執行start()介面方法啟動搜尋過程。
def search(self):
"""
通過關鍵字搜尋歌曲
注意會啟動新的執行緒進行搜尋,通過 tencent 物件的訊息捕獲搜尋結果
"""
self.emit(SIGNAL('before_search()'))
self.tencent.keyword = self.keyword.encode('utf-8')
self.tencent.page_index = self.page_index
self.tencent.page_size = self.page_size
self.tencent.start()
就像上面這樣,設定好關鍵字、頁碼、每頁顯示的資料量這些引數之後就可以啟動搜尋,在初始化self.tencent的過程中已經為self.tencent連線了search_complete()訊號,每當搜尋完成之後就會執行槽方法:
self.tencent.connect(SIGNAL('search_complete()'), \
self.search_complete)
從繫結的過程可以看出來,一旦搜尋完成,self.search_complete方法就會被執行:
def search_complete(self):
"""
搜尋執行緒執行完畢之後觸發該訊息
清空表中內容,重新填充
如果存在異常則清空資料,顯示訊息
"""
if not self.tencent.has_exception:
self.song_table.fill_data(self.tencent.song_list)
else:
self.song_table.fill_data([])
txt = u'搜尋錯誤'
message = ''
for e in self.tencent.exception_list:
message += e.message
QMessageBox.warning(self, txt, txt+u'。錯誤訊息:'+ \
message, QMessageBox.Ok)
self.emit(SIGNAL('after_fill()'))
這裡要注意就像前面我們說過的一樣,在處理子執行緒內部的異常時,並不是直接在子執行緒中丟擲,而是將之存在一個列表中,當搜尋執行緒執行完畢之後,我們檢查是否存在異常,如果存在,我們需要通知使用者發生的具體異常是什麼。這樣操作的缺點是不利於除錯,如果沒有好的除錯工具,就無法展示一些異常資料,也可以考慮增加異常訊息日至檔案,將所有的異常訊息都儲存在日至檔案中。
搜尋完成之後要處理的工作很簡單,就是將搜尋結果在列表中顯示出來。這裡需要一張表格,用於顯示搜尋到的所有音樂資訊。
回憶一下我們在大多數其他播放器中的操作,在音樂列表上雙擊一首歌,就能立即播放這首歌。當我們選擇了一些歌曲就能新增到播放列表或下載這些選擇的歌曲。我們也要實現這些功能。
考慮到軟體中多次使用到顯示音樂資訊的表格,而且主要用來顯示音樂資訊,且要支援選擇行、雙擊、行背景顏色變化等功能。引出需要建立一個表格元件,從QTableWidget繼承,用於實現我們想要的功能
這個新的表格類是SongTable在view程式碼包中,每當雙擊一個單元格都會發出cell_double_clicked()訊息,同時SongTable的active_song_info屬性會隨之改變為正在雙擊的音樂資訊,這樣便於我們取得音樂資訊,也便於我們呼叫底部面板上的播放功能播放音樂。同時還為這個表格設計了列頭資訊宣告類,用於宣告列的標題(title)、對應SongInfo的屬性(filed)、寬度(width)、對齊方式(alignment)、是否顯示為選擇框(is_checkbox)等等一些輔助資訊。
在表格中展示SongInfo資訊列表時,是通過屬性名稱讀取資訊的,這很容易解釋我們的SongInfo類為什麼要從dict繼承並使用元類,因為我們既需要能夠通過點方式讀取屬性,也需要能夠通過key名稱的方式讀取屬性。
這個表格能幫助我們把音樂資訊顯示的很好,可以通過選擇框選擇想要的音樂,能夠發出雙擊訊號,這樣就滿足了我們的需求。
2. 底部面板
底部面板的主要作用是根據SongInfo播放音樂,需要用到AudioLoader和Player這兩個類。另外這個面板還有一個重要的屬性,就是song_list這是SongInfo列表,儲存的是所有需要播放的音樂資訊。在播放之前這個屬性必須先設定好,但並非設定好這個屬性就立即開始播放,必須通過設定song_index屬性,來指定要播放的音樂,一旦設定了這個屬性,裝載器loader(AudioLoader)就知道應該狀態哪首歌曲,裝載過程中會發出一系列的訊號,這些訊號將會一步一步通知面板該做什麼,比如:設定播放面板的資訊、呼叫播放器播放音樂等。播放器player(Player)開始播放音樂之後也會發出一系列訊號,通知面板該做什麼,比如:不斷修改播放進度和播放的當前時間資訊等。
底部面板上的上一首、下一首按鈕只是修改song_index屬性的值,通知loader和player應該播放哪首歌曲。這裡需要同時關注右邊的三個按鈕,他們是播放時切換按鈕,有三種狀態,分別是:列表迴圈、單曲迴圈、隨機播放,這三種狀態只是用於區分該如何取得下一首歌曲的索引進而修改song_index屬性。
3. 主程式介面
主程式介面負責將所有的面板按照佈局要求放在QApplication介面上,併為各面板的主要元件連線訊號-槽,以便讓各個面板元件協同工作。
如:當收到頂部搜尋按鈕的點選訊號時,需要設定搜尋面板的關鍵字、頁碼、每頁顯示資料量等引數,並調用搜索麵板的搜尋功能完成搜尋。
當搜尋面板的表格收到雙擊訊號時,需要修改底部播放面板的播放列表為搜尋列表,並設定播放索引為當前的歌曲索引,以便裝載器能裝載正確的歌曲,並交給播放器播放。
同樣的,在其他列表上雙擊某個歌曲也是執行類似的操作,只是底部播放面板的播放列表將會切換到其他列表,同時播放索引也會被改變,以播放指定的歌曲。
王子和公主幸福地生活在一起
故事到這裡就要結束了,通過這個小故事可以看到一款音樂播放軟體的產出過程,雖然這個軟體本身只能實現音樂搜尋、播放、下載這些簡單的功能,但是為了讓程式碼比較容易維護,讓軟體能儘量執行的比較可靠我把各部分的功能分解的比較清楚,相對比較獨立,耦合各部分功能的工作放在檢視上處理,它們之間通過訊號-槽的方式通訊,因為是小軟體就沒有設計獨立的控制層。
這個軟體本身還有很多不足之處,等我由時間在來慢慢修改維護吧,原始碼公開,如果你有時間也可以下載一份,按照你的想法調整。同時希望這個故事對已經有一些程式設計基礎,想繼續深入學習Python的童鞋起到拋磚引玉的作用。
這則故事所涉及到的所有原始碼可以在GitHub上下載到:
https://github.com/waynezwf/q2music/