Форум программистов, компьютерный форум, киберфорум
Curry
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

GUI-SDL2 – GUI написанный на Haskell. Часть 5.1. Делаем свой виджет

Запись от Curry размещена 04.07.2018 в 00:16
Показов 2209 Комментарии 0

(Предыдущая тема)

В GUI-SDL2 осталось ещё много виджетов про которые я пока не рассказал (и собираюсь рассказать в дальнейшем), тем не менее, сейчас подошло время продемонстрировать создание своего виджета. Вы уже имеете представление что такое виджет в GUI-SDL2, как его создавать, назначать обработчики событий и динамически менять его свойства.
А теперь посмотрим как это работает.

Для начала немного информации об устройстве GUI-SDL2.
В GUI-SDL2 имеется базовый слой (GUI.BaseLayer) который работает с типом Widget – некий виджет. Из таких виджетов он строит деревья, обрабатывает отрисовку с перекрытием, определяет над каким виджетом находится курсор, пересчитывает координаты курсора в координаты окна, и делает многое другое. Но продемонстрированные ранее виджеты (label,button,…) – это не такие виджеты. Их тип (фрагмент скопирован из GUI.BaseLayer.Widget)
Haskell
1
2
3
4
-- | Пользовательский виджет. Сочетает виджет базового уровня и данные конкретного виджета.
data GuiWidget a = GuiWidget { baseWidget :: Widget
                             , widgetData :: a
                             }
Где в качестве параметра типа a для каждого типа виджета подставляется свой, уникальный тип.
Если вы не сочиняете свой виджет, то использовать тип Widget вам вряд ли понадобится.
Но сейчас другой случай.

Создадим демонстрационный виджет отслеживающий позицию указателя мыши в своей области и рисующий на этом месте крестик.

Сперва понадобится обновить версию GUI-SDL2 до 0.1.21 (от 2.7.2018)
В этой версии я устранил некоторое дублирование кода касающееся как раз данной темы.
Заодно, там resolver: lts-11.16 вместо 11.13, но это не принципиально.

И так, вы обновили GUI-SDL2, например, через
Bash
1
git pull https://github.com/KolodeznyDiver/GUI-SDL2
Добавляем в package.yaml проекта GUI-SDL2-tutorial
Haskell
1
2
3
  5.1.CustomWidget:    
    source-dirs:      5.1.CustomWidget
    main:             Main.hs
создаём каталог 5.1.CustomWidget и в нём Main.hs. Его содержимое рассмотрим потом.
Сейчас создадим ещё и модуль в котором будет реализован виджет.

Как в Haskell принято, модули, даже относящиеся к разным пакетам, но со схожей сущностью, размещаются по одним составным путям.
По этому вас не должно сильно удивить моё предположение создать в каталоге 5.1.CustomWidget подкаталог GUI, в нём подкаталог Widget, а в нём уже файл MouseWatcher.hs, так что имя модуля будет GUI.Widget.MouseWatcher, что согласуется с именами GUI.Widget.Label, GUI.Widget.Button.
Это принятое в Haskell соглашение об именах модулей, так же, как модули Data.Maybe или Data.Bits из базового пакета, а так же модуль Data.Text из пакета text находятся по одному пути Data.

Вообще то ничто не мешает написать код виджета хоть прямо в Main.hs. Но будем делать примеры по правилам.
Поместите заготовку для виджета в файл MouseWatcher.hs
Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
module GUI.Widget.MouseWatcher(
    MouseWatcherData,MouseWatcherDef(..)
    ,mouseWatcher
    ) where
 
import Control.Monad
import Control.Monad.IO.Class
import Data.Bits
import Data.Maybe
import qualified SDL
import SDL.Vect
import Data.Default
import GUI
import GUI.Widget.Handlers
 
data MouseWatcherData = MouseWatcherData
 
-- | Параметры настройки виджета @mouseWatcher@.
data MouseWatcherDef = MouseWatcherDef {
    mouseWatcherFormItemDef  :: FormItemWidgetDef -- ^ Общие настройки для всех виджетов для форм
  , mouseWatcherSize      :: GuiSize -- ^ размер без полей.
  , mouseWatcherFlags     :: WidgetFlags -- ^ Флаги базового виджета.
                                       }
 
instance Default MouseWatcherDef where
    def = MouseWatcherDef   { mouseWatcherFormItemDef = def
                            , mouseWatcherSize = zero
                            , mouseWatcherFlags = WidgetVisible .|. WidgetEnable 
                            }
 
-- | Создание виджета @mouseWatcher@.
mouseWatcher :: MonadIO m =>
                 MouseWatcherDef ->  -- ^ Параметры виджета.
                 Widget ->  -- ^ Будующий предок в дереве виджетов.
                 Skin -> -- ^ Skin.
                 m (GuiWidget MouseWatcherData)
mouseWatcher MouseWatcherDef{..} parent skin = do
    mkFormWidget mouseWatcherFormItemDef mouseWatcherFlags skin id
            MouseWatcherData parent (noChildrenFns mouseWatcherSize)
Такой модуль откомпилируется, его удастся вставить, к примеру, в лайаут. Конечно отображать он ничего не будет.
Разберём что имеется в заготовке:

Haskell
1
data MouseWatcherData = MouseWatcherData
Это тип виджета, данные, которые используют функции установки обработчиков событий, получения и извлечения свойств виджета. При создании виджета получается (GuiWidget MouseWatcherData) (см. ранее про GuiWidget).

Наш виджет пока никак не управляем и MouseWatcherData просто MouseWatcherData, без полей с данными.
Однако, заметим, что у этого типа экспортируется только левая часть. Правая часть – конструктор данных, содержимое типа инкапсулируется в модуле. Этого следует придерживаться при создании виджетов.

Зато тип MouseWatcherDef предназначен для задания начальных параметров виджета и всегда экспортируется полностью.
Заметьте соглашение. Имя типа виджета с его внутренними данными заканчивается на Data, а имя типа с данными инициализации на Def.
Далее в заготовке создаётся экземпляр типа класса Default (из пакета data-default) и определяется единственная его функция def для MouseWatcherDef возвращающая MouseWatcherDef с параметрами по умолчанию.

Создавать экземпляр Default также крайне рекомендуется для типа данных инициализации виджета (и вообще, рекомендую использовать экземпляры Default где только можно). Устанавливать значения полей записи не по умолчанию можно синтаксисом редактирования записей
Haskell
1
        def{поле=значение, поле2=значение2}
При этом сохранится обратная совместимость даже если в записи появятся дополнительные поля.

Сама функция создания виджета mouseWatcher почти только и делает, что передаёт данные из аргументов в функцию mkFormWidget. Нюанс в последнем аргументе (noChildrenFns mouseWatcherSize).

В этом аргументе должна быть передана запись WidgetFunctions из GUI.BaseLayer.Types.
Каждое её поле – функция вызываемая базовым слоем GUI-SDL2 в разных случаях. Многие функции типичны для схожих виджетов, и для таких случаев есть функции возвращаюшие подготовленные записи функций WidgetFunctions. В данном случае подходит функция noChildrenFns из модуля таких заготовок GUI.Widget.Handlers. Из её названия понятно что она делает заготовку WidgetFunctions для виджета не намеревающегося быть контейнером для других виджетов. В качестве аргумента ей передаётся начальный размер виджета который она будет передавать в callback-функции виджету предку. Но это так, детали.

Что нам нужно для задуманной идеи – отслеживать позицию указателя мыши?
Значит реагировать на события перемещения мыши, попадание и уход мыши в/за пределы виджета, а так же рисовать на запрос отрисовки. После (noChildrenFns mouseWatcherSize) можно поставить фигурные скобки и описать замену четырёх полей из возвращаемой noChildrenFns записи
Haskell
1
2
3
4
5
6
7
8
9
10
        (noChildrenFns mouseWatcherSize){
        -- указатель попадает в видимую область виджета.
    onGainedMouseFocus = \widget pnt -> -- сохраняем состояние мыши и запрашиваем перерисовку
    -- указатель перемещается по виджету.
    ,onMouseMotion = \widget _btnsLst pnt _relMv -> -- сохраняем состояние мыши и запрашиваем перерисовку
        -- указатель покинул область виджета.
        ,onLostMouseFocus = \widget -> -- сохраняем состояние мыши и запрашиваем перерисовку
        
        ,onDraw= \widget -> -- перерисовываем
}
Во всех функциях из WidgetFunctions widget – это наш виджет (типа Widget).

Нам нужно хранить состояние мыши - координату и признак что есть ли вообще мышь в окне.
Для этого подойдёт тип Maybe GuiPoint. Значение это будет изменяться, по этому используем IORef (Maybe GuiPoint). Функции newMonadIORef, writeMonadIORef, readMonadIORef – лёгкие обёртки над newIORef, writeIORef, readIORef для использования их в монаде MonadIO.
Работающий виджет в MouseWatcher.hs
Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
module GUI.Widget.MouseWatcher(
    MouseWatcherData,MouseWatcherDef(..)
    ,mouseWatcher
    ) where
 
import Control.Monad
import Control.Monad.IO.Class
import Data.Bits
import Data.IORef
import Data.Maybe
import Data.Monoid
import qualified TextShow as TS
import TextShow (showb)
import qualified SDL
import SDL.Vect
import Data.Default
import GUI
import GUI.Widget.Handlers
 
data MouseWatcherData = MouseWatcherData
 
-- | Параметры настройки виджета @mouseWatcher@.
data MouseWatcherDef = MouseWatcherDef {
    mouseWatcherFormItemDef  :: FormItemWidgetDef -- ^ Общие настройки для всех виджетов для форм
                                              -- в настоящий момент только margin's.
  , mouseWatcherSize      :: GuiSize -- ^ размер без полей.
  , mouseWatcherFlags     :: WidgetFlags -- ^ Флаги базового виджета.
                                       }
 
instance Default MouseWatcherDef where
    def = MouseWatcherDef   { mouseWatcherFormItemDef = def
                            , mouseWatcherSize = zero
                            , mouseWatcherFlags = WidgetVisible .|. WidgetEnable 
                            }
 
-- | Создание виджета @mouseWatcher@.
mouseWatcher :: MonadIO m =>
                 MouseWatcherDef ->  -- ^ Параметры виджета.
                 Widget ->  -- ^ Будущий предок в дереве виджетов.
                 Skin -> -- ^ Skin.
                 m (GuiWidget MouseWatcherData)
mouseWatcher MouseWatcherDef{..} parent skin = do
    rfState <- newMonadIORef Nothing
    mkFormWidget mouseWatcherFormItemDef mouseWatcherFlags skin id
            MouseWatcherData parent (noChildrenFns mouseWatcherSize){
        onGainedMouseFocus = \widget pnt -> do
            writeMonadIORef rfState $ Just pnt
            markWidgetForRedraw widget
        ,onMouseMotion = \widget _btnsLst pnt _relMv -> do
            writeMonadIORef rfState $ Just pnt
            markWidgetForRedraw widget
        ,onLostMouseFocus = \widget -> do
            writeMonadIORef rfState Nothing 
            markWidgetForRedraw widget
        ,onDraw= \widget -> do
            rectAll <- getWidgetCanvasRect widget
            setColor $ grayColor 100
            fillRect rectAll
            fnt <- getFont "label"
            state <- readMonadIORef rfState
            s <- case state of
                Just p@(P (V2 x y)) -> do
                    setColor $ grayColor 255
                    let crossSz = 10
                    drawLine (p .-^ V2 crossSz 0) (p .+^ V2 crossSz 0)
                    drawLine (p .-^ V2 0 crossSz) (p .+^ V2 0 crossSz)
                    return $ TS.toText $ showb x <> "x" <> showb y
                _ -> return "No mouse"
            drawText fnt (grayColor 255) (P (V2 5 5)) s
       }

С функциями рисования используемыми в onDraw можно ознакомится в модуле GUI.BaseLayer.Canvas. Обратите внимание что для них требуется другая монада – они не могут (и не должны) вызываться из других функций виджета.
(Вообще то есть такая возможность. Для генерации текстуры заранее или получения размера строки текста до создания виджета можно временно создать неотображаемую Canvas через функцию runProxyCanvas, но я опять отвлекаюсь).
Основной файл Main.hs
Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{-# LANGUAGE OverloadedStrings #-}
module Main where
 
import Control.Monad
import Data.Default
import qualified SDL
import GUI
import GUI.Skin.DefaultSkin
import GUI.Widget.Layout.LinearLayout
import SDL.Vect
import GUI.Widget.MouseWatcher
 
main :: IO ()
main = runGUI defSkin  -- Запуск GUI с оформлением по умолчанию
 
        -- Список предзагруженных шрифтов : ключ, имя файла, размер шрифта, опции
        [GuiFontDef "label"       "PTN57F.ttf" 15 def
        ] 
        def $ \gui -> do
    win <- newWindow gui "5.1.CustomWidget" SDL.defaultWindow{SDL.windowInitialSize  = V2 400 400}
    vL <- win $+ vLayout def
    void $ vL $+ mouseWatcher def{mouseWatcherSize= V2 (-1) (-1)}
И не забыть добавить зависимость от пакета text-show в package.yaml
Haskell
1
2
3
4
5
  5.1.CustomWidget:    
    source-dirs:      5.1.CustomWidget
    main:             Main.hs
    dependencies:
    - text-show
Проверяем что получается
Bash
1
stack build --exec 5.1.CustomWidget
Нажмите на изображение для увеличения
Название: 5.1.CustomWidget.png
Просмотров: 815
Размер:	18.1 Кб
ID:	4907

В следующий раз к виджету приделаем обработчик события, геттер и сеттер.
Размещено в GUI-SDL2
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
PhpStorm 2025.3: WSL Terminal всегда стартует в ~
and_y87 14.12.2025
PhpStorm 2025. 3: WSL Terminal всегда стартует в ~ (home), игнорируя директорию проекта Симптом: После обновления до PhpStorm 2025. 3 встроенный терминал WSL открывается в домашней директории. . .
Access
VikBal 11.12.2025
Помогите пожалуйста !! Как объединить 2 одинаковые БД Access с разными данными.
Новый ноутбук
volvo 07.12.2025
Всем привет. По скидке в "черную пятницу" взял себе новый ноутбук Lenovo ThinkBook 16 G7 на Амазоне: Ryzen 5 7533HS 64 Gb DDR5 1Tb NVMe 16" Full HD Display Win11 Pro
Музыка, написанная Искусственным Интеллектом
volvo 04.12.2025
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
От async/await к виртуальным потокам в Python
IndentationError 23.11.2025
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
Поиск "дружественных имён" СОМ портов
Argus19 22.11.2025
Поиск "дружественных имён" СОМ портов На странице: https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/ нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Programma_Boinc 20.11.2025
Сколько Государство потратило денег на меня, обеспечивая инсулином. Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов. . . .
Ломающие изменения в C#.NStar Alpha
Etyuhibosecyu 20.11.2025
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
Мысли в слух
kumehtar 18.11.2025
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
Создание Single Page Application на фреймах
krapotkin 16.11.2025
Статья исключительно для начинающих. Подходы оригинальностью не блещут. В век Веб все очень привыкли к дизайну Single-Page-Application . Быстренько разберем подход "на фреймах". Мы делаем одну. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru