NimでWindowsアプリを作ってみた 続編
前回の記事が好評(?)だったみたいで、wNimの続編を記載していこうと思いました。
まあ、単にwNimは機能が多すぎるため、一回で説明出来なかっただけですけどね。ですので、前回はレイアウト回りの話を主にして、今回はイベント制御回りの内容を記載します。
主にタイマーや描画の制御と言った所です。
環境
私のPC環境はWindowsで動作させています。
- OS: Windows11
- Nim 2.0.6
- wNim 0.13.3
- arrarymancer 0.7.32
wNimでGUIアプリを作ってみる
1. タイマーイベント処理
まずは、タイマーイベントについて説明します。
下記のプログラムでは、画面に関しては、AutoLayoutを利用せずに、テキストを画面の固定位置に設定し、
タイマーイベントが発生した時に、現在時間を描画させているプログラムです。
イベント発生は、startTimer関数
で発生させる時間を設定しています。
描画位置が固定ですので、画面リサイズを行っても表示する位置は変わりません。
#[
時間表示アプリ
]#
import resource
import wNim/[wApp, wFrame, wPanel, wStaticText, wFont]
import std/times
let app = App()
let frame = Frame(title="時間表示", size=(380, 200))
let panel = Panel(frame)
panel.setBackgroundColor(wLightSteelBlue) # パネルのバックカラー
# タイトル設定 (固定位置を設定)
let timeLbl = StaticText(panel, label="現在の時間を表示", pos=(20, 10), size=(300, 40))
timeLbl.font = Font(22, faceName="メイリオ", weight=wFontWeightNormal)
timeLbl.setForegroundColor(wYellow) # テキストカラーを設定
# 時刻表示 (固定位置を設定)
let timeCtrl = StaticText(panel, label="00:00:00", pos=(20, 50), size=(320, 70), style=wAlignCenter)
timeCtrl.font = Font(48, faceName="メイリオ", weight=wFontWeightBold)
timeCtrl.setForegroundColor(wDarkOliveGreen) # 時間表示の色設定
# 1秒単位にイベントを発生
frame.startTimer(1.0)
# イベント制御
frame.wEvent_Timer do (event: wEvent): # TimerEventの時だけdoに引数を入れる
let dt = now() # 現在時間表示
timeCtrl.setLabel(dt.format("HH:mm:ss")) # ラベルに時間表示
# auto layoutは使わない設定
frame.center()
frame.show()
app.mainLoop()
$ nim c -r -d:release -d:strip --opt:size --app:gui sample11.nim
- 出力結果
2. 画像表示処理
次に、画像の表示についてですが、下記の画像プログラムは、画像を読み込み後に予めメモリに記載し、描画イベント(refresh関数
が呼ばれた時)が発生した時に、PaintDCに書き込むと言ったプログラムです。
画像を掴んでマウス移動した状態の時に、描画イベントを呼び出し、メモリ上のデータを画面に書き込んでいるだけですけどね。
一応、ダブルバッファ制御で描画していますが、画像の移動時に若干のちらつきはありますが、シングルバッファよりはマシって程度ですかね。
#[
image ダブルバッファ
]#
import resource
import wNim/[wApp, wFrame, wPanel, wMemoryDC, wPaintDC, wImage,
wMenu, wMenuBar, wButton, wStatusBar, wBitmap, wBrush]
import std/strformat
# オブジェクトの設定
let
app = App(wSystemDpiAware)
frame = Frame(title="imageを表示", size=(600,400))
menuBar = MenuBar(frame)
statusBar = StatusBar(frame)
menu = Menu(menuBar, "ファイル")
panel = Panel(frame)
var
px, sx, mx: int = 0
py, sy, my: int = 0
mflg: bool = true
menu.append(wIdExit, "終了", "終了")
# ダブルバッファ制御(メモリデバイスコンテキストに画像を描画)
var memDc = MemoryDC()
var img: wImage = Image("easter-celebration-with-dreamy-bunny.jpg")
memDc.selectObject(Bitmap(img.size)) # 画像サイズに設定
memDc.clear()
memDc.setBackground(wWhiteBrush)
memDc.setBrush(wWhiteBrush)
memDc.drawImage(img, 0, 0) # 画像の表示
panel.refresh() # 再描画
# イベント処理
frame.wIdExit do (): # 終了制御
frame.close()
panel.wEvent_Paint do ():
# ダブルバッファ制御
var dc = PaintDC(panel)
dc.blit(source=memDc, xdest=mx, ydest=my, width=img.getSize().width, height=img.getSize().height)
dc.delete
panel.wEvent_MouseMove do (event: wEvent):
if event.leftDown : # 左クリックの確認
if mflg : # 左クリック開始時の座標を設定
sx = event.mMousePos.x
sy = event.mMousePos.y
mflg = false
else: # 移動量を設定
mx = event.mMousePos.x - sx + px
my = event.mMousePos.y - sy + py
statusBar.setStatusText(fmt"""マウス座標 {event.mMousePos.x}:{event.mMousePos.y}""")
panel.refresh() # 再描画
else:
mflg = true
if px != mx and py != my: # 離した場合の座標を設定
px = mx
py = my
panel.center()
frame.center()
frame.show()
app.mainLoop()
$ nim c -r -d:release -d:strip --opt:size --app:gui sample12.nim
- 出力結果
3. グラフ表示処理
下記プログラムは、グラフ描画用のプログラムになります。グラフ描画には多次元配列で利用したarraymancerライブラリ
を使って、一括でデータを書き換えます。(多次元配列は以前記事にしましたので、「Nimで多次元配列を操作」を参考にしてください)
そのため、仮想環境に、下記のようにarraymancerを入れます。(nimble install arraymancerでもいいです。)
$ atlas use arrarymancer
メモリDCに書き込む部分を、drawGraph2Memory関数にして外出しにしています。
また、グラフは力技で作成してるので、余り参考にはならないですが、多次元配列を使った所は、参考程度にはなると思います。
x軸に0から10までの数字を0.1刻みで配列し、それをarraymancerを使って一括でsin(x),cos(x)にy軸の値を求めます。
その後、panelサイズに合わせて、グラフを描画します。drawLines関数を使って一括で描画すべきか考えましたが、面倒なのでsin、cos毎に100回の線を引いてます。
#[
graph ダブルバッファ
]#
import resource
import wNim/[wApp, wFrame, wPanel, wMemoryDC, wPaintDC,
wMenu, wMenuBar, wButton, wStatusBar, wPen, wBitmap, wBrush]
import arraymancer
# メモリにグラフを描画
proc drawGraph2Memory(): wMemoryDC =
let
x = arange(0.0'f64, 10, 0.1)
sin_y = sin(x)
cos_y = cos(x)
var
graph_x = arange(0, 400, 4) # 400pixel
graph_y = ((sin_y +. 1.0) *. 100) # sinのy軸範囲は-1.0<=sin(x)<=1.0 なので1.0加算し、100倍に設定
result = MemoryDC()
result.selectObject(Bitmap(size=(400,200)))
result.clear()
result.setBackground(wWhiteBrush)
result.setBrush(wWhiteBrush)
result.setPen(Pen(color=wRed, width=3))
graph_y = abs(graph_y -. 200) # 画像の高さに変更
for i in 0 ..< 100: # sin図を描画
if i > 0:
result.drawLine(int(graph_x[i-1]), int(graph_y[i-1]), int(graph_x[i]), int(graph_y[i]))
result.setPen(Pen(color=wBlue, width=3))
graph_y = ((cos_y +. 1.0) *. 100) # cosのy軸範囲は-1.0<=cos(x)<=1.0 なので1.0加算し、100倍に設定
graph_y = abs(graph_y -. 200) # 画像の高さに変更
for i in 0 ..< 100: # cos図を描画
if i > 0:
result.drawLine(int(graph_x[i-1]), int(graph_y[i-1]), int(graph_x[i]), int(graph_y[i]))
# オブジェクトの設定
let
app = App(wSystemDpiAware)
frame = Frame(title="graphを表示", size=(420,290))
menuBar = MenuBar(frame)
statusBar = StatusBar(frame)
menu = Menu(menuBar, "ファイル")
panel = Panel(frame)
menu.append(wIdExit, "終了", "終了")
# ダブルバッファ制御(メモリDCに貯める)
var memDc = drawGraph2Memory()
panel.refresh()
# イベント処理
frame.wIdExit do (): # 終了制御
frame.close()
panel.wEvent_Paint do ():
# ダブルバッファ制御
var dc = PaintDC(panel)
dc.blit(source=memDc, width=memDc.getSize().width, height=memDc.getSize().height)
dc.delete
panel.center()
frame.center()
frame.show()
app.mainLoop()
$ nim c -r -d:release -d:strip --opt:size --app:gui sample13.nim
- 出力結果
4. ListViewの表示
最後に、ListViewを扱った下記プログラムですが、wNim/examples内にListViewを使ったサンプルがなかったので作成してみました。ListViewの表を作成するには、ListCtrlを使います。(スタイルはwLcReportでなければ、複数のカラムが表示されませんので、注意が必要。)
また、AutoLayoutを利用して入力欄とListViewの表示項目を上下に分けて表示させています。
import resource
import wNim/[wApp, wFrame, wPanel, wStaticText, wTextCtrl, wStaticBox, wListCtrl, wFont, wComboBox, wSpinCtrl,
wMenu, wMenuBar, wButton, wStatusBar]
# オブジェクトの設定
let
app = App(wSystemDpiAware)
frame = Frame(title="autolayoutを表示", size=(600,400))
menuBar = MenuBar(frame)
statusBar = StatusBar(frame)
menu = Menu(menuBar, "ファイル")
panel = Panel(frame)
box = StaticBox(panel, label="記入欄")
input = TextCtrl(panel, value="山田 太郎", style=wBorderSunken)
combobox1 = ComboBox(panel, value="東京", choices=["東京", "名古屋", "大阪", "北海道", "福岡"], style=wCbReadOnly)
combobox2 = ComboBox(panel, value="大学生", choices=["ITエンジニア", "医師", "看護師", "大学生", "教師", "建築士", "公務員"], style=wCbReadOnly)
spinctrl = SpinCtrl(panel, value=30, style=wSpArrowKeys)
btn = Button(panel, label="押せ")
listview = ListCtrl(panel, style=wLcReport or wBorderSunken or wLcSingleSel)
input.font = Font(10, faceName="メイリオ", weight=wFontWeightNormal)
menu.append(wIdExit, "終了", "ヘルプ機能:ステータスバーに終了が表示")
listview.font = Font(10, faceName="メイリオ", weight=wFontWeightNormal)
listview.setExtendedStyle(0x1) # リスト内に区切り線を引く
listview.appendColumn("氏名", width=200, format=wListFormatLeft)
listview.appendColumn("地名", width=120, format=wListFormatLeft)
listview.appendColumn("職種", width=150, format=wListFormatLeft)
listview.appendColumn("年齢", width=100, format=wListFormatLeft)
listview.setAlternateRowColor(wLightBlue) # リスト行の間隔カラー表示
listview.appendItem(["徳川 家康", "福岡", "建築士", "62"])
listview.appendItem(["織田 信長", "大阪", "公務員", "38"])
listview.appendItem(["豊臣 秀吉", "東京", "医師", "45"])
listview.appendItem(["明智 光秀", "北海道", "看護師", "22"])
listview.appendItem(["石田 三成", "名古屋", "ITエンジニア", "25"])
# レイアウト処理
proc layout() = # auto layout languageによって表現されている
panel.autolayout """
H:|-[box,listview]-|
V:|-[box]-[listview]-|
outer: box
H:|-5-{stack1:[input]-[combobox1]-[combobox2]-[spinctrl]-[btn(50)]}-5-|
V:|-5-[stack1]-5-|
"""
# イベント処理
frame.wIdExit do (): # 終了制御
frame.close()
panel.wEvent_Size do (): # リサイズ制御
layout()
btn.wEvent_Button do (): # ボタンが押された時のイベント制御
let
str1 = input.getValue()
str2 = combobox1.getValue()
str3 = combobox2.getValue()
age: int = spinctrl.getValue()
listview.appendItem([str1, str2, str3, $age])
# 画面の表示
layout()
frame.center()
frame.show()
app.mainLoop()
$ nim c -r -d:release -d:strip --opt:size --app:gui sample14.nim
- 出力結果
おわりに
wNimライブラリは、コンポーネントの多さと60行程度で簡単にプログラムが書けてしまうところが、売りかなとは思います、実際、初心者でも理解しやすいライブラリだとは思います。但し、MediaShow関連は未対応ですし、Windows上だけしか動きません。
昨年からGUIアプリを色々試してみて思った事は、2番目に良いGUIライブラリだと思います。1番は、やはりPython+QMLが素晴らしかったなと思います。(笑)
nimqmlでNimでもQMLが使えるみたいなのですが、以前インストールしてみて上手く動かなかったので、諦めてしまいました。また、暇が出来たら再チャレンジしてみたいとは思っています。
最後まで記事を読んで頂き、ありがとうございました。
出来ましたら、ハートマークを押して貰えたら、また、記事を書く気になるかもしれません。(笑)
Discussion