Ch 08 對話盒(1)

常常有許多簡單的程式,使用者只要輸入文字或數字,而程式能根據使用者輸入的資料,計算結果,這種程式是一種『對話盒』。這一章,小木偶將藉由撰寫判斷西元某年是否為閏年 ( 何謂閏年?請參考註一 ) 來介紹如何撰寫對話盒。此對話盒程式名為 LEAPYEAR.EXE,執行後畫面如下:

LEAPYEAR.EXE 執行情形
使用者可以在視窗內輸入西元年數,按下查詢後,程式會計算該年是否為閏年。按下離開後程式會彈出一個視窗問是否真的離開。這個程式也包含了選單。


對話盒其實就是包含許多子視窗的視窗

其實對話盒其實就是包含許多子視窗的視窗,而這些子視窗,也稱之為『控制元件 ( controls )』( 或簡稱『控件』 )。以上面 LEAPYEAR.EXE 所顯示的視窗而言,這個視窗包含了四個子視窗,分別是

編號子 視 窗 視窗類別
1 『輸入西元年數:』字串static
2 編輯框edit
3 『查詢』按鈕button
4 『離開』按鈕button

所以小木偶只要在 LEAPYEAR.EXE 所建立的視窗中 ( 此視窗稱為『父視窗』 ),連續呼叫四次 CreatWindowEx API 建立四個子視窗就可以了,而這四個子視窗各有各的位置、視窗代碼、視窗類別等等,因此在呼叫 CreateWindowEx 建立子視窗時,必須分別指定這些數值。一般而言,大多在系統發出 WM_CREATE 訊息時,也就是剛建立父視窗後就立即建立其子視窗。

微軟公司在 Win32 系統堣w經預先定義好幾種常用的視窗類別,程式設計師只要拿來使用即可,而不需要再經由 RegisterClassEx 註冊。要使用這些已預先定義的視窗時,只需在呼叫 CreateWindowEx API 時,把正確的視窗類別填入 lpClassName 參數即可,這樣就節省了程式設計師的許多工作。用這些已預先定義好的視窗類別所建立的子視窗就稱為控制元件,常用的有下面幾種:

視窗類別 名 稱 說   明
button 按鈕類別 在視窗媗膆雂@個長方形區域當作按鈕,使用者可以按下按鈕或鬆開按鈕。
edit 編輯框類別 在視窗堳堨艉@個編輯框的子視窗,可以讓使用者輸入文字或數字。
static 靜態類別 建立一個長方形區域的子視窗,可以顯示字串,這種子視窗無法讓使用者做輸出輸入動作。
listbox 清單類別 建立一塊長方形區域,其中有許多字串可供使用者選擇,例如開啟檔案時顯示現有檔案的子視窗。
combobox 複合類別 是編輯框和清單的結合,通常有一塊區域顯示清單供使用者選擇,或者按下右邊的下拉鈕後才顯示清單。
scrollbar 捲軸元件 一長條形的區域,上面有一滑動桿可供使用者拖動此滑動桿來選擇適當的數值,例如調整音量的大小。

還記得 CreateWindowEx 的原型嗎?

HWND CreateWindowEx(
    DWORD dwExStyle,        // extended window style
    LPCTSTR lpClassName,    // pointer to registered class name
    LPCTSTR lpWindowName,   // pointer to window name
    DWORD dwStyle,          // window style
    int x,                  // horizontal position of window
    int y,                  // vertical position of window
    int nWidth,             // window width
    int nHeight,            // window height
    HWND hWndParent,        // handle to parent or owner window
    HMENU hMenu,            // handle to menu, or child-window identifier
    HINSTANCE hInstance,    // handle to application instance
    LPVOID lpParam          // pointer to window-creation data
   );

當建立子視窗時,有幾個地方須注意。

在 LEAPYEAR 程式中的編輯框只允許使用者輸入西元年份,所以只能是數字,不可以是英文字母或其他符號,所以在第 156 行的 dwStyle 還指定了一個風格,ES_NUMBER,來達到上述目的。這些以定義的視窗類別都有其風格可供使用,小木偶將在稍後幾章內介紹,或者請自行參考 Win32 程式手冊。


原始碼

底下是 LEAPYEAR.ASM 的原始檔:

        .386
        .model  flat,stdcall
        option  casemap:none

include         windows.inc
include         user32.inc
include         kernel32.inc
includelib      user32.lib
includelib      kernel32.lib

EditID          equ     1001h    ;11 控制元件,編輯框的 ID
StrID           equ     1002h    ;12 控制元件,字串的 ID
ButtonExitID    equ     1003h    ;13 控制元件,離開的 ID
ButtonInquireID equ     1004h    ;14 控制元件,查詢的 ID
IDM_Exit        equ     2101h    ;15 選單,離開的 ID
IDM_Clear       equ     2102h    ;16 選單,清除的 ID
IDM_Inquire     equ     2103h    ;17 選單,查詢的 ID
IDM_Help        equ     2104h    ;18 選單,說明的 ID

WndProc         proto   :HWND,:UINT,:WPARAM,:LPARAM

        .DATA
ClassName       db          'SimpleWinClass',0
AppName         db          '是否閏年?',0
AskExit         db          '是否退出此程式?',0
AskExitTitle    db          '詢問',0
AskYear         db          '輸入西元年數︰',0
EditClass       db          'edit',0            ;28 視窗類別,edit
ButtonClass     db          'button',0          ;29 視窗類別,button
StrClass        db          'static',0          ;30 視窗類別,static
ButtonInquire   db          '查詢',0
ButtonExit      db          '離開',0
MenuName        db          'LEAPYEAR',0
HelpText        db          '你可以在編輯框媬擗J西元年數,',0dh,0ah
                db          'LEAP YEAR 會計算該年是否為閏年。',0
HelpTitle       db          '說明',0
ResultTitle     db          '查詢結果',0
Leap_Year       db          '閏年'
Common_Year     db          '平年'
ResultStr       db          '西元 ', 20 dup (0) ;40 顯示結果的字串
hInstance       HINSTANCE   ?
hwnd            HWND        ?
hMenu           HMENU       ?
hEdit           HWND        ?
hStr            HWND        ?
hExitButton     HWND        ?
hInquireButton  HWND        ?
wc              WNDCLASSEX  <30h,?,?,0,0,?,?,?,?,0,offset ClassName,?>
msg             MSG         <?>
year            db          10 dup (?)

        .CODE
start:  invoke  GetModuleHandle,NULL
        mov     hInstance,eax
        mov     wc.style,CS_HREDRAW or CS_VREDRAW
        mov     wc.lpfnWndProc,offset WndProc
        mov     eax,hInstance
        mov     wc.hInstance,eax
        mov     wc.hbrBackground,COLOR_BTNFACE+1        ;59 設定父視窗背景色
        invoke  LoadIcon,NULL,IDI_APPLICATION
        mov     wc.hIcon,eax
        mov     wc.hIconSm,eax
        invoke  LoadCursor,NULL,IDC_ARROW
        mov     wc.hCursor,eax
        invoke  LoadMenu,hInstance,offset MenuName
        mov     hMenu,eax
        invoke  RegisterClassEx,offset wc               ;67 註冊父視窗之視窗類別
        invoke  CreateWindowEx,NULL,offset ClassName,\  ;68 建立父視窗
                offset AppName,WS_OVERLAPPEDWINDOW,0,\
                0,250,150,NULL,hMenu,hInstance,NULL
        mov     hwnd,eax
        invoke  ShowWindow,hwnd,SW_SHOWNORMAL
        invoke  UpdateWindow,hwnd

.while  TRUE
        invoke  GetMessage,offset msg,NULL,0,0
.break  .if     !eax
        invoke  TranslateMessage,offset msg     ;78 轉換使用者輸入的按鍵
        invoke  DispatchMessage,offset msg
.endw        
        mov     eax,msg.wParam
        invoke  ExitProcess,eax
;---------------------------------------
;計算年份是否為閏年,此副程式取得 year 所指的年份字     ;85
;元,計算是否為閏年,並使結果填入 ResultStr 字串      ;86
calc_year       proc
        push    esi
        push    edi
        sub     dx,dx   ;89 DX 將存有 16 進位的西元年份
        mov     esi,offset year
        mov     edi,offset ResultStr+5
        mov     ax,dx
        mov     cl,10
nxt0:   lodsb
        or      al,al
        jz      ok0
        stosb           ;97 把每位西元年份的 ASCII 字元存入 ResultStr
        sub     al,'0'  ;98 計算每位西元年份的 16 進位數
        xchg    ax,dx
        mul     cl
        add     dx,ax
        jmp     nxt0

ok0:    mov     al,' '
        stosb           ;105 存入空白字元到 ResultStr 字串
        mov     esi,offset HelpText+50
        lodsd           ;107 存入『年是』4 個位元組到 ResultStr 字串
        stosd

        mov     si,dx   ;110 DX、SI 存有 16 進位之西元年份
        mov     cx,400
        mov     ax,dx
        sub     dx,dx
        div     cx
        or      dx,dx
        jz      leap

        mov     ax,si
        sub     dx,dx
        mov     cx,100
        div     cx
        or      dx,dx
        jz      common

        mov     ax,si
        mov     cx,4
        sub     dx,dx
        div     cx
        or      dx,dx
        jz      leap
;若為閏年 ESI 指向『閏年』字串,反之指向『平年』字串
common: mov     esi,offset Common_Year
        jmp     ok1
leap:   mov     esi,offset Leap_Year
ok1:    lodsd
        stosd

        mov     esi,offset HelpText+62
        lodsw
        stosw           ;140 存入『。』於 ResultStr 字串
        sub     al,al   ;141 存入 NULL 於 ResultStr 字串
        stosb
        pop     edi
        pop     esi
        ret
calc_year   endp
;---------------------------------------
WndProc proc    hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
.if     uMsg==WM_CREATE         ;149 建立四個控制元件
        invoke  CreateWindowEx,WS_EX_LEFT,addr StrClass,\
                addr AskYear,WS_CHILD or WS_VISIBLE or ES_LEFT,\
                10,22,145,28,hWnd,StrID,hInstance,NULL
        mov     hStr,eax
        invoke  CreateWindowEx,WS_EX_CLIENTEDGE,addr EditClass,\
                NULL,WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT\
                or ES_AUTOHSCROLL or ES_NUMBER,156,18,80,28,\
                hWnd,EditID,hInstance,NULL
        mov     hEdit,eax
        invoke  CreateWindowEx,NULL,addr ButtonClass,addr ButtonInquire\
                ,WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,48,60,\
                52,28,hWnd,ButtonInquireID,hInstance,NULL
        mov     hInquireButton,eax
        invoke  CreateWindowEx,NULL,addr ButtonClass,addr ButtonExit\
                ,WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,148,60,\
                52,28,hWnd,ButtonExitID,hInstance,NULL
        mov     hExitButton,eax
        invoke  SetFocus,hEdit          ;167 使編輯框成為正在作用中的視窗
                
.elseif uMsg==WM_COMMAND
        mov     edx,wParam
        .if     lParam==0               ;171 按下選單或快捷鍵
                .if     dx==IDM_Exit
                        jmp     close
                .elseif dx==IDM_Clear
                        invoke  SetWindowText,hEdit,NULL
                .elseif dx==IDM_Inquire
                        jmp     inquire
                .elseif dx==IDM_Help
                        invoke  MessageBox,hWnd,offset HelpText,\
                                offset HelpTitle,MB_OK
                .endif
        .else                           ;182 按下控制元件
                .if     dx==ButtonExitID
                        jmp     close
                .elseif dx==ButtonInquireID
inquire:                invoke  GetWindowText,hEdit,offset year,5
                        call    calc_year
                        invoke  MessageBox,hWnd,offset ResultStr,\
                                offset ResultTitle,MB_OK
                .endif
        .endif                          ;191

.elseif uMsg==WM_CLOSE
close:  invoke  MessageBox,NULL,offset AskExit,\
                offset AskExitTitle,MB_YESNO or MB_ICONQUESTION
        .if     eax==IDYES
                invoke  DestroyWindow,hWnd
        .endif

.elseif uMsg==WM_DESTROY
        invoke  PostQuitMessage,NULL

.else
def:    invoke  DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
.endif
        xor     eax,eax
        ret
WndProc endp

end     start

底下是 LEAPYEAR.RC 資源描述檔的內容:

#define     IDM_Exit    8449
#define     IDM_Clear   8450
#define     IDM_Inquire 8451
#define     IDM_Help    8452

LEAPYEAR    MENU
{
POPUP       "檔案(&F)"
            {
            MENUITEM    "離開(&X)",IDM_Exit
            }

POPUP       "編輯(&E)"
            {
            MENUITEM    "清除(&C)",IDM_Clear
            MENUITEM    "查詢(&E)",IDM_Inquire
            }
            
MENUITEM    "說明(&H)",IDM_Help
}

底下是 makefile 檔,LEAPYEAR.MAK 檔的內容:

ALL:LEAPYEAR.EXE

LEAPYEAR.EXE : LEAPYEAR.ASM LEAPYEAR.RES
    ml /coff LEAPYEAR.ASM /link /SUBSYSTEM:WINDOWS LEAPYEAR.RES
    
LEAPYEAR.RES : LEAPYEAR.RC
    rc LEAPYEAR.RC

組譯這個程式,只需在 DOS 模式下輸入

d:\homepage\source>nmake -f LEAPYEAR.MAK [Enter]

Microsoft (R) Program Maintenance Utility   Version 1.50
Copyright (c) Microsoft Corp 1988-94. All rights reserved.

        rc LEAPYEAR.RC
        ml /coff LEAPYEAR.ASM /link /SUBSYSTEM:WINDOWS LEAPYEAR.RES
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

 Assembling: LEAPYEAR.ASM
Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

/SUBSYSTEM:WINDOWS /SUBSYSTEM:WINDOWS
"LEAPYEAR.obj"
"/OUT:LEAPYEAR.exe"
"LEAPYEAR.RES"

d:\homepage\source>

即可,NMAKE 會自動幫我們組譯及連結好。


解說

LEAPYEAR.ASM 的第 11 到第 14 行是控制元件的識別碼 ( ID ),這些識別碼將分別填在 WM_CREATE 訊息中,為建立的四個子控制元件而呼叫 CreateWindowEx 的 hMenu 參數堙A您可以到程式第 152、157、161 及第 165 行觀察是否互相配合。

程式第 15 到第 18 行是選單的識別碼,這些識別碼必須和資源描述檔,LEAPYRAE.RC,堜w義選單的識別碼相同,但是因為在 LEAPYEAR.RC 堥洏峇Q進位而在 LEAPYEAR.ASM 堥洏峇Q六進位,所以必須換算使兩者數值相同即可。當然您也可以不必這麼麻煩,使它們同時採用十進位或同時採用十六進位,在資源檔堣Q六進位表示法採用 C/C++ 格式,也就是在數字前加上『0x』就變成十六進位,例如:

#define     IDM_Exit    0x2101
#define     IDM_Clear   0x2102
#define     IDM_Inquire 0x2103

LEAPYEAR.ASM 的第 28 行到第 30 行是系統已預先定義的視窗類別名,將來在第 150、154、159、163 行呼叫 CreateWindowEx 時,必須填入 CreateWindowEx 的 lpClassName 堙C

第 40 行的 ResultStr 字串是用來印出最後的結果:

查詢結果
這個字串前面 5 個位元組是『西元 』,這 5 個位元組是固定的,而後面的文字則是在第 85 行到第 146 行的 calc_year 副程式堻Q一一填進去的。

程式第 55 行到第 67 行是設定 WNDCLASSEX 結構體的每個欄位,並向系統註冊 SimpleWinClass 視窗類別 ( 第 23 行 ),其中第 59 行是設定父視窗的背景色,COLOR_BTNFACE+1。程式第 78 行呼叫 TranslateMessage 是必要的,否則無法由 edit 控制元件輸入任何字元。

底下我們先看看第 148 行開始的視窗函式。LEAPYEAR 處理四個訊息,WM_CREATE、WM_COMMAND、WM_CLOSE 和 WM_DESTROY,我把焦點集中在 WM_CREATE 和 WM_COMMAND。

前面已經提到,程式設計師在撰寫程式時,可利用系統已經定義好的控制元件作為子視窗,節省許多工作,這些子視窗都在父視窗剛建立時也立即建立。所以一般而言,我們在處理 WM_CREATE 訊息時建立這四個子視窗。此外,小木偶希望能馬上就讓使用者輸入年份,所以在第 167 行利用 SetFocus 把編輯框設為輸入焦點,以接收鍵盤輸入。

SetFocus API

這個 API 設定鍵盤輸入到那一個視窗,其原型為:

HWND SetFocus(
    HWND    hWnd    // handle of window to receive focus
   );

SetFocus 如果設定成功,會傳回原輸入焦點的視窗代碼,並且對原輸入焦點的視窗送出 WM_KILLFOCUS,對設定輸入焦點的視窗送出 WM_SETFOCUS;若失敗則傳回 NULL。

再談 WM_COMMAND

當使用者按下控制元件、快捷鍵或點選選單時,都會產生 WM_COMMAND 訊息。我們藉由 lParam 來分辨按下控制元件、按下選單或是按下快捷鍵,如果按下控制元件則 lParam 為子視窗代碼,否則為零 ( 請參考第六章有關 WM_COMMAND 的說明 )。事實上,不僅按鈕控制元件會發出 WM_COMMAND 訊息,絕大部份的控制元件都是靠 WM_COMMAND 將訊息傳遞傳遞給父視窗。

當使用者按下按鈕控制元件後,按鈕控制元件傳給父視窗的 WM_COMMAND 訊息中,在 wParam 參數埵s有使用者對按鈕的動作,常用的有兩種:BN_CLICKED 和 BN_DOUBLECLICKED,前者表示使用者曾按下過按鈕,後者表示使用者曾連續很快地按下按鈕,又彈起來兩次。附帶一提,父視窗也可對按鈕控制元件發出 WM_COMMAND 訊息,詳細情形請參閱第十章

當確定使用者是按下按鈕後,還要再檢查使用者是按下那一個按鈕,可以檢查 wParam 較低的 16 位元,這 16 個位元藏有按鈕的 ID,也就是程式第 13、14 行所定義的『查詢』及『離開』這兩個按鈕 ID。

所以你可以看到第 171 行到 191 行是一個 IF-ELSE-ENDIF 區塊,這段程式用來檢查使用者點選選單或者按下按鈕。如果點選選單則執行第 172 到 181 行,這幾行又是一個 IF-ELSE-ENDIF 區塊,這個 IF-ELSE-ENDIF 區塊則是依使用者所點選的選單執行各個不同的功能。如果使用者按下按鈕則執行第 183 到 190 行,這幾行也是一個 IF-ELSE-ENDIF 區塊,這個區塊則是依使用者所按下的按鈕執行『查詢』或『離開』的動作。

選單堛漸\能大多提過,僅有一個 SetWindowText API 。

SetWindowText API

SetWindowText 是用來設定視窗標題文字的,假如所指定的視窗是子控制元件的話,那麼就會改變這個子控制元件的文字,例如:如果是按鈕的話,就改變按鈕上的文字;如果是編輯框的話,就會改變編輯框內的文字;如果是靜態控制元件,就改變其文字。SetWindowText 的原型是:

BOOL SetWindowText(
    HWND    hWnd,       // handle of window or control
    LPCTSTR lpString    // address of string
);

hWnd 是想改變標題的視窗代碼。lpString 是指向改變後的字串位址,此字串以 NULL 結尾。如果 SetWindowText 執行成功,EAX 傳回非零值,失敗則傳回 NULL。

當使用者按下『清除』選單時,就執行在程式第 175 行的 SetWindowText,而使編輯框內的文字設為空字串,達到清除字串的目的。

與設定字串相反的是 GetWindowText API。所以當執行『查詢』時,就是執行 GetWindowText。換句話說,要取得視窗標題文字或是取得子控制元件的文字,可以使用 GetWindowText API。

GetWindowText API

GetWindowText API 取得指定視窗的標題欄字串,假如所指定的視窗是控制元件的話,則取得其內容。原型如下;

int GetWindowText(
    HWND    hWnd,       // handle of window or control with text
    LPTSTR  lpString,   // address of buffer for text
    int     nMaxCount   // maximum number of characters to copy
   );

hWnd 是要取得視窗標題欄內容的視窗代碼,lpString 是指定所取得的標題內容存放處位址,nMaxCount 是要取得的標題內容長度,以位元組為單位。因為現在西元年份最多也不過四位數,所以 LEAPYEAR 僅僅取得前四位而已,即使使用者輸入超過四位數,程式也僅僅取得前四位數。LEAPYEAR 指定 GetWindowText 把使用者所輸入的字串存入 year 字串堙C

GetWindowTextLength API

假如您想取得全部輸入字串的話,就得先知道輸入的字串長度,也可以用 GetWindowTextLength API 得到字串長度。

int GetWindowTextLength(
    HWND    hWnd    // handle of window or control with text
   );

hWnd 是要取得視窗標題或控制元件內容的視窗代碼。返回時其內容長度以位元組為單位存於 EAX 堙A要注意的是,這堛 EAX 可不包含結尾字元,NULL。

接下來便是呼叫 calc_year 副程式,此副程式是用來把由 GetWindowTexet 傳回的西元年份字串轉換成十六進位數,再計算此十六進位數的西元年份是否為閏年,最後存於 ResultStr 字串堙C再來便是把 ResultStr 字串交由 MessageBox API 顯示在螢幕上。


值得改良之處

假如您依照小木偶此章的方法去撰寫對話盒程式,例如攝氏與華氏溫度的換算、公尺與英呎的換算、公斤與磅的換算、輸入圓半徑求圓面積、輸入長方形長與寬求其面積等等小程式,您將發現您會花許多時間在安排這些控制元件的位置上。其實這個工作可以經由許多對話框編輯程式幫我們完成,這樣就會節省許多時間。下一章將介紹這些。


註一:

陽曆曆法

此處所指的陽曆是以太陽運行為參考所制定的曆法,此種曆法還可分為平年與閏年,平年每年分成 12 個月,每個月從 28 天到 31 天不等,共 365 天:

一月
Jan.
二月
Feb.
三月
Mar.
四月
Apr.
五月
May
六月
Jun.
七月
Jul.
八月
Aug.
九月
Sep.
十月
Oct.
十一月
Nov.
十二月
Dec.
31 天 28 天 31 天30 天 31 天30 天 31 天31 天 30 天31 天 30 天 31 天

閏年一年有 366 天,閏年的二月變為 29 天,其餘和平年一樣。那麼那一年是閏年呢?陽曆曆法是每四年設置一個閏年,因此以西元年數能被四整除的為閏年,例如西元 1988、1992、1996 均為閏年,但是像西元 1700、1800、1900、2100 年能被四整除卻不能被 400 整除的卻不是閏年,而西元 1600、2000、2400 年,能被 400 整除的才是閏年。看起來好像很複雜,小木偶稍微整理一下,應該可以便簡單一點:西元年數後面是 00 的,例如 1700、1800、1900、2000 年等需要能被 400 整除的才是閏年,否則是平年;而西元年數後面不是 00 的,只要能被四整除的就是閏年,不能被四整除的就是平年。

為什麼閏年的計算方式如此複雜呢?這是因為地球繞太陽公轉與地球自轉的時間並非恰好整數,地球繞太陽一圈費時 365.2422 日,但我們曆法上的平年只有 365 天,假如不去處理他的話,那麼每四年日曆上的日期就會比地球公轉快 0.9688 天,大約一天,40 年就快了 10 天,400 年就快了 100 天,大約快一季了,這時如果曆法再不修正,那麼正值寒風刺骨的一月隆冬天氣時,日曆上卻顯示的是四月春暖花開時,於是造成日曆和實際氣候違背的情形,古人所謂『六月雪』大概就是曆法不準確的關係吧?( 但有時我卻寧願真有奇冤時,老天也該發發慈悲 )

言歸正傳,假如每四年多一天,就會和實際情形相符,但即使如此,還是有誤差,因為曆法少了 0.9688 天,所以快了 0.9688 天,現在如果每四年多一天,就會變成曆法每四年慢了 0.0312 天,因此每四百年就慢了 3.12 天,在 400 年內必須減去三天,這三天就設定在像 100、200、300 無法為 400 整除的年數堙C這就是陽曆曆法的由來。


到第七章回到首頁到第九章