跳转到内容

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 ] ]

在此处插入关于事件过滤器的文本

华夏公益教科书