跳轉到內容

Haskell/GUI

來自華夏公益教科書

Haskell 至少有四種工具包用於程式設計圖形介面

  • wxHaskell - 為跨平臺 wxWidgets 工具包提供 Haskell 介面,該工具包支援 Windows、OS X 和 GNU/Linux 上的 Gtk+ 等。
  • Gtk2Hs - 為 GTK+ 庫提供 Haskell 介面
  • hoc(文件在 sourceforge 上) - 為 Objective-C 提供 Haskell 繫結,允許使用者訪問 MacOS X 上的 Cocoa 庫
  • qtHaskell - 為 Qt Widget 庫提供一組 Haskell 繫結

在本教程中,我們將重點關注 wxHaskell 工具包。

獲取和執行 wxHaskell

[edit | edit source]

要安裝 wxHaskell,請在以下位置查詢您的版本說明:GNU/Linux Mac Windows


或者wxHaskell 下載頁面,並按照 wxHaskell 下載頁面上提供的安裝說明進行操作。不要忘記將 wxHaskell 註冊到 GHC,否則它將無法執行(使用 Cabal 自動註冊)。要編譯 source.hs(它恰好使用 wxHaskell 程式碼),請開啟命令列並鍵入

ghc -package wx source.hs -o bin

GHCi 的程式碼類似

ghci -package wx

然後,您可以從 GHCi 介面中載入這些檔案。要測試一切是否正常,請轉到 $wxHaskellDir/samples/wx($wxHaskellDir 是您安裝它的目錄)並載入(或編譯)HelloWorld.hs。它應該顯示一個標題為“Hello World!”的視窗,一個帶有“檔案”和“關於”的選單欄,以及底部顯示“歡迎使用 wxHaskell”的狀態列。

如果它不起作用,您可能嘗試將 $wxHaskellDir/lib 目錄的內容複製到 ghc 安裝目錄。

Debian 和 Ubuntu 的快捷方式

[edit | edit source]

如果您的作業系統是 Debian 或 Ubuntu,您可以簡單地從終端執行以下命令

   sudo apt-get install g++
   sudo apt-get install libglu-dev
   sudo apt-get install libwxgtk2.8-dev

Hello World

[edit | edit source]

這是基本的 Haskell“Hello World”程式

module Main where

main :: IO ()
main = putStr "Hello World!"

它將順利編譯,但我們如何實際使用它進行 GUI 工作?首先,您必須匯入 wxHaskell 庫 Graphics.UI.WXGraphics.UI.WXCore 有一些其他內容,但我們現在不需要它們。

要啟動 GUI,請使用 start gui。在這種情況下,gui 是一個函式的名稱,我們將使用它來構建介面。它必須具有 IO 型別。讓我們看看我們有什麼

module Main where

import Graphics.UI.WX

main :: IO ()
main = start gui

gui :: IO ()
gui = do
  --GUI stuff

要製作一個框架,我們使用 frame,它具有型別 [Prop (Frame ())] -> IO (Frame ())。它接受一個“框架屬性”列表,並返回相應的框架。我們將在以後更深入地研究屬性,但屬性通常是屬性和值的組合。我們現在感興趣的是標題。它在 text 屬性中,並且具有型別 (Textual w) => Attr w String。這裡最重要的是它是一個 String 屬性。以下是我們的編碼方式

gui :: IO (Frame ())
gui = do
  frame [text := "Hello World!"]

運算子 (:=) 接受一個屬性和一個值,並將兩者組合成一個屬性。請注意,frame 返回一個 IO (Frame ())start 函式具有型別 IO a -> IO ()。您可以將 gui 的型別更改為 IO (Frame ()),但可能最好只新增 return ()。現在我們擁有自己的 GUI,它包含一個標題為“Hello World!”的框架。它的原始碼

module Main where

import Graphics.UI.WX

main :: IO ()
main = start gui

gui :: IO ()
gui = do
  frame [text := "Hello World!"]
  return ()

結果應類似於螢幕截圖。(它在 Linux 或 MacOS X 上可能看起來略有不同,wxhaskell 也在這些平臺上執行)

控制元件

[edit | edit source]

文字標籤

[edit | edit source]

一個簡單的框架沒有太多作用。在本節中,我們將新增更多元素。讓我們從一個標籤開始。wxHaskell 有一個 label,但它是一個佈局的東西。我們將在下一節進行佈局。我們正在尋找的是一個 staticText。它在 Graphics.UI.WX.Controls 中。staticText 函式接受一個 Window 作為引數,以及一個屬性列表。我們有視窗嗎?是的!請檢視 Graphics.UI.WX.Frame。在那裡,我們看到 Frame 僅僅是一種特殊型別視窗的類型別名。我們將更改 gui 中的程式碼,使其看起來像這樣

Hello StaticText! (winXP)
gui :: IO ()
gui = do
  f <- frame [text := "Hello World!"]
  staticText f [text := "Hello StaticText!"]
  return ()

同樣,textstaticText 物件的一個屬性,因此它有效。試試看!

按鈕

[edit | edit source]

現在讓我們進行更多互動。一個按鈕。我們不會在關於事件的章節之前為它新增功能,但當您單擊它時,您會看到一些可見的東西。

一個 button 是一個控制元件,就像 staticText 一樣。在 Graphics.UI.WX.Controls 中查詢它。

同樣,我們需要一個視窗和一個屬性列表。我們將再次使用框架。text 也是按鈕的一個屬性

重疊的按鈕和 StaticText (winXP)
gui :: IO ()
gui = do
  f <- frame [text := "Hello World!"]
  staticText f [text := "Hello StaticText!"]
  button f [text := "Hello Button!"]
  return ()

將其載入到 GHCi 中(或使用 GHC 編譯它),然後... 嘿?!這是什麼?按鈕被標籤遮住了!我們將在下一節解決這個問題。

佈局

[edit | edit source]

標籤和按鈕重疊的原因是我們還沒有為框架設定佈局。佈局是使用 Graphics.UI.WXCore.Layout 文件中找到的函式建立的。請注意,您無需匯入 Graphics.UI.WXCore 即可使用佈局。

文件說我們可以使用 `widget` 函式將小部件類的一個成員變成一個佈局。另外,視窗也是小部件類的一個成員。但是等等… 我們只有一個視窗,那就是框架!不… 我們還有更多,看看 `Graphics.UI.WX.Controls`,然後點選 `Control` 這個單詞的所有出現位置。你會被帶到 `Graphics.UI.WXCore.WxcClassTypes`,在那裡我們可以看到,`Control` 也是一種特殊型別視窗的型別同義詞。我們需要稍微修改一下程式碼,但這裡就是它。

gui :: IO ()
gui = do
  f <- frame [text := "Hello World!"]
  st <- staticText f [text := "Hello StaticText!"]
  b <- button f [text := "Hello Button!"]
  return ()

現在我們可以使用 `widget st` 和 `widget b` 建立一個靜態文字和按鈕的佈局。`layout` 是框架的一個屬性,所以我們將在此處設定它。

帶有佈局的靜態文字(winXP)
gui :: IO ()
gui = do
  f <- frame [text := "Hello World!"]
  st <- staticText f [text := "Hello StaticText!"]
  b <- button f [text := "Hello Button!"]
  set f [layout := widget st]
  return ()

`set` 函式將在下面關於屬性的部分進行介紹。嘗試一下程式碼,有什麼問題嗎?這隻會顯示靜態文字,而不是按鈕。我們需要一種方法將兩者組合在一起。我們將為此使用 *佈局組合器*。`row` 和 `column` 看起來不錯。它們接收一個整數和一個佈局列表。我們可以很容易地製作一個按鈕和靜態文字的佈局列表。整數是列表元素之間的間距。讓我們試一試

一行佈局(winXP)
帶 25 間距的列布局(winXP)
gui :: IO ()
gui = do
  f <- frame [text := "Hello World!"]
  st <- staticText f [text := "Hello StaticText!"]
  b <- button f [text := "Hello Button!"]
  set f [layout := 
          row 0 [widget st, widget b]
        ]
  return ()

嘗試改變整數並看看會發生什麼。另外,將 `row` 更改為 `column`。嘗試更改列表中元素的順序,以瞭解它的工作原理。為了好玩,嘗試在列表中多次新增 `widget b`。會發生什麼?

這裡有一些練習可以激發你的想象力。記得使用文件!


練習
  1. 新增一個複選框控制元件。它現在還不需要做任何事情,只需確保它在使用行佈局時出現在靜態文字和按鈕旁邊,或者在使用列布局時出現在它們下方。`text` 也是複選框的一個屬性。
  2. 請注意,`row` 和 `column` 接收一個 *佈局* 列表,並且本身也生成一個佈局。利用這一事實,讓你的複選框出現在靜態文字和按鈕的左側,靜態文字和按鈕處於列布局中。
  3. 你能弄清楚單選按鈕控制元件是如何工作的嗎?使用上一練習的佈局,在複選框、靜態文字和按鈕下方新增一個具有兩個(或更多)選項的單選按鈕。使用文件!
  4. 使用 `boxed` 組合器在四個控制元件周圍建立一個漂亮的邊框,以及在靜態文字和按鈕周圍建立另一個邊框。(*注意:`boxed` 組合器可能在 MacOS X 上不起作用 - 你可能會得到無法互動的小部件。這很可能只是 wxhaskell 中的一個錯誤。*)


完成練習後,最終結果應該看起來像這樣

練習答案

你可以為 `row` 和 `column` 使用不同的間距,或者讓單選按鈕的選項水平顯示。

屬性

[edit | edit source]

經過這一切,你可能想知道:“那個 `set` 函式從哪裡冒出來的?”以及“我如何知道 `text` 是否是某個東西的屬性?”這兩個答案都存在於 wxHaskell 的屬性系統中。

設定和修改屬性

[edit | edit source]

在 wxHaskell 程式中,你可以透過兩種方式設定小部件的屬性。

  1. 在建立時:`f <- frame [ text := "Hello World!" ]`
  2. 使用 `set` 函式:`set f [ layout := widget st ]`

`set` 函式接收兩個引數:一個型別為 `w` 的東西以及 `w` 的屬性。在 wxHaskell 中,它們將是小部件以及這些小部件的屬性。一些屬性只能在建立時設定,例如 `textEntry` 的 `alignment`,但你可以將大多數其他屬性設定在程式中的任何 IO 函式中 - 只要你有對它的引用(`set f [stuff]` 中的 `f`)。

除了設定屬性外,你還可以獲取它們。這可以透過 `get` 函式來完成。這是一個愚蠢的例子。

gui :: IO ()
gui = do
  f <- frame [ text := "Hello World!" ]
  st <- staticText f []
  ftext <- get f text
  set st [ text := ftext]
  set f [ text := ftext ++ " And hello again!" ]

看看 `get` 的型別簽名。它是 `w -> Attr w a -> IO a`。`text` 是一個 `String` 屬性,所以我們有一個 `IO String`,可以將其繫結到 `ftext`。最後一行編輯了框架的文字。是的,wxHaskell 中允許進行破壞性更新。我們可以使用 `(:=)` 隨時使用 `set` 覆蓋屬性。這啟發了我們編寫一個修改函式。

modify :: w -> Attr w a -> (a -> a) -> IO ()
modify w attr f = do
  val <- get w attr
  set w [ attr := f val ]

首先獲取值,然後在應用函式後再次設定它。我們肯定不是第一個想到這一點的人…

看看這個運算子:`(:~)`. 你可以在 `set` 中使用它,因為它接收一個屬性和一個函式。結果是一個屬性,其中原始值被函式修改。這意味著我們可以編寫

gui :: IO ()
gui = do
  f <- frame [ text := "Hello World!" ]
  st <- staticText f []
  ftext <- get f text
  set st [ text := ftext ]
  set f [ text :~ ++ " And hello again!" ]

這是一個使用 lambda 表示式的匿名函式的好地方。

我們還可以使用另外兩個運算子來設定或修改屬性:`(::=)` 和 `(::~)`. 它們與 `(:=)` 和 `(:~) `幾乎相同,只是需要一個型別為 `w -> orig` 的函式,其中 `w` 是小部件型別,而 `orig` 是原始“值”型別(`(:=)` 情況下為 `a`,`(:~) `情況下為 `a -> a`)。我們現在不會使用它們,因為我們只遇到了非 IO 型別的屬性,並且函式中需要的那個小部件通常只在 IO 塊中才有用。

如何查詢屬性

[edit | edit source]

現在第二個問題。我們去哪裡確定 `text` 是所有這些東西的一個屬性?去文件…

讓我們看看按鈕有什麼屬性:轉到 Graphics.UI.WX.Controls。點選標有 "Button" 的連結。你會看到 `Button` 是一個特殊型別的 `Control` 的型別同義詞,以及一個可用於建立按鈕的函式列表。在每個函式之後,都有一個“例項”列表。對於普通的 `button` 函式,我們看到 *Commanding -- Textual, Literate, Dimensions, Colored, Visible, Child, Able, Tipped, Identity, Styled, Reactive, Paint*。這就是按鈕是其例項的類的列表。請閱讀 類和型別 章節。這意味著按鈕可以使用一些特定於類的函式。例如,`Textual` 添加了 `text` 和 `appendText` 函式。如果一個小部件是 `Textual` 類的例項,這意味著它有一個 `text` 屬性!

請注意,雖然 `StaticText` 沒有例項列表,但它仍然是 `Control`,而 `Control` 是某種 `Window` 的同義詞。當檢視 `Textual` 類時,它說 `Window` 是它的例項。這是文件方面的一個錯誤!

讓我們看看框架的屬性。它們可以在 `Graphics.UI.WX.Frame` 中找到。這裡文件中另一個錯誤:它說 `Frame` 例項化了 `HasImage`。這在 wxHaskell 的一箇舊版本中是正確的。它應該說是 `Pictured`。除此之外,我們還有 `Form`、`Textual`、`Dimensions`、`Colored`、`Able` 以及其他一些。我們已經看到了 `Textual` 和 `Form`。任何是 `Form` 例項的東西都具有 `layout` 屬性。

`Dimensions` 添加了(除其他外)`clientSize` 屬性。它是一個 `Size` 型別的屬性,可以用 `sz` 建立。請注意,`layout` 屬性也可以改變大小。如果你想使用 `clientSize`,你應該在 `layout` 之後設定它。

`Colored` 添加了 `color` 和 `bgcolor` 屬性。

`Able` 添加了布林值 `enabled` 屬性。這可以用來啟用或停用某些表單元素,這通常顯示為一個灰色的選項。

還有很多其他屬性,請閱讀每個類的文件。

事件

[edit | edit source]

有一些類值得特別注意。它們是 `Reactive` 類和 `Commanding` 類。如你在這些類的文件中看到的,它們沒有新增屬性(`Attr w a` 的形式),而是添加了 *事件*。`Commanding` 類添加了 `command` 事件。我們將使用一個按鈕來演示事件處理。

這裡有一個簡單的 GUI,帶有一個按鈕和一個靜態文字。

之前(winXP)
gui :: IO ()
gui = do
  f <- frame [ text := "Event Handling" ]
  st <- staticText f [ text := "You haven\'t clicked the button yet." ]
  b <- button f [ text := "Click me!" ]
  set f [ layout := column 25 [ widget st, widget b ] ]

我們想在按下按鈕時更改靜態文字。我們需要 `on` 函式。

   b <- button f [ text := "Click me!"
                 , on command := --stuff
                 ]

`on` 的型別:`Event w a -> Attr w a`。`command` 的型別為 `Event w (IO ())`,所以我們需要一個 IO 函式。這個函式被稱為 *事件處理程式*。下面是我們得到的結果



gui :: IO ()
gui = do
  f <- frame [ text := "Event Handling" ]
  st <- staticText f [ text := "You haven\'t clicked the button yet." ]
  b <- button f [ text := "Click me!"
                , on command := set st [ text := "You have clicked the button!" ]
                ]
  set f [ layout := column 25 [ widget st, widget b ] ]

在此處插入關於事件過濾器的文字

華夏公益教科書