第二十三章:用 gtk2hs 進行圖形界面編程

2018-08-12 22:19 更新

在本書前面的內(nèi)容中,我們開發(fā)了一系列簡單的文本工具。盡管這些工具提供的文本接口在大部分情況下都能令人滿意,但在某些情況下,我們還是需要用到圖形用戶界面(GUI)。有很多可供 Haskell 使用的圖形界面工具。在這一章中,我們將使用其中的一個,gtk2hs [53] 。

安裝 gtk2hs?

在我們研究如何使用 gtk2hs 工作前,需要先安裝它。在大多數(shù) Linux,BSD,或者其它 POSIX 平臺,有已經(jīng)打包好的 gtk2hs 安裝包。你一般需要安裝 GTK+ 開發(fā)環(huán)境,Glade,和 gtk2hs。安裝的細節(jié)不同版本各有不同。

使用 Windows 和 Mac 的開發(fā)者應(yīng)該查閱 gtk2hs 下載站 。從下載 gtk2hs 開始,然后你需要 Glade version 3 的版本。Mac 開發(fā)者可以從 macports 找到,Windows 開發(fā)者應(yīng)該查閱 sourceforge 。

概覽 GTK+ 開發(fā)棧?

在深入代碼前,讓我們暫停一會考慮一下我們將要使用的系統(tǒng)的架構(gòu)。首先,我們使用的 GTK+ 是一個跨平臺的,用 C 語言來實現(xiàn)的 GUI 工具集??梢耘茉?Windows,Mac,Linux,BSD 等等操作系統(tǒng)上。Gnome 桌面環(huán)境的下層就是用了它。

然后,我們使用的 Glade 是一個用戶界面設(shè)計工具,可以讓你用圖形化的方式來設(shè)計你應(yīng)用的窗口和對話框等。Glade 把你的設(shè)計保存在 XML 文件中,你的應(yīng)用程序會在運行時加載這些 XML 文件。

最后使用的是 gtk2hs。這是一個 GTK+,Glade 以及一些依賴庫的 Haskell 綁定。它只是很多編程語言對 GTK+ 綁定的一種。

使用 Glade 進行用戶界面設(shè)計?

在這一小節(jié)中,我們將為 第22章中開發(fā)的播客下載器 開發(fā)一個圖形界面版本。我們的第一項任務(wù)就是在 Glade 中設(shè)計圖形界面。當我們完成設(shè)計時,我們將編寫 Haskell 代碼集成進應(yīng)用中。

因為這是一本 Haskell 書,而不是一本圖形界面設(shè)計書,所以我們快速帶過前面的步驟。需要更多關(guān)于使用 Glade 設(shè)計圖形界面的信息,你可以參考下面的資源:

  • Glade 主頁 ,包含了 Glade 的文檔。
  • GTK+ 的主頁 包含了不同窗口小部件的信息。參考文檔章節(jié),然后進入穩(wěn)定版 GTK 文檔的區(qū)域。
  • gtk2hs 主頁 也有很有用的文檔,包含了 gtk2hs 的 API 參考和一個 glade 的教程。

Glade 基本概念?

Glade 是一個圖形界面設(shè)計工具。讓我們用圖形界面的方式來設(shè)計圖形界面。我們可以使用一堆 GTK+ 的函數(shù)來創(chuàng)建窗口組件,但更簡單的方式是使用 Glade。

我們要使用 GTK+ 來開發(fā)的基礎(chǔ)的東西叫窗口小部件。一個窗口小部件代表了 GUI 的一部分,可能這個小部件還包含了別的小部件。比如一些小部件包含了一個窗口,對話框,按鈕,以及帶文字的按鈕。

我們在 Glade 中初始化小部件樹,最高級的窗口在樹的根部。你可以把 Glade 和小部件想象成 HTML:你可以像 table 布局一樣排列組件,然后設(shè)置 padding 規(guī)則,然后組織完整的繼承邏輯。

Glade 把組件描述保存在 XML 文件中。我們的程序在運行時加載這些文件。我們通過指定名字從 Glade 運行時庫中加載對應(yīng)的組件。

下面是一個使用 Glade 設(shè)計我們應(yīng)用主界面的截圖:

../_images/ch23-gui-glade-3.png

在本書的附加下載材料中,你可以找到完整的 Glade XML 文件(podresources.glade),然后你可以加載它或者按你希望的修改它。

基于事件編程?

GTK+ 就像其它的 GUI 工具集一樣,是事件驅(qū)動的工具集。這就意味著,我們不是要顯示一個對話框,然后等待用戶點擊按鈕,相反的,我們是要告訴 gtk2hs 當點擊某個按鈕時要調(diào)用什么函數(shù),而不是坐在那兒等待點擊對話框。

這跟傳統(tǒng)的控制臺編程是不同的模式。一個 GUI 程序應(yīng)該有多個窗口打開,但坐在那兒編寫代碼來組合輸入特性組合的打開窗口是一個復(fù)雜的命題。

事件驅(qū)動編程很好的補充了 Haskell。就像我們在書中一遍又一遍的討論,函數(shù)是語言通過傳遞函數(shù)來繁榮昌盛。所以當某些事件發(fā)生時,我們將調(diào)用傳給 gtk2hs 的函數(shù)。這種做法被稱為回調(diào)函數(shù)。

GTK+ 程序的核心是主循環(huán)(main loop)。這部分程序等待用戶或者程序命令運行,然后執(zhí)行它們。GTK+ 的主循環(huán)由 GTK+ 來掌控。對于我們來說,它看起來就像一個 I/O 操作,我們執(zhí)行命令,然后知道主循環(huán)執(zhí)行到我們的命令才返回結(jié)果(即不立即返回)。

因為主循環(huán)負責響應(yīng)一切的點擊鼠標重繪窗口事件,所以它必須始終是可用狀態(tài)的。我們不能執(zhí)行一個很耗時的任務(wù) – 比如在主循環(huán)中下載一個播客節(jié)目。這會使得 GUI 出于無法響應(yīng)的狀態(tài),所有的動作比如點擊取消按鈕將不會被及時的執(zhí)行。

所以,我們將使用多線程來處理這些耗時任務(wù)。更多關(guān)于多線程的信息請查看[本書第24章]()。現(xiàn)在,你只需要知道我們將使用 forkIO 來創(chuàng)建新的線程來處理像下載播客的節(jié)目單和節(jié)目。對于很快的任務(wù),像是添加一個播客到數(shù)據(jù)庫里,就不用新開一個線程來處理了,因為它快到用戶無法感知。

初始化 GUI?

第一步我們先來初始化我們的 GUI 項目。我們將創(chuàng)建一個小文件 PodLocalMain.hs 然后加載 PodMain 然后把它的路徑傳到 podresources.glade,這個被 Glade 保存的 XML 文件提供了我們的 GUI 組件的信息,這么做的原因我們將在 [使用 Cabal]() 這一章中解釋。

-- file: ch23/PodLocalMain.hs
module Main where
import qualified PodMainGUI
main = PodMainGUI.main "podresources.glade"

現(xiàn)在讓我們來考慮一下 PodMainGUI.hs 該怎么寫。這個文件是我們在 第 22 章 的例子基礎(chǔ)上唯一要修改的文件,我們修改它以便于讓它可以作為 GUI 工作。我們先把 PodMainGUI.hs 重命名為 PodMain.hs 使它更加清晰。

-- file: ch23/PodMainGUI.hs
module PodMainGUI where

import PodDownload
import PodDB
import PodTypes
import System.Environment
import Database.HDBC
import Network.Socket(withSocketsDo)

-- GUI libraries

import Graphics.UI.Gtk hiding (disconnect)
import Graphics.UI.Gtk.Glade

-- Threading

import Control.Concurrent

PodMainGUI.hs 的第一部分跟非GUI版本基本相同。我們引入三個附加的組件,首先,我們引入 Graphics.UI.Gtk,它提供了我們需要使用的大部分 GTK+ 函數(shù)。這個模塊和叫 Database.HDBC 的模塊都提供了一個函數(shù)叫 disconnect。我們將使用 HDBC 版本提供的,而不是 GTK+ 版本的,所以我們不從 Graphics.UI.Gtk 導(dǎo)入這個函數(shù)。Graphics.UI.Gtk.Glade 包含了需要加載的函數(shù)且可以跟我們的 Glade 文件協(xié)同工作。

然后我們引入 Control.Concurrent,它提供了多線程編程的基礎(chǔ)。我們從這里開始將使用少量的函數(shù)來描述上面提到的功能。接下來,讓我們定義一個類型來存儲我們的 GUI 信息。

-- file: ch23/PodMainGUI.hs
-- | Our main GUI type
data GUI = GUI {
      mainWin :: Window,
      mwAddBt :: Button,
      mwUpdateBt :: Button,
      mwDownloadBt :: Button,
      mwFetchBt :: Button,
      mwExitBt :: Button,
      statusWin :: Dialog,
      swOKBt :: Button,
      swCancelBt :: Button,
      swLabel :: Label,
      addWin :: Dialog,
      awOKBt :: Button,
      awCancelBt :: Button,
      awEntry :: Entry}

我們的新 GUI 類型存儲所有我們在程序中需要關(guān)心的組件。即使是規(guī)模較大的程序,通常也不會用到這么單一而龐大的類型。但是對于這個小示例來說,單一類型更容易在函數(shù)之間傳遞,并使得我們可以隨時拿到所需的信息,因此我們不妨在這里開個特例。

這個類型記錄中,我們有 Window(頂層窗口), Dialog(對話框窗口), Button(可被點擊的按鈕), Label(文本),以及 Entry(用戶輸入文本的地方)。讓我們馬上看一下 main 函數(shù):

-- file: ch23/PodMainGUI.hs
main :: FilePath -> IO ()
main gladepath = withSocketsDo $ handleSqlError $
    do initGUI                  -- Initialize GTK+ engine

       -- Every so often, we try to run other threads.
       timeoutAddFull (yield >> return True)
                      priorityDefaultIdle 100

       -- Load the GUI from the Glade file
       gui <- loadGlade gladepath

       -- Connect to the database
       dbh <- connect "pod.db"

       -- Set up our events
       connectGui gui dbh

       -- Run the GTK+ main loop; exits after GUI is done
       mainGUI

       -- Disconnect from the database at the end
       disconnect dbh

注意這里的 main 函數(shù)的類型與通常的優(yōu)點區(qū)別,因為它被PodLocalMain.hs中的 main 調(diào)用。我們一開始調(diào)用了 initGUI 來初始化 GTK+ 系統(tǒng)。接下來我們調(diào)用了 timeoutAddFull。這個調(diào)用只有在進行多線程 GTK+ 編程才需要。它告訴 GTK+ 的主循環(huán)時不時地給其它線程機會去執(zhí)行。

之后,我們調(diào)用 loadGlade 函數(shù)(見下面的代碼)來加載我們的 Glade XML 文件。接著,我們連接數(shù)據(jù)庫并調(diào)用 connectGui 函數(shù)來設(shè)置我們的回調(diào)函數(shù)。然后,我們啟動 GTK+ 主循環(huán)。我們期望它在 mainGUI 返回之前可能執(zhí)行數(shù)分鐘,數(shù)小時,甚至是數(shù)天。當 mainGUI 返回時,它表示用戶已經(jīng)關(guān)閉了主窗口或者是點擊了退出按鈕。這時,我們關(guān)閉數(shù)據(jù)庫連接并且結(jié)束程序?,F(xiàn)在,來看看 loadGlade 函數(shù):

-- file: ch23/PodMainGUI.hs
loadGlade gladepath =
    do -- Load XML from glade path.
       -- Note: crashes with a runtime error on console if fails!
       Just xml <- xmlNew gladepath

       -- Load main window
       mw <- xmlGetWidget xml castToWindow "mainWindow"

       -- Load all buttons

       [mwAdd, mwUpdate, mwDownload, mwFetch, mwExit, swOK, swCancel,
        auOK, auCancel] <-
           mapM (xmlGetWidget xml castToButton)
           ["addButton", "updateButton", "downloadButton",
            "fetchButton", "exitButton", "okButton", "cancelButton",
            "auOK", "auCancel"]

       sw <- xmlGetWidget xml castToDialog "statusDialog"
       swl <- xmlGetWidget xml castToLabel "statusLabel"

       au <- xmlGetWidget xml castToDialog "addDialog"
       aue <- xmlGetWidget xml castToEntry "auEntry"

       return $ GUI mw mwAdd mwUpdate mwDownload mwFetch mwExit
              sw swOK swCancel swl au auOK auCancel aue

這個函數(shù)從調(diào)用 xmlNew 開始來加載 Glade XML 文件。當發(fā)生錯誤時它返回 Nothing。當執(zhí)行成功時我們用模式匹配來獲取結(jié)果值。如果失敗,那么命令行將會有異常被輸出;這是這一章結(jié)束的練習題之一。

現(xiàn)在 Glade XML 文件已經(jīng)被加載了,你將看到一大堆 xmlGetWidget 的函數(shù)調(diào)用。這個 Glade 函數(shù)被用來加載一個組件的 XML 定義,同時返回一個 GTK+ 組件類型給對應(yīng)的組件。我們將傳給這個函數(shù)一個值來指出我們期望的 GTK+ 類型 – 當類型不匹配的時候會得到一個運行時錯誤。

我們開始在主窗口創(chuàng)建一個組件。它在 XML 里被定義為 mainWindow 并被加載,然后存到 mw 這個變量里。接著我們通過模式匹配和 mapM 來加載所有的按鈕。然后,我們有了兩個對話框,一個標簽,和一個被加載的實體。最后,我們使用所有的這些來建立 GUI 類型并且返回。接下來,我們設(shè)置回調(diào)函數(shù)作為事件控制器:

-- file: ch23/PodMainGUI.hs
connectGui gui dbh =
    do -- When the close button is clicked, terminate GUI loop
       -- by calling GTK mainQuit function
       onDestroy (mainWin gui) mainQuit

       -- Main window buttons
       onClicked (mwAddBt gui) (guiAdd gui dbh)
       onClicked (mwUpdateBt gui) (guiUpdate gui dbh)
       onClicked (mwDownloadBt gui) (guiDownload gui dbh)
       onClicked (mwFetchBt gui) (guiFetch gui dbh)
       onClicked (mwExitBt gui) mainQuit

       -- We leave the status window buttons for later

我們通過調(diào)用 onDestroy 來開始調(diào)用 connectGui 函數(shù)。這意味著當某個人點擊了操作系統(tǒng)的關(guān)閉按鈕(在 Windows 或者 Linux 上 是標題欄上面的 X 標志,在 Mac OS X 上 是紅色的圓點),我們在主窗口調(diào)用 mainQuit 函數(shù)。mainQuit 關(guān)閉所有的 GUI 窗口然后結(jié)束 GTK+ 主循環(huán)。

接下來,我們調(diào)用 onClicked 對五個不同按鈕的點擊來注冊事件控制器。對于每個按鈕,當用戶通過鍵盤選擇按鈕時控制器同樣會被觸發(fā)。點擊這些按鈕將會調(diào)用比如 guiAdd 這樣的函數(shù),傳遞 GUI 記錄以及一個對數(shù)據(jù)庫的調(diào)用。

現(xiàn)在,我們完整地定義了我們 GUI 播客的主窗口。它看起來像下面的截圖。

../_images/ch23-gui-pod-mainwin.png

增加播客窗口?

現(xiàn)在,我們已經(jīng)完整介紹了主窗口,讓我們來介紹別的需要呈現(xiàn)的窗口,從增加播客窗口開始。當用戶點擊增加一個播客的時候,我們需要彈出一個對話框來提示輸入播客的 URL。我們已經(jīng)在 Glade 中定義了這個對話框,所以接下來需要做的就是設(shè)置它:

-- file: ch23/PodMainGUI.hs
guiAdd gui dbh =
    do -- Initialize the add URL window
       entrySetText (awEntry gui) ""
       onClicked (awCancelBt gui) (widgetHide (addWin gui))
       onClicked (awOKBt gui) procOK

       -- Show the add URL window
       windowPresent (addWin gui)
       where procOK =
           do url <- entryGetText (awEntry gui)
               widgetHide (addWin gui) -- Remove the dialog
               add dbh url             -- Add to the DB

我們通過調(diào)用 entrySetText 來設(shè)置輸入框(用戶填寫播客 URL 的地方)的內(nèi)容,讓我們先設(shè)置為一個空字符串。這是因為這個組件在我們程序的生命周期中會被復(fù)用,所以我們不希望用戶最后添加的 URL 被留在輸入框中。接下來,我們設(shè)置對話框中兩個按鈕的事件。如果用戶點擊取消按鈕,我們就調(diào)用 widgetHide 函數(shù)來從屏幕上移除這個對話框。如果用戶點擊了 OK按鈕,我們調(diào)用 procOK。

procOK 先獲取輸入框中提供的 URL。接下來,它用 widgetHide 函數(shù)來隱藏輸入框,最后它調(diào)用 add 函數(shù)來往輸入庫里增加 URL。這個 add 函數(shù)跟我們沒有 GUI 版本的程序中的一樣。

我們在 guiAdd 里做的最后一件事是彈出窗口,這個通過調(diào)用 windowPresent 來做,這個函數(shù)功能正好跟 widgetHide 相反。

注意 guiAdd 函數(shù)會立即返回。它只是設(shè)置組件并且讓輸入框顯示出來;它不會阻塞自己等待輸入。下圖顯示了對話框看起來是什么樣的。

../_images/ch23-gui-pod-addwin.png

長時間執(zhí)行的任務(wù)?

在主窗口的按鈕中,有三個點擊之后的任務(wù)是需要等一會才會完成的,這三個分別是 更新(update),下載(download),已經(jīng)獲取(fetch)。當這些操作發(fā)生時,我們希望做兩件事:提供給用戶當前操作的進度,以及可以取消當前正在執(zhí)行的操作的功能。

因為這些操作都非常類似,所以可以提供一個通用的處理方式來處理這些交互。我們已經(jīng)在 Glade 文件中定義了一個狀態(tài)窗口組件,這個組件將會被這三個操作使用。在我們的 Haskell 代碼中,我們定義了一個通用的 statusWindow 函數(shù)來同時被這三個操作使用。

statusWindow 需要 4 個參數(shù):GUI 信息,數(shù)據(jù)庫信息,表示該窗口標題的字符串,一個執(zhí)行操作的函數(shù)。這個函數(shù)自己將會被當做參數(shù)傳遞給匯報進度的那個函數(shù)。下面是代碼:

-- file: ch23/PodMainGUI.hs
statusWindow :: IConnection conn =>
                GUI
             -> conn
             -> String
             -> ((String -> IO ()) -> IO ())
             -> IO ()
statusWindow gui dbh title func =
    do -- Clear the status text
       labelSetText (swLabel gui) ""

       -- Disable the OK button, enable Cancel button
       widgetSetSensitivity (swOKBt gui) False
       widgetSetSensitivity (swCancelBt gui) True

       -- Set the title
       windowSetTitle (statusWin gui) title

       -- Start the operation
       childThread <- forkIO childTasks

       -- Define what happens when clicking on Cancel
       onClicked (swCancelBt gui) (cancelChild childThread)

       -- Show the window
       windowPresent (statusWin gui)
    where childTasks =
              do updateLabel "Starting thread..."
                 func updateLabel
                 -- After the child task finishes, enable OK
                 -- and disable Cancel
                 enableOK

          enableOK =
      do widgetSetSensitivity (swCancelBt gui) False
         widgetSetSensitivity (swOKBt gui) True
         onClicked (swOKBt gui) (widgetHide (statusWin gui))
         return ()

  updateLabel text =
      labelSetText (swLabel gui) text
  cancelChild childThread =
      do killThread childThread
         yield
         updateLabel "Action has been cancelled."
         enableOK

這個函數(shù)一開始清理了它上次運行時的標簽內(nèi)容。接下來,我們使 OK 按鈕不可被點擊(變灰色),同時使取消按鈕可被點擊。當操作在進行中時,點擊 OK 按鈕不起任何作用,當操作結(jié)束后,點擊取消按鈕不起任何作用。

接著,我們設(shè)置窗口的標題。這個標題會出現(xiàn)在系統(tǒng)顯示的窗口標題欄中。最后,我們啟動一個新的線程(通過調(diào)用 childTasks),然后保存這個線程ID。然后,我們定義當用戶點擊取消按鈕之后的行為 – 我們調(diào)用 cancelChild 傳入線程 ID。最后,我們調(diào)用 windowPresent 來顯示進度窗口。

在子任務(wù)中,我們顯示一條信息來說明我們正在啟動線程。然后我們調(diào)用真正的工作函數(shù),傳入 updateLabel 函數(shù)來顯示狀態(tài)信息。注意命令行版本的程序可以傳入 putStrLn 函數(shù)。

最后,當工作函數(shù)退出后,我們調(diào)用 enableOK 函數(shù)。這個函數(shù)使取消按鈕變得不可被點擊,并且讓 OK 按鈕變得可點擊,順便定義在點擊 OK 按鈕時候的行為 – 讓進度窗口消失。

updateLabel 簡單地調(diào)用在標簽組件上的 labelSetText 函數(shù)來更新標簽顯示信息。最后,cancelChild 函數(shù)被調(diào)用來殺死執(zhí)行任務(wù)的線程,更新標簽信息,并且使 OK 按鈕可被點擊。

現(xiàn)在我們需要的基礎(chǔ)功能都就位了。他們看起來像下面這樣:

-- file: ch23/PodMainGUI.hs
guiUpdate :: IConnection conn => GUI -> conn -> IO ()
guiUpdate gui dbh =
    statusWindow gui dbh "Pod: Update" (update dbh)

guiDownload gui dbh =
    statusWindow gui dbh "Pod: Download" (download dbh)

guiFetch gui dbh =
    statusWindow gui dbh "Pod: Fetch"
                     (\logf -> update dbh logf >> download dbh logf)

我們只給出了第一個函數(shù)的類型,但是其實三個函數(shù)類型都是相同的,Haskell 可以通過類型推斷來推導(dǎo)出它們的類型。注意我們實現(xiàn)的 guiFetch 函數(shù),我們不用調(diào)用兩次 statusWindow 函數(shù),相反,我們在它的操作中組合函數(shù)來實現(xiàn)。

最后一點構(gòu)成三個函數(shù)的部分是真正做想要的工作。add 函數(shù)是命令行版本直接拿過來的,沒有任何修改。updatedownload 函數(shù)僅僅修改了一小部分 – 通過一個記錄函數(shù)(logging function)來取代調(diào)用 putStrLn 函數(shù)來更新進度狀態(tài)。

-- file: ch23/PodMainGUI.hs
add dbh url =
    do addPodcast dbh pc
       commit dbh
    where pc = Podcast {castId = 0, castURL = url}

update :: IConnection conn => conn -> (String -> IO ()) -> IO ()
update dbh logf =
    do pclist <- getPodcasts dbh
       mapM_ procPodcast pclist
       logf "Update complete."
    where procPodcast pc =
              do logf $ "Updating from " ++ (castURL pc)
                 updatePodcastFromFeed dbh pc

download dbh logf =
    do pclist <- getPodcasts dbh
       mapM_ procPodcast pclist
       logf "Download complete."
    where procPodcast pc =
              do logf $ "Considering " ++ (castURL pc)
                 episodelist <- getPodcastEpisodes dbh pc
                 let dleps = filter (\ep -> epDone ep == False)
                             episodelist
                 mapM_ procEpisode dleps
          procEpisode ep =
              do logf $ "Downloading " ++ (epURL ep)
                 getEpisode dbh ep

下圖展示了更新操作執(zhí)行完成的結(jié)果是什么樣子的。

../_images/ch23-gui-update-complete.png

使用 Cabal?

我們通過一個 Cabal 文件來構(gòu)建我們命令行版本的項目。我們需要做一些修改來讓它支持構(gòu)建我們 GUI 版本的項目。首先我們需要增加 gtk2hs 包的依賴。當然還有 Glade XML 文件的問題。

在前面,我們寫了PodLocalMain.hs文件來假定配置文件叫 podresources.glade,然后把它存到當前目錄下。但是對于真正的系統(tǒng)安裝來說,我們不能做這個假設(shè)。而且,不同的操作系統(tǒng)會把文件放到不同的路徑下。

Cabal 提供了處理這個問題的方法。它自動生成一個模塊,這個模塊可以通過導(dǎo)出函數(shù)來查詢環(huán)境變量。我們必須在 Cabal 依賴文件里增加一行 Data-files。這個文件名稱表示了所有需要一同安裝的數(shù)據(jù)文件。然后,Cabal 將會導(dǎo)出一個 Paths_pod 模塊(pod 部分來自 Cabal文件中的 Name 行),我們可以使用這個模塊來在運行時查看文件路徑。下面是我們新的 Cabal 依賴文件:

-- ch24/pod.cabal
name: pod
Version: 1.0.0
Build-type: Simple
Build-Depends: HTTP, HaXml, network, HDBC, HDBC-sqlite3, base,
               gtk, glade
Data-files: podresources.glade

Executable: pod
Main-Is: PodCabalMain.hs
GHC-Options: -O2

當然還有 PodCabalMain.hs

-- file: ch23/PodCabalMain.hs
module Main where

import qualified PodMainGUI
import Paths_pod(getDataFileName)

main =
    do gladefn <- getDataFileName "podresources.glade"
       PodMainGUI.main gladefn

練習?

  1. 如果調(diào)用 xmlNew 返回了 Nothging,顯示一個圖形話的出錯信息。
  2. 修改項目來實現(xiàn)在同一個代碼倉庫既可以使用圖形界面的方式,又可以選擇命令行模式來運行程序。提示:把通用代碼移出 PodMainGUI.hs,然后創(chuàng)建兩個 main 模塊,一個為圖形界面服務(wù),一個為命令行服務(wù)。
  3. 為什么 guiFetch 函數(shù)組合工作函數(shù)而不是調(diào)用 statusWindow 兩次?
[53]還有很多別的選擇,除了 gtk2hs 之外,wxHaskell 也是非常杰出的跨平臺圖形界面工具集。
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號