I'm trying to write a simple UI in gtk2hs. It starts with two text boxes and a button. When the button is pressed, it makes a table of buttons of size (m,n) where m and n are taken from the text boxes. For some reason, when the button is pressed, space is allocated for the table but none of the buttons are shown!
import Graphics.UI.Gtk
import Control.Concurrent
t2l :: Int -> Int -> Int -> Int -> Int
t2l r c rr cc = (r * cc) + c
buildTable :: Int -> Int -> IO(Table, [Button])
buildTable r c = do
t <- tableNew r c True
buttons <- sequence $ take (r * c) (repeat buttonNew)
mapM (`set` [buttonLabel := "HELLO"]) buttons
return [tableAttachDefaults t (buttons !! (t2l rr cc r c)) cc (cc+1) rr (rr+1) | cc <- [0..(c+1)] , rr <- [0..(r+1)]]
return (t,buttons)
main = do
initGUI
window <- windowNew
mainSplit <- vBoxNew False 10
contPannel <- hBoxNew False 5
rowTF <- entryNew
colTF <- entryNew
buildBTN <- buttonNew
set buildBTN [buttonLabel := "Build Table"]
set window [containerChild := mainSplit]
boxPackStart mainSplit contPannel PackGrow 0
boxPackStart contPannel rowTF PackGrow 0
boxPackStart contPannel colTF PackGrow 0
boxPackStart contPannel buildBTN PackNatural 0
on window objectDestroy mainQuit
widgetShowAll window
on buildBTN buttonActivated $ do
rT <- get rowTF entryText
cT <- get colTF entryText
r <- return $ read rT
c <- return $ read cT
(t,b) <- buildTable r c
boxPackStart mainSplit t PackGrow 0
widgetShowAll t
return ()
mainGUI
I'm not 100% sure why the error arose, I have some new code which works:
import Graphics.UI.Gtk
import Control.Concurrent
mkBtn :: String -> IO Button
mkBtn label = buttonNew >>= (\b -> set b [buttonLabel := label] >> return b)
buildTable :: Int -> Int -> IO(Grid)
buildTable r c = do
t <- gridNew
gridSetRowHomogeneous t True
mapM (\f -> mkBtn "Hello" >>= (\b -> gridAttach t b (f `mod` c) (f `div` c) 1 1)) [0..(r*c)-1]
return (t)
main = do
initGUI
window <- windowNew
mainSplit <- vBoxNew False 10
contPannel <- hBoxNew False 5
rowTF <- entryNew
colTF <- entryNew
buildBTN <- buttonNew
set buildBTN [buttonLabel := "Build Table"]
set window [containerChild := mainSplit]
boxPackStart mainSplit contPannel PackGrow 0
boxPackStart contPannel rowTF PackGrow 0
boxPackStart contPannel colTF PackGrow 0
boxPackStart contPannel buildBTN PackNatural 0
on window objectDestroy mainQuit
widgetShowAll window
on buildBTN buttonActivated $ do
rT <- get rowTF entryText
cT <- get colTF entryText
r <- return $ read rT
c <- return $ read cT
t <- buildTable r c
boxPackStart mainSplit t PackGrow 0
widgetShowAll t
return ()
mainGUI
Maybe someone will know why this works and the last didn't? I assume it was how I was creating new buttons.
First thing I changed was from gtk2 to 3, this enabled me to use grid instead of table. Instead of using repeat, I used a helper function mkBtn. The other changes are just how I went about populating the grid. Instead of a rather silly list comprehension, I used mapM and converted the index in the list of buttons to table coords instead of table coords to list index (originally done in t2l)
I'm trying to make a combo box of pictures (as bellow) inside a treeview cell to make a selection.
I tried to use a cellRendererComboNew to render the combo but the options to fill the combobox cellComboTextModel := work only for String and I can't render pictures.
I tried to use a cellRendererPixbufNew. It render images but I can't perform a selection on it.
What is the correct approach to make that?
An example in Haskell, Python, or in any language would be very helpfull.
Best regards.
In PyGobject I came up with this solution. The example is fully functional but requires 2 png files in the same directory. I used two pngs with 100 x 20 pixel format.
The previous example used Gtk.ComboBox.new_with_model_and_entry() and I was missing the set_entry_text_colum() function that has to go with such kind of combobox.
#!/usr/bin/python3
from gi.repository import Gtk, Gdk, GdkPixbuf
class ComboBoxWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="ComboBox Pixbuf Example")
self.set_border_width(10)
store = Gtk.ListStore(str, GdkPixbuf.Pixbuf)
solid_line = GdkPixbuf.Pixbuf.new_from_file("solid_line.png")
store.append(["1", solid_line])
dashed_line = GdkPixbuf.Pixbuf.new_from_file("dashed_line.png")
store.append(["2", dashed_line])
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
combo = Gtk.ComboBox.new_with_model(store)
rend_int = Gtk.CellRendererText()
rend_pixbuf = Gtk.CellRendererPixbuf()
combo.pack_start(rend_int, False)
combo.add_attribute(rend_int, "text", 0)
combo.pack_start(rend_pixbuf, True)
combo.add_attribute(rend_pixbuf, "pixbuf", 1)
combo.connect("changed", self.on_combo_changed)
vbox.pack_start(combo, False, False, 0)
self.add(vbox)
def on_combo_changed(self, combo):
tree_iter = combo.get_active_iter()
if tree_iter != None:
model = combo.get_model()
row = model[tree_iter][0]
print("Selected row {0}".format(row))
win = ComboBoxWindow()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()
Similar question:
Show icon or color in Gtk TreeView tree
Source:
http://python-gtk-3-tutorial.readthedocs.org/en/latest/combobox.html
This is my solution in Haskell:
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
import Data.String
import Graphics.UI.Gtk
import qualified Data.Map as Map
import qualified Data.Text as T
import Data.Maybe
import qualified Graphics.UI.Gtk.Gdk.Pixbuf as Pixbuf
import Control.Monad
colorsRawL = [(0,0,0),(254,26,89),(255,0,0),(0,255,0),(0,0,255),(255,255,0),(0,255,255),(255,0,255),(192,192,192),(128,128,128),(128,0,0),(128,128,0),(0,128,0),(128,0,128),(0,128,128),(0,0,128)]
manufacturers = [("Sony"::String), ("LG"::String), ("Panasonic"::String), ("Toshiba"::String), ("Nokia"::String), ("Samsung"::String)]
data ListElement = ListElement { name :: String , selected::Pixbuf }
getManufacturers::IO[ListElement]
getManufacturers = mapM (\x -> do
pbn <- Pixbuf.pixbufNew ColorspaceRgb False 8 16 16
Pixbuf.pixbufFill pbn 255 255 255 1
let el = ListElement x pbn
return el
) manufacturers
pixBufListS::IO [(String,Pixbuf)]
pixBufListS = mapM (\(r,g,b)-> do
pbn <- Pixbuf.pixbufNew ColorspaceRgb False 8 16 16
Pixbuf.pixbufFill pbn r g b 1
let name::String = ("Color ("++(show r)++" "++(show g)++" "++(show b)++ ")")
return (name,pbn)
) colorsRawL
getMap::IO (Map.Map String Pixbuf)
getMap = do
list <- pixBufListS
let mp = Map.fromList list
return mp
main :: IO ()
main = do
initGUI
window <- windowNew
fixed <- fixedNew
pixList <-pixBufListS
manus <- getManufacturers
lststoreManus::(ListStore ListElement) <- listStoreNew manus
treeview <- treeViewNew
treeViewSetModel treeview lststoreManus
treeViewSetHeadersVisible treeview True
colName <- treeViewColumnNew
imgCol <- treeViewColumnNew
colCombo <- treeViewColumnNew
treeViewColumnSetTitle imgCol ("Image column"::T.Text )
treeViewColumnSetTitle colName ("String column"::T.Text )
treeViewColumnSetTitle colCombo ("Combo"::T.Text )
iconRenderer <- cellRendererPixbufNew
renderer1 <- cellRendererTextNew
comboRenderer <- cellRendererComboNew
cellLayoutPackStart imgCol iconRenderer True
cellLayoutPackStart colName renderer1 True
cellLayoutPackStart colCombo comboRenderer True
cellLayoutSetAttributes imgCol iconRenderer lststoreManus $ (\ListElement { selected = t } -> [cellPixbuf := t])
cellLayoutSetAttributes colName renderer1 lststoreManus $ \row -> [ cellText := name row ]
cellLayoutSetAttributeFunc colCombo comboRenderer lststoreManus $
(\iter -> do (tmodel, colid) <- comboTextModel
(ListElement a b) <- treeModelGetRow lststoreManus iter
set comboRenderer [ cellVisible := True
, cellComboTextModel := (tmodel, colid)
, cellTextEditable := True
, cellComboHasEntry := False
, cellText := ("Choose pixbuf"::String)])
treeViewAppendColumn treeview colName
treeViewAppendColumn treeview imgCol
treeViewAppendColumn treeview colCombo
_ <- on comboRenderer editingStarted $ \widget treepath -> do
case treepath of
[k] -> do
let comboPix::ComboBox = castToComboBox widget
lststorerep::(ListStore (String,Pixbuf)) <- listStoreNew pixList
customStoreSetColumn lststorerep (makeColumnIdString 0) fst
customStoreSetColumn lststorerep (makeColumnIdPixbuf 1) snd
comboBoxSetModel comboPix (Just lststorerep)
rendertxt <- cellRendererTextNew
renderpic <- cellRendererPixbufNew
cellLayoutPackStart comboPix rendertxt False
cellLayoutPackStart comboPix renderpic True
cellLayoutAddColumnAttribute comboPix renderpic cellPixbuf $ makeColumnIdPixbuf 1
_ <- on comboRenderer edited $ \_treePath newStringValue -> do
case _treePath of
[k] -> do
(ListElement a b) <- listStoreGetValue lststoreManus k
myMap <- getMap
let finded = fromJust ( Map.lookup newStringValue myMap )
let toStore = ListElement a finded
listStoreSetValue lststoreManus k toStore
putStrLn $ "new value: " ++ newStringValue
fixedPut fixed treeview (10,10)
widgetSetSizeRequest treeview 500 100
containerAdd window fixed
onDestroy window mainQuit
windowSetDefaultSize window 600 500
windowSetPosition window WinPosCenter
widgetShowAll window
mainGUI
comboTextModel = do store <- listStoreNew []
let column = makeColumnIdString 0 :: ColumnId String String
return (store, column)
{-
dependencies :
- base >= 4.7 && < 5
- gtk
- text
- containers
-}
I have written a small application which tracks my progress in TV Series. The application is written in Haskell with functional reactive programming (FRP) with reactive banana.
The application can:
add/remove new TV Series to the table
change the season and episode of an series
I have problems writing the code that adds a new TV series to the table and wires the new events. The CRUD example from here didn't quite help me because I have more requirements then just selecting an element from the list.
How do I write a reactiveTable function like the reactiveListDisplay function from the CRUD Example in a FRP way? How can events be added for the remove button and the season and episode spin buttons after the network has been compiled?
data Series = Series { name :: String
, season :: Int
, episode :: Int
}
insertIntoTable :: TableClass t => t -> SeriesChangeHandler -> SeriesRemoveHandler -> Series -> IO ()
insertIntoTable table changeHandler removeHandler (Series name s e) = do
(rows, cols) <- tableGetSize table
tableResize table (rows+1) cols
nameLabel <- labelNew $ Just name
adjustmentS <- adjustmentNew (fromIntegral s) 1 1000 1 0 0
adjustmentE <- adjustmentNew (fromIntegral e) 1 1000 1 0 0
seasonButton <- spinButtonNew adjustmentS 1.0 0
episodeButton <- spinButtonNew adjustmentE 1.0 0
removeButton <- buttonNewWithLabel "remove"
let getSeries = do
s <- spinButtonGetValue seasonButton
e <- spinButtonGetValue episodeButton
return $ Series name (round s) (round e)
handleSeries onEvent widget handler = do
onEvent widget $ do
series <- getSeries
handler series
handleSeries onValueSpinned seasonButton changeHandler
handleSeries onValueSpinned episodeButton changeHandler
onPressed removeButton $ do
series <- getSeries
containerRemove table nameLabel
containerRemove table seasonButton
containerRemove table episodeButton
containerRemove table removeButton
removeHandler series
let tadd widget x = tableAdd table widget x (rows - 1)
tadd nameLabel 0
tadd seasonButton 1
tadd episodeButton 2
tadd removeButton 3
widgetShowAll table
main :: IO ()
main = do
initGUI
window <- windowNew
scroll <- scrolledWindowNew Nothing Nothing
table <- tableNew 1 5 True
addButton <- buttonNewWithLabel "add series"
vbox <- vBoxNew False 10
containerAdd window vbox
boxPackStart vbox addButton PackNatural 0
let networkDescription :: forall t. Frameworks t => Moment t ()
networkDescription = do
addEvent <- eventButton addButton
(changeHandler,fireChange) <- liftIO $ newAddHandler
changeEvent <- fromAddHandler changeHandler
(removeHandler,fireRemove) <- liftIO $ newAddHandler
removeEvent <- fromAddHandler removeHandler
let insertIntoTable' = insertIntoTable table fireChange fireRemove
addSeries e = do
s <- addSeriesDialog
liftIO $ insertIntoTable' s
liftIO $ mapM_ insertIntoTable' initSeries
reactimate $ addSeries <$> addEvent
reactimate $ updateSeries conn <$> changeEvent
reactimate $ removeSeries conn <$> removeEvent
network <- compile networkDescription
actuate network
onDestroy window $ do
D.disconnect conn
mainQuit
widgetShowAll window
mainGUI
I want to refactor the insertIntoTable method to use events and behaviors rather than using simple callbacks.
EDIT:
I have tried the gtk TreeView with a ListStore backend. In this scenario you don't need dynamic event switching. I have written the reactiveList function below to get a list behavior out of insert, change and remove events. It works ^^
reactiveList :: (Frameworks t)
=> ListStore a
-> Event t (Int,a) -- insert event
-> Event t (Int,a) -- change event
-> Event t (Int,a) -- remove event
-> Moment t (Behavior t [a])
reactiveList store insertE changeE removeE = do
(listHandler,fireList) <- liftIO $ newAddHandler
let onChange f (i,a) = do
f i a
list <- listStoreToList store
fireList list
reactimate $ onChange (listStoreInsert store) <$> insertE
reactimate $ onChange (listStoreSetValue store) <$> changeE
reactimate $ onChange (const . listStoreRemove store) <$> removeE
initList <- liftIO $ listStoreToList store
fromChanges initList listHandler
main :: IO ()
main = do
initGUI
window <- windowNew
addButton <- buttonNewWithLabel "add series"
vbox <- vBoxNew False 10
seriesList <- listStoreNew (initSeries :: [Series])
listView <- treeViewNewWithModel seriesList
treeViewSetHeadersVisible listView True
let newCol title newRenderer f = do
col <- treeViewColumnNew
treeViewColumnSetTitle col title
renderer <- newRenderer
cellLayoutPackStart col renderer False
cellLayoutSetAttributes col renderer seriesList f
treeViewAppendColumn listView col
return renderer
newCol "Image" cellRendererPixbufNew $ \s -> [cellPixbuf :=> newPixbuf s]
newCol "Name" cellRendererTextNew $ \s -> [cellText := name s]
seasonSpin <- newCol "Season" cellRendererSpinNew $ \s ->
[ cellRendererSpinAdjustment :=> adjustmentNew (fromIntegral (season s)) 1 1000 1 0 0
, cellText := (show $ season s)
, cellTextEditable := True
]
episodeSpin <- newCol "Episode" cellRendererSpinNew $ \s ->
[ cellRendererSpinAdjustment :=> adjustmentNew (fromIntegral (episode s)) 1 1000 1 0 0
, cellText := (show $ episode s)
, cellTextEditable := True
]
containerAdd window vbox
boxPackStart vbox listView PackGrow 0
boxPackStart vbox addButton PackNatural 0
let networkDescription :: forall t. Frameworks t => Moment t ()
networkDescription = do
(addHandler,fireAdd) <- liftIO $ newAddHandler
maybeSeriesE <- fromAddHandler addHandler
(removeHandler,fireRemove) <- liftIO $ newAddHandler
removeE <- fromAddHandler removeHandler
-- when the add button was pressed,
-- open a dialog and return maybe a new series
askSeriesE <- eventButton addButton
reactimate $ (const $ fireAdd =<< askSeries) <$> askSeriesE
-- ommit all nothing series
let insertE = filterJust maybeSeriesE
insert0E = ((,) 0) <$> insertE
seasonSpinE <- eventSpin seasonSpin seriesList
episodeSpinE <- eventSpin episodeSpin seriesList
let changeSeason (i,d,s) = (i,s {season = round d})
changeEpisode (i,d,s) = (i,s {episode = round d})
let changeE = (changeSeason <$> seasonSpinE) `union` (changeEpisode <$> episodeSpinE)
listB <- reactiveList seriesList insert0E changeE removeE
listE <- (changes listB)
reactimate $ (putStrLn . unlines . map show) <$> listE
reactimate $ insertSeries conn <$> insertE
reactimate $ updateSeries conn . snd <$> changeE
reactimate $ removeSeries conn . snd <$> removeE
return ()
network <- compile networkDescription
actuate network
onDestroy window $ do
D.disconnect conn
mainQuit
widgetShowAll window
mainGUI
I'm open for comments and suggestions.
It sounds like your problem is much closer to the Bar Tab example than the CRUD one.
The basic idea for adding new widgets--along with new behaviors and events--is to use so-called "dynamic event switching". Essentially, this is a way to put newly created events and behaviors back into your network.
The action to create a new widget has two parts. The first part is to just create the widget, using liftIO. The second is to get its inputs and use trimE or trimB as appropriate. Leaving out most of the GTk-specific details (I don't know how to use GTk :P), it'll look something like this:
let newSeries name = do
label <- liftIO . labelNew $ Just name
liftIO $ tadd labelNew 0
{- ... the rest of your controls here ... -}
seasonNumber <- trimB $ getSpinButtonBehavior seasonButton
{- ... wrap the rest of the inputs using trimB and trimE ... -}
return (label, seasonNumber, ...)
So this function creates a new widget, "trims" its inputs and returns the values to you. Now you have to actually use these values:
newSeasons <- execute (FrameworkMoment newSeries <$> nameEvents)
here nameEvents should be an Event String containing an event with the name of the new series each time you want to add it.
Now that you have a stream of all of the new seasons, you can combine it all into a single behavior of a list of entries using something like stepper.
For more details--including things like getting the aggregate information out of all of your widgets--look at the actual example code.
main :: IO()
main = runCurses $ do
setEcho False
w <- defaultWindow
canvas <- newWindow 19 19 0 0
panel <- newPanel canvas
updateWindow canvas $ do
drawString "Hello world!"
render
waitFor w (\ev -> ev == EventCharacter 'q' || ev == EventCharacter 'Q')
waitFor :: Window -> (Event -> Bool) -> Curses ()
waitFor w p = loop where
loop = do
ev <- getEvent w Nothing
case ev of
Nothing -> loop
Just ev' -> unless (p ev') loop
Hello. It is a bit modified example of ncurses package.
Problem is that is do not display anything. In C API, as I remember, I have to refresh each window independently. But Haskell offer only render. Where I am wrong? I want to have some moveable widget.
You have just to call refreshPanels.
When I create two windows and redraw them in two different threads (one per window), it seems like all drawing goes to first created window. It constantly switches between what should be displayed in both windows. And second one remains mostly black.
The code was working well with only one window, and then I updated it - inserted currentWindow $= Just glWindow in the beginning of the functions which set callbacks and call rendering methods.
What do you think is the cause of the problems?
EDIT:
Code skeleton:
module Chart.Window where
import Graphics.UI.GLUT hiding (Window, postRedisplay, <etc>)
import qualified Graphics.UI.GLUT as GLUT
import qualified Graphics.Rendering.OpenGL as GL
data Window = Window
{ glWindow :: GLUT.Window
, viewListRef :: IORef [Line]
}
main = do
forkOS start <params1>
forkOS start <params2>
start <params> = do
win <- new <params>
run win
mainLoop
new :: Strict -> (Int, Int) -> (Int, Int) -> IO Window
new name (posx, posy) (w, h) = do
initGLUT
glWindow <- createWindow name
currentWindow $= Just glWindow
windowSize $= Size (fromIntegral w) (fromIntegral h)
windowPosition $= Position (fromIntegral posx) (fromIntegral posy)
return Window {..}
initGLUT :: IO ()
initGLUT = do
beenInit <- get initState
unless beenInit $ void getArgsAndInitialize
initialDisplayMode $= [WithDepthBuffer, DoubleBuffered, RGBAMode]
initialWindowSize $= Size 100 100
initialWindowPosition $= Position 100 100
actionOnWindowClose $= ContinueExectuion
run :: Window -> IO ()
run win#Window{..} = do
-- this will fork (with forkIO) threads
-- which will call "repaint" when chart needs to be updated
initListeners repaint
initCallbacks win
where
repaint :: [Line] -> IO ()
repaint viewList = do
writeIORef viewListRef viewList
postRedisplay win
postRedisplay Window{..} = GLUT.postRedisplay $ Just glWindow
initCallbacks win#Window{..} = do
currentWindow $= Just glWindow
GLUT.displayCallback $= display win
GLUT.idleCallback $= Just (postRedisplay win)
display Window{..} = do
currentWindow $= Just glWindow
Size w h <- get windowSize
viewList <- readIORef viewListRef
drawChart viewList otherOptions
reshapeCallback :: Window -> GLUT.ReshapeCallback
reshapeCallback win#Window{..} size#(Size w h) = do
currentWindow $= Just glWindow
GL.viewport $= (Position 0 0, size)
GL.matrixMode $= GL.Projection
GL.loadIdentity
GL.ortho2D 0 (fromIntegral w) 0 (fromIntegral h)
GL.matrixMode $= GL.Modelview 0
GL.loadIdentity
... -- send command for listener thread to change internal state and postRedisplay
drawChart viewList otherOptions = do
...
-- chart consists of several horizontal panels. Call this for each panel:
glViewport 0 panelYPosition width winHeight
glScissor 0 panelYPosition (fromIntegral width) (fromIntegral panelHeight)
GL.clear [GL.ColorBuffer]
...
-- and then for each line=(Vertex2 v1 v2) from viewList
GL.renderPrimitive GL.Lines $ do
GL.vertex v1
GL.vertex v2
...
BTW, when I commented the line which sets reshapeCallback (and window is reshaped at the beginning) and launched charting with only one window, I got exactly the same effect as in multi-window launch. I mean, the (only) window was mostly empty as if it was secondly created.
I had a similar problem. I work with a thread that calculates the iterations of a genetic algorithm and in each iteration I call to "GL.postRedisplay (Just window)" but it didn't draw anything.
I solved my problem by calling "GL.postRedisplay (Just window)" from the idle function:
idle window = CC.threadDelay (1000*500) >> GL.postRedisplay (Just window)
Don't forget to setup your idle callback function like this:
GL.idleCallback GL.$= Just (idle window) >>
CC and GL mean:
import qualified Control.Concurrent as CC
import qualified Graphics.UI.GLUT as GL