プログラミング
HaskellでGTKアプリケーションを作成する、パート1
Making a GTK application in Haskell, part 1 (floreal.tech)
要約
この記事は、Haskell、GTK 4、およびAdwaitaライブラリを使用してToDoリストアプリケーションを構築するチュートリアルシリーズのパート1です。GNOMEプロジェクトのデザイン言語であるAdwaitaのコンポーネントと、HaskellからGTKライブラリにアクセスするためのhaskell-giツールキットについて説明しています。また、Elmアーキテクチャのパターンを導入し、モデル、メッセージ、更新関数を定義しています。
全文翻訳
チュートリアル haskell gtk Feriel Choutri de Tarlé HaskellでGTKアプリケーションを作成する、パート1 2026-10-05 このシリーズでは、Haskell、GTK 4、およびAdwaitaライブラリを使用してToDoリストアプリケーションを構築します。Adwaitaは多くの便利なウィジェットとスタイルを提供します。早速始めましょう!このシリーズの対象読者は、Haskellの開発経験を持つ中級者です。
GTK 4、Adwaita
Adwaitaは、GNOMEプロジェクトのデザイン言語として機能するGTKコンポーネントのライブラリです。要するに、GNOMEプロジェクトがアクセシビリティとスタイル(Human Interface Guidelines、HIG)に関して行ったすべての決定はlibadwaitaにエンコードされています。Libawaitaは多くの機能を提供します。例えば、レスポンシブデザインのアプリケーションを作成でき、デスクトップがライトテーマとダークテーマを切り替えるときに実行時に色が変わります。
HaskellとGTK
このシリーズ全体を通して、GTKライブラリからHaskellバインディングを自動生成するhaskell-giツールキットを使用します。これにより、C APIに関連付けられたままのGTKへのHaskellインターフェースを取得できます。この投稿を読みやすくするために、コードは完全なものではありません。主な概念を強調するために、コードを省略しています。プロジェクト全体はhttps://github.com/Floreal-Technologies/adwaita-todoで見つけることができます。
最初のウィンドウ
まず、GTK 4 / Adwaitaアプリケーションの構造を示す自己完結型の例を示します。リソース管理(クールなAdwaitaスタイルシートを含む)を処理するAdwaita "Application"を作成しましょう。
module Main (main) where
import GI.Awd qualified as Adw
import GI.Gio qualified as Gio
import GI.GTK qualified as Gtk
main :: IO ()
main = do
-- `new X [#attribute := value]`構文はプロパティを持つGtkオブジェクトを作成します。
-- ハッシュ構文はOverloadedLabelsと呼ばれます。
app <- new Adw.Application [#applicationId := "tech.floreal.TodoApp"]
-- "activate"シグナルを"activate"ハンドラに接続します。
on app #activate (activate app)
-- メインアプリケーションループを実行します。
Gio.applicationRun app Nothing
pure ()
activate :: Adw.Application -> IO ()
activate app = do
header <- new Adw.HeaderBar []
toolbar <- new Adw.ToolbarView []
Adw.toolbarViewAddTopBar toolbar header
window <- new Adw.ApplicationWindow
[
#application := app,
#title := "Todos",
#defaultWidth := 480,
#defaultHeight := 640,
#content := toolbar
]
Gtk.windowPresent window
見てください! "Todos"というタイトルと480x640の寸法を持つ空のウィンドウが表示されました。
モデル・ビュー・アップデート:Elmアーキテクチャ
Elmアーキテクチャ(またはTEA)は、インタラクティブなプログラムを設計するためのパターンです。その核心は3つの概念です。
モデル
アプリケーションの状態
ビュー
モデルをユーザーインターフェース(HTML、GTKなど)に変換する方法
アップデート
メッセージに基づいてモデルを更新する方法
これらの概念に加えて、次のものがあります。
メッセージ
ユーザーのアプリケーションとのすべての可能な相互作用の列挙
エフェクト
Update関数は更新されたモデルを返すだけでなく、副作用として実行されるアクションのリストも返します。これらはエフェクトと呼ばれます。
このアプローチはElmによって広く普及し、GTKツールキットの命令型性質を制御するのに非常に適しています。
ToDoアプリ
今、私たちのアプリケーションの状態とそのアクションを表現する時が来ました。Elmアーキテクチャの精神に則り、すべてがデータ構造としてモデル化されるため、トリガーされたアクションとその内容を完全に可視化できます。
モデル
newtype TodoId = TodoId Word deriving stock (Show)
deriving newtype (Eq, Ord)
data Todo = Todo {
id :: TodoId,
title :: Text,
done :: Bool
} deriving stock (Eq, Show)
data Model = Model {
todos :: Map TodoId Todo,
nextId :: TodoId
} deriving stock (Eq, Show)
-- この場合、エフェクトはToDoリストをディスクに保存すること表します。
data Effect = Save [Todo] deriving stock (Eq, Show)
init :: Model
init = Model {
todos = Map.empty,
nextId = TodoId 0
}
メッセージ
アプリケーションに対するユーザーの相互作用はメッセージとしてモデル化されます。
モデルを変更する明確なアクションがある既知のアクションのセットです。モデルを更新するためにユーザーがトリガーできるいくつかのメッセージから始めましょう。
data Message = Add Text | SetDoneStatus TodoId Bool deriving stock (Eq, Ord)
今のところ多くはありませんが、進むにつれて追加していきます。
モデルの更新
update :: Message -> Model -> (Model, [Effect])
update message model = case message of
Add raw ->
let text = Text.strip raw
todoId@(TodoId n) = model.nextId
todo = Todo { id = todoId, title = text, done = False }
in if Text.null text
then (model, [])
else withTodos (Map.insert todo.id todo) model { nextId = TodoId (n + 1) }
SetDoneStatus todoId value -> withTodos (Map.adjust (\todo -> todo { done = value }) todoId) model
where
-- ここでToDoリストが変更されたかどうかを判断し、保存します。
withTodos f changed =
let result = changed { todos = f changed.todos }
in if result.todos == model.todos
then (result, [])
else (result, [Save (Map.elems result.todos)])
それではGHCiを開いて試してみましょう。
$ cabal repl
-- レタスを買うタスクを追加します
ghci> let (m1, e1) = update (Add "Buy leeks") init
-- 未完了のタスクに対する"Save"エフェクトがトリガーされます
ghci> e1
[Save [Todo {id = TodoId 0, title = "Buy leeks", done = False}]]
-- タスクを完了済みに設定します
ghci> let (m2, e2) = update (SetDoneStatus (TodoId 0) True) m1
-- ステータスが変更されたため、保存する必要があります
ghci> e2
[Save [Todo {id = TodoId 0, title = "Buy leeks", done = True}]]
-- タスクを再度Trueに設定してもSaveエフェクトはトリガーされないため、エフェクトリストは空です。
ghci> update (SetDoneStatus (TodoId 0) True) m2
(Model{nextId = TodoId 1, todos = fromList [(TodoId 0,Todo{id = TodoId 0,title = "Buy leeks",done = True})]},[])
-- ← 空リスト!
これは、メッセージの合計型と状態を変更する更新関数としてエンコードされたドメインロジックです。それでは、インターフェースを設計しましょう。
ビュー
ユーザーインターフェースを開始する前に、期待を書き留めておくのは良いことです。経験上、デザインはデータから直接生まれるわけではないため、データの形状を見てデザインを決定することは少なくなっています。この場合、それは非常にシンプルですが、私は描くのが好きです。
ウィジェット
いくつかのウィジェットを使用します(名前をクリックすると、その外観のスクリーンショットが表示されます)。
Box
ボックスは子ウィジェットを行または列に配置します。
ListBox
動的にフィルタリングおよびソートできる行のリスト。後でステータスでフィルタリングしたり、年齢でソートしたりするのに役立ちます。
EntryRow
タイトル、プレースホルダーテキスト、編集可能であることを示すアイコンを備えた行のエントリ。ListBoxのサブクラスであるため、ListBox内に配置されます。
ActionRow
インプレースで編集できない、EntryRowのより制限されたバージョン。アクションアイコンを取得でき、ListBox内に配置されます。
Clamp
子ウィジェットを指定されたサイズに制限するウィジェット。背景が端に見えるようにマージンを強制するのに役立ちます。
ScrolledWindow
このウィジェットは、子ウィジェットをスクロール可能にします。その名前が示すとおりの機能です。
ToolbarView
ページと上下のバーを含むビューウィジェット。
時間2 レゴ
これらのビルディングブロックを手元に、ウィジェットが互いにどのように接続されているかを書き留めましょう。
module Todo.View (view) where
-- Imports
-- [...]
-- `dispatch`は後でRuntimeモジュールで定義され、メッセージを状態/モデルの変更に変換する関数です。
view :: (Message -> IO ()) -> Model -> IO Gtk.Widget
view dispatch model = do
-- ここで、入力行を定義します。タイトルと、アクティブ化されたときに
-- 入力を取得するためにアクティブ化されるシグナルハンドラを備えています。
inputRow <- new Adw.EntryRow [#title := "New task"]
on entry #entryActivated $ do
text <- Gtk.editableGetText inputRow
dispatch (Add text) -- "Add"メッセージを送信します
-- EntryRowはListBoxのサブクラスなので、inputRowをentryBox内に追加する必要があります。
entryBox <- newBoxedList
Gtk.listBoxAppend entryBox inputRow
-- タスクを含むListBox
todoList <- newBoxedList
forM_ (Map.elems model.todos) $ \todo -> do
let row = new Adw.ActionRow []
Gtk.widgetSetLabel (unsafeCoerce row) (todo.title)
Gtk.widgetSetTooltipText (unsafeCoerce row) (Just $ "ID: " <> show todo.id)
Gtk.listBoxAppend todoList row
-- Boxウィジェットを使用して、入力行とタスクリストを垂直に配置します。
vbox <- new Gtk.Box [#orientation := Gtk.OrientationVertical, #spacing := 12]
Gtk.boxAppend vbox entryBox
Gtk.boxAppend vbox todoList
-- ScrolledWindowを使用して、リストが長くなった場合にスクロール可能にします。
scrolledWindow <- new Gtk.ScrolledWindow []
Gtk.scrolledWindowSetChild scrolledWindow vbox
-- Clampを使用して、ウィンドウの端にマージンを適用します。
clamp <- new Gtk.Clamp []
Gtk.clampSetChild clamp scrolledWindow
-- ToolbarViewにコンテンツを設定します。
Adw.toolbarViewSetTopBar toolbar header
Adw.toolbarViewSetContent toolbar clamp
pure (unsafeCoerce window)
newBoxedList :: IO Gtk.ListBox
newBoxedList = do
list <- new Gtk.ListBox []
Gtk.widgetSetSizeRequest list 480 (-1)
pure list