一個在線音樂軟件的故事(五、讓我們開始寫代碼吧)

讓我們開始寫代碼吧

現在有了明確的功能需求,幾乎克服了所有的技術障礙,那麼就可以開始動手編寫這個音樂播放軟件了。

一、組織項目結構

這個故事所講的在線音樂播放軟件並沒有很複雜的功能需求,界面數量很少,沒有數據庫操作。這樣的項目幾乎可以任意組織代碼文件,甚至可以沒有任何結構,把所有的代碼都保存在同一個目錄中,但我們依然希望能有一套便於組織和維護的項目結構。

我們把項目劃分爲:項目配置、協議分析、音樂播放、其他獨立代碼、用戶界面與控制這幾個代碼包,他們各自負責各自的功能,相對獨立。因爲項目比較簡單,就沒有將View層與控制層再進行區分,所以在用戶界面與控制包中可以看到很多應該在控制層的代碼。

項目配置代碼包和其他獨立代碼包中的代碼非常簡單,特別是前者,僅存放一些常量配置數據,比如:網絡請求的超時時間、緩衝區大小、各種資源文件的保存路徑、搜索音頻資源所用到的各種請求路徑等。其他獨立代碼包中是一些功能完全獨立的函數,如:是否在py2exe編譯環境中執行、項目的主目錄位置和一些其他的函數。

configutil兩個代碼包目錄中可找到對應的源碼文件。

二、獲得音樂數據,分析協議

在音樂從哪裏來那一節中已經說明了通過什麼方式獲得音樂數據,但並沒有介紹如何實現他們。我在協議分析代碼包中實現這些功能,這裏有個名叫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

 

在搜索函數中還需要完成一項重要任務,就是通知別的線程搜索已經完成,但是否存在錯誤需要其他線程自己檢查。這個通知任務也就是線程間通訊的主要目的,因爲搜索是在一個獨立的線程中執行的,當搜索完成之後,主線程或其他啓動線程並不知道,我們需要發出一個信號通知其他正在運行的線程,搜索任務已經完成,可以繼續執行後續動作。

在搜索函數的最後一行可以看到,通過調用QThreademit()方法發出一個信號,這個信號的名稱是search_complete(),並且這個信號不帶任何參數。如果這個信號帶有參數,那麼需要在信號名稱中指定參數的類型,比如search_complete(int),並且要在emit方法中提供這個參數值,寫成類似這樣的結構:

self.emit(SIGNAL('search_complete(int)'), 10)

實際範例可以在這個類的下載函數中找到。這樣其他線程可以通過槽連接到這個信號,來捕捉這個信號,一旦捕捉到這個信號,槽中的函數就被調用執行,也就意味着後續動作開始了。在PyQtPySideGUI組件也是通過信號-的方式來傳遞事件信號的。

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.pyTencentProtocol.py兩個源碼文件,以上所講內容在這兩個代碼文件中都有對應實現。

四、載入和播放音樂

前面已經講了很多播放音樂時應該注意的問題,但是並沒有詳細實現,現在就要來完成這部分工作。

按照前面的分析,我們應該首先解決音樂載入的問題,因爲音樂並不總是從網絡載入,對於已經播放過的並且成功緩存的音樂應該從本地載入,只有沒有播放過的或緩存失敗的音樂才需要從網絡載入。

因此在player代碼包中有個稱爲音樂裝載器的類AudioLoader專門負責載入音樂文件,無論是從網絡載入還是從本地載入,都是由它負責。但是要使用這個類,必須先有一個SongInfo對象,然後才能通過音樂裝載器載入對應的音樂文件。前文已經介紹了通過關鍵字搜索可以獲得一批SongInfo對象,其實還有其他的方式獲得SongInfo對象,後面會講到。

既然AudioLoader類要負責從文件或網絡載入音頻數據,顯然AudioLoader類也必須支持多線程操作,所以也是從QThread繼承。這個類的source_type屬性用於區分從網絡加載數據,還是從文件加載數據。由AudioLoaderrun()方法要負責判斷,並調用不同的載入方法加載數據。

 

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的結尾,由播放函數負責寫入聲卡數據流。

可以看到通過AudioLoaderPlayer兩個類的配合使用,就能完成對音頻的加載、緩存、播放這幾個操作。

五、構建簡約時尚的GUI界面

還是根據那張圖片來構建軟件的GUI界面,從圖片上可以看出來,這個界面主要分爲頂部綠色搜索區、左邊面板選擇按鈕區、中部音樂列表面板區和底部深色的音樂播放控制區這幾個主要的區域。

每個面板都是從PySideQFrame組件繼承而來,QFrameQWidgetQApplication一樣都支持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應該顯示哪個面板。

通過PySidelayout佈局方式構建像上面圖片那樣的軟件界面並不是很麻煩,可以說很簡單。但軟件界面上還有很多Icon、圖片等輔助資源,這些資源必須事先準備好,並顯示在正確的位置上,否則一個只有文字的軟件界面是很枯燥無味的。

這裏需要說明PySide支持的圖標格式比較豐富,但是我們用的比較多的是PNGSVG兩種,這兩種圖標都能很好地支持Alpha通道的透明部分,其中PNG是點陣圖,放大縮小PNG最好不要幅度太大,否則變形會比較嚴重。而SVGXML格式描述的圖片,支持矢量變化,縮放比例可以很大且不失真。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"] {
    height32px;
    width140px;
    bordernone;
    padding-left10px;
    padding-right10px;
    border-radius5px;
    text-alignleft;
    color:#555555;
}

 

可以看到屬性名稱聲明是class,屬性值是highlight_button。這段聲明的實際含義是:應用在所有LeftPanel實例對象的QPushButtonQToolButton對象上,並且這些按鈕對象要具有名爲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 
in self.tencent.exception_list:
            message += e.message
        QMessageBox.warning(self, txt, txt+u'。錯誤消息:'+ \

                                message, QMessageBox.Ok)
    self.emit(SIGNAL('after_fill()'))

 

這裏要注意就像前面我們說過的一樣,在處理子線程內部的異常時,並不是直接在子線程中拋出,而是將之存在一個列表中,當搜索線程執行完畢之後,我們檢查是否存在異常,如果存在,我們需要通知用戶發生的具體異常是什麼。這樣操作的缺點是不利於調試,如果沒有好的調試工具,就無法展示一些異常數據,也可以考慮增加異常消息日至文件,將所有的異常消息都保存在日至文件中。

搜索完成之後要處理的工作很簡單,就是將搜索結果在列表中顯示出來。這裏需要一張表格,用於顯示搜索到的所有音樂信息。

回憶一下我們在大多數其他播放器中的操作,在音樂列表上雙擊一首歌,就能立即播放這首歌。當我們選擇了一些歌曲就能添加到播放列表或下載這些選擇的歌曲。我們也要實現這些功能。

考慮到軟件中多次使用到顯示音樂信息的表格,而且主要用來顯示音樂信息,且要支持選擇行、雙擊、行背景顏色變化等功能。引出需要創建一個表格組件,從QTableWidget繼承,用於實現我們想要的功能

這個新的表格類是SongTableview代碼包中,每當雙擊一個單元格都會發出cell_double_clicked()消息,同時SongTableactive_song_info屬性會隨之改變爲正在雙擊的音樂信息,這樣便於我們取得音樂信息,也便於我們調用底部面板上的播放功能播放音樂。同時還爲這個表格設計了列頭信息聲明類,用於聲明列的標題(title)、對應SongInfo的屬性(filed)、寬度(width)、對齊方式(alignment)、是否顯示爲選擇框(is_checkbox)等等一些輔助信息。

在表格中展示SongInfo信息列表時,是通過屬性名稱讀取信息的,這很容易解釋我們的SongInfo類爲什麼要從dict繼承並使用元類,因爲我們既需要能夠通過點方式讀取屬性,也需要能夠通過key名稱的方式讀取屬性。

這個表格能幫助我們把音樂信息顯示的很好,可以通過選擇框選擇想要的音樂,能夠發出雙擊信號,這樣就滿足了我們的需求。

 

2. 底部面板

底部面板的主要作用是根據SongInfo播放音樂,需要用到AudioLoaderPlayer這兩個類。另外這個面板還有一個重要的屬性,就是song_list這是SongInfo列表,保存的是所有需要播放的音樂信息。在播放之前這個屬性必須先設置好,但並非設置好這個屬性就立即開始播放,必須通過設置song_index屬性,來指定要播放的音樂,一旦設置了這個屬性,裝載器loaderAudioLoader)就知道應該狀態哪首歌曲,裝載過程中會發出一系列的信號,這些信號將會一步一步通知面板該做什麼,比如:設置播放面板的信息、調用播放器播放音樂等。播放器playerPlayer)開始播放音樂之後也會發出一系列信號,通知面板該做什麼,比如:不斷修改播放進度和播放的當前時間信息等。

底部面板上的上一首、下一首按鈕只是修改song_index屬性的值,通知loaderplayer應該播放哪首歌曲。這裏需要同時關注右邊的三個按鈕,他們是播放時切換按鈕,有三種狀態,分別是:列表循環、單曲循環、隨機播放,這三種狀態只是用於區分該如何取得下一首歌曲的索引進而修改song_index屬性。

 

3. 主程序界面

主程序界面負責將所有的面板按照佈局要求放在QApplication界面上,併爲各面板的主要組件連接信號-槽,以便讓各個面板組件協同工作。

如:當收到頂部搜索按鈕的點擊信號時,需要設置搜索面板的關鍵字、頁碼、每頁顯示數據量等參數,並調用搜索麪板的搜索功能完成搜索。

當搜索面板的表格收到雙擊信號時,需要修改底部播放面板的播放列表爲搜索列表,並設置播放索引爲當前的歌曲索引,以便裝載器能裝載正確的歌曲,並交給播放器播放。

同樣的,在其他列表上雙擊某個歌曲也是執行類似的操作,只是底部播放面板的播放列表將會切換到其他列表,同時播放索引也會被改變,以播放指定的歌曲。

王子和公主幸福地生活在一起

故事到這裏就要結束了,通過這個小故事可以看到一款音樂播放軟件的產出過程,雖然這個軟件本身只能實現音樂搜索、播放、下載這些簡單的功能,但是爲了讓代碼比較容易維護,讓軟件能儘量運行的比較可靠我把各部分的功能分解的比較清楚,相對比較獨立,耦合各部分功能的工作放在視圖上處理,它們之間通過信號-槽的方式通訊,因爲是小軟件就沒有設計獨立的控制層。

這個軟件本身還有很多不足之處,等我由時間在來慢慢修改維護吧,源碼公開,如果你有時間也可以下載一份,按照你的想法調整。同時希望這個故事對已經有一些編程基礎,想繼續深入學習Python的童鞋起到拋磚引玉的作用。

這則故事所涉及到的所有源代碼可以在GitHub上下載到:

https://github.com/waynezwf/q2music/

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章