Форум программистов, компьютерный форум, киберфорум
Наши страницы
Curry
Войти
Регистрация
Восстановить пароль
Оценить эту запись

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

Запись от Curry размещена 04.07.2018 в 00:16
Обновил(-а) Curry 04.07.2018 в 09:15

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

В 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
Просмотров: 132
Размер:	18.1 Кб
ID:	4907

В следующий раз к виджету приделаем обработчик события, геттер и сеттер.
Размещено в Без категории
Просмотров 152 Комментарии 0
Всего комментариев 0
Комментарии
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2018, vBulletin Solutions, Inc.
Рейтинг@Mail.ru