🐥

NimでWindowsアプリを作ってみた 続編

2024/06/28に公開

前回の記事が好評(?)だったみたいで、wNimの続編を記載していこうと思いました。
まあ、単にwNimは機能が多すぎるため、一回で説明出来なかっただけですけどね。ですので、前回はレイアウト回りの話を主にして、今回はイベント制御回りの内容を記載します。
主にタイマーや描画の制御と言った所です。

環境

私のPC環境はWindowsで動作させています。

  • OS: Windows11
  • Nim 2.0.6
  • wNim 0.13.3
  • arrarymancer 0.7.32

wNimでGUIアプリを作ってみる

1. タイマーイベント処理

まずは、タイマーイベントについて説明します。
下記のプログラムでは、画面に関しては、AutoLayoutを利用せずに、テキストを画面の固定位置に設定し、
タイマーイベントが発生した時に、現在時間を描画させているプログラムです。
イベント発生は、startTimer関数で発生させる時間を設定しています。
描画位置が固定ですので、画面リサイズを行っても表示する位置は変わりません。

sample11.nim
#[
  時間表示アプリ
]#
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()
vscodeで実行
$ nim c -r -d:release -d:strip --opt:size --app:gui sample11.nim
  • 出力結果

2. 画像表示処理

次に、画像の表示についてですが、下記の画像プログラムは、画像を読み込み後に予めメモリに記載し、描画イベント(refresh関数が呼ばれた時)が発生した時に、PaintDCに書き込むと言ったプログラムです。
画像を掴んでマウス移動した状態の時に、描画イベントを呼び出し、メモリ上のデータを画面に書き込んでいるだけですけどね。
一応、ダブルバッファ制御で描画していますが、画像の移動時に若干のちらつきはありますが、シングルバッファよりはマシって程度ですかね。

sample12.nim
#[
  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()
vscodeで実行
$ 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回の線を引いてます。

sample13.nim
#[
  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()
vscodeで実行
$ nim c -r -d:release -d:strip --opt:size --app:gui sample13.nim
  • 出力結果

4. ListViewの表示

最後に、ListViewを扱った下記プログラムですが、wNim/examples内にListViewを使ったサンプルがなかったので作成してみました。ListViewの表を作成するには、ListCtrlを使います。(スタイルはwLcReportでなければ、複数のカラムが表示されませんので、注意が必要。)
また、AutoLayoutを利用して入力欄とListViewの表示項目を上下に分けて表示させています。

sample14.nim
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()
vscodeで実行
$ 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