拥有实际的小部件对象是非常面向对象的。函数世界中常用的技术是使用函数响应式编程 (FRP)。我将简要概述纯 Haskell 中的小部件库在使用 FRP 时的外观。
tl/dr:您不处理“小部件对象”,而是处理“事件流”的集合,而不关心来自哪些小部件或这些流来自何处。
在 FRP 中,有Event a 的基本概念,可以将其视为无限列表[(Time, a)]。所以,如果你想为一个计数的计数器建模,你可以把它写成[(00:01, 1), (00:02, 4), (00.03, 7), ...],它将一个特定的计数器值与给定的时间相关联。如果你想为一个被按下的按钮建模,你会产生一个[(00:01, ButtonPressed), (00:02, ButtonReleased), ...]
通常还有一种称为Signal a 的东西,它类似于Event a,只是模型值是连续的。您在特定时间没有一组离散的值,但您可以向Signal 询问其值,例如00:02:231,它会给您4.754 或其他值。将信号视为模拟信号,例如医院的心脏电荷计(心电图设备/Holter 监视器)上的信号:它是一条上下跳跃但从不产生“间隙”的连续线。例如,一个窗口总是有一个标题(但也许它是空字符串),所以你总是可以询问它的值。
在 GUI 库中,在底层,会有 mouseMovement :: Event (Int, Int) 和 mouseAction :: Event (MouseButton, MouseAction) 之类的。 mouseMovement 是实际的 USB/PS2 鼠标输出,因此您只能将位置差异作为事件获得(例如,当用户向上移动鼠标时,您会收到事件 (12:35:235, (0, -5))。然后您就可以“集成" 或者更确切地说,“累积”移动事件以获得mousePosition :: Signal (Int, Int),它为您提供绝对鼠标坐标。mousePosition 还可以考虑绝对指针设备,例如触摸屏,或重新定位鼠标光标的操作系统事件等。
与键盘类似,会有一个keyboardAction :: Event (Key, Action),也可以将该事件流“集成”到一个keyboardState :: Signal (Key -> KeyState) 中,让您可以随时读取键的状态。
当您想要在屏幕上绘制内容并与小部件交互时,事情会变得更加复杂。
要只创建一个窗口,可以使用一个名为“魔术函数”:
window :: Event DrawCommand -> Signal WindowIcon -> Signal WindowTitle -> ...
-> FRP (Event (Int, Int) {- mouse events -},
Event (Key, Action) {- key events -},
...)
这个函数会很神奇,因为它必须调用特定于操作系统的函数并创建一个窗口(除非操作系统本身是 FRP,但我对此表示怀疑)。这也是它在 FRP monad 中的原因,因为它会在 IO monad 的幕后调用 createWindow 和 setTitle 和 registerKeyCallback 等。
当然可以将所有这些值分组到数据结构中,这样就会有:
window :: WindowProperties -> ReactiveWidget
-> FRP (ReactiveWindow, ReactiveWidget)
WindowProperties 是决定窗口外观和行为的信号和事件(例如,是否应该有关闭按钮、标题应该是什么等)。
ReactiveWidget 表示 S&E,即键盘和鼠标事件,以防您想在应用程序中模拟鼠标点击,Event DrawCommand 表示您想在窗口上绘制的一连串内容。这种数据结构对所有小部件都是通用的。
ReactiveWindow 表示窗口最小化等事件,而输出 ReactiveWidget 表示来自外部/用户的鼠标和键盘事件。
然后创建一个实际的小部件,比如说一个按钮。它会有签名:
button :: ButtonProperties -> ReactiveWidget -> (ReactiveButton, ReactiveWidget)
ButtonProperties 将确定按钮的颜色/文本/等,ReactiveButton 将包含例如一个Event ButtonAction 和Signal ButtonState 来读取按钮的状态。
请注意,button 函数是一个纯函数,因为它只依赖于纯 FRP 值,例如事件和信号。
如果想要对小部件进行分组(例如将它们水平堆叠),则必须创建例如一个:
horizontalLayout :: HLayoutProperties -> ReactiveWidget
-> (ReactiveLayout, ReactiveWidget)
HLayoutProperties 将包含有关边框大小的信息以及所包含小部件的ReactiveWidgets。然后,ReactiveLayout 将包含一个 [ReactiveWidget],每个子小部件都有一个元素。
布局的作用是它有一个内部Signal [Int],它决定了布局中每个小部件的高度。然后它将接收来自输入ReactiveWidget 的所有事件,然后根据分区布局选择一个输出ReactiveWidget 将事件发送到,同时还转换例如的来源。分区偏移量的鼠标事件。
要演示此 API 的工作原理,请考虑以下程序:
main = runFRP $ do rec -- Recursive do, lets us use winInp lazily before it is defined
-- Create window:
(win, winOut) <- window winProps winInp
-- Create some arbitrary layout with our 2 widgets:
let (lay, layOut) = layout (def { widgets = [butOut, labOut] }) layInp
-- Create a button:
(but, butOut) = button butProps butInp
-- Create a label:
(lab, labOut) = label labProps labInp
-- Connect the layout input to the window output
layInp = winOut
-- Connect the layout output to the window input
winInp = layOut
-- Get the spliced input from the layout
[butInp, layInp] = layoutWidgets lay
-- "pure" is of course from Applicative Functors and indicates a constant Signal
winProps = def { title = pure "Hello, World!", size = pure (800, 600) }
butProps = def { title = pure "Click me!" }
labProps = def { text = reactiveIf
(buttonPressed but)
(pure "Button pressed") (pure "Button not pressed") }
return ()
(def 来自Data.Default 中的data-default)
这会创建一个事件图,如下所示:
Input events -> Input events ->
win ---------------------- lay ---------------------- but \
<- Draw commands etc. \ <- Draw commands etc. | | Button press ev.
\ Input events -> | V
\---------------------- lab /
<- Draw commands etc.
请注意,任何地方都不必有任何“小部件对象”。布局只是一个根据分区系统转换输入和输出事件的函数,因此您可以将获得访问权限的事件流用于小部件,或者您可以让另一个子系统完全生成流。按钮和标签也是如此:它们只是将点击事件转换为绘制命令或类似内容的函数。它是完全解耦的一种表现形式,并且在本质上非常灵活。