NimでWindowsアプリを作ってみた
GUIアプリの記事を書こうと思ったら、Nim2.0.6が出てしまったので、先にNim2.0.6の話を記載します。
Nim2.0.6は、2.0.4のバグを200件修正したパッチリリースになります。その後、Nimは2.2に移行するらしいです。Nim2.2の詳細は分かりませんが、恐らくException回りの変更とVSCodeのデバッカーでしょうかね?(知らんけど・・・)
では、話を戻しますが、今回取り扱うライブラリは、wNim
と言うGUIライブラリです。
GUIアプリケーション用のライブラリは、いくつかありますが、Windowsに完全対応したツールが良いと思い使って見ました。
wNimは、winim
と言うWindowsAPIをバインドしたライブラリを多様しており、作者も同一人物である事を考えて、これを使ってみました。
ただwNimライブラリですが、マクロなどを多数使っており、ソースを追っかける事が全くできません。
昨年までは、GUIアプリとして、Nimformsと言うライブラリを
使った簡単なGUIアプリを、趣味で作成していましたが、Nim2.0が出た事で、使えなくなりました。
原因は、ポインタの持ち方がNim1.6とNim2.0で違ってきた為だと思います。
※Nimformsは、イベント処理が関数ポインタ(onClickなど)を使用しているので、ソースを追いかけやすかった。
環境
私のPC環境はWindowsで動作させています。
- OS: Windows11
- Nim 2.0.6
- wNim 0.13.3
wNimでGUIアプリを作ってみる
1. atlasで仮想環境にwNimを入れてみる
Nim2.0から、atlas
コマンドは使えるようになります。
nimble install wNim
でもいいのですが、それだと$HOME/.nimble内にwNimのライブラリが格納されるため、VSCodeでwNim内のexampleを動かすのが面倒なので、仮想環境構築ツールのatlas
を使って見ました。
atlasの使い方については、この方が詳細に記載されています。
$ atlas init
$ atlas use wNim
プロジェクト直下でatlas init
を実行すれば、同一フォルダ内に、atlas.workspaceファイルが作成されます。
また、atlas use wNim
でgithub上のソースをプロジェクトフォルダ内にコピーされます。
その時に、wNim内で利用されるmemlib npeg winimなどのライブラリもコピーされます。
以下に、その時の構成を記載。
sample + atlas.workspace ← init時に勝手に出来ます。workspaceの場所を示すファイルです。
+ packages + config.nims
+ 以下:いっぱいのファイル
+ memlib + memlib.nimble
+ 以下:いっぱいのファイル
+ npeg + npeg.nimble
+ 以下:いっぱいのファイル
+ winim + winim.nimble
+ 以下:いっぱいのファイル
+ wNim + wNim.nimble
+ 以下:いっぱいのファイル
+ sample.nimble ← 今回使わないから、どうでも良い
+ nim.cfg ← 利用するライブラリのパスが指定されている
2. 超簡単なGUIアプリを作成
実際にソースを書いて、実行してみます。wNimのgithubに記載されているソースをそのまま実行してます。
import wNim
# オブジェクトの設定
let app = App()
let frame = Frame(title="はじめてのwNim画面", size=(400, 300))
# 画面の中央に表示
frame.center()
frame.show()
app.mainLoop()
AppオブジェクトとFrameオブジェクトを呼び出し、画面表示するだけの簡単なプログラムです。
$ nim c -r -d:release -d:strip --opt:size --app:gui sample01.nim
-r はコンパイル後に、即時実行する事を意味し、
-d:release --opt:sizeは、コンパイル時の最適化指定で、
--app:guiは、コマンド実行と切り離して起動する事を意味します。
※-d:stripがわからなく、調べて見た結果、デバック情報を加えない指定みたいで、所謂最適化ですね。
- 出力結果
3. GUIアプリにメニューと、フレーム内に文字とボタンを入れてみます。
ソースはwNim/examples/frame.nimをコピーし、sample02.nimとしました。
import resource
import wNim/[wApp, wIcon, wFont, wCursor, wAcceleratorTable, wFrame, wPanel,
wMenu, wMenuBar, wButton, wStatusBar, wStaticText, wTextEntryDialog, wFontDialog]
# オブジェクトの設定
let app = App(wSystemDpiAware)
let frame = Frame(title="wNimのframe.nimを表示")
frame.dpiAutoScale:
frame.minSize = (300, 200)
frame.size = (600, 400)
let menuBar = MenuBar(frame)
let statusBar = StatusBar(frame)
let menu = Menu(menuBar, "&File")
menu.append(wIdExit, "終了", "ヘルプ機能:ステータスバーに終了が表示")
let accel = AcceleratorTable(frame)
accel.add(wAccelAlt, wKey_F4, wIdExit)
let panel = Panel(frame)
let staticText = StaticText(panel, label="フレームに日本語を表示")
staticText.font = Font(14, family=wFontFamilyDefault, weight=wFontWeightBold)
staticText.cursor = wHandCursor # Textエリアにマウスが移動するとハンドカーソルに変わる
staticText.fit()
let btn = Button(panel, label="フォント")
# レイアウト処理
proc layout() = # auto layout languageに準拠して表示処理
panel.autolayout """
HV:|-[staticText]->[btn]-|
"""
# イベント処理
staticText.wEvent_CommandLeftClick do ():
let textEntryDialog = TextEntryDialog(frame, value=staticText.label,
caption="Change The Text")
if textEntryDialog.showModal() == wIdOk:
staticText.label = textEntryDialog.value
staticText.fit()
staticText.refresh()
btn.wEvent_Button do ():
let fontDialog = FontDialog(frame, staticText.font)
fontDialog.color = staticText.foregroundColor
fontDialog.enableSymbols(false)
fontDialog.range = 0..24
if fontDialog.showModal() == wIdOk:
staticText.font = fontDialog.chosenFont
staticText.foregroundColor = fontDialog.color
staticText.fit()
staticText.refresh()
frame.wIdExit do ():
frame.close()
panel.wEvent_Size do ():
layout()
# 画面の表示
layout()
frame.center()
frame.show()
app.mainLoop()
行数にすると60行程のソースで、テキスト表示、ボタン表示、メニュー制御、テキストダイアログ制御、フォントダイアログ制御、キー制御までを行えます。
import resource
については、この後に説明します。上記ソースは、オブジェクトの生成とレイアウトの設定、イベント制御の3構成に別れます。
レイアウト制御は、layout()
で画面を制御しており、これはアップル社のauto layout(Visual Format Language)を真似た言語ですが、完全に一致する物ではないですが、Visual Format Languageを若干拡張したような物です。
Visual Format Languageの詳細な説明は、Visual Format Languageを使う【Swift3.0】が参考になると思います。
また、autolayout.jsで操作結果の表示が可能です。
イベント制御では、オブジェクト名.イベントID
をdo
記法(無名プロシージャ)みたいな物で、全て制御し、そのイベントが発生した時に呼ばれる流れになります。
※これって wNimのソースを確認したら、do
記法なのかな?、よくわかりません。do
記法(無名プロシージャ)でした。typeでproc()を別名にしていたので、見つけるのが困難ですね。
{.used.}
when defined(vcc):
{.link: "wNim/examples/resource/wNimVcc.res".}
elif defined(cpu64):
when defined(tcc):
{.link: "wNim/examples/resource/wNimTcc64.res".}
else:
{.link: "wNim/examples/resource/wNim64.res".}
else:
when defined(tcc):
{.link: "wNim/examples/resource/wNimTcc32.res".}
else:
{.link: "wNim/examples/resource/wNim32.res".}
resource.nimファイル内のresファイル、windows APIで利用されるuser32.dllなどが一つにまとまった物と思われます。また、それぞれのresファイルはコンパイラの指定によって呼び出されるresファイルが異なる事を意味しています。
import wNim
と記述すれば、リソースファイルも自動でロードされるのですが、必要ないソースまでコンパイルされるため、コンパイル時間もアプリケーションサイズも増えてきます。なので、リソースファイルを作成して必要なコンポーネントのみをimportする方がよいでしょう。
$ nim c -r -d:release -d:strip --opt:size --app:gui .\sample02.nim
- 出力結果
3.1. Visual Format Language
Visual Format Languageについては、実際にソースを起動してみて試した方がわかりやすいと思います。
オブジェクト名.autolayout
が、記述されている部分が、Visual Format Languageになります。この他にも、layout DSL記述でも表示出来ますが、Visual Format Languageを理解出来れば、こちらの方が記述がわかりやすいかもしれません。
H:
水平方向、V:
縦方向、[オブジェクト名]
で宣言したオブジェクトが表示、(長さ)
でpixelまたは%表示で長さを表示、[オブジェクト名,オブジェクト名]
で同じ長さ、~
は均等を示します、outer:オブジェクト名
ボックス内のオブジェクトを下に記載できる {stack1:[オブジェクト]}
良くわからないがouterで分割した後に使うらしい。
簡単にボタンをレイアウトの配置としてWindowsアプリを作ってみました。
import resource
import wNim/[wApp, wFrame, wPanel,
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)
btn1 = Button(panel, label="ボタン1")
btn2 = Button(panel, label="ボタン2")
menu.append(wIdExit, "終了", "ヘルプ機能:ステータスバーに終了が表示")
# レイアウト処理
proc layout() = # auto layout languageによって表現されている
panel.autolayout """
H:|-[btn1(20%)]-10-[btn2]-|
V:|-[btn1,btn2]-|
"""
#[
1行目水平方向にbtn1とbtn2の間に10pixel離して配置、btn1は水平方向に20%の幅
2行目縦方向にbtn1とbtn2を同じ高さで配置
]#
# イベント処理
frame.wIdExit do (): # 終了制御
frame.close()
panel.wEvent_Size do (): # リサイズ制御
layout()
# 画面の表示
layout()
frame.center()
frame.show()
app.mainLoop()
- 出力結果
次に、3つのボタンで横と高さを表示したWindowsアプリになります。
import resource
import wNim/[wApp, wFrame, wPanel,
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)
btn1 = Button(panel, label="ボタン1")
btn2 = Button(panel, label="ボタン2")
btn3 = Button(panel, label="ボタン3")
menu.append(wIdExit, "終了", "ヘルプ機能:ステータスバーに終了が表示")
# レイアウト処理
proc layout() = # auto layout languageによって表現されている
panel.autolayout """
H:|-[btn1(20%)]-[btn2,btn3]-|
V:|-[btn1,btn2]-|
V:|-[btn2(80%)]-[btn3]-|
"""
# イベント処理
frame.wIdExit do (): # 終了制御
frame.close()
panel.wEvent_Size do (): # リサイズ制御
layout()
# 画面の表示
layout()
frame.center()
frame.show()
app.mainLoop()
- 出力結果
最後に、トップに入力欄を集めて表示させてWindowsアプリになります。
outer:
で分割して横と縦を表示させています。
import resource
import wNim/[wApp, wFrame, wPanel, wStaticText, wTextCtrl, wStaticBox, wListBox,
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)
box1 = StaticBox(panel, label="入力欄")
text1 = StaticText(panel, label="固定文字表示")
textc1 = TextCtrl(panel, value="入力してね!", style=wBorderSunken)
btn1 = Button(panel, label="押せ")
list1 = ListBox(panel, style=wLbNoSel or wBorderSimple or wLbNeededScroll)
menu.append(wIdExit, "終了", "ヘルプ機能:ステータスバーに終了が表示")
# レイアウト処理
proc layout() = # auto layout languageによって表現されている
panel.autolayout """
H:|-[box1,list1]-|
V:|-[box1]-[list1]-|
outer: box1
H:|-5-{stack1:[text1]-[textc1]-[btn1(50)]}-5-|
V:|-5-[stack1]-5-|
"""
# イベント処理
frame.wIdExit do (): # 終了制御
frame.close()
panel.wEvent_Size do (): # リサイズ制御
layout()
btn1.wEvent_Button do (): # ボタンが押された時のイベント制御
let str = textc1.getValue()
list1.append(str)
# 画面の表示
layout()
frame.center()
frame.show()
app.mainLoop()
- 出力結果
3.2. イベント制御
イベント制御は、オブジェクト名.イベントID do()
で制御できます。
イベントID | 内容 |
---|---|
wEvent_Button | ボタン押下時 |
wIdExit | 画面終了時 |
wEvent_Size | 画面のリサイズ時 |
wEvent_Timer | タイマーイベント |
wEvent_MouseEnter | マウス押下時 |
wEvent_MouseHover | マウス移動時 |
wEvent_MouseLeave | マウス押下後 |
※その他、多数のイベント制御が可能
4. wNimのexampleを実行してみる
$ cd wNim/examples
$ nimble tasks ← タスクの一覧が表示され、その中にexampleも含まれます。
$ nimble example ← 実行するとwNim/exampleの全てのサンプルがコンパイルされます。
※nimble exampleを実行すると、結局は$HOME/.nimbleにwNimライブラリが入ってしまいますけど・・・
Nim2.0.6でコンパイルをすると、example/reversi.nimでコンパイルエラーになります。(Nim2.0.4ではコンパイルは通りますが・・・)
理由は、thread用のソース(example/mcts/mcts.nim)内をimportしていないのが原因です。
多分、別のサンプルでthread用のソースを共有したいがために、メイン側でimportを記載してthread用のソースには記載していないのが原因です。
Nim2.0.6はより厳格なチェックをしているため、曖昧な部分はエラーにしてるんでしょうね。
example/reversi.nimをコンパイルするのであれば、example/mcts/mcts.nimにimport engine_reversi
を記入すればOKです。
但し、その後のtictactoe.nimでも同様のエラーが出ると思いますので、その時は、import engine_tictactoe
と記述を変更しなくてはいけません。
まあ、exampleフォルダ内のサンプルは、どう動くかのサンプルなので、ソースを見て確認する物だと思って貰えればいいかと思います。
おわりに
wNimは、コンパイルこそ少し時間がかかりますが、動作自体は速く、何より行数が少ない事もあり、プログラムしやすいGUIライブラリだと思います。
ただし、前述した通り、wNim自体複雑過ぎるので、wNim自体でバグが出たりした場合には、ソースを追っかけるのが至難ではあります。
(個人的にはNimformsの方が、わかりやすくて好きですがね)
ただ、昨年のnimフォーラムでnim作者がNim純正のWindowシステムを探していたので、近いうちにPythonのようなTkinterみたいなのが出てくる可能性は高いです。
(次のNim2.2でGUIライブラリが標準で付いたりする・・・かも)
Discussion