アイコンを表示(Twitterアイコンを取得→表示)
かなり空きましたが、投げたわけではないですよ?
大学始まったので忙しいのです。
さて、何をやっていたかというと、ちょっと混沌とし始めたコードを若干整理したのと、今まで改行だけでダラーッと一つのStringをプリントするだけだったタイムライン表示を、個別の領域に割り当てて表示するようにしました。
で、ここまで出来たのだから、そろそろ絵的に映えるものが欲しいと思い、アイコン画像を取得して表示するようにしました。
まず、画像の取得はおいておいて、画像がファイルにあった場合。
画像の表示は、流石にファイルをそのまま表示することは出来ませんが
ファイル名からPixBufを作成→それを描画
という簡単な処理で済みます。
ファイル名からPixBufを作成するのは
Graphics.UI.Gtk.Gdk.Pixbuf.pixbufNewFromFile :: FilePath -> IO Pixbuf
で行い、PixBufの表示は
Graphics.UI.Gtk.Gdk.Drawable.drawPixbuf :: DrawableClass d => d -> GC -> Pixbuf -> Int -> Int -> Int -> Int -> Int -> Int -> Dither -> Int -> Int -> IO ()
で行います。
後者の関数がちょっとわかりづらいですね。このページ
http://d.hatena.ne.jp/kakurasan/20100319/p1
のコード例が、言語は違えど役に立ちました。(gtk2hsのバージョンにはDither型の引数が追加されています)
引数の意味はそれぞれ
drawPixbuf ::
(d: 描画ウィジェット)
(GC: グラフィカルコンテキスト)
(Pixbuf: 表示する画像のPixbuf)
(Int Int: 画像の切り取り開始位置の座標)
(Int Int: 描画領域の描画開始位置の座標)
(Int Int: 画像から切り出す大きさ)
(Dither: ディザリングの指定。None, Normal, Maxから選べる)
(Int Int: 表示する領域の大きさ。-1を指定すると画像の大きさと同じになる)
となっているようです。
画像を表示させる処理はこれでいいとして、問題はその表示させるファイルを取得する部分で、これは当然ダウンロードして手に入れることになります。
各々のツイートには発現者のアイコンのURLが入っているので、これにアクセスして画像を手に入れればいいわけです。
いままで画像をローカルにダウンロードするプログラムを書いたことがなかったのですが、「ネット上から複数の画像ファイルをダウンロードする」というプログラムにはいいサンプルがありますね。どんな言語でもたいてい誰かがこのプログラム書いてます。
いかにしておっぱい画像をダウンロードするか~2012 Haskell編
http://d.hatena.ne.jp/D_Rascal/20120320/1332244651
はい。並列化もしてますし、かなりいいサンプルですよね。
Conduit版もあったような気がしましたが、普通にNetwork.HTTPでやることにしました。
で、これでいいんですが、いちいちアイコン画像を全部ダウンロードしてくるのは明らかに重いので、URLをハッシュテーブルに保存して一回ダウンロードした画像はローカルに保存したものを使うようにしました。
さらに、ローカルに保存したアイコンは消さないでおき、前回起動時に保存したアイコンがあればそれを使うようにしました。(しかしこれだと、アイコンがいつまでたっても積もり積もってしまうことになるので、そこは後で考えなければならない所)
この部分のコードはこんな感じ
これでアイコンが表示できるようになりました。
今回までのコード
https://github.com/xenophobia/hshstter/tree/660407074e297412b090d0e0a85c0ca2a66f9f29
なお現在、RT・ふぁぼの機能を実装しようとしていて、その手前のGUIまわりで詰まってる所です。
できたら記事書きますが。
DrawingAreaの描画について
タイムラインをDrawingAreaに描画する仕様に変えた際(http://hshstter.hatenablog.com/entry/2012/04/14/190927)に、ちょっとまずいことをしていたようなのでメモ。
DrawingAreaの再描画を行うのに、いままで
Graphics.UI.Gtk.Gdk.DrawWindow.drawWindowClearAreaExpose ::
DrawWindowClass self => self -> Int -> Int -> Int -> Int -> IO ()
を使っていたのだが、これだと一旦DrawingAreaをクリアしてから再描画するので、これではなく
Graphics.UI.Gtk.Abstract.Widget.widgetQueueDraw :: WidgetClass self => self -> IO ()
を使うべきでした。
前者のほうだとタイムライン更新時に一瞬画面が白くクリアされるのが見えてしまっていましたが、これで解決。
「.gladeファイルのデータ→GUIの各ウィジェット読込」のコードをTHで自動生成する。
前記事(http://hshstter.hatenablog.com/entry/2012/04/15/121001)でTemplate Haskellを使ってみましたが、Mainモジュールにはもっと大きなboilerplateがあるので、それを何とか出来ないかと思ってやってみました。今まで気になっていたけどどうしても直せなかった箇所です。
現在、GUIのデータは次のようなmonolithicな型で管理しています。
-- GUIデータ型
data GUI = GUI {
mainWin :: !Window,
information :: !Label,
tweetEntry :: !Entry,
tweetButton :: !Button,
timelineWin :: !ScrolledWindow,
timeline :: !(IORef [Timeline]),
accessTokenGetWin :: !Window,
hint :: !Label,
authorizationButton :: !Button,
cancelButton :: !Button,
pinEntry :: !Entry,
authorizationURL :: !Entry
}
で、これらデータの読み込みはtimelineを除いてGladeで出力したxmlデータから次のように読み込んでいます。
xmlNewIO :: FilePath -> IO GladeXML
xmlNewIO gladePath = do
maybeXML <- xmlNew gladePath
case maybeXML of
Nothing -> fail "XML format error."
Just xml -> return xml
-- .gladeファイル(XML形式)をロード・GUI型に変換
loadGlade :: FilePath -> IO GUI
loadGlade gladePath = do
-- XMLをロード
xml <- xmlNewIO gladePath
mainWin <- xmlGetWidget xml castToWindow "mainWin" -- メインウインドウをロード
information <- xmlGetWidget xml castToLabel "information" -- 情報
tweetEntry <- xmlGetWidget xml castToEntry "tweetEntry" -- ツイート入力部をロード
tweetButton <- xmlGetWidget xml castToButton "tweetButton" -- ツイートボタンをロード
timelineWin <- xmlGetWidget xml castToScrolledWindow "timelineWin" -- タイムライン表示部をロード
accessTokenGetWin <- xmlGetWidget xml castToWindow "accessTokenGetWin" -- Access Token取得ウインドウをロード
hint <- xmlGetWidget xml castToLabel "hint" -- 認証用メッセージをロード
authorizationButton <- xmlGetWidget xml castToButton "authorizationButton" -- Authorizationボタンをロード
cancelButton <- xmlGetWidget xml castToButton "cancelButton" -- Cancelボタンをロード
pinEntry <- xmlGetWidget xml castToEntry "pinEntry" -- PIN入力部をロード
authorizationURL <- xmlGetWidget xml castToEntry "authorizationURL" -- 認証用URL表示
timeline <- newIORef
return $ GUI mainWin information tweetEntry tweetButton timelineWin timeline
accessTokenGetWin hint authorizationButton cancelButton pinEntry authorizationURL
このロード→キャスト部は次のようなほとんど同じ構造をしています。
(guiField :: GuiTypeの読み込み)
guiField <- xmlGetWidget xml castToGuiType "guiField"
そこで、この部分を自動生成するマクロを書いてみました。
まず、GUI型からタイムラインのリストtimelineを分離し(考えてみたらGUIウィジェットでは無いのでこの型のフィールドになってるのはおかしいですね……)、GUI型をウィジェット名に対応したフィールドのみの型にします。
-- GUIデータ型
data GUI = GUI {
mainWin :: !Window,
information :: !Label,
tweetEntry :: !Entry,
tweetButton :: !Button,
timelineWin :: !ScrolledWindow,
accessTokenGetWin :: !Window,
hint :: !Label,
authorizationButton :: !Button,
cancelButton :: !Button,
pinEntry :: !Entry,
authorizationURL :: !Entry
}
で、次のような感じで、パーツを態々指定せずにxmlデータをGUI型に一発でキャストしたい。
-- .gladeファイル(XML形式)からGUIウィジェットロード・GUI型に変換
loadGUI :: FilePath -> IO GUI
loadGUI = $(castToGUI ''GUI) <=< xmlNewIO
これを実現するためには、
(1)データ名GUI( :: Name)から各フィールドの名前を取得(2)ロード→キャスト部テンプレの列を自動生成(3)GUI構成子に包んで返す
の手順を踏めばいいだけです。(2)(3)はただ前回記事でやったように構文木を構成すればいいだけの話で、(1)もreify関数を使えば出来ます。
reify :: Name -> Q Info
この関数に先のGUI型を突っ込むと次のような情報が得られます。
TyConI (DataD Main.GUI [RecC Main.GUI [(Main.mainWin,IsStrict,ConT Graphics.UI.Gtk.Types.Window),(Main.information,IsStrict,ConT Graphics.UI.Gtk.Types.Label),(Main.tweetEntry,IsStrict,ConT Graphics.UI.Gtk.Types.Entry),(Main.tweetButton,IsStrict,ConT Graphics.UI.Gtk.Types.Button),(Main.timelineWin,IsStrict,ConT Graphics.UI.Gtk.Types.ScrolledWindow),(Main.accessTokenGetWin,IsStrict,ConT Graphics.UI.Gtk.Types.Window),(Main.hint,IsStrict,ConT Graphics.UI.Gtk.Types.Label),(Main.authorizationButton,IsStrict,ConT Graphics.UI.Gtk.Types.Button),(Main.cancelButton,IsStrict,ConT Graphics.UI.Gtk.Types.Button),(Main.pinEntry,IsStrict,ConT Graphics.UI.Gtk.Types.Entry),(Main.authorizationURL,IsStrict,ConT Graphics.UI.Gtk.Types.Entry)]] )
データ名と構成子の名前が一致しているのでわかりづらいですが、青で示したほうがデータ構成子で、紫で示したほうがデータ名です。黄色で示した部分は各フィールドに関する情報のリストで、それぞれ
type VarStrictType = (Name, Strict, Type)
Name: フィールド名
Strict: 正格かどうかのフラグ
Type: フィールドの型
という 形式をしています。
次に、do式の構文木ですが、試しに
do
x <- newIORef ""
y <- newIORef 0
z <- newIORef 1
return ()
の構文木を調べると
DoE [
BindS (VarP x_0) (AppE (VarE GHC.IORef.newIORef) (LitE (StringL ""))),
BindS (VarP y_1) (AppE (VarE GHC.IORef.newIORef) (LitE (IntegerL 0))),
BindS (VarP z_2) (AppE (VarE GHC.IORef.newIORef) (LitE (IntegerL 1))),
NoBindS (AppE (VarE GHC.Base.return) (ConE GHC.Tuple.()))
]
となっているようです。見た目にはわかりやすいですね。変数束縛している行がBindS、していない行がNoBindSで表されているようです。
これに沿ってGUI読込部の構文木を生成するマクロを書くとこうなります。
これを先ほど示したインタフェースで呼び出すと、GUI部品の読み込みに成功しました!
最近はMainモジュールがごちゃごちゃしていたのでリファクタリングにとりくんでいたのですが、やっぱりTHはboilerplate潰しが捗りますね。ちなみに今回のcommitでMainモジュールが200行切りました。
全体のコード
https://github.com/xenophobia/hshstter/tree/35b64dd2f9b860a8ce456deaa2996a4fecdc8dab
Template Haskellでタプル版map, mapMっぽいものを作ってみた。
小ネタ。
Mainモジュールに次のようなコード
(body, width, height) <- (,,) <$> newIORef "" <*> newIORef 300 <*> newIORef 0 (tlBody, tlWidth, tlHeight) <- (,,) <$> readIORef body <*> readIORef width <*> readIORef height
があり、ふと「タプル版mapMみたいなものがあればもっとすっきり書けないか?」と思い、Template Haskellの練習がてらタプル版のmapとmapMを作ってみました。2,3コのタプルにだけ定義するのもつまらなかったので、個数は一般としました。
Template Haskellの書き方はこの記事が大変参考になりました。
できる!Template Haskell(完)- はてな使ったら負けだと思っている deriving Haskell
http://haskell.g.hatena.ne.jp/mr_konn/20111218/1324220725
まずは任意長のタプルに対するmap, mapT(uple)を。ちなみにインターフェースはmapT, mapMT共にInt(タプルの長さ) -> ExpQ(適用する関数) -> ExpQとしました。$(mapT 3 [|show|]) (1, '2', "3")みたいな使い方を想定しています。
参考記事に倣ってまずは(\(x, y, z) -> (f x, f y, f z))の構文木を調べると
LamE [TupP [VarP x_3,VarP y_4,VarP z_5]] (TupE [AppE (VarE f_1627404183) (VarE x_3),AppE (VarE f_1627404183) (VarE y_4),AppE (VarE f_1627404183) (VarE z_5)])
となっており、整理して個数を一般化すると
(f :: Exp とする)
LamE
[TupP [VarP x_0,VarP x_1, ..]]
(TupE
[AppE f (VarE x_0),AppE f (VarE x_1), ..]
)
となります。この構文木を生成するプログラムを作ればいいのでこんな感じになります。
fleshな識別子の供給は
Language.Haskell.TH.Syntax.newName :: String -> Q Name
で行い、構文木の生成には構文木の構成子の頭文字を小文字にしたものを使えばOKです。
これを外部ライブラリとして読んで、次のように使えます。
> $(mapT 3 [|show|]) (1,[2,3],'4')
("1","[2,3]","'4'")
> $(mapT 3 [|(:[])|]) (1,[2,3],'4')
([1],[[2,3]],"4")
式クオートとして渡しているので、上のように返り値の型がタプルの各要素で異なっていても使えます。
次にmapMを書きます。 \(x,y,z) -> (<*>) *1 (f y)) (f z) の構文木を調べると
LamE [TupP [VarP x_6,VarP y_7,VarP z_8]] (AppE (AppE (VarE Control.Applicative.<*>) (AppE (AppE (VarE Control.Applicative.<*>) (AppE (AppE (VarE Data.Functor.<$>) (ConE GHC.Tuple.(,,))) (AppE (VarE f_1627397566) (VarE x_6)))) (AppE (VarE f_1627397566) (VarE y_7)))) (AppE (VarE f_1627397566) (VarE z_8)))
これを整理して個数について一般化すると、
LamE
[TupP [VarP x_0,VarP x_1, ..]]
(AppE (AppE (VarE <*>)
..
(AppE (AppE (VarE <*>)
(AppE (AppE (VarE <*>)
(AppE (AppE (VarE <$>)
(ConE (,, ..)))
(AppE f (VarE x_0)))
(AppE f (VarE x_1))))
(AppE f (VarE x_2))))
..
(AppE f (VarE x_n))))
わかりづらいですが、(ConE (,, ..))を開始値として
\x y ->
(AppE (AppE (VarE <*>【または <$>】)
x)
(AppE f (VarE y)))
を演算として変数のリストを畳み込んだ形になっています。
厄介なのは最初のみ<*>ではなく<$>である場合があることですが、演算子のリスト[<$>, <*>, <*>, ..]をzipして一緒に畳み込むことで解決しました。ちょっと強引な解決法ですが……
これを用いて、最初のboilerplateはこんなふうに書けます。
(body, width, height) <- $(mapMT 3 [|newIORef|]) ("", 300, 0)
(tlBody, tlWidth, tlHeight) <- $(mapMT 3 [|readIORef|]) (body, width, height)
なかなかすっきりしました。newIORefやreadIORefが一箇所にまとまっているのがいい感じです。
というわけでTemplate Haskellを試してみたという記事でした。
今後も使えるところがあれば使っていこうと思いますので、THUtilityモジュールとして専用のモジュールにTemplate Haskellを用いたユーティリティをまとめておくことにしました。
DrawingAreaにタイムラインを表示できた
前回(http://hshstter.hatenablog.com/entry/2012/04/12/232132)挑戦して解決しなかったDrawingArea更新の件が解決しました。
わかったことは、
(1)onExposeイベントにセットしたアクションは、drawWindowClearAreaExposeなどで再描画するとちゃんと実行される。
(2)たとえば、変数xに依存するアクションact(x)をonExposeイベントにセットして実行する場合、
act(a)をonExposeにセットして、act(b)を再度onExposeにセットして再描画しても、act(a)の結果が表示される。
→おそらく、2回目以降のonExposeの実行は無視される?(かなり怪しい。それ以外の問題があるのかもしれない)
なので、前回のようなコードではダメで、解決法として、 更新する可能性がある変数をIORefで持っておいて、drawWindowClearAreaExposeでonExposeにセットしたアクションが実行される際に表示するテキストをreadIORefで読み込み直す という実装にするとちゃんと更新されました。
つまりたとえば
str <- makeString
onExpose $ \_ -> do
drawString str
ではなく
onExpose $ \_ -> do
str <- readIORef stringRef
drawString str
とすれば、再描画時にDrawingArea内が変更されます。(変更したい場合は、ここで指定したstringRefの内容をwriteIORefで書き換えればよい)
タイムラインを追加する関数(addTimeline)/タイムラインを更新する関数(updateTimeline)を分け、タイムライン追加関数でonExposeイベントにタイムラインを表示し、更新はupdateTimeline関数でdrawWindowClearAreaExposeアクションを実行することで行うようにしました。
ただ、この実装のままだとちょっと問題があって、2つ以上タイムラインを表示させたい時(List, Mentionなど)に追加できないという問題があります。将来的にはそういう機能は付けたいので、GUIデータ型にある「タイムライン一覧」をIORefにし、onExposeでこの一覧を読み込んですべて表示する、という変更が必要になると思います。(その場合、addTimelineではonExposeイベントへのアクションの設定は行わず、GUIのタイムライン一覧への追加のみ行うようにすればいいですね)
それにしても、副作用ばかりでHaskellっぽくないコードができつつありますね……何とかしたいです。
DrawAreaにタイムラインを表示したい(Pangoで文字列描画)
ちょっと現状報告。
ずいぶん間が開いてしまいましたが、別にやめたわけではないです。
タイムラインにアイコンやらボタンやらを表示させたい関係で、タイムラインの表示方法をTextAreaからDrawAreaに変更することを目論んでコードを書き換えているのですが……
色々とわからない事だらけ。とりあえず以下のような文字列を表示する関数を書きました。
(マジックナンバーは適当。暫定的なものなので後で書き換えます。)
しかし、これを単純に使っても、DrawAreaには何も描画されませんでした。
下の関数はタイムラインを描画する関数で、タイマで30秒ごとに起動します。
しかし、これを次のように変更するとちゃんと表示されます。
つまり、PangoLayoutを描画する関数をonExposeで起動するようにし、drawWindowの再描画はonExposeの外で行う(このdrawWindowClearAreaExposeアクションまでonExposeに結びつけてしまうとダメでした。)
ところが、これだとタイムラインが更新されません(再描画はされているらしいが内容が同じなので見た目が変わらない)
このへんいろいろ試しているのですが、未だに打開策を見いだせず。
Gtkについてもうちょっと勉強が必要なようです……
ファイル入出力をConduitに
メインモジュールがごちゃごちゃしてきたのでリファクタリング中。
ファイル入出力に関わる部分をConduitを用いたコードに直してみました。
最近Hotなので練習の意味も兼ねて。
以下のPDFを参照しました。
http://mew.org/~kazu/material/2012-conduit.pdf
とりあえず、Source型とSink型を$$演算子でくっつけて、runResourceTで走らせればいいと。
・ファイルから複数行に渡るデータを入力
Data.Conduit.Binary.sourceFile ::
MonadResource m =>
FilePath -> Source m Data.ByteString.Internal.ByteString
ファイルを指定してファイルの内容を供給するSourceを生成
Data.Conduit.Binary.lines ::
Monad m =>
Conduit
Data.ByteString.Internal.ByteString
m
Data.ByteString.Internal.ByteString
文字列データ(ByteString)を '\n' で区切る。このあとData.Conduit.Listの関数を適用すると、この一区切りが1単位として処理される。
例)
出力)
>test0
DATA: data0
data1
data2
data3
>test1
DATA: data0DATA: data1DATA: data2DATA: data3
Data.Conduit.List.map :: Monad m => (a -> b) -> Conduit a m b
map関数のConduit版。Sourceから得られるデータに対し第一引数の関数を適用する。
Data.Conduit.List.take :: Monad m => Int -> Sink a m [a]
take関数のConduit版、指定した個数だけ値を消費するSinkを生成。
で、上の関数を使うとこんなふうになります。アクセストークン及びユーザ情報を取り出す関数。
・Stringのリストを複数行にファイル出力
Data.Conduit.List.sourceList :: Monad m => [a] -> Source m a
リストの値を供給するSourceを生成
Data.Conduit.Binary.sinkFile ::
MonadResource m =>
FilePath -> Sink Data.ByteString.Internal.ByteString m ()
流れてきたデータをファイルへ書き込むSinkを生成。
さっきのファイル出力と逆のことをやればいい訳ですが、Data.List.unlinesにあたるConduitがなかったので、代わりにmapで文字列末尾に改行を付け足しました。
unlinesに当たるConduitを書こうとも思ったんですが、書けませんでした……