HN 日本語サマリー

← 一覧へ戻る
プログラミング

HaskellでGTKアプリケーションを作成する、パート1

Making a GTK application in Haskell, part 1 (floreal.tech)

5 pointsby Vosporos0 コメント

要約

この記事は、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